/** * Der Frage-Steckbrief. * * **Der Anlass.** `FrageStand` transportierte `versuche`, `richtige`, * `zuletztBeantwortet`, `faelligAb` und `letzteBewertung` seit jeher über die * Brücke – die Oberfläche benutzte davon bis 0.22.0 **ausschließlich** * `gemerkt`. Der Lernende konnte nirgends beantworten: Wie oft hatte ich diese * Frage schon? Wie oft davon richtig? „Warum kommt die schon wieder?“ blieb * ohne Antwort, obwohl sie in der Datenbank stand. * * Gezeigt wird ausdrücklich der Stand **vor** der heutigen Antwort, und der * Text sagt das. Der Stand danach wäre bei offenen Fragen ohnehin nicht zu * haben – dort wird die Buchung bis zum Weiterblättern zurückgehalten. Eine * Zahl, die je nach Fragetyp etwas anderes bedeutet, wäre schlimmer als keine. */ import { render, screen } from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { Fragesteckbrief } from '../src/renderer/src/components/lernen/Fragesteckbrief'; import { abstandText } from '../src/renderer/src/lernen/zeitsaetze'; import type { FrageStand } from '../src/shared/lernstand'; function stand(teil: Partial = {}): FrageStand { return { frageId: 'I.1-01', versuche: 4, richtige: 3, zuletztBeantwortet: '2026-08-17T09:00:00.000Z', faelligAb: '2026-09-04T09:00:00.000Z', gemerkt: false, letzteBewertung: 'gut', ...teil, }; } function bruecke(antwort: FrageStand | Error): void { vi.stubGlobal('lernApp', { lernFragestand: vi.fn(() => antwort instanceof Error ? Promise.reject(antwort) : Promise.resolve(antwort), ), }); } afterEach(() => { vi.unstubAllGlobals(); }); describe('Zeitabstand in Worten', () => { const jetzt = new Date(2026, 7, 29, 14, 0, 0); it('sagt „heute“ und „gestern“', () => { expect(abstandText(new Date(2026, 7, 29, 8).toISOString(), jetzt)).toBe('heute'); expect(abstandText(new Date(2026, 7, 28, 22).toISOString(), jetzt)).toBe('gestern'); }); it('rechnet über Kalendertage, nicht über Stunden', () => { /* „Gestern“ soll auch dann „gestern“ heißen, wenn dreißig Stunden dazwischenliegen – dieselbe Rechnung wie im Anwendungskern. */ expect(abstandText(new Date(2026, 7, 28, 6).toISOString(), jetzt)).toBe('gestern'); }); it('zählt darüber in Tagen', () => { expect(abstandText(new Date(2026, 7, 17, 9).toISOString(), jetzt)).toBe('vor 12 Tagen'); }); it('kommt mit einer unlesbaren Zeit zurecht', () => { expect(abstandText('kein Datum', jetzt)).toBe(''); }); }); describe('Steckbrief', () => { it('nennt Bearbeitungen, Treffer und den letzten Zeitpunkt', async () => { bruecke(stand()); render(); expect(await screen.findByText('Diese Frage bisher')).toBeInTheDocument(); expect(screen.getByText(/4-mal bearbeitet/u)).toBeInTheDocument(); expect(screen.getByText(/3-mal richtig/u)).toBeInTheDocument(); }); it('schweigt bei einer zum ersten Mal vorgelegten Frage', async () => { /* Eine Zeile „0-mal bearbeitet, 0-mal richtig“ wäre kein Steckbrief, sondern Lärm. */ bruecke(stand({ versuche: 0, richtige: 0, zuletztBeantwortet: null })); const { container } = render(); await Promise.resolve(); expect(container.querySelector('.steckbrief')).toBeNull(); }); it('beugt die Einzahl richtig', async () => { bruecke(stand({ versuche: 1, richtige: 1 })); render(); expect(await screen.findByText(/einmal bearbeitet/u)).toBeInTheDocument(); expect(screen.getByText(/davon einmal richtig/u)).toBeInTheDocument(); }); it('bleibt still, wenn der Kanal fehlt', () => { /* Der Steckbrief ist eine Zugabe. Eine ältere Fassung des Anwendungskerns darf die Sitzung nicht stören. */ vi.stubGlobal('lernApp', {}); const { container } = render(); expect(container.querySelector('.steckbrief')).toBeNull(); }); it('bleibt still, wenn das Lesen scheitert', async () => { bruecke(new Error('Datenbank gesperrt')); const { container } = render(); await Promise.resolve(); expect(container.querySelector('.steckbrief')).toBeNull(); }); it('fragt ohne Lernprofil gar nicht erst', () => { bruecke(stand()); render(); expect( (globalThis as { lernApp?: { lernFragestand?: unknown } }).lernApp?.lernFragestand, ).not.toHaveBeenCalled(); }); });