/** * Der Lernstand-Hook, unmittelbar am Zustand geprüft. * * Hier geht es um etwas, das sich über die Oberfläche nicht fassen lässt: ein * Aufblitzen. Die Profil-ID lief beim Neuladen für einen Renderdurchlauf * durch `null`, und jeder Verbraucher las das als „kein Profil" – die * Prüfungsauswertung meldete kurz „Ohne Lernprofil wird kein Verlauf * geführt", der Lernbericht wurde unbedienbar. Ein Test über die Oberfläche * käme immer zu spät: Bis er hinsieht, ist die Zahl längst wieder da. * `renderHook` sieht genau den Durchlauf dazwischen. */ import { act, renderHook, waitFor } from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { useLernstand } from '../src/renderer/src/hooks/useLernstand'; import { EINSTELLUNGEN_STANDARD, type Einstellungen } from '../src/shared/ipc'; import type { Lernuebersicht, Profil } from '../src/shared/lernstand'; function profil(id: number, name: string): Profil { return { id, name, pruefungstermin: null, kapitelAusschluss: [], erstelltAm: '2026-01-01T08:00:00.000Z', }; } const OLAF = profil(1, 'Olaf'); const ANNA = profil(2, 'Anna'); const UEBERSICHT: Lernuebersicht = { fragenGesamt: 575, beantwortet: 0, belegt: 0, reifegrad: 0 / 575, stufe: 'ohne_beleg', deckelnd: [], faellig: 0, gemerkt: 0, offen: 0, fehler: 0, heuteRichtig: 0, heuteFalsch: 0, heuteBearbeitet: 0, tageSeitLetzterAntwort: null, bereiche: [], themengruppen: [], }; interface Bruecke { profilListe: () => Promise; lernUebersicht: (profilId: number) => Promise; einstellungenLesen: () => Promise; einstellungenSchreiben: (aenderung: Partial) => Promise; } function stelle(gespeichert?: number): Bruecke { const bruecke: Bruecke = { profilListe: vi.fn(() => Promise.resolve([OLAF, ANNA])), lernUebersicht: vi.fn(() => Promise.resolve(UEBERSICHT)), einstellungenLesen: vi.fn(() => Promise.resolve( gespeichert === undefined ? EINSTELLUNGEN_STANDARD : { ...EINSTELLUNGEN_STANDARD, profilId: gespeichert }, ), ), einstellungenSchreiben: vi.fn(() => Promise.resolve(EINSTELLUNGEN_STANDARD)), }; vi.stubGlobal('lernApp', bruecke); return bruecke; } afterEach(() => { vi.unstubAllGlobals(); }); describe('useLernstand', () => { it('nimmt das zuletzt benutzte Profil aus den Einstellungen', async () => { const bruecke = stelle(2); const { result } = renderHook(() => useLernstand()); await waitFor(() => { expect(result.current.profilId).toBe(2); }); expect(bruecke.lernUebersicht).toHaveBeenCalledWith(2); }); it('führt die Profil-Liste mit, damit die Oberfläche wählen lassen kann', async () => { stelle(); const { result } = renderHook(() => useLernstand()); await waitFor(() => { expect(result.current.profile).toHaveLength(2); }); expect(result.current.profile.map((p) => p.name)).toEqual(['Olaf', 'Anna']); }); it('lässt die Profil-ID beim Neuladen stehen', async () => { stelle(); const { result } = renderHook(() => useLernstand()); await waitFor(() => { expect(result.current.profilId).toBe(1); }); act(() => { result.current.neuLaden(); }); /* Genau hier lag der Fehler: `neuLaden` setzte den ganzen Zustand auf „lädt", und „lädt" hieß `profilId: null`. */ expect(result.current.profilId).toBe(1); await waitFor(() => { expect(result.current.uebersicht).not.toBeNull(); }); expect(result.current.profilId).toBe(1); }); it('springt beim Wechsel direkt auf die neue Zahl, nie über null', async () => { const bruecke = stelle(); const { result } = renderHook(() => useLernstand()); await waitFor(() => { expect(result.current.profilId).toBe(1); }); act(() => { result.current.wechselnZu(2); }); /* Ohne den direkten Sprung schriebe ein Verbraucher in genau diesem Augenblick ins Leere – die Antwort landete bei keinem Profil. */ expect(result.current.profilId).toBe(2); await waitFor(() => { expect(bruecke.lernUebersicht).toHaveBeenCalledWith(2); }); expect(bruecke.einstellungenSchreiben).toHaveBeenCalledWith({ profilId: 2 }); }); it('merkt sich auch die Bestätigung des schon laufenden Profils', async () => { const bruecke = stelle(); const { result } = renderHook(() => useLernstand()); await waitFor(() => { expect(result.current.profilId).toBe(1); }); (bruecke.lernUebersicht as ReturnType).mockClear(); act(() => { result.current.wechselnZu(1); }); /* Beim ersten Start steht in den Einstellungen noch nichts – die Bestätigung „ja, ich bin das" gehört hinein. Neu geladen wird deshalb aber nichts: Es hat sich nichts geändert. */ expect(bruecke.einstellungenSchreiben).toHaveBeenCalledWith({ profilId: 1 }); expect(bruecke.lernUebersicht).not.toHaveBeenCalled(); expect(result.current.laedt).toBe(false); }); it('übergeht ein gespeichertes Profil, das es nicht mehr gibt', async () => { stelle(99); const { result } = renderHook(() => useLernstand()); await waitFor(() => { expect(result.current.profilId).toBe(1); }); expect(result.current.meldung).toBeNull(); }); });