waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ app e2e axe-hilfe.ts

3,5 KB Rohdatei
app/e2e/axe-hilfe.ts — 93 Zeilen
1 import { readFileSync } from 'node:fs';
2 import { createRequire } from 'node:module';
3 import { dirname, join } from 'node:path';
4
5 import type { Page } from '@playwright/test';
6 import type { AxeResults, run as axeRun, RunOptions } from 'axe-core';
7
8 /**
9 * axe-core in einer Electron-Seite ausführen.
10 *
11 * WARUM NICHT `@axe-core/playwright`?
12 * Dieses Paket ruft intern `browserContext.newPage()` auf, um die
13 * axe-Quelle zu serialisieren. Electron unterstützt das nicht – der
14 * Aufruf endet in `Protocol error (Target.createTarget): Not supported`.
15 * Deshalb wird hier die Quelle direkt injiziert.
16 *
17 * Die Injektion läuft über `page.evaluate()` (CDP `Runtime.evaluate`) statt
18 * über `addScriptTag`. Nur so bleibt die strenge Produktions-CSP
19 * (`script-src 'self'`) unangetastet – wir prüfen die Seite also genau in
20 * der Konfiguration, die auch ausgeliefert wird.
21 */
22
23 const require_ = createRequire(__filename);
24 const axeQuellPfad = join(dirname(require_.resolve('axe-core')), 'axe.min.js');
25 const axeQuelle = readFileSync(axeQuellPfad, 'utf8');
26
27 /*
28 Der Regelsatz steht in src/shared/wcag.ts – eine Quelle für Unit- und
29 E2E-Ebene. Hier wird er nur weitergereicht, damit die Suiten unter e2e/
30 ihn nicht quer durch den Baum importieren müssen.
31 */
32 export { WCAG_TAGS } from '../src/shared/wcag';
33
34 /** Ein einzelner Befund in lesbarer Form (statt eines rohen Objekt-Dumps). */
35 export interface Befund {
36 readonly regel: string;
37 readonly wirkung: string;
38 readonly hilfe: string;
39 readonly betroffen: readonly string[];
40 }
41
42 async function axeSicherstellen(seite: Page): Promise<void> {
43 const schonGeladen = await seite.evaluate(
44 () => typeof (globalThis as Record<string, unknown>)['axe'] !== 'undefined',
45 );
46 if (!schonGeladen) {
47 await seite.evaluate(axeQuelle);
48 }
49 }
50
51 /** Führt axe-core auf der gesamten Seite aus. */
52 export async function axePruefen(seite: Page, optionen: RunOptions): Promise<AxeResults> {
53 await axeSicherstellen(seite);
54
55 return seite.evaluate(async (opts: RunOptions) => {
56 const axe = (globalThis as unknown as { axe: { run: typeof axeRun } }).axe;
57 return axe.run(document, opts);
58 }, optionen);
59 }
60
61 function verdichten(ergebnisse: AxeResults['violations'], nurRegeln?: readonly string[]): Befund[] {
62 return ergebnisse
63 .filter((v) => !nurRegeln || nurRegeln.includes(v.id))
64 .map((v) => ({
65 regel: v.id,
66 wirkung: v.impact ?? 'unbekannt',
67 hilfe: v.help,
68 betroffen: v.nodes.map((n) => n.target.join(' ')),
69 }));
70 }
71
72 /** Verdichtet Verstöße zu einer Liste, die im Fehlerfall gut lesbar ist. */
73 export function befunde(ergebnis: AxeResults, nurRegeln?: readonly string[]): Befund[] {
74 return verdichten(ergebnis.violations, nurRegeln);
75 }
76
77 /**
78 * Die Fälle, die axe **nicht entscheiden konnte** (`incomplete`).
79 *
80 * Diese Liste ist bewusst getrennt und bricht keinen Lauf: axe legt hier ab,
81 * was es nicht sicher beurteilen kann – etwa Farbkontrast über einem
82 * Verlaufshintergrund. Sie zu einem Fehler zu machen hieße, Rauschen zum
83 * Befund zu erklären.
84 *
85 * Sie aber wortlos zu verschweigen wäre schlimmer: Vorher las
86 * {@link befunde} ausschließlich `violations`, und alles Unentschiedene
87 * verschwand spurlos. Ein Lauf meldete „keine Befunde", während axe in
88 * Wahrheit gesagt hatte: „kann ich nicht beurteilen." Wer den Anspruch
89 * WCAG 2.2 AA erhebt, muss diese Fälle wenigstens sehen.
90 */
91 export function unentschieden(ergebnis: AxeResults, nurRegeln?: readonly string[]): Befund[] {
92 return verdichten(ergebnis.incomplete, nurRegeln);
93 }