waffensachkunde

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

/ app e2e reifeampel.spec.ts

6,0 KB Rohdatei
app/e2e/reifeampel.spec.ts — 156 Zeilen
1 /**
2 * Die Prüfungsreife-Ampel im echten Fenster.
3 *
4 * Was `tests/reife.test.ts` an der reinen Rechnung prüft und
5 * `tests/lernstand.test.ts` an einer Datenbank, läuft hier durch die ganze
6 * Kette: echte SQLite-Datei, echter Katalog mit 575 Fragen, echte Oberfläche.
7 *
8 * Der Schwerpunkt liegt auf dem, was ein Bildschirmleser bekommt. Eine Ampel,
9 * die nur farbig funktioniert, wäre für diese Anwendung kein Merkmal, sondern
10 * ein Mangel – und der Rauchtest mit axe sieht Farbe nicht.
11 */
12
13 import { type ElectronApplication, type Page } from '@playwright/test';
14
15 import { amStart, appStarten, bauPruefen } from './electron-hilfe';
16 import { expect, test } from './konsolenwache';
17
18 let app: ElectronApplication | undefined;
19 let fenster: Page;
20
21 test.beforeAll(async () => {
22 bauPruefen();
23 ({ app, fenster } = await appStarten());
24 });
25
26 test.afterAll(async () => {
27 await app?.close();
28 });
29
30 test.describe.configure({ mode: 'serial' });
31
32 function gesamtampel() {
33 return fenster.getByRole('progressbar', { name: 'Prüfungsreife insgesamt' });
34 }
35
36 async function zumStart(): Promise<void> {
37 for (let i = 0; i < 4; i++) {
38 if (await amStart(fenster)) {
39 return;
40 }
41 const zurueck = fenster.getByRole('button', { name: /^(Zum Start|Sitzung beenden)$/u });
42 if ((await zurueck.count()) === 0) {
43 return;
44 }
45 await zurueck.first().click();
46 await fenster.waitForTimeout(250);
47 }
48 }
49
50 test.describe('Prüfungsreife-Ampel', () => {
51 test('trägt ihre Aussage vollständig im Text, nicht in der Farbe', async () => {
52 await zumStart();
53
54 /* Der Balken meldet Stufe und Zahl über `aria-valuetext`. Ohne das läse
55 ein Screenreader eine nackte Zahl zwischen 0 und 575 vor – richtig und
56 nutzlos. */
57 const text = await gesamtampel().getAttribute('aria-valuetext');
58 expect(text).toBeTruthy();
59 expect(text).toMatch(/belegt|Tag Abstand/u);
60
61 /* Und das Stufenwort steht sichtbar daneben, nicht nur im Attribut. */
62 await expect(
63 fenster
64 .getByText(/Noch kein belegter Stand|Noch nicht so weit|Auf der Kippe|Prüfungsreif/u)
65 .first(),
66 ).toBeVisible();
67 });
68
69 test('nennt bei leerem Stand die Regel statt einer Stufe', async () => {
70 /* Wer noch keine Frage wiedergesehen hat, bekommt keine Einstufung
71 vorgegaukelt, sondern die Auskunft, woran das liegt. */
72 await expect(gesamtampel()).toHaveAttribute('aria-valuenow', '0');
73 expect(await gesamtampel().getAttribute('aria-valuetext')).toContain(
74 'mindestens einem Tag Abstand',
75 );
76 });
77
78 test('verspricht nirgends eine Bestehenswahrscheinlichkeit', async () => {
79 /* Eine Angabe wie „neun von zehn Läufen“ setzte voraus, dass alle Fragen
80 dieselbe Trefferchance haben. Das Gegenteil ist der Fall – die
81 Verteilung ist zweigipflig, und die Zahl wäre exakt aussehend und
82 falsch. Siehe docs/entscheidung-reifegrad.md. */
83 const seite = (await fenster.locator('main').innerText()).toLowerCase();
84
85 expect(seite).not.toMatch(/von zehn (prüfungen|läufen)/u);
86 expect(seite).not.toContain('wahrscheinlichkeit zu bestehen');
87 });
88
89 test('bleibt nach einer einzigen richtigen Antwort bei null', async () => {
90 /*
91 Der Kern der ganzen Umstellung, durch die volle Kette geprüft. Mit der
92 rohen FSRS-Abrufwahrscheinlichkeit stünde eine so beantwortete Frage bei
93 0,947 – ein Katalog einmal zur Hälfte richtig durchgearbeitet hätte
94 „Prüfungsreif“ gemeldet.
95 */
96 await zumStart();
97 await fenster.getByRole('button', { name: /Weiterlernen/u }).click();
98 await fenster
99 .getByRole('heading', { name: /Frage /u })
100 .first()
101 .waitFor();
102
103 /*
104 Eine Frage beantworten – gleich welche Antwort, es geht um die Anzahl.
105
106 Beide Fragetypen müssen bedient werden: Der Katalog hat 104 offene
107 Fragen unter 575, die Sitzung zieht also in etwa jedem fünften Lauf
108 eine ohne Antwortkästchen. Die erste Fassung dieses Tests klickte blind
109 auf ein Kästchen und war damit unzuverlässig – sie fiel im vollen Lauf
110 um und ging allein durch.
111 */
112 const auswahl = fenster.getByRole('checkbox').or(fenster.getByRole('radio'));
113 if ((await auswahl.count()) > 0) {
114 await auswahl.first().check();
115 await fenster.getByRole('button', { name: 'Antwort bestätigen', exact: true }).click();
116 } else {
117 await fenster.getByRole('button', { name: 'Musterantwort anzeigen' }).click();
118 await fenster.getByRole('button', { name: 'Gewusst', exact: true }).click();
119 }
120 await fenster.getByRole('button', { name: /^Nächste Frage$/u }).click();
121
122 await zumStart();
123 await expect(gesamtampel()).toHaveAttribute('aria-valuenow', '0');
124 });
125
126 test('erklärt aufklappbar, was „belegt“ heißt – und die Erklärung nennt den Vorbehalt', async () => {
127 await zumStart();
128
129 const erklaerung = fenster.getByText('Was heißt „belegt“?');
130 await erklaerung.scrollIntoViewIfNeeded();
131 await erklaerung.click();
132
133 await expect(fenster.getByText(/Raten ist nicht eingerechnet/u)).toBeVisible();
134 await expect(fenster.getByText(/allein der Prüfungsausschuss/u)).toBeVisible();
135
136 /* Und genau einmal: Die Lernplan-Fußnote trug denselben Vorbehalt ein
137 zweites Mal, für eine Trefferquote, die es als eigene Zahl nicht mehr
138 gibt. Zwei Erklärungen derselben Sache driften auseinander. */
139 expect(await fenster.getByText(/Raten ist nicht eingerechnet/u).count()).toBe(1);
140 });
141
142 test('führt jeden Bereich mit eigener Ampel auf', async () => {
143 await zumStart();
144
145 const bereiche = fenster.getByText('Nach Kapiteln und Abschnitten');
146 await bereiche.scrollIntoViewIfNeeded();
147 await bereiche.click();
148
149 /* Notwehr und Notstand ist der Bereich, an dem das Gesamturteil hängen
150 kann – er muss einzeln sichtbar sein, sonst ist der Hinweis im
151 Kernsatz nicht nachvollziehbar. */
152 await expect(
153 fenster.getByRole('progressbar', { name: /Prüfungsreife in Notwehr und Notstand/u }),
154 ).toBeAttached();
155 });
156 });