waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ app tests sprachausgabe.test.ts

6,8 KB Rohdatei
app/tests/sprachausgabe.test.ts — 202 Zeilen
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 });