import { readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; import type { Page } from '@playwright/test'; import type { AxeResults, run as axeRun, RunOptions } from 'axe-core'; /** * axe-core in einer Electron-Seite ausführen. * * WARUM NICHT `@axe-core/playwright`? * Dieses Paket ruft intern `browserContext.newPage()` auf, um die * axe-Quelle zu serialisieren. Electron unterstützt das nicht – der * Aufruf endet in `Protocol error (Target.createTarget): Not supported`. * Deshalb wird hier die Quelle direkt injiziert. * * Die Injektion läuft über `page.evaluate()` (CDP `Runtime.evaluate`) statt * über `addScriptTag`. Nur so bleibt die strenge Produktions-CSP * (`script-src 'self'`) unangetastet – wir prüfen die Seite also genau in * der Konfiguration, die auch ausgeliefert wird. */ const require_ = createRequire(__filename); const axeQuellPfad = join(dirname(require_.resolve('axe-core')), 'axe.min.js'); const axeQuelle = readFileSync(axeQuellPfad, 'utf8'); /* Der Regelsatz steht in src/shared/wcag.ts – eine Quelle für Unit- und E2E-Ebene. Hier wird er nur weitergereicht, damit die Suiten unter e2e/ ihn nicht quer durch den Baum importieren müssen. */ export { WCAG_TAGS } from '../src/shared/wcag'; /** Ein einzelner Befund in lesbarer Form (statt eines rohen Objekt-Dumps). */ export interface Befund { readonly regel: string; readonly wirkung: string; readonly hilfe: string; readonly betroffen: readonly string[]; } async function axeSicherstellen(seite: Page): Promise { const schonGeladen = await seite.evaluate( () => typeof (globalThis as Record)['axe'] !== 'undefined', ); if (!schonGeladen) { await seite.evaluate(axeQuelle); } } /** Führt axe-core auf der gesamten Seite aus. */ export async function axePruefen(seite: Page, optionen: RunOptions): Promise { await axeSicherstellen(seite); return seite.evaluate(async (opts: RunOptions) => { const axe = (globalThis as unknown as { axe: { run: typeof axeRun } }).axe; return axe.run(document, opts); }, optionen); } function verdichten(ergebnisse: AxeResults['violations'], nurRegeln?: readonly string[]): Befund[] { return ergebnisse .filter((v) => !nurRegeln || nurRegeln.includes(v.id)) .map((v) => ({ regel: v.id, wirkung: v.impact ?? 'unbekannt', hilfe: v.help, betroffen: v.nodes.map((n) => n.target.join(' ')), })); } /** Verdichtet Verstöße zu einer Liste, die im Fehlerfall gut lesbar ist. */ export function befunde(ergebnis: AxeResults, nurRegeln?: readonly string[]): Befund[] { return verdichten(ergebnis.violations, nurRegeln); } /** * Die Fälle, die axe **nicht entscheiden konnte** (`incomplete`). * * Diese Liste ist bewusst getrennt und bricht keinen Lauf: axe legt hier ab, * was es nicht sicher beurteilen kann – etwa Farbkontrast über einem * Verlaufshintergrund. Sie zu einem Fehler zu machen hieße, Rauschen zum * Befund zu erklären. * * Sie aber wortlos zu verschweigen wäre schlimmer: Vorher las * {@link befunde} ausschließlich `violations`, und alles Unentschiedene * verschwand spurlos. Ein Lauf meldete „keine Befunde", während axe in * Wahrheit gesagt hatte: „kann ich nicht beurteilen." Wer den Anspruch * WCAG 2.2 AA erhebt, muss diese Fälle wenigstens sehen. */ export function unentschieden(ergebnis: AxeResults, nurRegeln?: readonly string[]): Befund[] { return verdichten(ergebnis.incomplete, nurRegeln); }