/** * IssueTable Component Tests */ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { IssueTable } from '@/features/issues/components/IssueTable'; import type { IssueSummary, IssueSort } from '@/features/issues/types'; const mockIssues: IssueSummary[] = [ { id: 'issue-1', number: 42, type: 'bug', title: 'Test Issue 1', description: 'Description 1', status: 'open', priority: 'high', labels: ['bug', 'frontend'], sprint: 'Sprint 1', assignee: { id: 'user-1', name: 'Test User', type: 'human' }, created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-02T00:00:00Z', sync_status: 'synced', }, { id: 'issue-2', number: 43, type: 'story', title: 'Test Issue 2', description: 'Description 2', status: 'in_progress', priority: 'medium', labels: ['feature'], sprint: null, assignee: null, created_at: '2025-01-02T00:00:00Z', updated_at: '2025-01-03T00:00:00Z', sync_status: 'pending', }, ]; describe('IssueTable', () => { const defaultSort: IssueSort = { field: 'number', direction: 'asc' }; const mockOnSelectionChange = jest.fn(); const mockOnIssueClick = jest.fn(); const mockOnSortChange = jest.fn(); beforeEach(() => { mockOnSelectionChange.mockClear(); mockOnIssueClick.mockClear(); mockOnSortChange.mockClear(); }); it('renders issue rows', () => { render( ); expect(screen.getByText('Test Issue 1')).toBeInTheDocument(); expect(screen.getByText('Test Issue 2')).toBeInTheDocument(); }); it('displays issue numbers', () => { render( ); expect(screen.getByText('42')).toBeInTheDocument(); expect(screen.getByText('43')).toBeInTheDocument(); }); it('shows labels for issues', () => { render( ); expect(screen.getByText('bug')).toBeInTheDocument(); expect(screen.getByText('frontend')).toBeInTheDocument(); expect(screen.getByText('feature')).toBeInTheDocument(); }); it('calls onIssueClick when row is clicked', async () => { const user = userEvent.setup(); render( ); const row = screen.getByTestId('issue-row-issue-1'); await user.click(row); expect(mockOnIssueClick).toHaveBeenCalledWith('issue-1'); }); it('handles issue selection', async () => { const user = userEvent.setup(); render( ); // Find checkbox for first issue const checkbox = screen.getByRole('checkbox', { name: /select issue 42/i }); await user.click(checkbox); expect(mockOnSelectionChange).toHaveBeenCalledWith(['issue-1']); }); it('handles select all', async () => { const user = userEvent.setup(); render( ); // Find select all checkbox const selectAllCheckbox = screen.getByRole('checkbox', { name: /select all issues/i }); await user.click(selectAllCheckbox); expect(mockOnSelectionChange).toHaveBeenCalledWith(['issue-1', 'issue-2']); }); it('handles deselect all when all selected', async () => { const user = userEvent.setup(); render( ); // Find deselect all checkbox const selectAllCheckbox = screen.getByRole('checkbox', { name: /deselect all issues/i }); await user.click(selectAllCheckbox); expect(mockOnSelectionChange).toHaveBeenCalledWith([]); }); it('handles sorting by number', async () => { const user = userEvent.setup(); render( ); // Click the # column header const numberHeader = screen.getByRole('button', { name: /#/i }); await user.click(numberHeader); expect(mockOnSortChange).toHaveBeenCalled(); }); it('handles sorting by priority', async () => { const user = userEvent.setup(); render( ); // Click the Priority column header const priorityHeader = screen.getByRole('button', { name: /priority/i }); await user.click(priorityHeader); expect(mockOnSortChange).toHaveBeenCalledWith({ field: 'priority', direction: 'desc' }); }); it('shows empty state when no issues', () => { render( ); expect(screen.getByText('No issues found')).toBeInTheDocument(); expect(screen.getByText('Try adjusting your search or filters')).toBeInTheDocument(); }); it('shows unassigned text for issues without assignee', () => { render( ); expect(screen.getByText('Unassigned')).toBeInTheDocument(); }); it('shows backlog text for issues without sprint', () => { render( ); expect(screen.getByText('Backlog')).toBeInTheDocument(); }); it('deselects issue when clicking selected checkbox', async () => { const user = userEvent.setup(); render( ); // Find checkbox for first issue (already selected) const checkbox = screen.getByRole('checkbox', { name: /select issue 42/i }); await user.click(checkbox); expect(mockOnSelectionChange).toHaveBeenCalledWith([]); }); it('handles keyboard navigation for number column sorting', async () => { const user = userEvent.setup(); render( ); // Find the # column header and press Enter const numberHeader = screen.getByRole('button', { name: /#/i }); numberHeader.focus(); await user.keyboard('{Enter}'); expect(mockOnSortChange).toHaveBeenCalled(); }); it('handles keyboard navigation for priority column sorting', async () => { const user = userEvent.setup(); render( ); // Find the Priority column header and press Enter const priorityHeader = screen.getByRole('button', { name: /priority/i }); priorityHeader.focus(); await user.keyboard('{Enter}'); expect(mockOnSortChange).toHaveBeenCalledWith({ field: 'priority', direction: 'desc' }); }); it('toggles sort direction when clicking same column', async () => { const user = userEvent.setup(); render( ); // Click number header (currently sorted asc) const numberHeader = screen.getByRole('button', { name: /#/i }); await user.click(numberHeader); // Should toggle to desc expect(mockOnSortChange).toHaveBeenCalledWith({ field: 'number', direction: 'desc' }); }); it('shows agent icon for agent assignee', () => { const issueWithAgentAssignee: IssueSummary[] = [ { id: 'issue-1', number: 42, type: 'bug', title: 'Test Issue', description: 'Description', status: 'open', priority: 'high', labels: [], sprint: null, assignee: { id: 'agent-1', name: 'Test Agent', type: 'agent' }, created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-02T00:00:00Z', sync_status: 'synced', }, ]; render( ); // Check that agent name is displayed expect(screen.getByText('Test Agent')).toBeInTheDocument(); }); it('truncates labels when more than 3', () => { const issueWithManyLabels: IssueSummary[] = [ { id: 'issue-1', number: 42, type: 'bug', title: 'Test Issue', description: 'Description', status: 'open', priority: 'high', labels: ['label1', 'label2', 'label3', 'label4', 'label5'], sprint: null, assignee: null, created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-02T00:00:00Z', sync_status: 'synced', }, ]; render( ); // First 3 labels should be shown expect(screen.getByText('label1')).toBeInTheDocument(); expect(screen.getByText('label2')).toBeInTheDocument(); expect(screen.getByText('label3')).toBeInTheDocument(); // +2 badge should be shown expect(screen.getByText('+2')).toBeInTheDocument(); }); it('displays actions dropdown menu', async () => { const user = userEvent.setup(); render( ); // Find and click the actions button const actionsButton = screen.getByRole('button', { name: /actions for issue 42/i }); await user.click(actionsButton); // Dropdown menu items should be visible expect(screen.getByRole('menuitem', { name: /view details/i })).toBeInTheDocument(); expect(screen.getByRole('menuitem', { name: /edit/i })).toBeInTheDocument(); expect(screen.getByRole('menuitem', { name: /assign/i })).toBeInTheDocument(); expect(screen.getByRole('menuitem', { name: /sync with tracker/i })).toBeInTheDocument(); expect(screen.getByRole('menuitem', { name: /delete/i })).toBeInTheDocument(); }); it('does not trigger row click when clicking actions menu', async () => { const user = userEvent.setup(); render( ); // Find and click the actions button const actionsButton = screen.getByRole('button', { name: /actions for issue 42/i }); await user.click(actionsButton); // Row click should not be triggered expect(mockOnIssueClick).not.toHaveBeenCalled(); }); it('calls onIssueClick when View Details is clicked from menu', async () => { const user = userEvent.setup(); render( ); // Open the actions menu const actionsButton = screen.getByRole('button', { name: /actions for issue 42/i }); await user.click(actionsButton); // Click View Details const viewDetailsItem = screen.getByRole('menuitem', { name: /view details/i }); await user.click(viewDetailsItem); expect(mockOnIssueClick).toHaveBeenCalledWith('issue-1'); }); it('shows descending sort icon when sorted descending', () => { render( ); // The column header should have aria-sort="descending" const numberHeader = screen.getByRole('button', { name: /#/i }); expect(numberHeader).toHaveAttribute('aria-sort', 'descending'); }); it('shows ascending sort icon when sorted ascending', () => { render( ); // The column header should have aria-sort="ascending" const numberHeader = screen.getByRole('button', { name: /#/i }); expect(numberHeader).toHaveAttribute('aria-sort', 'ascending'); }); it('applies custom className', () => { const { container } = render( ); expect(container.firstChild).toHaveClass('custom-class'); }); });