waffensachkunde

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

/ app e2e vorlesen.spec.ts

8,3 KB Rohdatei
app/e2e/vorlesen.spec.ts — 207 Zeilen
1 /**
2 * Die Sprachausgabe im echten Electron.
3 *
4 * Die Einheitentests bilden `speechSynthesis` nach; hier läuft die richtige
5 * Engine. Genau dort steckte die Überraschung: `getVoices()` gibt unter
6 * Windows eine leere Liste zurück und `voiceschanged` bleibt aus, **bis zum
7 * ersten `speak()`**. Gemessen an dieser Maschine sind danach drei deutsche
8 * Stimmen da (Hedda, Katja, Stefan). Eine Umsetzung, die auf die Liste
9 * wartet, bevor sie etwas anbietet, wartet also ewig.
10 *
11 * Was hier geprüft wird, hängt trotzdem nicht davon ab, ob dieses Gerät eine
12 * Stimme besitzt: Beobachtet wird der Auftrag an die Engine, nicht der Klang.
13 * Ob wirklich zu hören ist, was hier losgeschickt wird, kann nur ein Mensch
14 * beurteilen – dafür steht Schritt S9 im Prüfplan.
15 */
16
17 import { type ElectronApplication, type Page } from '@playwright/test';
18
19 import { amStart, appStarten, bauPruefen } from './electron-hilfe';
20 import { expect, test } from './konsolenwache';
21
22 let app: ElectronApplication | undefined;
23 let fenster: Page;
24
25 test.beforeAll(async () => {
26 // Fehlender oder veralteter Bau ist ein Fehler, kein Grund zum Schweigen.
27 bauPruefen();
28 ({ app, fenster } = await appStarten());
29 });
30
31 test.afterAll(async () => {
32 await app?.close();
33 });
34
35 /** Hängt sich vor `speak()` und `cancel()` und schreibt beides mit. */
36 async function mithoeren(seite: Page): Promise<void> {
37 await seite.evaluate(() => {
38 const fenster = window as unknown as {
39 __gesprochen?: { text: string; lang: string }[];
40 __abbrueche?: number;
41 };
42 fenster.__gesprochen = [];
43 fenster.__abbrueche = 0;
44
45 const echt = window.speechSynthesis.speak.bind(window.speechSynthesis);
46 window.speechSynthesis.speak = (aeusserung: SpeechSynthesisUtterance): void => {
47 fenster.__gesprochen?.push({ text: aeusserung.text, lang: aeusserung.lang });
48 echt(aeusserung);
49 };
50
51 const echtesAbbrechen = window.speechSynthesis.cancel.bind(window.speechSynthesis);
52 window.speechSynthesis.cancel = (): void => {
53 fenster.__abbrueche = (fenster.__abbrueche ?? 0) + 1;
54 echtesAbbrechen();
55 };
56 });
57 }
58
59 /**
60 * Zurück auf den Startbildschirm.
61 *
62 * Die Tests dieser Datei teilen sich eine Anwendung und laufen der Reihe
63 * nach; die vorigen lassen sie in einer Sitzung stehen. Wer den Schalter für
64 * die Sprachausgabe braucht, muss also erst hierher zurück.
65 */
66 async function zumStart(seite: Page): Promise<void> {
67 for (let i = 0; i < 4; i++) {
68 if (await amStart(seite)) {
69 return;
70 }
71 const zurueck = seite.getByRole('button', { name: /^(Zum Start|Sitzung beenden)$/u });
72 if ((await zurueck.count()) === 0) {
73 return;
74 }
75 await zurueck.first().click();
76 await seite.waitForTimeout(250);
77 }
78 }
79
80 async function abbrueche(seite: Page): Promise<number> {
81 return seite.evaluate(() => (window as unknown as { __abbrueche?: number }).__abbrueche ?? 0);
82 }
83
84 async function gesprochenes(seite: Page): Promise<{ text: string; lang: string }[]> {
85 return seite.evaluate(
86 () =>
87 (window as unknown as { __gesprochen?: { text: string; lang: string }[] }).__gesprochen ?? [],
88 );
89 }
90
91 test.describe('Vorlesen', () => {
92 test('bietet die Sprachausgabe an, obwohl noch keine Stimme gemeldet ist', async () => {
93 /* Der Fallstrick in Person: Vor dem ersten `speak()` ist die Liste leer.
94 Der Schalter muss trotzdem bedienbar sein. */
95 const stimmenVorher = await fenster.evaluate(() => window.speechSynthesis.getVoices().length);
96 expect(stimmenVorher).toBe(0);
97
98 const schalter = fenster.getByRole('checkbox', {
99 name: 'Fragen und Antworten vorlesen lassen',
100 });
101
102 await expect(schalter).toBeEnabled();
103 await expect(schalter).not.toBeChecked();
104 });
105
106 test('liest die Frage vor und lässt sich wieder anhalten', async () => {
107 await mithoeren(fenster);
108
109 await fenster
110 .getByRole('checkbox', { name: 'Fragen und Antworten vorlesen lassen' })
111 .setChecked(true);
112
113 await fenster.getByRole('button', { name: /Weiterlernen/u }).click();
114 await expect(fenster.getByRole('heading', { level: 1, name: /, Frage \d/u })).toBeVisible();
115
116 const vorlesen = fenster.getByRole('button', { name: /Frage vorlesen/u });
117 await expect(vorlesen).toBeVisible();
118 await vorlesen.click();
119
120 const stuecke = await gesprochenes(fenster);
121 expect(stuecke.length).toBeGreaterThan(0);
122 expect(stuecke.every((stueck) => stueck.lang === 'de-DE')).toBe(true);
123 /* Kein Stück darf so lang sein, dass Chromium es nach 15 Sekunden
124 abschneidet. */
125 expect(Math.max(...stuecke.map((stueck) => stueck.text.length))).toBeLessThanOrEqual(180);
126
127 /* WCAG 1.4.2: Der Halt-Knopf steht an derselben Stelle. */
128 const anhalten = fenster.getByRole('button', { name: /Vorlesen beenden/u });
129 await expect(anhalten).toBeVisible();
130 await anhalten.click();
131 await expect(fenster.getByRole('button', { name: /Frage vorlesen/u })).toBeVisible();
132 });
133
134 test('schreibt Paragrafen und Gesetzeskürzel für die Stimme aus', async () => {
135 /* Über den ganzen Katalog gesehen kommt „§“ in nahezu jeder Erklärung
136 vor. Geprüft wird deshalb nicht ein bestimmter Wortlaut, sondern dass
137 gesprochener Text keine Kürzel mehr enthält, die eine Stimme
138 buchstabieren müsste. */
139 const stuecke = await gesprochenes(fenster);
140 const gesamt = stuecke.map((stueck) => stueck.text).join(' ');
141
142 expect(gesamt).not.toContain('§');
143 expect(gesamt).not.toMatch(/\bAbs\.|\bNr\.\s|\bWaffG\b/u);
144 });
145
146 test('meldet nach dem ersten Sprechen die Systemstimmen', async () => {
147 /* Der eigentliche Befund dieser Datei, festgehalten als Test: Erst jetzt
148 ist die Liste gefüllt. Ist sie es nicht, hat diese Maschine keine
149 Stimme installiert – das ist kein Fehler der Anwendung, deshalb wird
150 hier nur berichtet und nicht abgebrochen. */
151 const stimmen = await fenster.evaluate(() =>
152 window.speechSynthesis.getVoices().map((stimme) => `${stimme.lang} ${stimme.name}`),
153 );
154
155 console.info(`Stimmen nach dem ersten speak(): ${stimmen.join(', ') || '(keine)'}`);
156 expect(Array.isArray(stimmen)).toBe(true);
157 });
158 });
159
160 test.describe('Vorlesen und Fragenwechsel', () => {
161 /*
162 Gemeldet von einem Nutzer: „Wenn eine Frage oder Antwort vorgelesen wird
163 und es wird die nächste Frage ausgewählt, dann wird immer noch die alte
164 Frage oder Antwort vorgelesen.“
165
166 Hier läuft die richtige Engine – geprüft wird, dass ihr das Schweigen
167 auch wirklich aufgetragen wird, und dass die Oberfläche danach nicht mehr
168 anbietet, etwas zu beenden, das zur vorigen Frage gehörte.
169 */
170 test('bricht die Ausgabe beim Wechsel zur nächsten Frage ab', async () => {
171 await mithoeren(fenster);
172 await zumStart(fenster);
173
174 await fenster
175 .getByRole('checkbox', { name: 'Fragen und Antworten vorlesen lassen' })
176 .setChecked(true);
177 await fenster.getByRole('button', { name: /Weiterlernen/u }).click();
178 await expect(fenster.getByRole('heading', { level: 1, name: /, Frage \d/u })).toBeVisible();
179
180 await fenster.getByRole('button', { name: /Frage vorlesen/u }).click();
181 await expect(fenster.getByRole('button', { name: /Vorlesen beenden/u })).toBeVisible();
182 const vorher = await abbrueche(fenster);
183
184 /* Beide Fragetypen bedienen: Der Katalog hat 104 offene Fragen unter 575. */
185 const auswahl = fenster.getByRole('checkbox').or(fenster.getByRole('radio'));
186 if ((await auswahl.count()) > 0) {
187 await auswahl.first().check();
188 await fenster.getByRole('button', { name: 'Antwort bestätigen', exact: true }).click();
189 } else {
190 await fenster.getByRole('button', { name: 'Musterantwort anzeigen' }).click();
191 await fenster.getByRole('button', { name: 'Gewusst', exact: true }).click();
192 }
193 await fenster.getByRole('button', { name: /^Nächste Frage$/u }).click();
194
195 expect(await abbrueche(fenster)).toBeGreaterThan(vorher);
196 await expect(fenster.getByRole('button', { name: /Vorlesen beenden/u })).toHaveCount(0);
197 await expect(fenster.getByRole('button', { name: /Frage vorlesen/u })).toBeVisible();
198
199 /* Zustand hinterlassen, wie er vorgefunden wurde. „Sitzung beenden“
200 führt auf die Zusammenfassung, nicht auf den Startbildschirm – der
201 Schalter steht erst einen Schritt weiter. */
202 await zumStart(fenster);
203 await fenster
204 .getByRole('checkbox', { name: 'Fragen und Antworten vorlesen lassen' })
205 .setChecked(false);
206 });
207 });