lsa-planer

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

/ tests ui zwischenzeitfensterEngstelle.test.ts

6,9 KB Rohdatei
tests/ui/zwischenzeitfensterEngstelle.test.ts — 185 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 } from '@/domain/model/factory';
9 import type { Project } from '@/domain/model/project';
10 import type { ViewDefinition } from '@/ui/shell';
11 import { conflictsView } from '@/ui/views/conflictsView';
12
13 /**
14 * Das Zwischenzeitfenster einer einstreifigen Verkehrsfuehrung.
15 *
16 * ANLASS (Befund aus dem Betrieb, 11.09.2026, mit Bildschirmfoto). Unter dem
17 * Feld "Raeumgeschwindigkeit" stand "Regelwert 10 m/s", waehrend der Rechenweg
18 * zwei Zeilen tiefer im selben Fenster "Raeumgeschwindigkeit vr 11,11 m/s"
19 * auswies. An einer Engstelle rechnet der Fachkern nach RiLSA 2015,
20 * Abschnitt 5.2.2 - mit der Raeumgeschwindigkeitsstaffel, nicht mit dem
21 * Vorgabenwert der Knotenpunktrechnung. Die Beschriftung kannte die Anlagenart
22 * nicht.
23 *
24 * DAS IST DIE UNSICHERE RICHTUNG. Der genannte Wert ist die Groesse, gegen die
25 * der Bearbeiter seine Handeingabe abwaegt. 10 m/s sieht strenger aus als die
26 * 11,11 m/s des Verfahrens; eine Eingabe daneben erschien harmloser, als sie
27 * ist.
28 *
29 * Dieselbe Datei bewacht die beiden Nachbarfelder: Die Ueberfahrzeit steht an
30 * der Engstelle mit 4 s fest, und die Einfahrgeschwindigkeit geht in gar keine
31 * Zahl ein - dort darf kein Regelwert stehen, sondern der Grund.
32 */
33
34 const FESTES_DATUM = new Date('2026-01-01T00:00:00Z');
35
36 function zeichne(
37 view: ViewDefinition,
38 project: Project,
39 ): { wurzel: HTMLElement; aufraeumen: () => void } {
40 const wurzel = document.createElement('div');
41 document.body.append(wurzel);
42 const abmelden = view.render(wurzel, {
43 store: new ProjectStore(project),
44 refresh: () => {},
45 navigate: () => {},
46 target: null,
47 });
48 return {
49 wurzel,
50 aufraeumen: () => {
51 abmelden();
52 wurzel.remove();
53 },
54 };
55 }
56
57 /** Sichtbarer Text einer Beschriftung - ohne den Hilfeknopf. */
58 function beschriftungstext(label: HTMLLabelElement): string {
59 return [...label.childNodes]
60 .filter((knoten) => !(knoten instanceof Element && knoten.classList.contains('hilfe-knopf')))
61 .map((knoten) => knoten.textContent ?? '')
62 .join('')
63 .trim();
64 }
65
66 function hinweisZu(wurzel: ParentNode, beschriftung: string): string {
67 const label = [...wurzel.querySelectorAll('label')].find(
68 (l) => beschriftungstext(l) === beschriftung,
69 );
70 const id = label?.getAttribute('for');
71 expect(id, `Das Feld "${beschriftung}" fehlt im Fenster`).toBeTruthy();
72 const feld = wurzel.querySelector<HTMLElement>(`[id="${id ?? ''}"]`);
73 const beschrieben = feld?.getAttribute('aria-describedby');
74 if (beschrieben === null || beschrieben === undefined) return '';
75 return beschrieben
76 .split(' ')
77 .map((teil) => wurzel.querySelector(`[id="${teil}"]`)?.textContent ?? '')
78 .join(' ');
79 }
80
81 /** Erlaeuterungsspalte einer Zeile des Rechenwegs. */
82 function rechenwegWert(fenster: ParentNode, bezeichnung: string): string {
83 const zeile = [...fenster.querySelectorAll('tbody tr')].find(
84 (tr) => tr.children[0]?.textContent?.trim() === bezeichnung,
85 );
86 if (!zeile) throw new Error(`Zeile "${bezeichnung}" fehlt im Rechenweg`);
87 return zeile.children[1]?.textContent?.trim() ?? '';
88 }
89
90 /**
91 * Zwei Kfz-Stroeme ueber eine einstreifige Engstelle, 44 m Haltlinienabstand,
92 * 50 km/h zulaessig - der Aufbau des gemeldeten Falls.
93 */
94 function baustelle(anlagenart: 'einstreifig' | 'knotenpunkt'): Project {
95 const basis = createEmptyProject('Engstelle', FESTES_DATUM, anlagenart);
96 const a = createSignalGroup({ name: 'K1', mode: 'kfz', vZul: 50, index: 0 });
97 const b = createSignalGroup({ name: 'K2', mode: 'kfz', vZul: 50, index: 1 });
98 const p1 = createPhase('Seite A', [a.id]);
99 const p2 = createPhase('Seite B', [b.id]);
100 return {
101 ...basis,
102 signalGroups: [a, b],
103 conflicts: [
104 { ...createConflict(a.id, b.id), clearingDistance: 44, enteringDistance: 0 },
105 { ...createConflict(b.id, a.id), clearingDistance: 44, enteringDistance: 0 },
106 ],
107 phases: [p1, p2],
108 program: { ...basis.program, phaseOrder: [p1.id, p2.id] },
109 };
110 }
111
112 function oeffneErsteBeziehung(wurzel: HTMLElement): ParentNode {
113 const zellen = [...wurzel.querySelectorAll<HTMLButtonElement>('.matrix-zelle')];
114 expect(zellen.length, 'Die Matrix hat keine Zellen').toBeGreaterThan(0);
115 zellen[0]?.click();
116 const fenster = document.querySelector('.dialog');
117 expect(fenster, 'Das Zwischenzeitfenster ist nicht offen').not.toBeNull();
118 return fenster as ParentNode;
119 }
120
121 afterEach(() => {
122 document.body.replaceChildren();
123 });
124
125 describe('An der Engstelle nennt das Fenster die Werte des Abschnitts 5.2', () => {
126 it('nennt unter dem Feld dieselbe Raeumgeschwindigkeit, mit der es rechnet', () => {
127 // Der gemeldete Widerspruch, in einem Fall: Beschriftung gegen Rechenweg.
128 const { wurzel, aufraeumen } = zeichne(conflictsView, baustelle('einstreifig'));
129 try {
130 const fenster = oeffneErsteBeziehung(wurzel);
131 expect(hinweisZu(fenster, 'Räumgeschwindigkeit')).toContain('11,11 m/s');
132 expect(rechenwegWert(fenster, 'Räumgeschwindigkeit vr')).toContain('11,11');
133 } finally {
134 aufraeumen();
135 }
136 });
137
138 it('nennt nicht mehr den Regelwert der Knotenpunktrechnung', () => {
139 const { wurzel, aufraeumen } = zeichne(conflictsView, baustelle('einstreifig'));
140 try {
141 const fenster = oeffneErsteBeziehung(wurzel);
142 expect(hinweisZu(fenster, 'Räumgeschwindigkeit')).not.toContain('10 m/s');
143 } finally {
144 aufraeumen();
145 }
146 });
147
148 it('nennt statt einer Einfahrgeschwindigkeit den Grund, warum keine wirkt', () => {
149 // Ein Regelwert unter einem Feld ohne Wirkung laedt dazu ein, daran zu
150 // drehen. Hier steht deshalb der Abschnitt, nicht die Zahl.
151 const { wurzel, aufraeumen } = zeichne(conflictsView, baustelle('einstreifig'));
152 try {
153 const hinweis = hinweisZu(oeffneErsteBeziehung(wurzel), 'Einfahrgeschwindigkeit');
154 expect(hinweis).toContain('Ohne Wirkung');
155 expect(hinweis).toContain('5.2.2');
156 expect(hinweis).not.toContain('Regelwert');
157 } finally {
158 aufraeumen();
159 }
160 });
161
162 it('nennt die feste Ueberfahrzeit von 4 s', () => {
163 const { wurzel, aufraeumen } = zeichne(conflictsView, baustelle('einstreifig'));
164 try {
165 expect(hinweisZu(oeffneErsteBeziehung(wurzel), 'Überfahrzeit')).toContain('4 s');
166 } finally {
167 aufraeumen();
168 }
169 });
170 });
171
172 describe('Am Knotenpunkt bleibt das Fenster, wie es war', () => {
173 it('nennt dort weiterhin den Regelwert der Knotenpunktrechnung', () => {
174 // Dieselbe Geometrie, andere Anlagenart. Ohne diesen Fall waere nicht
175 // gezeigt, dass die Berichtigung nur die Engstelle betrifft.
176 const { wurzel, aufraeumen } = zeichne(conflictsView, baustelle('knotenpunkt'));
177 try {
178 const fenster = oeffneErsteBeziehung(wurzel);
179 expect(hinweisZu(fenster, 'Räumgeschwindigkeit')).toContain('10 m/s');
180 expect(hinweisZu(fenster, 'Einfahrgeschwindigkeit')).toContain('Regelwert');
181 } finally {
182 aufraeumen();
183 }
184 });
185 });