/** * Das Farbschema: führende Quelle und Zwischenspeicher. * * Die Wahl liegt in `einstellungen.json` – wie jede andere Einstellung. Der * `localStorage` ist ein **Zwischenspeicher**, kein zweiter Ablageort: Er * liefert den Startwert für den Augenblick, in dem der asynchrone * IPC-Aufruf noch unterwegs ist. Ohne ihn zeichnete die Anwendung eine Runde * lang das Standardthema und spränge dann um – ein Aufblitzen genau bei * denen, die auf ein blendarmes oder kontrastreiches Thema angewiesen sind. * * Damit der Zwischenspeicher nie gegen die Datei steht, gilt eine Regel: Er * wird nur geschrieben, wenn die Datei den Wert hat. Diese Datei prüft beide * Hälften – dass er den Start trägt, und dass er der Datei folgt statt ihr * vorauszueilen. */ import { act, renderHook, waitFor } from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { useThema } from '../src/renderer/src/hooks/useThema'; import { EINSTELLUNGEN_STANDARD, type Einstellungen } from '../src/shared/ipc'; import type { ThemeAuswahl } from '../src/shared/theme'; const SCHLUESSEL = 'waffensachkunde.thema'; /** Bildet den Anwendungskern nach; `schreibfehler` stellt einen vollen Datenträger nach. */ function kern( gespeichert: ThemeAuswahl, { schreibfehler = false }: { schreibfehler?: boolean } = {}, ): { readonly geschrieben: Partial[] } { let stand: Einstellungen = { ...EINSTELLUNGEN_STANDARD, thema: gespeichert }; const geschrieben: Partial[] = []; vi.stubGlobal('lernApp', { einstellungenLesen: vi.fn(() => Promise.resolve(stand)), einstellungenSchreiben: vi.fn((aenderung: Partial) => { if (schreibfehler) { return Promise.reject(new Error('Die Einstellungen konnten nicht gespeichert werden.')); } geschrieben.push(aenderung); stand = { ...stand, ...aenderung }; return Promise.resolve(stand); }), }); return { geschrieben }; } /** Was gerade wirklich gezeichnet wird. */ function gezeichnet(): string | undefined { return document.documentElement.dataset['thema']; } afterEach(() => { vi.unstubAllGlobals(); }); describe('Farbschema beim Start', () => { it('liegt schon in der ersten Renderrunde an – ohne Aufblitzen', () => { /* Der entscheidende Test: NICHT abgewartet. Die Antwort des Anwendungskerns steht zu diesem Zeitpunkt noch aus; was hier am Wurzelelement steht, ist das, was der Nutzer als Erstes sieht. */ window.localStorage.setItem(SCHLUESSEL, 'dunkel'); kern('dunkel'); const { result } = renderHook(() => useThema()); expect(result.current.auswahl).toBe('dunkel'); expect(gezeichnet()).toBe('dunkel'); }); it('zeichnet ohne Zwischenspeicher zuerst die Vorgabe – die Lücke, die er schließt', () => { /* Die Gegenprobe zum Test darüber: Ohne den Startwert steht in der ersten Runde die Systemvorgabe (hier hell, siehe matchMedia-Attrappe) und springt danach um. Genau dieses Umspringen verhindert der Zwischenspeicher. */ kern('dunkel'); const { result } = renderHook(() => useThema()); expect(gezeichnet()).toBe('hell'); return waitFor(() => { expect(result.current.auswahl).toBe('dunkel'); expect(gezeichnet()).toBe('dunkel'); }); }); it('führt den Zwischenspeicher der Datei nach', async () => { /* Damit derselbe Start nur einmal misslingt: Nach dem ersten Lauf trägt der Zwischenspeicher, was in der Datei steht – auch dann, wenn diese von woanders kommt (übernommenes Benutzerverzeichnis, von Hand bearbeitet). */ kern('hochkontrast'); const erster = renderHook(() => useThema()); await waitFor(() => { expect(window.localStorage.getItem(SCHLUESSEL)).toBe('hochkontrast'); }); erster.unmount(); // Neustart: jetzt trägt der Zwischenspeicher die erste Runde. const { result } = renderHook(() => useThema()); expect(result.current.auswahl).toBe('hochkontrast'); expect(gezeichnet()).toBe('hochkontrast'); }); }); describe('Farbschema umstellen', () => { it('schreibt in die Einstellungen und danach in den Zwischenspeicher', async () => { const { geschrieben } = kern('system'); const { result } = renderHook(() => useThema()); act(() => { result.current.auswahlSetzen('dunkel'); }); // Sofort wirksam – unabhängig davon, ob das Sichern schon gelungen ist. expect(gezeichnet()).toBe('dunkel'); await waitFor(() => { expect(geschrieben).toContainEqual({ thema: 'dunkel' }); }); await waitFor(() => { expect(window.localStorage.getItem(SCHLUESSEL)).toBe('dunkel'); }); }); it('vergiftet den Zwischenspeicher nicht, wenn das Sichern scheitert', async () => { /* Der Fall, der die Regel trägt: Läge die neue Wahl im Zwischenspeicher, obwohl die Datei sie nicht hat, zeichnete der nächste Start zuerst das neue Thema und spränge dann auf das alte zurück – ein Aufblitzen, erzeugt von dem Speicher, der es verhindern soll. Und das ausgerechnet bei jemandem, dem gerade gesagt wurde, dass nichts gesichert wurde. */ vi.spyOn(console, 'warn').mockImplementation(() => undefined); window.localStorage.setItem(SCHLUESSEL, 'hell'); kern('hell', { schreibfehler: true }); const { result } = renderHook(() => useThema()); act(() => { result.current.auswahlSetzen('dunkel'); }); expect(gezeichnet()).toBe('dunkel'); await waitFor(() => { expect(result.current.ansage).toMatch(/nicht dauerhaft gespeichert/u); }); expect(window.localStorage.getItem(SCHLUESSEL)).toBe('hell'); }); it('behält den Zwischenspeicher, wenn es gar keinen Anwendungskern gibt', () => { /* Ohne Preload (Vorschau im Browser) gibt es keine Datei, die führen könnte – dann ist der Zwischenspeicher alles, was bleibt. */ const { result } = renderHook(() => useThema()); act(() => { result.current.auswahlSetzen('hochkontrast'); }); expect(window.localStorage.getItem(SCHLUESSEL)).toBe('hochkontrast'); }); });