waffensachkunde

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

/ app e2e fensterlage.spec.ts

7,1 KB Rohdatei
app/e2e/fensterlage.spec.ts — 180 Zeilen
1 /**
2 * Das Fenster muss **sichtbar** werden — auch mit gemerkter Lage.
3 *
4 * ## Der Vorfall, dem diese Datei ihr Dasein verdankt
5 *
6 * Seit 0.23.0 merkt sich die Anwendung Größe und Lage des Fensters. Am
7 * 29.08.2026 zeigte sich: Sobald eine Position gespeichert war und beim Start
8 * wieder angelegt wurde, starb auf einem Windows-11-Rechner der GPU-Prozess
9 * (`GPU process exited unexpectedly: exit_code=-1`), danach der Renderer.
10 * `ready-to-show` feuerte deshalb nie — und weil das Fenster erst dort
11 * gezeigt wurde, blieb es für immer unsichtbar.
12 *
13 * Für den Anwender: Die Anwendung startet einmal, er verschiebt das Fenster,
14 * und danach lässt sie sich nie wieder öffnen. Kein Fenster, keine Meldung,
15 * vier laufende Prozesse. An die Einstellung kommt er nicht heran, weil dafür
16 * ein Fenster nötig wäre.
17 *
18 * ## Warum keine der 189 vorhandenen Prüfungen das gefunden hat
19 *
20 * Playwright greift das Fenster über die Entwicklerschnittstelle, nicht über
21 * den Bildschirm. `firstWindow()` liefert eine Seite, sobald der Renderer da
22 * ist — **ob das Fenster jemals gezeigt wurde, fragt niemand.** Die ganze
23 * Prüfkette lief grün gegen eine Anwendung, die kein Mensch hätte benutzen
24 * können.
25 *
26 * Zweitens legt jeder andere Lauf ein **frisches** Profil an. Ohne
27 * gespeicherte Lage tritt der Fehler nicht auf. Diese Datei ist deshalb die
28 * einzige, die ein Profil **vorbelegt**.
29 *
30 * Geprüft wird darum genau das, was fehlte: `isVisible()` am echten Fenster.
31 */
32 import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
33 import { tmpdir } from 'node:os';
34 import { join } from 'node:path';
35
36 import type { BrowserWindow } from 'electron';
37
38 import {
39 _electron as electron,
40 type ElectronApplication,
41 type JSHandle,
42 type Page,
43 } from '@playwright/test';
44
45 import { paketstand } from '../tools/paketstand.mjs';
46 import { projektWurzel, weiterAmZuschnitt } from './electron-hilfe';
47 import { expect, konsoleErwartet, konsolenwacheAnhaengen, test } from './konsolenwache';
48
49 const EXE = join(projektWurzel, 'release', 'win-unpacked', 'Waffensachkunde Lernsoftware.exe');
50
51 /**
52 * Eine gespeicherte Lage, wie sie nach dem ersten Verschieben entsteht.
53 *
54 * Die Zahlen stammen aus dem Profil, an dem der Fehler auftrat. Sie liegen
55 * mitten auf einem gewöhnlichen Bildschirm — es ist keine abwegige Lage,
56 * sondern die alltäglichste, die es gibt.
57 */
58 const GEMERKTE_LAGE = { breite: 1182, hoehe: 823, x: 507, y: 183 };
59
60 /**
61 * So lange darf es dauern, bis das Fenster steht.
62 *
63 * Großzügig über der Notbremse in `fenster.ts` (4 s): Der Test soll den
64 * Rückfall noch als Erfolg werten — gezeigt ist gezeigt —, aber nicht
65 * unbegrenzt warten.
66 */
67 const GEDULD_MS = 20_000;
68
69 /**
70 * Wie weit die gemeldete Groesse von der gesetzten abweichen darf.
71 *
72 * Windows legt um ein veränderbares Fenster einen unsichtbaren Greifrahmen.
73 * `getNormalBounds()` zählt ihn mit, die Angabe im Konstruktor nicht — gemessen
74 * sind es sieben Bildpunkte in der Breite. Der Spielraum ist bewusst knapp:
75 * Er soll den Rahmen durchlassen und ein Fenster, das bei jedem Start weiter
76 * wächst, trotzdem auffallen lassen.
77 */
78 const RAHMEN_SPIELRAUM = 16;
79
80 let app: ElectronApplication | undefined;
81 let profil: string;
82
83 /**
84 * Der Griff auf das echte Fenster, mit Typ.
85 *
86 * `browserWindow()` liefert einen untypisierten Griff; ohne diese Zusage
87 * arbeitete jede Abfrage darauf mit `any`, und der Linter hätte recht damit,
88 * das zu bemängeln.
89 */
90 async function fensterGriff(seite: Page): Promise<JSHandle<BrowserWindow>> {
91 return (await app!.browserWindow(seite)) as JSHandle<BrowserWindow>;
92 }
93
94 test.beforeAll(async () => {
95 const paket = paketstand(projektWurzel);
96 test.skip(!paket.vorhanden || paket.veraltet, paket.grund);
97
98 profil = mkdtempSync(join(tmpdir(), 'wsk-fensterlage-'));
99 /* Das Profil wird vorbelegt, bevor die Anwendung es zum ersten Mal sieht.
100 Genau diese Ausgangslage hat kein anderer Lauf. */
101 writeFileSync(
102 join(profil, 'einstellungen.json'),
103 `${JSON.stringify({ fenster: GEMERKTE_LAGE }, null, 2)}\n`,
104 'utf8',
105 );
106
107 app = await electron.launch({ executablePath: EXE, args: [`--user-data-dir=${profil}`] });
108 konsolenwacheAnhaengen(app);
109 });
110
111 test.beforeEach(() => {
112 /*
113 Die Notbremse darf hier melden — sie ist der Gegenstand der Prüfung.
114
115 Auf der Maschine, an der der Fehler auftrat, stirbt mit gemerkter Lage der
116 GPU-Prozess, `ready-to-show` bleibt aus, und die Notbremse zeigt das
117 Fenster nach der Frist. Genau dann schreibt sie diese Warnung. Sie
118 stillzulegen hieße, die einzige Spur zu löschen, die den Vorfall im
119 Fehlerprotokoll des Anwenders sichtbar macht.
120
121 Auf einer Maschine, auf der `ready-to-show` normal feuert, bleibt die
122 Meldung aus — angemeldet zu sein schadet dort nichts.
123 */
124 konsoleErwartet(
125 /Fenster ohne ersten Frame gezeigt/u,
126 'Die Notbremse ist hier der Prüfgegenstand; sie meldet, wenn sie greift.',
127 );
128 });
129
130 test.afterAll(async () => {
131 await app?.close();
132 if (profil) {
133 rmSync(profil, { recursive: true, force: true });
134 }
135 });
136
137 test('zeigt das Fenster, obwohl eine Lage gespeichert ist', async () => {
138 /*
139 Die eigentliche Zusage dieser Datei. `firstWindow()` allein genügt nicht —
140 sie war beim Vorfall erfolgreich, während auf dem Bildschirm nichts stand.
141 Gefragt wird deshalb das Fenster selbst.
142 */
143 const seite = await app!.firstWindow({ timeout: GEDULD_MS });
144 await seite.waitForLoadState('domcontentloaded');
145
146 const fenster = await fensterGriff(seite);
147 await expect
148 .poll(async () => fenster.evaluate((w) => w.isVisible()), {
149 timeout: GEDULD_MS,
150 message:
151 'Das Fenster ist nie sichtbar geworden. Die Anwendung läuft dann als Prozess ' +
152 'ohne Oberfläche – für den Anwender startet sie einfach nicht.',
153 })
154 .toBe(true);
155 });
156
157 test('behält die gespeicherte Lage bei', async () => {
158 /* Wäre die Antwort auf den Fehler gewesen, die gemerkte Lage einfach nicht
159 mehr anzuwenden, liefe der Test oben ebenfalls grün — und die Funktion
160 wäre still verschwunden. Deshalb wird auch geprüft, dass sie noch wirkt. */
161 const seite = await app!.firstWindow();
162 const fenster = await fensterGriff(seite);
163 const lage = await fenster.evaluate((w) => w.getNormalBounds());
164
165 /* Die Position muss auf den Punkt stimmen — sie ist das, woran der Anwender
166 merkt, dass sich die Anwendung etwas gemerkt hat. Bei der Größe zählt
167 Windows den unsichtbaren Greifrahmen mit; verglichen wird deshalb mit
168 Spielraum. */
169 expect(lage.x, 'Die gemerkte Position wurde nicht angewandt.').toBe(GEMERKTE_LAGE.x);
170 expect(lage.y, 'Die gemerkte Position wurde nicht angewandt.').toBe(GEMERKTE_LAGE.y);
171 expect(Math.abs(lage.width - GEMERKTE_LAGE.breite)).toBeLessThanOrEqual(RAHMEN_SPIELRAUM);
172 expect(Math.abs(lage.height - GEMERKTE_LAGE.hoehe)).toBeLessThanOrEqual(RAHMEN_SPIELRAUM);
173 });
174
175 test('ist mit gemerkter Lage auch bedienbar', async () => {
176 /* Ein sichtbares, aber leeres Fenster wäre keine Abhilfe. */
177 const seite = await app!.firstWindow();
178 await weiterAmZuschnitt(seite);
179 await expect(seite.getByRole('heading', { level: 1 })).toBeVisible({ timeout: GEDULD_MS });
180 });