lsa-planer

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

/ tests ui hintergrundLadevorgang.test.ts

6,2 KB Rohdatei
tests/ui/hintergrundLadevorgang.test.ts — 208 Zeilen
1 import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
2 import * as actions from '@/app/actions';
3 import { ProjectStore } from '@/app/store';
4 import type { Lageplan } from '@/domain/geometrie/lageplan';
5 import { ASSISTENT_VORGABE, projektAusAngaben } from '@/ui/assistent';
6 import { lageplanView } from '@/ui/views/lageplanView';
7
8 /**
9 * Strittiger Befund S3 zur Fassung 5.9.0 (Lebensdauer der Ansicht).
10 *
11 * WAS DER ANWENDER SONST ERLEBT
12 *
13 * Er schlaegt die Ansicht "Lageplan" mit einem grossen eingebetteten
14 * Hintergrundbild auf und wechselt, waehrend das Bild noch dekodiert wird, in
15 * eine andere Ansicht. Der Ladevorgang laeuft weiter: `onload` zeichnet die
16 * verlassene Ansicht zu Ende, und `onerror` schiebt ueber das globale `notify`
17 * eine Fehlermeldung in die Ansicht, die inzwischen offensteht - Fehler laufen
18 * nicht ab, sie bleibt also dort stehen.
19 *
20 * Die Aufraeumfunktion sagt genau das Gegenteil zu ("Alle Empfaenger abmelden -
21 * sonst zeichnet die verlassene Ansicht weiter"), meldete den Bildlader aber
22 * als einzigen nicht ab. Im selben Sachgebiet wird derselbe Fall bewusst
23 * anders behandelt: `src/ui/lageplan/luftbild.ts` fuehrt fuer den Kachelabruf
24 * ein Abbruchmerkmal, "damit ein noch laufender Abruf mitbekommt, dass niemand
25 * mehr auf ihn wartet".
26 *
27 * Geprueft wird an der Zusage, nicht an der Wahrscheinlichkeit des
28 * Zusammentreffens: Der Bildlader wird hier von Hand gesteuert, weil jsdom
29 * keine Daten-URL abholt.
30 */
31
32 const FESTES_DATUM = '2026-01-01T00:00:00.000Z';
33
34 /**
35 * Bildelement, das erst meldet, wenn der Prueffall es sagt.
36 *
37 * Die uebrigen Ansichts-Tests ersetzen `Image` durch einen Lader, der beim
38 * Setzen von `src` SOFORT meldet - damit umgehen sie den Wettlauf, um den es
39 * hier gerade geht.
40 */
41 class SchwebendesBild {
42 static readonly angelegte: SchwebendesBild[] = [];
43 onload: (() => void) | null = null;
44 onerror: (() => void) | null = null;
45 naturalWidth = 800;
46 naturalHeight = 600;
47
48 constructor() {
49 SchwebendesBild.angelegte.push(this);
50 }
51
52 set src(_wert: string) {
53 /* Nichts - der Ladevorgang schwebt, bis der Prueffall ihn beendet. */
54 }
55 }
56
57 /** Zeichenkontext, der nur zaehlt, wie oft gemalt wurde. */
58 class Zaehler {
59 anweisungen = 0;
60 strokeStyle = '';
61 fillStyle = '';
62 lineWidth = 0;
63 lineCap = 'butt';
64 lineJoin = 'miter';
65 globalAlpha = 1;
66 font = '';
67 textAlign = 'left';
68 imageSmoothingQuality = 'low';
69
70 save(): void {}
71 restore(): void {}
72 clearRect(): void {}
73 fillRect(): void {}
74 strokeRect(): void {}
75 drawImage(): void {}
76 beginPath(): void {}
77 moveTo(): void {}
78 lineTo(): void {}
79 arc(): void {}
80 fill(): void {}
81 fillText(): void {}
82 stroke(): void {}
83 setLineDash(): void {}
84 measureText(): { width: number } {
85 return { width: 24 };
86 }
87
88 setTransform(): void {
89 // `male()` beginnt jedes Zeichnen damit - genau einmal je Durchgang.
90 this.anweisungen += 1;
91 }
92 }
93
94 const stift = new Zaehler();
95
96 const urspruenglichesPath2D = (globalThis as { Path2D?: unknown }).Path2D;
97 const urspruenglichesImage = (globalThis as { Image?: unknown }).Image;
98
99 beforeAll(() => {
100 (globalThis as { Path2D?: unknown }).Path2D = class {
101 moveTo(): void {}
102 lineTo(): void {}
103 };
104 (globalThis as { Image?: unknown }).Image = SchwebendesBild;
105 vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockImplementation(
106 () => stift as unknown as CanvasRenderingContext2D,
107 );
108 });
109
110 afterAll(() => {
111 (globalThis as { Path2D?: unknown }).Path2D = urspruenglichesPath2D;
112 (globalThis as { Image?: unknown }).Image = urspruenglichesImage;
113 });
114
115 afterEach(() => {
116 document.body.replaceChildren();
117 SchwebendesBild.angelegte.length = 0;
118 stift.anweisungen = 0;
119 });
120
121 function planMitBild(): Lageplan {
122 return {
123 arbeitsbereiche: [],
124 signalgeber: [],
125 bild: {
126 datenUrl: 'data:image/png;base64,iVBORw0KGgo=',
127 breite: 800,
128 hoehe: 600,
129 herkunft: 'Prüfbild',
130 geladenAm: FESTES_DATUM,
131 },
132 kalibrierung: {
133 von: { x: 0, y: 0 },
134 bis: { x: 100, y: 0 },
135 laengeMeter: 20,
136 herkunft: 'gemessen',
137 },
138 haltlinien: [],
139 linien: [],
140 };
141 }
142
143 /** Baut die Ansicht auf und gibt die Aufraeumfunktion zurueck. */
144 function zeichne(): () => void {
145 const store = new ProjectStore(projektAusAngaben({ ...ASSISTENT_VORGABE }, FESTES_DATUM));
146 store.update((p) => actions.setLageplan(p, planMitBild()), { label: 'Pruefplan' });
147
148 const wurzel = document.createElement('div');
149 document.body.append(wurzel);
150 const schliessen = lageplanView.render(wurzel, {
151 store,
152 refresh: () => {},
153 navigate: () => {},
154 target: null,
155 });
156 return () => {
157 schliessen();
158 wurzel.remove();
159 };
160 }
161
162 function schwebenderLader(): SchwebendesBild {
163 const bild = SchwebendesBild.angelegte[SchwebendesBild.angelegte.length - 1];
164 if (bild === undefined) throw new Error('Die Ansicht hat keinen Bildladevorgang begonnen');
165 return bild;
166 }
167
168 describe('Lageplan: der schwebende Bildladevorgang wird beim Verlassen der Ansicht entwertet', () => {
169 it('meldet nach dem Ansichtswechsel keinen Bildfehler mehr in die neue Ansicht', () => {
170 const aufraeumen = zeichne();
171 const lader = schwebenderLader();
172
173 // Der Anwender wechselt die Ansicht, bevor das Bild fertig ist.
174 aufraeumen();
175 lader.onerror?.();
176
177 const meldungen = [...document.querySelectorAll('.meldungen .meldung')].map(
178 (m) => m.textContent ?? '',
179 );
180 expect(
181 meldungen.filter((t) => t.includes('konnte nicht dargestellt werden')),
182 'die verlassene Ansicht meldet in die neue',
183 ).toEqual([]);
184 });
185
186 it('zeichnet nach dem Ansichtswechsel nicht weiter', () => {
187 const aufraeumen = zeichne();
188 const lader = schwebenderLader();
189
190 aufraeumen();
191 const vorher = stift.anweisungen;
192 lader.onload?.();
193
194 expect(stift.anweisungen, 'die verlassene Ansicht zeichnet weiter').toBe(vorher);
195 });
196
197 it('laesst den Ladevorgang wirken, solange die Ansicht steht', () => {
198 const aufraeumen = zeichne();
199 try {
200 const lader = schwebenderLader();
201 const vorher = stift.anweisungen;
202 lader.onload?.();
203 expect(stift.anweisungen, 'das geladene Bild wird nicht gezeichnet').toBeGreaterThan(vorher);
204 } finally {
205 aufraeumen();
206 }
207 });
208 });