import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { mkdtempSync } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { FENSTER_GRUND_DUNKEL, FENSTER_GRUND_HELL, fensterGrundfarbe, } from '../../electron/fenstergrund'; /** * Befund 30: Die Fenstergrundfarbe deckte das Kontrastdesign nicht ab, obwohl * der Kopfkommentar von electron/fenstergrund.ts es ausdruecklich als behobenen * Fall auffuehrt ("Wer im dunklen Modus ODER IN EINEM DUNKLEN KONTRASTDESIGN * arbeitet, bekam damit bei jedem Start und bei jedem 'Neu laden' einen hellen * Blitz"). * * `grundfarbe()` in electron/main.ts kannte nur `nativeTheme.shouldUseDarkColors` * - den Windows-Schalter fuer App-Farben. Er steht bei "App-Farbmodus: Hell" * auch dann auf `false`, wenn ein dunkles Kontrastdesign ("Nacht", "Wüste") * eingeschaltet ist. Der Hauptprozess malte also `#f4f6f9`, waehrend die Anzeige * unter `forced-colors: active` den nahezu schwarzen Systemgrund `Canvas` * zeichnet: genau der helle Blitz, den Befund L12 fuer Anwender mit * Lichtempfindlichkeit, Migraene oder Photophobie beseitigen sollte. Erreichbar * ueber "Neu laden" (Strg+R, electron/menu.ts, auch im Auslieferungsstand) und * ueber das Vergroessern des Fensters am Rand. */ const welt = vi.hoisted(() => { type Horcher = (...werte: unknown[]) => void; class Ereignisquelle { private readonly horcher = new Map(); on(name: string, fn: Horcher): this { const liste = this.horcher.get(name) ?? []; liste.push(fn); this.horcher.set(name, liste); return this; } once(name: string, fn: Horcher): this { return this.on(name, fn); } removeListener(name: string, fn: Horcher): this { this.horcher.set( name, (this.horcher.get(name) ?? []).filter((h) => h !== fn), ); return this; } off(name: string, fn: Horcher): this { return this.removeListener(name, fn); } ausloesen(name: string, ...werte: unknown[]): void { for (const fn of this.horcher.get(name) ?? []) fn(...werte); } } class Anzeigeinhalt extends Ereignisquelle { send(): void { /* Im Test ohne Wirkung. */ } setZoomFactor(): void { /* Im Test ohne Wirkung. */ } setWindowOpenHandler(): void { /* Im Test ohne Wirkung. */ } isCrashed(): boolean { return false; } isDestroyed(): boolean { return false; } } const zustand = { fenster: null as Fenster | null, benutzerdaten: '.', /** `nativeTheme.shouldUseDarkColors` - der Windows-Schalter fuer App-Farben. */ dunkel: false, /** `nativeTheme.shouldUseHighContrastColors` - das Kontrastdesign. */ kontrastdesign: false, /** Was `systemPreferences.getColor('window')` liefert. */ systemgrund: '#000000' as string | null, /** Beim Aufbau des Fensters gesetzte Grundfarbe. */ grundBeimAufbau: null as string | null, /** Nachgefuehrte Grundfarben ueber `setBackgroundColor`. */ nachgefuehrt: [] as string[], /** Horcher auf `nativeTheme.on('updated')`. */ erscheinungsbildHorcher: [] as (() => void)[], }; class Fenster extends Ereignisquelle { readonly webContents = new Anzeigeinhalt(); constructor(optionen: { backgroundColor?: string }) { super(); zustand.grundBeimAufbau = optionen.backgroundColor ?? null; zustand.fenster = this; } loadFile(): Promise { return Promise.resolve(); } show(): void { /* Im Test ohne Wirkung. */ } setBackgroundColor(farbe: string): void { zustand.nachgefuehrt.push(farbe); } setDocumentEdited(): void { /* Im Test ohne Wirkung. */ } isDestroyed(): boolean { return false; } close(): void { /* Im Test ohne Wirkung. */ } static getAllWindows(): Fenster[] { return zustand.fenster === null ? [] : [zustand.fenster]; } } const electron = { app: { isPackaged: false, requestSingleInstanceLock: (): boolean => true, on: (): void => undefined, whenReady: (): Promise => Promise.resolve(), getPath: (): string => zustand.benutzerdaten, getVersion: (): string => '5.10.0', quit: (): void => undefined, }, BrowserWindow: Fenster, Menu: { buildFromTemplate: (): { getMenuItemById: () => null } => ({ getMenuItemById: () => null }), setApplicationMenu: (): void => undefined, }, dialog: { showMessageBoxSync: (): number => 0, showErrorBox: (): void => undefined, showSaveDialog: (): Promise<{ canceled: boolean }> => Promise.resolve({ canceled: true }), showOpenDialog: (): Promise<{ canceled: boolean; filePaths: string[] }> => Promise.resolve({ canceled: true, filePaths: [] }), }, ipcMain: { handle: (): void => undefined, on: (): void => undefined, }, nativeTheme: { get shouldUseDarkColors(): boolean { return zustand.dunkel; }, get shouldUseHighContrastColors(): boolean { return zustand.kontrastdesign; }, on: (_name: string, fn: () => void): void => { zustand.erscheinungsbildHorcher.push(fn); }, }, systemPreferences: { getColor: (): string => { if (zustand.systemgrund === null) throw new Error('getColor gibt es hier nicht'); return zustand.systemgrund; }, }, session: { defaultSession: { webRequest: { onHeadersReceived: (): void => undefined }, setPermissionRequestHandler: (): void => undefined, setPermissionCheckHandler: (): void => undefined, }, }, shell: { openExternal: (): Promise => Promise.resolve() }, }; return { electron, zustand, zuruecksetzen: (): void => { zustand.fenster = null; zustand.dunkel = false; zustand.kontrastdesign = false; zustand.systemgrund = '#000000'; zustand.grundBeimAufbau = null; zustand.nachgefuehrt = []; zustand.erscheinungsbildHorcher = []; }, }; }); vi.mock('electron', () => welt.electron); process.setMaxListeners(64); async function starteHauptprozess(): Promise { vi.resetModules(); await import('../../electron/main'); for (let i = 0; i < 5; i += 1) await Promise.resolve(); } describe('Fenstergrund: das Kontrastdesign ist die zweite Eingangsgroesse', () => { it('nimmt den gemeldeten Systemgrund statt einer der beiden Anwendungsfarben', () => { // Unter `forced-colors: active` malt die Anzeige keine der beiden Farben - // Windows ersetzt sie durch `Canvas`. Der Fenstergrund muss dorthin folgen. expect(fensterGrundfarbe(false, '#000000')).toBe('#000000'); expect(fensterGrundfarbe(true, '#ffffff')).toBe('#ffffff'); }); it('bleibt ohne Kontrastdesign bei den beiden Anwendungsfarben', () => { expect(fensterGrundfarbe(false, null)).toBe(FENSTER_GRUND_HELL); expect(fensterGrundfarbe(true, null)).toBe(FENSTER_GRUND_DUNKEL); expect(fensterGrundfarbe(false)).toBe(FENSTER_GRUND_HELL); expect(fensterGrundfarbe(true)).toBe(FENSTER_GRUND_DUNKEL); }); it('verwirft eine Farbangabe, mit der sich kein Fenster fuellen laesst', () => { // Nicht jedes Betriebssystem kennt die Abfrage, und was zurueckkommt, ist // keine geprueftes Gut. Ein unbrauchbarer Wert faellt auf die // Anwendungsfarbe zurueck, statt das Fenster schwarz oder gar nicht zu // fuellen. expect(fensterGrundfarbe(false, '')).toBe(FENSTER_GRUND_HELL); expect(fensterGrundfarbe(true, 'Canvas')).toBe(FENSTER_GRUND_DUNKEL); expect(fensterGrundfarbe(false, '#12345')).toBe(FENSTER_GRUND_HELL); }); }); describe('Hauptprozess: der Fenstergrund folgt dem dunklen Kontrastdesign', () => { beforeEach(() => { vi.spyOn(console, 'error').mockImplementation(() => undefined); welt.zuruecksetzen(); welt.zustand.benutzerdaten = mkdtempSync(path.join(os.tmpdir(), 'lsa-hauptprozess-grund-')); }); afterEach(() => { vi.restoreAllMocks(); }); it('malt bei dunklem Kontrastdesign und App-Farbmodus "Hell" nicht mehr hell', async () => { // Genau der Ausloeser: Windows 11, Barrierefreiheit -> Kontrastdesign // "Nacht", App-Farbmodus "Hell". `shouldUseDarkColors` steht dabei auf // `false`. welt.zustand.dunkel = false; welt.zustand.kontrastdesign = true; welt.zustand.systemgrund = '#000000'; await starteHauptprozess(); expect(welt.zustand.grundBeimAufbau, 'der helle Blitz aus Befund L12').not.toBe( FENSTER_GRUND_HELL, ); expect(welt.zustand.grundBeimAufbau).toBe('#000000'); }); it('folgt einem hellen Kontrastdesign ebenso', async () => { welt.zustand.dunkel = true; welt.zustand.kontrastdesign = true; welt.zustand.systemgrund = '#ffffff'; await starteHauptprozess(); expect(welt.zustand.grundBeimAufbau).toBe('#ffffff'); }); it('bleibt ohne Kontrastdesign bei der Farbe des Erscheinungsbilds', async () => { welt.zustand.dunkel = true; welt.zustand.kontrastdesign = false; await starteHauptprozess(); expect(welt.zustand.grundBeimAufbau).toBe(FENSTER_GRUND_DUNKEL); }); it('zieht den Fenstergrund nach, wenn das Kontrastdesign erst spaeter eingeschaltet wird', async () => { welt.zustand.dunkel = false; welt.zustand.kontrastdesign = false; await starteHauptprozess(); expect(welt.zustand.grundBeimAufbau).toBe(FENSTER_GRUND_HELL); welt.zustand.kontrastdesign = true; welt.zustand.systemgrund = '#000000'; for (const horcher of welt.zustand.erscheinungsbildHorcher) horcher(); expect(welt.zustand.nachgefuehrt.at(-1)).toBe('#000000'); }); it('faellt auf die Anwendungsfarbe zurueck, wenn das System keine Farbe nennt', async () => { // `systemPreferences.getColor` gibt es nur unter Windows und macOS. Ein // Wurf darf den Start nicht anhalten. welt.zustand.dunkel = false; welt.zustand.kontrastdesign = true; welt.zustand.systemgrund = null; await starteHauptprozess(); expect(welt.zustand.grundBeimAufbau).toBe(FENSTER_GRUND_HELL); }); });