waffensachkunde

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

/ app e2e anzeigegroesse.spec.ts

23,9 KB Rohdatei
app/e2e/anzeigegroesse.spec.ts — 594 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 name: 'Hilfe',
390 hin: async () => {
391 await zumStart();
392 await fenster.getByRole('contentinfo').getByRole('button', { name: 'Hilfe' }).click();
393 await fenster.getByRole('dialog', { name: 'Hilfe' }).waitFor();
394 },
395 },
396 {
397 name: 'Trefferliste der Suche',
398 hin: async () => {
399 /* Der Suchbegriff ist mit Bedacht gewählt: „waffenbesitzkarten-
400 pflichtigen" ist mit 29 Zeichen das längste Wort im amtlichen
401 Katalog. Damit steht der harte Fall des Prüfplans – ein langer
402 amtlicher Text – zuverlässig in der Liste und nicht nur zufällig. */
403 await zumStart();
404 await fenster.getByRole('button', { name: /Fragen durchsuchen/u }).click();
405 await fenster.getByRole('heading', { name: 'Fragen durchsuchen', level: 1 }).waitFor();
406 await fenster.getByLabel('Suchbegriff').fill('waffenbesitzkartenpflichtigen');
407 await fenster.waitForTimeout(500);
408 },
409 },
410 {
411 name: 'Lernsitzung nach der Antwort',
412 hin: async () => {
413 await zumStart();
414 await fenster.getByRole('button', { name: /Weiterlernen/u }).click();
415 await fenster
416 .getByRole('button', { name: /^(Antwort bestätigen|Musterantwort anzeigen)$/u })
417 .waitFor();
418
419 const auswahlfeld = fenster.getByRole('radio').or(fenster.getByRole('checkbox'));
420 if ((await auswahlfeld.count()) > 0) {
421 await auswahlfeld.first().click();
422 await fenster.getByRole('button', { name: 'Antwort bestätigen' }).click();
423 } else {
424 await fenster.getByRole('button', { name: 'Musterantwort anzeigen' }).click();
425 await fenster.getByRole('button', { name: 'Nicht gewusst' }).click();
426 }
427 await fenster.locator('.ergebnis').first().waitFor();
428 },
429 },
430 {
431 name: 'Prüfungslauf mit Fragenübersicht',
432 hin: async () => {
433 await zumStart();
434 await fenster.getByRole('button', { name: /Prüfungssimulation vorbereiten/u }).click();
435 await fenster.getByRole('radio', { name: /Selbst einstellen/u }).check();
436 await fenster.getByLabel('Fragen im Bogen', { exact: true }).fill('40');
437 await fenster.getByLabel('Bearbeitungszeit', { exact: true }).fill('60');
438 await fenster.getByLabel('Anteil offener Fragen', { exact: true }).fill('0');
439 await fenster.getByRole('button', { name: 'Simulation starten' }).click();
440 await fenster.getByRole('button', { name: 'Simulation abgeben' }).waitFor();
441 /* Eine Antwort setzen: Erst dann führt die Übersicht beide Zustände
442 nebeneinander, und erst dann ist ihr Raster wirklich gefüllt. */
443 await fenster.getByRole('radio').or(fenster.getByRole('checkbox')).first().click();
444 },
445 },
446 {
447 name: 'Prüfungsauswertung mit Verlaufstabelle',
448 hin: async () => {
449 /* Kein `zumStart()`: Dieser Schritt gibt den Bogen ab, den der vorige
450 gestartet hat. Ihn eigens noch einmal zusammenzustellen kostete
451 einen zweiten Durchlauf durch die Prüfungswahl, und die Auswertung
452 bekäme dieselben Zahlen. */
453 await fenster.getByRole('button', { name: 'Simulation abgeben' }).click();
454 await fenster.getByRole('button', { name: 'Jetzt abgeben und auswerten' }).click();
455 await fenster
456 .getByRole('heading', { name: 'Auswertung der Prüfungssimulation', level: 1 })
457 .waitFor();
458 /* Der Verlauf kommt aus der Datenbank nach. Ohne dieses Warten misst
459 der Lauf den Ladehinweis statt der Tabelle. */
460 await fenster.getByText('Der Verlauf wird geladen …').waitFor({ state: 'hidden' });
461 },
462 },
463 ];
464
465 for (const ansicht of ANSICHTEN) {
466 test(ansicht.name, async () => {
467 await anzeigegroesseSetzen(100);
468 await ansicht.hin();
469 await anzeigegroesseSetzen(400);
470
471 const mass = await reflowMessen(fenster);
472
473 expect(
474 mass.innen,
475 `Gemessen wird bei ${String(mass.innen)} CSS-Pixeln – das ist nicht der Prüffall.`,
476 ).toBeLessThanOrEqual(320);
477 expect(
478 mass.innen,
479 `Nur ${String(mass.innen)} CSS-Pixel: Die Messung wäre strenger als das Kriterium.`,
480 ).toBeGreaterThan(300);
481
482 expect(
483 mass.doku,
484 `${ansicht.name}: Die Seite ist ${String(mass.doku)} px breit, sichtbar sind ` +
485 `${String(mass.innen)} px – seitliches Rollen für Fließtext.`,
486 ).toBeLessThanOrEqual(mass.innen);
487
488 expect(mass.ueberlauf, `${ansicht.name}: Inhalt ragt über seinen Kasten hinaus.`).toEqual([]);
489 });
490 }
491
492 test('die Messung fängt einen absichtlich erzeugten Überlauf', async () => {
493 /*
494 Gegenprobe zur Gegenprobe.
495
496 Eine Messung, die immer grün ist, sagt nichts. Hier bekommt die Seite
497 nacheinander drei Fehler eingesetzt, die sie in Wirklichkeit nicht hat –
498 einen Kasten zwei Pixel breiter als sein Elternteil, ein unteilbares
499 langes Wort und einen abgeschnittenen Kasten. Jeder muss auffallen,
500 danach muss die Seite wieder sauber sein.
501
502 Die zwei Pixel sind der Punkt: Die frühere Messung ließ durch ihren
503 Bezug auf `getBoundingClientRect().width` plus Rundung rund vier Pixel
504 durchgehen und hätte diesen Fall verschwiegen.
505 */
506 await anzeigegroesseSetzen(100);
507 await zumStart();
508 await anzeigegroesseSetzen(400);
509
510 const vorher = await reflowMessen(fenster);
511 expect(vorher.ueberlauf, 'Die Ansicht ist vor der Gegenprobe nicht sauber.').toEqual([]);
512
513 for (const fall of ['zweiPixel', 'langesWort', 'abgeschnitten'] as const) {
514 await fenster.evaluate((art: string) => {
515 const wirt = document.querySelector('.karte');
516 if (wirt === null) {
517 throw new Error('Keine Karte gefunden, an der sich messen ließe.');
518 }
519 const stoerer = document.createElement('div');
520 stoerer.id = 'gegenprobe';
521
522 if (art === 'zweiPixel') {
523 stoerer.style.inlineSize = `${String(wirt.clientWidth + 2)}px`;
524 stoerer.style.blockSize = '20px';
525 } else if (art === 'langesWort') {
526 stoerer.style.whiteSpace = 'nowrap';
527 stoerer.textContent = 'Deaktivierungsdurchfuehrungsverordnungsaenderungsgesetz';
528 } else {
529 stoerer.style.blockSize = '20px';
530 stoerer.style.overflow = 'hidden';
531 stoerer.textContent = 'Eine Zeile, zwei Zeilen, drei Zeilen, vier Zeilen, fuenf Zeilen.';
532 }
533
534 wirt.appendChild(stoerer);
535 }, fall);
536
537 const mitFehler = await reflowMessen(fenster);
538 expect(
539 mitFehler.ueberlauf.length,
540 `Die Messung übersieht den Fall „${fall}".`,
541 ).toBeGreaterThan(0);
542
543 await fenster.evaluate(() => {
544 document.getElementById('gegenprobe')?.remove();
545 });
546 }
547
548 const nachher = await reflowMessen(fenster);
549 expect(nachher.ueberlauf, 'Die Gegenprobe hat etwas hinterlassen.').toEqual([]);
550 expect(nachher.doku).toBeLessThanOrEqual(nachher.innen);
551 });
552
553 test('das Fenster lässt sich bis auf 320 CSS-Pixel schmal ziehen', async () => {
554 /*
555 Der zweite Weg zu 320 Pixeln, unabhängig vom Zoom: das Fenster selbst.
556
557 Bis Fassung 0.20.0 stand `minWidth` auf 720 – zusammen mit einer Leiter
558 bis 200 Prozent war der schmalste erreichbare Bereich 360 CSS-Pixel, der
559 Prüffall also nicht herstellbar. Geprüft wird an der eingestellten
560 Mindestgröße und nicht an der gemessenen Breite: `minWidth` meint die
561 Fenster-, nicht die Inhaltsbreite, und wie viel der Rahmen davon
562 abzieht, entscheidet das Betriebssystem.
563 */
564 const minBreite =
565 (await app?.evaluate(
566 ({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.getMinimumSize()[0] ?? 0,
567 )) ?? 0;
568
569 expect(minBreite, 'Das Fenster lässt sich nicht schmal genug ziehen.').toBeLessThanOrEqual(400);
570 expect(
571 minBreite,
572 'Unter 320 Pixeln gibt es keine Zusage mehr – so schmal soll es nicht gehen.',
573 ).toBeGreaterThanOrEqual(320);
574
575 // Und die Ansicht trägt auch bei dieser Breite, bei 100 Prozent Anzeige.
576 await anzeigegroesseSetzen(100);
577 await zumStart();
578 await app?.evaluate(({ BrowserWindow }) => {
579 BrowserWindow.getAllWindows()[0]?.setSize(200, 800);
580 });
581 await fenster.waitForTimeout(300);
582
583 const mass = await reflowMessen(fenster);
584 expect(mass.doku, 'Am schmalsten Fenster rollt die Seite seitlich.').toBeLessThanOrEqual(
585 mass.innen,
586 );
587 expect(mass.ueberlauf, 'Am schmalsten Fenster ragt Inhalt aus seinem Kasten.').toEqual([]);
588
589 await app?.evaluate(({ BrowserWindow }) => {
590 BrowserWindow.getAllWindows()[0]?.setContentSize(1280, 1024);
591 });
592 await fenster.waitForTimeout(300);
593 });
594 });