waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
/ app e2e barrierefreiheit.spec.ts
| 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 | }); |