waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
/ app e2e bildschirmfotos.spec.ts
| 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 | }); |