lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests services wiederherstellungspunkteImLeerlauf.test.ts
| 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 | }); |