waffensachkunde

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

/ app e2e barrierefreiheit.spec.ts

5,5 KB Rohdatei
app/e2e/barrierefreiheit.spec.ts — 134 Zeilen
1 /**
2 * Grundprüfungen der Barrierefreiheit am Startbildschirm, im echten Fenster.
3 *
4 * ## Was diese Datei prüft
5 *
6 * Drei Dinge, die keine Ansichtsrundreise brauchen:
7 *
8 * 1. einen axe-Rauchtest am Startbildschirm gegen den vollen Satz WCAG 2.2 AA
9 * – die schnellste Antwort auf „ist überhaupt etwas kaputt?“,
10 * 2. den **AAA**-Zielwert 7:1 (`color-contrast-enhanced`) als Bericht: Er wird
11 * erhoben und ausgegeben, aber nicht erzwungen. AA ist das
12 * Abnahmekriterium, AAA das Ziel,
13 * 3. Struktureigenschaften, die axe nicht misst: die drei Landmarken, ein
14 * zugänglicher Name an **jeder** vorhandenen `<section>` und ein sichtbarer
15 * Fokusring nach echten Tabulatorschritten.
16 *
17 * ## Was diese Datei ausdrücklich NICHT prüft
18 *
19 * Die Abdeckung **je Ansicht und je Farbschema** steht vollständig in
20 * `barrierefreiheit-ansichten.spec.ts`. Hier lief früher zusätzlich eine
21 * Schleife über die drei Schemata – aber nur mit der Einzelregel
22 * `color-contrast` und ohne den Startbildschirm je zu verlassen. Das las sich
23 * nach „alle drei Farbschemata geprüft“ und war die Messung einer einzigen
24 * Ansicht. Die Schleife ist dorthin gewandert, wo sie etwas aussagt; hier
25 * stünde sie nur noch doppelt.
26 *
27 * Ein grüner Lauf heißt auch hier nicht „barrierefrei“: Automatische Prüfung
28 * entscheidet nur, was sich maschinell entscheiden lässt. Die Szenarien an
29 * NVDA, JAWS und VoiceOver in `docs/barrierefreiheit-pruefplan.md` ersetzt
30 * diese Suite nicht.
31 */
32
33 import { type ElectronApplication, type Page } from '@playwright/test';
34
35 import { appStarten, bauPruefen, themaSetzen } from './electron-hilfe';
36 import { axePruefen, befunde, WCAG_TAGS } from './axe-hilfe';
37 import { expect, test } from './konsolenwache';
38
39 // `undefined`, solange der beforeAll-Hook übersprungen wurde.
40 let app: ElectronApplication | undefined;
41 let fenster: Page;
42
43 test.beforeAll(async () => {
44 // Fehlender oder veralteter Bau ist ein Fehler, kein Grund zum Schweigen.
45 bauPruefen();
46 ({ app, fenster } = await appStarten());
47 });
48
49 test.afterAll(async () => {
50 await app?.close();
51 });
52
53 test.describe('axe-core Rauchtest', () => {
54 test('Startbildschirm ist frei von WCAG-A/AA-Verstößen', async () => {
55 const ergebnis = await axePruefen(fenster, {
56 runOnly: { type: 'tag', values: [...WCAG_TAGS] },
57 });
58
59 expect(befunde(ergebnis)).toEqual([]);
60 });
61
62 test('meldet den AAA-Zielwert (7:1) als Information, ohne den Lauf zu brechen', async () => {
63 await themaSetzen(fenster, 'hell');
64
65 const ergebnis = await axePruefen(fenster, {
66 runOnly: { type: 'rule', values: ['color-contrast-enhanced'] },
67 });
68
69 const offen = befunde(ergebnis, ['color-contrast-enhanced']);
70 if (offen.length > 0) {
71 console.info(
72 `[a11y] AAA-Zielwert noch nicht überall erreicht (${String(offen.length)} Stellen):`,
73 JSON.stringify(offen, null, 2),
74 );
75 }
76
77 // Bewusst keine Zusicherung: AAA ist Zielwert, nicht Abnahmekriterium.
78 expect(Array.isArray(offen)).toBe(true);
79 });
80 });
81
82 test.describe('Strukturelle Barrierefreiheit', () => {
83 test('bietet die erwarteten Landmarken', async () => {
84 await expect(fenster.getByRole('banner')).toBeVisible();
85 await expect(fenster.getByRole('main')).toBeVisible();
86 await expect(fenster.getByRole('contentinfo')).toBeVisible();
87 });
88
89 test('jede Sektion trägt einen zugänglichen Namen', async () => {
90 /* Eine <section> wird nur dann als Landmarke („region“) an den Screenreader
91 gemeldet, wenn sie benannt ist – sonst ist sie für die Navigation
92 wertlos. Geprüft wird deshalb jede vorhandene Sektion, nicht eine feste
93 Anzahl: So fällt auch eine später hinzugefügte Sektion ohne Namen auf. */
94 const namenlose = await fenster.evaluate(() =>
95 [...document.querySelectorAll('section')]
96 .filter((s) => !s.getAttribute('aria-labelledby') && !s.getAttribute('aria-label'))
97 .map((s) => s.className || s.textContent.trim().slice(0, 40) || '(ohne Kennzeichen)'),
98 );
99
100 expect(namenlose, `Sektionen ohne zugänglichen Namen: ${namenlose.join(' | ')}`).toEqual([]);
101 expect(await fenster.locator('section').count()).toBeGreaterThan(0);
102 });
103
104 test('zeigt bei Tastaturfokus einen sichtbaren Fokusindikator', async () => {
105 /* Der Ring hängt an `:focus-visible`. Chromium wendet das nur an, wenn
106 zuletzt die Tastatur benutzt wurde – ein programmatisches `.focus()`
107 nach einem Mausklick reicht nicht. Deshalb echte Tab-Schritte.
108 Der erste Tabstopp ist der Skip-Link; geprüft wird der zweite, also das
109 erste reguläre Bedienelement. Welches das ist, hängt vom Funktionsumfang
110 ab – der Test stellt deshalb nur sicher, dass es überhaupt eines gibt. */
111 await fenster.locator('body').press('Tab');
112 await fenster.keyboard.press('Tab');
113
114 const fokussiert = fenster.locator(':focus');
115 await expect(fokussiert).toBeVisible();
116
117 const stil = await fokussiert.evaluate((element) => {
118 const berechnet = getComputedStyle(element);
119 return {
120 outlineStyle: berechnet.outlineStyle,
121 outlineWidth: berechnet.outlineWidth,
122 outlineColor: berechnet.outlineColor,
123 boxShadow: berechnet.boxShadow,
124 };
125 });
126
127 expect(stil.outlineStyle).toBe('solid');
128 expect(Number.parseFloat(stil.outlineWidth)).toBeGreaterThanOrEqual(2);
129 expect(stil.outlineColor).not.toBe('transparent');
130 // Zweiter, gegenfarbiger Ring aus `box-shadow` – sorgt für Sichtbarkeit
131 // auf hellem UND dunklem Untergrund.
132 expect(stil.boxShadow).not.toBe('none');
133 });
134 });