// @vitest-environment node /** * Die Textaufbereitung für die Sprachausgabe. * * Geprüft wird hier nur, was ohne Stimme prüfbar ist – also alles außer dem * Klang: Ersetzungen, Zerlegung, Stimmenwahl. Ob das Betriebssystem eine * deutsche Stimme hat, gehört nicht hierher; das entscheidet das Gerät des * Nutzers und darf keinen Testlauf zum Scheitern bringen. */ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import { BUCHSTABIERT, HAEPPCHEN_LAENGE, SPRECHWEISE, fuerSprachausgabe, inHaeppchen, stimmeWaehlen, } from '../src/renderer/src/lernen/sprachausgabe'; describe('fuerSprachausgabe', () => { it('spricht eine Fundstelle aus, statt sie zu buchstabieren', () => { expect(fuerSprachausgabe('§ 12 Abs. 4 Nr. 2 WaffG')).toBe( 'Paragraf 12 Absatz 4 Nummer 2 Waffengesetz', ); }); it('kennt den Plural der Paragrafen', () => { expect(fuerSprachausgabe('§§ 62, 63 UrhG')).toBe('Paragrafen 62, 63 Urheberrechtsgesetz'); }); it('löst „in Verbindung mit“ auf', () => { expect(fuerSprachausgabe('§ 2 Abs. 2 i. V. m. Anl. 2')).toBe( 'Paragraf 2 Absatz 2 in Verbindung mit Anlage 2', ); }); it('trifft das längere Kürzel zuerst', () => { expect(fuerSprachausgabe('BeschussV und BeschG')).toBe('Beschussverordnung und Beschussgesetz'); }); it('greift nicht in zusammengesetzte Wörter ein', () => { /* „WaffGÄndG“ ist ein eigenes Wort; würde daraus „WaffengesetzÄndG“, spräche die Stimme etwas aus, das es nicht gibt. */ expect(fuerSprachausgabe('WaffGÄndG')).toBe('WaffGÄndG'); expect(fuerSprachausgabe('Die WBK-Nummer')).toBe('Die WBK-Nummer'); }); it('lässt Kürzel stehen, die buchstabiert gehören', () => { for (const kuerzel of BUCHSTABIERT) { expect(fuerSprachausgabe(`Das ${kuerzel} entscheidet.`)).toContain(kuerzel); } }); it('macht aus Aufzählungsstrichen eine Sprechpause', () => { expect(fuerSprachausgabe('Voraussetzungen:\n- Sachkunde\n- Bedürfnis')).toBe( 'Voraussetzungen:\n. Sachkunde\n. Bedürfnis', ); }); it('lässt eine Datumsangabe unberührt', () => { expect(fuerSprachausgabe('bis zum 6. Juli 2017')).toBe('bis zum 6. Juli 2017'); }); /* Die eine Lückentextfrage des Katalogs (5.01) trägt ihre Lücken als Reihe von Unterstrichen – so druckt das amtliche PDF dort eine Schreiblinie. Ohne diese Ersetzung hörte man „Notwehr ist diejenige, die ist, um …“: einen Satz ohne Lücken, den niemand beantworten kann. */ it('macht aus einer Schreiblinie das Wort „Lücke“', () => { expect(fuerSprachausgabe('Notwehr ist diejenige _____, die _____ ist.')).toBe( 'Notwehr ist diejenige Lücke, die Lücke ist.', ); }); it('lässt einzelne Unterstriche in Wörtern in Ruhe', () => { expect(fuerSprachausgabe('Datei lernstand_alt speichern')).toBe( 'Datei lernstand_alt speichern', ); }); }); describe('inHaeppchen', () => { it('gibt für leeren Text nichts zurück', () => { expect(inHaeppchen(' ')).toEqual([]); }); it('lässt einen kurzen Text als ein Stück', () => { expect(inHaeppchen('Kurz und gut.')).toEqual(['Kurz und gut.']); }); it('bündelt kurze Sätze, statt bei jedem Punkt abzusetzen', () => { const stuecke = inHaeppchen('Eins. Zwei. Drei.'); expect(stuecke).toEqual(['Eins. Zwei. Drei.']); }); it('hält jedes Stück unter der Höchstlänge', () => { const satz = 'Der Erwerb einer Schusswaffe setzt ein Bedürfnis voraus. '; const stuecke = inHaeppchen(satz.repeat(40)); expect(stuecke.length).toBeGreaterThan(5); for (const stueck of stuecke) { expect(stueck.length).toBeLessThanOrEqual(HAEPPCHEN_LAENGE); } }); it('teilt auch einen einzigen überlangen Satz ohne Punkt', () => { const stuecke = inHaeppchen('Wort '.repeat(200).trim()); for (const stueck of stuecke) { expect(stueck.length).toBeLessThanOrEqual(HAEPPCHEN_LAENGE); } }); it('verliert kein Wort', () => { const text = 'Wer eine Schusswaffe erwerben will, braucht eine Erlaubnis. ' + 'Diese wird auf Antrag erteilt, wenn Sachkunde, Bedürfnis und ' + 'Zuverlässigkeit vorliegen; über den Antrag entscheidet die Behörde. ' + 'Ohne Erlaubnis ist der Erwerb strafbar.'; const woerter = (satz: string): string[] => satz.split(/\s+/u).filter((w) => w.length > 0); expect(woerter(inHaeppchen(text, 60).join(' '))).toEqual(woerter(text)); }); it('trennt nicht mitten in einer Datumsangabe', () => { const stuecke = inHaeppchen('Die Frist endete am 6. Juli 2017 endgültig.'); expect(stuecke).toEqual(['Die Frist endete am 6. Juli 2017 endgültig.']); }); }); describe('stimmeWaehlen', () => { const stimme = (lang: string, name: string, localService = true): Stimmenzeile => ({ lang, name, localService, }); interface Stimmenzeile { readonly lang: string; readonly name: string; readonly localService: boolean; } it('nimmt keine Stimme, wenn keine deutsche dabei ist', () => { expect(stimmeWaehlen([stimme('en-US', 'Zira'), stimme('fr-FR', 'Julie')])).toBeNull(); }); it('nimmt die deutsche Stimme', () => { const gewaehlt = stimmeWaehlen([stimme('en-US', 'Zira'), stimme('de-DE', 'Katja')]); expect(gewaehlt?.name).toBe('Katja'); }); it('bevorzugt die lokale Stimme vor der aus dem Netz', () => { const gewaehlt = stimmeWaehlen([ stimme('de-DE', 'Cloud-Stimme', false), stimme('de-DE', 'Hedda', true), ]); expect(gewaehlt?.name).toBe('Hedda'); }); it('nimmt auch de-AT und de-CH', () => { expect(stimmeWaehlen([stimme('de-AT', 'Michael')])?.name).toBe('Michael'); }); }); describe('Sprechformen und Glossar', () => { /* Zwei Listen derselben Kürzel driften auseinander – das ist in diesem Projekt schon dreimal passiert. Hier fällt es beim ersten neuen Glossareintrag auf und nicht erst, wenn eine Stimme etwas buchstabiert. */ it('kennt jedes Kürzel des Glossars', () => { const wurzel = fileURLToPath(new URL('..', import.meta.url)); const glossar = JSON.parse( readFileSync(join(wurzel, '..', 'content', 'glossar.json'), 'utf8'), ) as { eintraege: { begriff: string; art: string }[] }; const unbekannt = glossar.eintraege .filter((eintrag) => eintrag.art === 'abkuerzung') .map((eintrag) => eintrag.begriff) .filter((kuerzel) => !(kuerzel in SPRECHWEISE) && !BUCHSTABIERT.includes(kuerzel)); expect( unbekannt, 'Neue Kürzel gehören in SPRECHWEISE oder – bewusst – in BUCHSTABIERT.', ).toEqual([]); }); it('führt kein Kürzel in beiden Listen', () => { expect(BUCHSTABIERT.filter((kuerzel) => kuerzel in SPRECHWEISE)).toEqual([]); }); });