lsa-planer

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

/ tests ui vorgabenRegelwert.test.ts

13,4 KB Rohdatei
tests/ui/vorgabenRegelwert.test.ts — 319 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { ProjectStore } from '@/app/store';
3 import { createStandardIntersectionProject } from '@/domain/model/factory';
4 import { buildSignalPlan } from '@/domain/plan/signalPlan';
5 import { ANLAGENART_GRENZEN, RILSA_DEFAULTS } from '@/domain/rilsa/constants';
6 import { resolveRilsaSettings, settingFields } from '@/domain/rilsa/settings';
7 import { fundstellenkennwerte } from '@/services/export/bewertung';
8 import { settingsView } from '@/ui/views/settingsView';
9 import type { Anlagenart, Project } from '@/domain/model/project';
10 import type { RilsaOverrides } from '@/domain/rilsa/settings';
11
12 /*
13 * Der letzte offene Rest von Befund 48 (tests/domain/regelbereichBezug.test.ts)
14 * und seiner ersten Behebung (tests/export/regelbereichFundstelle.test.ts).
15 *
16 * Die Abweichungsliste misst die beiden Regelbereichsfelder der Umlaufzeit
17 * seit Befund 48 gegen den Wert, der OHNE Eintrag fuer die ANLAGENART gilt, und
18 * das Fundstellenverzeichnis des Ausdrucks seit der ersten Behebung
19 * ebenfalls. Die VORGABENANSICHT am Bildschirm tat es nicht: Spalte
20 * "Regelwert", Platzhalter des leeren Feldes und die Sichtbarkeit des Knopfes
21 * "Zuruecksetzen" lasen `field.standard` aus `settingFields`, also den
22 * Knotenpunktwert.
23 *
24 * WAS DER ANWENDER SONST ERLEBT - an einer Fussgaengerschutzanlage, deren
25 * Regelbereich 40 bis 70 s betraegt und mit der der Plan auch rechnet:
26 *
27 * (a) Die Spalte "Regelwert" nennt 60 s und der Platzhalter des leeren
28 * Feldes ebenfalls - beides fuer einen Wert, den weder der Plan noch die
29 * Abweichungsliste noch der Ausdruck kennen. Wer den Platzhalter fuer den
30 * geltenden Wert haelt und ihn eintraegt, verschiebt den Regelbereich von
31 * 40 auf 60 s, ohne es zu merken.
32 * (b) Der Knopf "Zuruecksetzen" stand an der falschen Zeile: bei einem
33 * Eintrag von 40 s - dem Wert, der dort ohnehin gilt - erschien er, bei
34 * einem Eintrag von 60 s, der den Regelbereich wirklich verschiebt,
35 * nicht. Genau dort, wo etwas zurueckzunehmen war, fehlte er.
36 *
37 * DIESE WACHE HAELT DIE AUSGABEN GEGENEINANDER; ihr Auseinanderlaufen ist der
38 * Befund. Geprueft wird die Ansicht deshalb nicht gegen eine hier
39 * hingeschriebene Zahl, sondern gegen `plan.grenzen` (womit gerechnet wird),
40 * gegen `resolveRilsaSettings(...).deviations` (die gedruckte Aufstellung
41 * "Abweichungen von den Regelwerten") und gegen `fundstellenkennwerte` (das
42 * Fundstellenverzeichnis desselben Ausdrucks).
43 *
44 * GEGEN DEN ALTSTAND schlagen sechs der zehn Faelle fehl. Gruen bleiben dort
45 * die Vorbedingung, die Gegenprobe (Knotenpunkt und groesste Umlaufzeit) und
46 * die beiden Faelle zum Vermerk "wirksam N s".
47 *
48 * DER UMBAU VON `wirksamerWert` IST FUER SICH WIRKUNGSGLEICH, und das ist
49 * gemessen: Setzt man allein seinen Rumpf auf die fruehere Fassung zurueck
50 * (`bezug = eingetragen ?? feld.standard`) und laesst Spalte, Platzhalter und
51 * Knopf umgestellt, bleiben alle zehn Faelle gruen. Rot wird der Fall
52 * "behauptet am leeren Feld keinen abweichenden wirksamen Wert" erst gegen die
53 * HALBE Umstellung, bei der nur die BEZUGSSEITE des Vergleichs auf `regelwert`
54 * gezogen wird (`bezug = eingetragen ?? feld.regelwert`, `wirksam`
55 * unveraendert): Dann steht unter dem leeren Feld "wirksam 60 s", waehrend die
56 * Zeile daneben 40 s als Regelwert fuehrt - gemessen, 1 von 10 rot. Genau das
57 * bewacht der Fall. Der Fall daneben ("meldet den gekappten Wert weiterhin")
58 * ist die Gegenprobe dazu und bleibt in allen drei Staenden gruen.
59 */
60
61 const FESTES_DATUM = new Date('2026-01-01T00:00:00Z');
62
63 const VON = 'Regelbereich Umlaufzeit von';
64 const BIS = 'Regelbereich Umlaufzeit bis';
65
66 afterEach(() => {
67 document.body.replaceChildren();
68 });
69
70 function projekt(anlagenart: Anlagenart, rilsa: RilsaOverrides = {}): Project {
71 const roh = createStandardIntersectionProject('Vorgaben', FESTES_DATUM);
72 return {
73 ...roh,
74 anlagenart,
75 settings: { ...roh.settings, rilsa: { ...roh.settings.rilsa, ...rilsa } },
76 };
77 }
78
79 interface Ansicht {
80 readonly wurzel: HTMLElement;
81 readonly aufraeumen: () => void;
82 }
83
84 function zeichne(project: Project): Ansicht {
85 const wurzel = document.createElement('div');
86 document.body.append(wurzel);
87 const schliessen = settingsView.render(wurzel, {
88 store: new ProjectStore(project),
89 refresh: () => {},
90 navigate: () => {},
91 target: null,
92 });
93 return {
94 wurzel,
95 aufraeumen: () => {
96 schliessen();
97 wurzel.remove();
98 },
99 };
100 }
101
102 /** Zeile der Vorgabentabelle zu einem Kennwert. */
103 function zeileZu(wurzel: ParentNode, label: string): HTMLTableRowElement {
104 const zeile = [...wurzel.querySelectorAll<HTMLTableRowElement>('tbody tr')].find(
105 (tr) => tr.children[0]?.textContent?.trim() === label,
106 );
107 if (!zeile) throw new Error(`Keine Zeile "${label}"`);
108 return zeile;
109 }
110
111 /** Was die Zeile in Spalte "Regelwert", im Platzhalter und am Knopf zeigt. */
112 interface Zeilenbild {
113 readonly spalte: string;
114 readonly platzhalter: string;
115 readonly zuruecksetzen: boolean;
116 readonly text: string;
117 }
118
119 function bild(wurzel: ParentNode, label: string): Zeilenbild {
120 const zeile = zeileZu(wurzel, label);
121 return {
122 spalte: zeile.children[2]?.textContent?.trim() ?? '',
123 platzhalter: zeile.querySelector('input')?.placeholder ?? '',
124 zuruecksetzen: [...zeile.querySelectorAll('button')].some(
125 (b) => b.textContent?.trim() === 'Zurücksetzen',
126 ),
127 text: zeile.textContent ?? '',
128 };
129 }
130
131 /** Der Bezugswert der gedruckten Aufstellung "Abweichungen von den Regelwerten". */
132 function bezugDerAufstellung(
133 anlagenart: Anlagenart,
134 key: keyof RilsaOverrides,
135 eintrag: number,
136 ): number | undefined {
137 return resolveRilsaSettings({ [key]: eintrag }, RILSA_DEFAULTS, anlagenart).deviations.find(
138 (d) => d.key === key,
139 )?.standardValue;
140 }
141
142 describe('Der Regelwert der Vorgabenzeile', () => {
143 it('haelt an der Fussgaengerschutzanlage die Zahlen der Anlagenart bereit', () => {
144 // Vorbedingung des Aufbaus, wie in regelbereichBezug: Die Anlagenart
145 // fuehrt einen anderen Regelbereich als der Knotenpunkt.
146 expect(ANLAGENART_GRENZEN.fussgaengerschutzanlage.cycleTime.recommendedMin).toBe(40);
147 expect(ANLAGENART_GRENZEN.fussgaengerschutzanlage.cycleTime.recommendedMax).toBe(70);
148 // Seit 5.27.0 der Richtwert der RiLSA (Abschnitt 2.6) statt der 60 s.
149 expect(RILSA_DEFAULTS.cycleTime.recommendedMin).toBe(30);
150 expect(RILSA_DEFAULTS.cycleTime.recommendedMax).toBe(90);
151 });
152
153 it('nennt in Spalte und Platzhalter die Zahl, mit der der Plan rechnet', () => {
154 const p = projekt('fussgaengerschutzanlage');
155 // Womit gerechnet wird - nicht behauptet, sondern am Plan abgelesen.
156 const grenzen = buildSignalPlan(p).grenzen.cycleTime;
157 expect([grenzen.recommendedMin, grenzen.recommendedMax]).toEqual([40, 70]);
158
159 const ansicht = zeichne(p);
160 try {
161 const von = bild(ansicht.wurzel, VON);
162 expect(von.spalte, 'Spalte "Regelwert"').toBe(String(grenzen.recommendedMin));
163 expect(von.platzhalter, 'Platzhalter des leeren Feldes').toBe(String(grenzen.recommendedMin));
164 const bis = bild(ansicht.wurzel, BIS);
165 expect(bis.spalte).toBe(String(grenzen.recommendedMax));
166 expect(bis.platzhalter).toBe(String(grenzen.recommendedMax));
167 } finally {
168 ansicht.aufraeumen();
169 }
170 });
171
172 it('nennt dieselbe Zahl wie die Aufstellung "Abweichungen von den Regelwerten"', () => {
173 // Ein Eintrag von 60 s verschiebt den Regelbereich; die Aufstellung nennt
174 // dafuer ihren Bezugswert. Die Ansicht darf keinen anderen fuehren.
175 const bezug = bezugDerAufstellung('fussgaengerschutzanlage', 'cycleTimeRecommendedMin', 60);
176 expect(bezug, 'Vorbedingung: die Aufstellung fuehrt die Zeile').toBeDefined();
177
178 const ansicht = zeichne(projekt('fussgaengerschutzanlage', { cycleTimeRecommendedMin: 60 }));
179 try {
180 expect(bild(ansicht.wurzel, VON).spalte).toBe(String(bezug));
181 } finally {
182 ansicht.aufraeumen();
183 }
184 });
185
186 it('nennt dieselbe Zahl wie das Fundstellenverzeichnis desselben Ausdrucks', () => {
187 // Bildschirm und Ausdruck bilden ihren Bezugswert nicht mehr unabhaengig
188 // voneinander - das war der Befund, dreimal hintereinander.
189 const gedruckt = fundstellenkennwerte({}, 'fussgaengerschutzanlage');
190 const zeile = settingFields(RILSA_DEFAULTS, 'fussgaengerschutzanlage');
191 for (const key of ['cycleTimeRecommendedMin', 'cycleTimeRecommendedMax'] as const) {
192 const feld = zeile.find((f) => f.key === key);
193 expect(feld?.regelwert, `Vorgabenzeile "${key}"`).toBe(gedruckt.get(key)?.regelwert);
194 }
195
196 const ansicht = zeichne(projekt('fussgaengerschutzanlage'));
197 try {
198 expect(bild(ansicht.wurzel, VON).spalte).toBe(
199 String(gedruckt.get('cycleTimeRecommendedMin')?.regelwert),
200 );
201 expect(bild(ansicht.wurzel, BIS).spalte).toBe(
202 String(gedruckt.get('cycleTimeRecommendedMax')?.regelwert),
203 );
204 } finally {
205 ansicht.aufraeumen();
206 }
207 });
208
209 it('bietet "Zuruecksetzen" bei der wirksamen Eingabe an', () => {
210 // 60 s verschieben den Regelbereich von 40 auf 60 - hier gibt es etwas
211 // zurueckzunehmen, und die Aufstellung fuehrt die Zeile.
212 expect(bezugDerAufstellung('fussgaengerschutzanlage', 'cycleTimeRecommendedMin', 60)).toBe(40);
213
214 const ansicht = zeichne(projekt('fussgaengerschutzanlage', { cycleTimeRecommendedMin: 60 }));
215 try {
216 expect(bild(ansicht.wurzel, VON).zuruecksetzen).toBe(true);
217 } finally {
218 ansicht.aufraeumen();
219 }
220 });
221
222 it('bietet "Zuruecksetzen" bei der wirkungslosen Eingabe nicht an', () => {
223 // 40 s gelten dort ohnehin; die Aufstellung fuehrt die Zeile deshalb nicht.
224 expect(
225 bezugDerAufstellung('fussgaengerschutzanlage', 'cycleTimeRecommendedMin', 40),
226 ).toBeUndefined();
227
228 const ansicht = zeichne(projekt('fussgaengerschutzanlage', { cycleTimeRecommendedMin: 40 }));
229 try {
230 expect(bild(ansicht.wurzel, VON).zuruecksetzen).toBe(false);
231 } finally {
232 ansicht.aufraeumen();
233 }
234 });
235
236 it('behauptet am leeren Feld keinen abweichenden wirksamen Wert', () => {
237 /*
238 * Der Vermerk "wirksam N s" vergleicht gegen das, was die Zeile als geltend
239 * darstellt. Mit dem Regelwert der Anlagenart in Spalte und Platzhalter
240 * muss ihm der Vergleich AUF BEIDEN SEITEN folgen: `resolveRilsaSettings`
241 * fuehrt im aufgeloesten Kennwertsatz weiterhin die Knotenpunktzahl, weil
242 * der Regelbereich der Anlagenart erst in `anlagenartGrenzen` einsetzt -
243 * und zwar nur, solange nichts eingetragen ist. Zieht nur die Bezugsseite
244 * auf den Regelwert um, steht unter dem leeren Feld "wirksam 60 s",
245 * waehrend der Plan mit 40 s rechnet (gemessen; siehe Kopfkommentar).
246 */
247 const ansicht = zeichne(projekt('fussgaengerschutzanlage'));
248 try {
249 expect(bild(ansicht.wurzel, VON).text).not.toContain('wirksam');
250 expect(bild(ansicht.wurzel, BIS).text).not.toContain('wirksam');
251 } finally {
252 ansicht.aufraeumen();
253 }
254 });
255
256 it('meldet den gekappten Wert weiterhin, wo einer gekappt wird', () => {
257 // Gegenprobe zum Fall darueber: Ein Eintrag ueber der Schranke der
258 // Anlagenart wird auf 120 s gekappt, und das steht neben dem Feld
259 // (Befund 56 zur Fassung 5.9.0).
260 const aufgeloest = resolveRilsaSettings(
261 { cycleTimeRecommendedMin: 500 },
262 RILSA_DEFAULTS,
263 'fussgaengerschutzanlage',
264 );
265 expect(aufgeloest.defaults.cycleTime.recommendedMin).toBe(120);
266
267 const ansicht = zeichne(projekt('fussgaengerschutzanlage', { cycleTimeRecommendedMin: 500 }));
268 try {
269 expect(bild(ansicht.wurzel, VON).text).toContain('wirksam 120 s');
270 } finally {
271 ansicht.aufraeumen();
272 }
273 });
274
275 it('folgt auch bei einstreifiger Verkehrsfuehrung der Anlagenart', () => {
276 // Nicht nur die Fussgaengerschutzanlage: Den Regelbereich der einstreifigen
277 // Verkehrsfuehrung (60 bis 180 s) setzt dieses Programm ebenso selbst.
278 const p = projekt('einstreifig');
279 const grenzen = buildSignalPlan(p).grenzen.cycleTime;
280 expect([grenzen.recommendedMin, grenzen.recommendedMax]).toEqual([60, 180]);
281
282 const ansicht = zeichne(p);
283 try {
284 expect(bild(ansicht.wurzel, BIS).spalte).toBe(String(grenzen.recommendedMax));
285 expect(bild(ansicht.wurzel, BIS).platzhalter).toBe(String(grenzen.recommendedMax));
286 } finally {
287 ansicht.aufraeumen();
288 }
289 });
290
291 it('laesst Knotenpunkt und groesste Umlaufzeit unveraendert', () => {
292 /*
293 * Die Gegenproben, in beiden Staenden gruen. Am Knotenpunkt sind
294 * Anlagenart- und Regelwerkszahl dieselben. Und bei der kleinsten und
295 * groessten Umlaufzeit bleibt der Knotenpunktwert der Bezug, auch wo die
296 * Anlagenart mehr zulaesst: Er ist dort ein Regelwerkswert (30 bis 120 s,
297 * von der RiLSA nicht nach Anlagenart gestaffelt), waehrend die 300 s der
298 * einstreifigen Fuehrung dieses Programm setzt.
299 */
300 const amKnoten = zeichne(projekt('knotenpunkt'));
301 try {
302 expect(bild(amKnoten.wurzel, VON).spalte).toBe('30');
303 expect(bild(amKnoten.wurzel, VON).platzhalter).toBe('30');
304 expect(bild(amKnoten.wurzel, BIS).spalte).toBe('90');
305 } finally {
306 amKnoten.aufraeumen();
307 }
308
309 expect(ANLAGENART_GRENZEN.einstreifig.cycleTime.max).toBe(300);
310 const einstreifig = zeichne(projekt('einstreifig'));
311 try {
312 const groesste = bild(einstreifig.wurzel, 'Größte Umlaufzeit');
313 expect(groesste.spalte, 'Regelwerkswert, nicht die Schranke der Anlagenart').toBe('120');
314 expect(groesste.platzhalter).toBe('120');
315 } finally {
316 einstreifig.aufraeumen();
317 }
318 });
319 });