lsa-planer

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

/ tests ui zeitdiagrammBreite.test.ts

8,2 KB Rohdatei
tests/ui/zeitdiagrammBreite.test.ts — 212 Zeilen
1 import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2 import { ProjectStore } from '@/app/store';
3 import { createStandardIntersectionProject } from '@/domain/model/factory';
4 import { MINDEST_ZEICHENBREITE } from '@/render/signalPlanDrawing';
5 import type { Project } from '@/domain/model/project';
6 import type { ViewDefinition } from '@/ui/shell';
7 import { planView } from '@/ui/views/planView';
8 import { simulationView } from '@/ui/views/simulationView';
9
10 /**
11 * Befunde 60 und 57 zur Fassung 5.10.0 - die Breite des Zeitdiagramms.
12 *
13 * BEFUND 60. Die Zeichenbreite war
14 * `Math.max(MINDEST_ZEICHENBREITE, frame.clientWidth - 2, 640 * zoom)`. Der
15 * Vergroesserungsfaktor wirkte erst, wenn `640 * zoom` die eingepasste Breite
16 * uebersteigt. Beim Vorgabefenster 1440 x 920 bleibt der Rahmen rund 1125
17 * Bildpunkte breit; der erste Druck auf "+" (640 x 1,5 = 960) war damit ohne
18 * jede Wirkung, maximiert auf 1920 sogar die ersten drei. Der Knopf blieb
19 * bedienbar, das Bild Bildpunkt fuer Bildpunkt dasselbe, eine Rueckmeldung gab
20 * es nicht. Die Zahl 640 ist an keiner Stelle des Projekts begruendet.
21 *
22 * BEFUND 57. Die Simulation zeichnete nur aus `tick`, `stop`, "Einen Umlauf",
23 * "Zurücksetzen", dem Haken und einmal beim Aufbau - kein Beobachter der
24 * Rahmengroesse. Da die Leinwand ihre Breite als feste Bildpunktzahl traegt
25 * und `.plan-rahmen` waagerecht rollt, bekam der STEHENDE Plan beim
26 * Verkleinern eine Rollleiste, obwohl er passte, und beim Vergroessern blieb
27 * rechts Weissraum. Gerade der stehende Stand ist der, den man abliest; im
28 * Lauf heilt es das naechste Einzelbild. Der Signalzeitenplan macht es seit
29 * jeher richtig (ResizeObserver am Rahmen, abgemeldet im Rueckgabewert).
30 */
31
32 const STICHTAG = new Date('2026-01-01T00:00:00Z');
33
34 /** Aufzeichnende Ersatzausfuehrung - jsdom bringt keinen ResizeObserver mit. */
35 class ProbeResizeObserver {
36 static readonly angemeldet: ProbeResizeObserver[] = [];
37 readonly ziele: Element[] = [];
38 abgemeldet = false;
39
40 constructor(private readonly rueckmeldung: () => void) {
41 ProbeResizeObserver.angemeldet.push(this);
42 }
43
44 observe(ziel: Element): void {
45 this.ziele.push(ziel);
46 }
47
48 disconnect(): void {
49 this.abgemeldet = true;
50 }
51
52 /** Meldet eine Groessenaenderung, wie es der Browser taete. */
53 melde(): void {
54 this.rueckmeldung();
55 }
56 }
57
58 type MitBeobachter = { ResizeObserver?: unknown };
59
60 let aufraeumen: (() => void) | null = null;
61
62 beforeEach(() => {
63 ProbeResizeObserver.angemeldet.length = 0;
64 (globalThis as MitBeobachter).ResizeObserver = ProbeResizeObserver;
65 });
66
67 afterEach(() => {
68 aufraeumen?.();
69 aufraeumen = null;
70 delete (globalThis as MitBeobachter).ResizeObserver;
71 document.body.replaceChildren();
72 });
73
74 function zeichne(view: ViewDefinition, project: Project): HTMLElement {
75 const wurzel = document.createElement('div');
76 document.body.append(wurzel);
77 const schliessen = view.render(wurzel, {
78 store: new ProjectStore(project),
79 refresh: () => {},
80 navigate: () => {},
81 target: null,
82 });
83 aufraeumen = () => {
84 schliessen();
85 wurzel.remove();
86 };
87 return wurzel;
88 }
89
90 /**
91 * Setzt die Breite des Rahmens. jsdom rechnet keine Darstellung und meldet
92 * durchweg 0 - ohne diese Vorgabe fiele jede Zeichnung auf die Mindestbreite
93 * zurueck, und genau die Lage, um die es geht (Rahmen breiter als 640 x zoom),
94 * kaeme nie zustande.
95 */
96 function setzeRahmenbreite(wurzel: HTMLElement, breite: number): HTMLElement {
97 const rahmen = wurzel.querySelector<HTMLElement>('.plan-rahmen');
98 expect(rahmen, 'Kein Rahmen .plan-rahmen').not.toBeNull();
99 Object.defineProperty(rahmen, 'clientWidth', { value: breite, configurable: true });
100 return rahmen as HTMLElement;
101 }
102
103 /** Zuletzt gezeichnete Breite der Leinwand in Bildpunkten. */
104 function leinwandbreite(wurzel: HTMLElement): number {
105 const leinwand = wurzel.querySelector<HTMLCanvasElement>('canvas.plan-leinwand');
106 expect(leinwand, 'Keine Leinwand').not.toBeNull();
107 return Number.parseFloat((leinwand as HTMLCanvasElement).style.width);
108 }
109
110 function knopf(wurzel: HTMLElement, name: string): HTMLButtonElement {
111 const treffer = [...wurzel.querySelectorAll('button')].find(
112 (b) => b.getAttribute('aria-label') === name || b.textContent?.trim() === name,
113 );
114 expect(treffer, `Keine Schaltflaeche "${name}"`).toBeDefined();
115 return treffer as HTMLButtonElement;
116 }
117
118 /** Wartet den naechsten Bilddurchlauf ab. */
119 const naechstesBild = (): Promise<void> =>
120 new Promise((fertig) => {
121 requestAnimationFrame(() => {
122 fertig();
123 });
124 });
125
126 describe('Befund 60 - jede Stufe von "Vergrößern" wirkt', () => {
127 it('vergroessert vom eingepassten Bild aus, nicht von 640 Bildpunkten', () => {
128 const wurzel = zeichne(planView, createStandardIntersectionProject('Zoom', STICHTAG));
129 // Rahmen des Vorgabefensters 1440 x 920: rund 1200 Bildpunkte.
130 setzeRahmenbreite(wurzel, 1200);
131 knopf(wurzel, 'Einpassen').click();
132 const eingepasst = leinwandbreite(wurzel);
133 expect(eingepasst).toBe(1198);
134
135 knopf(wurzel, 'Vergrößern').click();
136 expect(leinwandbreite(wurzel), 'Der erste Druck blieb ohne Wirkung').toBe(1198 * 1.5);
137
138 knopf(wurzel, 'Vergrößern').click();
139 expect(leinwandbreite(wurzel)).toBe(1198 * 2);
140
141 knopf(wurzel, 'Verkleinern').click();
142 expect(leinwandbreite(wurzel)).toBe(1198 * 1.5);
143 });
144
145 it('laesst "Einpassen" die Ausgangslage bleiben', () => {
146 const wurzel = zeichne(planView, createStandardIntersectionProject('Zoom', STICHTAG));
147 setzeRahmenbreite(wurzel, 1200);
148 knopf(wurzel, 'Vergrößern').click();
149 knopf(wurzel, 'Einpassen').click();
150 expect(leinwandbreite(wurzel)).toBe(1198);
151 });
152
153 it('haelt die Mindestbreite auch im schmalen Fenster ein', () => {
154 const wurzel = zeichne(planView, createStandardIntersectionProject('Zoom', STICHTAG));
155 // Schmalster zulaessiger Rahmen: Die Untergrenze greift, und ab der ersten
156 // Stufe wirkt jeder Druck - das war schon bisher so und bleibt es.
157 setzeRahmenbreite(wurzel, 300);
158 knopf(wurzel, 'Einpassen').click();
159 expect(leinwandbreite(wurzel)).toBe(MINDEST_ZEICHENBREITE);
160 knopf(wurzel, 'Vergrößern').click();
161 expect(leinwandbreite(wurzel)).toBe(MINDEST_ZEICHENBREITE * 1.5);
162 });
163 });
164
165 describe('Befund 57 - das Zeitdiagramm der Simulation folgt der Fenstergroesse', () => {
166 it('zeichnet den stehenden Plan in der neuen Breite neu', async () => {
167 const wurzel = zeichne(simulationView, createStandardIntersectionProject('Groesse', STICHTAG));
168 const rahmen = setzeRahmenbreite(wurzel, 1200);
169
170 const beobachter = ProbeResizeObserver.angemeldet.at(-1);
171 expect(beobachter, 'Kein ResizeObserver angemeldet').toBeDefined();
172 expect(beobachter?.ziele).toContain(rahmen);
173
174 beobachter?.melde();
175 await naechstesBild();
176 expect(leinwandbreite(wurzel)).toBe(1198);
177
178 setzeRahmenbreite(wurzel, 800);
179 beobachter?.melde();
180 await naechstesBild();
181 expect(leinwandbreite(wurzel)).toBe(798);
182 });
183
184 it('zeichnet nicht neu, wenn sich die Breite nicht geaendert hat', async () => {
185 const wurzel = zeichne(simulationView, createStandardIntersectionProject('Groesse', STICHTAG));
186 setzeRahmenbreite(wurzel, 1200);
187 const beobachter = ProbeResizeObserver.angemeldet.at(-1);
188 expect(beobachter, 'Kein ResizeObserver angemeldet').toBeDefined();
189 beobachter?.melde();
190 await naechstesBild();
191
192 const leinwand = wurzel.querySelector<HTMLCanvasElement>('canvas.plan-leinwand');
193 leinwand?.setAttribute('data-probe', 'unveraendert');
194 leinwand!.style.width = '4711px';
195 beobachter?.melde();
196 await naechstesBild();
197 // Unveraendert: Ohne die Abfrage auf eine wirklich geaenderte Breite loeste
198 // jeder Zeichenvorgang die naechste Groessenmeldung aus.
199 expect(leinwandbreite(wurzel)).toBe(4711);
200 });
201
202 it('meldet den Beobachter beim Verlassen der Ansicht ab', () => {
203 zeichne(simulationView, createStandardIntersectionProject('Groesse', STICHTAG));
204 const beobachter = ProbeResizeObserver.angemeldet.at(-1);
205 expect(beobachter?.abgemeldet).toBe(false);
206 aufraeumen?.();
207 aufraeumen = null;
208 // Sonst zeichnet die verlassene Ansicht bei jeder Fenstergroessenaenderung
209 // weiter - dieselbe Zusage gibt der Signalzeitenplan.
210 expect(beobachter?.abgemeldet).toBe(true);
211 });
212 });