From 184a2a16c9d4e85568c4712e3dec279d7ec1629b Mon Sep 17 00:00:00 2001 From: Knacky Date: Wed, 10 Jun 2026 20:22:45 +0200 Subject: [PATCH] fix(frontend): a11y on clickable rows + correct c2 source field + pill metric alignment (sprint 8 design-review) F1: add tabIndex/role/onKeyDown/aria-expanded to C2TasksPanel expander rows and C2CallbackPicker callback rows; focus-visible ring via Tailwind utilities F2: add source:'mimic'|'import' to C2TaskListItem; C2TasksPanel reads task.source instead of mapping_applied for the Source badge label F3: align C2TaskStatusBadge and C2CallbackPicker Active/Inactive pill metrics to py-[6px] text-[14px] font-medium (matches SimulationStatusBadge / StatusBadge) F4: replace hand-rolled Source pill class string with badge-pill-outline recipe Tests: 212/212 passing (+3 new: Enter/Space key on expander, Enter key on callback row) Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/api/types.ts | 1 + frontend/src/components/C2CallbackPicker.tsx | 12 ++++++-- frontend/src/components/C2TaskStatusBadge.tsx | 2 +- frontend/src/components/C2TasksPanel.tsx | 15 ++++++++-- frontend/tests/SimulationFormPage.test.tsx | 1 + frontend/tests/api/c2.test.ts | 1 + .../tests/components/C2TasksPanel.test.tsx | 28 +++++++++++++++++-- .../components/ExecuteViaC2Modal.test.tsx | 11 ++++++++ 8 files changed, 63 insertions(+), 8 deletions(-) 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(); + }); });