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 — 174 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 };
50
51 interface Bruecke {
52 profilListe: () => Promise<Profil[]>;
53 lernUebersicht: (profilId: number) => Promise<Lernuebersicht>;
54 einstellungenLesen: () => Promise<Einstellungen>;
55 einstellungenSchreiben: (aenderung: Partial<Einstellungen>) => Promise<Einstellungen>;
56 }
57
58 function stelle(gespeichert?: number): Bruecke {
59 const bruecke: Bruecke = {
60 profilListe: vi.fn(() => Promise.resolve([OLAF, ANNA])),
61 lernUebersicht: vi.fn(() => Promise.resolve(UEBERSICHT)),
62 einstellungenLesen: vi.fn(() =>
63 Promise.resolve(
64 gespeichert === undefined
65 ? EINSTELLUNGEN_STANDARD
66 : { ...EINSTELLUNGEN_STANDARD, profilId: gespeichert },
67 ),
68 ),
69 einstellungenSchreiben: vi.fn(() => Promise.resolve(EINSTELLUNGEN_STANDARD)),
70 };
71 vi.stubGlobal('lernApp', bruecke);
72 return bruecke;
73 }
74
75 afterEach(() => {
76 vi.unstubAllGlobals();
77 });
78
79 describe('useLernstand', () => {
80 it('nimmt das zuletzt benutzte Profil aus den Einstellungen', async () => {
81 const bruecke = stelle(2);
82 const { result } = renderHook(() => useLernstand());
83
84 await waitFor(() => {
85 expect(result.current.profilId).toBe(2);
86 });
87 expect(bruecke.lernUebersicht).toHaveBeenCalledWith(2);
88 });
89
90 it('führt die Profil-Liste mit, damit die Oberfläche wählen lassen kann', async () => {
91 stelle();
92 const { result } = renderHook(() => useLernstand());
93
94 await waitFor(() => {
95 expect(result.current.profile).toHaveLength(2);
96 });
97 expect(result.current.profile.map((p) => p.name)).toEqual(['Olaf', 'Anna']);
98 });
99
100 it('lässt die Profil-ID beim Neuladen stehen', async () => {
101 stelle();
102 const { result } = renderHook(() => useLernstand());
103
104 await waitFor(() => {
105 expect(result.current.profilId).toBe(1);
106 });
107
108 act(() => {
109 result.current.neuLaden();
110 });
111
112 /* Genau hier lag der Fehler: `neuLaden` setzte den ganzen Zustand auf
113 „lädt", und „lädt" hieß `profilId: null`. */
114 expect(result.current.profilId).toBe(1);
115
116 await waitFor(() => {
117 expect(result.current.uebersicht).not.toBeNull();
118 });
119 expect(result.current.profilId).toBe(1);
120 });
121
122 it('springt beim Wechsel direkt auf die neue Zahl, nie über null', async () => {
123 const bruecke = stelle();
124 const { result } = renderHook(() => useLernstand());
125
126 await waitFor(() => {
127 expect(result.current.profilId).toBe(1);
128 });
129
130 act(() => {
131 result.current.wechselnZu(2);
132 });
133
134 /* Ohne den direkten Sprung schriebe ein Verbraucher in genau diesem
135 Augenblick ins Leere – die Antwort landete bei keinem Profil. */
136 expect(result.current.profilId).toBe(2);
137
138 await waitFor(() => {
139 expect(bruecke.lernUebersicht).toHaveBeenCalledWith(2);
140 });
141 expect(bruecke.einstellungenSchreiben).toHaveBeenCalledWith({ profilId: 2 });
142 });
143
144 it('merkt sich auch die Bestätigung des schon laufenden Profils', async () => {
145 const bruecke = stelle();
146 const { result } = renderHook(() => useLernstand());
147
148 await waitFor(() => {
149 expect(result.current.profilId).toBe(1);
150 });
151 (bruecke.lernUebersicht as ReturnType<typeof vi.fn>).mockClear();
152
153 act(() => {
154 result.current.wechselnZu(1);
155 });
156
157 /* Beim ersten Start steht in den Einstellungen noch nichts – die
158 Bestätigung „ja, ich bin das" gehört hinein. Neu geladen wird
159 deshalb aber nichts: Es hat sich nichts geändert. */
160 expect(bruecke.einstellungenSchreiben).toHaveBeenCalledWith({ profilId: 1 });
161 expect(bruecke.lernUebersicht).not.toHaveBeenCalled();
162 expect(result.current.laedt).toBe(false);
163 });
164
165 it('übergeht ein gespeichertes Profil, das es nicht mehr gibt', async () => {
166 stelle(99);
167 const { result } = renderHook(() => useLernstand());
168
169 await waitFor(() => {
170 expect(result.current.profilId).toBe(1);
171 });
172 expect(result.current.meldung).toBeNull();
173 });
174 });