/** * 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 }) => (
{project.name}
)), })); jest.mock('@/components/projects/AgentPanel', () => ({ AgentPanel: jest.fn(({ agents, onManageAgents, onAgentAction }) => (
Agents: {agents.length}
)), })); jest.mock('@/components/projects/SprintProgress', () => ({ SprintProgress: jest.fn(({ sprint }) => (
{sprint.name}
)), })); jest.mock('@/components/projects/IssueSummary', () => ({ IssueSummary: jest.fn(({ summary, onViewAllIssues }) => (
Total: {summary.total}
)), })); jest.mock('@/components/projects/RecentActivity', () => ({ RecentActivity: jest.fn(({ activities, onViewAll, onActionClick }) => (
Activities: {activities.length}
)), })); jest.mock('@/components/events/ConnectionStatus', () => ({ ConnectionStatus: jest.fn(({ state, onReconnect }) => (
State: {state}
)), })); // 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(); expect(screen.getByTestId('project-dashboard')).toBeInTheDocument(); }); it('renders ProjectHeader component', () => { render(); expect(screen.getByTestId('mock-project-header')).toBeInTheDocument(); expect(screen.getByText('E-Commerce Platform Redesign')).toBeInTheDocument(); }); it('renders AgentPanel component', () => { render(); expect(screen.getByTestId('mock-agent-panel')).toBeInTheDocument(); expect(screen.getByText('Agents: 5')).toBeInTheDocument(); }); it('renders SprintProgress component', () => { render(); expect(screen.getByTestId('mock-sprint-progress')).toBeInTheDocument(); expect(screen.getByText('Sprint 3')).toBeInTheDocument(); }); it('renders IssueSummary component', () => { render(); expect(screen.getByTestId('mock-issue-summary')).toBeInTheDocument(); expect(screen.getByText('Total: 70')).toBeInTheDocument(); }); it('renders RecentActivity component', () => { render(); expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument(); }); it('applies custom className', () => { render(); expect(screen.getByTestId('project-dashboard')).toHaveClass('custom-class'); }); }); describe('Connection Status', () => { it('does not show ConnectionStatus when connected', () => { mockUseProjectEventsResult.connectionState = 'connected'; render(); expect(screen.queryByTestId('mock-connection-status')).not.toBeInTheDocument(); }); it('shows ConnectionStatus when disconnected', () => { mockUseProjectEventsResult.connectionState = 'disconnected'; render(); expect(screen.getByTestId('mock-connection-status')).toBeInTheDocument(); expect(screen.getByText('State: disconnected')).toBeInTheDocument(); }); it('shows ConnectionStatus when connecting', () => { mockUseProjectEventsResult.connectionState = 'connecting'; render(); 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(); expect(screen.getByTestId('mock-connection-status')).toBeInTheDocument(); }); }); describe('Navigation callbacks', () => { it('navigates to settings when Settings is clicked', async () => { const user = userEvent.setup(); render(); 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(); 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(); 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(); 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(); 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(); 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(); // 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, 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument(); }); });