waffensachkunde

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

/ app e2e ueberschriften.spec.ts

6,9 KB Rohdatei
app/e2e/ueberschriften.spec.ts — 195 Zeilen
1 /**
2 * Die Überschriftenstruktur jeder Ansicht.
3 *
4 * Wer mit einem Screenreader arbeitet, springt nach Überschriftenebenen.
5 * Dafür muss in jeder Ansicht dasselbe gelten: genau eine h1, die benennt,
6 * worum es hier geht, und darunter keine übersprungene Ebene.
7 *
8 * Vorher war es uneinheitlich – in der Lernsitzung war die Frage die h1, in
9 * den Nebenansichten der Anwendungsname, und die eigentliche
10 * Ansichtsüberschrift steckte eine Ebene tiefer. Mal landete man an der
11 * Sache, mal beim Programmnamen.
12 */
13
14 import { type ElectronApplication, type Page } from '@playwright/test';
15
16 import { amStart, appStarten, bauPruefen } from './electron-hilfe';
17 import { expect, test } from './konsolenwache';
18
19 let app: ElectronApplication | undefined;
20 let fenster: Page;
21
22 test.beforeAll(async () => {
23 // Fehlender oder veralteter Bau ist ein Fehler, kein Grund zum Schweigen.
24 bauPruefen();
25 ({ app, fenster } = await appStarten());
26 });
27
28 test.afterAll(async () => {
29 await app?.close();
30 });
31
32 interface Befund {
33 readonly ebenen: number[];
34 readonly texte: string[];
35 }
36
37 async function ueberschriften(): Promise<Befund> {
38 return fenster.evaluate(() => {
39 const alle = [...document.querySelectorAll('h1, h2, h3, h4, h5, h6')].filter((h) => {
40 /* Zugeklappte <details> zählen mit: Ein Screenreader führt sie in der
41 Überschriftenliste, und die Ebene gilt dort genauso. Wirklich
42 verborgene Elemente nicht. */
43 return h.closest('[hidden]') === null;
44 });
45 return {
46 ebenen: alle.map((h) => Number(h.tagName.slice(1))),
47 texte: alle.map((h) => h.textContent.trim().slice(0, 40)),
48 };
49 });
50 }
51
52 const ANWENDUNGSNAME = 'Waffensachkunde – Lernsoftware';
53
54 /** Genau eine h1, Beginn bei 1, kein Sprung nach unten. */
55 function pruefen(name: string, befund: Befund, benenntAnsicht = true): void {
56 const einsen = befund.ebenen.filter((e) => e === 1).length;
57 const liste = befund.ebenen
58 .map((e, i) => `h${String(e)} „${befund.texte[i] ?? ''}“`)
59 .join('\n ');
60
61 expect(einsen, `${name}: ${String(einsen)} h1 statt genau einer.\n ${liste}`).toBe(1);
62 expect(befund.ebenen[0], `${name}: beginnt nicht mit h1.\n ${liste}`).toBe(1);
63
64 /*
65 Die h1 muss die Ansicht benennen, nicht das Programm. Sonst ist die
66 Struktur zwar formal in Ordnung, hilft aber niemandem: Wer per
67 Überschrift navigiert, landete in jeder Ansicht bei derselben,
68 nichtssagenden Antwort auf die Frage „wo bin ich?“.
69 */
70 if (benenntAnsicht) {
71 expect(befund.texte[0], `${name}: Die h1 nennt nur den Anwendungsnamen.`).not.toBe(
72 ANWENDUNGSNAME,
73 );
74 }
75
76 let vorige = 1;
77 for (const [i, ebene] of befund.ebenen.entries()) {
78 expect(
79 ebene,
80 `${name}: Sprung auf h${String(ebene)} bei „${befund.texte[i] ?? ''}“.\n ${liste}`,
81 ).toBeLessThanOrEqual(vorige + 1);
82 vorige = ebene;
83 }
84 }
85
86 async function zumStart(): Promise<void> {
87 for (let i = 0; i < 4; i++) {
88 if (await amStart(fenster)) {
89 return;
90 }
91 const zurueck = fenster.getByRole('button', { name: /^(Zum Start|Sitzung beenden)$/u });
92 if ((await zurueck.count()) === 0) {
93 return;
94 }
95 await zurueck.first().click();
96 await fenster.waitForTimeout(250);
97 }
98 }
99
100 test.describe('Überschriften je Ansicht', () => {
101 test('Startbildschirm', async () => {
102 await zumStart();
103 /* Hier ist der Anwendungsname die richtige Antwort auf „wo bin ich?“. */
104 pruefen('Startbildschirm', await ueberschriften(), false);
105 });
106
107 test('Glossar', async () => {
108 await zumStart();
109 await fenster.getByRole('button', { name: /Fachbegriffe nachschlagen/u }).click();
110 await fenster.getByRole('heading', { name: 'Glossar', exact: true }).waitFor();
111 pruefen('Glossar', await ueberschriften());
112 });
113
114 test('Gesetze lesen', async () => {
115 await zumStart();
116 await fenster.getByRole('button', { name: /In den Gesetzen nachlesen/u }).click();
117 await fenster.getByRole('heading', { name: 'Gesetze lesen', level: 1 }).waitFor();
118 pruefen('Gesetze lesen', await ueberschriften());
119 });
120
121 test('Über diese Software', async () => {
122 await zumStart();
123 await fenster.getByRole('button', { name: /Lizenzen und Herkunft anzeigen/u }).click();
124 await fenster.waitForTimeout(400);
125 pruefen('Über diese Software', await ueberschriften());
126 });
127
128 test('Fragen durchsuchen', async () => {
129 /* Mit Trefferliste geprüft, nicht leer: Jede Trefferkarte trägt eine h2,
130 und genau dort könnte die Hierarchie brechen. */
131 await zumStart();
132 await fenster.getByRole('button', { name: /Fragen durchsuchen/u }).click();
133 await fenster.getByRole('heading', { name: 'Fragen durchsuchen', level: 1 }).waitFor();
134 await fenster.getByLabel('Suchbegriff').fill('Anscheinswaffe');
135 await fenster.waitForTimeout(300);
136 pruefen('Fragen durchsuchen', await ueberschriften());
137 });
138
139 test('Kapitelwahl', async () => {
140 await zumStart();
141 await fenster.getByRole('button', { name: /Kapitel wählen/u }).click();
142 await fenster.waitForTimeout(300);
143 pruefen('Kapitelwahl', await ueberschriften());
144 });
145
146 test('Prüfungswahl', async () => {
147 await zumStart();
148 await fenster.getByRole('button', { name: /Prüfungssimulation vorbereiten/u }).click();
149 await fenster.waitForTimeout(400);
150 pruefen('Prüfungswahl', await ueberschriften());
151 });
152
153 test('Lernsitzung', async () => {
154 await zumStart();
155 await fenster.getByRole('button', { name: /Weiterlernen/u }).click();
156 await fenster.waitForTimeout(500);
157 pruefen('Lernsitzung', await ueberschriften());
158 });
159
160 test('Lernsitzung nach der Antwort', async () => {
161 /*
162 Gesucht ist irgendeine Auswahlfrage, nicht ausdruecklich eine mit
163 Einfachauswahl: Die Sitzung wird zufaellig zusammengestellt, und ob
164 unter den ersten Fragen eine Einfachauswahl ist, entscheidet der
165 Zufall. Der Test hat deshalb sporadisch versagt, ohne dass etwas
166 kaputt war. Fuer die Ueberschriftenstruktur macht es keinen
167 Unterschied, ob angekreuzt oder angehakt wird.
168 */
169 const auswahlfeld = fenster.getByRole('radio').or(fenster.getByRole('checkbox'));
170
171 for (let i = 0; i < 20; i++) {
172 if ((await auswahlfeld.count()) > 0) {
173 break;
174 }
175 await fenster
176 .getByRole('button', { name: /Musterantwort anzeigen|^Antwort bestätigen$/u })
177 .first()
178 .click();
179 const b = fenster.getByRole('button', { name: /^(Gewusst|Sicher gewusst)$/u });
180 if ((await b.count()) > 0) {
181 await b.first().click();
182 }
183 const w = fenster.getByRole('button', { name: /^Nächste Frage$/u });
184 if ((await w.count()) === 0) {
185 break;
186 }
187 await w.first().click();
188 await fenster.waitForTimeout(150);
189 }
190 await auswahlfeld.first().click();
191 await fenster.getByRole('button', { name: 'Antwort bestätigen' }).click();
192 await fenster.waitForTimeout(300);
193 pruefen('Lernsitzung nach der Antwort', await ueberschriften());
194 });
195 });