// @vitest-environment node /** * Blättern und Suchen in den mitgelieferten Gesetzen. * * Geprüft wird gegen die **echte** `content/normtexte.json`, nicht gegen eine * Attrappe: Die Aussagen dieser Ansicht sind Aussagen über den amtlichen * Wortlaut. Eine Suche, die an einer erfundenen Vorlage funktioniert und am * Waffengesetz nicht, wäre genau die Art Zusicherung, die niemandem hilft. */ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import { bezeichnungVon, gesetzeAuflisten, normenAuflisten, trefferJeGesetz, } from '../src/shared/gesetzesuche'; import { NORMTEXTE_LEER, type Normtexte } from '../src/shared/normtexte'; const wurzel = join(fileURLToPath(new URL('..', import.meta.url)), '..'); const normtexte = JSON.parse( readFileSync(join(wurzel, 'content/normtexte.json'), 'utf8'), ) as Normtexte; describe('Die Gesetze auflisten', () => { it('führt alle mitgelieferten Gesetze mit Stand und Quelle', () => { const gesetze = gesetzeAuflisten(normtexte); expect(gesetze.length).toBe(Object.keys(normtexte.gesetze).length); for (const gesetz of gesetze) { /* Ein Gesetzestext ohne Fassungsangabe ist eine Behauptung über heute, die morgen falsch sein kann. */ expect(gesetz.stand.length, gesetz.kuerzel).toBeGreaterThan(0); expect(gesetz.quelle.length, gesetz.kuerzel).toBeGreaterThan(0); expect(gesetz.normenGesamt, gesetz.kuerzel).toBeGreaterThan(0); } }); it('sortiert nach der Bezeichnung, die auch dasteht', () => { const bezeichnungen = gesetzeAuflisten(normtexte).map((g) => g.bezeichnung); expect(bezeichnungen).toEqual([...bezeichnungen].sort((a, b) => a.localeCompare(b, 'de'))); }); it('kommt ohne mitgelieferte Normtexte zurecht', () => { expect(gesetzeAuflisten(NORMTEXTE_LEER)).toEqual([]); expect(normenAuflisten(NORMTEXTE_LEER, 'WaffG')).toEqual([]); }); }); describe('Die Normen eines Gesetzes', () => { it('stehen in der Ordnung des Gesetzes und nicht alphabetisch', () => { /* Der Fehler, der ohne eigenen Schlüssel entstünde: „§ 10“ vor „§ 2“. Wer im Gesetz blättert, blättert nach Nummern. */ const normen = normenAuflisten(normtexte, 'WaffG'); const paragrafen = normen .filter((n) => !n.istAnlage) .map((n) => Number(/\d+/u.exec(n.norm)?.[0] ?? 0)); expect(paragrafen.length).toBeGreaterThan(10); expect(paragrafen).toEqual([...paragrafen].sort((a, b) => a - b)); }); it('stellt die Anlagen hinter die Paragrafen', () => { const normen = normenAuflisten(normtexte, 'WaffG'); const ersteAnlage = normen.findIndex((n) => n.istAnlage); const letzterParagraf = normen.map((n) => n.istAnlage).lastIndexOf(false); expect(ersteAnlage).toBeGreaterThan(-1); expect(ersteAnlage).toBeGreaterThan(letzterParagraf); }); it('führt zu jeder Norm eine Überschrift – bis auf sechs, die keine haben', () => { /* Nachgesehen im amtlichen XML (`content/gesetze/BJNR021410977.xml`): Sechs Paragrafen der Ersten Verordnung zum Sprengstoffgesetz tragen dort weder `titel` noch `gliederungstitel`. Das ist kein Fehler der Umsetzung, sondern der Wortlaut – und die Anzeige muss damit umgehen, statt eine Überschrift zu erfinden. Die Liste steht ausgeschrieben da, damit eine siebte auffällt: Die wäre entweder eine neue Fassung des Gesetzes oder ein Fehler in der Aufbereitung, und beides gehört gesehen. */ const ohne = gesetzeAuflisten(normtexte) .flatMap((gesetz) => normenAuflisten(normtexte, gesetz.kuerzel)) .filter((norm) => norm.titel.trim().length === 0) .map((norm) => `${norm.gesetz} ${norm.norm}`); expect(ohne.sort()).toEqual([ '1. SprengV § 1', '1. SprengV § 20', '1. SprengV § 22', '1. SprengV § 23', '1. SprengV § 32', '1. SprengV § 4', ]); }); }); describe('Suchen', () => { it('findet eine Norm über ihre Bezeichnung – mit und ohne Paragrafenzeichen', () => { /* Ein Suchfeld, das an einem Leerzeichen scheitert, wird nicht wieder benutzt. */ for (const eingabe of ['§ 13', '§13', '13']) { const treffer = normenAuflisten(normtexte, 'WaffG', eingabe); expect( treffer.some((n) => n.norm === '§ 13'), eingabe, ).toBe(true); } }); it('nennt, warum ein Treffer ein Treffer ist', () => { const ueberBezeichnung = normenAuflisten(normtexte, 'WaffG', '§ 13').find( (n) => n.norm === '§ 13', ); expect(ueberBezeichnung?.treffer).toBe('bezeichnung'); /* Ein Wort, das im Wortlaut steht und in keiner Überschrift dieses Gesetzes – der Treffer muss als Textfund ausgewiesen sein. */ const imText = normenAuflisten(normtexte, 'WaffG', 'Schießstand'); expect(imText.length).toBeGreaterThan(0); expect(imText.some((n) => n.treffer === 'text')).toBe(true); }); it('lässt ohne Suchbegriff jede Norm stehen und kennzeichnet keinen Treffer', () => { const alle = normenAuflisten(normtexte, 'AWaffV'); expect(alle.length).toBe(Object.keys(normtexte.gesetze['AWaffV']?.normen ?? {}).length); expect(alle.every((n) => n.treffer === null)).toBe(true); }); it('gibt bei einem Wort, das nirgends steht, nichts zurück', () => { expect(normenAuflisten(normtexte, 'WaffG', 'Zebrastreifenwärter')).toEqual([]); }); it('sagt für jedes Gesetz, wie viele Treffer darin stehen', () => { /* Sonst müsste der Suchende sieben Gesetze der Reihe nach durchklicken. */ const zahlen = trefferJeGesetz(normtexte, 'Munition'); expect(zahlen.size).toBe(Object.keys(normtexte.gesetze).length); expect([...zahlen.values()].reduce((a, b) => a + b, 0)).toBeGreaterThan(0); for (const [kuerzel, zahl] of zahlen) { expect(zahl, kuerzel).toBe(normenAuflisten(normtexte, kuerzel, 'Munition').length); } }); it('sucht auch in Anlagenblöcken', () => { /* Anlage 1 des WaffG ist der Begriffskatalog – wer dort nachschlägt, sucht ein Wort und keine Nummer. Läge die Suche nur auf Absätzen, bliebe der größte Textbestand des Pakets unauffindbar. */ const anlagen = normenAuflisten(normtexte, 'WaffG', 'Schusswaffen').filter((n) => n.istAnlage); expect(anlagen.length).toBeGreaterThan(0); }); it('schreibt jedes Kürzel aus', () => { expect(bezeichnungVon('WaffG')).toBe('Waffengesetz'); expect(bezeichnungVon('1. SprengV')).toBe('Erste Verordnung zum Sprengstoffgesetz'); }); });