lsa-planer

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

/ tests ui stilleErsetzungenOberflaeche.test.ts

9,9 KB Rohdatei
tests/ui/stilleErsetzungenOberflaeche.test.ts — 248 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { ProjectStore } from '@/app/store';
3 import { buildSignalPlan } from '@/domain/plan/signalPlan';
4 import { createStandardIntersectionProject } from '@/domain/model/factory';
5 import { ANLAGENART_GRENZEN } from '@/domain/rilsa/constants';
6 import type { Project } from '@/domain/model/project';
7 import type { ViewDefinition } from '@/ui/shell';
8 import { HILFE } from '@/ui/help/texte';
9 import { ERSATZWERT_ERLAEUTERUNG, planView } from '@/ui/views/planView';
10 import { phasesView } from '@/ui/views/phasesView';
11 import { projectView } from '@/ui/views/projectView';
12 import { settingsView } from '@/ui/views/settingsView';
13 import { signalGroupsView } from '@/ui/views/signalGroupsView';
14
15 /**
16 * Die stillen Ersetzungen an der Oberflaeche:
17 *
18 * - Vorgaben-Ansicht: Eine Vorgabe ausserhalb der Schranken steht nicht nur
19 * im Pruefbericht - der Balken oben nennt die Meldung, "wirksam N s" steht
20 * neben dem Feld (wie an der Mindestfreigabezeit, Befund C9).
21 * - Projekt-Ansicht: Der Dialog vor dem Anlagenart-Wechsel nennt auch die
22 * Umlaufzeit-Vorgaben, die durch den Wechsel zum Fehler mit Sperre werden.
23 * - Signalzeitenplan und Phasen: Der Ersatzwert ist "die groesste zulaessige
24 * Umlaufzeit", nicht "die Obergrenze der Anlagenart" - eine engere eigene
25 * Vorgabe gilt ebenso.
26 * - Signalgruppen: Schwerverkehrsanteil bis 100 %, wie Schema und Rechnung.
27 *
28 * Gegen den Altstand schlagen die Faelle fehl: kein Balken, kein "wirksam",
29 * kein Satz zur Vorgabe im Dialog, "Obergrenze der Anlagenart", max 90.
30 */
31
32 const FESTES_DATUM = new Date('2026-01-01T00:00:00Z');
33
34 function zeichne(
35 view: ViewDefinition,
36 project: Project,
37 ): { wurzel: HTMLElement; store: ProjectStore; aufraeumen: () => void } {
38 const wurzel = document.createElement('div');
39 document.body.append(wurzel);
40 const store = new ProjectStore(project);
41 const aufraeumen = view.render(wurzel, {
42 store,
43 refresh: () => {},
44 navigate: () => {},
45 target: null,
46 });
47 return {
48 wurzel,
49 store,
50 aufraeumen: () => {
51 aufraeumen();
52 wurzel.remove();
53 },
54 };
55 }
56
57 function mitVorgabe(
58 anlagenart: Project['anlagenart'],
59 rilsa: Project['settings']['rilsa'],
60 ): Project {
61 const roh = createStandardIntersectionProject('Vorgaben', FESTES_DATUM);
62 return {
63 ...roh,
64 anlagenart,
65 settings: { ...roh.settings, rilsa: { ...roh.settings.rilsa, ...rilsa } },
66 };
67 }
68
69 function uebersaettigt(rilsa: Project['settings']['rilsa'] = {}): Project {
70 const p = createStandardIntersectionProject('Ersatzwert', FESTES_DATUM);
71 return {
72 ...p,
73 settings: { ...p.settings, rilsa: { ...p.settings.rilsa, ...rilsa } },
74 demands: p.demands.map((d) => ({ ...d, volume: 1500 })),
75 };
76 }
77
78 /** Zeile der Vorgabentabelle zu einem Kennwert. */
79 function zeileZu(wurzel: ParentNode, label: string): HTMLTableRowElement {
80 const zeile = [...wurzel.querySelectorAll<HTMLTableRowElement>('tbody tr')].find(
81 (tr) => tr.children[0]?.textContent?.trim() === label,
82 );
83 if (!zeile) throw new Error(`Keine Zeile "${label}"`);
84 return zeile;
85 }
86
87 function knopfMit(text: string): HTMLButtonElement | undefined {
88 return [...document.querySelectorAll('button')].find(
89 (b) => (b.textContent ?? '').trim() === text,
90 );
91 }
92
93 async function abwarten(): Promise<void> {
94 await new Promise((fertig) => setTimeout(fertig, 0));
95 }
96
97 afterEach(() => {
98 document.body.replaceChildren();
99 });
100
101 describe('Vorgaben: unzulaessige Vorgabe sichtbar an Ort und Stelle', () => {
102 it('nennt am Knotenpunkt bei "Groesste Umlaufzeit 250 s" die Meldung im Balken und "wirksam 120 s" am Feld', () => {
103 const p = mitVorgabe('knotenpunkt', { cycleTimeMax: 250 });
104 const { wurzel, aufraeumen } = zeichne(settingsView, p);
105 try {
106 const balken = wurzel.querySelector('.hinweisbalken-fehler');
107 expect(balken, 'Kein Fehlerbalken fuer die unzulaessige Vorgabe').not.toBeNull();
108 expect(balken!.textContent).toContain('1 Vorgabe wird nicht wie eingetragen angesetzt');
109 expect(balken!.textContent).toContain('"Größte Umlaufzeit" wurde auf 250 s gesetzt');
110 expect(balken!.textContent).toContain('höchstens 120 s');
111
112 const zeile = zeileZu(wurzel, 'Größte Umlaufzeit');
113 const feld = zeile.querySelector('input')!;
114 // Der eingetragene Wert bleibt stehen - nichts wird stumm ueberschrieben.
115 expect(feld.value).toBe('250');
116 expect(zeile.textContent).toContain('wirksam 120 s');
117 } finally {
118 aufraeumen();
119 }
120 });
121
122 it('zeigt bei einstreifiger Fuehrung mit 250 s weder Balken noch "wirksam"', () => {
123 const p = mitVorgabe('einstreifig', { cycleTimeMax: 250 });
124 expect(ANLAGENART_GRENZEN.einstreifig.cycleTime.max).toBeGreaterThanOrEqual(250);
125 const { wurzel, aufraeumen } = zeichne(settingsView, p);
126 try {
127 expect(wurzel.querySelector('.hinweisbalken-fehler')).toBeNull();
128 expect(wurzel.textContent).not.toContain('nicht wie eingetragen angesetzt');
129 expect(zeileZu(wurzel, 'Größte Umlaufzeit').textContent).not.toContain('wirksam');
130 } finally {
131 aufraeumen();
132 }
133 });
134
135 it('nennt "wirksam" nur an dem Feld, das abweicht', () => {
136 const p = mitVorgabe('knotenpunkt', { cycleTimeMax: 250, cycleTimeMin: 40 });
137 const { wurzel, aufraeumen } = zeichne(settingsView, p);
138 try {
139 expect(zeileZu(wurzel, 'Größte Umlaufzeit').textContent).toContain('wirksam 120 s');
140 expect(zeileZu(wurzel, 'Kleinste Umlaufzeit').textContent).not.toContain('wirksam');
141 } finally {
142 aufraeumen();
143 }
144 });
145 });
146
147 describe('Projekt: Anlagenart-Wechsel warnt vor Vorgaben, die zum Fehler werden', () => {
148 function auswahl(wurzel: ParentNode): HTMLSelectElement {
149 const label = [...wurzel.querySelectorAll('label')].find(
150 (l) => (l.textContent ?? '').trim() === 'Anlagenart',
151 );
152 const feld = wurzel.querySelector<HTMLSelectElement>(`#${label?.htmlFor ?? ''}`);
153 expect(feld, 'Kein Auswahlfeld "Anlagenart"').not.toBeNull();
154 return feld as HTMLSelectElement;
155 }
156
157 it('nennt beim Wechsel einstreifig -> Knotenpunkt die groesste Umlaufzeit von 250 s', async () => {
158 const p = mitVorgabe('einstreifig', { cycleTimeMax: 250 });
159 const { wurzel, store, aufraeumen } = zeichne(projectView, p);
160 try {
161 const feld = auswahl(wurzel);
162 feld.value = 'knotenpunkt';
163 feld.dispatchEvent(new Event('change'));
164 const dialog = document.querySelector('.dialog');
165 expect(dialog, 'Die Umstellung geschah ohne Rueckfrage').not.toBeNull();
166 const text = dialog?.textContent ?? '';
167 expect(text).toContain('„Größte Umlaufzeit“ von 250 s');
168 expect(text).toContain('30 bis 120 s');
169 expect(text).toContain('gerechnet wird mit 120 s');
170 expect(text).toContain('Export ist gesperrt');
171
172 knopfMit('Anlagenart behalten')?.click();
173 await abwarten();
174 expect(store.getProject().anlagenart).toBe('einstreifig');
175 expect(store.getProject().settings.rilsa.cycleTimeMax).toBe(250);
176 } finally {
177 aufraeumen();
178 }
179 });
180
181 it('fragt nicht nach, wenn die Vorgabe schon jetzt ausserhalb liegt oder nach dem Wechsel passt', async () => {
182 // Knotenpunkt mit 250 s ist bereits ein Fehler; einstreifig nimmt 250 s an.
183 const p = mitVorgabe('knotenpunkt', { cycleTimeMax: 250 });
184 const { wurzel, store, aufraeumen } = zeichne(projectView, p);
185 try {
186 const feld = auswahl(wurzel);
187 feld.value = 'einstreifig';
188 feld.dispatchEvent(new Event('change'));
189 await abwarten();
190 expect(document.querySelector('.dialog')).toBeNull();
191 expect(store.getProject().anlagenart).toBe('einstreifig');
192 } finally {
193 aufraeumen();
194 }
195 });
196 });
197
198 describe('Ersatzwert: "groesste zulaessige Umlaufzeit", nicht "Obergrenze der Anlagenart"', () => {
199 it('sagt es in der Fussnote der Kennzahl und im Phasen-Hinweis - mit EINEM Text', () => {
200 // Knotenpunkt mit engerer Vorgabe 90 s: der Ersatzwert ist 90 s, nicht 120 s.
201 const p = uebersaettigt({ cycleTimeMax: 90 });
202 const plan = buildSignalPlan(p);
203 expect(plan.cycleResult?.bounded, 'Vorbedingung').toBe('uebersaettigt');
204 expect(plan.cycleTime).toBe(90);
205 expect(ERSATZWERT_ERLAEUTERUNG).toContain('größte zulässige Umlaufzeit');
206 expect(ERSATZWERT_ERLAEUTERUNG).toContain('engere eigene Vorgabe');
207 expect(ERSATZWERT_ERLAEUTERUNG).not.toContain('Obergrenze der Anlagenart');
208
209 const plan1 = zeichne(planView, p);
210 try {
211 const fussnote = plan1.wurzel.querySelector('.kennzahl-fussnote');
212 expect(fussnote?.textContent).toBe(ERSATZWERT_ERLAEUTERUNG);
213 expect(plan1.wurzel.textContent).not.toContain('Obergrenze der Anlagenart');
214 } finally {
215 plan1.aufraeumen();
216 }
217 const phasen = zeichne(phasesView, p);
218 try {
219 expect(phasen.wurzel.textContent).toContain(ERSATZWERT_ERLAEUTERUNG);
220 expect(phasen.wurzel.textContent).not.toContain('Obergrenze der Anlagenart');
221 } finally {
222 phasen.aufraeumen();
223 }
224 });
225
226 it('sagt es auch in der Hilfe - und nennt die Zeichnung als Ausgabe mit Vermerk', () => {
227 expect(HILFE.umlaufzeit.woher).toContain('größte zulässige Umlaufzeit');
228 expect(HILFE.umlaufzeit.woher).toContain('engere eigene Vorgabe');
229 expect(HILFE.umlaufzeit.woher).toContain('die Zeichnung');
230 expect(HILFE.umlaufzeit.woher).not.toContain('Obergrenze der Anlagenart');
231 });
232 });
233
234 describe('Signalgruppen: Schwerverkehrsanteil bis 100 %', () => {
235 it('laesst das Feld bis 100 % zu - wie Schema und Rechnung', () => {
236 const p = createStandardIntersectionProject('SV', FESTES_DATUM);
237 const { wurzel, aufraeumen } = zeichne(signalGroupsView, p);
238 try {
239 const felder = [...wurzel.querySelectorAll<HTMLInputElement>('input[type="number"]')].filter(
240 (f) => f.getAttribute('max') === '100' && f.getAttribute('min') === '0',
241 );
242 expect(felder.length).toBeGreaterThan(0);
243 expect(wurzel.querySelector('input[max="90"]')).toBeNull();
244 } finally {
245 aufraeumen();
246 }
247 });
248 });