waffensachkunde

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

/ app tests gesetzesuche.test.ts

6,5 KB Rohdatei
app/tests/gesetzesuche.test.ts — 168 Zeilen
1 // @vitest-environment node
2 /**
3 * Blättern und Suchen in den mitgelieferten Gesetzen.
4 *
5 * Geprüft wird gegen die **echte** `content/normtexte.json`, nicht gegen eine
6 * Attrappe: Die Aussagen dieser Ansicht sind Aussagen über den amtlichen
7 * Wortlaut. Eine Suche, die an einer erfundenen Vorlage funktioniert und am
8 * Waffengesetz nicht, wäre genau die Art Zusicherung, die niemandem hilft.
9 */
10
11 import { readFileSync } from 'node:fs';
12 import { join } from 'node:path';
13 import { fileURLToPath } from 'node:url';
14
15 import { describe, expect, it } from 'vitest';
16
17 import {
18 bezeichnungVon,
19 gesetzeAuflisten,
20 normenAuflisten,
21 trefferJeGesetz,
22 } from '../src/shared/gesetzesuche';
23 import { NORMTEXTE_LEER, type Normtexte } from '../src/shared/normtexte';
24
25 const wurzel = join(fileURLToPath(new URL('..', import.meta.url)), '..');
26 const normtexte = JSON.parse(
27 readFileSync(join(wurzel, 'content/normtexte.json'), 'utf8'),
28 ) as Normtexte;
29
30 describe('Die Gesetze auflisten', () => {
31 it('führt alle mitgelieferten Gesetze mit Stand und Quelle', () => {
32 const gesetze = gesetzeAuflisten(normtexte);
33
34 expect(gesetze.length).toBe(Object.keys(normtexte.gesetze).length);
35 for (const gesetz of gesetze) {
36 /* Ein Gesetzestext ohne Fassungsangabe ist eine Behauptung über heute,
37 die morgen falsch sein kann. */
38 expect(gesetz.stand.length, gesetz.kuerzel).toBeGreaterThan(0);
39 expect(gesetz.quelle.length, gesetz.kuerzel).toBeGreaterThan(0);
40 expect(gesetz.normenGesamt, gesetz.kuerzel).toBeGreaterThan(0);
41 }
42 });
43
44 it('sortiert nach der Bezeichnung, die auch dasteht', () => {
45 const bezeichnungen = gesetzeAuflisten(normtexte).map((g) => g.bezeichnung);
46 expect(bezeichnungen).toEqual([...bezeichnungen].sort((a, b) => a.localeCompare(b, 'de')));
47 });
48
49 it('kommt ohne mitgelieferte Normtexte zurecht', () => {
50 expect(gesetzeAuflisten(NORMTEXTE_LEER)).toEqual([]);
51 expect(normenAuflisten(NORMTEXTE_LEER, 'WaffG')).toEqual([]);
52 });
53 });
54
55 describe('Die Normen eines Gesetzes', () => {
56 it('stehen in der Ordnung des Gesetzes und nicht alphabetisch', () => {
57 /*
58 Der Fehler, der ohne eigenen Schlüssel entstünde: „§ 10“ vor „§ 2“.
59 Wer im Gesetz blättert, blättert nach Nummern.
60 */
61 const normen = normenAuflisten(normtexte, 'WaffG');
62 const paragrafen = normen
63 .filter((n) => !n.istAnlage)
64 .map((n) => Number(/\d+/u.exec(n.norm)?.[0] ?? 0));
65
66 expect(paragrafen.length).toBeGreaterThan(10);
67 expect(paragrafen).toEqual([...paragrafen].sort((a, b) => a - b));
68 });
69
70 it('stellt die Anlagen hinter die Paragrafen', () => {
71 const normen = normenAuflisten(normtexte, 'WaffG');
72 const ersteAnlage = normen.findIndex((n) => n.istAnlage);
73 const letzterParagraf = normen.map((n) => n.istAnlage).lastIndexOf(false);
74
75 expect(ersteAnlage).toBeGreaterThan(-1);
76 expect(ersteAnlage).toBeGreaterThan(letzterParagraf);
77 });
78
79 it('führt zu jeder Norm eine Überschrift – bis auf sechs, die keine haben', () => {
80 /*
81 Nachgesehen im amtlichen XML (`content/gesetze/BJNR021410977.xml`):
82 Sechs Paragrafen der Ersten Verordnung zum Sprengstoffgesetz tragen
83 dort weder `titel` noch `gliederungstitel`. Das ist kein Fehler der
84 Umsetzung, sondern der Wortlaut – und die Anzeige muss damit umgehen,
85 statt eine Überschrift zu erfinden.
86
87 Die Liste steht ausgeschrieben da, damit eine siebte auffällt: Die
88 wäre entweder eine neue Fassung des Gesetzes oder ein Fehler in der
89 Aufbereitung, und beides gehört gesehen.
90 */
91 const ohne = gesetzeAuflisten(normtexte)
92 .flatMap((gesetz) => normenAuflisten(normtexte, gesetz.kuerzel))
93 .filter((norm) => norm.titel.trim().length === 0)
94 .map((norm) => `${norm.gesetz} ${norm.norm}`);
95
96 expect(ohne.sort()).toEqual([
97 '1. SprengV § 1',
98 '1. SprengV § 20',
99 '1. SprengV § 22',
100 '1. SprengV § 23',
101 '1. SprengV § 32',
102 '1. SprengV § 4',
103 ]);
104 });
105 });
106
107 describe('Suchen', () => {
108 it('findet eine Norm über ihre Bezeichnung – mit und ohne Paragrafenzeichen', () => {
109 /* Ein Suchfeld, das an einem Leerzeichen scheitert, wird nicht wieder
110 benutzt. */
111 for (const eingabe of ['§ 13', '§13', '13']) {
112 const treffer = normenAuflisten(normtexte, 'WaffG', eingabe);
113 expect(
114 treffer.some((n) => n.norm === '§ 13'),
115 eingabe,
116 ).toBe(true);
117 }
118 });
119
120 it('nennt, warum ein Treffer ein Treffer ist', () => {
121 const ueberBezeichnung = normenAuflisten(normtexte, 'WaffG', '§ 13').find(
122 (n) => n.norm === '§ 13',
123 );
124 expect(ueberBezeichnung?.treffer).toBe('bezeichnung');
125
126 /* Ein Wort, das im Wortlaut steht und in keiner Überschrift dieses
127 Gesetzes – der Treffer muss als Textfund ausgewiesen sein. */
128 const imText = normenAuflisten(normtexte, 'WaffG', 'Schießstand');
129 expect(imText.length).toBeGreaterThan(0);
130 expect(imText.some((n) => n.treffer === 'text')).toBe(true);
131 });
132
133 it('lässt ohne Suchbegriff jede Norm stehen und kennzeichnet keinen Treffer', () => {
134 const alle = normenAuflisten(normtexte, 'AWaffV');
135 expect(alle.length).toBe(Object.keys(normtexte.gesetze['AWaffV']?.normen ?? {}).length);
136 expect(alle.every((n) => n.treffer === null)).toBe(true);
137 });
138
139 it('gibt bei einem Wort, das nirgends steht, nichts zurück', () => {
140 expect(normenAuflisten(normtexte, 'WaffG', 'Zebrastreifenwärter')).toEqual([]);
141 });
142
143 it('sagt für jedes Gesetz, wie viele Treffer darin stehen', () => {
144 /* Sonst müsste der Suchende sieben Gesetze der Reihe nach durchklicken. */
145 const zahlen = trefferJeGesetz(normtexte, 'Munition');
146
147 expect(zahlen.size).toBe(Object.keys(normtexte.gesetze).length);
148 expect([...zahlen.values()].reduce((a, b) => a + b, 0)).toBeGreaterThan(0);
149 for (const [kuerzel, zahl] of zahlen) {
150 expect(zahl, kuerzel).toBe(normenAuflisten(normtexte, kuerzel, 'Munition').length);
151 }
152 });
153
154 it('sucht auch in Anlagenblöcken', () => {
155 /*
156 Anlage 1 des WaffG ist der Begriffskatalog – wer dort nachschlägt,
157 sucht ein Wort und keine Nummer. Läge die Suche nur auf Absätzen,
158 bliebe der größte Textbestand des Pakets unauffindbar.
159 */
160 const anlagen = normenAuflisten(normtexte, 'WaffG', 'Schusswaffen').filter((n) => n.istAnlage);
161 expect(anlagen.length).toBeGreaterThan(0);
162 });
163
164 it('schreibt jedes Kürzel aus', () => {
165 expect(bezeichnungVon('WaffG')).toBe('Waffengesetz');
166 expect(bezeichnungVon('1. SprengV')).toBe('Erste Verordnung zum Sprengstoffgesetz');
167 });
168 });