lsa-planer

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

/ tests electron fenstergrundKontrast.test.ts

10,0 KB Rohdatei
tests/electron/fenstergrundKontrast.test.ts — 306 Zeilen
1 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2 import { mkdtempSync } from 'node:fs';
3 import os from 'node:os';
4 import path from 'node:path';
5 import {
6 FENSTER_GRUND_DUNKEL,
7 FENSTER_GRUND_HELL,
8 fensterGrundfarbe,
9 } from '../../electron/fenstergrund';
10
11 /**
12 * Befund 30: Die Fenstergrundfarbe deckte das Kontrastdesign nicht ab, obwohl
13 * der Kopfkommentar von electron/fenstergrund.ts es ausdruecklich als behobenen
14 * Fall auffuehrt ("Wer im dunklen Modus ODER IN EINEM DUNKLEN KONTRASTDESIGN
15 * arbeitet, bekam damit bei jedem Start und bei jedem 'Neu laden' einen hellen
16 * Blitz").
17 *
18 * `grundfarbe()` in electron/main.ts kannte nur `nativeTheme.shouldUseDarkColors`
19 * - den Windows-Schalter fuer App-Farben. Er steht bei "App-Farbmodus: Hell"
20 * auch dann auf `false`, wenn ein dunkles Kontrastdesign ("Nacht", "Wüste")
21 * eingeschaltet ist. Der Hauptprozess malte also `#f4f6f9`, waehrend die Anzeige
22 * unter `forced-colors: active` den nahezu schwarzen Systemgrund `Canvas`
23 * zeichnet: genau der helle Blitz, den Befund L12 fuer Anwender mit
24 * Lichtempfindlichkeit, Migraene oder Photophobie beseitigen sollte. Erreichbar
25 * ueber "Neu laden" (Strg+R, electron/menu.ts, auch im Auslieferungsstand) und
26 * ueber das Vergroessern des Fensters am Rand.
27 */
28
29 const welt = vi.hoisted(() => {
30 type Horcher = (...werte: unknown[]) => void;
31
32 class Ereignisquelle {
33 private readonly horcher = new Map<string, Horcher[]>();
34
35 on(name: string, fn: Horcher): this {
36 const liste = this.horcher.get(name) ?? [];
37 liste.push(fn);
38 this.horcher.set(name, liste);
39 return this;
40 }
41
42 once(name: string, fn: Horcher): this {
43 return this.on(name, fn);
44 }
45
46 removeListener(name: string, fn: Horcher): this {
47 this.horcher.set(
48 name,
49 (this.horcher.get(name) ?? []).filter((h) => h !== fn),
50 );
51 return this;
52 }
53
54 off(name: string, fn: Horcher): this {
55 return this.removeListener(name, fn);
56 }
57
58 ausloesen(name: string, ...werte: unknown[]): void {
59 for (const fn of this.horcher.get(name) ?? []) fn(...werte);
60 }
61 }
62
63 class Anzeigeinhalt extends Ereignisquelle {
64 send(): void {
65 /* Im Test ohne Wirkung. */
66 }
67 setZoomFactor(): void {
68 /* Im Test ohne Wirkung. */
69 }
70 setWindowOpenHandler(): void {
71 /* Im Test ohne Wirkung. */
72 }
73 isCrashed(): boolean {
74 return false;
75 }
76 isDestroyed(): boolean {
77 return false;
78 }
79 }
80
81 const zustand = {
82 fenster: null as Fenster | null,
83 benutzerdaten: '.',
84 /** `nativeTheme.shouldUseDarkColors` - der Windows-Schalter fuer App-Farben. */
85 dunkel: false,
86 /** `nativeTheme.shouldUseHighContrastColors` - das Kontrastdesign. */
87 kontrastdesign: false,
88 /** Was `systemPreferences.getColor('window')` liefert. */
89 systemgrund: '#000000' as string | null,
90 /** Beim Aufbau des Fensters gesetzte Grundfarbe. */
91 grundBeimAufbau: null as string | null,
92 /** Nachgefuehrte Grundfarben ueber `setBackgroundColor`. */
93 nachgefuehrt: [] as string[],
94 /** Horcher auf `nativeTheme.on('updated')`. */
95 erscheinungsbildHorcher: [] as (() => void)[],
96 };
97
98 class Fenster extends Ereignisquelle {
99 readonly webContents = new Anzeigeinhalt();
100
101 constructor(optionen: { backgroundColor?: string }) {
102 super();
103 zustand.grundBeimAufbau = optionen.backgroundColor ?? null;
104 zustand.fenster = this;
105 }
106
107 loadFile(): Promise<void> {
108 return Promise.resolve();
109 }
110 show(): void {
111 /* Im Test ohne Wirkung. */
112 }
113 setBackgroundColor(farbe: string): void {
114 zustand.nachgefuehrt.push(farbe);
115 }
116 setDocumentEdited(): void {
117 /* Im Test ohne Wirkung. */
118 }
119 isDestroyed(): boolean {
120 return false;
121 }
122 close(): void {
123 /* Im Test ohne Wirkung. */
124 }
125
126 static getAllWindows(): Fenster[] {
127 return zustand.fenster === null ? [] : [zustand.fenster];
128 }
129 }
130
131 const electron = {
132 app: {
133 isPackaged: false,
134 requestSingleInstanceLock: (): boolean => true,
135 on: (): void => undefined,
136 whenReady: (): Promise<void> => Promise.resolve(),
137 getPath: (): string => zustand.benutzerdaten,
138 getVersion: (): string => '5.10.0',
139 quit: (): void => undefined,
140 },
141 BrowserWindow: Fenster,
142 Menu: {
143 buildFromTemplate: (): { getMenuItemById: () => null } => ({ getMenuItemById: () => null }),
144 setApplicationMenu: (): void => undefined,
145 },
146 dialog: {
147 showMessageBoxSync: (): number => 0,
148 showErrorBox: (): void => undefined,
149 showSaveDialog: (): Promise<{ canceled: boolean }> => Promise.resolve({ canceled: true }),
150 showOpenDialog: (): Promise<{ canceled: boolean; filePaths: string[] }> =>
151 Promise.resolve({ canceled: true, filePaths: [] }),
152 },
153 ipcMain: {
154 handle: (): void => undefined,
155 on: (): void => undefined,
156 },
157 nativeTheme: {
158 get shouldUseDarkColors(): boolean {
159 return zustand.dunkel;
160 },
161 get shouldUseHighContrastColors(): boolean {
162 return zustand.kontrastdesign;
163 },
164 on: (_name: string, fn: () => void): void => {
165 zustand.erscheinungsbildHorcher.push(fn);
166 },
167 },
168 systemPreferences: {
169 getColor: (): string => {
170 if (zustand.systemgrund === null) throw new Error('getColor gibt es hier nicht');
171 return zustand.systemgrund;
172 },
173 },
174 session: {
175 defaultSession: {
176 webRequest: { onHeadersReceived: (): void => undefined },
177 setPermissionRequestHandler: (): void => undefined,
178 setPermissionCheckHandler: (): void => undefined,
179 },
180 },
181 shell: { openExternal: (): Promise<void> => Promise.resolve() },
182 };
183
184 return {
185 electron,
186 zustand,
187 zuruecksetzen: (): void => {
188 zustand.fenster = null;
189 zustand.dunkel = false;
190 zustand.kontrastdesign = false;
191 zustand.systemgrund = '#000000';
192 zustand.grundBeimAufbau = null;
193 zustand.nachgefuehrt = [];
194 zustand.erscheinungsbildHorcher = [];
195 },
196 };
197 });
198
199 vi.mock('electron', () => welt.electron);
200
201 process.setMaxListeners(64);
202
203 async function starteHauptprozess(): Promise<void> {
204 vi.resetModules();
205 await import('../../electron/main');
206 for (let i = 0; i < 5; i += 1) await Promise.resolve();
207 }
208
209 describe('Fenstergrund: das Kontrastdesign ist die zweite Eingangsgroesse', () => {
210 it('nimmt den gemeldeten Systemgrund statt einer der beiden Anwendungsfarben', () => {
211 // Unter `forced-colors: active` malt die Anzeige keine der beiden Farben -
212 // Windows ersetzt sie durch `Canvas`. Der Fenstergrund muss dorthin folgen.
213 expect(fensterGrundfarbe(false, '#000000')).toBe('#000000');
214 expect(fensterGrundfarbe(true, '#ffffff')).toBe('#ffffff');
215 });
216
217 it('bleibt ohne Kontrastdesign bei den beiden Anwendungsfarben', () => {
218 expect(fensterGrundfarbe(false, null)).toBe(FENSTER_GRUND_HELL);
219 expect(fensterGrundfarbe(true, null)).toBe(FENSTER_GRUND_DUNKEL);
220 expect(fensterGrundfarbe(false)).toBe(FENSTER_GRUND_HELL);
221 expect(fensterGrundfarbe(true)).toBe(FENSTER_GRUND_DUNKEL);
222 });
223
224 it('verwirft eine Farbangabe, mit der sich kein Fenster fuellen laesst', () => {
225 // Nicht jedes Betriebssystem kennt die Abfrage, und was zurueckkommt, ist
226 // keine geprueftes Gut. Ein unbrauchbarer Wert faellt auf die
227 // Anwendungsfarbe zurueck, statt das Fenster schwarz oder gar nicht zu
228 // fuellen.
229 expect(fensterGrundfarbe(false, '')).toBe(FENSTER_GRUND_HELL);
230 expect(fensterGrundfarbe(true, 'Canvas')).toBe(FENSTER_GRUND_DUNKEL);
231 expect(fensterGrundfarbe(false, '#12345')).toBe(FENSTER_GRUND_HELL);
232 });
233 });
234
235 describe('Hauptprozess: der Fenstergrund folgt dem dunklen Kontrastdesign', () => {
236 beforeEach(() => {
237 vi.spyOn(console, 'error').mockImplementation(() => undefined);
238 welt.zuruecksetzen();
239 welt.zustand.benutzerdaten = mkdtempSync(path.join(os.tmpdir(), 'lsa-hauptprozess-grund-'));
240 });
241
242 afterEach(() => {
243 vi.restoreAllMocks();
244 });
245
246 it('malt bei dunklem Kontrastdesign und App-Farbmodus "Hell" nicht mehr hell', async () => {
247 // Genau der Ausloeser: Windows 11, Barrierefreiheit -> Kontrastdesign
248 // "Nacht", App-Farbmodus "Hell". `shouldUseDarkColors` steht dabei auf
249 // `false`.
250 welt.zustand.dunkel = false;
251 welt.zustand.kontrastdesign = true;
252 welt.zustand.systemgrund = '#000000';
253
254 await starteHauptprozess();
255
256 expect(welt.zustand.grundBeimAufbau, 'der helle Blitz aus Befund L12').not.toBe(
257 FENSTER_GRUND_HELL,
258 );
259 expect(welt.zustand.grundBeimAufbau).toBe('#000000');
260 });
261
262 it('folgt einem hellen Kontrastdesign ebenso', async () => {
263 welt.zustand.dunkel = true;
264 welt.zustand.kontrastdesign = true;
265 welt.zustand.systemgrund = '#ffffff';
266
267 await starteHauptprozess();
268
269 expect(welt.zustand.grundBeimAufbau).toBe('#ffffff');
270 });
271
272 it('bleibt ohne Kontrastdesign bei der Farbe des Erscheinungsbilds', async () => {
273 welt.zustand.dunkel = true;
274 welt.zustand.kontrastdesign = false;
275
276 await starteHauptprozess();
277
278 expect(welt.zustand.grundBeimAufbau).toBe(FENSTER_GRUND_DUNKEL);
279 });
280
281 it('zieht den Fenstergrund nach, wenn das Kontrastdesign erst spaeter eingeschaltet wird', async () => {
282 welt.zustand.dunkel = false;
283 welt.zustand.kontrastdesign = false;
284
285 await starteHauptprozess();
286 expect(welt.zustand.grundBeimAufbau).toBe(FENSTER_GRUND_HELL);
287
288 welt.zustand.kontrastdesign = true;
289 welt.zustand.systemgrund = '#000000';
290 for (const horcher of welt.zustand.erscheinungsbildHorcher) horcher();
291
292 expect(welt.zustand.nachgefuehrt.at(-1)).toBe('#000000');
293 });
294
295 it('faellt auf die Anwendungsfarbe zurueck, wenn das System keine Farbe nennt', async () => {
296 // `systemPreferences.getColor` gibt es nur unter Windows und macOS. Ein
297 // Wurf darf den Start nicht anhalten.
298 welt.zustand.dunkel = false;
299 welt.zustand.kontrastdesign = true;
300 welt.zustand.systemgrund = null;
301
302 await starteHauptprozess();
303
304 expect(welt.zustand.grundBeimAufbau).toBe(FENSTER_GRUND_HELL);
305 });
306 });