lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ e2e rauchtest.spec.ts

8,2 KB Rohdatei
e2e/rauchtest.spec.ts — 214 Zeilen
1 import { mkdtempSync, readFileSync } from 'node:fs';
2 import { tmpdir } from 'node:os';
3 import { join } from 'node:path';
4
5 import {
6 _electron as electron,
7 expect,
8 test,
9 type ElectronApplication,
10 type Page,
11 } from '@playwright/test';
12
13 /**
14 * Rauchtest: Startet die gebaute Anwendung wirklich?
15 *
16 * Die Modultests pruefen Fachlogik, Ausdruck und Oberflaechenfunktionen -
17 * aber keiner von ihnen startet das Programm. Genau dort liegen die Fehler, die
18 * erst beim Anwender auffallen: ein Preload-Bundle, das nach dem Bauen nicht
19 * mehr passt; ein Pfad, der nur im Paket anders liegt; eine Sicherheits-
20 * richtlinie, die das Einsetzen beim Bauen nicht ueberlebt hat und die eigenen
21 * Skripte blockt.
22 *
23 * Dieser Test laeuft deshalb gegen `build/` - gegen das Erzeugnis, nicht gegen
24 * den Quelltext. Er braucht `npm run build` davor; `npm run e2e` erledigt das.
25 *
26 * Er prueft bewusst wenig, aber das Richtige: Startet sie, ist die Bruecke
27 * vollstaendig, haelt die Abschottung, und kam die Sicherheitsrichtlinie an?
28 */
29
30 const WURZEL = join(__dirname, '..');
31
32 /** Die Methoden, die `electron/preload.ts` ueber contextBridge freigibt. */
33 const BRUECKE = [
34 'platform',
35 'saveFile',
36 'openFile',
37 'writeFile',
38 'readFile',
39 'setDirty',
40 'reportError',
41 'logPath',
42 'fetchMapImage',
43 'onMenuCommand',
44 'onBeforeClose',
45 ] as const;
46
47 let anwendung: ElectronApplication;
48 let fenster: Page;
49 const fehlerBeimStart: string[] = [];
50
51 test.beforeAll(async () => {
52 /*
53 * Eigenes, leeres Anwenderverzeichnis je Lauf.
54 *
55 * Ohne das laeuft der Rauchtest gegen den Sitzungsspeicher des Rechners, auf
56 * dem er startet - und beurteilt damit nicht das Erzeugnis, sondern was
57 * jemand zuletzt darin bearbeitet hat. Gemessen: Beim ersten Lauf gruen, beim
58 * zweiten rot, weil der erste beim Schliessen seinen Stand hinterlassen
59 * hatte. Ein Rauchtest, dessen Ergebnis vom vorigen Lauf abhaengt, taugt als
60 * Wache nichts.
61 *
62 * Geprueft wird damit der Start, den ein Anwender nach der Installation
63 * vorfindet.
64 */
65 const anwenderverzeichnis = mkdtempSync(join(tmpdir(), 'lsa-planer-rauchtest-'));
66
67 anwendung = await electron.launch({
68 args: [WURZEL, `--user-data-dir=${anwenderverzeichnis}`],
69 cwd: WURZEL,
70 });
71
72 fenster = await anwendung.firstWindow();
73
74 // Alles einsammeln, was der Renderer beim Hochfahren an Fehlern wirft.
75 // Ein Programm, das startet und dabei Ausnahmen schluckt, ist nicht heil.
76 fenster.on('pageerror', (fehler) => fehlerBeimStart.push(String(fehler)));
77 fenster.on('console', (zeile) => {
78 if (zeile.type() === 'error') {
79 fehlerBeimStart.push(zeile.text());
80 }
81 });
82
83 await fenster.waitForLoadState('domcontentloaded');
84 });
85
86 test.afterAll(async () => {
87 // Das Fragezeichen sieht nach dem Typ ueberfluessig aus (der Linter meldet
88 // es auch), ist es aber nicht: `anwendung` wird erst in beforeAll gesetzt.
89 // Schlaegt der Start fehl, laeuft afterAll trotzdem - und ohne die Abfrage
90 // ueberdeckte hier ein TypeError den eigentlichen Startfehler, also genau
91 // die Meldung, wegen der es diesen Test gibt.
92 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- anwendung ist vor beforeAll undefined; siehe darueber
93 await anwendung?.close();
94 });
95
96 test('das Fenster oeffnet sich und der Renderer setzt den Titel', async () => {
97 // Nicht auf den festen Titel aus index.html pruefen: Den traegt das Fenster
98 // schon, bevor irgendetwas laeuft. Die Anwendung stellt ihm zur Laufzeit den
99 // Projektnamen voran - genau daran zeigt sich, dass der Renderer uebernommen
100 // hat und nicht bloss die HTML-Huelle dasteht.
101 await expect(fenster).toHaveTitle(/^Projekt .+ – LSA-Planer Professional$/u);
102 });
103
104 test('die Oberflaeche baut sich auf, nicht nur die leere Huelle', async () => {
105 const wurzel = fenster.locator('#anwendung');
106 await expect(wurzel).toBeAttached();
107
108 // Ohne diese Pruefung wuerde auch eine Anwendung bestehen, die zwar ein
109 // Fenster oeffnet, in dem aber nur das <noscript> aus index.html steht.
110 await expect
111 .poll(async () => (await wurzel.innerHTML()).length, { timeout: 15_000 })
112 .toBeGreaterThan(500);
113 });
114
115 test('das Fenster laesst sich nicht wegnavigieren', async () => {
116 /*
117 * DIE NAVIGATIONSGRENZE AM GEBAUTEN STAND.
118 *
119 * `will-navigate` in electron/main.ts bricht jede Navigation ab, die nicht
120 * die geladene `index.html` ist. Bis zu dieser Fassung pruefte das kein
121 * einziger Test - waehrend `vitest.config.ts` die Abdeckungsluecke in
122 * `electron/main.ts` ausdruecklich damit begruendete, "genau das" pruefe der
123 * Rauchtest hier. Sechs Faelle, null Nennungen: eine Ausnahme, gedeckt von
124 * einem Test, den es nicht gab.
125 *
126 * WARUM MIT `http:` UND NICHT MIT `https:`: Eine https-Adresse gibt die
127 * Wache an den Standardbrowser weiter - dieser Test oeffnete dann bei jedem
128 * Lauf ein Browserfenster auf dem Rechner dessen, der ihn faehrt. Geprueft
129 * wird deshalb hier die Verdrahtung an einer Adresse, die NICHT hinausgeht;
130 * dass die Entscheidung fuer jedes Protokoll richtig faellt, prueft
131 * `tests/tools/navigationGrenze.test.ts` an der reinen Funktion.
132 *
133 * Die Adresse zeigt auf `.invalid` - eine Namensendung, die nach RFC 2606
134 * niemals aufgeloest wird. Selbst wenn die Wache fiele, ginge kein Abruf an
135 * einen fremden Rechner.
136 */
137 const vorher = fenster.url();
138 expect(vorher, 'der Rauchtest laeuft nicht auf der eigenen Seite').toContain('index.html');
139
140 await fenster.evaluate(() => {
141 globalThis.location.href = 'http://wegnavigiert.invalid/';
142 });
143
144 // Kurz warten: Eine Navigation, die durchginge, braucht einen Augenblick.
145 await fenster.waitForTimeout(500);
146 expect(fenster.url(), 'das Fenster hat die eigene Seite verlassen').toBe(vorher);
147
148 /*
149 * Und die Oberflaeche steht noch - ein Abbruch, der das Dokument leert, waere
150 * kein Abbruch.
151 *
152 * Ueber `evaluate` und nicht ueber einen Locator: Chromium fuehrt nach dem
153 * abgebrochenen Versuch noch eine schwebende Navigation, und `toBeAttached`
154 * wartet darauf, dass sie endet - sie endet nie, und der Fall lief in die
155 * Zeitschranke, obwohl die Wache getan hatte, was sie soll. GEMESSEN: Die
156 * Zeile darueber war gruen, erst diese fiel. `evaluate` fragt den
157 * gegenwaertigen Baum, ohne auf eine Navigation zu warten.
158 */
159 const stehtNoch = await fenster.evaluate(
160 () => document.getElementById('anwendung')?.childElementCount ?? 0,
161 );
162 expect(stehtNoch, 'die Oberflaeche ist beim Abbruch leer geworden').toBeGreaterThan(0);
163 });
164
165 test('der Renderer meldet beim Start keine Fehler', () => {
166 expect(fehlerBeimStart).toEqual([]);
167 });
168
169 test('die Bruecke ist vollstaendig freigegeben', async () => {
170 const vorhanden = await fenster.evaluate(() => {
171 const bruecke = (globalThis as unknown as { lsaPlaner?: Record<string, unknown> }).lsaPlaner;
172 return bruecke ? Object.keys(bruecke).sort() : null;
173 });
174
175 expect(vorhanden).not.toBeNull();
176 for (const name of BRUECKE) {
177 expect(vorhanden).toContain(name);
178 }
179 });
180
181 test('der Renderer kommt nicht an Node heran', async () => {
182 // contextIsolation und sandbox stehen in electron/main.ts. Ob sie im
183 // gebauten Stand tatsaechlich greifen, zeigt sich nur hier: Kaeme der
184 // Renderer an `require`, waere die Abschottung offen, und eine praeparierte
185 // Projektdatei koennte beliebigen Code ausfuehren.
186 const erreichbar = await fenster.evaluate(() => {
187 const global = globalThis as unknown as Record<string, unknown>;
188 return {
189 require: typeof global.require,
190 process: typeof global.process,
191 module: typeof global.module,
192 electron: typeof global.electron,
193 };
194 });
195
196 expect(erreichbar).toEqual({
197 require: 'undefined',
198 process: 'undefined',
199 module: 'undefined',
200 electron: 'undefined',
201 });
202 });
203
204 test('die Sicherheitsrichtlinie hat das Bauen ueberstanden', () => {
205 // Das Einsetzen erledigt ein Vite-Plugin (vite.config.ts,
206 // "lsa-inhaltssicherheitsrichtlinie") und nur im Auslieferungsstand. Faellt
207 // es aus, sieht man das dem laufenden Programm nicht an - die Anwendung
208 // startet weiterhin, nur ohne Richtlinie.
209 const html = readFileSync(join(WURZEL, 'build', 'renderer', 'index.html'), 'utf8');
210
211 expect(html).toContain('http-equiv="Content-Security-Policy"');
212 expect(html).toContain("default-src 'none'");
213 expect(html).not.toContain('<!--sicherheitsrichtlinie-->');
214 });