lsa-planer

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

/ tests ui herkunftFesteWerte.test.ts

12,1 KB Rohdatei
tests/ui/herkunftFesteWerte.test.ts — 281 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { ProjectStore } from '@/app/store';
3 import { createStandardIntersectionProject } from '@/domain/model/factory';
4 import { festeWerteZuschreibungSatz } from '@/domain/rilsa/ansaetze';
5 import { RILSA_DEFAULTS, UEBERFAHRZEIT_ANSATZ_LABELS } from '@/domain/rilsa/constants';
6 import type { Project } from '@/domain/model/project';
7 import type { ViewContext } from '@/ui/shell';
8 import { projectView } from '@/ui/views/projectView';
9
10 /**
11 * Befund 6 zur Fassung 5.9.0, Rest in der Projektansicht.
12 *
13 * Der Feldhinweis am Ueberfahrzeit-Ansatz holt die RICHTUNG des Vergleichs
14 * inzwischen aus dem Fachkern (`ueberfahrzeitAnsatzRichtungSatz`), schrieb
15 * danach aber unbedingt: "Die beiden Werte stammen aus behördlichen
16 * Leitfäden." Beide Werte sind ueber die Vorgabenverwaltung einstellbar
17 * (`crossingTimeKfzGeradeaus`, `crossingTimeKfzAbbiegend`, 1 bis 10 s). Wer
18 * 6 s eintraegt, bekam seine eigene Eingabe als Leitfadenwert zurueck - in
19 * derselben Zeile, die ihn davor warnen soll, einen Praxisansatz fuer einen
20 * Regelwerkswert zu halten.
21 *
22 * Im Ausdruck ist dieselbe Zuschreibung behoben (`festeWerteHerkunftSatz` in
23 * services/export/bewertung.ts); hier ist sie nachgezogen.
24 */
25
26 const FESTES_DATUM = new Date('2026-01-01T00:00:00Z');
27 const BESCHRIFTUNG = 'Überfahrzeit des Kraftfahrzeugverkehrs';
28
29 let aufraeumen: (() => void) | null = null;
30 afterEach(() => {
31 aufraeumen?.();
32 aufraeumen = null;
33 document.body.replaceChildren();
34 });
35
36 function zeichne(project: Project): HTMLElement {
37 const wurzel = document.createElement('div');
38 document.body.append(wurzel);
39 const store = new ProjectStore(project);
40 const context: ViewContext = {
41 store,
42 refresh: () => {},
43 navigate: () => {},
44 target: null,
45 };
46 const abmelden = projectView.render(wurzel, context);
47 aufraeumen = () => {
48 abmelden();
49 wurzel.remove();
50 };
51 return wurzel;
52 }
53
54 /** Sichtbarer Text einer Beschriftung - ohne den Hilfeknopf. */
55 function beschriftungstext(label: HTMLLabelElement): string {
56 return [...label.childNodes]
57 .filter((knoten) => !(knoten instanceof Element && knoten.classList.contains('hilfe-knopf')))
58 .map((knoten) => knoten.textContent ?? '')
59 .join('')
60 .trim();
61 }
62
63 /** Der Feldhinweis, der zum Auswahlfeld des Ansatzes vorgelesen wird. */
64 function feldhinweis(wurzel: HTMLElement): string {
65 const label = [...wurzel.querySelectorAll('label')].find(
66 (l) => beschriftungstext(l) === BESCHRIFTUNG,
67 );
68 const id = label?.getAttribute('for') ?? '';
69 const feld = wurzel.querySelector<HTMLElement>(`[id="${id}"]`);
70 const beschreibung = feld?.getAttribute('aria-describedby') ?? '';
71 return beschreibung
72 .split(' ')
73 .map((teil) => wurzel.querySelector(`[id="${teil}"]`)?.textContent ?? '')
74 .join(' ');
75 }
76
77 function mitVorgaben(rilsa: Record<string, number>): Project {
78 const basis = createStandardIntersectionProject('Überfahrzeit', FESTES_DATUM);
79 return { ...basis, settings: { ...basis.settings, rilsa } };
80 }
81
82 describe('Projekt: die Herkunft der festen Überfahrzeiten steht nur, wo sie zutrifft', () => {
83 it('nennt bei unveränderten Vorgaben die behördlichen Leitfäden', () => {
84 // Grenzwache: Solange die Regelwerte gelten, ist die Zuschreibung richtig
85 // und muss stehen bleiben.
86 //
87 // MIT DER SATZFUGE: Der Herkunftssatz steht zwischen zwei anderen Saetzen,
88 // und der folgende beginnt mit "Der Wortlaut der RiLSA 2015". Schliesst der
89 // Zweig mit einem Strichpunkt, liest sich der Hinweis "... Leitfäden; Der
90 // Wortlaut ..." - nach einem Strichpunkt steht kein grossgeschriebener
91 // Artikel. Ein blosses toContain('behördlichen Leitfäden') bewacht die
92 // Stelle nicht, deshalb die Fuge im Text.
93 const wurzel = zeichne(createStandardIntersectionProject('Überfahrzeit', FESTES_DATUM));
94 const hinweis = feldhinweis(wurzel);
95
96 expect(hinweis, 'Vorbedingung: der Hinweis am Ansatzfeld wurde gefunden').toContain(
97 UEBERFAHRZEIT_ANSATZ_LABELS.gelbzeit,
98 );
99 // Seit 5.27.0 nennt der Satz die Fundstelle: Die beiden Zahlen stehen in
100 // der RiLSA 2015 und nicht nur in behoerdlichen Leitfaeden.
101 expect(hinweis).toContain('Die beiden Werte sind die Regelwerte der RiLSA 2015');
102 });
103
104 it('schreibt eingetragene Vorgabewerte nicht behördlichen Leitfäden zu', () => {
105 const wurzel = zeichne(
106 mitVorgaben({ crossingTimeKfzGeradeaus: 6, crossingTimeKfzAbbiegend: 6 }),
107 );
108 const hinweis = feldhinweis(wurzel);
109
110 expect(hinweis, 'Vorbedingung: die eingetragenen Werte stehen im Hinweis').toContain('6 s');
111 expect(
112 hinweis,
113 'Der Hinweis gibt eingetragene Vorgabewerte als Leitfadenwerte aus',
114 ).not.toContain('sind die Regelwerte der RiLSA 2015 (Abschnitt 2.5.2).');
115 // Die belegten Werte gehoeren daneben - sonst weiss der Leser nicht, wovon
116 // sein Plan abweicht.
117 expect(hinweis).toContain(
118 `${RILSA_DEFAULTS.crossingTime.kfzGeradeaus} s geradeaus und ` +
119 `${RILSA_DEFAULTS.crossingTime.kfzAbbiegend} s abbiegend`,
120 );
121 });
122
123 it('nennt das Regelwerk in beiden Fällen', () => {
124 /*
125 * UMGESTELLT MIT 5.27.0: Hier stand der Vorbehalt "der Wortlaut der
126 * RiLSA 2015 zu den Zwischenzeitfällen 1 und 2 ist öffentlich nicht
127 * abgleichbar". Er ist entfallen; geblieben ist, dass der Hinweis das
128 * Regelwerk auch dann nennt, wenn eigene Werte eingetragen sind - dann
129 * eben als das, wovon sie abweichen.
130 */
131 const wurzel = zeichne(mitVorgaben({ crossingTimeKfzGeradeaus: 6 }));
132 const hinweis = feldhinweis(wurzel);
133
134 expect(hinweis).not.toContain('nicht abgleichbar');
135 expect(hinweis).toContain('RiLSA 2015');
136 });
137 });
138
139 /*
140 * NACHTRAG (Fassung 5.43.0): Derselbe Befund eine Zeile tiefer.
141 *
142 * Der Feldhinweis oben unterscheidet seit 5.9.0 zwischen Regelwerten und
143 * Vorgaben dieses Plans. Der WARNBALKEN darunter - er erscheint, sobald der
144 * feste Ansatz gewaehlt ist - tat es nicht: Er sagte unbedingt "Das sind die
145 * Regelwerte der RiLSA 2015 (Abschnitt 2.5.2, Fälle 1 und 2)" und nannte die
146 * Gelbzeitkopplung "den längeren Ansatz dieses Programms". Beides war zu stark:
147 * Bei 50 km/h geradeaus ergeben beide Ansaetze 3 s, und bei 6 s festen Vorgaben
148 * ist die Gelbzeit die kuerzere.
149 */
150 describe('Projekt: der Warnbalken zum festen Ansatz sagt, woher die Zahlen stammen', () => {
151 function mitFestemAnsatz(rilsa: Record<string, number> = {}): Project {
152 const basis = createStandardIntersectionProject('Überfahrzeit', FESTES_DATUM);
153 return { ...basis, settings: { ...basis.settings, ueberfahrzeitAnsatz: 'fest', rilsa } };
154 }
155
156 /** Text des Warnbalkens unter dem Auswahlfeld des Ansatzes. */
157 function balken(wurzel: HTMLElement): string {
158 const treffer = [...wurzel.querySelectorAll('.hinweisbalken-warnung')].find((k) =>
159 (k.textContent ?? '').includes('Feste Überfahrzeit gewählt'),
160 );
161 expect(treffer, 'Der Warnbalken zum festen Ansatz fehlt').not.toBeUndefined();
162 return (treffer?.textContent ?? '').replace(/\s+/gu, ' ');
163 }
164
165 /** Nur die Ueberschrift des Balkens - die staerkste Aussage darin. */
166 function ueberschrift(wurzel: HTMLElement): string {
167 const treffer = [...wurzel.querySelectorAll('.hinweisbalken-warnung')].find((k) =>
168 (k.textContent ?? '').includes('Feste Überfahrzeit gewählt'),
169 );
170 return treffer?.querySelector('strong')?.textContent ?? '';
171 }
172
173 it('bleibt bei den Regelwerten bei der Zuschreibung an die RiLSA', () => {
174 // Grenzwache: Solange die Regelwerte gelten, ist der Satz richtig.
175 const text = balken(zeichne(mitFestemAnsatz()));
176 expect(text).toContain('Die Werte sind die Regelwerte der RiLSA 2015 (Abschnitt 2.5.2');
177 expect(text, 'die Richtung des Regelansatzes').toContain('rechnet nie kürzer als diese');
178 expect(text, 'die Uebertreibung').not.toContain('längere Ansatz');
179 });
180
181 it('nennt eingetragene Werte Vorgaben dieses Plans', () => {
182 const text = balken(
183 zeichne(mitFestemAnsatz({ crossingTimeKfzGeradeaus: 6, crossingTimeKfzAbbiegend: 6 })),
184 );
185 expect(text, 'Vorbedingung: die eingetragenen Werte stehen im Balken').toContain('6 s');
186 expect(text).toContain('Die Werte sind Vorgaben dieses Plans');
187 expect(text).not.toContain('Die Werte sind die Regelwerte der RiLSA 2015');
188 expect(text, 'die belegten Zahlen gehoeren daneben').toContain(
189 `${RILSA_DEFAULTS.crossingTime.kfzGeradeaus} s geradeaus und ` +
190 `${RILSA_DEFAULTS.crossingTime.kfzAbbiegend} s abbiegend`,
191 );
192 });
193
194 /*
195 * NACHTRAG (Befund 4): Der Satz im Balken war bedingt gemacht, die
196 * UEBERSCHRIFT darueber nicht - und sie ist die staerkste und sichtbarste
197 * Aussage des Balkens. Bei 6 s festen Vorgaben stand dort weiter "die
198 * Zwischenzeiten fallen kürzer aus", waehrend der Satz drei Zeilen tiefer
199 * "nirgends kürzer ... nicht verkürzt" sagte. Gemessen:
200 * `ueberfahrzeitVerkuerzung` liefert dann -1 s je Richtung.
201 */
202 it('sagt in der Ueberschrift nur die Richtung, die die Vorgaben hergeben', () => {
203 expect(
204 ueberschrift(zeichne(mitFestemAnsatz())),
205 'bei den Regelwerten verkuerzt der Ansatz wirklich',
206 ).toBe('Feste Überfahrzeit gewählt – die Zwischenzeiten fallen kürzer aus');
207
208 const mitSechs = ueberschrift(
209 zeichne(mitFestemAnsatz({ crossingTimeKfzGeradeaus: 6, crossingTimeKfzAbbiegend: 6 })),
210 );
211 expect(mitSechs, 'Vorbedingung: der Balken ist da').toContain('Feste Überfahrzeit gewählt');
212 expect(mitSechs).not.toContain('fallen kürzer aus');
213 expect(mitSechs).toContain('nicht verkürzt');
214 });
215
216 it('widerspricht dem Feldhinweis darueber nicht', () => {
217 /*
218 * DIE PROBE, DIE DEN BEFUND GEFUNDEN HAETTE: Ueberschrift, Feldhinweis und
219 * Balken stehen im selben Bildschirmausschnitt und beantworten dieselbe
220 * Frage. Der Feldhinweis holte die Richtung seit 5.9.0 aus dem Fachkern,
221 * die Ueberschrift des Balkens und der Nebensatz zum Pruefbericht nicht -
222 * drei Zeilen auseinander stand beides, "nirgends kürzer" und "fallen
223 * kürzer aus".
224 */
225 const wurzel = zeichne(
226 mitFestemAnsatz({ crossingTimeKfzGeradeaus: 6, crossingTimeKfzAbbiegend: 6 }),
227 );
228 const zusammen = `${ueberschrift(wurzel)} ${feldhinweis(wurzel)} ${balken(wurzel)}`;
229
230 expect(zusammen, 'Vorbedingung: die Richtung steht im Feldhinweis').toContain(
231 'fällt die Überfahrzeit nirgends kürzer aus',
232 );
233 expect(zusammen, 'keine Verkuerzung behaupten, wo keine ist').not.toContain(
234 'fallen kürzer aus',
235 );
236 expect(zusammen, 'auch nicht im Nebensatz zum Pruefbericht').not.toContain(
237 'kürzere Zwischenzeiten ergibt',
238 );
239 });
240
241 it('sagt auch der Sprachausgabe nur die Richtung, die gilt', () => {
242 /*
243 * DIE KURZMELDUNG BEIM UMSCHALTEN. Sie ist fuer den da, der ohne Bildschirm
244 * plant - er hoert diesen einen Satz und sieht den Balken nie. Er sagte
245 * unbedingt "Die Zwischenzeiten der Kraftfahrzeugströme fallen kürzer aus".
246 */
247 const wurzel = zeichne(
248 mitVorgaben({ crossingTimeKfzGeradeaus: 6, crossingTimeKfzAbbiegend: 6 }),
249 );
250 const label = [...wurzel.querySelectorAll('label')].find(
251 (l) => beschriftungstext(l) === BESCHRIFTUNG,
252 );
253 const feld = wurzel.querySelector<HTMLSelectElement>(
254 `[id="${label?.getAttribute('for') ?? ''}"]`,
255 );
256 expect(feld, 'Vorbedingung: das Auswahlfeld ist da').not.toBeNull();
257 feld!.value = 'fest';
258 feld!.dispatchEvent(new Event('change'));
259
260 const meldung = [...document.querySelectorAll('.meldung')]
261 .map((k) => k.textContent ?? '')
262 .join(' ');
263 expect(meldung, 'Vorbedingung: die Kurzmeldung ist erschienen').toContain(
264 'Feste Überfahrzeit gewählt',
265 );
266 expect(meldung).toContain('nirgends kürzer aus');
267 expect(meldung).not.toContain('fallen kürzer aus');
268 });
269
270 it('haelt Balken und Pruefbericht auf derselben Antwort', () => {
271 /*
272 * Der Balken holt den Satz aus demselben Fachkern wie der Pruefbericht
273 * (ansaetze.ts, festeWerteZuschreibungSatz). Ohne das waeren es zwei
274 * Fassungen derselben Aussage - der Fehlertyp, aus dem dieser Befund
275 * entstanden ist.
276 */
277 expect(balken(zeichne(mitFestemAnsatz()))).toContain(
278 festeWerteZuschreibungSatz(RILSA_DEFAULTS),
279 );
280 });
281 });