waffensachkunde

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

/ app e2e aufklappmarke.spec.ts

8,0 KB Rohdatei
app/e2e/aufklappmarke.spec.ts — 212 Zeilen
1 /**
2 * Jeder Aufklapper zeigt sein Dreieck.
3 *
4 * ## Warum das im echten Fenster gemessen wird
5 *
6 * Ein `<summary>` ohne Marke sieht aus wie eine gewöhnliche Zeile: Es sagt
7 * niemandem, dass sich dahinter etwas aufklappen lässt. Wer die Maus nicht
8 * darüberführt, findet den Inhalt nie. Chromium malt die Marke nur, solange
9 * das Element `display: list-item` behält – ein `display: flex` (oder `grid`,
10 * `inline-flex`, `block`) auf dem `<summary>` schaltet sie stillschweigend ab.
11 *
12 * Genau das war in `styles/lernen.css` bei `.sitzung__kuerzel summary` und in
13 * `styles/ueber.css` bei `.lizenzblock__schalter` passiert. Aufgefallen ist es
14 * erst im Bild: „Tastenkürzel dieser Ansicht“ stand ohne Dreieck neben zwei
15 * Nachbarn, die eines hatten. Die Warnung davor stand zu diesem Zeitpunkt
16 * bereits im Kommentar vor `.erklaerung--klappbar` in derselben Datei.
17 *
18 * Unter jsdom ist die Frage nicht zu beantworten: Dort gibt es keine
19 * CSS-Kaskade, `getComputedStyle` liefert für ein `<summary>` weder das
20 * `list-item` der UA-Vorlage noch das `flex` aus dem Stilblatt. Erst Chromium
21 * rechnet – deshalb steht diese Wache hier und nicht in `tests/`.
22 *
23 * ## Was gemessen wird
24 *
25 * Für jedes sichtbare `<summary>` der Ansicht: `display` muss `list-item`
26 * sein, der Markentyp darf nicht `none` sein und der Inhalt des `::marker`
27 * darf nicht abgeschaltet sein. Alle drei Wege führen sonst zum selben
28 * Ergebnis – ein Aufklapper, dem man das Aufklappen nicht ansieht.
29 */
30
31 import { type ElectronApplication, type Page } from '@playwright/test';
32
33 import { amStart, appStarten, bauPruefen } from './electron-hilfe';
34 import { expect, test } from './konsolenwache';
35
36 let app: ElectronApplication | undefined;
37 let fenster: Page;
38
39 test.beforeAll(async () => {
40 // Fehlender oder veralteter Bau ist ein Fehler, kein Grund zum Schweigen.
41 bauPruefen();
42 ({ app, fenster } = await appStarten());
43 });
44
45 test.afterAll(async () => {
46 await app?.close();
47 });
48
49 // Electron-Fenster, ein Profil: die Ansichten werden nacheinander angefahren.
50 test.describe.configure({ mode: 'serial' });
51
52 /** Ein `<summary>`, dem die Aufklappmarke fehlt – samt Grund. */
53 interface Befund {
54 readonly text: string;
55 readonly klasse: string;
56 readonly display: string;
57 readonly markentyp: string;
58 readonly markeninhalt: string;
59 }
60
61 /**
62 * Alle sichtbaren `<summary>` der laufenden Ansicht ohne Aufklappmarke.
63 *
64 * Zugeklappte `<details>` zählen mit: Ihr `<summary>` steht sichtbar da, und
65 * gerade dort braucht es die Marke am dringendsten. Ausgenommen sind nur
66 * wirklich verborgene Elemente – die malt Chromium ohnehin nicht.
67 */
68 async function ohneMarke(): Promise<Befund[]> {
69 return fenster.evaluate(() => {
70 const sichtbare = [...document.querySelectorAll('summary')].filter(
71 (summary) =>
72 summary.getClientRects().length > 0 && getComputedStyle(summary).visibility !== 'hidden',
73 );
74
75 return sichtbare
76 .map((summary) => {
77 const stil = getComputedStyle(summary);
78 return {
79 text: summary.textContent.trim().slice(0, 60),
80 klasse: summary.getAttribute('class') ?? '(ohne Klasse)',
81 display: stil.display,
82 markentyp: stil.listStyleType,
83 markeninhalt: getComputedStyle(summary, '::marker').content,
84 };
85 })
86 .filter(
87 (b) =>
88 /* Alle drei Wege enden beim selben Bild: kein Dreieck. */
89 b.display !== 'list-item' || b.markentyp === 'none' || b.markeninhalt === 'none',
90 );
91 });
92 }
93
94 /** Meldet die Fundstellen so, dass die Ursache ohne Nachsuchen dasteht. */
95 async function markenPruefen(ansicht: string): Promise<void> {
96 const befunde = await ohneMarke();
97 const liste = befunde
98 .map(
99 (b) =>
100 `„${b.text}“ (${b.klasse}): display=${b.display}, ` +
101 `list-style-type=${b.markentyp}, ::marker content=${b.markeninhalt}`,
102 )
103 .join('\n ');
104
105 expect(
106 befunde,
107 `${ansicht}: ${String(befunde.length)} Aufklapper ohne Dreieck.\n ${liste}\n` +
108 'Ein `display` ausser `list-item` auf einem <summary> schaltet die Marke ab.',
109 ).toEqual([]);
110 }
111
112 /** Zurück zum Startbildschirm, egal wo der vorige Test aufgehört hat. */
113 async function zumStart(): Promise<void> {
114 for (let i = 0; i < 4; i++) {
115 if (await amStart(fenster)) {
116 return;
117 }
118 const zurueck = fenster.getByRole('button', { name: /^(Zum Start|Sitzung beenden)$/u });
119 if ((await zurueck.count()) === 0) {
120 return;
121 }
122 await zurueck.first().click();
123 await fenster.waitForTimeout(250);
124 }
125 }
126
127 test.describe('Aufklappmarke je Ansicht', () => {
128 test('Startbildschirm', async () => {
129 await zumStart();
130 await markenPruefen('Startbildschirm');
131 });
132
133 test('Lernsitzung', async () => {
134 /* Die Ansicht des ursprünglichen Befundes: Hier stehen „Tastenkürzel
135 dieser Ansicht“ und „Fehler zu dieser Frage melden“ nebeneinander.
136 Die Tastenkürzel sind ab Werk eingeschaltet – ohne Zutun sichtbar. */
137 await zumStart();
138 await fenster.getByRole('button', { name: /Weiterlernen/u }).click();
139 await fenster.getByRole('heading', { level: 1, name: /, Frage \d/u }).waitFor();
140
141 await expect(fenster.getByText('Tastenkürzel dieser Ansicht')).toBeVisible();
142 await markenPruefen('Lernsitzung');
143 });
144
145 test('Lernsitzung nach der Antwort', async () => {
146 /* Erst nach der Antwort erscheinen „Warum das so ist“ und – bei Fragen
147 mit Bild – „Was dieses Zeichen bedeutet“. Gesucht ist irgendeine
148 Auswahlfrage: Die Sitzung wird zufällig zusammengestellt. */
149 const auswahlfeld = fenster.getByRole('radio').or(fenster.getByRole('checkbox'));
150
151 for (let i = 0; i < 20; i++) {
152 if ((await auswahlfeld.count()) > 0) {
153 break;
154 }
155 await fenster
156 .getByRole('button', { name: /Musterantwort anzeigen|^Antwort bestätigen$/u })
157 .first()
158 .click();
159 const bewerten = fenster.getByRole('button', { name: /^(Gewusst|Sicher gewusst)$/u });
160 if ((await bewerten.count()) > 0) {
161 await bewerten.first().click();
162 }
163 const weiter = fenster.getByRole('button', { name: /^Nächste Frage$/u });
164 if ((await weiter.count()) === 0) {
165 break;
166 }
167 await weiter.first().click();
168 await fenster.waitForTimeout(150);
169 }
170
171 await auswahlfeld.first().click();
172 await fenster.getByRole('button', { name: 'Antwort bestätigen' }).click();
173 await fenster.waitForTimeout(300);
174
175 await markenPruefen('Lernsitzung nach der Antwort');
176 });
177
178 test('Über diese Software', async () => {
179 /* Zweite Fundstelle desselben Fehlers: `.lizenzblock__schalter`. */
180 await zumStart();
181 await fenster.getByRole('button', { name: /Lizenzen und Herkunft anzeigen/u }).click();
182 await fenster.getByRole('heading', { name: 'Über diese Software' }).waitFor();
183
184 await expect(fenster.locator('summary.lizenzblock__schalter').first()).toBeVisible();
185 await markenPruefen('Über diese Software');
186 });
187
188 test('Fragen durchsuchen', async () => {
189 /* Mit Trefferliste, nicht leer: Erst dann steht in jeder Trefferkarte der
190 Aufklapper „Antwortmöglichkeiten anzeigen“. */
191 await zumStart();
192 await fenster.getByRole('button', { name: /Fragen durchsuchen/u }).click();
193 await fenster.getByRole('heading', { name: 'Fragen durchsuchen', level: 1 }).waitFor();
194 await fenster.getByLabel('Suchbegriff').fill('Anscheinswaffe');
195 await fenster.waitForTimeout(400);
196
197 await markenPruefen('Fragen durchsuchen');
198 await zumStart();
199 });
200
201 test('Startbildschirm mit Lernstand', async () => {
202 /* Die Lernstandsübersicht steht auf dem Startbildschirm, erscheint dort
203 aber erst, wenn etwas beantwortet ist – der erste Test dieser Datei
204 läuft auf einem frischen Profil und sieht sie noch nicht. Ihre beiden
205 Aufklapper („Was heißt ‚belegt‘?“, „Nach Kapiteln und Abschnitten“)
206 kommen deshalb erst hier unter die Lupe. */
207 await zumStart();
208
209 await expect(fenster.getByText('Was heißt „belegt“?')).toBeVisible();
210 await markenPruefen('Startbildschirm mit Lernstand');
211 });
212 });