lsa-planer

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

/ tests domain hilfetexteGelbzeitkopplung.test.ts

15,6 KB Rohdatei
tests/domain/hilfetexteGelbzeitkopplung.test.ts — 342 Zeilen
1 import { readFileSync } from 'node:fs';
2 import { describe, expect, it } from 'vitest';
3 import { HILFE } from '@/ui/help/texte';
4 import * as actions from '@/app/actions';
5 import { createEmptyProject } from '@/domain/model/factory';
6 import { buildSignalPlan, massgebendeRaeumbeziehung } from '@/domain/plan/signalPlan';
7 import { computeIntergreen, regelUeberfahrzeit } from '@/domain/rilsa/intergreen';
8 import type { ClearingStream, EnteringStream } from '@/domain/rilsa/types';
9 import { RILSA_DEFAULTS } from '@/domain/rilsa/constants';
10 import { resolveRilsaSettings } from '@/domain/rilsa/settings';
11 import { gelbzeitstaffelSatz } from '@/services/export/bewertung';
12
13 /*
14 * Fassung 5.12.0 - die beiden Hilfetexte, die dem Anwender Falsches sagten.
15 *
16 * Beide Waechter stehen in zwei Stufen, wie die Unterlagenwachen derselben Fassung:
17 * zuerst der SACHSTAND am Quelltext beziehungsweise am laufenden Rechenkern,
18 * dann der Wortlaut des Hilfetextes. Faellt der Sachstand, faellt er zuerst -
19 * dann hat sich nicht der Text geirrt, sondern das Programm hat sich geaendert
20 * und der Text ist mitzufuehren.
21 *
22 * Die Hilfe ist ein Versprechen an den Anwender: Er handelt danach. Ein
23 * Hilfetext, der eine Wirkung zusagt, die es nicht gibt, ist deshalb kein
24 * Schoenheitsfehler - beim vZul-Eintrag zeigte er in die unsichere Richtung.
25 */
26
27 const FEST = new Date('2026-01-01T00:00:00Z');
28
29 const PDF = readFileSync('src/services/export/pdf.ts', 'utf8');
30
31 /** Absaetze der Hilfe brechen selbst um; verglichen wird ohne Umbrueche. */
32 function einzeilig(text: string): string {
33 return text.replace(/\s+/g, ' ');
34 }
35
36 /**
37 * Ein langer Satz steht im Quelltext als Kette aneinandergehaengter
38 * Zeichenkettenteile. Fuer den Wortlautvergleich werden die Nahtstellen
39 * geschlossen; danach liest sich der Satz so, wie er auf dem Papier steht.
40 */
41 function zusammengesetzt(quelltext: string): string {
42 return einzeilig(quelltext.replace(/'\s*\+\s*'/g, ''));
43 }
44
45 describe('A.1: Der Eintrag "Raeumgeschwindigkeit" nennt den massgebenden Strom', () => {
46 /*
47 * Sachstand: Ueber die Fahrbeziehung entscheidet
48 * `massgebendeRaeumbeziehung` - die der KONFLIKTBEZIEHUNG, wo eine
49 * festgehalten ist, sonst die der SIGNALGRUPPE.
50 */
51 it('entscheidet der Rechenkern an der Beziehung, nicht an der Gruppe', () => {
52 expect(
53 massgebendeRaeumbeziehung({ movement: 'geradeaus' }, { raeumbeziehung: 'rechts' }),
54 'die Fahrbeziehung der Beziehung setzt sich nicht mehr durch',
55 ).toBe('rechts');
56 expect(
57 massgebendeRaeumbeziehung({ movement: 'geradeaus' }, {}),
58 'ohne festgehaltenen Strom gilt nicht mehr die Gruppe',
59 ).toBe('geradeaus');
60 });
61
62 const woher = einzeilig(HILFE.raeumgeschwindigkeit.woher ?? '');
63
64 it('sagt, welche Fahrbeziehung gilt', () => {
65 expect(woher, 'der massgebende Strom ist weiterhin nicht genannt').toContain(
66 'entscheidet der Strom, dem der Räumweg dieser Konfliktbeziehung gehört, und nicht die ' +
67 'räumende Signalgruppe',
68 );
69 expect(woher, 'der Regelfall der Sammelsignalgruppe fehlt').toContain(
70 'Führt eine Signalgruppe geradeaus und rechts aus einem Fahrstreifen',
71 );
72 });
73
74 it('nennt den Rueckfall auf die Fahrbeziehung der Signalgruppe', () => {
75 expect(woher, 'der Rueckfall fehlt').toContain(
76 'Ist nicht festgehalten, welcher Strom den Räumweg gefahren ist, wird mit der ' +
77 'Fahrbeziehung der Signalgruppe gerechnet, und der Prüfbericht meldet das.',
78 );
79 expect(woher, 'woher der massgebende Strom kommt, steht nicht dabei').toContain(
80 'Festgehalten wird der maßgebende Strom allein bei der Übernahme aus dem Lageplan',
81 );
82 });
83
84 it('sagt den Rueckfall woertlich wie die Planunterlage', () => {
85 /*
86 * Der Grund, aus dem der Satz einmal formuliert ist
87 * (`raeumbeziehungSatz`, domain/rilsa/ansaetze.ts): Vier Auskunftsstellen
88 * mit vier Formulierungen desselben Sachverhalts sind vier Gelegenheiten
89 * auseinanderzulaufen. Aufrufen kann der Hilfetext die Funktion nicht - sie
90 * braucht Signalgruppe und Konfliktbeziehung, die es hier nicht gibt -,
91 * also bindet ihn diese Wache an den Wortlaut der Planunterlage.
92 */
93 const rueckfall =
94 'Ist nicht festgehalten, welcher Strom den Räumweg gefahren ist, wird mit der ' +
95 'Fahrbeziehung der Signalgruppe gerechnet, und der Prüfbericht meldet das.';
96 expect(zusammengesetzt(PDF), 'der Wortlaut der Planunterlage hat sich geaendert').toContain(
97 rueckfall,
98 );
99 expect(woher, 'Hilfe und Planunterlage sagen den Rueckfall verschieden').toContain(rueckfall);
100 });
101 });
102
103 describe('A.2: Der Eintrag "Zulaessige Hoechstgeschwindigkeit" schreibt sich die Gelbzeit nicht mehr zu', () => {
104 /*
105 * Sachstand am laufenden Rechenkern, nicht an einer Zeichenkette: Der
106 * Knotenpunktwert belegt neu angelegte Signalgruppen vor - mehr nicht. Wird
107 * er nachtraeglich angehoben, aendert sich keine Gelbzeit.
108 */
109 function planMitEinerKfzGruppe(intersectionVZul: number, gruppenVZul?: number) {
110 let projekt = createEmptyProject('A2', FEST);
111 projekt = actions.setIntersection(projekt, { vZul: intersectionVZul });
112 projekt = actions.addSignalGroup(projekt, 'kfz');
113 const gruppe = projekt.signalGroups[0]!;
114 if (gruppenVZul !== undefined) {
115 projekt = actions.updateSignalGroup(projekt, gruppe.id, { vZul: gruppenVZul });
116 }
117 // Ohne Phase liefert `buildSignalPlan` einen leeren Plan.
118 projekt = actions.addPhase(projekt);
119 projekt = actions.updatePhase(projekt, projekt.phases[0]!.id, {
120 signalGroupIds: [gruppe.id],
121 });
122 return buildSignalPlan(projekt);
123 }
124
125 it('belegt der Knotenpunktwert nur vor und bestimmt keine Gelbzeit', () => {
126 // Vorbelegung: Die Gruppe entsteht mit dem Knotenpunktwert, und AN IHR
127 // haengt die Gelbzeit.
128 expect(planMitEinerKfzGruppe(50).groups[0]?.times.yellow, 'Vorbelegung 50 km/h').toBe(3);
129 expect(planMitEinerKfzGruppe(70).groups[0]?.times.yellow, 'Vorbelegung 70 km/h').toBe(5);
130
131 // Und die unsichere Richtung: Der Knotenpunktwert steht auf 70, die
132 // bestehende Gruppe auf 50 - gerechnet wird mit 3 s, nicht mit 5 s.
133 expect(
134 planMitEinerKfzGruppe(70, 50).groups[0]?.times.yellow,
135 'der Knotenpunktwert wirkt nachtraeglich doch auf die Gelbzeit',
136 ).toBe(3);
137 });
138
139 const wennFalsch = einzeilig(HILFE.vZulKnoten.wennFalsch ?? '');
140
141 it('nimmt die falsche Zuschreibung zurueck und bleibt sicherheitsrelevant', () => {
142 expect(wennFalsch, 'die falsche Zuschreibung steht noch da').not.toContain(
143 'Aus diesem Wert leitet das Programm die Gelbzeit ab',
144 );
145 expect(wennFalsch, 'die Sicherheitsrelevanz ist mit weggefallen').toContain(
146 'Sicherheitsrelevant',
147 );
148 expect(wennFalsch, 'der Eintrag sagt nicht, dass er selbst keine Gelbzeit bestimmt').toContain(
149 'Dieser Wert bestimmt selbst keine Gelbzeit',
150 );
151 expect(wennFalsch, 'die Vorbelegung ist nicht benannt').toContain(
152 'Er belegt neu angelegte Zufahrten und Signalgruppen vor; bestehende behalten ihren Wert.',
153 );
154 expect(wennFalsch, 'die mittelbare Wirkung fehlt').toContain(
155 'über die daraus angelegten Gruppen',
156 );
157 });
158
159 it('nennt die unsichere Richtung', () => {
160 expect(wennFalsch, 'die unsichere Richtung ist nicht benannt').toContain(
161 'das ist die unsichere Richtung',
162 );
163 expect(wennFalsch, 'der Fall aus Befund 23 fehlt').toContain(
164 'von 50 auf 70 km/h setzt, um längere Zwischenzeiten zu bekommen, bekommt keine',
165 );
166 // Was der Anwender statt dessen tun muss - sonst ist die Warnung
167 // folgenlos.
168 expect(wennFalsch, 'der Ausweg fehlt').toContain(
169 'an jeder betroffenen Signalgruppe nach (Spalte „V zul“ unter „Signalgruppen“)',
170 );
171 });
172
173 it('gibt die einstellbare Gelbzeitstaffel nicht als geltende Zahlen aus', () => {
174 /*
175 * Befund C21: `yellowKfzUpTo50`, `yellowKfzUpTo60` und `yellowKfzAbove60`
176 * sind einstellbar. Die Projektansicht setzt deshalb
177 * `gelbzeitstaffelSatz()` ein. Die Hilfe wird einmal beim Laden des Moduls
178 * gebildet und kennt die Vorgaben des geoeffneten Plans nicht; auch
179 * `gelbzeitstaffelSatz()` lieferte hier nur die Regelstaffel. Deshalb nennt
180 * der Eintrag keine Zahlen, sondern die Stellen mit dem wirksamen Wert. Der
181 * Nachweis dafuer steht unten unter "Die Begruendungen in den Kommentaren".
182 */
183 expect(wennFalsch, 'die feste Zahlenstaffel steht wieder im Text').not.toMatch(/\bSekunden?\b/);
184 expect(wennFalsch, 'die alte Staffel ist zurueck').not.toMatch(/bis\s*50\s*km\/h/);
185 expect(wennFalsch, 'der Verweis auf die wirksame Staffel fehlt').toContain(
186 'Die Staffel ist unter „Vorgaben“ einstellbar',
187 );
188 });
189 });
190
191 describe('A.3: Die Kopplung Gelbzeit → Zwischenzeit gilt nur bei einem der beiden Ansaetze', () => {
192 /*
193 * NACHTRAG (Fassung 5.12.0): Beide Eintraege sagten die
194 * Kopplung unbedingt zu - "die Gelbzeit geht zugleich als Ueberfahrzeit in
195 * die Zwischenzeit ein". Das ist dieselbe Bauart wie die feste
196 * Zahlenstaffel, die A.2 zu Recht entfernt hat: eine einstellbare Wirkung
197 * als unbedingte Zusage. Der Ueberfahrzeit-Ansatz ist eine
198 * Projekteinstellung (Vorgabe 'gelbzeit', domain/model/factory.ts).
199 *
200 * Sachstand am Rechenkern, nicht an einer Zeichenkette.
201 */
202 const OHNE_EINFAHRWEG: EnteringStream = {
203 mode: 'kfz',
204 movement: 'geradeaus',
205 enteringDistance: 0,
206 };
207
208 /** Abbiegender Kfz-Strom, 20 m Raeumweg, Pkw - nur vZul und Ansatz wechseln. */
209 function abbieger(vZul: number, ueberfahrzeitAnsatz: 'gelbzeit' | 'fest'): ClearingStream {
210 return {
211 mode: 'kfz',
212 movement: 'rechts',
213 clearingDistance: 20,
214 vehicleClass: 'pkw',
215 vZul,
216 ueberfahrzeitAnsatz,
217 };
218 }
219
220 it('haengt die Zwischenzeit bei "feste Ueberfahrzeit" nicht an der Gelbzeit', () => {
221 // Ansatz "Ueberfahrzeit = Gelbzeit": 50 km/h -> 3 s, 70 km/h -> 5 s. Die
222 // Gelbzeit steht in der Zwischenzeit.
223 const gelb50 = computeIntergreen(abbieger(50, 'gelbzeit'), OHNE_EINFAHRWEG);
224 const gelb70 = computeIntergreen(abbieger(70, 'gelbzeit'), OHNE_EINFAHRWEG);
225 expect(gelb50.crossingTime, 'die Gelbzeit ist nicht mehr die Ueberfahrzeit').toBe(3);
226 expect(gelb70.crossingTime).toBe(5);
227 expect(gelb50.intergreen, 'die Gelbzeit wirkt nicht mehr auf die Zwischenzeit').not.toBe(
228 gelb70.intergreen,
229 );
230
231 // Ansatz "feste Ueberfahrzeit": 2 s abbiegend, gleich bei welcher
232 // Hoechstgeschwindigkeit. `kfzUeberfahrzeit` ruft `yellowTime` gar nicht
233 // auf - eine kuerzere Gelbzeit kuerzt hier keine Zwischenzeit.
234 const fest50 = computeIntergreen(abbieger(50, 'fest'), OHNE_EINFAHRWEG);
235 const fest70 = computeIntergreen(abbieger(70, 'fest'), OHNE_EINFAHRWEG);
236 expect(fest50.crossingTime, 'die feste Ueberfahrzeit folgt doch der Gelbzeit').toBe(2);
237 expect(fest70.crossingTime).toBe(2);
238 expect(fest50.intergreen, 'die Zwischenzeit haengt bei "fest" doch an vZul').toBe(
239 fest70.intergreen,
240 );
241
242 // Und der Radverkehr: Er hat eine Gelbzeit (`yellowRad`), seine
243 // Ueberfahrzeit haengt aber an keiner - deshalb sagt der Eintrag
244 // "Gelbzeit" die Kopplung nur fuer den Kraftfahrzeugverkehr zu.
245 expect(
246 regelUeberfahrzeit({ mode: 'rad', movement: 'geradeaus', vZul: 30 }),
247 'die Ueberfahrzeit des Radverkehrs folgt doch der Gelbzeit',
248 ).toBe(RILSA_DEFAULTS.crossingTime.radDefault);
249 expect(RILSA_DEFAULTS.crossingTime.radDefault, 'Regelwert Radverkehr').not.toBe(
250 RILSA_DEFAULTS.yellowRad,
251 );
252 });
253
254 it('binden beide Hilfetexte die Kopplung an den Ansatz', () => {
255 const vZul = einzeilig(HILFE.vZulKnoten.wennFalsch ?? '');
256 expect(vZul, 'die Kopplung steht wieder unbedingt da').not.toContain(
257 'weil die Gelbzeit zugleich als Überfahrzeit in die Zwischenzeit eingeht',
258 );
259 expect(vZul, 'der Ansatz, unter dem die Kopplung gilt, ist nicht genannt').toContain(
260 'beim Ansatz „Überfahrzeit = Gelbzeit“ – der Vorgabe – ist die Gelbzeit zugleich die ' +
261 'Überfahrzeit',
262 );
263 expect(vZul, 'der andere Ansatz fehlt').toContain(
264 'Beim Ansatz „feste Überfahrzeit“ tut sie das nicht',
265 );
266 expect(vZul, 'die Folge steht weiter unbedingt da').toContain(
267 'solange der Ansatz „Überfahrzeit = Gelbzeit“ gilt, damit zu kurze Zwischenzeiten',
268 );
269
270 // Derselbe Sachverhalt im Eintrag "Gelbzeit" - er stand dort wortverwandt
271 // und ebenso unbedingt. Zwei Eintraege, die einander widersprechen, sind
272 // schlimmer als einer, der irrt.
273 const gelb = einzeilig(HILFE.gelbzeit.wennFalsch ?? '');
274 expect(gelb, 'der Eintrag "Gelbzeit" sagt die Kopplung wieder unbedingt zu').not.toContain(
275 'Die Gelbzeit ist zugleich die Überfahrzeit in der Zwischenzeitberechnung.',
276 );
277 expect(gelb, 'der Ansatz fehlt im Eintrag "Gelbzeit"').toContain(
278 'Beim Kraftfahrzeugverkehr und dem Ansatz „Überfahrzeit = Gelbzeit“',
279 );
280 expect(gelb, 'der Fall "feste Überfahrzeit" fehlt im Eintrag "Gelbzeit"').toContain(
281 'Beim Ansatz „feste Überfahrzeit“ und beim Radverkehr geht sie in keine Zwischenzeit ein',
282 );
283 });
284 });
285
286 describe('Die Begruendungen in den Kommentaren halten dem eigenen Quelltext stand', () => {
287 /*
288 * NACHTRAG (Fassung 5.12.0): Zwei Kommentare begruendeten
289 * eine richtige Behebung falsch. Eine widerlegte Begruendung ist keine
290 * Kleinigkeit - sie laedt die naechste Gruppe zur falschen Behebung ein: Wer
291 * liest, ein Hilfetext koenne keine Funktion aufrufen, setzt
292 * `gelbzeitstaffelSatz()` ein und haelt C21 fuer erledigt.
293 */
294 const TEXTE = readFileSync('src/ui/help/texte.ts', 'utf8');
295 const ANSAETZE = readFileSync('src/domain/rilsa/ansaetze.ts', 'utf8');
296 const ENGINE = readFileSync('src/domain/validation/engine.ts', 'utf8');
297
298 /** Blockkommentare tragen je Zeile ein fuehrendes "*"; verglichen wird ohne. */
299 const KOMMENTARFLUSS = einzeilig(TEXTE.replace(/\n\s*\*/g, ' '));
300
301 it('nennt fuer die fehlende Zahlenstaffel den Grund, der traegt', () => {
302 // Widerlegung der alten Begruendung: Ein Hilfetext dieser Datei setzt sehr
303 // wohl einen gerechneten Wert ein.
304 expect(
305 HILFE.mindestfreigabezeit.wasIstDas,
306 'kein Hilfetext dieser Datei setzt mehr einen Kennwert ein',
307 ).toContain(`${RILSA_DEFAULTS.minGreen.oepnv} s`);
308
309 // Der wirkliche Grund: `HILFE` entsteht beim Laden des Moduls, und ohne
310 // die Vorgaben des Plans liefert `gelbzeitstaffelSatz()` die Regelstaffel.
311 expect(gelbzeitstaffelSatz(), 'der Vorgabeparameter ist nicht mehr die Regelstaffel').toBe(
312 gelbzeitstaffelSatz(RILSA_DEFAULTS),
313 );
314 const wirksam = resolveRilsaSettings({ yellowKfzUpTo50: 4 }).defaults;
315 expect(
316 gelbzeitstaffelSatz(wirksam),
317 'die Staffel haengt nicht mehr an den Vorgaben - dann traegt die Begruendung nicht',
318 ).not.toBe(gelbzeitstaffelSatz());
319
320 expect(KOMMENTARFLUSS, 'die widerlegte Begruendung steht wieder da').not.toContain(
321 'Ein Hilfetext ist eine statische Zeichenkette und kann die Funktion nicht aufrufen',
322 );
323 expect(KOMMENTARFLUSS, 'der tragende Grund fehlt').toContain(
324 'kennt die Vorgaben des geoeffneten Plans nicht',
325 );
326 });
327
328 it('nennt `raeumbeziehungSatz` an seiner Fundstelle', () => {
329 expect(ANSAETZE, 'der Satz steht nicht mehr im Fachkern').toContain(
330 'export function raeumbeziehungSatz(',
331 );
332 expect(ENGINE, 'der Pruefbericht fuehrt den Satz wieder selbst').not.toContain(
333 'function raeumbeziehungSatz(',
334 );
335 expect(KOMMENTARFLUSS, 'die falsche Fundstelle steht wieder da').not.toContain(
336 'raeumbeziehungSatz in domain/validation/engine.ts',
337 );
338 expect(KOMMENTARFLUSS, 'die richtige Fundstelle fehlt').toContain(
339 '`raeumbeziehungSatz`, domain/rilsa/ansaetze.ts, verwendet in domain/validation/engine.ts',
340 );
341 });
342 });