diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index fb81155..11ff621 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -216,6 +216,7 @@ export interface C2TaskListItem { completed: boolean; output: string | null; mapping_applied: boolean; + source: 'mimic' | 'import'; created_at: string; completed_at: string | null; } diff --git a/frontend/src/components/C2CallbackPicker.tsx b/frontend/src/components/C2CallbackPicker.tsx index ee3566c..7c760c0 100644 --- a/frontend/src/components/C2CallbackPicker.tsx +++ b/frontend/src/components/C2CallbackPicker.tsx @@ -57,14 +57,22 @@ export function C2CallbackPicker({ key={cb.display_id} data-testid={rowTestId} onClick={() => onSelect(cb.display_id)} - className={`cursor-pointer border-b border-hairline ${ + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onSelect(cb.display_id); + } + }} + tabIndex={0} + role="button" + className={`cursor-pointer border-b border-hairline focus:outline-none focus-visible:ring-2 focus-visible:ring-primary ${ isSelected ? 'bg-primary-soft' : 'hover:bg-cloud' }`} > {cb.display_id} {status} diff --git a/frontend/src/components/C2TasksPanel.tsx b/frontend/src/components/C2TasksPanel.tsx index 466dab2..138729a 100644 --- a/frontend/src/components/C2TasksPanel.tsx +++ b/frontend/src/components/C2TasksPanel.tsx @@ -72,7 +72,16 @@ export function C2TasksPanel({ simulationId }: C2TasksPanelProps): JSX.Element { toggleExpand(task.id, task.completed)} - className={`border-b border-hairline ${canExpand ? 'cursor-pointer hover:bg-cloud' : ''}`} + onKeyDown={(e) => { + if (canExpand && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault(); + toggleExpand(task.id, task.completed); + } + }} + tabIndex={canExpand ? 0 : undefined} + role={canExpand ? 'button' : undefined} + aria-expanded={canExpand ? isExpanded : undefined} + className={`border-b border-hairline ${canExpand ? 'cursor-pointer hover:bg-cloud focus:outline-none focus-visible:ring-2 focus-visible:ring-primary' : ''}`} > {canExpand ? ( @@ -91,8 +100,8 @@ export function C2TasksPanel({ simulationId }: C2TasksPanelProps): JSX.Element { {task.command} - - {task.mapping_applied ? 'MIMIC' : 'IMPORT'} + + {task.source === 'mimic' ? 'MIMIC' : 'IMPORT'} diff --git a/frontend/tests/SimulationFormPage.test.tsx b/frontend/tests/SimulationFormPage.test.tsx index bee5da6..cd68ce4 100644 --- a/frontend/tests/SimulationFormPage.test.tsx +++ b/frontend/tests/SimulationFormPage.test.tsx @@ -388,6 +388,7 @@ describe('SimulationFormPage — C2 tasks panel visibility', () => { completed: true, output: 'SYSTEM', mapping_applied: false, + source: 'import', created_at: '2026-06-10T10:00:00', completed_at: '2026-06-10T10:00:05', }, diff --git a/frontend/tests/api/c2.test.ts b/frontend/tests/api/c2.test.ts index d6a25d1..7b76ed5 100644 --- a/frontend/tests/api/c2.test.ts +++ b/frontend/tests/api/c2.test.ts @@ -152,6 +152,7 @@ describe('getC2Tasks', () => { completed: true, output: 'NT AUTHORITY\\SYSTEM', mapping_applied: true, + source: 'mimic', created_at: '2026-06-10T10:00:00', completed_at: '2026-06-10T10:00:05', }, diff --git a/frontend/tests/components/C2TasksPanel.test.tsx b/frontend/tests/components/C2TasksPanel.test.tsx index fde40de..b4214aa 100644 --- a/frontend/tests/components/C2TasksPanel.test.tsx +++ b/frontend/tests/components/C2TasksPanel.test.tsx @@ -28,6 +28,7 @@ const COMPLETED_TASK = { completed: true, output: 'NT AUTHORITY\\SYSTEM', mapping_applied: true, + source: 'mimic' as const, created_at: '2026-06-10T10:00:00', completed_at: '2026-06-10T10:00:05', }; @@ -42,6 +43,7 @@ const PENDING_TASK = { completed: false, output: null, mapping_applied: false, + source: 'import' as const, created_at: '2026-06-10T10:00:10', completed_at: null, }; @@ -91,14 +93,14 @@ describe('C2TasksPanel — populated rows', () => { }); }); - it('shows MIMIC source badge for mapping_applied=true', async () => { + it('shows MIMIC source badge for source=mimic', async () => { renderWithProviders(); await waitFor(() => { expect(screen.getByText('MIMIC')).toBeInTheDocument(); }); }); - it('shows IMPORT source badge for mapping_applied=false', async () => { + it('shows IMPORT source badge for source=import', async () => { renderWithProviders(); await waitFor(() => { expect(screen.getByText('IMPORT')).toBeInTheDocument(); @@ -163,6 +165,28 @@ describe('C2TasksPanel — expand on click', () => { fireEvent.click(screen.getByTestId('c2-task-row')); expect(screen.queryByTestId('c2-task-output')).toBeNull(); }); + + it('Enter key on completed row toggles output (a11y)', async () => { + renderWithProviders(); + await waitFor(() => { + expect(screen.getAllByTestId('c2-task-row')).toHaveLength(1); + }); + const row = screen.getByTestId('c2-task-row'); + fireEvent.keyDown(row, { key: 'Enter' }); + expect(screen.getByTestId('c2-task-output')).toBeInTheDocument(); + fireEvent.keyDown(row, { key: 'Enter' }); + expect(screen.queryByTestId('c2-task-output')).toBeNull(); + }); + + it('Space key on completed row toggles output (a11y)', async () => { + renderWithProviders(); + await waitFor(() => { + expect(screen.getAllByTestId('c2-task-row')).toHaveLength(1); + }); + const row = screen.getByTestId('c2-task-row'); + fireEvent.keyDown(row, { key: ' ' }); + expect(screen.getByTestId('c2-task-output')).toBeInTheDocument(); + }); }); describe('C2TasksPanel — refresh indicator', () => { diff --git a/frontend/tests/components/ExecuteViaC2Modal.test.tsx b/frontend/tests/components/ExecuteViaC2Modal.test.tsx index bfeafa4..a92d1dc 100644 --- a/frontend/tests/components/ExecuteViaC2Modal.test.tsx +++ b/frontend/tests/components/ExecuteViaC2Modal.test.tsx @@ -157,4 +157,15 @@ describe('ExecuteViaC2Modal', () => { const textarea = screen.getByTestId('c2-commands-textarea') as HTMLTextAreaElement; expect(textarea.value).toBe('net user\nwhoami /all'); }); + + it('Enter key on callback row selects it (a11y)', async () => { + renderModal('whoami'); + await waitFor(() => { + expect(screen.getAllByTestId('c2-callback-row')).toHaveLength(2); + }); + const firstRow = screen.getAllByTestId('c2-callback-row')[0]; + fireEvent.keyDown(firstRow, { key: 'Enter' }); + // Row is now selected → Launch button should be enabled + expect(screen.getByTestId('c2-launch-btn')).not.toBeDisabled(); + }); });