waffensachkunde

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

/ app tests useLernstand.test.ts

5,3 KB Rohdatei
app/tests/useLernstand.test.ts — 175 Zeilen
1 /**
2 * Der Lernstand-Hook, unmittelbar am Zustand geprüft.
3 *
4 * Hier geht es um etwas, das sich über die Oberfläche nicht fassen lässt: ein
5 * Aufblitzen. Die Profil-ID lief beim Neuladen für einen Renderdurchlauf
6 * durch `null`, und jeder Verbraucher las das als „kein Profil" – die
7 * Prüfungsauswertung meldete kurz „Ohne Lernprofil wird kein Verlauf
8 * geführt", der Lernbericht wurde unbedienbar. Ein Test über die Oberfläche
9 * käme immer zu spät: Bis er hinsieht, ist die Zahl längst wieder da.
10 * `renderHook` sieht genau den Durchlauf dazwischen.
11 */
12
13 import { act, renderHook, waitFor } from '@testing-library/react';
14 import { afterEach, describe, expect, it, vi } from 'vitest';
15
16 import { useLernstand } from '../src/renderer/src/hooks/useLernstand';
17 import { EINSTELLUNGEN_STANDARD, type Einstellungen } from '../src/shared/ipc';
18 import type { Lernuebersicht, Profil } from '../src/shared/lernstand';
19
20 function profil(id: number, name: string): Profil {
21 return {
22 id,
23 name,
24 pruefungstermin: null,
25 kapitelAusschluss: [],
26 erstelltAm: '2026-01-01T08:00:00.000Z',
27 };
28 }
29
30 const OLAF = profil(1, 'Olaf');
31 const ANNA = profil(2, 'Anna');
32
33 const UEBERSICHT: Lernuebersicht = {
34 fragenGesamt: 575,
35 beantwortet: 0,
36 belegt: 0,
37 reifegrad: 0 / 575,
38 stufe: 'ohne_beleg',
39 deckelnd: [],
40 faellig: 0,
41 gemerkt: 0,
42 offen: 0,
43 fehler: 0,
44 heuteRichtig: 0,
45 heuteFalsch: 0,
46 heuteBearbeitet: 0,
47 tageSeitLetzterAntwort: null,
48 bereiche: [],
49 themengruppen: [],
50 };
51
52 interface Bruecke {
53 profilListe: () => Promise<Profil[]>;
54 lernUebersicht: (profilId: number) => Promise<Lernuebersicht>;
55 einstellungenLesen: () => Promise<Einstellungen>;
56 einstellungenSchreiben: (aenderung: Partial<Einstellungen>) => Promise<Einstellungen>;
57 }
58
59 function stelle(gespeichert?: number): Bruecke {
60 const bruecke: Bruecke = {
61 profilListe: vi.fn(() => Promise.resolve([OLAF, ANNA])),
62 lernUebersicht: vi.fn(() => Promise.resolve(UEBERSICHT)),
63 einstellungenLesen: vi.fn(() =>
64 Promise.resolve(
65 gespeichert === undefined
66 ? EINSTELLUNGEN_STANDARD
67 : { ...EINSTELLUNGEN_STANDARD, profilId: gespeichert },
68 ),
69 ),
70 einstellungenSchreiben: vi.fn(() => Promise.resolve(EINSTELLUNGEN_STANDARD)),
71 };
72 vi.stubGlobal('lernApp', bruecke);
73 return bruecke;
74 }
75
76 afterEach(() => {
77 vi.unstubAllGlobals();
78 });
79
80 describe('useLernstand', () => {
81 it('nimmt das zuletzt benutzte Profil aus den Einstellungen', async () => {
82 const bruecke = stelle(2);
83 const { result } = renderHook(() => useLernstand());
84
85 await waitFor(() => {
86 expect(result.current.profilId).toBe(2);
87 });
88 expect(bruecke.lernUebersicht).toHaveBeenCalledWith(2);
89 });
90
91 it('führt die Profil-Liste mit, damit die Oberfläche wählen lassen kann', async () => {
92 stelle();
93 const { result } = renderHook(() => useLernstand());
94
95 await waitFor(() => {
96 expect(result.current.profile).toHaveLength(2);
97 });
98 expect(result.current.profile.map((p) => p.name)).toEqual(['Olaf', 'Anna']);
99 });
100
101 it('lässt die Profil-ID beim Neuladen stehen', async () => {
102 stelle();
103 const { result } = renderHook(() => useLernstand());
104
105 await waitFor(() => {
106 expect(result.current.profilId).toBe(1);
107 });
108
109 act(() => {
110 result.current.neuLaden();
111 });
112
113 /* Genau hier lag der Fehler: `neuLaden` setzte den ganzen Zustand auf
114 „lädt", und „lädt" hieß `profilId: null`. */
115 expect(result.current.profilId).toBe(1);
116
117 await waitFor(() => {
118 expect(result.current.uebersicht).not.toBeNull();
119 });
120 expect(result.current.profilId).toBe(1);
121 });
122
123 it('springt beim Wechsel direkt auf die neue Zahl, nie über null', async () => {
124 const bruecke = stelle();
125 const { result } = renderHook(() => useLernstand());
126
127 await waitFor(() => {
128 expect(result.current.profilId).toBe(1);
129 });
130
131 act(() => {
132 result.current.wechselnZu(2);
133 });
134
135 /* Ohne den direkten Sprung schriebe ein Verbraucher in genau diesem
136 Augenblick ins Leere – die Antwort landete bei keinem Profil. */
137 expect(result.current.profilId).toBe(2);
138
139 await waitFor(() => {
140 expect(bruecke.lernUebersicht).toHaveBeenCalledWith(2);
141 });
142 expect(bruecke.einstellungenSchreiben).toHaveBeenCalledWith({ profilId: 2 });
143 });
144
145 it('merkt sich auch die Bestätigung des schon laufenden Profils', async () => {
146 const bruecke = stelle();
147 const { result } = renderHook(() => useLernstand());
148
149 await waitFor(() => {
150 expect(result.current.profilId).toBe(1);
151 });
152 (bruecke.lernUebersicht as ReturnType<typeof vi.fn>).mockClear();
153
154 act(() => {
155 result.current.wechselnZu(1);
156 });
157
158 /* Beim ersten Start steht in den Einstellungen noch nichts – die
159 Bestätigung „ja, ich bin das" gehört hinein. Neu geladen wird
160 deshalb aber nichts: Es hat sich nichts geändert. */
161 expect(bruecke.einstellungenSchreiben).toHaveBeenCalledWith({ profilId: 1 });
162 expect(bruecke.lernUebersicht).not.toHaveBeenCalled();
163 expect(result.current.laedt).toBe(false);
164 });
165
166 it('übergeht ein gespeichertes Profil, das es nicht mehr gibt', async () => {
167 stelle(99);
168 const { result } = renderHook(() => useLernstand());
169
170 await waitFor(() => {
171 expect(result.current.profilId).toBe(1);
172 });
173 expect(result.current.meldung).toBeNull();
174 });
175 });