theme-contrast.test.ts parses tokens.css and asserts every real UI colour pairing (4.5:1 text, 3:1 UI) for BOTH palettes, so palette drift fails unit tests instead of review. theme.test.ts covers resolve/apply logic (Node >= 22 ships a shadowing undefined localStorage global — the test brings its own in-memory storage). The a11y pack now runs its four scans in light AND dark via emulateMedia; the new theme pack exercises the three-way switch end to end (instant apply, reload persistence, live OS follow in system mode, override beats OS). ADR 0018 records the theming model broadly: modes now, accent themes by derivation later. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRtCnB3uLdQtFmvp9HXcRX
89 lines
3.6 KiB
TypeScript
89 lines
3.6 KiB
TypeScript
import AxeBuilder from '@axe-core/playwright';
|
||
import { expect, test, type Page } from '@playwright/test';
|
||
|
||
import { contextForUser } from './helpers';
|
||
|
||
/**
|
||
* A11y-Smoke-Pack (issue #171): axe-core-Scan der Kern-Oberflächen gegen
|
||
* WCAG 2.1 A/AA. Regressionsschutz für das Audit vom 21.07.2026 (Bericht im
|
||
* Workspace, Befunde A11Y-001…024) — die Screens hier waren nach den Fixes
|
||
* der Issues #162–#170 verletzungsfrei; jede neue Verletzung bricht den
|
||
* Build. Best-Practice-Regeln (axe-Tag best-practice) prüfen wir hier
|
||
* bewusst NICHT, nur normative WCAG-Kriterien.
|
||
*
|
||
* Seit issue #180 läuft jeder Scan in BEIDEN Farbschemata: emulateMedia
|
||
* setzt prefers-color-scheme, theme-init.js löst den Default „System“ zum
|
||
* konkreten data-theme auf, axe misst dann die echten Dark-Token-Farben.
|
||
*/
|
||
|
||
const BASE = process.env.E2E_BASE_URL ?? 'http://localhost:5173';
|
||
const TAGS = ['wcag2a', 'wcag21a', 'wcag2aa', 'wcag21aa'];
|
||
const SCHEMES = ['light', 'dark'] as const;
|
||
|
||
/** Bewusst tolerierte Regel-IDs — nur mit Begründung ergänzen. */
|
||
const ALLOWED_RULES: string[] = [];
|
||
|
||
async function expectClean(page: Page, label: string): Promise<void> {
|
||
const results = await new AxeBuilder({ page }).withTags(TAGS).analyze();
|
||
const violations = results.violations.filter((v) => !ALLOWED_RULES.includes(v.id));
|
||
expect(
|
||
violations.map((v) => ({
|
||
rule: v.id,
|
||
impact: v.impact,
|
||
help: v.help,
|
||
nodes: v.nodes.slice(0, 5).map((n) => n.target),
|
||
})),
|
||
`axe-Verletzungen auf ${label}`,
|
||
).toEqual([]);
|
||
}
|
||
|
||
for (const scheme of SCHEMES) {
|
||
test.describe(`${scheme} scheme`, () => {
|
||
test(`login page passes the axe WCAG A/AA scan (${scheme})`, async ({ page }) => {
|
||
await page.emulateMedia({ colorScheme: scheme });
|
||
await page.goto('/login');
|
||
await page.waitForLoadState('networkidle');
|
||
await expectClean(page, `/login (${scheme})`);
|
||
});
|
||
|
||
test(`reading and editing a page passes the axe WCAG A/AA scan (${scheme})`, async ({
|
||
browser,
|
||
}) => {
|
||
const context = await contextForUser(browser, BASE, 'fixture-user');
|
||
const page = await context.newPage();
|
||
await page.emulateMedia({ colorScheme: scheme });
|
||
await page.goto('/p/content-fixtures/every-element');
|
||
await page.waitForLoadState('networkidle');
|
||
await expectClean(page, `Lesemodus every-element (${scheme})`);
|
||
|
||
await page.locator('.editor-page__mode-toggle').click();
|
||
await page.locator('.ProseMirror[contenteditable="true"]').waitFor({ timeout: 10_000 });
|
||
await page.waitForTimeout(500);
|
||
await expectClean(page, `Editor every-element (${scheme})`);
|
||
await context.close();
|
||
});
|
||
|
||
test(`user settings pass the axe WCAG A/AA scan (${scheme})`, async ({ browser }) => {
|
||
const context = await contextForUser(browser, BASE, 'fixture-user');
|
||
const page = await context.newPage();
|
||
await page.emulateMedia({ colorScheme: scheme });
|
||
await page.goto('/settings');
|
||
await page.waitForLoadState('networkidle');
|
||
await expectClean(page, `/settings (${scheme})`);
|
||
await context.close();
|
||
});
|
||
|
||
test(`admin area passes the axe WCAG A/AA scan (${scheme})`, async ({ browser }) => {
|
||
const context = await contextForUser(browser, BASE, 'fixture-admin');
|
||
const page = await context.newPage();
|
||
await page.emulateMedia({ colorScheme: scheme });
|
||
await page.goto('/admin');
|
||
await page.waitForLoadState('networkidle');
|
||
// Personenliste sichtbar, inkl. der Icon-Aktionen (issue #175).
|
||
await page.locator('.user-manager__table .user-row').first().waitFor();
|
||
await expectClean(page, `/admin (${scheme})`);
|
||
await context.close();
|
||
});
|
||
});
|
||
}
|