waffensachkunde

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

/ app e2e bildschirmfotos.spec.ts

10,3 KB Rohdatei
app/e2e/bildschirmfotos.spec.ts — 291 Zeilen
1 /**
2 * Erzeugt Bildschirmfotos der Anwendung zur visuellen Abnahme.
3 *
4 * Kein Prüf-, sondern ein Dokumentationslauf: Er hält fest, wie die Oberfläche
5 * in den drei Farbschemata und in den wichtigsten Ansichten tatsächlich
6 * aussieht. Ausgabe nach `docs/bildschirmfotos/`.
7 *
8 * Aufruf: npx playwright test e2e/bildschirmfotos.spec.ts
9 */
10 import { mkdirSync } from 'node:fs';
11 import { join } from 'node:path';
12
13 import { type ElectronApplication, type Page } from '@playwright/test';
14
15 import { animationenAbwarten, appStarten, bauPruefen, projektWurzel } from './electron-hilfe';
16 import { test } from './konsolenwache';
17
18 const ZIEL = join(projektWurzel, '..', 'docs', 'bildschirmfotos');
19
20 let app: ElectronApplication;
21 let fenster: Page;
22
23 test.beforeAll(async () => {
24 // Fehlender oder veralteter Bau ist ein Fehler, kein Grund zum Schweigen.
25 bauPruefen();
26 mkdirSync(ZIEL, { recursive: true });
27 ({ app, fenster } = await appStarten());
28 await fenster.setViewportSize({ width: 1280, height: 900 });
29 });
30
31 test.afterAll(async () => {
32 await app.close();
33 });
34
35 async function ablegen(name: string): Promise<void> {
36 await animationenAbwarten(fenster);
37 await fenster.screenshot({ path: join(ZIEL, `${name}.png`), fullPage: true });
38 }
39
40 /**
41 * Bildausschnitt statt ganzer Seite.
42 *
43 * Nötig für alles, was in der obersten Ebene liegt: Ein modaler Dialog steht
44 * im sichtbaren Ausschnitt, während `fullPage` die ganze, viel längere Seite
45 * zusammensetzt. Er säße dann irgendwo im unteren Drittel eines Bildes, das
46 * ihn gar nicht zeigen soll.
47 */
48 async function ausschnittAblegen(name: string): Promise<void> {
49 await animationenAbwarten(fenster);
50 await fenster.screenshot({ path: join(ZIEL, `${name}.png`) });
51 }
52
53 async function themaWaehlen(beschriftung: string): Promise<void> {
54 await fenster.getByRole('radio', { name: beschriftung, exact: true }).check();
55 await animationenAbwarten(fenster);
56 }
57
58 /**
59 * Zurück zum Startbildschirm, egal aus welcher Ansicht.
60 *
61 * Die Ansichten bieten unterschiedliche Rückwege; probiert wird deshalb der
62 * Reihe nach, bis die Überschrift „Lernen“ wieder da ist.
63 */
64 async function zumStart(): Promise<void> {
65 const start = fenster.getByRole('heading', { name: 'Heute lernen', exact: true });
66 /*
67 Die Reihenfolge ist der Weg heraus, nicht bloß eine Liste: Aus einem
68 laufenden Bogen führen zwei Schritte – „Simulation abbrechen“ öffnet die
69 Rückfrage, „Abbrechen und verwerfen“ beantwortet sie.
70
71 Bis Fassung 0.20.0 stand hier `/prüfung abbrechen/i`. Mit der Umbenennung
72 von „Prüfung“ auf „Simulation“ traf das keinen Knopf mehr: Der Lauf blieb
73 im Bogen stehen, den der vorige Test gestartet hatte, und „Erklärung zur
74 Antwort“ lief in die Zeitüberschreitung. Aufgefallen ist es erst hier,
75 weil diese Datei zuvor nie gegen den geänderten Stand lief.
76 */
77 const wege = [
78 /sitzung beenden/i,
79 /zum start/i,
80 /simulation abbrechen/i,
81 /abbrechen und verwerfen/i,
82 ];
83
84 for (const weg of wege) {
85 if (await start.isVisible().catch(() => false)) {
86 return;
87 }
88 const knopf = fenster.getByRole('button', { name: weg }).first();
89 if (await knopf.isVisible().catch(() => false)) {
90 await knopf.click();
91 await fenster.waitForTimeout(400);
92 }
93 }
94 await start.waitFor({ timeout: 5000 });
95 }
96
97 test('Startbildschirm in allen drei Farbschemata', async () => {
98 await ablegen('01-startbildschirm-hell');
99
100 await themaWaehlen('Dunkel');
101 await ablegen('02-startbildschirm-dunkel');
102
103 await themaWaehlen('Hoher Kontrast');
104 await ablegen('03-startbildschirm-hochkontrast');
105
106 await themaWaehlen('Hell');
107 });
108
109 /**
110 * Blättert in der laufenden Sitzung vor bis zu einer Auswahlfrage.
111 *
112 * „Weiterlernen“ zieht die Frage nach der Wiedervorlage, und die kann eine
113 * offene sein. Dann gibt es weder Radioknopf noch Kästchen, und „Antwort
114 * bestätigen“ heißt „Musterantwort anzeigen“.
115 */
116 async function bisZurAuswahlfrage(): Promise<void> {
117 const auswahlfeld = fenster.getByRole('radio').or(fenster.getByRole('checkbox'));
118
119 for (let i = 0; i < 15; i++) {
120 if ((await auswahlfeld.count()) > 0) {
121 return;
122 }
123 await fenster
124 .getByRole('button', { name: /Musterantwort anzeigen|^Antwort bestätigen$/u })
125 .first()
126 .click();
127 const bewertung = fenster.getByRole('button', { name: /^(Gewusst|Sicher gewusst)$/u });
128 if ((await bewertung.count()) > 0) {
129 await bewertung.first().click();
130 }
131 const weiter = fenster.getByRole('button', { name: /^Nächste Frage$/u });
132 if ((await weiter.count()) === 0) {
133 throw new Error('Die Sitzung endete, ohne eine Auswahlfrage zu zeigen.');
134 }
135 await weiter.first().click();
136 await fenster.waitForTimeout(150);
137 }
138 throw new Error('Nach fünfzehn Fragen stand noch keine Auswahlfrage an.');
139 }
140
141 test('Lernsitzung mit Rückmeldung', async () => {
142 await fenster
143 .getByRole('button', { name: /Weiterlernen/i })
144 .first()
145 .click();
146 await fenster.waitForTimeout(700);
147
148 /*
149 Erst vorblättern, dann fotografieren – beide Bilder zeigen dieselbe Frage.
150
151 Vorher wurde geknipst, was gerade dastand, und Bild 05 nur dann, wenn
152 zufällig eine Auswahlfrage gezogen war: `/bestätigen|prüfen/i` trifft
153 „Musterantwort anzeigen“ nicht. Bei einer offenen Frage entstand still
154 kein neues Bild, und die alte Aufnahme blieb als vermeintlich aktuelle
155 liegen – genau der Fall, den `gepackt.spec.ts` für das Paket beschreibt.
156 Nachgemessen: Im Lauf vom 27.08. blieb 05 unverändert stehen, während
157 04 neu geschrieben wurde.
158 */
159 await bisZurAuswahlfrage();
160 await ablegen('04-lernsitzung');
161
162 await fenster.getByRole('radio').or(fenster.getByRole('checkbox')).first().check();
163 await fenster.getByRole('button', { name: 'Antwort bestätigen' }).click();
164 await fenster.waitForTimeout(600);
165 await ablegen('05-lernsitzung-feedback');
166
167 await zumStart();
168 });
169
170 test('Prüfungssimulation: Auswahl und Lauf', async () => {
171 await fenster
172 .getByRole('button', { name: /Prüfungssimulation vorbereiten/i })
173 .first()
174 .click();
175 await fenster.waitForTimeout(700);
176 await ablegen('06-pruefungswahl');
177
178 // Ein kleines, schnelles Profil wählen, damit der Lauf überschaubar bleibt.
179 const frei = fenster.getByRole('radio', { name: /Selbst einstellen/i }).first();
180 if (await frei.isVisible().catch(() => false)) {
181 await frei.check().catch(() => undefined);
182 await animationenAbwarten(fenster);
183 await ablegen('07-pruefungswahl-frei');
184 }
185
186 const starten = fenster.getByRole('button', { name: /simulation starten|starten/i }).last();
187 if (await starten.isVisible().catch(() => false)) {
188 await starten.click();
189 await fenster.waitForTimeout(900);
190 await ablegen('08-pruefungslauf');
191 }
192 });
193
194 test('Erklärung zur Antwort', async () => {
195 /* Abschnitt I.1 gezielt ansteuern: Dort liegen Erklärungen vor. Bei
196 „Weiterlernen" bestimmt die Wiedervorlage die Frage, und die kann eine
197 ohne Erklärung sein. */
198 await zumStart();
199 await fenster
200 .getByRole('button', { name: /Kapitel wählen/i })
201 .first()
202 .click();
203 await fenster.getByRole('button', { name: /^I\.1 – / }).click();
204 await fenster.waitForTimeout(700);
205
206 const option = fenster.getByRole('radio').first();
207 const kaestchen = fenster.getByRole('checkbox').first();
208 if (await option.isVisible().catch(() => false)) {
209 await option.check().catch(() => undefined);
210 } else if (await kaestchen.isVisible().catch(() => false)) {
211 await kaestchen.check().catch(() => undefined);
212 }
213
214 const bestaetigen = fenster.getByRole('button', { name: /bestätigen|anzeigen/i }).first();
215 if (await bestaetigen.isVisible().catch(() => false)) {
216 await bestaetigen.click();
217 await fenster.waitForTimeout(600);
218 }
219
220 // Bei offenen Fragen kommt zuerst die Selbstbewertung.
221 const gewusst = fenster.getByRole('button', { name: /^Gewusst/i }).first();
222 if (await gewusst.isVisible().catch(() => false)) {
223 await gewusst.click();
224 await fenster.waitForTimeout(500);
225 }
226
227 await ablegen('10-erklaerung');
228 await zumStart();
229 });
230
231 test('Kapitelwahl', async () => {
232 /* Es gab davon kein Bild, und genau hier ist am 01.09.2026 eine
233 verunglückte Anordnung nur deshalb aufgefallen, weil jemand die
234 Anwendung von Hand geöffnet hat. Seit die Ansicht den Lernstand je Wahl
235 trägt, ist sie eine Anzeige und nicht mehr nur eine Knopfreihe. */
236 await zumStart();
237 await fenster
238 .getByRole('button', { name: /Kapitel wählen/i })
239 .first()
240 .click();
241 await fenster.getByRole('heading', { name: 'Kapitel wählen', level: 1 }).waitFor();
242 await ablegen('14-kapitelwahl');
243 await fenster.getByRole('navigation', { name: 'Bereichswechsel' }).getByRole('button').click();
244 });
245
246 test('Glossar', async () => {
247 await zumStart();
248 await fenster
249 .getByRole('button', { name: /Fachbegriffe nachschlagen/i })
250 .first()
251 .click();
252 await fenster.getByRole('heading', { name: 'Glossar', level: 1 }).waitFor();
253 await ablegen('11-glossar');
254 await fenster.getByRole('navigation', { name: 'Bereichswechsel' }).getByRole('button').click();
255 });
256
257 test('Über diese Software', async () => {
258 // Bewusst ohne Absicherung: Kommt der Lauf hier nicht an, ist das ein
259 // Befund und kein Grund, still einen fehlenden Screenshot hinzunehmen.
260 await zumStart();
261 await fenster
262 .getByRole('button', { name: /Lizenzen und Herkunft anzeigen/i })
263 .first()
264 .click();
265 await fenster.getByRole('heading', { name: /Über diese Software/i, level: 1 }).waitFor();
266 await ablegen('09-ueber-software');
267 });
268
269 test('Hilfe', async () => {
270 /* Der Dialog liegt in der obersten Ebene über der Ansicht. Das Bild soll
271 genau das zeigen: Die Hilfe ersetzt den Startbildschirm nicht, sie legt
272 sich darüber. */
273 await zumStart();
274 await fenster.getByRole('contentinfo').getByRole('button', { name: 'Hilfe' }).click();
275 await fenster.getByRole('dialog', { name: 'Hilfe' }).waitFor();
276 await ausschnittAblegen('12-hilfe');
277 await fenster.keyboard.press('Escape');
278 });
279
280 test('Fragen durchsuchen', async () => {
281 await zumStart();
282 await fenster
283 .getByRole('button', { name: /Fragen durchsuchen/i })
284 .first()
285 .click();
286 await fenster.getByRole('heading', { name: 'Fragen durchsuchen', level: 1 }).waitFor();
287 await fenster.getByLabel('Suchbegriff').fill('Anscheinswaffe');
288 await fenster.waitForTimeout(600);
289 await ablegen('13-suche');
290 await zumStart();
291 });