waffensachkunde

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

/ app tests hilfsmittel.test.ts

10,4 KB Rohdatei
app/tests/hilfsmittel.test.ts — 290 Zeilen
1 /**
2 * Voreinstellung der Tastenkürzel bei erkanntem Hilfsmittel – und ihr Ort.
3 *
4 * Hintergrund der Voreinstellung: Im Lesemodus von NVDA und JAWS sind die
5 * Ziffern 1 bis 6 für die Navigation nach Überschriftenebenen belegt. Kürzel,
6 * die dieselben Tasten belegen, gehen dort ins Leere oder stören –
7 * ausgerechnet beim Beantworten einer Frage, dem wichtigsten Ablauf der
8 * Anwendung.
9 *
10 * Hintergrund des Ortes: Bis 0.21.0 lag die Entscheidung im `localStorage`
11 * des Renderers, also außerhalb von Einstellungen und Profil. Seither geht
12 * sie über `einstellungen:lesen`/`einstellungen:schreiben` nach
13 * `einstellungen.json` – denselben Weg wie „Vorlesen“ und die Anzeigegröße.
14 * Geprüft wird der echte Hook, nicht eine Nachbildung seiner Regel.
15 */
16 import { act, renderHook, waitFor } from '@testing-library/react';
17 import { afterEach, describe, expect, it, vi } from 'vitest';
18
19 import { useKuerzelEinstellung } from '../src/renderer/src/hooks/useTastenkuerzel';
20 import { EINSTELLUNGEN_STANDARD, type Einstellungen } from '../src/shared/ipc';
21
22 /** Der frühere Ablageort – nur noch für die einmalige Übernahme. */
23 const ALTER_SCHLUESSEL = 'waffensachkunde.tastenkuerzel';
24
25 /**
26 * Bildet den Anwendungskern nach: eine „Datei“, die gelesen und beschrieben
27 * wird. `schreibfehler` stellt einen vollen Datenträger nach.
28 *
29 * Zurück kommt die Liste der Teiländerungen, die den Kern erreicht haben –
30 * daran zeigt sich, ob wirklich `einstellungen.json` bedient wird.
31 */
32 function kern(
33 gespeichert: Partial<Einstellungen> = {},
34 { schreibfehler = false }: { schreibfehler?: boolean } = {},
35 ): { readonly geschrieben: Partial<Einstellungen>[] } {
36 let stand: Einstellungen = { ...EINSTELLUNGEN_STANDARD, ...gespeichert };
37 const geschrieben: Partial<Einstellungen>[] = [];
38
39 vi.stubGlobal('lernApp', {
40 einstellungenLesen: vi.fn(() => Promise.resolve(stand)),
41 einstellungenSchreiben: vi.fn((aenderung: Partial<Einstellungen>) => {
42 if (schreibfehler) {
43 return Promise.reject(new Error('Die Einstellungen konnten nicht gespeichert werden.'));
44 }
45 geschrieben.push(aenderung);
46 stand = { ...stand, ...aenderung };
47 return Promise.resolve(stand);
48 }),
49 });
50
51 return { geschrieben };
52 }
53
54 /* `tests/setup.ts` leert vor jedem Test den localStorage; die Brücke muss
55 hier weg, sonst sähe der Test „ohne Anwendungskern“ die Attrappe des
56 vorigen. */
57 afterEach(() => {
58 vi.unstubAllGlobals();
59 });
60
61 describe('Voreinstellung der Tastenkürzel', () => {
62 it('lässt die Kürzel an, solange kein Hilfsmittel läuft', async () => {
63 kern();
64 const { result } = renderHook(() => useKuerzelEinstellung(false));
65
66 await waitFor(() => {
67 expect(result.current.aktiv).toBe(true);
68 });
69 expect(result.current.wegenHilfsmittelAus).toBe(false);
70 });
71
72 it('schaltet sie ohne eigene Entscheidung ab, wenn ein Hilfsmittel läuft', async () => {
73 kern();
74 const { result } = renderHook(() => useKuerzelEinstellung(true));
75
76 await waitFor(() => {
77 expect(result.current.wegenHilfsmittelAus).toBe(true);
78 });
79 expect(result.current.aktiv).toBe(false);
80 });
81
82 it('achtet eine ausdrückliche Zusage auch bei laufendem Hilfsmittel', async () => {
83 // Wer die Kürzel bewusst eingeschaltet hat, soll sie behalten.
84 kern({ tastenkuerzel: true });
85 const { result } = renderHook(() => useKuerzelEinstellung(true));
86
87 await waitFor(() => {
88 expect(result.current.aktiv).toBe(true);
89 });
90 // Die Begründung „wegen Hilfsmittel“ wäre hier schlicht falsch.
91 expect(result.current.wegenHilfsmittelAus).toBe(false);
92 });
93
94 it('achtet eine ausdrückliche Absage auch ohne Hilfsmittel', async () => {
95 kern({ tastenkuerzel: false });
96 const { result } = renderHook(() => useKuerzelEinstellung(false));
97
98 await waitFor(() => {
99 expect(result.current.aktiv).toBe(false);
100 });
101 expect(result.current.wegenHilfsmittelAus).toBe(false);
102 });
103
104 it('begründet nichts, solange der Anwendungskern noch antwortet', () => {
105 /* Sonst blitzte der Hinweis „Es läuft ein Hilfsmittel …“ am Schalter
106 auf, um gleich wieder zu verschwinden – bei jemandem, der die Kürzel
107 längst eingeschaltet hat. */
108 kern({ tastenkuerzel: true });
109 const { result } = renderHook(() => useKuerzelEinstellung(true));
110
111 expect(result.current.wegenHilfsmittelAus).toBe(false);
112 });
113
114 it('überschreibt eine eigene Entscheidung nicht, wenn später ein Hilfsmittel startet', async () => {
115 kern();
116 const { result, rerender } = renderHook(
117 ({ hilfsmittel }: { hilfsmittel: boolean }) => useKuerzelEinstellung(hilfsmittel),
118 { initialProps: { hilfsmittel: false } },
119 );
120
121 await waitFor(() => {
122 expect(result.current.aktiv).toBe(true);
123 });
124
125 act(() => {
126 result.current.setzen(true);
127 });
128 expect(result.current.aktiv).toBe(true);
129
130 // Screenreader startet mitten in der Sitzung.
131 rerender({ hilfsmittel: true });
132
133 expect(result.current.aktiv).toBe(true);
134 expect(result.current.wegenHilfsmittelAus).toBe(false);
135 });
136
137 it('greift die Voreinstellung auf, wenn das Hilfsmittel ohne eigene Wahl startet', async () => {
138 kern();
139 const { result, rerender } = renderHook(
140 ({ hilfsmittel }: { hilfsmittel: boolean }) => useKuerzelEinstellung(hilfsmittel),
141 { initialProps: { hilfsmittel: false } },
142 );
143 await waitFor(() => {
144 expect(result.current.aktiv).toBe(true);
145 });
146
147 rerender({ hilfsmittel: true });
148
149 expect(result.current.aktiv).toBe(false);
150 expect(result.current.wegenHilfsmittelAus).toBe(true);
151 });
152 });
153
154 describe('Ablage der Entscheidung', () => {
155 it('schreibt die Wahl in die Einstellungen, nicht in den localStorage', async () => {
156 const { geschrieben } = kern();
157 const { result } = renderHook(() => useKuerzelEinstellung(false));
158 await waitFor(() => {
159 expect(result.current.aktiv).toBe(true);
160 });
161
162 act(() => {
163 result.current.setzen(false);
164 });
165
166 await waitFor(() => {
167 expect(geschrieben).toEqual([{ tastenkuerzel: false }]);
168 });
169 expect(window.localStorage.getItem(ALTER_SCHLUESSEL)).toBeNull();
170 });
171
172 it('liegt nach einem Neustart wieder an', async () => {
173 const { geschrieben } = kern();
174 const erster = renderHook(() => useKuerzelEinstellung(false));
175 await waitFor(() => {
176 expect(erster.result.current.aktiv).toBe(true);
177 });
178 act(() => {
179 erster.result.current.setzen(false);
180 });
181 await waitFor(() => {
182 expect(geschrieben).toHaveLength(1);
183 });
184 erster.unmount();
185
186 /* Neustart: dieselbe Attrappe, also dieselbe „Datei“ – ein neuer Hook
187 liest sie erneut. */
188 const { result } = renderHook(() => useKuerzelEinstellung(false));
189
190 await waitFor(() => {
191 expect(result.current.aktiv).toBe(false);
192 });
193 });
194
195 it('meldet, wenn die Wahl nicht gesichert werden konnte', async () => {
196 /* Der Schalter springt trotzdem um – aber dass er die Sitzung nicht
197 überlebt, muss gesagt werden statt verschwiegen. */
198 vi.spyOn(console, 'warn').mockImplementation(() => undefined);
199 kern({}, { schreibfehler: true });
200 const { result } = renderHook(() => useKuerzelEinstellung(false));
201 await waitFor(() => {
202 expect(result.current.aktiv).toBe(true);
203 });
204
205 act(() => {
206 result.current.setzen(false);
207 });
208
209 expect(result.current.aktiv).toBe(false);
210 await waitFor(() => {
211 expect(result.current.meldung).toMatch(/nicht dauerhaft gespeichert/u);
212 });
213 });
214 });
215
216 describe('Übernahme der bisherigen Wahl aus dem localStorage', () => {
217 it('übernimmt einen alten Wert beim ersten Start nach der Umstellung', async () => {
218 /* Wer die Kürzel vor der Umstellung abgeschaltet hat, soll sie nicht
219 beim nächsten Start wieder eingeschaltet vorfinden. */
220 window.localStorage.setItem(ALTER_SCHLUESSEL, 'aus');
221 const { geschrieben } = kern();
222
223 const { result } = renderHook(() => useKuerzelEinstellung(false));
224
225 await waitFor(() => {
226 expect(result.current.aktiv).toBe(false);
227 });
228 await waitFor(() => {
229 expect(geschrieben).toEqual([{ tastenkuerzel: false }]);
230 });
231 // Danach ist der alte Eintrag weg und wird nie wieder gelesen.
232 expect(window.localStorage.getItem(ALTER_SCHLUESSEL)).toBeNull();
233 });
234
235 it('übernimmt auch ein ausdrückliches „an“', async () => {
236 window.localStorage.setItem(ALTER_SCHLUESSEL, 'an');
237 const { geschrieben } = kern();
238
239 const { result } = renderHook(() => useKuerzelEinstellung(true));
240
241 await waitFor(() => {
242 expect(result.current.aktiv).toBe(true);
243 });
244 expect(geschrieben).toEqual([{ tastenkuerzel: true }]);
245 expect(result.current.wegenHilfsmittelAus).toBe(false);
246 });
247
248 it('lässt die Datei gewinnen, sobald sie einen Wert hat', async () => {
249 /* Nach der Übernahme wird der alte Ort nicht mehr gelesen – auch dann
250 nicht, wenn dort noch etwas steht (etwa nach einem gescheiterten
251 Löschversuch oder auf einem zweiten Gerät). */
252 window.localStorage.setItem(ALTER_SCHLUESSEL, 'aus');
253 const { geschrieben } = kern({ tastenkuerzel: true });
254
255 const { result } = renderHook(() => useKuerzelEinstellung(false));
256
257 await waitFor(() => {
258 expect(result.current.aktiv).toBe(true);
259 });
260 expect(geschrieben).toEqual([]);
261 expect(window.localStorage.getItem(ALTER_SCHLUESSEL)).toBeNull();
262 });
263
264 it('behält den alten Wert, wenn das Übernehmen scheitert', async () => {
265 /* Löschen vor dem erfolgreichen Schreiben wäre der eine Fehler, der die
266 Entscheidung endgültig vernichtet. Der nächste Start versucht es
267 stattdessen erneut. */
268 vi.spyOn(console, 'warn').mockImplementation(() => undefined);
269 window.localStorage.setItem(ALTER_SCHLUESSEL, 'aus');
270 kern({}, { schreibfehler: true });
271
272 const { result } = renderHook(() => useKuerzelEinstellung(false));
273
274 await waitFor(() => {
275 expect(result.current.aktiv).toBe(false);
276 });
277 await waitFor(() => {
278 expect(window.localStorage.getItem(ALTER_SCHLUESSEL)).toBe('aus');
279 });
280 });
281
282 it('kommt ohne Anwendungskern mit der Voreinstellung aus', () => {
283 /* Kein Preload (Vitest ohne Attrappe, Vorschau im Browser): Dann gibt es
284 nichts nachzuladen, und der Anfangswert ist schon der Endwert. */
285 const { result } = renderHook(() => useKuerzelEinstellung(true));
286
287 expect(result.current.aktiv).toBe(false);
288 expect(result.current.wegenHilfsmittelAus).toBe(true);
289 });
290 });