lsa-planer

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

/ tests services nachzueglerNachFehlschlag.test.ts

5,7 KB Rohdatei
tests/services/nachzueglerNachFehlschlag.test.ts — 172 Zeilen
1 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2 import { AutoSave } from '@/services/autosave';
3 import { ProjectStore } from '@/app/store';
4 import * as actions from '@/app/actions';
5 import { createEmptyProject } from '@/domain/model/factory';
6 import { addRecoveryPoint, saveSession } from '@/services/storage';
7 import type { AppSettings, StorageResult } from '@/services/storage';
8 import type { Project } from '@/domain/model/project';
9
10 /**
11 * Befund 53 zur Fassung 5.9.0.
12 *
13 * `writeSession` fasst Aenderungen, die waehrend eines laufenden
14 * Schreibvorgangs eintreffen, zu EINEM Nachzuegler zusammen; der zweite Aufruf
15 * setzt nur `nachholen` und kehrt zurueck. Der Kommentar daneben sagt zu: "Der
16 * laufende Vorgang holt den neuesten Stand selbst nach."
17 *
18 * Im Fehlerfall fiel genau das aus: Der Fehlerzweig kehrte bedingungslos
19 * zurueck, ohne `nachholen` anzusehen, waehrend der Erfolgszweig es tut. Der
20 * Entprellzeitgeber war zu diesem Zeitpunkt bereits abgeraeumt - es stand also
21 * kein weiterer Sitzungsschreibversuch mehr an, und der zuletzt eingegebene
22 * Wert lag nur noch im Arbeitsspeicher.
23 *
24 * Geprueft wird beides: dass der Nachzuegler ankommt UND dass daraus kein
25 * Dauerlauf wird, wenn der Speicher dauerhaft defekt ist.
26 *
27 * WARUM EINE EIGENE NACHBILDUNG: Die Pruefung laeuft in Node; dort gibt es
28 * weder `document` noch `window`, und der echte Sitzungsspeicher fiele ohne
29 * IndexedDB in den Ersatzweg. Der Schreibvorgang wird deshalb durch `vi.fn()`
30 * ersetzt - nur so laesst sich ein haengender und ein fehlschlagender Vorgang
31 * genau steuern.
32 */
33
34 vi.mock('@/services/storage', () => ({
35 saveSession: vi.fn(),
36 addRecoveryPoint: vi.fn(),
37 }));
38
39 class Ereignisziel {
40 private readonly empfaenger = new Map<string, Set<() => void>>();
41
42 addEventListener(typ: string, hoerer: () => void): void {
43 const menge = this.empfaenger.get(typ) ?? new Set<() => void>();
44 menge.add(hoerer);
45 this.empfaenger.set(typ, menge);
46 }
47
48 removeEventListener(typ: string, hoerer: () => void): void {
49 this.empfaenger.get(typ)?.delete(hoerer);
50 }
51 }
52
53 class Anzeige extends Ereignisziel {
54 visibilityState: 'visible' | 'hidden' = 'visible';
55 }
56
57 const EINSTELLUNGEN: AppSettings = {
58 theme: 'system',
59 autoSaveEnabled: true,
60 autoSaveIntervalSeconds: 120,
61 locale: 'de',
62 meldungsdauerSekunden: 5,
63 fundstellen: {},
64 };
65
66 const FEHLSCHLAG: StorageResult = { ok: false, message: 'Kein Speicher vorhanden.' };
67
68 let store: ProjectStore;
69 let laufend: AutoSave | null;
70
71 beforeEach(() => {
72 vi.useFakeTimers();
73 vi.mocked(saveSession).mockReset();
74 vi.mocked(saveSession).mockResolvedValue({ ok: true, message: '' });
75 vi.mocked(addRecoveryPoint).mockReset();
76 vi.mocked(addRecoveryPoint).mockResolvedValue(undefined);
77
78 store = new ProjectStore(createEmptyProject('Bahnhofstrasse'));
79 laufend = null;
80 vi.stubGlobal('document', new Anzeige());
81 vi.stubGlobal('window', new Ereignisziel());
82 });
83
84 afterEach(() => {
85 laufend?.stop();
86 vi.useRealTimers();
87 vi.unstubAllGlobals();
88 });
89
90 function starte(): AutoSave {
91 const dienst = new AutoSave(store, EINSTELLUNGEN, () => undefined);
92 dienst.start();
93 laufend = dienst;
94 return dienst;
95 }
96
97 /** Eine Aenderung, wie sie der Anwender ausloest: neues Projektobjekt, dirty. */
98 function aendere(name: string): void {
99 store.update((p) => actions.setMeta(p, { name }), { label: 'Name geaendert' });
100 }
101
102 async function warte(ms: number): Promise<void> {
103 await vi.advanceTimersByTimeAsync(ms);
104 }
105
106 /** Die Namen der Staende, die tatsaechlich geschrieben wurden - in Reihenfolge. */
107 function gesichert(): string[] {
108 return vi.mocked(saveSession).mock.calls.map(([projekt]: [Project]) => projekt.meta.name);
109 }
110
111 /** Ein Schreibvorgang, der erst auf Zuruf fertig wird. */
112 function aufschieben(): {
113 versprechen: Promise<StorageResult>;
114 loesen: (ergebnis: StorageResult) => void;
115 } {
116 let loesen: ((ergebnis: StorageResult) => void) | null = null;
117 const versprechen = new Promise<StorageResult>((erfuellen) => {
118 loesen = erfuellen;
119 });
120 return {
121 versprechen,
122 loesen: (ergebnis: StorageResult): void => {
123 loesen?.(ergebnis);
124 },
125 };
126 }
127
128 describe('Nachzuegler nach einem fehlgeschlagenen Sitzungsschreiben', () => {
129 it('kommt trotzdem an', async () => {
130 const erster = aufschieben();
131 vi.mocked(saveSession).mockReturnValueOnce(erster.versprechen);
132 starte();
133
134 aendere('Räumweg 12,0 m');
135 await warte(1500);
136 expect(saveSession).toHaveBeenCalledTimes(1);
137
138 // Waehrend der erste Vorgang noch haengt, berichtigt der Anwender den
139 // Raeumweg nach oben. Der zweite Anlauf merkt den Stand nur vor.
140 aendere('Räumweg 14,5 m');
141 await warte(1500);
142 expect(saveSession).toHaveBeenCalledTimes(1);
143
144 erster.loesen(FEHLSCHLAG);
145 await warte(120_000);
146
147 expect(
148 gesichert(),
149 'Der vorgemerkte Nachzuegler faellt nach einem Fehlschlag weg, obwohl der Entprellzeitgeber laengst abgeraeumt ist',
150 ).toEqual(['Räumweg 12,0 m', 'Räumweg 14,5 m']);
151 });
152
153 it('wird genau einmal nachgeholt und nicht in einer Schleife wiederholt', async () => {
154 const erster = aufschieben();
155 vi.mocked(saveSession).mockReturnValueOnce(erster.versprechen);
156 vi.mocked(saveSession).mockResolvedValue(FEHLSCHLAG);
157 starte();
158
159 aendere('Räumweg 12,0 m');
160 await warte(1500);
161 aendere('Räumweg 14,5 m');
162 await warte(1500);
163 erster.loesen(FEHLSCHLAG);
164
165 // Zehn Minuten bei dauerhaft defektem Speicher: Der nachgeholte Versuch
166 // scheitert ebenfalls, danach steht nichts mehr an. Wuerde der Fehlerzweig
167 // in der Schleife sofort weiterversuchen, liefe er hier durch.
168 await warte(600_000);
169
170 expect(saveSession, 'Der Fehlerzweig dreht durch').toHaveBeenCalledTimes(2);
171 });
172 });