waffensachkunde

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

/ app tests themen.test.ts

4,9 KB Rohdatei
app/tests/themen.test.ts — 135 Zeilen
1 /**
2 * Die redaktionelle Feingliederung der Kapitel II bis IV.
3 *
4 * **Der Anlass.** Der amtliche Katalog gliedert nur Kapitel I in Abschnitte.
5 * Für 230 Fragen steht `abschnitt` auf `null`; wer in der Simulation an
6 * Munitionskunde scheiterte, sah bis 0.22.0 nur „Kapitel II schwach“ und
7 * konnte nur das ganze Kapitel nachüben – 92 Fragen.
8 *
9 * Die eigentliche Zusage steht ganz unten: **Vollständigkeit**. Eine
10 * lückenhafte Gliederung wäre schlimmer als keine, weil „Thema üben“ dann
11 * Fragen stillschweigend überginge.
12 */
13
14 import { readFileSync } from 'node:fs';
15 import { join } from 'node:path';
16
17 import { describe, expect, it } from 'vitest';
18
19 import type { Katalog } from '../src/shared/katalog';
20 import { gruppenZuKapitel, gruppeZuFrage, THEMEN_LEER, type Themen } from '../src/shared/themen';
21
22 const WURZEL = join(process.cwd(), '..');
23 const THEMEN = JSON.parse(readFileSync(join(WURZEL, 'content', 'themen.json'), 'utf8')) as Themen;
24 const KATALOG = JSON.parse(
25 readFileSync(join(WURZEL, 'content', 'katalog', 'katalog.json'), 'utf8'),
26 ) as Katalog;
27
28 /** Kapitel ohne amtliche Abschnittsgliederung. */
29 const OHNE_ABSCHNITTE = ['II', 'III', 'IV'];
30
31 describe('gruppenZuKapitel', () => {
32 it('liefert die Gruppen eines Kapitels in der Reihenfolge der Datei', () => {
33 const gruppen = gruppenZuKapitel(THEMEN, 'III');
34
35 expect(gruppen.length).toBeGreaterThan(3);
36 expect(gruppen.every((gruppe) => gruppe.kapitel === 'III')).toBe(true);
37 });
38
39 it('liefert für Kapitel I nichts', () => {
40 /* Dort gibt es die amtlichen Abschnitte. Eine zweite Gliederung daneben
41 wäre die nächste Zahl, die still auseinanderläuft. */
42 expect(gruppenZuKapitel(THEMEN, 'I')).toEqual([]);
43 });
44
45 it('kommt ohne geladene Gliederung zurecht', () => {
46 expect(gruppenZuKapitel(THEMEN_LEER, 'II')).toEqual([]);
47 expect(gruppeZuFrage(THEMEN_LEER, 'II-01')).toBeNull();
48 });
49 });
50
51 describe('gruppeZuFrage', () => {
52 it('findet die Gruppe einer Frage', () => {
53 expect(gruppeZuFrage(THEMEN, 'II-34')?.titel).toBe('Schalldämpfer und Schallentstehung');
54 expect(gruppeZuFrage(THEMEN, 'III-08')?.kapitel).toBe('III');
55 });
56
57 it('gibt für Fragen aus Kapitel I nichts zurück', () => {
58 expect(gruppeZuFrage(THEMEN, 'I.1-01')).toBeNull();
59 });
60 });
61
62 describe('Die ausgelieferte Gliederung', () => {
63 const zugeordnet = new Map<string, string>();
64 for (const gruppe of THEMEN.gruppen) {
65 for (const frageId of gruppe.fragen) {
66 zugeordnet.set(frageId, gruppe.id);
67 }
68 }
69
70 it('ordnet jede Frage der Kapitel II bis IV genau einer Gruppe zu', () => {
71 /* Die eigentliche Zusage. Ohne sie überginge „Thema üben“ Fragen, ohne es
72 zu sagen – und niemandem fiele auf, welche. */
73 const fehlend = KATALOG.fragen
74 .filter((frage) => OHNE_ABSCHNITTE.includes(frage.kapitel) && !zugeordnet.has(frage.id))
75 .map((frage) => frage.id);
76
77 expect(fehlend).toEqual([]);
78 });
79
80 it('lässt Kapitel I in Ruhe', () => {
81 const zuviel = KATALOG.fragen
82 .filter((frage) => !OHNE_ABSCHNITTE.includes(frage.kapitel) && zugeordnet.has(frage.id))
83 .map((frage) => frage.id);
84
85 expect(zuviel).toEqual([]);
86 });
87
88 it('nennt keine Frage in zwei Gruppen', () => {
89 const alle = THEMEN.gruppen.flatMap((gruppe) => gruppe.fragen);
90
91 expect(alle.length).toBe(new Set(alle).size);
92 expect(alle.length).toBe(230);
93 });
94
95 it('kennt nur Fragen, die es gibt', () => {
96 const bekannt = new Set(KATALOG.fragen.map((frage) => frage.id));
97 const erfunden = [...zugeordnet.keys()].filter((frageId) => !bekannt.has(frageId));
98
99 expect(erfunden).toEqual([]);
100 });
101
102 it('mischt in keiner Gruppe zwei Kapitel', () => {
103 const kapitelJeFrage = new Map(KATALOG.fragen.map((frage) => [frage.id, frage.kapitel]));
104 const gemischt = THEMEN.gruppen
105 .filter(
106 (gruppe) =>
107 !gruppe.fragen.every((frageId) => kapitelJeFrage.get(frageId) === gruppe.kapitel),
108 )
109 .map((gruppe) => gruppe.id);
110
111 expect(gemischt).toEqual([]);
112 });
113
114 it('führt keine Gruppe mit weniger als drei Fragen', () => {
115 // Eine Gruppe mit einer Frage ist kein Thema, sondern ein Einzelfall.
116 const winzig = THEMEN.gruppen
117 .filter((gruppe) => gruppe.fragen.length < 3)
118 .map((gruppe) => `${gruppe.id} (${String(gruppe.fragen.length)})`);
119
120 expect(winzig).toEqual([]);
121 });
122
123 it('vergibt eindeutige Kennungen und nichtleere Titel', () => {
124 const kennungen = THEMEN.gruppen.map((gruppe) => gruppe.id);
125
126 expect(kennungen.length).toBe(new Set(kennungen).size);
127 expect(THEMEN.gruppen.every((gruppe) => gruppe.titel.trim().length > 0)).toBe(true);
128 });
129
130 it('weist sich selbst als Ergänzung dieser Software aus', () => {
131 /* Der amtliche Katalog darf nicht verändert wiedergegeben werden; was
132 eigener Zusatz ist, muss erkennbar sein – auch in der Datei. */
133 expect(THEMEN.meta.hinweis).toContain('nicht Teil des amtlichen');
134 });
135 });