waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
/ app tests sprachausgabe.test.ts
| 1 | // @vitest-environment node |
| 2 | /** |
| 3 | * Die Textaufbereitung für die Sprachausgabe. |
| 4 | * |
| 5 | * Geprüft wird hier nur, was ohne Stimme prüfbar ist – also alles außer dem |
| 6 | * Klang: Ersetzungen, Zerlegung, Stimmenwahl. Ob das Betriebssystem eine |
| 7 | * deutsche Stimme hat, gehört nicht hierher; das entscheidet das Gerät des |
| 8 | * Nutzers und darf keinen Testlauf zum Scheitern bringen. |
| 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 | BUCHSTABIERT, |
| 19 | HAEPPCHEN_LAENGE, |
| 20 | SPRECHWEISE, |
| 21 | fuerSprachausgabe, |
| 22 | inHaeppchen, |
| 23 | stimmeWaehlen, |
| 24 | } from '../src/renderer/src/lernen/sprachausgabe'; |
| 25 | |
| 26 | describe('fuerSprachausgabe', () => { |
| 27 | it('spricht eine Fundstelle aus, statt sie zu buchstabieren', () => { |
| 28 | expect(fuerSprachausgabe('§ 12 Abs. 4 Nr. 2 WaffG')).toBe( |
| 29 | 'Paragraf 12 Absatz 4 Nummer 2 Waffengesetz', |
| 30 | ); |
| 31 | }); |
| 32 | |
| 33 | it('kennt den Plural der Paragrafen', () => { |
| 34 | expect(fuerSprachausgabe('§§ 62, 63 UrhG')).toBe('Paragrafen 62, 63 Urheberrechtsgesetz'); |
| 35 | }); |
| 36 | |
| 37 | it('löst „in Verbindung mit“ auf', () => { |
| 38 | expect(fuerSprachausgabe('§ 2 Abs. 2 i. V. m. Anl. 2')).toBe( |
| 39 | 'Paragraf 2 Absatz 2 in Verbindung mit Anlage 2', |
| 40 | ); |
| 41 | }); |
| 42 | |
| 43 | it('trifft das längere Kürzel zuerst', () => { |
| 44 | expect(fuerSprachausgabe('BeschussV und BeschG')).toBe('Beschussverordnung und Beschussgesetz'); |
| 45 | }); |
| 46 | |
| 47 | it('greift nicht in zusammengesetzte Wörter ein', () => { |
| 48 | /* „WaffGÄndG“ ist ein eigenes Wort; würde daraus „WaffengesetzÄndG“, |
| 49 | spräche die Stimme etwas aus, das es nicht gibt. */ |
| 50 | expect(fuerSprachausgabe('WaffGÄndG')).toBe('WaffGÄndG'); |
| 51 | expect(fuerSprachausgabe('Die WBK-Nummer')).toBe('Die WBK-Nummer'); |
| 52 | }); |
| 53 | |
| 54 | it('lässt Kürzel stehen, die buchstabiert gehören', () => { |
| 55 | for (const kuerzel of BUCHSTABIERT) { |
| 56 | expect(fuerSprachausgabe(`Das ${kuerzel} entscheidet.`)).toContain(kuerzel); |
| 57 | } |
| 58 | }); |
| 59 | |
| 60 | it('macht aus Aufzählungsstrichen eine Sprechpause', () => { |
| 61 | expect(fuerSprachausgabe('Voraussetzungen:\n- Sachkunde\n- Bedürfnis')).toBe( |
| 62 | 'Voraussetzungen:\n. Sachkunde\n. Bedürfnis', |
| 63 | ); |
| 64 | }); |
| 65 | |
| 66 | it('lässt eine Datumsangabe unberührt', () => { |
| 67 | expect(fuerSprachausgabe('bis zum 6. Juli 2017')).toBe('bis zum 6. Juli 2017'); |
| 68 | }); |
| 69 | |
| 70 | /* |
| 71 | Die eine Lückentextfrage des Katalogs (5.01) trägt ihre Lücken als Reihe |
| 72 | von Unterstrichen – so druckt das amtliche PDF dort eine Schreiblinie. |
| 73 | Ohne diese Ersetzung hörte man „Notwehr ist diejenige, die ist, um …“: |
| 74 | einen Satz ohne Lücken, den niemand beantworten kann. |
| 75 | */ |
| 76 | it('macht aus einer Schreiblinie das Wort „Lücke“', () => { |
| 77 | expect(fuerSprachausgabe('Notwehr ist diejenige _____, die _____ ist.')).toBe( |
| 78 | 'Notwehr ist diejenige Lücke, die Lücke ist.', |
| 79 | ); |
| 80 | }); |
| 81 | |
| 82 | it('lässt einzelne Unterstriche in Wörtern in Ruhe', () => { |
| 83 | expect(fuerSprachausgabe('Datei lernstand_alt speichern')).toBe( |
| 84 | 'Datei lernstand_alt speichern', |
| 85 | ); |
| 86 | }); |
| 87 | }); |
| 88 | |
| 89 | describe('inHaeppchen', () => { |
| 90 | it('gibt für leeren Text nichts zurück', () => { |
| 91 | expect(inHaeppchen(' ')).toEqual([]); |
| 92 | }); |
| 93 | |
| 94 | it('lässt einen kurzen Text als ein Stück', () => { |
| 95 | expect(inHaeppchen('Kurz und gut.')).toEqual(['Kurz und gut.']); |
| 96 | }); |
| 97 | |
| 98 | it('bündelt kurze Sätze, statt bei jedem Punkt abzusetzen', () => { |
| 99 | const stuecke = inHaeppchen('Eins. Zwei. Drei.'); |
| 100 | |
| 101 | expect(stuecke).toEqual(['Eins. Zwei. Drei.']); |
| 102 | }); |
| 103 | |
| 104 | it('hält jedes Stück unter der Höchstlänge', () => { |
| 105 | const satz = 'Der Erwerb einer Schusswaffe setzt ein Bedürfnis voraus. '; |
| 106 | const stuecke = inHaeppchen(satz.repeat(40)); |
| 107 | |
| 108 | expect(stuecke.length).toBeGreaterThan(5); |
| 109 | for (const stueck of stuecke) { |
| 110 | expect(stueck.length).toBeLessThanOrEqual(HAEPPCHEN_LAENGE); |
| 111 | } |
| 112 | }); |
| 113 | |
| 114 | it('teilt auch einen einzigen überlangen Satz ohne Punkt', () => { |
| 115 | const stuecke = inHaeppchen('Wort '.repeat(200).trim()); |
| 116 | |
| 117 | for (const stueck of stuecke) { |
| 118 | expect(stueck.length).toBeLessThanOrEqual(HAEPPCHEN_LAENGE); |
| 119 | } |
| 120 | }); |
| 121 | |
| 122 | it('verliert kein Wort', () => { |
| 123 | const text = |
| 124 | 'Wer eine Schusswaffe erwerben will, braucht eine Erlaubnis. ' + |
| 125 | 'Diese wird auf Antrag erteilt, wenn Sachkunde, Bedürfnis und ' + |
| 126 | 'Zuverlässigkeit vorliegen; über den Antrag entscheidet die Behörde. ' + |
| 127 | 'Ohne Erlaubnis ist der Erwerb strafbar.'; |
| 128 | const woerter = (satz: string): string[] => satz.split(/\s+/u).filter((w) => w.length > 0); |
| 129 | |
| 130 | expect(woerter(inHaeppchen(text, 60).join(' '))).toEqual(woerter(text)); |
| 131 | }); |
| 132 | |
| 133 | it('trennt nicht mitten in einer Datumsangabe', () => { |
| 134 | const stuecke = inHaeppchen('Die Frist endete am 6. Juli 2017 endgültig.'); |
| 135 | |
| 136 | expect(stuecke).toEqual(['Die Frist endete am 6. Juli 2017 endgültig.']); |
| 137 | }); |
| 138 | }); |
| 139 | |
| 140 | describe('stimmeWaehlen', () => { |
| 141 | const stimme = (lang: string, name: string, localService = true): Stimmenzeile => ({ |
| 142 | lang, |
| 143 | name, |
| 144 | localService, |
| 145 | }); |
| 146 | interface Stimmenzeile { |
| 147 | readonly lang: string; |
| 148 | readonly name: string; |
| 149 | readonly localService: boolean; |
| 150 | } |
| 151 | |
| 152 | it('nimmt keine Stimme, wenn keine deutsche dabei ist', () => { |
| 153 | expect(stimmeWaehlen([stimme('en-US', 'Zira'), stimme('fr-FR', 'Julie')])).toBeNull(); |
| 154 | }); |
| 155 | |
| 156 | it('nimmt die deutsche Stimme', () => { |
| 157 | const gewaehlt = stimmeWaehlen([stimme('en-US', 'Zira'), stimme('de-DE', 'Katja')]); |
| 158 | |
| 159 | expect(gewaehlt?.name).toBe('Katja'); |
| 160 | }); |
| 161 | |
| 162 | it('bevorzugt die lokale Stimme vor der aus dem Netz', () => { |
| 163 | const gewaehlt = stimmeWaehlen([ |
| 164 | stimme('de-DE', 'Cloud-Stimme', false), |
| 165 | stimme('de-DE', 'Hedda', true), |
| 166 | ]); |
| 167 | |
| 168 | expect(gewaehlt?.name).toBe('Hedda'); |
| 169 | }); |
| 170 | |
| 171 | it('nimmt auch de-AT und de-CH', () => { |
| 172 | expect(stimmeWaehlen([stimme('de-AT', 'Michael')])?.name).toBe('Michael'); |
| 173 | }); |
| 174 | }); |
| 175 | |
| 176 | describe('Sprechformen und Glossar', () => { |
| 177 | /* |
| 178 | Zwei Listen derselben Kürzel driften auseinander – das ist in diesem |
| 179 | Projekt schon dreimal passiert. Hier fällt es beim ersten neuen |
| 180 | Glossareintrag auf und nicht erst, wenn eine Stimme etwas buchstabiert. |
| 181 | */ |
| 182 | it('kennt jedes Kürzel des Glossars', () => { |
| 183 | const wurzel = fileURLToPath(new URL('..', import.meta.url)); |
| 184 | const glossar = JSON.parse( |
| 185 | readFileSync(join(wurzel, '..', 'content', 'glossar.json'), 'utf8'), |
| 186 | ) as { eintraege: { begriff: string; art: string }[] }; |
| 187 | |
| 188 | const unbekannt = glossar.eintraege |
| 189 | .filter((eintrag) => eintrag.art === 'abkuerzung') |
| 190 | .map((eintrag) => eintrag.begriff) |
| 191 | .filter((kuerzel) => !(kuerzel in SPRECHWEISE) && !BUCHSTABIERT.includes(kuerzel)); |
| 192 | |
| 193 | expect( |
| 194 | unbekannt, |
| 195 | 'Neue Kürzel gehören in SPRECHWEISE oder – bewusst – in BUCHSTABIERT.', |
| 196 | ).toEqual([]); |
| 197 | }); |
| 198 | |
| 199 | it('führt kein Kürzel in beiden Listen', () => { |
| 200 | expect(BUCHSTABIERT.filter((kuerzel) => kuerzel in SPRECHWEISE)).toEqual([]); |
| 201 | }); |
| 202 | }); |