waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
/ app tests gesetzesuche.test.ts
| 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 | }); |