Files
mimic/e2e/tests/us22-mitre-input-redesign.spec.ts
Knacky 5aa839d105 test(e2e): sprint 4 acceptance tests — US-17 to US-23
Add new spec files for US-17 (UI polish), US-18 (done read-only + reopen),
US-19 (engagement auto-status), US-20 (matrix fits modal), US-21 (tactic
selection), US-22 (MITRE input redesign), US-23 (dark mode).

Adapt sprint 2/3 specs for sprint 4 UI renames: matrix icon button replaces
text buttons, inline search replaces Quick Search, Save replaces Save Red Team,
New replaces New Engagement, topbar uses bg-slab tokens, Apply N item(s) replaces
Apply N technique(s), done→review_required transition now valid (Reopen flow).

Mark AC-21.6 Apply-from-modal as test.fail: known defect where /api/mitre/matrix
returns slug tactic IDs but PATCH /simulations/:id expects TA-format IDs.

Final result: 156 passed, 0 failed (1 expected failure via test.fail).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 21:27:12 +02:00

251 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* US-22 — Refonte input MITRE dans le form.
* Covers AC-22.1 → AC-22.5.
* Key change: no "Add technique" / "Quick search" text buttons.
* Instead: inline autocomplete input + grid icon for matrix.
* Chips show T-id only (name in title=).
*/
import { test, expect } from '@playwright/test';
import {
adminToken,
createEngagement,
deleteEngagement,
deleteUserByUsername,
ensureUser,
login,
makeClient,
} from '../fixtures/api';
import { seedTokenInStorage } from '../fixtures/auth';
const REDTEAM_USER = 'us22-redteam';
const SOC_USER = 'us22-soc';
const PASS = 'us22-pass-strong';
interface Simulation { id: number; [key: string]: unknown; }
async function createSimulation(token: string, engagementId: number, name = 'US-22 sim'): Promise<Simulation> {
const r = await makeClient(token).post(`/engagements/${engagementId}/simulations`, { name });
if (r.status !== 201) throw new Error(`create sim: ${r.status}`);
return r.data as Simulation;
}
async function deleteSimulation(token: string, simId: number): Promise<void> {
await makeClient(token).delete(`/simulations/${simId}`);
}
test.describe('US-22 — MITRE input redesign', () => {
let redteamToken: string;
let socToken: string;
let engagementId: number;
test.beforeAll(async () => {
await ensureUser(REDTEAM_USER, PASS, 'redteam');
await ensureUser(SOC_USER, PASS, 'soc');
redteamToken = (await login(REDTEAM_USER, PASS)).token;
socToken = (await login(SOC_USER, PASS)).token;
const eng = await createEngagement(redteamToken, {
name: 'US-22 Engagement',
start_date: '2026-01-01',
});
engagementId = eng.id;
});
test.afterAll(async () => {
try {
const tok = await adminToken();
await deleteEngagement(tok, engagementId);
for (const u of [REDTEAM_USER, SOC_USER]) await deleteUserByUsername(tok, u);
} catch { /* noop */ }
});
test('AC-22.1 — layout: inline autocomplete input + matrix icon present, NO text buttons', async ({
page,
context,
}) => {
const sim = await createSimulation(redteamToken, engagementId, 'AC-22.1 layout');
await seedTokenInStorage(context, redteamToken);
await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`);
// Matrix icon button present
await expect(page.getByLabel(/open mitre matrix/i)).toBeVisible();
// Search input placeholder visible (inline autocomplete)
await expect(page.getByText(/search technique/i)).toBeVisible();
// No old-style text buttons
await expect(page.getByRole('button', { name: /add technique/i })).not.toBeVisible();
await expect(page.getByRole('button', { name: /quick search/i })).not.toBeVisible();
await deleteSimulation(redteamToken, sim.id);
});
test('AC-22.1 — inline autocomplete: click input shows combobox, type shows dropdown', async ({
page,
context,
}) => {
const sim = await createSimulation(redteamToken, engagementId, 'AC-22.1 autocomplete');
await seedTokenInStorage(context, redteamToken);
await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`);
// Click the fake-input placeholder to reveal the combobox
await page.getByText(/search technique/i).click();
const picker = page.getByRole('combobox', { name: /mitre technique/i });
await expect(picker).toBeVisible();
// Type to get results
await picker.fill('T1059');
const listbox = page.getByRole('listbox', { name: /mitre techniques/i });
await expect(listbox).toBeVisible({ timeout: 5_000 });
// Select via keyboard
await picker.press('ArrowDown');
await picker.press('Enter');
// Tag appears
await expect(page.getByTestId('techniques-tag-list')).toContainText('T1059', { timeout: 5_000 });
// Auto-save toast
await expect(page.getByText(/techniques updated/i)).toBeVisible({ timeout: 5_000 });
await deleteSimulation(redteamToken, sim.id);
});
test('AC-22.1 — matrix icon opens MitreMatrixModal', async ({
page,
context,
}) => {
const sim = await createSimulation(redteamToken, engagementId, 'AC-22.1 matrix icon');
await seedTokenInStorage(context, redteamToken);
await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`);
await page.getByLabel(/open mitre matrix/i).click();
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10_000 });
await expect(page.getByRole('dialog')).toContainText(/mitre att&?ck matrix/i);
await deleteSimulation(redteamToken, sim.id);
});
// AC-22.2 — chips show T-id only, name in title
test('AC-22.2 — technique chips display T-id only, name in title= attribute', async ({
page,
context,
}) => {
const sim = await createSimulation(redteamToken, engagementId, 'AC-22.2 chip format');
await makeClient(redteamToken).patch(`/simulations/${sim.id}`, { technique_ids: ['T1059'] });
await seedTokenInStorage(context, redteamToken);
await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`);
const chip = page.getByTestId('mitre-technique-tag').first();
await expect(chip).toBeVisible();
// Text content is T-id only
const text = await chip.textContent();
expect(text?.trim()).toMatch(/^T1059/);
// Must NOT contain the full name inline
expect(text).not.toMatch(/Command and Scripting Interpreter/);
// Name appears in title attribute
const title = await chip.getAttribute('title');
expect(title).toMatch(/T1059/);
expect(title).toMatch(/Command and Scripting Interpreter/);
await deleteSimulation(redteamToken, sim.id);
});
test('AC-22.2 — tactic chips display TA-id only, name in title= attribute', async ({
page,
context,
}) => {
const sim = await createSimulation(redteamToken, engagementId, 'AC-22.2 tactic chip format');
await makeClient(redteamToken).patch(`/simulations/${sim.id}`, { tactic_ids: ['TA0007'] });
await seedTokenInStorage(context, redteamToken);
await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`);
const chip = page.getByTestId('mitre-tactic-tag').first();
await expect(chip).toBeVisible();
const text = await chip.textContent();
expect(text?.trim()).toMatch(/^TA0007/);
expect(text).not.toMatch(/Discovery/);
const title = await chip.getAttribute('title');
expect(title).toMatch(/TA0007/);
expect(title).toMatch(/Discovery/);
await deleteSimulation(redteamToken, sim.id);
});
// AC-22.4 — empty state
test('AC-22.4 — empty state: "No techniques selected" visible, input and matrix icon still shown', async ({
page,
context,
}) => {
const sim = await createSimulation(redteamToken, engagementId, 'AC-22.4 empty');
await seedTokenInStorage(context, redteamToken);
await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`);
// Empty state message
await expect(page.getByText(/no techniques selected/i)).toBeVisible();
// Input and matrix icon still present in non-disabled mode
await expect(page.getByLabel(/open mitre matrix/i)).toBeVisible();
await expect(page.getByText(/search technique/i)).toBeVisible();
await deleteSimulation(redteamToken, sim.id);
});
// AC-22.5 — read-only mode
test('AC-22.5 — SOC on in_progress sim: chips visible (no ×), input + matrix icon hidden', async ({
page,
context,
}) => {
const sim = await createSimulation(redteamToken, engagementId, 'AC-22.5 soc readonly');
// Add a technique and advance to review_required for SOC access
await makeClient(redteamToken).patch(`/simulations/${sim.id}`, { technique_ids: ['T1059'] });
await makeClient(redteamToken).post(`/simulations/${sim.id}/transition`, { to: 'review_required' });
await seedTokenInStorage(context, socToken);
await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`);
// Chip is visible
await expect(page.getByTestId('mitre-technique-tag')).toBeVisible();
// No × remove button (read-only for SOC on technique chips)
await expect(page.getByRole('button', { name: /remove T1059/i })).not.toBeVisible();
// Matrix icon and input hidden in disabled mode
await expect(page.getByLabel(/open mitre matrix/i)).not.toBeVisible();
await expect(page.getByText(/search technique/i)).not.toBeVisible();
await deleteSimulation(redteamToken, sim.id);
});
test('AC-22.5 — done sim: all chips read-only, no input', async ({
page,
context,
}) => {
const sim = await createSimulation(redteamToken, engagementId, 'AC-22.5 done readonly');
await makeClient(redteamToken).patch(`/simulations/${sim.id}`, { technique_ids: ['T1059'] });
// Drive to done
await makeClient(redteamToken).post(`/simulations/${sim.id}/transition`, { to: 'review_required' });
await makeClient(redteamToken).post(`/simulations/${sim.id}/transition`, { to: 'done' });
await seedTokenInStorage(context, redteamToken);
await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`);
// Chip visible
await expect(page.getByTestId('mitre-technique-tag')).toBeVisible();
// No × remove
await expect(page.getByRole('button', { name: /remove T1059/i })).not.toBeVisible();
// No matrix icon
await expect(page.getByLabel(/open mitre matrix/i)).not.toBeVisible();
await deleteSimulation(redteamToken, sim.id);
});
});