lsa-planer

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

/ tests ui speichernRiegel.test.ts

5,3 KB Rohdatei
tests/ui/speichernRiegel.test.ts — 147 Zeilen
1 import { beforeAll, describe, expect, it, vi } from 'vitest';
2 import { ladeSitzung, saveProjectToFile, saveSession } from '@/services/storage';
3 import type { FileOperationResult } from '@/services/storage';
4
5 /**
6 * Befund 14 zur Fassung 5.9.0.
7 *
8 * `saveProject` fuehrte kein Merkmal "laeuft gerade". Weder die
9 * Kopfzeilenschaltflaeche (sie bekommt kein `enabled` und ist damit nie
10 * gesperrt) noch der Tastenweg noch der Menuebefehl hielten einen zweiten Lauf
11 * auf. Der Waechter aus Befund 16 vergleicht nur den EIGENEN Stand mit dem
12 * jetzigen und weiss von einem zweiten Lauf nichts.
13 *
14 * Folge bei zwei ueberlappenden Laeufen mit verschiedenem Inhalt (A alt, B neu):
15 * Lauf B endet zuerst, sieht seinen Stand unveraendert und ruft `markSaved` -
16 * die Aenderungsmarke faellt, Kopfzeile und Fenstertitel melden "gespeichert".
17 * Lauf A endet danach, benennt zuletzt um und legt damit den AELTEREN Stand auf
18 * die Platte; er warnt zwar, ruft aber nur `setFilePath` - die Marke bleibt
19 * geloescht. Der Anwender hat danach eine Datei mit dem alten Stand, ein
20 * Programm, das "gespeichert" sagt, und keinen Waechter mehr vor "Neu",
21 * "Öffnen" oder dem Schliessen. Ging bei dem Stand ein VERLAENGERTER Raeumweg
22 * verloren, traegt die angeordnete Unterlage weiter die kuerzere Zwischenzeit.
23 *
24 * Der Sitzungsspeicher fuehrt denselben Riegel laengst (services/autosave.ts:
25 * "Es laeuft nie mehr als ein Schreibvorgang gleichzeitig").
26 *
27 * Geprueft wird am echten Einstiegspunkt: `src/main.ts` wird eingelesen und
28 * startet die ganze Anwendung; ersetzt sind nur Sitzungsspeicher und
29 * Dateizugriff.
30 */
31
32 vi.mock('@/services/storage', async (importOriginal) => {
33 const echt = await importOriginal<typeof import('@/services/storage')>();
34 return {
35 ...echt,
36 ladeSitzung: vi.fn(),
37 saveSession: vi.fn(),
38 addRecoveryPoint: vi.fn(),
39 saveProjectToFile: vi.fn(),
40 };
41 });
42
43 async function warte(ms = 0): Promise<void> {
44 await new Promise((fertig) => setTimeout(fertig, ms));
45 }
46
47 function offeneFenster(): string[] {
48 return [...document.querySelectorAll('.dialog-hintergrund h2')].map(
49 (h) => h.textContent?.trim() ?? '',
50 );
51 }
52
53 /** Das Eingabefeld zu einer sichtbaren Beschriftung. */
54 function feld(beschriftung: string): HTMLInputElement {
55 const label = [...document.querySelectorAll('label')].find(
56 (l) => l.textContent?.trim() === beschriftung,
57 );
58 const id = label?.getAttribute('for');
59 const eingabe = id === null || id === undefined ? null : document.getElementById(id);
60 if (!(eingabe instanceof HTMLInputElement)) {
61 throw new Error(`Eingabefeld "${beschriftung}" nicht gefunden.`);
62 }
63 return eingabe;
64 }
65
66 function druecke(taste: string): void {
67 window.dispatchEvent(
68 new KeyboardEvent('keydown', { key: taste, ctrlKey: true, bubbles: true, cancelable: true }),
69 );
70 }
71
72 function benenne(name: string): void {
73 const eingabe = feld('Projektbezeichnung');
74 eingabe.value = name;
75 eingabe.dispatchEvent(new Event('change'));
76 }
77
78 /** Jeder Aufruf bekommt seine eigene Zusage, die erst auf Zuruf fertig wird. */
79 const offeneSchreibvorgaenge: ((ergebnis: FileOperationResult) => void)[] = [];
80
81 beforeAll(async () => {
82 vi.mocked(ladeSitzung).mockResolvedValue({ art: 'leer' });
83 vi.mocked(saveSession).mockResolvedValue({ ok: true, message: '' });
84 vi.mocked(saveProjectToFile).mockImplementation(
85 () =>
86 new Promise<FileOperationResult>((erfuellen) => {
87 offeneSchreibvorgaenge.push(erfuellen);
88 }),
89 );
90
91 document.body.replaceChildren();
92 const wurzel = document.createElement('div');
93 wurzel.id = 'anwendung';
94 document.body.append(wurzel);
95
96 await import('@/main');
97 await warte(20);
98 });
99
100 describe('Ein zweites Strg+S während des Schreibens', () => {
101 it('startet keinen zweiten Speichervorgang daneben', async () => {
102 benenne('Bahnhofstraße');
103 await warte(0);
104
105 druecke('s');
106 await warte(0);
107 expect(saveProjectToFile).toHaveBeenCalledTimes(1);
108
109 // Der Schreibvorgang laeuft noch - der Anwender berichtigt einen Raeumweg
110 // und drueckt erneut Strg+S.
111 benenne('Bahnhofstraße / Ringstraße');
112 await warte(0);
113 druecke('s');
114 await warte(10);
115
116 expect(
117 saveProjectToFile,
118 'Ein zweiter Speichervorgang laeuft neben dem ersten',
119 ).toHaveBeenCalledTimes(1);
120 });
121
122 it('lässt die Änderungsmarke stehen, weil der geschriebene Stand nicht mehr gilt', async () => {
123 // Alles, was noch aussteht, wird fertig. Beim Riegel ist das genau ein
124 // Lauf, und er trug den Stand von vor der zweiten Eingabe. Ohne den Riegel
125 // sind es zwei, und der zweite - mit dem jetzt geltenden Stand - loeschte
126 // die Aenderungsmarke, obwohl der erste danach den aelteren Stand auf die
127 // Platte legt.
128 while (offeneSchreibvorgaenge.length > 0) {
129 offeneSchreibvorgaenge.shift()?.({
130 ok: true,
131 canceled: false,
132 filePath: 'C:/Plaene/knoten.lsap',
133 message: 'Gespeichert: C:/Plaene/knoten.lsap',
134 });
135 }
136 await warte(20);
137
138 // Die Probe: Steht der Stand noch aus, muss "Neu" zurueckfragen. Ohne den
139 // Riegel haette der zweite Lauf hier bereits `markSaved` gerufen.
140 druecke('n');
141 await warte(10);
142 expect(offeneFenster()).toEqual(['Ungespeicherte Änderungen']);
143
144 document.querySelector<HTMLButtonElement>('.dialog-hintergrund button')?.click();
145 await warte(10);
146 });
147 });