lsa-planer

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

/ tests ui menuewegAusgabe.test.ts

5,6 KB Rohdatei
tests/ui/menuewegAusgabe.test.ts — 157 Zeilen
1 import { beforeAll, describe, expect, it, vi } from 'vitest';
2 import { ladeSitzung, saveProjectToFile, saveSession } from '@/services/storage';
3 import type { MenuCommand } from '@shared/ipc';
4
5 /**
6 * Befund 22 zur Fassung 5.9.0, der Rest.
7 *
8 * "Datei > Speichern" und "Datei > Speichern unter" tragen bereits
9 * den Waechter `if (offeneFensterAnzahl() > 0) break;`. `case 'export'`
10 * trug ihn nicht - und ein Ansichtswechsel ist hier kein harmloserer Vorgang
11 * als ein Speichervorgang:
12 *
13 * 1. `shell.navigate` ist ein ORTSWECHSEL und endet mit `ziel.focus()` auf der
14 * Ueberschrift der neuen Ansicht. Der Hintergrund ist hinter einem modalen
15 * Fenster stillgelegt (`legeHintergrundStill`, aria-hidden und inert) - der
16 * Fokus wird also aus dem offenen Fenster heraus in einen stillgelegten
17 * Teilbaum gesetzt. Genau diesen Zustand beschreiben die
18 * Befunde M1/M8 als unzulaessig, und
19 * eine Sprachausgabe liest dann Inhalte vor, die optisch nicht da sind.
20 * 2. Der Tastenempfaenger in `src/main.ts` bricht aus demselben Grund schon
21 * bei Strg+Z ab: "Strg+Z/Strg+Y bauten die stillgelegte Ansicht unter dem
22 * Dialog neu auf". Ein Ortswechsel tut mehr als ein Neuaufbau - er raeumt
23 * die Ansicht ab, aus der das offene Fenster stammt.
24 *
25 * Geprueft wird am echten Einstiegspunkt: `src/main.ts` startet die ganze
26 * Anwendung; ersetzt sind nur Sitzungsspeicher, Dateizugriff und die
27 * Desktop-Bruecke, ueber die der Menuebefehl hereinkommt.
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 saveProjectToFile: vi.fn(),
38 };
39 });
40
41 async function warte(ms = 0): Promise<void> {
42 await new Promise((fertig) => setTimeout(fertig, ms));
43 }
44
45 function offeneFenster(): string[] {
46 return [...document.querySelectorAll('.dialog-hintergrund h2')].map(
47 (h) => h.textContent?.trim() ?? '',
48 );
49 }
50
51 /** Die Ueberschrift der gerade sichtbaren Ansicht. */
52 function sichtbareAnsicht(): string {
53 return document.querySelector('main h1')?.textContent?.trim() ?? '';
54 }
55
56 function feld(beschriftung: string): HTMLInputElement {
57 const label = [...document.querySelectorAll('label')].find(
58 (l) => l.textContent?.trim() === beschriftung,
59 );
60 const id = label?.getAttribute('for');
61 const eingabe = id === null || id === undefined ? null : document.getElementById(id);
62 if (!(eingabe instanceof HTMLInputElement)) {
63 throw new Error(`Eingabefeld "${beschriftung}" nicht gefunden.`);
64 }
65 return eingabe;
66 }
67
68 function druecke(taste: string): void {
69 window.dispatchEvent(
70 new KeyboardEvent('keydown', { key: taste, ctrlKey: true, bubbles: true, cancelable: true }),
71 );
72 }
73
74 /** Der Menuebefehl, so wie ihn der Hauptprozess schickt. */
75 let sendeMenuebefehl: (befehl: MenuCommand) => void = () => undefined;
76
77 beforeAll(async () => {
78 vi.mocked(ladeSitzung).mockResolvedValue({ art: 'leer' });
79 vi.mocked(saveSession).mockResolvedValue({ ok: true, message: '' });
80 vi.mocked(saveProjectToFile).mockResolvedValue({
81 ok: true,
82 canceled: false,
83 filePath: 'C:/Plaene/knoten.lsap',
84 message: 'Gespeichert: C:/Plaene/knoten.lsap',
85 });
86
87 (window as unknown as { lsaPlaner: unknown }).lsaPlaner = {
88 platform: 'electron',
89 appVersion: 'pruefung',
90 setDirty: () => undefined,
91 reportError: () => undefined,
92 logPath: () => Promise.resolve('C:/Protokoll/lsa-planer.log'),
93 onMenuCommand: (behandler: (befehl: MenuCommand) => void) => {
94 sendeMenuebefehl = behandler;
95 return () => undefined;
96 },
97 onBeforeClose: () => undefined,
98 };
99
100 document.body.replaceChildren();
101 const wurzel = document.createElement('div');
102 wurzel.id = 'anwendung';
103 document.body.append(wurzel);
104
105 await import('@/main');
106 await warte(20);
107 });
108
109 describe('Datei > Ausgabe aus dem Anwendungsmenü', () => {
110 it('wechselt hinter einem offenen Fenster die Ansicht nicht', async () => {
111 const name = feld('Projektbezeichnung');
112 name.value = 'Bahnhofstraße';
113 name.dispatchEvent(new Event('change'));
114 await warte(0);
115
116 expect(sichtbareAnsicht(), 'Vorbedingung: die Anwendung steht in "Projekt"').toBe('Projekt');
117
118 // Der Anwender greift zu "Neu"; wegen der ungespeicherten Aenderung steht
119 // die Rueckfrage offen. Das Anwendungsmenue bleibt daneben bedienbar - es
120 // wird von der Stilllegung des Hintergrunds nicht erfasst.
121 druecke('n');
122 await warte(10);
123 expect(offeneFenster()).toEqual(['Ungespeicherte Änderungen']);
124 const dialog = document.querySelector('.dialog-hintergrund .dialog');
125 expect(
126 dialog?.contains(document.activeElement),
127 'Vorbedingung: der Fokus steht im Fenster',
128 ).toBe(true);
129
130 sendeMenuebefehl('export');
131 await warte(10);
132
133 expect(
134 sichtbareAnsicht(),
135 'Der Menueweg wechselt hinter der offenen Rueckfrage die Ansicht',
136 ).toBe('Projekt');
137 expect(
138 dialog?.contains(document.activeElement),
139 'Der Ansichtswechsel zieht den Fokus aus dem offenen Fenster in den stillgelegten Hintergrund',
140 ).toBe(true);
141 expect(offeneFenster()).toEqual(['Ungespeicherte Änderungen']);
142
143 // Die Rueckfrage abbrechen; das Fenster ist danach vom Stapel.
144 document.querySelector<HTMLButtonElement>('.dialog-hintergrund button')?.click();
145 await warte(10);
146 expect(offeneFenster()).toEqual([]);
147 });
148
149 it('wechselt ohne offenes Fenster weiterhin in die Ausgabe', async () => {
150 // Grenzwache zur vorigen Pruefung: Der Waechter darf den Regelweg nicht
151 // mitsperren.
152 sendeMenuebefehl('export');
153 await warte(10);
154
155 expect(sichtbareAnsicht()).toBe('Ausgabe');
156 });
157 });