lsa-planer

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

/ tests ui anlagenartWechsel.test.ts

5,9 KB Rohdatei
tests/ui/anlagenartWechsel.test.ts — 161 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { ProjectStore } from '@/app/store';
3 import { ASSISTENT_VORGABE, projektAusAngaben } from '@/ui/assistent';
4 import { ANLAGENART_GRENZEN } from '@/domain/rilsa/constants';
5 import { CURRENT_SCHEMA_VERSION, type Project } from '@/domain/model/project';
6 import { loadProject } from '@/domain/model/migrate';
7 import type { ViewDefinition } from '@/ui/shell';
8 import { projectView } from '@/ui/views/projectView';
9
10 /**
11 * Der Dialog beim Wechsel der Anlagenart muss den ZEITPUNKT der Kuerzung
12 * richtig nennen (Befund 12 zur Fassung 5.8.0).
13 *
14 * WAS DER ANWENDER SONST ERLEBT: Er stellt eine einstreifige Fuehrung mit
15 * 400 m Raeumweg auf "Fussgaengerschutzanlage" um. Der Dialog nennt als
16 * Zeitpunkt der Kuerzung nur "beim naechsten Aendern eines dieser Felder" - er
17 * fasst also kein Feld mehr an und glaubt die vermessene Laenge sicher.
18 * Gekuerzt wird aber schon beim naechsten EINLESEN: Nach einem Programmstart
19 * holt die Sitzungswiederherstellung das Projekt mit 100 m zurueck, und die
20 * Zwischenzeit steht rund 30 s zu kurz da.
21 */
22
23 const FESTES_DATUM = '2026-01-01T00:00:00.000Z';
24 const FESTES_DATUM_OBJEKT = new Date(FESTES_DATUM);
25
26 /** Einstreifige Fuehrung, alle Konfliktwege am Lageplan mit 400 m vermessen. */
27 function projektMitLangenWegen(): Project {
28 const basis = projektAusAngaben(
29 { ...ASSISTENT_VORGABE, fussgaenger: true, rad: true, linksabbieger: true },
30 FESTES_DATUM,
31 );
32 return {
33 ...basis,
34 anlagenart: 'einstreifig',
35 conflicts: basis.conflicts.map((c) => ({ ...c, clearingDistance: 400 })),
36 };
37 }
38
39 function zeichne(
40 view: ViewDefinition,
41 project: Project,
42 ): { wurzel: HTMLElement; store: ProjectStore; aufraeumen: () => void } {
43 const wurzel = document.createElement('div');
44 document.body.append(wurzel);
45 const store = new ProjectStore(project);
46 const aufraeumen = view.render(wurzel, {
47 store,
48 refresh: () => {},
49 navigate: () => {},
50 target: null,
51 });
52 return {
53 wurzel,
54 store,
55 aufraeumen: () => {
56 aufraeumen();
57 wurzel.remove();
58 },
59 };
60 }
61
62 /** Das Auswahlfeld der Anlagenart in der Projektansicht. */
63 function auswahl(wurzel: ParentNode): HTMLSelectElement {
64 const label = [...wurzel.querySelectorAll('label')].find(
65 (l) => (l.textContent ?? '').trim() === 'Anlagenart',
66 );
67 const feld = wurzel.querySelector<HTMLSelectElement>(`#${label?.htmlFor ?? ''}`);
68 expect(feld, 'Kein Auswahlfeld "Anlagenart" in der Projektansicht').not.toBeNull();
69 return feld as HTMLSelectElement;
70 }
71
72 function knopfMit(text: string): HTMLButtonElement | undefined {
73 return [...document.querySelectorAll('button')].find(
74 (b) => (b.textContent ?? '').trim() === text,
75 );
76 }
77
78 /** Laesst die Warteschlange der Mikroaufgaben leerlaufen. */
79 async function abwarten(): Promise<void> {
80 await new Promise((fertig) => setTimeout(fertig, 0));
81 }
82
83 /**
84 * Das Projekt so einlesen, wie es die Sitzungswiederherstellung tut: ueber die
85 * gespeicherte Rohgestalt und `loadProject`. Kein Feld wird dabei angefasst.
86 */
87 function wiedereingelesen(project: Project): ReturnType<typeof loadProject> {
88 const roh: unknown = JSON.parse(
89 JSON.stringify({ ...project, schemaVersion: CURRENT_SCHEMA_VERSION }),
90 );
91 return loadProject(roh, FESTES_DATUM_OBJEKT);
92 }
93
94 afterEach(() => {
95 // Dialoge und Kurzmeldungen haengen an document.body, nicht an der Ansicht.
96 document.body.replaceChildren();
97 });
98
99 describe('Anlagenartwechsel: der Dialog nennt den Zeitpunkt der Kuerzung', () => {
100 it('nennt das naechste Oeffnen des Projekts und die Sitzungswiederherstellung, nicht nur das Aendern eines Feldes', async () => {
101 const { wurzel, aufraeumen } = zeichne(projectView, projektMitLangenWegen());
102 try {
103 const feld = auswahl(wurzel);
104 feld.value = 'fussgaengerschutzanlage';
105 feld.dispatchEvent(new Event('change'));
106
107 const text = document.querySelector('.dialog')?.textContent ?? '';
108 expect(text, 'Die Umstellung geschah ohne Rueckfrage').not.toBe('');
109 // Die betroffenen Zahlen stehen weiterhin im Dialog.
110 expect(text).toContain('400');
111 expect(text).toContain(
112 String(ANLAGENART_GRENZEN.fussgaengerschutzanlage.maxClearingDistance),
113 );
114
115 // Der eigentliche Punkt: Gekuerzt wird beim Einlesen, also auch ohne
116 // jede Bedienhandlung. Steht das nicht da, verlaesst sich der Bearbeiter
117 // darauf, dass die vermessenen 400 m bleiben, solange er sie in Ruhe
118 // laesst.
119 expect(
120 text,
121 'Der Dialog nennt das Oeffnen des Projekts nicht als Zeitpunkt der Kuerzung',
122 ).toContain('nächsten Öffnen');
123 expect(
124 text,
125 'Der Dialog nennt die Sitzungswiederherstellung nicht, obwohl sie ebenso kuerzt',
126 ).toContain('Sitzung');
127
128 knopfMit('Umstellen')?.click();
129 await abwarten();
130 } finally {
131 aufraeumen();
132 }
133 });
134
135 it('kuerzt den vermessenen Weg tatsaechlich schon beim Wiedereinlesen, ohne dass ein Feld angefasst wurde', async () => {
136 const { wurzel, store, aufraeumen } = zeichne(projectView, projektMitLangenWegen());
137 try {
138 const feld = auswahl(wurzel);
139 feld.value = 'fussgaengerschutzanlage';
140 feld.dispatchEvent(new Event('change'));
141 knopfMit('Umstellen')?.click();
142 await abwarten();
143
144 // Im Speicher steht weiterhin die vermessene Laenge - so weit haelt die
145 // Zusage des Dialogs.
146 const umgestellt = store.getProject();
147 expect(umgestellt.anlagenart).toBe('fussgaengerschutzanlage');
148 expect(umgestellt.conflicts[0]?.clearingDistance).toBe(400);
149
150 // Nach dem Wiedereinlesen nicht mehr. Das ist der Zeitpunkt, den der
151 // Dialog nennen muss.
152 const ergebnis = wiedereingelesen(umgestellt);
153 expect(ergebnis.project.conflicts[0]?.clearingDistance).toBe(
154 ANLAGENART_GRENZEN.fussgaengerschutzanlage.maxClearingDistance,
155 );
156 expect(ergebnis.issues.some((i) => i.message.includes('Räumweg'))).toBe(true);
157 } finally {
158 aufraeumen();
159 }
160 });
161 });