waffensachkunde

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

/ app tests glossarabdeckung.test.ts

4,8 KB Rohdatei
app/tests/glossarabdeckung.test.ts — 125 Zeilen
1 /**
2 * Wie weit das Glossar den Katalog wirklich abdeckt.
3 *
4 * **Der Anlass.** Die 87 Einträge vor 0.22.0 waren fast durchweg
5 * Rechtsbegriffe. In Kapitel II – Waffentechnik – erreichten nur zwei von fünf
6 * Fragen überhaupt einen Treffer, und selbst diese Treffer waren meist
7 * Rechtsbegriffe wie „Schusswaffe“. Das Glossar ist die WCAG-3.1.3-Zusage
8 * dieser Software; eingelöst wurde sie ausgerechnet in dem Kapitel nicht,
9 * dessen Vokabular Einsteigern am fremdesten ist.
10 *
11 * **Warum gemessen und nicht gezählt.** Eine Zahl wie „118 Einträge“ sagt
12 * nichts darüber, ob sie dort greifen, wo gelernt wird. Gemessen wird deshalb
13 * mit **derselben Begriffssuche**, die auch unter der Frage läuft: Wie viele
14 * Fragen eines Kapitels finden mindestens einen Begriff?
15 */
16
17 import { readFileSync } from 'node:fs';
18 import { join } from 'node:path';
19
20 import { describe, expect, it } from 'vitest';
21
22 import { begriffssucheBauen, type Glossar } from '../src/shared/glossar';
23 import type { Katalog } from '../src/shared/katalog';
24
25 const WURZEL = join(process.cwd(), '..');
26 const GLOSSAR = JSON.parse(
27 readFileSync(join(WURZEL, 'content', 'glossar.json'), 'utf8'),
28 ) as Glossar;
29 const KATALOG = JSON.parse(
30 readFileSync(join(WURZEL, 'content', 'katalog', 'katalog.json'), 'utf8'),
31 ) as Katalog;
32
33 const SUCHE = begriffssucheBauen(GLOSSAR);
34
35 /** Der Text, den die Oberfläche nach Begriffen durchsucht. */
36 function volltext(frage: Katalog['fragen'][number]): string {
37 const optionen = (frage.optionen ?? []).map((option) => option.inhalt.text).join(' ');
38 return `${frage.frage.text} ${optionen} ${frage.musterantwort?.text ?? ''}`;
39 }
40
41 /** Anteil der Fragen eines Kapitels mit mindestens einem Begriff, in Prozent. */
42 function abdeckung(kapitelId: string): number {
43 const fragen = KATALOG.fragen.filter((frage) => frage.kapitel === kapitelId);
44 const getroffen = fragen.filter((frage) => SUCHE.imText(volltext(frage)).length > 0);
45 return Math.round((100 * getroffen.length) / fragen.length);
46 }
47
48 describe('Abdeckung des Glossars', () => {
49 it('erreicht in der Waffentechnik mindestens vier von fünf Fragen', () => {
50 /* Der eigentliche Befund: Kapitel II lag bei 40 Prozent. Die Schwelle
51 steht bewusst unter dem gemessenen Wert – sie soll einen Rückfall
52 melden, nicht jede Schwankung. */
53 expect(abdeckung('II')).toBeGreaterThanOrEqual(80);
54 });
55
56 it('fällt in keinem Kapitel unter drei von vier Fragen', () => {
57 for (const kapitel of KATALOG.kapitel) {
58 expect(abdeckung(kapitel.id), kapitel.id).toBeGreaterThanOrEqual(75);
59 }
60 });
61
62 it('kennt die tragenden Begriffe der Waffentechnik', () => {
63 /* Namentlich, damit ein stilles Verschwinden auffällt: Ein Schwellwert
64 allein bliebe grün, wenn ein Begriff wegfiele und ein anderer
65 dazukäme. */
66 const begriffe = new Set(GLOSSAR.eintraege.map((eintrag) => eintrag.begriff));
67 for (const begriff of [
68 'Munition',
69 'Patrone',
70 'Kaliber',
71 'Lauf',
72 'Verschluss',
73 'Patronenlager',
74 'Geschoss',
75 'Magazin',
76 'Trommel',
77 'Revolver',
78 'Pistole',
79 'Büchse',
80 'Flinte',
81 'Züge und Felder',
82 'Drall',
83 'Mündungsenergie',
84 'Höchstreichweite',
85 ]) {
86 expect(begriffe.has(begriff), begriff).toBe(true);
87 }
88 });
89
90 it('kennt die beiden Maßstäbe der Aufbewahrung und hält sie auseinander', () => {
91 /* Widerstandsgrad ist geltendes Recht, Sicherheitsstufe lebt nur über die
92 Übergangsregelung fort. Wer beides verwechselt, verwechselt
93 Bestandsschutz mit Anforderung. */
94 const widerstand = GLOSSAR.eintraege.find((e) => e.begriff === 'Widerstandsgrad');
95 const stufe = GLOSSAR.eintraege.find((e) => e.begriff === 'Sicherheitsstufe');
96
97 expect(widerstand?.fundstellen.some((f) => f.gesetz === 'AWaffV')).toBe(true);
98 expect(stufe?.text).toContain('Übergangsregelung');
99 expect(stufe?.kurz).toContain('nicht mehr vor');
100 });
101
102 it('begründet jeden Eintrag ohne Fundstelle', () => {
103 /* Bei der Waffentechnik ist das der Regelfall – sie ist fachliche Praxis,
104 kein Gesetzestext. Das auszusprechen ist die Alternative dazu, eine
105 Fundstelle zu dehnen. */
106 const stumm = GLOSSAR.eintraege
107 .filter(
108 (eintrag) => eintrag.fundstellen.length === 0 && eintrag.ohneFundstelleGrund === undefined,
109 )
110 .map((eintrag) => eintrag.begriff);
111
112 expect(stumm).toEqual([]);
113 });
114
115 it('lässt keinen „Siehe auch“-Verweis ins Leere laufen', () => {
116 const begriffe = new Set(GLOSSAR.eintraege.map((eintrag) => eintrag.begriff));
117 const kaputt = GLOSSAR.eintraege.flatMap((eintrag) =>
118 (eintrag.siehe ?? [])
119 .filter((ziel) => !begriffe.has(ziel))
120 .map((ziel) => `${eintrag.begriff} -> ${ziel}`),
121 );
122
123 expect(kaputt).toEqual([]);
124 });
125 });