waffensachkunde

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

/ app tests hilfe-zahlen.test.ts

4,1 KB Rohdatei
app/tests/hilfe-zahlen.test.ts — 108 Zeilen
1 // @vitest-environment node
2 /**
3 * Die Zahlen im Handbuch gegen den **echten** Katalog.
4 *
5 * Seit 0.26.4 sagt das Kapitel „Die Lernsitzung“, wie eine frei zu
6 * formulierende Antwort gebaut ist – und begründet es mit gezählten
7 * Anfangswörtern: 26 Fragen beginnen mit „Was“, 25 mit „Welche“, 7 mit
8 * „Wie“, je 5 mit „Wann“ und „Nennen Sie“. Solche Sätze sind Zusagen: Ein
9 * neuer Katalogstand verschiebt sie, ohne dass es jemandem auffiele, und
10 * dann steht im Handbuch eine Zahl, die niemand mehr nachzählen kann.
11 *
12 * Deshalb zählt dieser Test sie bei jedem Lauf nach. Er liest die Zahlen aus
13 * dem Handbuchtext selbst – nicht aus einer zweiten Liste daneben, die
14 * ihrerseits auseinanderlaufen könnte.
15 */
16
17 import { readFileSync } from 'node:fs';
18 import { join } from 'node:path';
19 import { fileURLToPath } from 'node:url';
20
21 import { describe, expect, it } from 'vitest';
22
23 import { HILFE_KAPITEL } from '../src/shared/hilfe';
24 import type { Katalog } from '../src/shared/katalog';
25
26 const wurzel = fileURLToPath(new URL('..', import.meta.url));
27
28 const KATALOG = JSON.parse(
29 readFileSync(join(wurzel, '..', 'content', 'katalog', 'katalog.json'), 'utf8'),
30 ) as Katalog;
31
32 /** Alle Fließtexte des Handbuchs, zu einer Zeichenkette verbunden. */
33 function handbuchtext(): string {
34 return HILFE_KAPITEL.flatMap((kapitel) =>
35 kapitel.bloecke.map((block) => {
36 if (block.art === 'absatz') return block.text;
37 if (block.art === 'liste' || block.art === 'schritte') return block.punkte.join(' ');
38 return '';
39 }),
40 ).join(' ');
41 }
42
43 /** Die offenen Fragen: die ohne Antwortmöglichkeiten. */
44 function offeneFragen() {
45 return KATALOG.fragen.filter((frage) => (frage.optionen ?? []).length === 0);
46 }
47
48 /** Das erste großgeschriebene Wort des Fragetextes. */
49 function anfangswort(text: string): string {
50 return /[A-ZÄÖÜ][a-zäöüß]+/u.exec(text)?.[0] ?? '';
51 }
52
53 describe('Die Zahlen des Handbuchs stimmen mit dem Katalog überein', () => {
54 it('nennt so viele offene Fragen, wie der Katalog hat', () => {
55 const text = handbuchtext();
56 const anzahl = offeneFragen().length;
57
58 expect(text, `Der Katalog hat ${String(anzahl)} offene Fragen.`).toContain(
59 `${String(anzahl)} offenen Fragen des Katalogs`,
60 );
61 expect(text).toContain(`Nur 5 der ${String(anzahl)} Fragen nennen selbst eine Anzahl.`);
62 });
63
64 it('zählt die Anfangswörter richtig ab', () => {
65 const text = handbuchtext();
66 const gezaehlt = new Map<string, number>();
67 for (const frage of offeneFragen()) {
68 const wort = anfangswort(frage.frage.text);
69 gezaehlt.set(wort, (gezaehlt.get(wort) ?? 0) + 1);
70 }
71
72 /* Genau die fünf, die der Satz im Handbuch aufzählt. */
73 expect(gezaehlt.get('Was')).toBe(26);
74 expect(gezaehlt.get('Welche')).toBe(25);
75 expect(gezaehlt.get('Wie')).toBe(7);
76 expect(gezaehlt.get('Wann')).toBe(5);
77 expect(gezaehlt.get('Nennen')).toBe(5);
78
79 expect(text).toContain(
80 '26 beginnen mit „Was“, 25 mit „Welche“, 7 mit „Wie“, je 5 mit „Wann“ und „Nennen Sie“',
81 );
82 });
83
84 it('nennt so viele Glossareinträge, wie das Glossar führt', () => {
85 /*
86 Die Wache, die gefehlt hat. Das Kapitel „Die Lernsitzung“ sagt, wie viele
87 Fachbegriffe das Glossar führt — eine Zahl, die mit jeder redaktionellen
88 Ergänzung wandert. Sie stand am 03.09.2026 richtig auf 118; ohne diesen
89 Fall bliebe sie beim nächsten Zuwachs stumm stehen, wie es
90 `docs/stand.md` mit derselben Zahl passiert ist (dort 87).
91 */
92 const glossar = JSON.parse(
93 readFileSync(join(wurzel, '..', 'content', 'glossar.json'), 'utf8'),
94 ) as { eintraege: readonly unknown[] };
95
96 expect(handbuchtext()).toContain(`das Glossar führt ${String(glossar.eintraege.length)} davon`);
97 });
98
99 it('nennt so viele Fragen mit ausgeschriebener Anzahl, wie es gibt', () => {
100 /* „zwei Fälle“, „drei Voraussetzungen“ – ausgeschrieben, nicht als Ziffer. */
101 const mitAnzahl = offeneFragen().filter((frage) =>
102 /\b(zwei|drei|vier|fünf|sechs)\b/iu.test(frage.frage.text),
103 );
104
105 expect(mitAnzahl).toHaveLength(5);
106 expect(handbuchtext()).toContain('Nur 5 der');
107 });
108 });