// @vitest-environment node /** * Begriffserkennung im Fragetext. * * Der heikle Teil ist die Wortgrenze. `\b` kennt nur ASCII-Wortzeichen – * in „Schießstätte" gälte damit jedes ß und ä als Grenze. Und „Waffe" darf * nicht in „Waffenbesitzkarte" treffen, sonst hängt an jeder zweiten Frage * ein Begriff, der dort gar nicht steht. */ import { describe, expect, it } from 'vitest'; import { begriffssucheBauen, type Glossar, type Glossareintrag } from '../src/shared/glossar'; function eintrag(begriff: string, varianten: string[]): Glossareintrag { return { begriff, art: 'begriff', kurz: `Bedeutung von ${begriff} in einem Satz, lang genug zum Anzeigen.`, varianten, fundstellen: [], ohneFundstelleGrund: 'Prüfeintrag ohne gesetzliche Grundlage.', }; } function glossar(...eintraege: Glossareintrag[]): Glossar { return { meta: { version: 1, stand: '2026-08-21', gesetzesstand: {}, hinweis: 'Prüfstand' }, eintraege, }; } describe('Begriffe im Text finden', () => { it('findet die Grundform', () => { const suche = begriffssucheBauen(glossar(eintrag('Führen', ['führen', 'führt', 'geführt']))); expect(suche.imText('Wer eine Waffe führt, braucht einen Waffenschein.')).toHaveLength(1); }); it('findet alle angegebenen Wortformen', () => { const suche = begriffssucheBauen(glossar(eintrag('Führen', ['führen', 'führt', 'geführt']))); for (const satz of ['Darf man führen?', 'Er führt sie.', 'Sie wurde geführt.']) { expect(suche.imText(satz), satz).toHaveLength(1); } }); it('achtet nicht auf Groß- und Kleinschreibung', () => { const suche = begriffssucheBauen(glossar(eintrag('Führen', ['führen']))); expect(suche.imText('Das Führen ist erlaubnispflichtig.')).toHaveLength(1); }); it('trifft nicht innerhalb eines längeren Wortes', () => { /* Der wichtigste Fall: Sonst hinge an fast jeder Frage ein Begriff, der dort gar nicht steht. */ const suche = begriffssucheBauen(glossar(eintrag('Waffe', ['Waffe', 'Waffen']))); expect(suche.imText('Die Waffenbesitzkarte wird ausgestellt.')).toHaveLength(0); expect(suche.imText('Eine Schusswaffe liegt vor.')).toHaveLength(0); expect(suche.imText('Die Waffe ist geladen.')).toHaveLength(1); }); it('behandelt Umlaute und ß als Wortzeichen', () => { /* `\\b` würde in „Schießstätte" an jedem ß und ä eine Grenze sehen. Dann fände „Stätte" einen Treffer, den es nicht gibt. */ const suche = begriffssucheBauen( glossar( eintrag('Schießstätte', ['Schießstätte', 'Schießstätten']), eintrag('Stätte', ['Stätte']), ), ); const gefunden = suche.imText('Auf einer Schießstätte darf geschossen werden.'); expect(gefunden.map((e) => e.begriff)).toEqual(['Schießstätte']); }); it('findet mehrteilige Begriffe', () => { const suche = begriffssucheBauen( glossar(eintrag('Wesentliche Teile', ['wesentliche Teile', 'wesentlichen Teile'])), ); expect(suche.imText('Auch die wesentlichen Teile sind erfasst.')).toHaveLength(1); }); /* Deutsch schreibt Substantive groß, und daran hängt hier eine Bedeutung: „Geschossen" ist der Dativ Plural von „Geschoss", „geschossen" das Partizip von „schießen". Bis Fassung 0.24.1 suchte die Begriffssuche ausnahmslos ohne Rücksicht auf Groß- und Kleinschreibung; am Katalog nachgezählt bekamen zwölf Fragen mit dem Verb den Substantiveintrag angeboten und sieben mit dem Substantiv den Verbeintrag. */ it('unterscheidet groß notierte Varianten von klein geschriebenen Wörtern', () => { const suche = begriffssucheBauen( glossar( eintrag('Geschoss', ['Geschoss', 'Geschosse', 'Geschossen']), eintrag('Schießen', ['schießen', 'geschossen', 'schießt']), ), ); expect( suche.imText('Darf unter Alkoholeinfluss geschossen werden?').map((e) => e.begriff), ).toEqual(['Schießen']); expect( suche.imText('Der Gefährdungsbereich von Geschossen der Patrone.').map((e) => e.begriff), ).toEqual(['Geschoss']); }); it('findet eine klein notierte Variante auch am Satzanfang', () => { /* Ein Verb steht dort groß, und die Variante ist klein notiert – hier muss die Suche weiterhin ohne Rücksicht arbeiten. */ const suche = begriffssucheBauen(glossar(eintrag('Führen', ['führen', 'führt']))); expect(suche.imText('Führt jemand eine Waffe, braucht er einen Schein.')).toHaveLength(1); }); it('gibt die Begriffe in deutscher Sortierung zurück', () => { // Ohne Locale stünde „Überlassen" hinter „Zuverlässigkeit". const suche = begriffssucheBauen( glossar( eintrag('Zuverlässigkeit', ['Zuverlässigkeit']), eintrag('Überlassen', ['überlässt']), eintrag('Bedürfnis', ['Bedürfnis']), ), ); const text = 'Wer die Zuverlässigkeit besitzt und ein Bedürfnis hat, dem überlässt man sie.'; expect(suche.imText(text).map((e) => e.begriff)).toEqual([ 'Bedürfnis', 'Überlassen', 'Zuverlässigkeit', ]); }); it('meldet jeden Begriff höchstens einmal', () => { const suche = begriffssucheBauen(glossar(eintrag('Führen', ['führen', 'führt']))); expect(suche.imText('Wer führt, muss führen dürfen.')).toHaveLength(1); }); it('übergeht Einträge ohne Wortformen', () => { /* Ein Ausdruck aus einer leeren Alternativenliste passt auf jede Stelle und hängte das ganze Glossar an jede Frage. */ const suche = begriffssucheBauen(glossar(eintrag('Ohne Formen', []))); expect(suche.imText('Beliebiger Text.')).toHaveLength(0); expect(suche.eintraege).toHaveLength(1); }); it('nimmt Sonderzeichen in einer Wortform wörtlich', () => { // „F im Fünfeck" enthält keine Regex-Zeichen, aber § und Punkte kommen vor. const suche = begriffssucheBauen(glossar(eintrag('Paragraf', ['§ 1', 'Abs.']))); expect(suche.imText('Siehe § 1 des Gesetzes.')).toHaveLength(1); expect(suche.imText('Siehe Abs. 2.')).toHaveLength(1); expect(suche.imText('Siehe § 12.')).toHaveLength(0); }); it('findet in einem leeren Text nichts', () => { const suche = begriffssucheBauen(glossar(eintrag('Führen', ['führen']))); expect(suche.imText('')).toHaveLength(0); }); });