Files
syndarix/frontend/tests/components/projects/ProjectDashboard.test.tsx
Felipe Cardoso a4c91cb8c3 refactor(frontend): clean up code by consolidating multi-line JSX into single lines where feasible
- Refactored JSX elements to improve readability by collapsing multi-line props and attributes into single lines if their length permits.
- Improved consistency in component imports by grouping and consolidating them.
- No functional changes, purely restructuring for clarity and maintainability.
2026-01-01 11:46:57 +01:00

463 lines
16 KiB
TypeScript

/**
* ProjectDashboard Component Tests
*
* Comprehensive tests for the main project dashboard component.
*/
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ProjectDashboard } from '@/components/projects/ProjectDashboard';
import { EventType, type ProjectEvent, type ConnectionState } from '@/lib/types/events';
// Mock child components to isolate ProjectDashboard testing
jest.mock('@/components/projects/ProjectHeader', () => ({
ProjectHeader: jest.fn(({ project, onSettings }) => (
<div data-testid="mock-project-header">
<span>{project.name}</span>
<button onClick={onSettings}>Settings</button>
</div>
)),
}));
jest.mock('@/components/projects/AgentPanel', () => ({
AgentPanel: jest.fn(({ agents, onManageAgents, onAgentAction }) => (
<div data-testid="mock-agent-panel">
<span>Agents: {agents.length}</span>
<button onClick={onManageAgents}>Manage Agents</button>
<button onClick={() => onAgentAction('agent-001', 'view')}>View Agent</button>
</div>
)),
}));
jest.mock('@/components/projects/SprintProgress', () => ({
SprintProgress: jest.fn(({ sprint }) => (
<div data-testid="mock-sprint-progress">
<span>{sprint.name}</span>
</div>
)),
}));
jest.mock('@/components/projects/IssueSummary', () => ({
IssueSummary: jest.fn(({ summary, onViewAllIssues }) => (
<div data-testid="mock-issue-summary">
<span>Total: {summary.total}</span>
<button onClick={onViewAllIssues}>View Issues</button>
</div>
)),
}));
jest.mock('@/components/projects/RecentActivity', () => ({
RecentActivity: jest.fn(({ activities, onViewAll, onActionClick }) => (
<div data-testid="mock-recent-activity">
<span>Activities: {activities.length}</span>
<button onClick={onViewAll}>View All</button>
<button onClick={() => onActionClick('act-001')}>Action Click</button>
</div>
)),
}));
jest.mock('@/components/events/ConnectionStatus', () => ({
ConnectionStatus: jest.fn(({ state, onReconnect }) => (
<div data-testid="mock-connection-status">
<span>State: {state}</span>
<button onClick={onReconnect}>Reconnect</button>
</div>
)),
}));
// Mock useProjectEvents hook
const mockReconnect = jest.fn();
const mockDisconnect = jest.fn();
const mockClearEvents = jest.fn();
const mockUseProjectEventsDefault = {
events: [] as ProjectEvent[],
isConnected: true,
connectionState: 'connected' as ConnectionState,
error: null as {
message: string;
timestamp: string;
code?: string;
retryAttempt?: number;
} | null,
retryCount: 0,
reconnect: mockReconnect,
disconnect: mockDisconnect,
clearEvents: mockClearEvents,
};
let mockUseProjectEventsResult = { ...mockUseProjectEventsDefault };
jest.mock('@/lib/hooks/useProjectEvents', () => ({
useProjectEvents: jest.fn(() => mockUseProjectEventsResult),
}));
// Mock next/navigation
const mockPush = jest.fn();
jest.mock('next/navigation', () => ({
useRouter: () => ({
push: mockPush,
back: jest.fn(),
forward: jest.fn(),
refresh: jest.fn(),
replace: jest.fn(),
prefetch: jest.fn(),
}),
}));
describe('ProjectDashboard', () => {
const projectId = 'test-project-123';
beforeEach(() => {
jest.clearAllMocks();
mockUseProjectEventsResult = { ...mockUseProjectEventsDefault };
});
describe('Rendering', () => {
it('renders the dashboard with test id', () => {
render(<ProjectDashboard projectId={projectId} />);
expect(screen.getByTestId('project-dashboard')).toBeInTheDocument();
});
it('renders ProjectHeader component', () => {
render(<ProjectDashboard projectId={projectId} />);
expect(screen.getByTestId('mock-project-header')).toBeInTheDocument();
expect(screen.getByText('E-Commerce Platform Redesign')).toBeInTheDocument();
});
it('renders AgentPanel component', () => {
render(<ProjectDashboard projectId={projectId} />);
expect(screen.getByTestId('mock-agent-panel')).toBeInTheDocument();
expect(screen.getByText('Agents: 5')).toBeInTheDocument();
});
it('renders SprintProgress component', () => {
render(<ProjectDashboard projectId={projectId} />);
expect(screen.getByTestId('mock-sprint-progress')).toBeInTheDocument();
expect(screen.getByText('Sprint 3')).toBeInTheDocument();
});
it('renders IssueSummary component', () => {
render(<ProjectDashboard projectId={projectId} />);
expect(screen.getByTestId('mock-issue-summary')).toBeInTheDocument();
expect(screen.getByText('Total: 70')).toBeInTheDocument();
});
it('renders RecentActivity component', () => {
render(<ProjectDashboard projectId={projectId} />);
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
});
it('applies custom className', () => {
render(<ProjectDashboard projectId={projectId} className="custom-class" />);
expect(screen.getByTestId('project-dashboard')).toHaveClass('custom-class');
});
});
describe('Connection Status', () => {
it('does not show ConnectionStatus when connected', () => {
mockUseProjectEventsResult.connectionState = 'connected';
render(<ProjectDashboard projectId={projectId} />);
expect(screen.queryByTestId('mock-connection-status')).not.toBeInTheDocument();
});
it('shows ConnectionStatus when disconnected', () => {
mockUseProjectEventsResult.connectionState = 'disconnected';
render(<ProjectDashboard projectId={projectId} />);
expect(screen.getByTestId('mock-connection-status')).toBeInTheDocument();
expect(screen.getByText('State: disconnected')).toBeInTheDocument();
});
it('shows ConnectionStatus when connecting', () => {
mockUseProjectEventsResult.connectionState = 'connecting';
render(<ProjectDashboard projectId={projectId} />);
expect(screen.getByTestId('mock-connection-status')).toBeInTheDocument();
});
it('shows ConnectionStatus when error', () => {
mockUseProjectEventsResult.connectionState = 'error';
mockUseProjectEventsResult.error = {
message: 'Connection failed',
timestamp: new Date().toISOString(),
};
render(<ProjectDashboard projectId={projectId} />);
expect(screen.getByTestId('mock-connection-status')).toBeInTheDocument();
});
});
describe('Navigation callbacks', () => {
it('navigates to settings when Settings is clicked', async () => {
const user = userEvent.setup();
render(<ProjectDashboard projectId={projectId} />);
await user.click(screen.getByText('Settings'));
expect(mockPush).toHaveBeenCalledWith(`/projects/${projectId}/settings`);
});
it('navigates to agents page when Manage Agents is clicked', async () => {
const user = userEvent.setup();
render(<ProjectDashboard projectId={projectId} />);
await user.click(screen.getByText('Manage Agents'));
expect(mockPush).toHaveBeenCalledWith(`/projects/${projectId}/agents`);
});
it('navigates to issues page when View Issues is clicked', async () => {
const user = userEvent.setup();
render(<ProjectDashboard projectId={projectId} />);
await user.click(screen.getByText('View Issues'));
expect(mockPush).toHaveBeenCalledWith(`/projects/${projectId}/issues`);
});
it('navigates to activity page when View All activity is clicked', async () => {
const user = userEvent.setup();
render(<ProjectDashboard projectId={projectId} />);
await user.click(screen.getByText('View All'));
expect(mockPush).toHaveBeenCalledWith(`/projects/${projectId}/activity`);
});
});
describe('Action callbacks', () => {
it('handles agent action', async () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
const user = userEvent.setup();
render(<ProjectDashboard projectId={projectId} />);
await user.click(screen.getByText('View Agent'));
expect(consoleSpy).toHaveBeenCalledWith('Agent action: view on agent-001');
consoleSpy.mockRestore();
});
it('handles activity action click', async () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
const user = userEvent.setup();
render(<ProjectDashboard projectId={projectId} />);
await user.click(screen.getByText('Action Click'));
expect(consoleSpy).toHaveBeenCalledWith('Action clicked for activity: act-001');
consoleSpy.mockRestore();
});
});
describe('SSE Events Integration', () => {
it('merges SSE events with mock activities', () => {
const sseEvent: ProjectEvent = {
id: 'sse-event-1',
type: EventType.AGENT_MESSAGE,
timestamp: new Date().toISOString(),
project_id: projectId,
actor_id: 'agent-001',
actor_type: 'agent',
payload: {
message: 'Test message',
agent_name: 'Test Agent',
},
};
mockUseProjectEventsResult.events = [sseEvent];
render(<ProjectDashboard projectId={projectId} />);
// RecentActivity should receive merged activities
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
});
});
});
// Test eventToActivity helper function behavior through component integration
describe('Event to Activity Conversion', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseProjectEventsResult = { ...mockUseProjectEventsDefault };
});
const createMockEvent = (
type: EventType,
payload: Record<string, unknown>,
actorType: 'agent' | 'system' | 'user' = 'agent'
): ProjectEvent => ({
id: `event-${Date.now()}`,
type,
timestamp: new Date().toISOString(),
project_id: 'test-project',
actor_id: actorType === 'system' ? null : 'actor-001',
actor_type: actorType,
payload,
});
it('handles AGENT_SPAWNED events', () => {
const event = createMockEvent(EventType.AGENT_SPAWNED, {
agent_name: 'Product Owner',
role: 'product_owner',
});
mockUseProjectEventsResult.events = [event];
render(<ProjectDashboard projectId="test" />);
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
});
it('handles AGENT_MESSAGE events', () => {
const event = createMockEvent(EventType.AGENT_MESSAGE, {
message: 'Task completed',
agent_name: 'Backend Engineer',
});
mockUseProjectEventsResult.events = [event];
render(<ProjectDashboard projectId="test" />);
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
});
it('handles AGENT_STATUS_CHANGED events', () => {
const event = createMockEvent(EventType.AGENT_STATUS_CHANGED, {
new_status: 'working',
agent_name: 'QA Engineer',
});
mockUseProjectEventsResult.events = [event];
render(<ProjectDashboard projectId="test" />);
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
});
it('handles ISSUE_CREATED events', () => {
const event = createMockEvent(EventType.ISSUE_CREATED, {
title: 'New Feature Request',
issue_id: 'issue-001',
});
mockUseProjectEventsResult.events = [event];
render(<ProjectDashboard projectId="test" />);
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
});
it('handles ISSUE_UPDATED events', () => {
const event = createMockEvent(EventType.ISSUE_UPDATED, {
issue_id: 'issue-001',
});
mockUseProjectEventsResult.events = [event];
render(<ProjectDashboard projectId="test" />);
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
});
it('handles APPROVAL_REQUESTED events', () => {
const event = createMockEvent(EventType.APPROVAL_REQUESTED, {
description: 'Please approve the design',
approval_id: 'approval-001',
});
mockUseProjectEventsResult.events = [event];
render(<ProjectDashboard projectId="test" />);
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
});
it('handles SPRINT_STARTED events', () => {
const event = createMockEvent(
EventType.SPRINT_STARTED,
{
sprint_name: 'Sprint 4',
},
'system'
);
mockUseProjectEventsResult.events = [event];
render(<ProjectDashboard projectId="test" />);
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
});
it('handles unknown event types gracefully', () => {
// Create an event with an unknown type prefix
const event: ProjectEvent = {
id: 'unknown-event',
type: 'custom.event' as EventType,
timestamp: new Date().toISOString(),
project_id: 'test-project',
actor_id: null,
actor_type: 'system',
payload: {},
};
mockUseProjectEventsResult.events = [event];
render(<ProjectDashboard projectId="test" />);
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
});
it('handles agent events with agent_name in payload', () => {
const event = createMockEvent(EventType.AGENT_MESSAGE, {
message: 'Hello',
agent_name: 'Test Agent',
});
mockUseProjectEventsResult.events = [event];
render(<ProjectDashboard projectId="test" />);
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
});
it('defaults agent name when not in payload', () => {
const event = createMockEvent(EventType.AGENT_MESSAGE, {
message: 'Hello',
});
mockUseProjectEventsResult.events = [event];
render(<ProjectDashboard projectId="test" />);
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
});
it('handles system actor type', () => {
const event = createMockEvent(EventType.SPRINT_STARTED, { sprint_name: 'Sprint 5' }, 'system');
mockUseProjectEventsResult.events = [event];
render(<ProjectDashboard projectId="test" />);
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
});
it('handles user actor type', () => {
const event = createMockEvent(EventType.APPROVAL_REQUESTED, { description: 'Approve' }, 'user');
mockUseProjectEventsResult.events = [event];
render(<ProjectDashboard projectId="test" />);
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
});
});
// Test sorting and limiting of activities
describe('Activity Management', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseProjectEventsResult = { ...mockUseProjectEventsDefault };
});
it('limits activities to 10 items', () => {
// Create more than 10 SSE events
const manyEvents: ProjectEvent[] = Array.from({ length: 15 }, (_, i) => ({
id: `event-${i}`,
type: EventType.AGENT_MESSAGE,
timestamp: new Date(Date.now() - i * 60000).toISOString(),
project_id: 'test-project',
actor_id: 'agent-001',
actor_type: 'agent' as const,
payload: { message: `Message ${i}` },
}));
mockUseProjectEventsResult.events = manyEvents;
render(<ProjectDashboard projectId="test" />);
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
// The mock shows activities count - with SSE + mock = max 10
expect(screen.getByText(/Activities:/)).toBeInTheDocument();
});
it('sorts activities by timestamp (newest first)', () => {
const oldEvent: ProjectEvent = {
id: 'old-event',
type: EventType.AGENT_MESSAGE,
timestamp: new Date(Date.now() - 100000).toISOString(),
project_id: 'test-project',
actor_id: 'agent-001',
actor_type: 'agent',
payload: { message: 'Old message' },
};
const newEvent: ProjectEvent = {
id: 'new-event',
type: EventType.AGENT_MESSAGE,
timestamp: new Date().toISOString(),
project_id: 'test-project',
actor_id: 'agent-001',
actor_type: 'agent',
payload: { message: 'New message' },
};
mockUseProjectEventsResult.events = [oldEvent, newEvent];
render(<ProjectDashboard projectId="test" />);
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
});
});