waffensachkunde

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

/ app e2e bildschirmfotos.spec.ts

9,7 KB Rohdatei
app/e2e/bildschirmfotos.spec.ts — 276 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('Glossar', async () => {
232 await zumStart();
233 await fenster
234 .getByRole('button', { name: /Fachbegriffe nachschlagen/i })
235 .first()
236 .click();
237 await fenster.getByRole('heading', { name: 'Glossar', level: 1 }).waitFor();
238 await ablegen('11-glossar');
239 await fenster.getByRole('navigation', { name: 'Bereichswechsel' }).getByRole('button').click();
240 });
241
242 test('Über diese Software', async () => {
243 // Bewusst ohne Absicherung: Kommt der Lauf hier nicht an, ist das ein
244 // Befund und kein Grund, still einen fehlenden Screenshot hinzunehmen.
245 await zumStart();
246 await fenster
247 .getByRole('button', { name: /Lizenzen und Herkunft anzeigen/i })
248 .first()
249 .click();
250 await fenster.getByRole('heading', { name: /Über diese Software/i, level: 1 }).waitFor();
251 await ablegen('09-ueber-software');
252 });
253
254 test('Hilfe', async () => {
255 /* Der Dialog liegt in der obersten Ebene über der Ansicht. Das Bild soll
256 genau das zeigen: Die Hilfe ersetzt den Startbildschirm nicht, sie legt
257 sich darüber. */
258 await zumStart();
259 await fenster.getByRole('contentinfo').getByRole('button', { name: 'Hilfe' }).click();
260 await fenster.getByRole('dialog', { name: 'Hilfe' }).waitFor();
261 await ausschnittAblegen('12-hilfe');
262 await fenster.keyboard.press('Escape');
263 });
264
265 test('Fragen durchsuchen', async () => {
266 await zumStart();
267 await fenster
268 .getByRole('button', { name: /Fragen durchsuchen/i })
269 .first()
270 .click();
271 await fenster.getByRole('heading', { name: 'Fragen durchsuchen', level: 1 }).waitFor();
272 await fenster.getByLabel('Suchbegriff').fill('Anscheinswaffe');
273 await fenster.waitForTimeout(600);
274 await ablegen('13-suche');
275 await zumStart();
276 });