waffensachkunde

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

/ app tests useThema.test.ts

6,0 KB Rohdatei
app/tests/useThema.test.ts — 164 Zeilen
1 /**
2 * Das Farbschema: führende Quelle und Zwischenspeicher.
3 *
4 * Die Wahl liegt in `einstellungen.json` – wie jede andere Einstellung. Der
5 * `localStorage` ist ein **Zwischenspeicher**, kein zweiter Ablageort: Er
6 * liefert den Startwert für den Augenblick, in dem der asynchrone
7 * IPC-Aufruf noch unterwegs ist. Ohne ihn zeichnete die Anwendung eine Runde
8 * lang das Standardthema und spränge dann um – ein Aufblitzen genau bei
9 * denen, die auf ein blendarmes oder kontrastreiches Thema angewiesen sind.
10 *
11 * Damit der Zwischenspeicher nie gegen die Datei steht, gilt eine Regel: Er
12 * wird nur geschrieben, wenn die Datei den Wert hat. Diese Datei prüft beide
13 * Hälften – dass er den Start trägt, und dass er der Datei folgt statt ihr
14 * vorauszueilen.
15 */
16
17 import { act, renderHook, waitFor } from '@testing-library/react';
18 import { afterEach, describe, expect, it, vi } from 'vitest';
19
20 import { useThema } from '../src/renderer/src/hooks/useThema';
21 import { EINSTELLUNGEN_STANDARD, type Einstellungen } from '../src/shared/ipc';
22 import type { ThemeAuswahl } from '../src/shared/theme';
23
24 const SCHLUESSEL = 'waffensachkunde.thema';
25
26 /** Bildet den Anwendungskern nach; `schreibfehler` stellt einen vollen Datenträger nach. */
27 function kern(
28 gespeichert: ThemeAuswahl,
29 { schreibfehler = false }: { schreibfehler?: boolean } = {},
30 ): { readonly geschrieben: Partial<Einstellungen>[] } {
31 let stand: Einstellungen = { ...EINSTELLUNGEN_STANDARD, thema: gespeichert };
32 const geschrieben: Partial<Einstellungen>[] = [];
33
34 vi.stubGlobal('lernApp', {
35 einstellungenLesen: vi.fn(() => Promise.resolve(stand)),
36 einstellungenSchreiben: vi.fn((aenderung: Partial<Einstellungen>) => {
37 if (schreibfehler) {
38 return Promise.reject(new Error('Die Einstellungen konnten nicht gespeichert werden.'));
39 }
40 geschrieben.push(aenderung);
41 stand = { ...stand, ...aenderung };
42 return Promise.resolve(stand);
43 }),
44 });
45
46 return { geschrieben };
47 }
48
49 /** Was gerade wirklich gezeichnet wird. */
50 function gezeichnet(): string | undefined {
51 return document.documentElement.dataset['thema'];
52 }
53
54 afterEach(() => {
55 vi.unstubAllGlobals();
56 });
57
58 describe('Farbschema beim Start', () => {
59 it('liegt schon in der ersten Renderrunde an – ohne Aufblitzen', () => {
60 /*
61 Der entscheidende Test: NICHT abgewartet. Die Antwort des
62 Anwendungskerns steht zu diesem Zeitpunkt noch aus; was hier am
63 Wurzelelement steht, ist das, was der Nutzer als Erstes sieht.
64 */
65 window.localStorage.setItem(SCHLUESSEL, 'dunkel');
66 kern('dunkel');
67
68 const { result } = renderHook(() => useThema());
69
70 expect(result.current.auswahl).toBe('dunkel');
71 expect(gezeichnet()).toBe('dunkel');
72 });
73
74 it('zeichnet ohne Zwischenspeicher zuerst die Vorgabe – die Lücke, die er schließt', () => {
75 /* Die Gegenprobe zum Test darüber: Ohne den Startwert steht in der
76 ersten Runde die Systemvorgabe (hier hell, siehe matchMedia-Attrappe)
77 und springt danach um. Genau dieses Umspringen verhindert der
78 Zwischenspeicher. */
79 kern('dunkel');
80
81 const { result } = renderHook(() => useThema());
82 expect(gezeichnet()).toBe('hell');
83
84 return waitFor(() => {
85 expect(result.current.auswahl).toBe('dunkel');
86 expect(gezeichnet()).toBe('dunkel');
87 });
88 });
89
90 it('führt den Zwischenspeicher der Datei nach', async () => {
91 /* Damit derselbe Start nur einmal misslingt: Nach dem ersten Lauf trägt
92 der Zwischenspeicher, was in der Datei steht – auch dann, wenn diese
93 von woanders kommt (übernommenes Benutzerverzeichnis, von Hand
94 bearbeitet). */
95 kern('hochkontrast');
96
97 const erster = renderHook(() => useThema());
98 await waitFor(() => {
99 expect(window.localStorage.getItem(SCHLUESSEL)).toBe('hochkontrast');
100 });
101 erster.unmount();
102
103 // Neustart: jetzt trägt der Zwischenspeicher die erste Runde.
104 const { result } = renderHook(() => useThema());
105 expect(result.current.auswahl).toBe('hochkontrast');
106 expect(gezeichnet()).toBe('hochkontrast');
107 });
108 });
109
110 describe('Farbschema umstellen', () => {
111 it('schreibt in die Einstellungen und danach in den Zwischenspeicher', async () => {
112 const { geschrieben } = kern('system');
113 const { result } = renderHook(() => useThema());
114
115 act(() => {
116 result.current.auswahlSetzen('dunkel');
117 });
118
119 // Sofort wirksam – unabhängig davon, ob das Sichern schon gelungen ist.
120 expect(gezeichnet()).toBe('dunkel');
121 await waitFor(() => {
122 expect(geschrieben).toContainEqual({ thema: 'dunkel' });
123 });
124 await waitFor(() => {
125 expect(window.localStorage.getItem(SCHLUESSEL)).toBe('dunkel');
126 });
127 });
128
129 it('vergiftet den Zwischenspeicher nicht, wenn das Sichern scheitert', async () => {
130 /*
131 Der Fall, der die Regel trägt: Läge die neue Wahl im Zwischenspeicher,
132 obwohl die Datei sie nicht hat, zeichnete der nächste Start zuerst das
133 neue Thema und spränge dann auf das alte zurück – ein Aufblitzen,
134 erzeugt von dem Speicher, der es verhindern soll. Und das ausgerechnet
135 bei jemandem, dem gerade gesagt wurde, dass nichts gesichert wurde.
136 */
137 vi.spyOn(console, 'warn').mockImplementation(() => undefined);
138 window.localStorage.setItem(SCHLUESSEL, 'hell');
139 kern('hell', { schreibfehler: true });
140 const { result } = renderHook(() => useThema());
141
142 act(() => {
143 result.current.auswahlSetzen('dunkel');
144 });
145
146 expect(gezeichnet()).toBe('dunkel');
147 await waitFor(() => {
148 expect(result.current.ansage).toMatch(/nicht dauerhaft gespeichert/u);
149 });
150 expect(window.localStorage.getItem(SCHLUESSEL)).toBe('hell');
151 });
152
153 it('behält den Zwischenspeicher, wenn es gar keinen Anwendungskern gibt', () => {
154 /* Ohne Preload (Vorschau im Browser) gibt es keine Datei, die führen
155 könnte – dann ist der Zwischenspeicher alles, was bleibt. */
156 const { result } = renderHook(() => useThema());
157
158 act(() => {
159 result.current.auswahlSetzen('hochkontrast');
160 });
161
162 expect(window.localStorage.getItem(SCHLUESSEL)).toBe('hochkontrast');
163 });
164 });