/** * Tests for ActivityFeed Component * * Tests cover: * - Rendering with events * - Connection state indicator * - Search functionality * - Filter functionality * - Event expansion * - Approval actions * - Time-based grouping * - Loading state * - Empty state */ import { render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ActivityFeed } from '@/components/activity/ActivityFeed'; import { EventType, type ProjectEvent } from '@/lib/types/events'; // ============================================================================ // Test Data // ============================================================================ const createMockEvent = (overrides: Partial = {}): ProjectEvent => ({ id: `event-${Math.random().toString(36).substr(2, 9)}`, type: EventType.AGENT_MESSAGE, timestamp: new Date().toISOString(), project_id: 'project-001', actor_id: 'agent-001', actor_type: 'agent', payload: { agent_instance_id: 'agent-001', message: 'Test message', message_type: 'info', }, ...overrides, }); const mockEvents: ProjectEvent[] = [ // Today's events createMockEvent({ id: 'event-001', type: EventType.APPROVAL_REQUESTED, timestamp: new Date().toISOString(), payload: { approval_id: 'apr-001', approval_type: 'architecture_decision', description: 'Approval required for API design', requested_by: 'Architect', }, }), createMockEvent({ id: 'event-002', type: EventType.AGENT_MESSAGE, timestamp: new Date(Date.now() - 1000 * 60 * 30).toISOString(), payload: { agent_instance_id: 'agent-002', message: 'Completed JWT implementation', message_type: 'info', }, }), // Yesterday's event createMockEvent({ id: 'event-003', type: EventType.ISSUE_CREATED, timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(), payload: { issue_id: 'issue-001', title: 'Add rate limiting', priority: 'medium', }, }), // This week's event createMockEvent({ id: 'event-004', type: EventType.SPRINT_STARTED, timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3).toISOString(), payload: { sprint_id: 'sprint-001', sprint_name: 'Sprint 1', goal: 'Complete auth module', }, }), // Older event createMockEvent({ id: 'event-005', type: EventType.WORKFLOW_COMPLETED, timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 * 10).toISOString(), payload: { workflow_id: 'wf-001', duration_seconds: 3600, }, }), ]; // ============================================================================ // Tests // ============================================================================ describe('ActivityFeed', () => { const defaultProps = { events: mockEvents, connectionState: 'connected' as const, }; describe('Rendering', () => { it('renders the activity feed with test id', () => { render(); expect(screen.getByTestId('activity-feed')).toBeInTheDocument(); }); it('renders the header with title', () => { render(); expect(screen.getByText('Activity Feed')).toBeInTheDocument(); }); it('renders custom title when provided', () => { render(); expect(screen.getByText('Project Activity')).toBeInTheDocument(); }); it('hides header when showHeader is false', () => { render(); expect(screen.queryByText('Activity Feed')).not.toBeInTheDocument(); }); it('renders events', () => { render(); expect(screen.getByTestId('event-item-event-001')).toBeInTheDocument(); expect(screen.getByTestId('event-item-event-002')).toBeInTheDocument(); }); it('applies custom className', () => { render(); expect(screen.getByTestId('activity-feed')).toHaveClass('custom-class'); }); }); describe('Connection State', () => { it('renders connection indicator', () => { render(); expect(screen.getByTestId('connection-indicator')).toBeInTheDocument(); }); it('shows "Live" when connected', () => { render(); expect(screen.getByText('Live')).toBeInTheDocument(); }); it('shows "Connecting..." when connecting', () => { render(); expect(screen.getByText('Connecting...')).toBeInTheDocument(); }); it('shows "Disconnected" when disconnected', () => { render(); expect(screen.getByText('Disconnected')).toBeInTheDocument(); }); it('shows "Error" when error state', () => { render(); expect(screen.getByText('Error')).toBeInTheDocument(); }); it('shows reconnect button when disconnected', () => { const onReconnect = jest.fn(); render( ); const reconnectButton = screen.getByLabelText('Reconnect'); expect(reconnectButton).toBeInTheDocument(); }); it('calls onReconnect when reconnect button clicked', async () => { const user = userEvent.setup(); const onReconnect = jest.fn(); render( ); await user.click(screen.getByLabelText('Reconnect')); expect(onReconnect).toHaveBeenCalledTimes(1); }); }); describe('Search Functionality', () => { it('renders search input when enableSearch is true', () => { render(); expect(screen.getByTestId('search-input')).toBeInTheDocument(); }); it('hides search input when enableSearch is false', () => { render(); expect(screen.queryByTestId('search-input')).not.toBeInTheDocument(); }); it('filters events based on search query', async () => { const user = userEvent.setup(); render(); const searchInput = screen.getByTestId('search-input'); await user.type(searchInput, 'JWT'); // Event with JWT in message should be visible expect(screen.getByText(/Completed JWT implementation/)).toBeInTheDocument(); // Other events should be filtered out expect(screen.queryByText(/Approval required for API design/)).not.toBeInTheDocument(); }); it('shows empty state when search finds no results', async () => { const user = userEvent.setup(); render(); const searchInput = screen.getByTestId('search-input'); await user.type(searchInput, 'nonexistent query xyz'); expect(screen.getByTestId('empty-state')).toBeInTheDocument(); expect(screen.getByText('No activity found')).toBeInTheDocument(); }); }); describe('Filter Functionality', () => { it('renders filter toggle when enableFiltering is true', () => { render(); expect(screen.getByTestId('filter-toggle')).toBeInTheDocument(); }); it('hides filter toggle when enableFiltering is false', () => { render(); expect(screen.queryByTestId('filter-toggle')).not.toBeInTheDocument(); }); it('shows filter panel when filter toggle is clicked', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByTestId('filter-toggle')); expect(screen.getByTestId('filter-panel')).toBeInTheDocument(); }); it('filters events by category when filter is selected', async () => { const user = userEvent.setup(); render(); // Open filter panel await user.click(screen.getByTestId('filter-toggle')); // Select Issues category const issuesCheckbox = screen.getByLabelText(/Issues/); await user.click(issuesCheckbox); // Only issue events should be visible expect(screen.getByText(/Add rate limiting/)).toBeInTheDocument(); // Agent events should be filtered out expect(screen.queryByText(/Completed JWT implementation/)).not.toBeInTheDocument(); }); it('shows pending only when filter is selected', async () => { const user = userEvent.setup(); render(); // Open filter panel await user.click(screen.getByTestId('filter-toggle')); // Select pending only const pendingCheckbox = screen.getByLabelText(/Show only pending approvals/); await user.click(pendingCheckbox); // Only approval requested events should be visible expect(screen.getByText(/Approval required for API design/)).toBeInTheDocument(); // Other events should be filtered out expect(screen.queryByText(/Completed JWT implementation/)).not.toBeInTheDocument(); }); it('clears filters when Clear Filters is clicked', async () => { const user = userEvent.setup(); render(); // Add search query await user.type(screen.getByTestId('search-input'), 'JWT'); // Open filter panel and select a filter await user.click(screen.getByTestId('filter-toggle')); await user.click(screen.getByLabelText(/Issues/)); // Clear filters await user.click(screen.getByText('Clear Filters')); // All events should be visible again expect(screen.getByText(/Completed JWT implementation/)).toBeInTheDocument(); expect(screen.getByText(/Approval required for API design/)).toBeInTheDocument(); }); }); describe('Event Expansion', () => { it('expands event details when clicked', async () => { const user = userEvent.setup(); render(); const eventItem = screen.getByTestId('event-item-event-001'); await user.click(eventItem); expect(screen.getByTestId('event-details')).toBeInTheDocument(); }); it('collapses event details when clicked again', async () => { const user = userEvent.setup(); render(); const eventItem = screen.getByTestId('event-item-event-001'); // Expand await user.click(eventItem); expect(screen.getByTestId('event-details')).toBeInTheDocument(); // Collapse await user.click(eventItem); expect(screen.queryByTestId('event-details')).not.toBeInTheDocument(); }); it('shows raw payload in expanded details', async () => { const user = userEvent.setup(); render(); const eventItem = screen.getByTestId('event-item-event-001'); await user.click(eventItem); // Check for payload content expect(screen.getByText(/View raw payload/)).toBeInTheDocument(); }); }); describe('Approval Actions', () => { it('shows approve and reject buttons for pending approvals', () => { render(); const eventItem = screen.getByTestId('event-item-event-001'); expect(within(eventItem).getByTestId('approve-button')).toBeInTheDocument(); expect(within(eventItem).getByTestId('reject-button')).toBeInTheDocument(); }); it('does not show action buttons for non-approval events', () => { render(); const eventItem = screen.getByTestId('event-item-event-002'); expect(within(eventItem).queryByTestId('approve-button')).not.toBeInTheDocument(); }); it('calls onApprove when approve button clicked', async () => { const user = userEvent.setup(); const onApprove = jest.fn(); render(); const eventItem = screen.getByTestId('event-item-event-001'); await user.click(within(eventItem).getByTestId('approve-button')); expect(onApprove).toHaveBeenCalledTimes(1); expect(onApprove).toHaveBeenCalledWith(expect.objectContaining({ id: 'event-001' })); }); it('calls onReject when reject button clicked', async () => { const user = userEvent.setup(); const onReject = jest.fn(); render(); const eventItem = screen.getByTestId('event-item-event-001'); await user.click(within(eventItem).getByTestId('reject-button')); expect(onReject).toHaveBeenCalledTimes(1); expect(onReject).toHaveBeenCalledWith(expect.objectContaining({ id: 'event-001' })); }); it('shows pending count badge', () => { render(); expect(screen.getByText('1 pending')).toBeInTheDocument(); }); }); describe('Time-Based Grouping', () => { it('groups events by time period', () => { render(); // Check for time period headers expect(screen.getByTestId('event-group-today')).toBeInTheDocument(); }); it('shows event count in group header', () => { render(); const todayGroup = screen.getByTestId('event-group-today'); // Today has 2 events in our mock data expect(within(todayGroup).getByText('2')).toBeInTheDocument(); }); }); describe('Loading State', () => { it('shows loading skeleton when isLoading is true', () => { render(); expect(screen.getByTestId('loading-skeleton')).toBeInTheDocument(); }); it('hides events when loading', () => { render(); expect(screen.queryByTestId('event-item-event-001')).not.toBeInTheDocument(); }); }); describe('Empty State', () => { it('shows empty state when no events', () => { render(); expect(screen.getByTestId('empty-state')).toBeInTheDocument(); }); it('shows appropriate message when no events and no filters', () => { render(); expect(screen.getByText(/Activity will appear here/)).toBeInTheDocument(); }); it('shows appropriate message when filtered to empty', async () => { const user = userEvent.setup(); render(); await user.type(screen.getByTestId('search-input'), 'nonexistent'); expect(screen.getByText(/Try adjusting your search or filters/)).toBeInTheDocument(); }); }); describe('Event Click Handler', () => { it('calls onEventClick when event is clicked', async () => { const user = userEvent.setup(); const onEventClick = jest.fn(); render(); await user.click(screen.getByTestId('event-item-event-001')); expect(onEventClick).toHaveBeenCalledTimes(1); expect(onEventClick).toHaveBeenCalledWith(expect.objectContaining({ id: 'event-001' })); }); }); describe('Compact Mode', () => { it('applies compact styling when compact is true', () => { render(); // Check for compact-specific styling (p-2 instead of p-4) const eventItem = screen.getByTestId('event-item-event-001'); // The event item should have compact padding expect(eventItem).toHaveClass('p-2'); }); }); describe('Accessibility', () => { it('has proper ARIA labels for interactive elements', () => { render( ); expect(screen.getByLabelText('Reconnect')).toBeInTheDocument(); }); it('event items are keyboard accessible', async () => { const user = userEvent.setup(); render(); const eventItem = screen.getByTestId('event-item-event-001'); // Focus and activate with keyboard eventItem.focus(); await user.keyboard('{Enter}'); expect(screen.getByTestId('event-details')).toBeInTheDocument(); }); it('renders semantic HTML structure', () => { render(); // Check for proper heading hierarchy const heading = screen.getByText('Today'); expect(heading.tagName).toBe('H3'); }); }); describe('Max Height', () => { it('applies max height styling', () => { const { container } = render(); const scrollContainer = container.querySelector('.overflow-y-auto'); expect(scrollContainer).toHaveStyle({ maxHeight: '500px' }); }); it('handles string max height', () => { const { container } = render(); const scrollContainer = container.querySelector('.overflow-y-auto'); expect(scrollContainer).toHaveStyle({ maxHeight: 'auto' }); }); }); });