waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
/ app e2e kapitelumfang.spec.ts
| 1 | /** |
| 2 | * Die dauerhafte Kapitelabwahl im echten Fenster. |
| 3 | * |
| 4 | * Was die Vitest-Tests an einer Attrappe und an einer Wegwerf-Datenbank |
| 5 | * prüfen, läuft hier am echten Katalog mit seinen 575 Fragen und an der |
| 6 | * echten SQLite-Datei: Der Schalter schreibt wirklich in die Profilzeile, |
| 7 | * die Zahl im Gesamtfortschritt ändert sich wirklich, und die Umstellung |
| 8 | * überlebt einen Neuaufbau der Oberfläche wirklich. |
| 9 | * |
| 10 | * Der Neuaufbau geschieht per `reload()` – kein Prozessneustart, aber genau |
| 11 | * der Weg, um den es geht: React baut frisch auf und liest die Profilzeile |
| 12 | * neu aus der Datenbank. |
| 13 | */ |
| 14 | |
| 15 | import { type ElectronApplication, type Page } from '@playwright/test'; |
| 16 | |
| 17 | import { amStart, appStarten, bauPruefen } from './electron-hilfe'; |
| 18 | import { expect, test } from './konsolenwache'; |
| 19 | |
| 20 | /** Der amtliche Katalog, und was ohne Kapitel IV davon übrig bleibt. */ |
| 21 | const GESAMT = 575; |
| 22 | const OHNE_KAPITEL_IV = 486; |
| 23 | |
| 24 | let app: ElectronApplication | undefined; |
| 25 | let fenster: Page; |
| 26 | |
| 27 | test.beforeAll(async () => { |
| 28 | bauPruefen(); |
| 29 | ({ app, fenster } = await appStarten()); |
| 30 | }); |
| 31 | |
| 32 | test.afterAll(async () => { |
| 33 | await app?.close(); |
| 34 | }); |
| 35 | |
| 36 | test.describe.configure({ mode: 'serial' }); |
| 37 | |
| 38 | function schalter() { |
| 39 | return fenster.getByRole('checkbox', { name: /Kapitel IV .* mitlernen/u }); |
| 40 | } |
| 41 | |
| 42 | /** |
| 43 | * Der Lernumfang, wie ihn die Ampel ausweist. |
| 44 | * |
| 45 | * Aus `aria-valuemax` statt aus dem Text: Der Satz der Ampel ändert seine |
| 46 | * Form je nach Stufe – bei leerem Stand nennt er die Regel statt einer Zahl –, |
| 47 | * die Obergrenze des Balkens ist dagegen immer der Umfang. |
| 48 | */ |
| 49 | async function umfang(): Promise<number> { |
| 50 | const wert = await fenster |
| 51 | .getByRole('progressbar', { name: 'Prüfungsreife insgesamt' }) |
| 52 | .getAttribute('aria-valuemax'); |
| 53 | return Number(wert ?? 0); |
| 54 | } |
| 55 | |
| 56 | async function zumStart(): Promise<void> { |
| 57 | for (let i = 0; i < 4; i++) { |
| 58 | if (await amStart(fenster)) { |
| 59 | return; |
| 60 | } |
| 61 | const zurueck = fenster.getByRole('button', { name: /^(Zum Start|Sitzung beenden)$/u }); |
| 62 | if ((await zurueck.count()) === 0) { |
| 63 | return; |
| 64 | } |
| 65 | await zurueck.first().click(); |
| 66 | await fenster.waitForTimeout(250); |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | test.describe('Kapitel IV dauerhaft abwählen', () => { |
| 71 | test('nimmt Kapitel IV aus dem Lernumfang und sagt es an', async () => { |
| 72 | await zumStart(); |
| 73 | expect(await umfang()).toBe(GESAMT); |
| 74 | |
| 75 | await schalter().click(); |
| 76 | |
| 77 | /* Die Ansage steht nur für Bildschirmleser im Dokument, ist aber ein |
| 78 | echtes Element – sichtbar im Sinne von Playwright ist sie nicht. */ |
| 79 | await expect(fenster.getByText(/Kapitel IV ist für dieses Profil abgewählt/u)).toBeAttached(); |
| 80 | await expect.poll(umfang).toBe(OHNE_KAPITEL_IV); |
| 81 | }); |
| 82 | |
| 83 | test('überlebt den Neuaufbau der Oberfläche', async () => { |
| 84 | /* Der Punkt der ganzen Übung: „dauerhaft“ heißt in der Profilzeile, nicht |
| 85 | im Zustand einer React-Komponente. */ |
| 86 | await fenster.reload(); |
| 87 | await fenster.waitForLoadState('domcontentloaded'); |
| 88 | await fenster.waitForSelector('h1', { state: 'visible' }); |
| 89 | |
| 90 | await expect(schalter()).not.toBeChecked(); |
| 91 | await expect.poll(umfang).toBe(OHNE_KAPITEL_IV); |
| 92 | }); |
| 93 | |
| 94 | test('lässt die Suche den ganzen Katalog behalten', async () => { |
| 95 | /* Abgewählt heißt: zählt nicht mit. Nicht: gibt es nicht mehr. Wer im |
| 96 | Lehrgang nach einer Signalpistole gefragt wird, muss sie nachschlagen |
| 97 | können. */ |
| 98 | await fenster.getByRole('button', { name: /Fragen durchsuchen/u }).click(); |
| 99 | await fenster.getByRole('heading', { name: 'Fragen durchsuchen', level: 1 }).waitFor(); |
| 100 | |
| 101 | await expect( |
| 102 | fenster.getByText(`Durchsucht wird der Wortlaut aller ${String(GESAMT)} amtlichen Fragen`, { |
| 103 | exact: false, |
| 104 | }), |
| 105 | ).toBeVisible(); |
| 106 | |
| 107 | await fenster.getByLabel('Suchbegriff').fill('Seenotsignalmittel'); |
| 108 | await expect(fenster.locator('.treffer__frage').first()).toBeVisible(); |
| 109 | }); |
| 110 | |
| 111 | test('vermerkt das Kapitel in der Kapitelwahl, statt es zu verbergen', async () => { |
| 112 | /* Weder verschwinden noch sperren: Verschwände es, fände niemand den Weg |
| 113 | zurück zum Schalter; und ein gesperrter Knopf nimmt keinen Fokus – die |
| 114 | Erklärung daneben würde nie vorgelesen. */ |
| 115 | await zumStart(); |
| 116 | await fenster.getByRole('button', { name: /Kapitel wählen/u }).click(); |
| 117 | await fenster.getByRole('heading', { name: 'Kapitel wählen', level: 1 }).waitFor(); |
| 118 | |
| 119 | await expect(fenster.getByText(/Für dieses Profil abgewählt/u)).toBeVisible(); |
| 120 | await expect(fenster.getByRole('button', { name: /Ganzes Kapitel IV üben/u })).toBeEnabled(); |
| 121 | }); |
| 122 | |
| 123 | test('holt das Kapitel vollständig zurück', async () => { |
| 124 | /* Zuletzt, damit die Datei den Zustand hinterlässt, in dem sie ihn |
| 125 | vorgefunden hat – die übrigen Prüfläufe rechnen mit 575. */ |
| 126 | await zumStart(); |
| 127 | await schalter().click(); |
| 128 | |
| 129 | await expect(fenster.getByText(/Kapitel IV wird wieder mitgelernt/u)).toBeAttached(); |
| 130 | await expect.poll(umfang).toBe(GESAMT); |
| 131 | }); |
| 132 | }); |