feat: sprint 4 — UI polish + dark mode + workflow tightening + process hygiene #7
@@ -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<HTMLElement>(
|
||||
'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<HTMLElement>(
|
||||
'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<HTMLElement>(
|
||||
'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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user