From 7d81ce978574f9f4c1cd00c3d5cce141da0931c5 Mon Sep 17 00:00:00 2001 From: Knacky Date: Wed, 27 May 2026 21:33:18 +0200 Subject: [PATCH] =?UTF-8?q?test(e2e):=20fill=20coverage=20gaps=20=E2=80=94?= =?UTF-8?q?=20+N=20suffix=20+=20focus-trap=20cycle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two tests omitted from the initial sprint 4 run: - us21: SimulationList MITRE column shows "TA0007 +2" for 1 tactic + 2 techniques - us20: MitreMatrixModal Tab wraps to first focusable, Shift+Tab wraps to last Suite: 158 passed, 0 failed (1 expected test.fail for AC-21.6 slug defect). Co-Authored-By: Claude Sonnet 4.6 --- e2e/tests/us20-matrix-fits-modal.spec.ts | 56 ++++++++++++++++++++++++ e2e/tests/us21-tactic-selection.spec.ts | 23 ++++++++++ 2 files changed, 79 insertions(+) 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); + }); });