waffensachkunde

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

/ app tests glossar.test.ts

6,3 KB Rohdatei
app/tests/glossar.test.ts — 165 Zeilen
1 // @vitest-environment node
2 /**
3 * Begriffserkennung im Fragetext.
4 *
5 * Der heikle Teil ist die Wortgrenze. `\b` kennt nur ASCII-Wortzeichen –
6 * in „Schießstätte" gälte damit jedes ß und ä als Grenze. Und „Waffe" darf
7 * nicht in „Waffenbesitzkarte" treffen, sonst hängt an jeder zweiten Frage
8 * ein Begriff, der dort gar nicht steht.
9 */
10
11 import { describe, expect, it } from 'vitest';
12
13 import { begriffssucheBauen, type Glossar, type Glossareintrag } from '../src/shared/glossar';
14
15 function eintrag(begriff: string, varianten: string[]): Glossareintrag {
16 return {
17 begriff,
18 art: 'begriff',
19 kurz: `Bedeutung von ${begriff} in einem Satz, lang genug zum Anzeigen.`,
20 varianten,
21 fundstellen: [],
22 ohneFundstelleGrund: 'Prüfeintrag ohne gesetzliche Grundlage.',
23 };
24 }
25
26 function glossar(...eintraege: Glossareintrag[]): Glossar {
27 return {
28 meta: { version: 1, stand: '2026-08-21', gesetzesstand: {}, hinweis: 'Prüfstand' },
29 eintraege,
30 };
31 }
32
33 describe('Begriffe im Text finden', () => {
34 it('findet die Grundform', () => {
35 const suche = begriffssucheBauen(glossar(eintrag('Führen', ['führen', 'führt', 'geführt'])));
36
37 expect(suche.imText('Wer eine Waffe führt, braucht einen Waffenschein.')).toHaveLength(1);
38 });
39
40 it('findet alle angegebenen Wortformen', () => {
41 const suche = begriffssucheBauen(glossar(eintrag('Führen', ['führen', 'führt', 'geführt'])));
42
43 for (const satz of ['Darf man führen?', 'Er führt sie.', 'Sie wurde geführt.']) {
44 expect(suche.imText(satz), satz).toHaveLength(1);
45 }
46 });
47
48 it('achtet nicht auf Groß- und Kleinschreibung', () => {
49 const suche = begriffssucheBauen(glossar(eintrag('Führen', ['führen'])));
50
51 expect(suche.imText('Das Führen ist erlaubnispflichtig.')).toHaveLength(1);
52 });
53
54 it('trifft nicht innerhalb eines längeren Wortes', () => {
55 /* Der wichtigste Fall: Sonst hinge an fast jeder Frage ein Begriff, der
56 dort gar nicht steht. */
57 const suche = begriffssucheBauen(glossar(eintrag('Waffe', ['Waffe', 'Waffen'])));
58
59 expect(suche.imText('Die Waffenbesitzkarte wird ausgestellt.')).toHaveLength(0);
60 expect(suche.imText('Eine Schusswaffe liegt vor.')).toHaveLength(0);
61 expect(suche.imText('Die Waffe ist geladen.')).toHaveLength(1);
62 });
63
64 it('behandelt Umlaute und ß als Wortzeichen', () => {
65 /* `\\b` würde in „Schießstätte" an jedem ß und ä eine Grenze sehen.
66 Dann fände „Stätte" einen Treffer, den es nicht gibt. */
67 const suche = begriffssucheBauen(
68 glossar(
69 eintrag('Schießstätte', ['Schießstätte', 'Schießstätten']),
70 eintrag('Stätte', ['Stätte']),
71 ),
72 );
73 const gefunden = suche.imText('Auf einer Schießstätte darf geschossen werden.');
74
75 expect(gefunden.map((e) => e.begriff)).toEqual(['Schießstätte']);
76 });
77
78 it('findet mehrteilige Begriffe', () => {
79 const suche = begriffssucheBauen(
80 glossar(eintrag('Wesentliche Teile', ['wesentliche Teile', 'wesentlichen Teile'])),
81 );
82
83 expect(suche.imText('Auch die wesentlichen Teile sind erfasst.')).toHaveLength(1);
84 });
85
86 /*
87 Deutsch schreibt Substantive groß, und daran hängt hier eine Bedeutung:
88 „Geschossen" ist der Dativ Plural von „Geschoss", „geschossen" das
89 Partizip von „schießen". Bis Fassung 0.24.1 suchte die Begriffssuche
90 ausnahmslos ohne Rücksicht auf Groß- und Kleinschreibung; am Katalog
91 nachgezählt bekamen zwölf Fragen mit dem Verb den Substantiveintrag
92 angeboten und sieben mit dem Substantiv den Verbeintrag.
93 */
94 it('unterscheidet groß notierte Varianten von klein geschriebenen Wörtern', () => {
95 const suche = begriffssucheBauen(
96 glossar(
97 eintrag('Geschoss', ['Geschoss', 'Geschosse', 'Geschossen']),
98 eintrag('Schießen', ['schießen', 'geschossen', 'schießt']),
99 ),
100 );
101
102 expect(
103 suche.imText('Darf unter Alkoholeinfluss geschossen werden?').map((e) => e.begriff),
104 ).toEqual(['Schießen']);
105 expect(
106 suche.imText('Der Gefährdungsbereich von Geschossen der Patrone.').map((e) => e.begriff),
107 ).toEqual(['Geschoss']);
108 });
109
110 it('findet eine klein notierte Variante auch am Satzanfang', () => {
111 /* Ein Verb steht dort groß, und die Variante ist klein notiert – hier
112 muss die Suche weiterhin ohne Rücksicht arbeiten. */
113 const suche = begriffssucheBauen(glossar(eintrag('Führen', ['führen', 'führt'])));
114
115 expect(suche.imText('Führt jemand eine Waffe, braucht er einen Schein.')).toHaveLength(1);
116 });
117
118 it('gibt die Begriffe in deutscher Sortierung zurück', () => {
119 // Ohne Locale stünde „Überlassen" hinter „Zuverlässigkeit".
120 const suche = begriffssucheBauen(
121 glossar(
122 eintrag('Zuverlässigkeit', ['Zuverlässigkeit']),
123 eintrag('Überlassen', ['überlässt']),
124 eintrag('Bedürfnis', ['Bedürfnis']),
125 ),
126 );
127 const text = 'Wer die Zuverlässigkeit besitzt und ein Bedürfnis hat, dem überlässt man sie.';
128
129 expect(suche.imText(text).map((e) => e.begriff)).toEqual([
130 'Bedürfnis',
131 'Überlassen',
132 'Zuverlässigkeit',
133 ]);
134 });
135
136 it('meldet jeden Begriff höchstens einmal', () => {
137 const suche = begriffssucheBauen(glossar(eintrag('Führen', ['führen', 'führt'])));
138
139 expect(suche.imText('Wer führt, muss führen dürfen.')).toHaveLength(1);
140 });
141
142 it('übergeht Einträge ohne Wortformen', () => {
143 /* Ein Ausdruck aus einer leeren Alternativenliste passt auf jede Stelle
144 und hängte das ganze Glossar an jede Frage. */
145 const suche = begriffssucheBauen(glossar(eintrag('Ohne Formen', [])));
146
147 expect(suche.imText('Beliebiger Text.')).toHaveLength(0);
148 expect(suche.eintraege).toHaveLength(1);
149 });
150
151 it('nimmt Sonderzeichen in einer Wortform wörtlich', () => {
152 // „F im Fünfeck" enthält keine Regex-Zeichen, aber § und Punkte kommen vor.
153 const suche = begriffssucheBauen(glossar(eintrag('Paragraf', ['§ 1', 'Abs.'])));
154
155 expect(suche.imText('Siehe § 1 des Gesetzes.')).toHaveLength(1);
156 expect(suche.imText('Siehe Abs. 2.')).toHaveLength(1);
157 expect(suche.imText('Siehe § 12.')).toHaveLength(0);
158 });
159
160 it('findet in einem leeren Text nichts', () => {
161 const suche = begriffssucheBauen(glossar(eintrag('Führen', ['führen'])));
162
163 expect(suche.imText('')).toHaveLength(0);
164 });
165 });