Files
mimic/e2e/tests/us5-design.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

162 lines
5.4 KiB
TypeScript
Raw Permalink 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-5 — UI respects DESIGN.md tokens, layout doesn't break at 1280×720,
* and loading/error/empty states are wired.
*
* Pixel-perfect fidelity is NOT enforced — instead we assert that the
* tokenised classes from tailwind.config.ts are present and that the canvas
* has no horizontal overflow at the design viewport.
*/
import { test, expect } from '@playwright/test';
import {
adminToken,
deleteAllEngagements,
deleteUserByUsername,
ensureUser,
login,
} from '../fixtures/api';
import { seedTokenInStorage } from '../fixtures/auth';
const REDTEAM_USER = 'us5-redteam';
const PASS = 'us5-passw0rd';
test.describe('US-5 — DESIGN.md fidelity, responsive, states', () => {
let redteamToken: string;
test.beforeAll(async () => {
await ensureUser(REDTEAM_USER, PASS, 'redteam');
redteamToken = (await login(REDTEAM_USER, PASS)).token;
});
test.afterAll(async () => {
try {
const tok = await adminToken();
await deleteAllEngagements(tok);
await deleteUserByUsername(tok, REDTEAM_USER);
} catch {
/* noop */
}
});
test('AC-5.1 — DESIGN.md tokens applied (Inter font, brand palette, named utilities)', async ({
page,
context,
}) => {
await seedTokenInStorage(context, redteamToken);
await page.goto('/engagements');
// Body font must resolve to a stack including "Inter".
const fontFamily = await page.evaluate(
() => window.getComputedStyle(document.body).fontFamily,
);
expect(fontFamily.toLowerCase()).toMatch(/inter/);
// Tailwind-compiled DESIGN palette: the primary chevron at the top-left of
// the nav uses `bg-primary` → rendered as rgb(2, 74, 216).
const chevronBg = await page
.locator('header a[aria-label="Mimic home"] span[aria-hidden]')
.first()
.evaluate((el) => window.getComputedStyle(el).backgroundColor);
expect(chevronBg.replace(/\s/g, '')).toBe('rgb(2,74,216)');
// Sprint 4: topbar utility-strip uses `bg-slab` (#111827 → rgb(17,24,39)).
const utilityBg = await page
.locator('div.bg-slab.text-slab-text')
.first()
.evaluate((el) => window.getComputedStyle(el).backgroundColor);
expect(utilityBg.replace(/\s/g, '')).toBe('rgb(17,24,39)');
// Spot-check a few semantic class names live in the DOM (proves tokens are
// wired through tailwind.config.ts and not ad-hoc hex values).
await expect(page.locator('.btn-primary, .card-product, .max-w-page').first()).toBeVisible();
});
test('AC-5.2 — no horizontal overflow at 1280×720 across key pages', async ({
page,
context,
}) => {
await seedTokenInStorage(context, redteamToken);
await page.setViewportSize({ width: 1280, height: 720 });
const routes = ['/engagements', '/engagements/new'];
for (const route of routes) {
await page.goto(route);
const overflow = await page.evaluate(() => ({
scrollWidth: document.documentElement.scrollWidth,
clientWidth: document.documentElement.clientWidth,
}));
expect(
overflow.scrollWidth,
`${route} should not horizontally overflow at 1280px`,
).toBeLessThanOrEqual(overflow.clientWidth + 1);
}
});
test('AC-5.3a — empty state renders when engagements list is empty', async ({
page,
context,
}) => {
await seedTokenInStorage(context, redteamToken);
const tok = await adminToken();
await deleteAllEngagements(tok);
await page.goto('/engagements');
await expect(page.getByRole('heading', { name: /no engagements yet/i })).toBeVisible();
});
test('AC-5.3b — loading state renders while engagements list is in-flight', async ({
page,
context,
}) => {
await seedTokenInStorage(context, redteamToken);
// Stall the list endpoint so the LoadingState becomes visible.
const handler = async (route: import('@playwright/test').Route) => {
const url = route.request().url();
if (/\/api\/engagements(\?.*)?$/.test(url)) {
await new Promise((r) => setTimeout(r, 1500));
}
try {
await route.continue();
} catch {
/* route may have been torn down by the time we resume */
}
};
await page.route('**/api/engagements**', handler);
const navPromise = page.goto('/engagements');
await expect(page.getByText(/loading engagements/i)).toBeVisible({ timeout: 3_000 });
await navPromise;
await page.unroute('**/api/engagements**', handler);
});
test('AC-5.3c — error state renders when the engagements list returns 500', async ({
page,
context,
}) => {
await seedTokenInStorage(context, redteamToken);
const handler = async (route: import('@playwright/test').Route) => {
const url = route.request().url();
if (/\/api\/engagements(\?.*)?$/.test(url)) {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Forced failure' }),
});
return;
}
try {
await route.continue();
} catch {
/* route teardown race — harmless */
}
};
await page.route('**/api/engagements**', handler);
await page.goto('/engagements');
await expect(page.getByTestId('error-state')).toBeVisible({ timeout: 8_000 });
await expect(page.getByTestId('error-state')).toContainText(/forced failure/i);
await expect(page.getByRole('button', { name: /retry/i })).toBeVisible();
await page.unroute('**/api/engagements**', handler);
});
});