waffensachkunde

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

/ app e2e ueberschriften.spec.ts

6,6 KB Rohdatei
app/e2e/ueberschriften.spec.ts — 188 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('Über diese Software', async () => {
115 await zumStart();
116 await fenster.getByRole('button', { name: /Lizenzen und Herkunft anzeigen/u }).click();
117 await fenster.waitForTimeout(400);
118 pruefen('Über diese Software', await ueberschriften());
119 });
120
121 test('Fragen durchsuchen', async () => {
122 /* Mit Trefferliste geprüft, nicht leer: Jede Trefferkarte trägt eine h2,
123 und genau dort könnte die Hierarchie brechen. */
124 await zumStart();
125 await fenster.getByRole('button', { name: /Fragen durchsuchen/u }).click();
126 await fenster.getByRole('heading', { name: 'Fragen durchsuchen', level: 1 }).waitFor();
127 await fenster.getByLabel('Suchbegriff').fill('Anscheinswaffe');
128 await fenster.waitForTimeout(300);
129 pruefen('Fragen durchsuchen', await ueberschriften());
130 });
131
132 test('Kapitelwahl', async () => {
133 await zumStart();
134 await fenster.getByRole('button', { name: /Kapitel wählen/u }).click();
135 await fenster.waitForTimeout(300);
136 pruefen('Kapitelwahl', await ueberschriften());
137 });
138
139 test('Prüfungswahl', async () => {
140 await zumStart();
141 await fenster.getByRole('button', { name: /Prüfungssimulation vorbereiten/u }).click();
142 await fenster.waitForTimeout(400);
143 pruefen('Prüfungswahl', await ueberschriften());
144 });
145
146 test('Lernsitzung', async () => {
147 await zumStart();
148 await fenster.getByRole('button', { name: /Weiterlernen/u }).click();
149 await fenster.waitForTimeout(500);
150 pruefen('Lernsitzung', await ueberschriften());
151 });
152
153 test('Lernsitzung nach der Antwort', async () => {
154 /*
155 Gesucht ist irgendeine Auswahlfrage, nicht ausdruecklich eine mit
156 Einfachauswahl: Die Sitzung wird zufaellig zusammengestellt, und ob
157 unter den ersten Fragen eine Einfachauswahl ist, entscheidet der
158 Zufall. Der Test hat deshalb sporadisch versagt, ohne dass etwas
159 kaputt war. Fuer die Ueberschriftenstruktur macht es keinen
160 Unterschied, ob angekreuzt oder angehakt wird.
161 */
162 const auswahlfeld = fenster.getByRole('radio').or(fenster.getByRole('checkbox'));
163
164 for (let i = 0; i < 20; i++) {
165 if ((await auswahlfeld.count()) > 0) {
166 break;
167 }
168 await fenster
169 .getByRole('button', { name: /Musterantwort anzeigen|^Antwort bestätigen$/u })
170 .first()
171 .click();
172 const b = fenster.getByRole('button', { name: /^(Gewusst|Sicher gewusst)$/u });
173 if ((await b.count()) > 0) {
174 await b.first().click();
175 }
176 const w = fenster.getByRole('button', { name: /^Nächste Frage$/u });
177 if ((await w.count()) === 0) {
178 break;
179 }
180 await w.first().click();
181 await fenster.waitForTimeout(150);
182 }
183 await auswahlfeld.first().click();
184 await fenster.getByRole('button', { name: 'Antwort bestätigen' }).click();
185 await fenster.waitForTimeout(300);
186 pruefen('Lernsitzung nach der Antwort', await ueberschriften());
187 });
188 });