waffensachkunde

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

/ app e2e anzeigegroesse.spec.ts

25,5 KB Rohdatei
app/e2e/anzeigegroesse.spec.ts — 628 Zeilen
1 /**
2 * Die Anzeigegröße im echten Fenster – und der Umbruch, den sie erzwingt.
3 *
4 * Der Zoom hängt an den `webContents`, nicht am Dokument – prüfbar ist er
5 * deshalb nur am laufenden Fenster. Die reine Rechnerei (welche Stufe folgt
6 * auf welche, wie werden unbrauchbare Werte bereinigt) prüft
7 * `tests/ansicht.test.ts`.
8 *
9 * Der zweite Teil dieser Datei misst WCAG 2.2 – 1.4.10 (Reflow) an dem Punkt,
10 * an dem das Kriterium ihn festmacht: 400 Prozent Vergrößerung auf einem
11 * 1280 Pixel breiten Fenster, also 320 CSS-Pixel Inhaltsbreite.
12 */
13
14 import { type ElectronApplication, type Page } from '@playwright/test';
15
16 import { amStart, appStarten, bauPruefen } from './electron-hilfe';
17 import { expect, test } from './konsolenwache';
18
19 let app: ElectronApplication | undefined;
20 let fenster: Page;
21
22 test.beforeAll(async () => {
23 // Fehlender oder veralteter Bau ist ein Fehler, kein Grund zum Schweigen.
24 bauPruefen();
25 ({ app, fenster } = await appStarten());
26 });
27
28 test.afterAll(async () => {
29 await app?.close();
30 });
31
32 async function faktor(): Promise<number> {
33 return (
34 (await app?.evaluate(({ BrowserWindow }) =>
35 Number(BrowserWindow.getAllWindows()[0]?.webContents.getZoomFactor().toFixed(3)),
36 )) ?? 0
37 );
38 }
39
40 test.describe('Anzeigegröße', () => {
41 test('beginnt bei 100 Prozent', async () => {
42 await expect(fenster.getByText('Aktuell: 100 Prozent')).toBeVisible();
43 expect(await faktor()).toBeCloseTo(1, 2);
44 });
45
46 test('vergrößert die ganze Anwendung, nicht nur den Text', async () => {
47 /* Der Zoomfaktor wirkt auf alles im Fenster – auch auf die siebzehn
48 Prüfzeichen, bei denen das Bild die Antwort ist. Genau deshalb Zoom
49 und nicht bloß eine größere Schrift. */
50 await fenster.getByRole('button', { name: /Größer/u }).click();
51 await expect(fenster.getByText('Aktuell: 110 Prozent')).toBeVisible();
52 expect(await faktor()).toBeCloseTo(1.1, 2);
53 });
54
55 test('reicht bis 400 Prozent (WCAG 1.4.4 und 1.4.10)', async () => {
56 /*
57 1.4.4 verlangt 200 Prozent. Die Leiter geht weiter bis 400, weil 1.4.10
58 dort misst: 400 Prozent auf 1280 Pixeln Fensterbreite sind die 320
59 CSS-Pixel, bei denen der Inhalt ohne seitliches Rollen benutzbar sein
60 muss. Endete die Leiter bei 200, wäre dieser Fall mit Bordmitteln nicht
61 einmal herstellbar.
62
63 Nach jedem Klick auf die neue Zahl warten. Blind hintereinander zu
64 klicken läuft in eine Wettlaufsituation: Playwright prüft, ob der Knopf
65 bedienbar ist, React schaltet ihn zwischendurch ab.
66 */
67 const knopf = fenster.getByRole('button', { name: /Größer/u });
68 const stand = fenster.locator('.anzeigegroesse__stand');
69 for (const stufe of [125, 150, 175, 200, 250, 300, 400]) {
70 await knopf.click();
71 await expect(stand).toHaveText(`Aktuell: ${String(stufe)} Prozent`);
72 }
73
74 await expect(fenster.getByText('Aktuell: 400 Prozent')).toBeVisible();
75 expect(await faktor()).toBeCloseTo(4, 2);
76 await expect(fenster.getByRole('button', { name: /Größer/u })).toBeDisabled();
77 });
78
79 test('bleibt bei 400 Prozent bedienbar', async () => {
80 /* Bei vierfacher Größe muss sich alles noch erreichen lassen – sonst wäre
81 die Vergrößerung wertlos. Geprüft wird an der Hauptschaltfläche. */
82 const weiterlernen = fenster.getByRole('button', { name: /Weiterlernen/u });
83
84 await expect(weiterlernen).toBeVisible();
85 await weiterlernen.scrollIntoViewIfNeeded();
86 await expect(weiterlernen).toBeEnabled();
87 });
88
89 test('kehrt auf Normalgröße zurück', async () => {
90 await fenster.getByRole('button', { name: 'Normalgröße' }).click();
91
92 await expect(fenster.getByText('Aktuell: 100 Prozent')).toBeVisible();
93 expect(await faktor()).toBeCloseTo(1, 2);
94 });
95
96 test('überlebt einen Neustart der Anwendung', async () => {
97 await fenster.getByRole('button', { name: /Größer/u }).click();
98 await fenster.getByRole('button', { name: /Größer/u }).click();
99 await expect(fenster.getByText('Aktuell: 125 Prozent')).toBeVisible();
100
101 /* Neu laden ersetzt keinen echten Neustart, prüft aber denselben Weg:
102 Der Hauptprozess legt die gespeicherte Größe auf das frisch geladene
103 Dokument. Ohne das blitzte die Anwendung beim Start klein auf. */
104 await fenster.reload();
105 await fenster.getByRole('heading', { level: 1 }).waitFor();
106
107 expect(await faktor()).toBeCloseTo(1.25, 2);
108 await expect(fenster.getByText('Aktuell: 125 Prozent')).toBeVisible();
109
110 await fenster.getByRole('button', { name: 'Normalgröße' }).click();
111 });
112
113 /*
114 Ganz am Ende, und das ist Absicht: Die Tests darueber haengen aneinander -
115 „kehrt auf Normalgroesse zurueck" verlaesst sich darauf, dass der vorige
116 Test bei der obersten Stufe endet, sonst ist der Knopf gesperrt. Dazwischen
117 eingeschoben brach dieser Block die Kette. Er stellt am Anfang jedes
118 Tests selbst her, was er braucht.
119 */
120 test.describe('über die Tastatur', () => {
121 /*
122 Gemeldet und nachgemessen: `Strg` und `+` taten unter Windows nichts.
123 Electron liest den Beschleunigernamen `Plus` als die umgeschaltete Form
124 von `=` und meldet damit faktisch `Strg+Umschalt+=` beim Betriebssystem
125 an – richtig für eine amerikanische Tastatur, falsch für eine deutsche,
126 auf der `+` eine eigene Taste ohne Umschalt ist.
127
128 Abgefangen wird deshalb im Renderer (`hooks/useZoomtasten.ts`), und zwar
129 über das Zeichen, das die Tastatur tatsächlich liefert. Damit ist es
130 auch prüfbar: Ein Menü-Beschleuniger läuft über die Beschleunigertabelle
131 des Betriebssystems und wird von keinem Prüflauf erreicht – nachgemessen
132 ebenso wie `before-input-event` im Hauptprozess, das für Tasten aus
133 einem Prüflauf überhaupt nichts meldet.
134 */
135 test.beforeEach(async () => {
136 await fenster.keyboard.press('Control+0');
137 await fenster.waitForTimeout(200);
138 });
139
140 test('vergrößert über alle drei Schreibweisen des Pluszeichens', async () => {
141 // Die deutsche Plustaste: dieselbe Taste wie `=` auf US-Layout.
142 await fenster.keyboard.press('Control+=');
143 await fenster.waitForTimeout(200);
144 expect(await faktor(), 'Strg und + (deutsche Tastatur)').toBeCloseTo(1.1, 2);
145
146 // Die amerikanische Schreibweise, mit Umschalt.
147 await fenster.keyboard.press('Control+Shift+=');
148 await fenster.waitForTimeout(200);
149 expect(await faktor(), 'Strg, Umschalt und +').toBeCloseTo(1.25, 2);
150
151 // Der Ziffernblock.
152 await fenster.keyboard.press('Control+NumpadAdd');
153 await fenster.waitForTimeout(200);
154 expect(await faktor(), 'Strg und + auf dem Ziffernblock').toBeCloseTo(1.5, 2);
155 });
156
157 test('verkleinert und setzt zurück', async () => {
158 await fenster.keyboard.press('Control+=');
159 await fenster.keyboard.press('Control+=');
160 await fenster.waitForTimeout(250);
161 expect(await faktor()).toBeCloseTo(1.25, 2);
162
163 await fenster.keyboard.press('Control+-');
164 await fenster.waitForTimeout(200);
165 expect(await faktor()).toBeCloseTo(1.1, 2);
166
167 await fenster.keyboard.press('Control+0');
168 await fenster.waitForTimeout(200);
169 expect(await faktor()).toBeCloseTo(1, 2);
170 });
171
172 test('wirkt auch, wenn der Fokus in einem Textfeld steht', async () => {
173 /* Wer eine offene Frage tippt und die Schrift zu klein findet, soll
174 nicht erst irgendwohin klicken müssen. */
175 const feld = fenster.getByLabel('Ihr Prüfungstermin');
176 await feld.click();
177 await fenster.keyboard.press('Control+=');
178 await fenster.waitForTimeout(200);
179 expect(await faktor()).toBeCloseTo(1.1, 2);
180 await fenster.keyboard.press('Control+0');
181 await fenster.waitForTimeout(200);
182 });
183
184 test('schluckt keine fremde Tastenkombination', async () => {
185 /* Der Zuhörer liegt am Fenster und sieht jede Taste mit Strg. Er darf
186 nur die drei eigenen abfangen – sonst bräche er Kopieren, Einfügen
187 und alles Weitere. */
188 const vorher = await faktor();
189 for (const taste of ['Control+a', 'Control+c', 'Control+v', 'Control+1', 'Control+9']) {
190 await fenster.keyboard.press(taste);
191 }
192 await fenster.waitForTimeout(250);
193 expect(await faktor()).toBeCloseTo(vorher, 2);
194 });
195 });
196 });
197
198 /* =====================================================================
199 Reflow – WCAG 2.2, 1.4.10
200 ===================================================================== */
201
202 /** Was eine Messung zurückgibt. */
203 interface Reflowmass {
204 /** Sichtbare Breite des Dokuments in CSS-Pixeln, ohne Rollbalken. */
205 readonly innen: number;
206 /** Tatsächliche Breite des Dokuments; darf `innen` nicht überschreiten. */
207 readonly doku: number;
208 /** Elemente, deren Inhalt über den eigenen Kasten hinausragt. */
209 readonly ueberlauf: readonly string[];
210 }
211
212 /**
213 * Misst den waagerechten und den senkrechten Überlauf der stehenden Ansicht.
214 *
215 * ## Was gegenüber der ersten Fassung schärfer geworden ist
216 *
217 * Die Vorgängerin dieser Messung war zu großzügig, und zwar an vier Stellen
218 * zugleich. Nachgezählt:
219 *
220 * 1. **Der Filter warf zu viel weg.** `kasten.width <= 1 || kasten.height <= 1`
221 * verwarf jedes Element, das in *einer* Richtung dünn ist – eine Trennlinie,
222 * ein Fortschrittsbalken, ein Rahmenstreifen. Gemeint war der Klammergriff
223 * für Screenreader-Texte (`.nur-screenreader`), und der ist in **beiden**
224 * Richtungen 1 Pixel groß. Jetzt steht dort `&&`.
225 * 2. **Die Toleranz war fast 2 Pixel.** Verglichen wurde `scrollWidth` (ohne
226 * Rahmen) gegen `Math.ceil(kasten.width) + 1` (mit Rahmen). Bei den hier
227 * üblichen 2 Pixel starken Rahmen ließ das rund 4 Pixel Überstand
228 * durchgehen. Jetzt stehen `scrollWidth` und `clientWidth` gegeneinander –
229 * beide ohne Rahmen – mit einer Toleranz von 1 Pixel für die Rundung: Beide
230 * Werte sind ganzzahlig, ein Layout mit Bruchteilen kann sie um eins
231 * auseinanderfallen lassen.
232 * 3. **Der Bezug war zu weit.** `window.innerWidth` zählt den senkrechten
233 * Rollbalken mit; die Seite durfte also um dessen Breite zu breit sein.
234 * Gemessen wird jetzt gegen `documentElement.clientWidth`.
235 * 4. **Senkrecht wurde gar nicht gemessen.** Abgeschnittener Inhalt in einem
236 * Kasten mit fester Höhe fiel nicht auf. Jetzt fällt er auf – ausgenommen
237 * die Seite selbst, denn senkrechtes Rollen erlaubt 1.4.10 ausdrücklich.
238 *
239 * Ausgenommen bleiben drei Fälle, jeder mit Grund:
240 *
241 * - **Absichtliche Rollbereiche** (`overflow-x`/`-y: auto` oder `scroll`).
242 * Genau dort liegt die Verlaufstabelle, und für Inhalte, die eine
243 * zweidimensionale Anordnung brauchen, lässt 1.4.10 das zu.
244 * - **Textfelder und Auswahllisten.** Ein `<input>` rollt seinen eigenen Wert
245 * von Haus aus; das ist keine verlorene Information.
246 * - **Unsichtbares** – Kästen ohne Fläche und `visibility: hidden`.
247 */
248 async function reflowMessen(seite: Page): Promise<Reflowmass> {
249 return seite.evaluate(() => {
250 const wurzel = document.documentElement;
251 const ueberlauf: string[] = [];
252
253 for (const element of [...document.querySelectorAll('*')]) {
254 const kasten = element.getBoundingClientRect();
255
256 if (kasten.width === 0 || kasten.height === 0) {
257 continue;
258 }
259 if (kasten.width <= 1 && kasten.height <= 1) {
260 continue;
261 }
262 if (element.tagName === 'INPUT' || element.tagName === 'SELECT') {
263 continue;
264 }
265
266 const stil = getComputedStyle(element);
267 if (stil.visibility === 'hidden') {
268 continue;
269 }
270
271 const istSeite = element === wurzel || element === document.body;
272 const rolltWaagerecht = stil.overflowX === 'auto' || stil.overflowX === 'scroll';
273 const rolltSenkrecht = istSeite || stil.overflowY === 'auto' || stil.overflowY === 'scroll';
274
275 const klasse = element.getAttribute('class');
276 const name = element.tagName + (klasse === null ? '' : `.${klasse.slice(0, 40)}`);
277
278 const zuBreit = element.scrollWidth - element.clientWidth;
279 if (!rolltWaagerecht && zuBreit > 1) {
280 ueberlauf.push(`${name}: ${String(zuBreit)} px zu breit`);
281 }
282
283 const zuHoch = element.scrollHeight - element.clientHeight;
284 if (!rolltSenkrecht && zuHoch > 1) {
285 ueberlauf.push(`${name}: ${String(zuHoch)} px zu hoch`);
286 }
287 }
288
289 return {
290 innen: wurzel.clientWidth,
291 doku: wurzel.scrollWidth,
292 // Zwölf reichen zum Suchen; hundert Zeilen im Fehlerbericht helfen nicht.
293 ueberlauf: ueberlauf.slice(0, 12),
294 };
295 });
296 }
297
298 /** Setzt die Anzeigegröße über denselben Weg, den auch die Oberfläche geht. */
299 async function anzeigegroesseSetzen(prozent: number): Promise<void> {
300 await fenster.evaluate(async (wert: number) => {
301 const bruecke = (
302 window as unknown as {
303 lernApp?: { anzeigegroesseSetzen: (p: number) => Promise<number> };
304 }
305 ).lernApp;
306 await bruecke?.anzeigegroesseSetzen(wert);
307 }, prozent);
308 await fenster.waitForTimeout(250);
309 }
310
311 test.describe('Reflow bei 320 CSS-Pixeln (WCAG 1.4.10)', () => {
312 /*
313 Eigener Block am Ende, aus demselben Grund wie der Tastaturblock: Er
314 verstellt Fenstergröße und Anzeigegröße und stellt beides danach wieder
315 her. Dazwischen eingeschoben zerrisse er die Kette der Tests darüber.
316
317 1280 Pixel Inhaltsbreite mal 400 Prozent ergibt genau die 320 CSS-Pixel,
318 an denen das Kriterium gemessen wird. `setContentSize` und nicht
319 `setSize`: Der Fensterrahmen zählt nicht zum Inhalt, und um den geht es.
320 */
321 const URSPRUNG = { breite: 1180, hoehe: 820 };
322
323 test.beforeAll(async () => {
324 await app?.evaluate(({ BrowserWindow }) => {
325 BrowserWindow.getAllWindows()[0]?.setContentSize(1280, 1024);
326 });
327 await fenster.waitForTimeout(300);
328 });
329
330 test.afterAll(async () => {
331 await anzeigegroesseSetzen(100);
332 await app?.evaluate(({ BrowserWindow }, groesse) => {
333 BrowserWindow.getAllWindows()[0]?.setContentSize(groesse.breite, groesse.hoehe);
334 }, URSPRUNG);
335 });
336
337 /** Zurück auf den Startbildschirm, egal wo der vorige Test aufgehört hat. */
338 async function zumStart(): Promise<void> {
339 if ((await fenster.getByRole('dialog').count()) > 0) {
340 await fenster.keyboard.press('Escape');
341 await fenster.waitForTimeout(150);
342 }
343
344 const ausstiege = [
345 /^Ohne Auswertung verwerfen$/u,
346 /^Auswertung anzeigen$/u,
347 /^Simulation abbrechen$/u,
348 /^Sitzung beenden$/u,
349 /^Zum Start$/u,
350 ];
351
352 for (let i = 0; i < 8; i++) {
353 if (await amStart(fenster)) {
354 return;
355 }
356
357 let geklickt = false;
358 for (const weg of ausstiege) {
359 const knopf = fenster.getByRole('button', { name: weg });
360 if ((await knopf.count()) > 0 && (await knopf.first().isVisible())) {
361 await knopf.first().click();
362 await fenster.waitForTimeout(250);
363 geklickt = true;
364 break;
365 }
366 }
367
368 if (!geklickt) {
369 return;
370 }
371 }
372 }
373
374 /**
375 * Die Ansichten, die der Prüfplan als Härtefälle führt (Zeile H8).
376 *
377 * Hingeführt wird bei 100 Prozent: Auf 320 CSS-Pixeln liegt zwischen zwei
378 * Schaltflächen ein halber Bildschirm, und jeder Klick würde zur
379 * Rollübung. Gemessen wird danach – der Zustand ist derselbe.
380 */
381 const ANSICHTEN: readonly { readonly name: string; readonly hin: () => Promise<void> }[] = [
382 {
383 name: 'Startbildschirm mit Lernstand',
384 hin: async () => {
385 await zumStart();
386 },
387 },
388 {
389 /*
390 Befund der Pruefrunde zu 0.27.2: Diese Ansicht fehlte hier, und genau
391 in ihr ist mit 0.27.1 ein Raster entstanden, dessen Mindestspalte
392 20 rem breit ist – also 320 CSS-Pixel plus Innenabstand. Bei 400
393 Prozent Anzeigegroesse sprengt das die Seite waagerecht. Die Messung,
394 die docs/stand.md als Beleg fuer 1.4.10 fuehrt, stammt von vor diesem
395 Umbau.
396 */
397 name: 'Kapitelwahl mit Lernstand',
398 hin: async () => {
399 await zumStart();
400 await fenster.getByRole('button', { name: /Kapitel waehlen|Kapitel wählen/u }).click();
401 await fenster.getByRole('heading', { name: 'Kapitel wählen', level: 1 }).waitFor();
402 },
403 },
404 {
405 /*
406 Die Datenschutzerklaerung fuehrt die einzige dreispaltige Tabelle der
407 Anwendung. Der Kommentar in ueber.css sagte ihr bis 0.27.2 einen
408 waagerecht rollenden Rahmen zu, den es nie gab – gemessen worden ist
409 die Ansicht hier nie.
410 */
411 name: 'Ueber diese Software mit Datenschutzerklaerung',
412 hin: async () => {
413 await zumStart();
414 await fenster.getByRole('button', { name: 'Lizenzen und Herkunft anzeigen' }).click();
415 await fenster.getByRole('heading', { name: 'Über diese Software', level: 1 }).waitFor();
416 /* Die Erklaerung steht zugeklappt; aufgeklappt bringt sie die einzige
417 dreispaltige Tabelle der Anwendung mit. */
418 await fenster.getByRole('button', { name: 'Datenschutzerklärung lesen' }).click();
419 await fenster.getByRole('button', { name: 'Datenschutzerklärung einklappen' }).waitFor();
420 },
421 },
422 {
423 name: 'Hilfe',
424 hin: async () => {
425 await zumStart();
426 await fenster.getByRole('contentinfo').getByRole('button', { name: 'Hilfe' }).click();
427 await fenster.getByRole('dialog', { name: 'Hilfe' }).waitFor();
428 },
429 },
430 {
431 name: 'Trefferliste der Suche',
432 hin: async () => {
433 /* Der Suchbegriff ist mit Bedacht gewählt: „waffenbesitzkarten-
434 pflichtigen" ist mit 29 Zeichen das längste Wort im amtlichen
435 Katalog. Damit steht der harte Fall des Prüfplans – ein langer
436 amtlicher Text – zuverlässig in der Liste und nicht nur zufällig. */
437 await zumStart();
438 await fenster.getByRole('button', { name: /Fragen durchsuchen/u }).click();
439 await fenster.getByRole('heading', { name: 'Fragen durchsuchen', level: 1 }).waitFor();
440 await fenster.getByLabel('Suchbegriff').fill('waffenbesitzkartenpflichtigen');
441 await fenster.waitForTimeout(500);
442 },
443 },
444 {
445 name: 'Lernsitzung nach der Antwort',
446 hin: async () => {
447 await zumStart();
448 await fenster.getByRole('button', { name: /Weiterlernen/u }).click();
449 await fenster
450 .getByRole('button', { name: /^(Antwort bestätigen|Musterantwort anzeigen)$/u })
451 .waitFor();
452
453 const auswahlfeld = fenster.getByRole('radio').or(fenster.getByRole('checkbox'));
454 if ((await auswahlfeld.count()) > 0) {
455 await auswahlfeld.first().click();
456 await fenster.getByRole('button', { name: 'Antwort bestätigen' }).click();
457 } else {
458 await fenster.getByRole('button', { name: 'Musterantwort anzeigen' }).click();
459 await fenster.getByRole('button', { name: 'Nicht gewusst' }).click();
460 }
461 await fenster.locator('.ergebnis').first().waitFor();
462 },
463 },
464 {
465 name: 'Prüfungslauf mit Fragenübersicht',
466 hin: async () => {
467 await zumStart();
468 await fenster.getByRole('button', { name: /Prüfungssimulation vorbereiten/u }).click();
469 await fenster.getByRole('radio', { name: /Selbst einstellen/u }).check();
470 await fenster.getByLabel('Fragen im Bogen', { exact: true }).fill('40');
471 await fenster.getByLabel('Bearbeitungszeit', { exact: true }).fill('60');
472 await fenster.getByLabel('Anteil offener Fragen', { exact: true }).fill('0');
473 await fenster.getByRole('button', { name: 'Simulation starten' }).click();
474 await fenster.getByRole('button', { name: 'Simulation abgeben' }).waitFor();
475 /* Eine Antwort setzen: Erst dann führt die Übersicht beide Zustände
476 nebeneinander, und erst dann ist ihr Raster wirklich gefüllt. */
477 await fenster.getByRole('radio').or(fenster.getByRole('checkbox')).first().click();
478 },
479 },
480 {
481 name: 'Prüfungsauswertung mit Verlaufstabelle',
482 hin: async () => {
483 /* Kein `zumStart()`: Dieser Schritt gibt den Bogen ab, den der vorige
484 gestartet hat. Ihn eigens noch einmal zusammenzustellen kostete
485 einen zweiten Durchlauf durch die Prüfungswahl, und die Auswertung
486 bekäme dieselben Zahlen. */
487 await fenster.getByRole('button', { name: 'Simulation abgeben' }).click();
488 await fenster.getByRole('button', { name: 'Jetzt abgeben und auswerten' }).click();
489 await fenster
490 .getByRole('heading', { name: 'Auswertung der Prüfungssimulation', level: 1 })
491 .waitFor();
492 /* Der Verlauf kommt aus der Datenbank nach. Ohne dieses Warten misst
493 der Lauf den Ladehinweis statt der Tabelle. */
494 await fenster.getByText('Der Verlauf wird geladen …').waitFor({ state: 'hidden' });
495 },
496 },
497 ];
498
499 for (const ansicht of ANSICHTEN) {
500 test(ansicht.name, async () => {
501 await anzeigegroesseSetzen(100);
502 await ansicht.hin();
503 await anzeigegroesseSetzen(400);
504
505 const mass = await reflowMessen(fenster);
506
507 expect(
508 mass.innen,
509 `Gemessen wird bei ${String(mass.innen)} CSS-Pixeln – das ist nicht der Prüffall.`,
510 ).toBeLessThanOrEqual(320);
511 expect(
512 mass.innen,
513 `Nur ${String(mass.innen)} CSS-Pixel: Die Messung wäre strenger als das Kriterium.`,
514 ).toBeGreaterThan(300);
515
516 expect(
517 mass.doku,
518 `${ansicht.name}: Die Seite ist ${String(mass.doku)} px breit, sichtbar sind ` +
519 `${String(mass.innen)} px – seitliches Rollen für Fließtext.`,
520 ).toBeLessThanOrEqual(mass.innen);
521
522 expect(mass.ueberlauf, `${ansicht.name}: Inhalt ragt über seinen Kasten hinaus.`).toEqual([]);
523 });
524 }
525
526 test('die Messung fängt einen absichtlich erzeugten Überlauf', async () => {
527 /*
528 Gegenprobe zur Gegenprobe.
529
530 Eine Messung, die immer grün ist, sagt nichts. Hier bekommt die Seite
531 nacheinander drei Fehler eingesetzt, die sie in Wirklichkeit nicht hat –
532 einen Kasten zwei Pixel breiter als sein Elternteil, ein unteilbares
533 langes Wort und einen abgeschnittenen Kasten. Jeder muss auffallen,
534 danach muss die Seite wieder sauber sein.
535
536 Die zwei Pixel sind der Punkt: Die frühere Messung ließ durch ihren
537 Bezug auf `getBoundingClientRect().width` plus Rundung rund vier Pixel
538 durchgehen und hätte diesen Fall verschwiegen.
539 */
540 await anzeigegroesseSetzen(100);
541 await zumStart();
542 await anzeigegroesseSetzen(400);
543
544 const vorher = await reflowMessen(fenster);
545 expect(vorher.ueberlauf, 'Die Ansicht ist vor der Gegenprobe nicht sauber.').toEqual([]);
546
547 for (const fall of ['zweiPixel', 'langesWort', 'abgeschnitten'] as const) {
548 await fenster.evaluate((art: string) => {
549 const wirt = document.querySelector('.karte');
550 if (wirt === null) {
551 throw new Error('Keine Karte gefunden, an der sich messen ließe.');
552 }
553 const stoerer = document.createElement('div');
554 stoerer.id = 'gegenprobe';
555
556 if (art === 'zweiPixel') {
557 stoerer.style.inlineSize = `${String(wirt.clientWidth + 2)}px`;
558 stoerer.style.blockSize = '20px';
559 } else if (art === 'langesWort') {
560 stoerer.style.whiteSpace = 'nowrap';
561 stoerer.textContent = 'Deaktivierungsdurchfuehrungsverordnungsaenderungsgesetz';
562 } else {
563 stoerer.style.blockSize = '20px';
564 stoerer.style.overflow = 'hidden';
565 stoerer.textContent = 'Eine Zeile, zwei Zeilen, drei Zeilen, vier Zeilen, fuenf Zeilen.';
566 }
567
568 wirt.appendChild(stoerer);
569 }, fall);
570
571 const mitFehler = await reflowMessen(fenster);
572 expect(
573 mitFehler.ueberlauf.length,
574 `Die Messung übersieht den Fall „${fall}".`,
575 ).toBeGreaterThan(0);
576
577 await fenster.evaluate(() => {
578 document.getElementById('gegenprobe')?.remove();
579 });
580 }
581
582 const nachher = await reflowMessen(fenster);
583 expect(nachher.ueberlauf, 'Die Gegenprobe hat etwas hinterlassen.').toEqual([]);
584 expect(nachher.doku).toBeLessThanOrEqual(nachher.innen);
585 });
586
587 test('das Fenster lässt sich bis auf 320 CSS-Pixel schmal ziehen', async () => {
588 /*
589 Der zweite Weg zu 320 Pixeln, unabhängig vom Zoom: das Fenster selbst.
590
591 Bis Fassung 0.20.0 stand `minWidth` auf 720 – zusammen mit einer Leiter
592 bis 200 Prozent war der schmalste erreichbare Bereich 360 CSS-Pixel, der
593 Prüffall also nicht herstellbar. Geprüft wird an der eingestellten
594 Mindestgröße und nicht an der gemessenen Breite: `minWidth` meint die
595 Fenster-, nicht die Inhaltsbreite, und wie viel der Rahmen davon
596 abzieht, entscheidet das Betriebssystem.
597 */
598 const minBreite =
599 (await app?.evaluate(
600 ({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.getMinimumSize()[0] ?? 0,
601 )) ?? 0;
602
603 expect(minBreite, 'Das Fenster lässt sich nicht schmal genug ziehen.').toBeLessThanOrEqual(400);
604 expect(
605 minBreite,
606 'Unter 320 Pixeln gibt es keine Zusage mehr – so schmal soll es nicht gehen.',
607 ).toBeGreaterThanOrEqual(320);
608
609 // Und die Ansicht trägt auch bei dieser Breite, bei 100 Prozent Anzeige.
610 await anzeigegroesseSetzen(100);
611 await zumStart();
612 await app?.evaluate(({ BrowserWindow }) => {
613 BrowserWindow.getAllWindows()[0]?.setSize(200, 800);
614 });
615 await fenster.waitForTimeout(300);
616
617 const mass = await reflowMessen(fenster);
618 expect(mass.doku, 'Am schmalsten Fenster rollt die Seite seitlich.').toBeLessThanOrEqual(
619 mass.innen,
620 );
621 expect(mass.ueberlauf, 'Am schmalsten Fenster ragt Inhalt aus seinem Kasten.').toEqual([]);
622
623 await app?.evaluate(({ BrowserWindow }) => {
624 BrowserWindow.getAllWindows()[0]?.setContentSize(1280, 1024);
625 });
626 await fenster.waitForTimeout(300);
627 });
628 });