waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
| 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 | } |