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 { 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(); }); }); }