lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests ui datenschutzAnsicht.test.ts

22,8 KB Rohdatei
tests/ui/datenschutzAnsicht.test.ts — 559 Zeilen
1 import { readFileSync } from 'node:fs';
2 import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
3 import { ladeSitzung, saveSession } from '@/services/storage';
4 import type { MenuCommand } from '@shared/ipc';
5 import { luftbildDialog } from '@/ui/lageplan/luftbild';
6
7 /**
8 * Datenschutz an der Oberflaeche - seit 5.43.0.
9 *
10 * Die Datenschutzhinweise gibt es seit 5.43.0 als docs/datenschutz.md; sie
11 * gehoeren in das Programm selbst und nicht nur auf eine Internetseite (Kopf
12 * von tests/domain/datenschutz.test.ts). Hier wird bewacht, dass und wie der
13 * Text im Programm ankommt, und die drei Stellen, an denen die Oberflaeche
14 * selbst etwas zum Thema sagt:
15 *
16 * 1. das "Über"-Fenster mit einer vierten Ansicht "Datenschutz", gelesen aus
17 * derselben Datei;
18 * 2. der Abschnitt "Fremdbestandteile" im selben Fenster, der je nach
19 * Installationsart nur formal stimmte;
20 * 3. der Luftbild-Dialog, der sagte, DASS geholt wird, aber nicht, was dabei
21 * den Rechner verlaesst;
22 * 4. die Kurzhilfe, die den Anwender allein auf einen Pfad schickte, den der
23 * Explorer je nach Installationsart nicht unbedingt zeigt.
24 *
25 * Geprueft wird am echten Einstiegspunkt wie in
26 * tests/ui/ueberFenster.test.ts: `src/main.ts` startet die ganze
27 * Anwendung, der Menuebefehl kommt ueber die ersetzte Desktop-Bruecke.
28 */
29
30 vi.mock('@/services/storage', async (importOriginal) => {
31 const echt = await importOriginal<typeof import('@/services/storage')>();
32 return {
33 ...echt,
34 ladeSitzung: vi.fn(),
35 saveSession: vi.fn(),
36 addRecoveryPoint: vi.fn(),
37 };
38 });
39
40 const DATENSCHUTZ = readFileSync('docs/datenschutz.md', 'utf8');
41 const MENUE = readFileSync('electron/menu.ts', 'utf8');
42 const PROTOKOLLPFAD = 'C:/Protokoll/lsa-planer.log';
43
44 const TITEL = 'Über LSA-Planer Professional';
45
46 async function warte(ms = 0): Promise<void> {
47 await new Promise((fertig) => setTimeout(fertig, ms));
48 }
49
50 function alleFenster(): HTMLElement[] {
51 return [...document.querySelectorAll<HTMLElement>('.dialog-hintergrund')];
52 }
53
54 function fenster(titel: string): HTMLElement | null {
55 return alleFenster().find((k) => k.querySelector('h2')?.textContent?.trim() === titel) ?? null;
56 }
57
58 function titelDerFenster(): string[] {
59 return alleFenster().map((k) => k.querySelector('h2')?.textContent?.trim() ?? '');
60 }
61
62 function knopf(kasten: HTMLElement, beschriftung: string): HTMLButtonElement {
63 const gefunden = [...kasten.querySelectorAll('button')].find(
64 (b) => b.textContent?.trim() === beschriftung,
65 );
66 if (!(gefunden instanceof HTMLButtonElement)) {
67 throw new Error(`Schaltfläche "${beschriftung}" fehlt im Fenster.`);
68 }
69 return gefunden;
70 }
71
72 /** Beschriftungen der Schaltflaechenleiste unten - ohne die im Inhalt. */
73 function leiste(kasten: HTMLElement): string[] {
74 return [...kasten.querySelectorAll('.dialog-aktionen button')].map(
75 (b) => b.textContent?.trim() ?? '',
76 );
77 }
78
79 function inhalt(kasten: HTMLElement): HTMLElement {
80 const bereich = kasten.querySelector<HTMLElement>('.dialog-inhalt');
81 if (bereich === null) throw new Error('Das Fenster hat keinen Inhaltsbereich.');
82 return bereich;
83 }
84
85 function einzeilig(text: string): string {
86 return text.replace(/\s+/g, ' ');
87 }
88
89 function schliesseAlle(): void {
90 for (const kasten of alleFenster()) {
91 kasten.querySelector<HTMLButtonElement>('.dialog-schliessen')?.click();
92 }
93 }
94
95 /**
96 * Die Datei, unabhaengig vom Leser gegliedert: Bloecke an Leerzeilen. Das geht
97 * nur, weil die Datei zwischen je zwei Bloecken eine Leerzeile fuehrt - und
98 * genau das sieht die Zerlegung je Block mit nach, damit der Abgleich nicht
99 * still an einer Annahme haengt.
100 */
101 interface Erwartung {
102 readonly titel: string;
103 /** Tag und Text jedes Knotens unter der Ueberschrift, `li` einzeln. */
104 readonly folge: readonly (readonly [string, string])[];
105 }
106
107 function erwartung(datei: string): Erwartung {
108 let titel = '';
109 const folge: (readonly [string, string])[] = [];
110 for (const block of datei.split(/\n[ \t]*\n/)) {
111 const zeilen = block
112 .split('\n')
113 .map((z) => z.trim())
114 .filter((z) => z !== '');
115 if (zeilen.length === 0) continue;
116 const erste = zeilen[0]!;
117 const punkte = zeilen.filter((z) => z.startsWith('- '));
118 const kopf = zeilen.filter((z) => z.startsWith('#'));
119 // Ein Block ist von einer Art - sonst traegt die Zerlegung an Leerzeilen nicht.
120 if (erste.startsWith('#'))
121 expect(zeilen, 'Überschrift mit Text im selben Block').toHaveLength(1);
122 if (erste.startsWith('- '))
123 expect(punkte, 'Aufzählung mit Text im selben Block').toEqual(zeilen);
124 else expect(punkte, 'Absatz mit Aufzählungspunkt im selben Block').toEqual([]);
125 expect(kopf.length, 'mehr als eine Überschrift im selben Block').toBeLessThanOrEqual(1);
126
127 if (erste.startsWith('# ')) titel = erste.slice(2);
128 else if (erste.startsWith('## ')) folge.push(['H3', erste.slice(3)]);
129 else if (erste.startsWith('- ')) for (const z of zeilen) folge.push(['LI', z.slice(2)]);
130 else folge.push(['P', zeilen.join(' ')]);
131 }
132 return { titel, folge };
133 }
134
135 /**
136 * Die Faelle, in denen src/ui/datenschutzText.ts anders gliedert als ein Leser
137 * nach CommonMark - die Liste im Kopf dort, hier als Wache ueber die Datei. Was
138 * die Formatwache in tests/domain/datenschutz.test.ts schon verbietet, steht
139 * nicht noch einmal hier.
140 */
141 const FALL = {
142 zeichenzeile: 'Zeile nur aus „-“, „=“, „*“ oder „_“',
143 zitat: 'Zitat',
144 tilden: 'Codeblock mit Tilden',
145 verweisdefinition: 'Verweisdefinition',
146 tabulator: 'Tabulator',
147 leerzeileZwischenPunkten: 'Leerzeile zwischen zwei Aufzählungspunkten',
148 eingerueckt: 'eingerückte Zeile nach Leerzeile hinter einem Aufzählungspunkt',
149 } as const;
150
151 const ZEILENMUSTER: readonly (readonly [RegExp, string])[] = [
152 [/^\s*[-=*_][-=*_\s]*$/, FALL.zeichenzeile],
153 [/^\s*>/, FALL.zitat],
154 [/^\s*~~~/, FALL.tilden],
155 [/^\s*\[[^\]]*\]:/, FALL.verweisdefinition],
156 [/\t/, FALL.tabulator],
157 ];
158
159 function gliederungsabweichungen(datei: string): string[] {
160 const funde: string[] = [];
161 /** Die letzte nicht leere Zeile gehoert zu einem Aufzaehlungspunkt. */
162 let imPunkt = false;
163 let leerzeileDavor = false;
164 datei.split('\n').forEach((roh, index) => {
165 const zeile = roh.replace(/\r$/, '');
166 const nummer = index + 1;
167 if (zeile.trim() === '') {
168 leerzeileDavor = true;
169 return;
170 }
171 for (const [muster, fall] of ZEILENMUSTER) {
172 if (muster.test(zeile)) funde.push(`Zeile ${nummer}: ${fall}`);
173 }
174 const punkt = zeile.startsWith('- ');
175 if (imPunkt && leerzeileDavor && punkt) {
176 funde.push(`Zeile ${nummer}: ${FALL.leerzeileZwischenPunkten}`);
177 }
178 // Ab zwei Leerzeichen: so weit ruecken die Zeilen eines Punkts "- " ein.
179 if (imPunkt && leerzeileDavor && /^ {2,}\S/.test(zeile)) {
180 funde.push(`Zeile ${nummer}: ${FALL.eingerueckt}`);
181 }
182 if (punkt) imPunkt = true;
183 else if (leerzeileDavor || zeile.startsWith('#')) imPunkt = false;
184 leerzeileDavor = false;
185 });
186 return funde;
187 }
188
189 let sendeMenuebefehl: (befehl: MenuCommand) => void = () => undefined;
190
191 beforeAll(async () => {
192 vi.mocked(ladeSitzung).mockResolvedValue({ art: 'leer' });
193 vi.mocked(saveSession).mockResolvedValue({ ok: true, message: '' });
194
195 (window as unknown as { lsaPlaner: unknown }).lsaPlaner = {
196 platform: 'electron',
197 setDirty: () => undefined,
198 reportError: () => undefined,
199 logPath: () => Promise.resolve(PROTOKOLLPFAD),
200 onMenuCommand: (behandler: (befehl: MenuCommand) => void) => {
201 sendeMenuebefehl = behandler;
202 return () => undefined;
203 },
204 onBeforeClose: () => undefined,
205 };
206 // Wie in ueberFenster.test.ts: Die Seite ist deutsch ausgezeichnet.
207 document.documentElement.setAttribute('lang', 'de');
208
209 document.body.replaceChildren();
210 const wurzel = document.createElement('div');
211 wurzel.id = 'anwendung';
212 document.body.append(wurzel);
213
214 await import('@/main');
215 await warte(50);
216 });
217
218 beforeEach(async () => {
219 schliesseAlle();
220 await warte(5);
221 });
222
223 async function oeffneUeber(): Promise<HTMLElement> {
224 sendeMenuebefehl('ueber');
225 await warte(5);
226 const kasten = fenster(TITEL);
227 expect(kasten, 'der Menübefehl "ueber" hat kein Fenster geöffnet').not.toBeNull();
228 return kasten!;
229 }
230
231 async function oeffneDatenschutz(): Promise<HTMLElement> {
232 const kasten = await oeffneUeber();
233 knopf(kasten, 'Datenschutz').click();
234 await warte(5);
235 return kasten;
236 }
237
238 describe('Über-Fenster: Ansicht "Datenschutz"', () => {
239 it('fuehrt die Datei in dem Format, das der Abgleich hier voraussetzt', () => {
240 // `erwartung` prueft je Block, dass er von einer Art ist - zwischen zwei
241 // Bloecken steht also eine Leerzeile.
242 const { titel, folge } = erwartung(DATENSCHUTZ);
243 expect(titel).toBe('Datenschutzhinweise zu LSA-Planer Professional');
244 expect(folge.length).toBeGreaterThan(20);
245 });
246
247 it('steht als Schaltfläche in der Leiste, hinter den Lizenztexten und vor "Schließen"', async () => {
248 const kasten = await oeffneUeber();
249 expect(leiste(kasten)).toEqual([
250 'Lizenztext (deutsch)',
251 'Lizenztext (englisch)',
252 'Datenschutz',
253 'Schließen',
254 ]);
255 });
256
257 it('lässt den ersten Fokus beim Öffnen im Inhalt, nicht auf der Leiste', async () => {
258 // Die Begruendung der Reihenfolge in src/ui/ueber.ts stuetzt sich darauf.
259 const kasten = await oeffneUeber();
260 await warte(5);
261 expect(document.activeElement).toBe(knopf(kasten, 'Quellcode öffnen'));
262 });
263
264 it('zeigt die Titelzeile der Datei als Überschrift und setzt den Fokus darauf', async () => {
265 const kasten = await oeffneDatenschutz();
266 expect(titelDerFenster(), 'die Ansicht öffnet ein zweites Fenster').toEqual([TITEL]);
267
268 const ueberschrift = inhalt(kasten).firstElementChild;
269 expect(ueberschrift?.tagName).toBe('H3');
270 expect(ueberschrift?.textContent).toBe('Datenschutzhinweise zu LSA-Planer Professional');
271 expect(ueberschrift?.getAttribute('tabindex')).toBe('-1');
272 expect(document.activeElement).toBe(ueberschrift);
273 expect(leiste(kasten)).toEqual(['Zurück', 'Schließen']);
274 });
275
276 it('zeigt Abschnitte, Absätze und Aufzählungspunkte in der Reihenfolge der Datei', async () => {
277 const kasten = await oeffneDatenschutz();
278 const { folge } = erwartung(DATENSCHUTZ);
279
280 const gezeigt = [...inhalt(kasten).querySelectorAll('h3, p, li')]
281 .slice(1) // die Ueberschrift der Ansicht
282 .map((k) => [k.tagName, k.textContent ?? ''] as const);
283 expect(gezeigt).toEqual(folge);
284 });
285
286 it('bringt jeden "## "-Abschnitt als Überschrift, in derselben Reihenfolge', async () => {
287 // Enger als der Fall darueber und unabhaengig von der Blockzerlegung: nur
288 // die Abschnittszeilen, wie sie in der Datei stehen.
289 const kasten = await oeffneDatenschutz();
290 const abschnitte = DATENSCHUTZ.split('\n')
291 .filter((z) => z.startsWith('## '))
292 .map((z) => z.slice(3).trim());
293 expect(abschnitte).toContain('Die einzige Verbindung ins Internet: amtliche Luftbilder');
294 const ueberschriften = [...inhalt(kasten).querySelectorAll('h3')]
295 .slice(1)
296 .map((h) => h.textContent);
297 expect(ueberschriften).toEqual(abschnitte);
298 });
299
300 it('bringt jeden "- "-Punkt als Listeneintrag in einer Aufzählung', async () => {
301 const kasten = await oeffneDatenschutz();
302 const punkte = DATENSCHUTZ.split('\n')
303 .filter((z) => z.startsWith('- '))
304 .map((z) => z.slice(2).trim());
305 const eintraege = [...inhalt(kasten).querySelectorAll('li')];
306 expect(eintraege.map((li) => li.textContent)).toEqual(punkte);
307 for (const li of eintraege) expect(li.parentElement?.tagName).toBe('UL');
308 // Anker gegen einen Abgleich, der sich selbst prueft.
309 expect(punkte).toContain(
310 'sgx.geodatenzentrum.de: Bundesamt für Kartographie und Geodäsie (TopPlusOpen)',
311 );
312 });
313
314 it('fügt die Zeilen eines Absatzes zusammen - kein Zeilenumbruch, keine Doppelabstände', async () => {
315 const kasten = await oeffneDatenschutz();
316 const absaetze = [...inhalt(kasten).querySelectorAll('p')].map((p) => p.textContent ?? '');
317 expect(absaetze.length).toBeGreaterThan(10);
318 for (const text of absaetze) {
319 expect(text, 'Zeilenumbruch im Absatz').not.toMatch(/[\r\n]/);
320 expect(text, 'doppelter Abstand').not.toMatch(/ {2}/);
321 expect(text, 'Rand').toBe(text.trim());
322 }
323 // Der erste Absatz der Datei laeuft ueber fuenf Zeilen.
324 expect(absaetze[1]).toMatch(
325 /^Diese Hinweise beschreiben, welche Daten .* Hinweise veröffentlicht sind\.$/,
326 );
327 });
328
329 it('zeigt keine Auszeichnungszeichen der Datei als Text', async () => {
330 const kasten = await oeffneDatenschutz();
331 for (const knoten of inhalt(kasten).querySelectorAll('h3, p, li')) {
332 const text = knoten.textContent ?? '';
333 expect(text, `${knoten.tagName} beginnt mit einer Raute`).not.toMatch(/^#/);
334 expect(text, `${knoten.tagName} beginnt mit einem Aufzählungsstrich`).not.toMatch(/^- /);
335 }
336 const alles = inhalt(kasten).textContent ?? '';
337 expect(alles).not.toContain('## ');
338 expect(alles).not.toContain('# Datenschutzhinweise');
339 });
340
341 it('zeichnet jeden Block des Textes ausdrücklich als deutsch aus', async () => {
342 // Wie die deutsche Lizenzfassung: ausdruecklich und nicht nur geerbt.
343 const kasten = await oeffneDatenschutz();
344 const bloecke = [...inhalt(kasten).children].slice(1);
345 expect(bloecke.length).toBeGreaterThan(0);
346 for (const block of bloecke) {
347 expect(
348 block.getAttribute('lang'),
349 `${block.tagName} "${(block.textContent ?? '').slice(0, 40)}…"`,
350 ).toBe('de');
351 }
352 });
353
354 it('führt mit "Zurück" zur Übersicht und setzt den Fokus auf deren Überschrift', async () => {
355 const kasten = await oeffneDatenschutz();
356 knopf(kasten, 'Zurück').click();
357 await warte(5);
358
359 const ueberschrift = inhalt(kasten).firstElementChild;
360 expect(ueberschrift?.textContent).toBe('Programm');
361 expect(document.activeElement).toBe(ueberschrift);
362 expect(leiste(kasten)).toContain('Datenschutz');
363 expect(inhalt(kasten).textContent).not.toContain('Das Wichtigste in Kürze');
364 });
365 });
366
367 describe('docs/datenschutz.md: dieselbe Gliederung wie bei einem Leser nach CommonMark', () => {
368 /*
369 * Die Datei steht auch auf der Internetseite des Herausgebers (Kopf von
370 * src/ui/datenschutzText.ts). Die Faelle unten nehmen die echte Datei und
371 * setzen je eine Zeile ein - so zeigt sich, dass die Wache an genau dieser
372 * Datei greift und nicht nur an einem ausgedachten Beispiel.
373 */
374 const zeilen = DATENSCHUTZ.split('\n').map((z) => z.replace(/\r$/, ''));
375
376 /** Die echte Datei, mit `zeile` hinter der Zeile `nach` (gezaehlt ab 1). */
377 function mitZeile(nach: number, zeile: string): string {
378 return [...zeilen.slice(0, nach), zeile, ...zeilen.slice(nach)].join('\n');
379 }
380
381 const absatzzeile =
382 zeilen.findIndex((z, i) => i > 0 && zeilen[i - 1] === '' && /^[^#\-\s]/.test(z)) + 1;
383 const punktpaar =
384 zeilen.findIndex((z, i) => z.startsWith('- ') && zeilen[i + 1]?.startsWith('- ')) + 1;
385 const letzterPunkt =
386 zeilen.findIndex(
387 (z, i) => z.startsWith('- ') && zeilen[i + 1] === '' && zeilen[i + 2] !== undefined,
388 ) + 1;
389
390 it('enthält keinen der Fälle, die der Leser anders gliedert', () => {
391 expect(gliederungsabweichungen(DATENSCHUTZ)).toEqual([]);
392 });
393
394 it.each<[string, () => [string, number, string]]>([
395 [
396 'eine Leerzeile zwischen zwei Punkten',
397 () => [mitZeile(punktpaar, ''), punktpaar + 2, FALL.leerzeileZwischenPunkten],
398 ],
399 [
400 'eine eingerückte Zeile nach Leerzeile hinter einem Punkt',
401 () => [mitZeile(letzterPunkt + 1, ' eingerückt'), letzterPunkt + 2, FALL.eingerueckt],
402 ],
403 [
404 'Bindestriche unter einer Absatzzeile',
405 () => [mitZeile(absatzzeile, '---'), absatzzeile + 1, FALL.zeichenzeile],
406 ],
407 [
408 'Gleichheitszeichen unter einer Absatzzeile',
409 () => [mitZeile(absatzzeile, '==='), absatzzeile + 1, FALL.zeichenzeile],
410 ],
411 [
412 'eine Trennlinie aus Sternen',
413 () => [mitZeile(absatzzeile - 1, '***'), absatzzeile, FALL.zeichenzeile],
414 ],
415 ['ein Zitat', () => [mitZeile(absatzzeile - 1, '> Zitat'), absatzzeile, FALL.zitat]],
416 [
417 'einen Codeblock mit Tilden',
418 () => [mitZeile(absatzzeile - 1, '~~~'), absatzzeile, FALL.tilden],
419 ],
420 [
421 'eine Verweisdefinition',
422 () => [
423 mitZeile(absatzzeile - 1, '[name]: https://example.org'),
424 absatzzeile,
425 FALL.verweisdefinition,
426 ],
427 ],
428 ['einen Tabulator', () => [mitZeile(absatzzeile - 1, '-\tPunkt'), absatzzeile, FALL.tabulator]],
429 ])('erkennt in der echten Datei %s', (_fall, baue) => {
430 expect(absatzzeile, 'keine Absatzzeile nach einer Leerzeile').toBeGreaterThan(1);
431 expect(punktpaar, 'keine zwei Punkte hintereinander').toBeGreaterThan(0);
432 expect(letzterPunkt, 'kein Punkt vor einer Leerzeile').toBeGreaterThan(0);
433 const [mutiert, nummer, fall] = baue();
434 expect(gliederungsabweichungen(mutiert)).toEqual([`Zeile ${nummer}: ${fall}`]);
435 });
436 });
437
438 describe('Über-Fenster: Übersicht', () => {
439 it('nennt die Lizenzschaltflächen beim Namen, seit eine dritte in der Leiste steht', async () => {
440 const kasten = await oeffneUeber();
441 const text = einzeilig(inhalt(kasten).textContent ?? '');
442 expect(text).not.toContain('die beiden Schaltflächen unten');
443 expect(text.match(/die beiden Schaltflächen „Lizenztext“ unten zeigen ihn/g)).toHaveLength(2);
444 // Und es sind wirklich genau zwei, die so heissen.
445 expect(leiste(kasten).filter((b) => b.startsWith('Lizenztext'))).toHaveLength(2);
446 });
447
448 /*
449 * Der Doppelpunkt fuehrt die Liste ein, und zwar mit dem Satz, zu dem sie
450 * gehoert. Stand der Satz ueber den geschuetzten Ordner vor dem
451 * Doppelpunkt, las sich die Liste als Inhalt dieses Ordners; er steht
452 * deshalb als eigener Absatz hinter der Liste.
453 */
454 it('nennt vor der Liste die vier Dateien und danach den geschützten Ordner', async () => {
455 const kasten = await oeffneUeber();
456 const liste = [...inhalt(kasten).querySelectorAll('ul')].find((ul) =>
457 (ul.textContent ?? '').includes('DRITTANBIETER-LIZENZEN.txt'),
458 );
459 expect(liste, 'die Liste der vier Dateien fehlt').toBeDefined();
460 const davor = liste!.previousElementSibling;
461 expect(davor?.tagName).toBe('P');
462 expect(davor?.textContent).toBe(
463 'Zu den Fremdbestandteilen liegen neben der Programmdatei vier Dateien:',
464 );
465 const danach = liste!.nextElementSibling;
466 expect(danach?.tagName).toBe('P');
467 expect(danach?.textContent).toBe(
468 'Je nach Installationsart steht die Programmdatei in einem Ordner, den Windows vor ' +
469 'Änderungen schützt.',
470 );
471 });
472 });
473
474 describe('Luftbild-Dialog', () => {
475 it('sagt vor dem Abruf, was den Rechner verlässt, wohin, und wo mehr dazu steht', async () => {
476 const ergebnis = luftbildDialog();
477 const kasten = fenster('Amtliches Luftbild laden');
478 expect(kasten, 'der Luftbild-Dialog ist nicht offen').not.toBeNull();
479 try {
480 const satz =
481 'Beim Abruf erhält der unter „Kartengrundlage“ gewählte Dienst die Lage des Ausschnitts ' +
482 'und, wie bei jeder Verbindung ins Internet, die IP-Adresse Ihres Internetanschlusses; ' +
483 'Projektinhalte gehen nicht mit. Mehr dazu unter ' +
484 '„Hilfe → Über LSA-Planer Professional → Datenschutz“.';
485 const absaetze = [...inhalt(kasten!).querySelectorAll('p')].map((p) => p.textContent);
486 expect(absaetze).toContain(satz);
487
488 // Die genannten Beschriftungen gibt es wirklich: das Feld im Dialog ...
489 expect(einzeilig(kasten!.textContent ?? '')).toContain('Kartengrundlage');
490 // ... und der Menueeintrag, der das "Über"-Fenster oeffnet.
491 expect(MENUE).toContain('label: `Über LSA-Planer Professional ${version}`');
492 // Dieselbe Aussage in den Datenschutzhinweisen - keine zweite Wahrheit.
493 expect(einzeilig(DATENSCHUTZ)).toContain('erfährt der Dienst die IP-Adresse');
494 expect(einzeilig(DATENSCHUTZ)).toContain('Projektinhalte enthält sie nicht');
495 } finally {
496 knopf(kasten!, 'Abbrechen').click();
497 await ergebnis;
498 }
499 });
500 });
501
502 describe('Kurzhilfe: "Bei Störungen"', () => {
503 async function stoerungen(): Promise<HTMLElement[]> {
504 sendeMenuebefehl('hilfe');
505 await warte(20);
506 const kasten = fenster('Kurzhilfe');
507 expect(kasten, 'die Kurzhilfe ist nicht offen').not.toBeNull();
508 const ueberschrift = [...inhalt(kasten!).querySelectorAll('h3')].find(
509 (h) => h.textContent === 'Bei Störungen',
510 );
511 expect(ueberschrift, 'Abschnitt "Bei Störungen" fehlt').toBeDefined();
512 const behaelter = ueberschrift!.nextElementSibling;
513 return [...(behaelter?.querySelectorAll<HTMLElement>('p') ?? [])];
514 }
515
516 it('nennt zuerst den Menüweg zur Kopie, mit der Beschriftung aus dem Menü', async () => {
517 const absaetze = await stoerungen();
518 expect(absaetze[0]?.textContent).toBe(
519 'Die Anwendung schreibt unerwartete Fehler in ein Protokoll. Über ' +
520 '„Hilfe → Fehlerprotokoll speichern …“ legen Sie eine Kopie an einem Ort Ihrer Wahl an; ' +
521 'bitte legen Sie diese Kopie einer Fehlermeldung bei. Das Protokoll ist darauf angelegt, ' +
522 'keine Projektinhalte aufzunehmen, und die Kopie ist eine gewöhnliche Textdatei, die Sie ' +
523 'vor dem Versenden lesen können.',
524 );
525 // Die Beschriftung steht so im Hilfemenue (electron/menu.ts).
526 expect(MENUE).toContain("label: 'Fehlerprotokoll speichern …'");
527 });
528
529 it('verspricht nicht mehr, das Protokoll enthalte nur technische Angaben', async () => {
530 const text = (await stoerungen()).map((p) => p.textContent).join(' ');
531 expect(text).not.toContain('nur technische Angaben');
532 });
533
534 it('lässt den Pfad als Auskunft stehen, mit dem Hinweis zur Installationsart', async () => {
535 /*
536 * Fassung 5.43.0, Befund 8: Der Grund stand ohne Einschraenkung da
537 * ("Windows führt die Programmdaten ... gesondert"). Beim Umsteiger ist
538 * gemessen das Gegenteil wahr - hatten Setup oder tragbares Programm den
539 * Ordner schon angelegt, landen auch neue Dateien dort (gemessen am
540 * 17.09.2026 unter Windows 11) -, und die Datenschutzhinweise desselben
541 * Programms sagen das. Der Grund nennt deshalb die Ausnahme, und zwar fuer
542 * Windows 11: Gemessen ist nur dort, und fuer Windows 10 vor Version 1903
543 * beschreibt Microsoft den Fall anders.
544 */
545 const absaetze = await stoerungen();
546 expect(absaetze.map((p) => p.textContent)).toEqual([
547 expect.stringContaining('Fehlerprotokoll speichern …'),
548 'Das Protokoll selbst liegt unter dem folgenden Pfad. Bei manchen Installationsarten ' +
549 'zeigt der Explorer die Datei dort nicht unbedingt an, weil Windows die Programmdaten ' +
550 'gesondert führt. Unter Windows 11 gilt das nicht, wenn das Programm aus dem Setup ' +
551 'oder das tragbare Programm ihren Ordner schon vorher angelegt hatte.',
552 PROTOKOLLPFAD,
553 ]);
554 // Dieselbe Ausnahme in den Datenschutzhinweisen - keine zweite Wahrheit.
555 expect(einzeilig(DATENSCHUTZ)).toContain(
556 'vorher schon vom Programm aus dem Setup oder vom tragbaren Programm angelegt war',
557 );
558 });
559 });