waffensachkunde

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

/ app tests fragesteckbrief.test.tsx

4,6 KB Rohdatei
app/tests/fragesteckbrief.test.tsx — 125 Zeilen
1 /**
2 * Der Frage-Steckbrief.
3 *
4 * **Der Anlass.** `FrageStand` transportierte `versuche`, `richtige`,
5 * `zuletztBeantwortet`, `faelligAb` und `letzteBewertung` seit jeher über die
6 * Brücke – die Oberfläche benutzte davon bis 0.22.0 **ausschließlich**
7 * `gemerkt`. Der Lernende konnte nirgends beantworten: Wie oft hatte ich diese
8 * Frage schon? Wie oft davon richtig? „Warum kommt die schon wieder?“ blieb
9 * ohne Antwort, obwohl sie in der Datenbank stand.
10 *
11 * Gezeigt wird ausdrücklich der Stand **vor** der heutigen Antwort, und der
12 * Text sagt das. Der Stand danach wäre bei offenen Fragen ohnehin nicht zu
13 * haben – dort wird die Buchung bis zum Weiterblättern zurückgehalten. Eine
14 * Zahl, die je nach Fragetyp etwas anderes bedeutet, wäre schlimmer als keine.
15 */
16
17 import { render, screen } from '@testing-library/react';
18 import { afterEach, describe, expect, it, vi } from 'vitest';
19
20 import { Fragesteckbrief } from '../src/renderer/src/components/lernen/Fragesteckbrief';
21 import { abstandText } from '../src/renderer/src/lernen/zeitsaetze';
22 import type { FrageStand } from '../src/shared/lernstand';
23
24 function stand(teil: Partial<FrageStand> = {}): FrageStand {
25 return {
26 frageId: 'I.1-01',
27 versuche: 4,
28 richtige: 3,
29 zuletztBeantwortet: '2026-08-17T09:00:00.000Z',
30 faelligAb: '2026-09-04T09:00:00.000Z',
31 gemerkt: false,
32 letzteBewertung: 'gut',
33 ...teil,
34 };
35 }
36
37 function bruecke(antwort: FrageStand | Error): void {
38 vi.stubGlobal('lernApp', {
39 lernFragestand: vi.fn(() =>
40 antwort instanceof Error ? Promise.reject(antwort) : Promise.resolve(antwort),
41 ),
42 });
43 }
44
45 afterEach(() => {
46 vi.unstubAllGlobals();
47 });
48
49 describe('Zeitabstand in Worten', () => {
50 const jetzt = new Date(2026, 7, 29, 14, 0, 0);
51
52 it('sagt „heute“ und „gestern“', () => {
53 expect(abstandText(new Date(2026, 7, 29, 8).toISOString(), jetzt)).toBe('heute');
54 expect(abstandText(new Date(2026, 7, 28, 22).toISOString(), jetzt)).toBe('gestern');
55 });
56
57 it('rechnet über Kalendertage, nicht über Stunden', () => {
58 /* „Gestern“ soll auch dann „gestern“ heißen, wenn dreißig Stunden
59 dazwischenliegen – dieselbe Rechnung wie im Anwendungskern. */
60 expect(abstandText(new Date(2026, 7, 28, 6).toISOString(), jetzt)).toBe('gestern');
61 });
62
63 it('zählt darüber in Tagen', () => {
64 expect(abstandText(new Date(2026, 7, 17, 9).toISOString(), jetzt)).toBe('vor 12 Tagen');
65 });
66
67 it('kommt mit einer unlesbaren Zeit zurecht', () => {
68 expect(abstandText('kein Datum', jetzt)).toBe('');
69 });
70 });
71
72 describe('Steckbrief', () => {
73 it('nennt Bearbeitungen, Treffer und den letzten Zeitpunkt', async () => {
74 bruecke(stand());
75 render(<Fragesteckbrief frageId="I.1-01" profilId={1} />);
76
77 expect(await screen.findByText('Diese Frage bisher')).toBeInTheDocument();
78 expect(screen.getByText(/4-mal bearbeitet/u)).toBeInTheDocument();
79 expect(screen.getByText(/3-mal richtig/u)).toBeInTheDocument();
80 });
81
82 it('schweigt bei einer zum ersten Mal vorgelegten Frage', async () => {
83 /* Eine Zeile „0-mal bearbeitet, 0-mal richtig“ wäre kein Steckbrief,
84 sondern Lärm. */
85 bruecke(stand({ versuche: 0, richtige: 0, zuletztBeantwortet: null }));
86 const { container } = render(<Fragesteckbrief frageId="I.1-01" profilId={1} />);
87
88 await Promise.resolve();
89 expect(container.querySelector('.steckbrief')).toBeNull();
90 });
91
92 it('beugt die Einzahl richtig', async () => {
93 bruecke(stand({ versuche: 1, richtige: 1 }));
94 render(<Fragesteckbrief frageId="I.1-01" profilId={1} />);
95
96 expect(await screen.findByText(/einmal bearbeitet/u)).toBeInTheDocument();
97 expect(screen.getByText(/davon einmal richtig/u)).toBeInTheDocument();
98 });
99
100 it('bleibt still, wenn der Kanal fehlt', () => {
101 /* Der Steckbrief ist eine Zugabe. Eine ältere Fassung des
102 Anwendungskerns darf die Sitzung nicht stören. */
103 vi.stubGlobal('lernApp', {});
104 const { container } = render(<Fragesteckbrief frageId="I.1-01" profilId={1} />);
105
106 expect(container.querySelector('.steckbrief')).toBeNull();
107 });
108
109 it('bleibt still, wenn das Lesen scheitert', async () => {
110 bruecke(new Error('Datenbank gesperrt'));
111 const { container } = render(<Fragesteckbrief frageId="I.1-01" profilId={1} />);
112
113 await Promise.resolve();
114 expect(container.querySelector('.steckbrief')).toBeNull();
115 });
116
117 it('fragt ohne Lernprofil gar nicht erst', () => {
118 bruecke(stand());
119 render(<Fragesteckbrief frageId="I.1-01" profilId={null} />);
120
121 expect(
122 (globalThis as { lernApp?: { lernFragestand?: unknown } }).lernApp?.lernFragestand,
123 ).not.toHaveBeenCalled();
124 });
125 });