/** * Tests for EventList Component */ import { render, screen, fireEvent } from '@testing-library/react'; import { EventList } from '@/components/events/EventList'; import { EventType, type ProjectEvent } from '@/lib/types/events'; /** * Helper to create mock event */ function createMockEvent(overrides: Partial = {}): ProjectEvent { return { id: `event-${Math.random().toString(36).substr(2, 9)}`, type: EventType.AGENT_MESSAGE, timestamp: new Date().toISOString(), project_id: 'project-123', actor_id: 'agent-456', actor_type: 'agent', payload: { message: 'Test message' }, ...overrides, }; } describe('EventList', () => { const mockOnEventClick = jest.fn(); beforeEach(() => { jest.clearAllMocks(); }); describe('empty state', () => { it('shows empty message when no events', () => { render(); expect(screen.getByText('No events yet')).toBeInTheDocument(); }); it('shows custom empty message', () => { render(); expect(screen.getByText('Waiting for activity...')).toBeInTheDocument(); }); }); describe('header', () => { it('shows header by default', () => { render(); expect(screen.getByText('Activity Feed')).toBeInTheDocument(); }); it('shows custom title', () => { render(); expect(screen.getByText('Project Events')).toBeInTheDocument(); }); it('hides header when showHeader is false', () => { render(); expect(screen.queryByText('Activity Feed')).not.toBeInTheDocument(); }); it('shows event count in header', () => { const events = [ createMockEvent({ id: 'event-1' }), createMockEvent({ id: 'event-2' }), createMockEvent({ id: 'event-3' }), ]; render(); expect(screen.getByText('3 events')).toBeInTheDocument(); }); it('shows singular "event" for one event', () => { const events = [createMockEvent()]; render(); expect(screen.getByText('1 event')).toBeInTheDocument(); }); }); describe('event display', () => { it('displays agent events correctly', () => { const events = [ createMockEvent({ type: EventType.AGENT_MESSAGE, payload: { message: 'Processing task...' }, }), ]; render(); expect(screen.getByText('Agent Message')).toBeInTheDocument(); expect(screen.getByText('Processing task...')).toBeInTheDocument(); }); it('displays issue events correctly', () => { const events = [ createMockEvent({ type: EventType.ISSUE_CREATED, payload: { title: 'Fix login bug' }, }), ]; render(); expect(screen.getByText('Issue Created')).toBeInTheDocument(); expect(screen.getByText('Fix login bug')).toBeInTheDocument(); }); it('displays sprint events correctly', () => { const events = [ createMockEvent({ type: EventType.SPRINT_STARTED, payload: { sprint_name: 'Sprint 1' }, }), ]; render(); expect(screen.getByText('Sprint Started')).toBeInTheDocument(); expect(screen.getByText(/Sprint "Sprint 1" started/)).toBeInTheDocument(); }); it('displays approval events correctly', () => { const events = [ createMockEvent({ type: EventType.APPROVAL_REQUESTED, payload: { description: 'Need approval for deployment' }, }), ]; render(); expect(screen.getByText('Approval Requested')).toBeInTheDocument(); expect(screen.getByText('Need approval for deployment')).toBeInTheDocument(); }); it('displays workflow events correctly', () => { const events = [ createMockEvent({ type: EventType.WORKFLOW_COMPLETED, payload: { duration_seconds: 120 }, }), ]; render(); expect(screen.getByText('Workflow Completed')).toBeInTheDocument(); expect(screen.getByText('Completed in 120s')).toBeInTheDocument(); }); it('displays actor type', () => { const events = [ createMockEvent({ actor_type: 'agent' }), createMockEvent({ actor_type: 'user', id: 'event-2' }), createMockEvent({ actor_type: 'system', id: 'event-3' }), ]; render(); expect(screen.getByText('Agent')).toBeInTheDocument(); expect(screen.getByText('User')).toBeInTheDocument(); expect(screen.getByText('System')).toBeInTheDocument(); }); }); describe('event sorting', () => { it('sorts events by timestamp, newest first', () => { const events = [ createMockEvent({ id: 'older', timestamp: '2024-01-01T10:00:00Z', payload: { message: 'Older event' }, }), createMockEvent({ id: 'newer', timestamp: '2024-01-01T12:00:00Z', payload: { message: 'Newer event' }, }), ]; render(); const eventTexts = screen.getAllByText(/event$/i); // The "Newer event" should appear first in the DOM const newerIndex = eventTexts.findIndex((el) => el.closest('[class*="flex gap-3"]')?.textContent?.includes('Newer') ); const olderIndex = eventTexts.findIndex((el) => el.closest('[class*="flex gap-3"]')?.textContent?.includes('Older') ); // In a sorted list, newer should have lower index expect(newerIndex).toBeLessThan(olderIndex); }); }); describe('payload expansion', () => { it('shows expand button when showPayloads is true', () => { const events = [createMockEvent()]; render(); // Should have a chevron button expect(screen.getByRole('button')).toBeInTheDocument(); }); it('expands payload on click', async () => { const events = [ createMockEvent({ payload: { custom_field: 'custom_value' }, }), ]; render(); const eventItem = screen.getByText('Agent Message').closest('[class*="flex gap-3"]'); expect(eventItem).toBeInTheDocument(); fireEvent.click(eventItem!); // Should show the JSON payload expect(screen.getByText(/"custom_field"/)).toBeInTheDocument(); expect(screen.getByText(/"custom_value"/)).toBeInTheDocument(); }); }); describe('event click', () => { it('calls onEventClick when event is clicked', () => { const events = [createMockEvent({ id: 'test-event' })]; render(); const eventItem = screen.getByText('Agent Message').closest('[class*="flex gap-3"]'); fireEvent.click(eventItem!); expect(mockOnEventClick).toHaveBeenCalledWith(expect.objectContaining({ id: 'test-event' })); }); it('makes event item focusable when clickable', () => { const events = [createMockEvent()]; render(); const eventItem = screen.getByText('Agent Message').closest('[class*="flex gap-3"]'); expect(eventItem).toHaveAttribute('tabIndex', '0'); }); it('handles keyboard activation', () => { const events = [createMockEvent({ id: 'keyboard-event' })]; render(); const eventItem = screen.getByText('Agent Message').closest('[class*="flex gap-3"]'); fireEvent.keyDown(eventItem!, { key: 'Enter' }); expect(mockOnEventClick).toHaveBeenCalledWith( expect.objectContaining({ id: 'keyboard-event' }) ); }); it('handles space key activation', () => { const events = [createMockEvent({ id: 'space-event' })]; render(); const eventItem = screen.getByText('Agent Message').closest('[class*="flex gap-3"]'); fireEvent.keyDown(eventItem!, { key: ' ' }); expect(mockOnEventClick).toHaveBeenCalledWith(expect.objectContaining({ id: 'space-event' })); }); }); describe('scrolling', () => { it('applies maxHeight style', () => { const events = [createMockEvent()]; const { container } = render(); const scrollContainer = container.querySelector('.overflow-y-auto'); expect(scrollContainer).toHaveStyle({ maxHeight: '300px' }); }); it('accepts string maxHeight', () => { const events = [createMockEvent()]; const { container } = render(); const scrollContainer = container.querySelector('.overflow-y-auto'); expect(scrollContainer).toHaveStyle({ maxHeight: '50vh' }); }); }); describe('className prop', () => { it('applies custom className', () => { const { container } = render(); expect(container.querySelector('.custom-event-list')).toBeInTheDocument(); }); }); describe('different event types', () => { it('handles agent spawned event', () => { const events = [ createMockEvent({ type: EventType.AGENT_SPAWNED, payload: { agent_name: 'Product Owner', role: 'po' }, }), ]; render(); expect(screen.getByText('Agent Spawned')).toBeInTheDocument(); expect(screen.getByText(/Product Owner spawned as po/)).toBeInTheDocument(); }); it('handles agent terminated event', () => { const events = [ createMockEvent({ type: EventType.AGENT_TERMINATED, payload: { termination_reason: 'Task completed' }, }), ]; render(); expect(screen.getByText('Agent Terminated')).toBeInTheDocument(); expect(screen.getByText('Task completed')).toBeInTheDocument(); }); it('handles workflow failed event', () => { const events = [ createMockEvent({ type: EventType.WORKFLOW_FAILED, payload: { error_message: 'Build failed' }, }), ]; render(); expect(screen.getByText('Workflow Failed')).toBeInTheDocument(); expect(screen.getByText('Build failed')).toBeInTheDocument(); }); it('handles workflow step completed event', () => { const events = [ createMockEvent({ type: EventType.WORKFLOW_STEP_COMPLETED, payload: { step_name: 'Build', step_number: 2, total_steps: 5 }, }), ]; render(); expect(screen.getByText('Step Completed')).toBeInTheDocument(); expect(screen.getByText(/Step 2\/5: Build/)).toBeInTheDocument(); }); it('handles approval denied event', () => { const events = [ createMockEvent({ type: EventType.APPROVAL_DENIED, payload: { reason: 'Security review needed' }, }), ]; render(); expect(screen.getByText('Approval Denied')).toBeInTheDocument(); expect(screen.getByText(/Denied: Security review needed/)).toBeInTheDocument(); }); it('handles agent status changed event', () => { const events = [ createMockEvent({ type: EventType.AGENT_STATUS_CHANGED, payload: { previous_status: 'idle', new_status: 'working' }, }), ]; render(); expect(screen.getByText('Status Changed')).toBeInTheDocument(); expect(screen.getByText(/Status: idle -> working/)).toBeInTheDocument(); }); it('handles issue updated event', () => { const events = [ createMockEvent({ type: EventType.ISSUE_UPDATED, payload: { issue_id: 'ISSUE-42' }, }), ]; render(); expect(screen.getByText('Issue Updated')).toBeInTheDocument(); expect(screen.getByText(/Issue ISSUE-42 updated/)).toBeInTheDocument(); }); it('handles issue assigned event with assignee', () => { const events = [ createMockEvent({ type: EventType.ISSUE_ASSIGNED, payload: { assignee_name: 'John Doe' }, }), ]; render(); expect(screen.getByText('Issue Assigned')).toBeInTheDocument(); expect(screen.getByText(/Assigned to John Doe/)).toBeInTheDocument(); }); it('handles issue assigned event without assignee', () => { const events = [ createMockEvent({ type: EventType.ISSUE_ASSIGNED, payload: {}, }), ]; render(); expect(screen.getByText('Issue Assigned')).toBeInTheDocument(); expect(screen.getByText(/Issue assignment changed/)).toBeInTheDocument(); }); it('handles issue closed event', () => { const events = [ createMockEvent({ type: EventType.ISSUE_CLOSED, payload: { resolution: 'Fixed in PR #123' }, }), ]; render(); expect(screen.getByText('Issue Closed')).toBeInTheDocument(); expect(screen.getByText(/Closed: Fixed in PR #123/)).toBeInTheDocument(); }); it('handles approval granted event', () => { const events = [ createMockEvent({ type: EventType.APPROVAL_GRANTED, payload: {}, }), ]; render(); expect(screen.getByText('Approval Granted')).toBeInTheDocument(); expect(screen.getByText('Approval granted')).toBeInTheDocument(); }); it('handles workflow started event', () => { const events = [ createMockEvent({ type: EventType.WORKFLOW_STARTED, payload: { workflow_type: 'CI/CD' }, }), ]; render(); expect(screen.getByText('Workflow Started')).toBeInTheDocument(); expect(screen.getByText(/CI\/CD workflow started/)).toBeInTheDocument(); }); it('handles sprint completed event', () => { const events = [ createMockEvent({ type: EventType.SPRINT_COMPLETED, payload: { sprint_name: 'Sprint 2' }, }), ]; render(); expect(screen.getByText('Sprint Completed')).toBeInTheDocument(); expect(screen.getByText(/Sprint "Sprint 2" completed/)).toBeInTheDocument(); }); it('handles project created event', () => { const events = [ createMockEvent({ type: EventType.PROJECT_CREATED, payload: {}, }), ]; const { container } = render(); // Project events use teal color styling expect(container.querySelector('.bg-teal-100')).toBeInTheDocument(); // And the event count should show expect(screen.getByText('1 event')).toBeInTheDocument(); }); }); });