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'
}`}
>
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();
+ });
});
|