diff --git a/e2e/tests/us20-matrix-fits-modal.spec.ts b/e2e/tests/us20-matrix-fits-modal.spec.ts index 05968c4..11e3a20 100644 --- a/e2e/tests/us20-matrix-fits-modal.spec.ts +++ b/e2e/tests/us20-matrix-fits-modal.spec.ts @@ -176,4 +176,60 @@ test.describe('US-20 — MITRE matrix fits modal', () => { await deleteSimulation(redteamToken, sim.id); }); + + // NIT code-reviewer + AC-15.5 regression: Tab focus-trap cycle in MitreMatrixModal + test('AC-15.5 regression — MitreMatrixModal Tab key cycles focus, Shift+Tab reverses', async ({ + page, + context, + }) => { + const sim = await createSimulation(redteamToken, engagementId, 'AC-15.5 focus trap'); + + await seedTokenInStorage(context, redteamToken); + await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`); + + await page.getByLabel(/open mitre matrix/i).click(); + const dialog = page.getByRole('dialog'); + await expect(dialog).toBeVisible({ timeout: 10_000 }); + + // Collect all focusable elements in the dialog + const focusableCount = await dialog.evaluate((el) => { + const focusables = el.querySelectorAll( + 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' + ); + return focusables.length; + }); + expect(focusableCount).toBeGreaterThan(1); + + // Focus the last focusable element + await dialog.evaluate((el) => { + const focusables = Array.from(el.querySelectorAll( + 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' + )); + focusables[focusables.length - 1].focus(); + }); + + // Tab from last → should wrap to first (focus trap) + await page.keyboard.press('Tab'); + const activeAfterTab = await dialog.evaluate((el) => { + const focusables = Array.from(el.querySelectorAll( + 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' + )); + return focusables.indexOf(document.activeElement as HTMLElement); + }); + // After Tab from last, focus should be on first (index 0) + expect(activeAfterTab).toBe(0); + + // Shift+Tab from first → should wrap to last + await page.keyboard.press('Shift+Tab'); + const activeAfterShiftTab = await dialog.evaluate((el) => { + const focusables = Array.from(el.querySelectorAll( + 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' + )); + return focusables.indexOf(document.activeElement as HTMLElement); + }); + // After Shift+Tab from first, focus should be on last + expect(activeAfterShiftTab).toBe(focusableCount - 1); + + await deleteSimulation(redteamToken, sim.id); + }); }); diff --git a/e2e/tests/us21-tactic-selection.spec.ts b/e2e/tests/us21-tactic-selection.spec.ts index 3980c97..131c31b 100644 --- a/e2e/tests/us21-tactic-selection.spec.ts +++ b/e2e/tests/us21-tactic-selection.spec.ts @@ -271,4 +271,27 @@ test.describe('US-21 — tactic selection', () => { await deleteSimulation(redteamToken, sim.id); }); + + // NIT code-reviewer: +N suffix in SimulationList MITRE column + test('AC-21.7 — SimulationList MITRE column shows first id + "+N" for mixed tactics+techniques', async ({ + page, + context, + }) => { + const sim = await createSimulation(redteamToken, engagementId, 'AC-21.7 +N suffix'); + await makeClient(redteamToken).patch(`/simulations/${sim.id}`, { + tactic_ids: ['TA0007'], + technique_ids: ['T1059', 'T1078'], + }); + + await seedTokenInStorage(context, redteamToken); + await page.goto(`/engagements/${engagementId}`); + + // The row for this simulation should show "TA0007 +2" in the MITRE column + // (1 tactic TA0007 is first, then +2 for T1059 and T1078) + const simRow = page.getByRole('row').filter({ hasText: 'AC-21.7 +N suffix' }); + await expect(simRow).toBeVisible({ timeout: 5_000 }); + await expect(simRow).toContainText('TA0007 +2'); + + await deleteSimulation(redteamToken, sim.id); + }); });