lsa-planer

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

/ tests services wiederherstellungspunkteImLeerlauf.test.ts

4,8 KB Rohdatei
tests/services/wiederherstellungspunkteImLeerlauf.test.ts — 146 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
9 /**
10 * Wiederherstellungspunkte im Leerlauf nach dem Programmstart (Befund 41).
11 *
12 * Was der Anwender sonst erlebt: Er startet das Programm, sieht seinen
13 * wiederhergestellten Stand an und geht zum Kaffee. Ohne dass er eine Taste
14 * anruehrt, legt das Programm alle zwei Minuten einen weiteren
15 * Wiederherstellungspunkt mit genau demselben Projekt an. Nach zehn Minuten
16 * enthaelt der Fuenferring nur noch identische Kopien des Startzustands - die
17 * Punkte frueherer Sitzungen sind verdraengt -, und es wird im Zweiminutentakt
18 * weitergeschrieben.
19 *
20 * Ursache: Die Sperre in `createRecoveryPoint()` lautete
21 * `if (!state.dirty && this.lastSavedAt > 0) return;`. `lastSavedAt` wird nur
22 * nach einem geglueckten Sitzungsspeichern gesetzt; `boot()` sichert beim Start
23 * aber nicht, also bleibt es 0 und die Sperre greift nie.
24 *
25 * Die Nachbildung entspricht der in tests/services/autosave.test.ts: Die
26 * Pruefung laeuft in Node, dort gibt es weder `document` noch `window`.
27 */
28
29 vi.mock('@/services/storage', () => ({
30 saveSession: vi.fn(),
31 addRecoveryPoint: vi.fn(),
32 }));
33
34 class Ereignisziel {
35 private readonly empfaenger = new Map<string, Set<() => void>>();
36
37 addEventListener(typ: string, hoerer: () => void): void {
38 const menge = this.empfaenger.get(typ) ?? new Set<() => void>();
39 menge.add(hoerer);
40 this.empfaenger.set(typ, menge);
41 }
42
43 removeEventListener(typ: string, hoerer: () => void): void {
44 this.empfaenger.get(typ)?.delete(hoerer);
45 }
46
47 ausloesen(typ: string): void {
48 for (const hoerer of [...(this.empfaenger.get(typ) ?? [])]) hoerer();
49 }
50 }
51
52 class Anzeige extends Ereignisziel {
53 visibilityState: 'visible' | 'hidden' = 'visible';
54 }
55
56 const EINSTELLUNGEN: AppSettings = {
57 theme: 'system',
58 autoSaveEnabled: true,
59 autoSaveIntervalSeconds: 120,
60 locale: 'de',
61 meldungsdauerSekunden: 5,
62 fundstellen: {},
63 };
64
65 const ERFOLG: StorageResult = { ok: true, message: '' };
66
67 let store: ProjectStore;
68 let laufend: AutoSave | null;
69
70 beforeEach(() => {
71 vi.useFakeTimers();
72 vi.mocked(saveSession).mockReset();
73 vi.mocked(saveSession).mockResolvedValue(ERFOLG);
74 vi.mocked(addRecoveryPoint).mockReset();
75 vi.mocked(addRecoveryPoint).mockResolvedValue(undefined);
76
77 store = new ProjectStore(createEmptyProject('Bahnhofstrasse'));
78 laufend = null;
79 vi.stubGlobal('document', new Anzeige());
80 vi.stubGlobal('window', new Ereignisziel());
81 });
82
83 afterEach(() => {
84 laufend?.stop();
85 vi.useRealTimers();
86 vi.unstubAllGlobals();
87 });
88
89 function starte(abweichung: Partial<AppSettings> = {}): AutoSave {
90 const dienst = new AutoSave(store, { ...EINSTELLUNGEN, ...abweichung }, () => {
91 /* Meldungen spielen hier keine Rolle. */
92 });
93 dienst.start();
94 laufend = dienst;
95 return dienst;
96 }
97
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 describe('Wiederherstellungspunkte im Leerlauf', () => {
107 it('legt nach dem Start genau einen Punkt an, solange nichts geaendert wird', async () => {
108 // Wie nach `boot()`: Der Stand ist wiederhergestellt, es wurde noch nichts
109 // gesichert (lastSavedAt === 0) und der Anwender ruehrt nichts an.
110 starte();
111
112 await warte(120_000);
113 expect(addRecoveryPoint).toHaveBeenCalledTimes(1);
114
115 // Zehn Minuten Leerlauf: Der Fuenferring darf sich nicht mit Kopien
116 // desselben Stands fuellen.
117 await warte(600_000);
118 expect(addRecoveryPoint).toHaveBeenCalledTimes(1);
119 });
120
121 it('legt nach einer echten Aenderung wieder einen Punkt an', async () => {
122 starte();
123 await warte(120_000);
124 expect(addRecoveryPoint).toHaveBeenCalledTimes(1);
125
126 aendere('Bahnhofstrasse Nord');
127 await warte(120_000);
128 expect(addRecoveryPoint).toHaveBeenCalledTimes(2);
129
130 // Und danach im Leerlauf wieder keinen.
131 await warte(240_000);
132 expect(addRecoveryPoint).toHaveBeenCalledTimes(2);
133 });
134
135 it('versucht einen gescheiterten Punkt beim naechsten Takt erneut', async () => {
136 // Ein Punkt, der nicht geschrieben werden konnte, ist nicht abgelegt - er
137 // darf den naechsten Versuch nicht sperren.
138 vi.mocked(addRecoveryPoint).mockRejectedValue(new Error('Kein Platz mehr.'));
139 starte();
140
141 await warte(120_000);
142 expect(addRecoveryPoint).toHaveBeenCalledTimes(1);
143 await warte(120_000);
144 expect(addRecoveryPoint).toHaveBeenCalledTimes(2);
145 });
146 });