lsa-planer

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

/ tests ui regelwertBeschriftung.test.ts

15,5 KB Rohdatei
tests/ui/regelwertBeschriftung.test.ts — 386 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { ProjectStore } from '@/app/store';
3 import {
4 createConflict,
5 createEmptyProject,
6 createPhase,
7 createSignalGroup,
8 createStandardIntersectionProject,
9 } from '@/domain/model/factory';
10 import { buildSignalPlan } from '@/domain/plan/signalPlan';
11 import { RILSA_ENGSTELLE, UEBERFAHRZEIT_ANSATZ_LABELS } from '@/domain/rilsa/constants';
12 import type { Project } from '@/domain/model/project';
13 import type { ViewDefinition } from '@/ui/shell';
14 import { conflictsView } from '@/ui/views/conflictsView';
15
16 /**
17 * Befunde zur Fassung 5.8.0.
18 *
19 * Befund 30 Das Konfliktfenster beschriftete die drei Felder
20 * Raeumgeschwindigkeit, Einfahrgeschwindigkeit und Ueberfahrzeit mit
21 * "Regelwert <Zahl>" und las die Zahl aus dem RECHENERGEBNIS. Bei
22 * gesetzter Handeingabe nannte das Feld damit die eigene Eingabe des
23 * Bearbeiters "Regelwert"; die Bezugsgroesse, gegen die er sie
24 * abwaegen soll, war im Fenster nicht mehr zu sehen. Bei der
25 * Einfahrgeschwindigkeit widersprach das Fenster unmittelbar der
26 * Warnung derselben Rechnung zwei Bloecke tiefer.
27 * Befund 58 Die Zeile "Ueberfahrzeit tue" im Rechenweg wies eine VERWORFENE
28 * Handeingabe weiterhin als "von Hand eingetragen" aus, obwohl mit
29 * dem Regelwert gerechnet wurde.
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; aufraeumen: () => void } {
38 const wurzel = document.createElement('div');
39 document.body.append(wurzel);
40 const abmelden = view.render(wurzel, {
41 store: new ProjectStore(project),
42 refresh: () => {},
43 navigate: () => {},
44 target: null,
45 });
46 return {
47 wurzel,
48 aufraeumen: () => {
49 abmelden();
50 wurzel.remove();
51 },
52 };
53 }
54
55 /** Sichtbarer Text einer Beschriftung - ohne den Hilfeknopf. */
56 function beschriftungstext(label: HTMLLabelElement): string {
57 return [...label.childNodes]
58 .filter((knoten) => !(knoten instanceof Element && knoten.classList.contains('hilfe-knopf')))
59 .map((knoten) => knoten.textContent ?? '')
60 .join('')
61 .trim();
62 }
63
64 function feldZu(wurzel: ParentNode, beschriftung: string): HTMLElement | null {
65 const label = [...wurzel.querySelectorAll('label')].find(
66 (l) => beschriftungstext(l) === beschriftung,
67 );
68 const id = label?.getAttribute('for');
69 if (id === undefined || id === null || id === '') return null;
70 return wurzel.querySelector<HTMLElement>(`[id="${id}"]`);
71 }
72
73 function hinweisZu(wurzel: ParentNode, beschriftung: string): string {
74 const feld = feldZu(wurzel, beschriftung);
75 expect(feld, `Das Feld "${beschriftung}" fehlt im Fenster`).not.toBeNull();
76 const id = feld!.getAttribute('aria-describedby');
77 if (id === null) return '';
78 return id
79 .split(' ')
80 .map((teil) => wurzel.querySelector(`[id="${teil}"]`)?.textContent ?? '')
81 .join(' ');
82 }
83
84 /** Erlaeuterungsspalte einer Zeile des Rechenwegs. */
85 function rechenwegZeile(fenster: ParentNode, bezeichnung: string): string {
86 const zeile = [...fenster.querySelectorAll('tbody tr')].find(
87 (tr) => tr.children[0]?.textContent?.trim() === bezeichnung,
88 );
89 if (!zeile) throw new Error(`Zeile "${bezeichnung}" fehlt im Rechenweg`);
90 return zeile.children[2]?.textContent?.trim() ?? '';
91 }
92
93 function oeffneBeziehung(wurzel: HTMLElement, project: Project, fromName: string): ParentNode {
94 const von = project.signalGroups.find((g) => g.name === fromName)!;
95 const konflikt = project.conflicts.find((c) => c.fromId === von.id)!;
96 const zeile = project.signalGroups.findIndex((g) => g.id === konflikt.fromId);
97 const spalte = project.signalGroups.findIndex((g) => g.id === konflikt.toId);
98 const zellen = [...wurzel.querySelectorAll<HTMLButtonElement>('.matrix-zelle')];
99 const n = project.signalGroups.length;
100 const versatz = spalte > zeile ? spalte - 1 : spalte;
101 zellen[zeile * (n - 1) + versatz]?.click();
102 const fenster = document.querySelector('.dialog');
103 expect(fenster, `Das Fenster zu ${fromName} ist nicht offen`).not.toBeNull();
104 return fenster as ParentNode;
105 }
106
107 /** Knotenpunkt, dessen erste Signalgruppe rechts abbiegt. */
108 function mitAbbieger(aenderung: Partial<Project['conflicts'][number]> = {}): Project {
109 const basis = createStandardIntersectionProject('Beschriftung', FESTES_DATUM);
110 const erste = basis.signalGroups[0]!;
111 return {
112 ...basis,
113 signalGroups: basis.signalGroups.map((g) =>
114 g.id === erste.id ? { ...g, movement: 'rechts' as const } : g,
115 ),
116 conflicts: basis.conflicts.map((c) => (c.fromId === erste.id ? { ...c, ...aenderung } : c)),
117 };
118 }
119
120 afterEach(() => {
121 document.body.replaceChildren();
122 });
123
124 describe('Befund 30 - "Regelwert" ist nicht die eigene Eingabe', () => {
125 it('nennt bei eingetragener Raeumgeschwindigkeit den Regelwert der Fahrbeziehung', () => {
126 const project = mitAbbieger({ clearingSpeedOverride: 9 });
127 const { wurzel, aufraeumen } = zeichne(conflictsView, project);
128 try {
129 const fenster = oeffneBeziehung(wurzel, project, 'K1');
130 const hinweis = hinweisZu(fenster, 'Räumgeschwindigkeit');
131 expect(hinweis).toContain('Angesetzt 9 m/s');
132 expect(hinweis).toContain('von Hand');
133 expect(hinweis).toContain('Regelwert 7 m/s');
134 expect(hinweis).not.toContain('Regelwert 9');
135 } finally {
136 aufraeumen();
137 }
138 });
139
140 it('nennt bei eingetragener Einfahrgeschwindigkeit den Regelwert des einfahrenden Stroms', () => {
141 const project = mitAbbieger({ enteringSpeedOverride: 5 });
142 const { wurzel, aufraeumen } = zeichne(conflictsView, project);
143 try {
144 const fenster = oeffneBeziehung(wurzel, project, 'K1');
145 const hinweis = hinweisZu(fenster, 'Einfahrgeschwindigkeit');
146 expect(hinweis).toContain('Angesetzt 5 m/s');
147 expect(hinweis).toContain('Regelwert 11,1 m/s');
148 expect(hinweis).not.toContain('Regelwert 5');
149 } finally {
150 aufraeumen();
151 }
152 });
153
154 it('nennt bei eingetragener Ueberfahrzeit den Wert des Ansatzes ohne diesen Eintrag', () => {
155 /*
156 * Seit Fassung 5.43.0 (Befund 17) nicht mehr "Regelwert":
157 * Die Gelbzeit ist der Ansatz dieses Programms, und beim Abbieger setzt
158 * die RiLSA 2 s, nicht 3 s. Die Bezugsgroesse steht trotzdem da - darum
159 * ging es in Befund 30.
160 */
161 const project = mitAbbieger({ crossingTimeOverride: 6 });
162 const { wurzel, aufraeumen } = zeichne(conflictsView, project);
163 try {
164 const fenster = oeffneBeziehung(wurzel, project, 'K1');
165 const hinweis = hinweisZu(fenster, 'Überfahrzeit');
166 expect(hinweis).toContain('Angesetzt 6 s');
167 expect(hinweis).toContain('ohne diesen Eintrag 3 s');
168 expect(hinweis).not.toContain('ohne diesen Eintrag 6');
169 expect(hinweis).not.toContain('Regelwert 6');
170 } finally {
171 aufraeumen();
172 }
173 });
174
175 it('sagt bei einer verworfenen Ueberfahrzeit, dass der Eintrag nicht gilt', () => {
176 const project = mitAbbieger({ crossingTimeOverride: 0 });
177 const { wurzel, aufraeumen } = zeichne(conflictsView, project);
178 try {
179 const fenster = oeffneBeziehung(wurzel, project, 'K1');
180 const hinweis = hinweisZu(fenster, 'Überfahrzeit');
181 expect(hinweis).toContain('verworfen');
182 expect(hinweis).not.toContain('Regelwert 0');
183 } finally {
184 aufraeumen();
185 }
186 });
187
188 it('beschriftet die Felder ohne Handeingabe weiterhin mit dem Regelwert', () => {
189 const project = mitAbbieger();
190 const { wurzel, aufraeumen } = zeichne(conflictsView, project);
191 try {
192 const fenster = oeffneBeziehung(wurzel, project, 'K1');
193 expect(hinweisZu(fenster, 'Räumgeschwindigkeit')).toContain('Regelwert 7 m/s');
194 expect(hinweisZu(fenster, 'Einfahrgeschwindigkeit')).toContain('Regelwert 11,1 m/s');
195 // Die Ueberfahrzeit des Abbiegers ist im Gelbzeitansatz kein Regelwert
196 // (Fassung 5.43.0, Befund 17), aber weiter angesetzt genannt.
197 expect(hinweisZu(fenster, 'Überfahrzeit')).toContain(
198 'Angesetzt 3 s – Gelbzeit (Ansatz dieses Programms)',
199 );
200 } finally {
201 aufraeumen();
202 }
203 });
204 });
205
206 describe('Befund 58 - der Rechenweg nennt keine Herkunft, die es nicht gibt', () => {
207 it('weist eine verworfene Ueberfahrzeit nicht als von Hand eingetragen aus', () => {
208 // Eingetragen sind 0 s, gerechnet wird mit der Gelbzeit von 3 s.
209 const project = mitAbbieger({ crossingTimeOverride: 0 });
210 const { wurzel, aufraeumen } = zeichne(conflictsView, project);
211 try {
212 const fenster = oeffneBeziehung(wurzel, project, 'K1');
213 const erlaeuterung = rechenwegZeile(fenster, 'Überfahrzeit tü');
214 expect(erlaeuterung).toContain('verworfen');
215 expect(erlaeuterung).not.toBe('an dieser Beziehung von Hand eingetragen');
216 } finally {
217 aufraeumen();
218 }
219 });
220
221 it('nennt eine uebernommene Ueberfahrzeit weiterhin als Handeingabe', () => {
222 const project = mitAbbieger({ crossingTimeOverride: 4 });
223 const { wurzel, aufraeumen } = zeichne(conflictsView, project);
224 try {
225 const fenster = oeffneBeziehung(wurzel, project, 'K1');
226 expect(rechenwegZeile(fenster, 'Überfahrzeit tü')).toBe(
227 'an dieser Beziehung von Hand eingetragen',
228 );
229 } finally {
230 aufraeumen();
231 }
232 });
233 });
234
235 /*
236 * NACHTRAG (Fassung 5.43.0): Die Zeile "Ueberfahrzeit tue" nannte an
237 * einer EINSTREIFIGEN Verkehrsfuehrung weiter die Beschriftung des
238 * Kfz-Ansatzes, obwohl der Fachkern dort nach RiLSA 2015, Abschnitt 5.2.2 mit
239 * festen 4 s rechnet - der Ansatz erreicht das Verfahren gar nicht. Die drei
240 * Felder darueber holen ihren Bezugswert seit dem Befund vom 11.09.2026 aus
241 * `bezugswerte` und kennen das Verfahren; diese Zeile nicht.
242 */
243 describe('Fassung 5.43.0 - der Rechenweg nennt das Verfahren der Engstelle', () => {
244 /** Zwei Richtungen einer Baustellenampel, 200 m Engstelle, 50 km/h. */
245 function baustelle(aenderung: Partial<Project['conflicts'][number]> = {}): Project {
246 const basis = createEmptyProject('Baustelle', FESTES_DATUM, 'einstreifig');
247 const a = createSignalGroup({ name: 'K1', mode: 'kfz', vZul: 50, index: 0 });
248 const b = createSignalGroup({ name: 'K2', mode: 'kfz', vZul: 50, index: 1 });
249 const p1 = createPhase('Seite A', [a.id]);
250 const p2 = createPhase('Seite B', [b.id]);
251 const beziehung = (von: string, nach: string) => ({
252 ...createConflict(von, nach),
253 clearingDistance: 200,
254 enteringDistance: 0,
255 ...aenderung,
256 });
257 return {
258 ...basis,
259 signalGroups: [a, b],
260 conflicts: [beziehung(a.id, b.id), beziehung(b.id, a.id)],
261 phases: [p1, p2],
262 program: { ...basis.program, phaseOrder: [p1.id, p2.id] },
263 };
264 }
265
266 it('nennt den Regelwert des Abschnitts 5.2.2 statt des Kfz-Ansatzes', () => {
267 const project = baustelle();
268 // Vorbedingung: Hier rechnet wirklich das zweite Verfahren.
269 const plan = buildSignalPlan(project);
270 expect(
271 [...plan.intergreens.values()].every((i) => i.calculation?.verfahren === 'engstelle'),
272 'Vorbedingung: das Engstellenverfahren greift',
273 ).toBe(true);
274
275 const { wurzel, aufraeumen } = zeichne(conflictsView, project);
276 try {
277 const fenster = oeffneBeziehung(wurzel, project, 'K1');
278 expect(rechenwegZeile(fenster, 'Überfahrzeit tü'), 'die Zahl daneben').toBe(
279 `Regelwert der Engstelle: tü = ${RILSA_ENGSTELLE.ueberfahrzeit} s ` +
280 '(RiLSA 2015, Abschnitt 5.2.2)',
281 );
282 expect(rechenwegZeile(fenster, 'Überfahrzeit tü')).not.toContain(
283 UEBERFAHRZEIT_ANSATZ_LABELS.gelbzeit,
284 );
285 } finally {
286 aufraeumen();
287 }
288 });
289
290 it('sagt dort auch, dass eine eingetragene Ueberfahrzeit nicht eingeht', () => {
291 // Das Verfahren liest `crossingTimeOverride` nicht; bis hierher hiess eine
292 // Eingabe von 6 s daneben "verworfen", als haette eine Pruefung sie
293 // abgelehnt.
294 const project = baustelle({ crossingTimeOverride: 6 });
295 const { wurzel, aufraeumen } = zeichne(conflictsView, project);
296 try {
297 const zeile = rechenwegZeile(oeffneBeziehung(wurzel, project, 'K1'), 'Überfahrzeit tü');
298 expect(zeile).toContain('gehen in dieses Verfahren nicht ein');
299 expect(zeile).toContain('6 s');
300 } finally {
301 aufraeumen();
302 }
303 });
304
305 it('laesst die Zeile am Knotenpunkt unveraendert', () => {
306 // Gegenprobe: Dieselbe Geometrie am Knotenpunkt nennt weiter den Ansatz.
307 const project: Project = { ...baustelle(), anlagenart: 'knotenpunkt' };
308 const { wurzel, aufraeumen } = zeichne(conflictsView, project);
309 try {
310 expect(rechenwegZeile(oeffneBeziehung(wurzel, project, 'K1'), 'Überfahrzeit tü')).toBe(
311 UEBERFAHRZEIT_ANSATZ_LABELS.gelbzeit,
312 );
313 } finally {
314 aufraeumen();
315 }
316 });
317
318 /*
319 * NACHTRAG (Befund 3): Der Rechenweg war berichtigt, der FELDHINWEIS
320 * desselben Feldes nicht. Er sagte weiter genau die beiden Wortlaute, die der
321 * Rechenweg als falsch ausweist - gemessen an diesem Projekt: mit einer
322 * eingetragenen Ueberfahrzeit von 4 s "Angesetzt 4 s - von Hand eingetragen;
323 * Regelwert 4 s", mit 6 s "Regelwert 4 s - die Handeingabe 6 s ist
324 * verworfen". Zwei Aussagen ueber denselben Eintrag im selben Fenster, und
325 * eine davon die verworfene.
326 *
327 * NUR DER SATZTEIL ZUM EINTRAG: Dass der Hinweis dort "Regelwert 4 s" sagt,
328 * ist eigens bewacht (konfliktfensterUeberfahrzeit.test.ts, Fassung 5.5.0,
329 * Befund F4, samt dem Gleichlauf mit dem Platzhalter des leeren Feldes). Der Bezugswert bleibt
330 * also stehen; falsch war, was DANEBEN ueber die Eingabe behauptet wurde.
331 */
332 it('nennt am Feld selbst keine Wirkung einer eingetragenen Ueberfahrzeit', () => {
333 for (const eintrag of [4, 6]) {
334 const project = baustelle({ crossingTimeOverride: eintrag });
335 const { wurzel, aufraeumen } = zeichne(conflictsView, project);
336 try {
337 const hinweis = hinweisZu(oeffneBeziehung(wurzel, project, 'K1'), 'Überfahrzeit');
338 expect(hinweis, `Eintrag ${eintrag} s`).toContain('gehen in dieses Verfahren nicht ein');
339 expect(hinweis, 'der Eintrag wird genannt').toContain(`${eintrag} s`);
340 // Der Bezugswert bleibt daneben stehen.
341 expect(hinweis, 'der Regelwert des Verfahrens').toContain(
342 `Regelwert ${RILSA_ENGSTELLE.ueberfahrzeit} s`,
343 );
344 expect(hinweis, 'die Herleitung, die nicht stattgefunden hat').not.toContain(
345 'von Hand eingetragen',
346 );
347 expect(hinweis, 'die Pruefung, die es nicht gab').not.toContain('verworfen');
348 } finally {
349 aufraeumen();
350 }
351 }
352 });
353
354 it('haengt ohne Eintrag nichts an den Hinweis', () => {
355 // Gegenprobe: Der Zusatz kommt nur, wo wirklich etwas eingetragen ist -
356 // dieselbe Bedingung wie im Rechenweg.
357 const project = baustelle();
358 const { wurzel, aufraeumen } = zeichne(conflictsView, project);
359 try {
360 expect(hinweisZu(oeffneBeziehung(wurzel, project, 'K1'), 'Überfahrzeit')).toContain(
361 `Regelwert ${RILSA_ENGSTELLE.ueberfahrzeit} s`,
362 );
363 expect(hinweisZu(oeffneBeziehung(wurzel, project, 'K1'), 'Überfahrzeit')).not.toContain(
364 'nicht ein',
365 );
366 } finally {
367 aufraeumen();
368 }
369 });
370
371 it('laesst den Feldhinweis am Knotenpunkt unveraendert', () => {
372 // Gegenprobe: Dort ist die Handeingabe wirksam, und dort heisst sie so.
373 const project: Project = {
374 ...baustelle({ crossingTimeOverride: 4 }),
375 anlagenart: 'knotenpunkt',
376 };
377 const { wurzel, aufraeumen } = zeichne(conflictsView, project);
378 try {
379 const hinweis = hinweisZu(oeffneBeziehung(wurzel, project, 'K1'), 'Überfahrzeit');
380 expect(hinweis).toContain('von Hand eingetragen');
381 expect(hinweis).not.toContain('nicht ein');
382 } finally {
383 aufraeumen();
384 }
385 });
386 });