lsa-planer

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

/ tests ui stilleErsetzungenAnzeige.test.ts

20,9 KB Rohdatei
tests/ui/stilleErsetzungenAnzeige.test.ts — 496 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { ProjectStore } from '@/app/store';
3 import { UMLAUFZEIT_ERSATZWERT_VERMERK, buildSignalPlan } from '@/domain/plan/signalPlan';
4 import { createStandardIntersectionProject } from '@/domain/model/factory';
5 import { ANLAGENART_GRENZEN } from '@/domain/rilsa/constants';
6 import type { Project } from '@/domain/model/project';
7 import type { ViewDefinition } from '@/ui/shell';
8 import {
9 ASSISTENT_VORGABE,
10 FURT_MINDESTWEGE,
11 assistentZusammenfassung,
12 fuelleZusammenfassung,
13 furtAnnahmeText,
14 furtWegeAnnahme,
15 projektAusAngaben,
16 starteAssistent,
17 } from '@/ui/assistent';
18 import { HILFE } from '@/ui/help/texte';
19 import { planView, umlaufzeitText } from '@/ui/views/planView';
20 import { settingsView } from '@/ui/views/settingsView';
21 import {
22 mindestfreigabeUntergrenze,
23 signalGroupsView,
24 wirksamerWertHinweis,
25 } from '@/ui/views/signalGroupsView';
26
27 /**
28 * Oberflaeche zu den Befunden C9 bis C12 - "stille Ersetzungen": Werte, die das
29 * Programm ersetzt, ohne es zu sagen.
30 *
31 * - C9: Das Feld der Mindestfreigabezeit hat als Untergrenze den Wert, der bei
32 * leerem Feld gilt; eine kleinere Eingabe wird angehoben und gemeldet, eine
33 * kleinere gespeicherte Vorgabe steht mit "wirksam N s" daneben.
34 * - C10: Die Vorgabenansicht loest die Vorgaben mit der Anlagenart auf - eine
35 * groesste Umlaufzeit von 250 s bleibt bei einstreifiger Fuehrung 250 s.
36 * - C11: Kennzahl und Kopfzeile des Signalzeitenplans tragen bei
37 * Uebersaettigung den Ersatzwert-Vermerk des Fachkerns.
38 * - C12: Der Assistent zeigt Schwerverkehrsanteil (Vorgabe 5 %) und
39 * Furt-Annahme mit Zahl und fasst die Annahmen im letzten Schritt zusammen.
40 * - Die Hilfe sagt, was das Programm tut - nicht mehr "wird abgewiesen".
41 *
42 * Gegen den Altstand schlagen die Faelle fehl: Feld ab 0 s ohne Hinweis,
43 * Ansicht ohne Anlagenart, nackte "120 s", stille 5 % und halbe Wege.
44 */
45
46 const FESTES_DATUM = new Date('2026-01-01T00:00:00Z');
47
48 function zeichne(
49 view: ViewDefinition,
50 project: Project,
51 ): { wurzel: HTMLElement; store: ProjectStore; aufraeumen: () => void } {
52 const wurzel = document.createElement('div');
53 document.body.append(wurzel);
54 const store = new ProjectStore(project);
55 const aufraeumen = view.render(wurzel, {
56 store,
57 refresh: () => {},
58 navigate: () => {},
59 target: null,
60 });
61 return {
62 wurzel,
63 store,
64 aufraeumen: () => {
65 aufraeumen();
66 wurzel.remove();
67 },
68 };
69 }
70
71 /** Spaltenindex zu einem sichtbaren Kopftext (ohne Hilfeknopf). */
72 function spalte(wurzel: ParentNode, kopf: string): number {
73 const koepfe = [...wurzel.querySelectorAll('thead th')].map((th) =>
74 [...th.childNodes]
75 .filter((k) => !(k instanceof Element && k.classList.contains('hilfe-knopf')))
76 .map((k) => k.textContent ?? '')
77 .join('')
78 .trim(),
79 );
80 const index = koepfe.indexOf(kopf);
81 if (index < 0) throw new Error(`Keine Spalte "${kopf}" in ${koepfe.join(' | ')}`);
82 return index;
83 }
84
85 function mindestfreigabeZelle(wurzel: ParentNode, id: string): HTMLElement {
86 const s = spalte(wurzel, 'Mindestfreigabe');
87 return wurzel.querySelector<HTMLTableRowElement>(`#sg-${id}`)!.children[s] as HTMLElement;
88 }
89
90 function uebersaettigt(): Project {
91 const p = createStandardIntersectionProject('Ersatzwert', FESTES_DATUM);
92 return { ...p, demands: p.demands.map((d) => ({ ...d, volume: 1500 })) };
93 }
94
95 afterEach(() => {
96 document.body.replaceChildren();
97 });
98
99 describe('Signalgruppen: Mindestfreigabezeit-Feld nimmt keine Unterschreitung an (Befund C9)', () => {
100 it('setzt die Untergrenze des Felds auf den Wert, der bei leerem Feld gilt', () => {
101 const roh = createStandardIntersectionProject('Furt', FESTES_DATUM);
102 const p: Project = {
103 ...roh,
104 signalGroups: roh.signalGroups.map((g) =>
105 g.name === 'F1' ? { ...g, blindenzusatz: true } : g,
106 ),
107 };
108 const plan = buildSignalPlan(p);
109 const { wurzel, aufraeumen } = zeichne(signalGroupsView, p);
110 try {
111 for (const gruppe of p.signalGroups) {
112 const feld = mindestfreigabeZelle(wurzel, gruppe.id).querySelector('input')!;
113 const erwartet = mindestfreigabeUntergrenze(p, gruppe, plan.defaults);
114 expect(feld.min, gruppe.name).toBe(String(erwartet));
115 expect(feld.title, gruppe.name).toContain(
116 `Eingaben unter ${erwartet} s werden auf ${erwartet} s angehoben.`,
117 );
118 // Ohne Vorgabe kein Zusatz "wirksam".
119 expect(mindestfreigabeZelle(wurzel, gruppe.id).textContent).not.toContain('wirksam');
120 }
121 // F1 mit Blindenzusatz: ganze Furt 12 m / 1,2 m/s = 10 s; K1: Regelwert 5 s.
122 const f1 = p.signalGroups.find((g) => g.name === 'F1')!;
123 const k1 = p.signalGroups.find((g) => g.name === 'K1')!;
124 expect(mindestfreigabeZelle(wurzel, f1.id).querySelector('input')!.min).toBe('10');
125 expect(mindestfreigabeZelle(wurzel, k1.id).querySelector('input')!.min).toBe('5');
126 } finally {
127 aufraeumen();
128 }
129 });
130
131 it('hebt eine Eingabe unter der Untergrenze auf diese an und speichert den angehobenen Wert', () => {
132 const p = createStandardIntersectionProject('Furt', FESTES_DATUM);
133 const k1 = p.signalGroups.find((g) => g.name === 'K1')!;
134 const { wurzel, store, aufraeumen } = zeichne(signalGroupsView, p);
135 try {
136 const feld = mindestfreigabeZelle(wurzel, k1.id).querySelector('input')!;
137 feld.value = '3';
138 feld.dispatchEvent(new Event('change'));
139 // Das Feld zeigt den angehobenen Wert und ist als korrigiert gekennzeichnet.
140 expect(feld.value).toBe('5');
141 expect(feld.getAttribute('aria-invalid')).toBe('true');
142 expect(store.getProject().signalGroups.find((g) => g.id === k1.id)!.minGreenOverride).toBe(5);
143 // Eine Eingabe ueber der Untergrenze geht unveraendert durch.
144 feld.value = '8';
145 feld.dispatchEvent(new Event('change'));
146 expect(store.getProject().signalGroups.find((g) => g.id === k1.id)!.minGreenOverride).toBe(8);
147 } finally {
148 aufraeumen();
149 }
150 });
151
152 it('zeigt neben einer gespeicherten kleineren Vorgabe den wirksamen Wert', () => {
153 // Altdatei: 3 s eingetragen, der Plan rechnet mit 5 s.
154 const roh = createStandardIntersectionProject('Altdatei', FESTES_DATUM);
155 const p: Project = {
156 ...roh,
157 signalGroups: roh.signalGroups.map((g) =>
158 g.name === 'K1' ? { ...g, minGreenOverride: 3 } : g,
159 ),
160 };
161 const k1 = p.signalGroups.find((g) => g.name === 'K1')!;
162 const k2 = p.signalGroups.find((g) => g.name === 'K2')!;
163 expect(buildSignalPlan(p).groups.find((g) => g.groupId === k1.id)!.times.minGreen).toBe(5);
164 const { wurzel, aufraeumen } = zeichne(signalGroupsView, p);
165 try {
166 const zelle = mindestfreigabeZelle(wurzel, k1.id);
167 const feld = zelle.querySelector('input')!;
168 // Der gespeicherte Wert bleibt sichtbar - er wird nicht still ueberschrieben.
169 expect(feld.value).toBe('3');
170 const zusatz = zelle.querySelector<HTMLElement>('.mindestfreigabe-wirksam');
171 expect(zusatz, 'Zusatz "wirksam" fehlt').not.toBeNull();
172 expect(zusatz!.textContent).toBe('wirksam 5 s');
173 expect(zusatz!.title).toContain('Vorgabe von 3 s');
174 expect(zusatz!.title).toContain('rechnet mit 5 s');
175 expect(zusatz!.title).toContain('Prüfbericht');
176 // Eine Gruppe ohne Vorgabe hat den Zusatz nicht.
177 expect(
178 mindestfreigabeZelle(wurzel, k2.id).querySelector('.mindestfreigabe-wirksam'),
179 ).toBeNull();
180 } finally {
181 aufraeumen();
182 }
183 });
184
185 it('bildet Untergrenze und Zusatz aus dem Fachkern, ohne eine hoehere Vorgabe zur Untergrenze zu machen', () => {
186 const roh = createStandardIntersectionProject('Vorgabe', FESTES_DATUM);
187 const p: Project = {
188 ...roh,
189 signalGroups: roh.signalGroups.map((g) =>
190 g.name === 'F1' ? { ...g, blindenzusatz: true, minGreenOverride: 15 } : g,
191 ),
192 };
193 const plan = buildSignalPlan(p);
194 const f1 = p.signalGroups.find((g) => g.name === 'F1')!;
195 // Massgebend ist die Vorgabe 15 s, die Untergrenze aber der Furtbedarf 10 s:
196 // Sonst liesse sich die Vorgabe nie mehr auf 13 s senken.
197 expect(plan.groups.find((g) => g.groupId === f1.id)!.times.minGreen).toBe(15);
198 expect(mindestfreigabeUntergrenze(p, f1, plan.defaults)).toBe(10);
199 expect(wirksamerWertHinweis(15, 15)).toBeNull();
200 expect(wirksamerWertHinweis(null, 5)).toBeNull();
201 expect(wirksamerWertHinweis(3, 5)).toBe('wirksam 5 s');
202 expect(wirksamerWertHinweis(5.5, 6)).toBe('wirksam 6 s');
203 });
204 });
205
206 describe('Vorgaben: Aufloesung mit der Anlagenart (Befund C10)', () => {
207 it('zeigt bei einstreifiger Fuehrung eine groesste Umlaufzeit von 250 s als Abweichung, nicht gekappt', () => {
208 const roh = createStandardIntersectionProject('Einstreifig', FESTES_DATUM);
209 const p: Project = {
210 ...roh,
211 anlagenart: 'einstreifig',
212 settings: { ...roh.settings, rilsa: { ...roh.settings.rilsa, cycleTimeMax: 250 } },
213 };
214 expect(ANLAGENART_GRENZEN.einstreifig.cycleTime.max).toBeGreaterThanOrEqual(250);
215 const { wurzel, aufraeumen } = zeichne(settingsView, p);
216 try {
217 const text = wurzel.textContent ?? '';
218 expect(text).toContain('Größte Umlaufzeit: 250 s statt 120 s');
219 } finally {
220 aufraeumen();
221 }
222 });
223
224 it('kappt dieselbe Vorgabe am Knotenpunkt auf 120 s und fuehrt sie nicht als Abweichung', () => {
225 const roh = createStandardIntersectionProject('Knotenpunkt', FESTES_DATUM);
226 const p: Project = {
227 ...roh,
228 settings: { ...roh.settings, rilsa: { ...roh.settings.rilsa, cycleTimeMax: 250 } },
229 };
230 const { wurzel, aufraeumen } = zeichne(settingsView, p);
231 try {
232 expect(wurzel.textContent ?? '').not.toContain('250 s statt 120 s');
233 } finally {
234 aufraeumen();
235 }
236 });
237 });
238
239 describe('Signalzeitenplan: Umlaufzeit mit Ersatzwert-Vermerk (Befund C11)', () => {
240 it('haengt bei Uebersaettigung den Vermerk an Kennzahl und Kopfzeile', () => {
241 const p = uebersaettigt();
242 const plan = buildSignalPlan(p);
243 expect(plan.cycleResult?.bounded, 'Vorbedingung').toBe('uebersaettigt');
244 const { wurzel, aufraeumen } = zeichne(planView, p);
245 try {
246 const kennzahl = [...wurzel.querySelectorAll('.kennzahl')].find(
247 (k) => k.querySelector('dt')?.textContent === 'Umlaufzeit',
248 )!;
249 // Der Vermerk steht IM WERT, nicht nur in einer Fussnote.
250 expect(kennzahl.querySelector('dd')?.textContent).toBe(
251 `${plan.cycleTime} s – ${UMLAUFZEIT_ERSATZWERT_VERMERK}`,
252 );
253 expect(kennzahl.querySelector('.kennzahl-fussnote')?.textContent).toContain('übersättigt');
254 // Und in der Kopfzeile der Ansicht.
255 const kopf = wurzel.querySelector('.ansicht-kopf p')!;
256 expect(kopf.textContent).toContain(
257 `Umlaufzeit ${plan.cycleTime} s – ${UMLAUFZEIT_ERSATZWERT_VERMERK}`,
258 );
259 } finally {
260 aufraeumen();
261 }
262 });
263
264 it('zeigt ohne Uebersaettigung die nackte Zahl', () => {
265 const p = createStandardIntersectionProject('Regelfall', FESTES_DATUM);
266 const plan = buildSignalPlan(p);
267 expect(umlaufzeitText(plan)).toBe(`${plan.cycleTime} s`);
268 const { wurzel, aufraeumen } = zeichne(planView, p);
269 try {
270 const kennzahl = [...wurzel.querySelectorAll('.kennzahl')].find(
271 (k) => k.querySelector('dt')?.textContent === 'Umlaufzeit',
272 )!;
273 expect(kennzahl.querySelector('dd')?.textContent).toBe(`${plan.cycleTime} s`);
274 expect(kennzahl.querySelector('.kennzahl-fussnote')).toBeNull();
275 expect(wurzel.textContent).not.toContain('Ersatzwert');
276 } finally {
277 aufraeumen();
278 }
279 });
280 });
281
282 describe('Assistent: Schwerverkehrsanteil und Furt-Annahme offengelegt (Befund C12)', () => {
283 it('traegt den angegebenen Schwerverkehrsanteil ein - Vorgabe 5 %', () => {
284 expect(ASSISTENT_VORGABE.schwerverkehrsanteil).toBe(5);
285 const vorgabe = projektAusAngaben(
286 { ...ASSISTENT_VORGABE, verkehrsstaerke: 400 },
287 'V',
288 FESTES_DATUM,
289 );
290 expect(vorgabe.demands.length).toBeGreaterThan(0);
291 for (const d of vorgabe.demands) expect(d.heavyVehicleShare).toBeCloseTo(0.05, 6);
292
293 const zwoelf = projektAusAngaben(
294 { ...ASSISTENT_VORGABE, verkehrsstaerke: 400, schwerverkehrsanteil: 12 },
295 'V',
296 FESTES_DATUM,
297 );
298 for (const d of zwoelf.demands) expect(d.heavyVehicleShare).toBeCloseTo(0.12, 6);
299 // Ohne Verkehrsstaerke keine Nachfrage - und damit auch kein Anteil.
300 expect(
301 projektAusAngaben({ ...ASSISTENT_VORGABE, schwerverkehrsanteil: 12 }, 'V', FESTES_DATUM)
302 .demands,
303 ).toEqual([]);
304 });
305
306 it('rechnet die Furtwege aus EINER Funktion und schreibt die Zahl in die Bemerkung der Beziehung', () => {
307 expect(furtWegeAnnahme({ raeumweg: 22 })).toEqual({
308 raeumweg: 11,
309 einfahrweg: 0,
310 });
311 /*
312 * Bei kurzen Wegen greift die Untergrenze von 6 m - aber nur am Raeumweg.
313 *
314 * GEAENDERT (Fassung 5.11.0, Befund 9): Hier stand die Erwartung, dass auch
315 * der EINFAHRWEG auf eine Untergrenze von 3 m angehoben wird. Damit schrieb
316 * dieser Fall ein Verhalten fest, das in die gefaehrliche Richtung wirkt:
317 * In tz = tue + tr - te wird die Einfahrzeit abgezogen, die Furt ist der
318 * einfahrende Strom, und ein groesserer Einfahrweg verkuerzt deshalb die
319 * Zwischenzeit ZUM Fussgaenger.
320 *
321 * NOCHMALS GEAENDERT (Fassung 5.43.0): Der Einfahrweg ist jetzt 0 m und
322 * nicht mehr die Haelfte - nach RiLSA 2015 ist die Einfahrzeit des
323 * Fussgaengers 0 s, wenn die Konfliktflaeche am Fahrbahnrand beginnt. Die
324 * Wache dazu steht in furtEinfahrweg.test.ts.
325 */
326 expect(furtWegeAnnahme({ raeumweg: 8 })).toEqual({
327 raeumweg: FURT_MINDESTWEGE.raeumweg,
328 einfahrweg: 0,
329 });
330
331 const p = projektAusAngaben(
332 { ...ASSISTENT_VORGABE, fussgaenger: true, raeumweg: 22, einfahrweg: 8 },
333 'F',
334 FESTES_DATUM,
335 );
336 const furten = new Set(p.signalGroups.filter((g) => g.mode === 'fuss').map((g) => g.id));
337 expect(furten.size).toBeGreaterThan(0);
338 for (const c of p.conflicts) {
339 if (furten.has(c.fromId)) {
340 expect(c.clearingDistance).toBe(11);
341 expect(c.note).toContain('halber Räumweg (11,0 m)');
342 expect(c.note).toContain('im Lageplan prüfen');
343 }
344 if (furten.has(c.toId)) {
345 expect(c.enteringDistance).toBe(0);
346 expect(c.note).toContain('Einfahrweg 0 m (Fahrbahnrand');
347 }
348 if (!furten.has(c.fromId) && !furten.has(c.toId)) {
349 expect(c.clearingDistance).toBe(22);
350 expect(c.enteringDistance).toBe(8);
351 expect(c.note).not.toContain('halber');
352 }
353 }
354 });
355
356 it('nennt die Furt-Annahme als Satz mit Zahl', () => {
357 const satz = furtAnnahmeText({ raeumweg: 22 });
358 expect(satz).toContain('Furtlänge = halber Räumweg: 11,0 m');
359 expect(satz).toContain('Einfahrweg 0 m');
360 // Nur noch der Raeumweg hat eine Untergrenze (Befund 9).
361 expect(satz).toContain('mindestens 6 m');
362 expect(satz).toContain('Annahme, im Lageplan prüfen');
363 });
364
365 it('fasst im letzten Schritt alle Annahmen mit Zahl zusammen', () => {
366 const kreuzung = assistentZusammenfassung({
367 ...ASSISTENT_VORGABE,
368 verkehrsstaerke: 400,
369 schwerverkehrsanteil: 7,
370 fussgaenger: true,
371 });
372 const text = kreuzung.join(' ');
373 expect(text).toContain('Schwerverkehrsanteil 7 %');
374 expect(text).toContain('400 Fz/h');
375 expect(text).toContain('Räumweg 22,0 m und Einfahrweg 8,0 m');
376 expect(text).toContain('Furtlänge = halber Räumweg: 11,0 m');
377 expect(text).toContain('50 km/h');
378
379 // Ohne Furten keine Furt-Annahme; ohne Verkehrsstaerke kein Anteil, dafuer der Hinweis.
380 const ohne = assistentZusammenfassung({ ...ASSISTENT_VORGABE, fussgaenger: false }).join(' ');
381 expect(ohne).not.toContain('Furtlänge');
382 expect(ohne).not.toContain('Schwerverkehrsanteil');
383 expect(ohne).toContain('Keine Verkehrsstärken');
384
385 // Die anderen Formen nennen ihre eigene Massannahme.
386 expect(
387 assistentZusammenfassung({
388 ...ASSISTENT_VORGABE,
389 form: 'fussgaengerschutzanlage',
390 fahrbahnbreite: 7,
391 }).join(' '),
392 ).toContain('Furtlänge = Fahrbahnbreite 7,0 m; Haltlinie 3 m vor der Furt');
393 expect(
394 assistentZusammenfassung({
395 ...ASSISTENT_VORGABE,
396 form: 'einstreifig',
397 engstellenlaenge: 120,
398 }).join(' '),
399 ).toContain('Länge der Engstelle 120 m');
400
401 // Der Kasten zeigt einen Satz je Zeile.
402 const kasten = document.createElement('div');
403 fuelleZusammenfassung(kasten, { ...ASSISTENT_VORGABE, verkehrsstaerke: 400 });
404 const punkte = [...kasten.querySelectorAll('li')].map((li) => li.textContent);
405 expect(punkte).toEqual(
406 assistentZusammenfassung({ ...ASSISTENT_VORGABE, verkehrsstaerke: 400 }),
407 );
408 expect(kasten.textContent).toContain('Das legt der Assistent an');
409 });
410
411 /**
412 * Der Dialog selbst: Im Verkehrs-Schritt gibt es das Feld, im Masse-Schritt
413 * den Satz zur Furt-Annahme mit Zahl (und er rechnet mit, wenn der Raeumweg
414 * sich aendert), und im letzten Schritt steht die Zusammenfassung ueber
415 * "anlegen". Was der Anwender im Feld eintraegt, landet im Projekt.
416 */
417 it('zeigt im Dialog Schwerverkehrsanteil, Furt-Annahme und Zusammenfassung - und uebernimmt die Eingabe', async () => {
418 const ergebnis = starteAssistent();
419 const dialog = (): HTMLElement => document.querySelector<HTMLElement>('.dialog')!;
420 const knopf = (beschriftung: string): HTMLButtonElement =>
421 [...dialog().querySelectorAll<HTMLButtonElement>('button')].find(
422 (b) => b.textContent?.trim() === beschriftung,
423 )!;
424 const feldMit = (beschriftung: string): HTMLInputElement => {
425 const label = [...dialog().querySelectorAll('label')].find(
426 (l) => l.textContent?.trim() === beschriftung,
427 )!;
428 expect(label, `Feld "${beschriftung}" fehlt`).toBeDefined();
429 return dialog().querySelector<HTMLInputElement>(`#${label.htmlFor}`)!;
430 };
431 const setzeZahl = (feld: HTMLInputElement, wert: string): void => {
432 feld.value = wert;
433 feld.dispatchEvent(new Event('change'));
434 };
435
436 // Schritt 1 (Art), Schritt 2 (Signalisierung): kein Zusammenfassungskasten.
437 expect(dialog().querySelector('.assistent-zusammenfassung')).toBeNull();
438 knopf('Weiter').click();
439 knopf('Weiter').click();
440
441 // Schritt 3: Verkehr - Verkehrsstaerke und Schwerverkehrsanteil mit Vorgabe 5.
442 const sv = feldMit('Schwerverkehrsanteil');
443 expect(sv.value).toBe('5');
444 expect(dialog().textContent).toContain('Vorgabe 5 %');
445 setzeZahl(feldMit('Verkehrsstärke je Zufahrt'), '400');
446 setzeZahl(sv, '9');
447 knopf('Weiter').click();
448
449 // Schritt 4: Masse - Furt-Annahme mit Zahl, rechnet mit; Zusammenfassung da.
450 const annahme = dialog().querySelector<HTMLElement>('.assistent-furtannahme')!;
451 expect(annahme).not.toBeNull();
452 expect(annahme.textContent).toContain(furtAnnahmeText({ raeumweg: 22 }));
453 setzeZahl(feldMit('Räumweg'), '30');
454 expect(annahme.textContent).toContain('halber Räumweg: 15,0 m');
455 const zusammenfassung = dialog().querySelector<HTMLElement>('.assistent-zusammenfassung')!;
456 expect(zusammenfassung).not.toBeNull();
457 expect(zusammenfassung.textContent).toContain('Schwerverkehrsanteil 9 %');
458 expect(zusammenfassung.textContent).toContain('halber Räumweg: 15,0 m');
459 expect(zusammenfassung.textContent).toContain('Räumweg 30,0 m');
460
461 knopf('Knotenpunkt anlegen').click();
462 const projekt = await ergebnis;
463 expect(projekt).not.toBeNull();
464 for (const d of projekt!.demands) expect(d.heavyVehicleShare).toBeCloseTo(0.09, 6);
465 const furten = new Set(projekt!.signalGroups.filter((g) => g.mode === 'fuss').map((g) => g.id));
466 const furtRaeumt = projekt!.conflicts.find((c) => furten.has(c.fromId))!;
467 expect(furtRaeumt.clearingDistance).toBe(15);
468 });
469 });
470
471 describe('Hilfetexte zu C9, C11 und C12', () => {
472 it('beschreibt bei der Mindestfreigabezeit das Anheben, nicht ein Abweisen', () => {
473 const text = HILFE.mindestfreigabezeit.wennFalsch ?? '';
474 expect(text).not.toContain('wird abgewiesen');
475 expect(text).toContain('auf die maßgebende Mindestfreigabezeit angehoben');
476 expect(text).toContain('wirksam');
477 expect(text).toContain('Vorgabe unter der Mindestfreigabezeit');
478 });
479
480 it('nennt bei der Umlaufzeit den Ersatzwert bei Uebersaettigung mit dem Wortlaut des Vermerks', () => {
481 expect(HILFE.umlaufzeit.woher).toContain('nicht bemessbar (übersättigt) – Ersatzwert');
482 expect(HILFE.umlaufzeit.woher).toContain('kein Bemessungsergebnis');
483 });
484
485 it('nennt beim Schwerverkehrsanteil die Vorgabe des Assistenten und beim Raeumweg die Furt-Annahme', () => {
486 expect(HILFE.schwerverkehrsanteil.woher).toContain(
487 `Vorgabe ${ASSISTENT_VORGABE.schwerverkehrsanteil} Prozent`,
488 );
489 expect(HILFE.raeumweg.woher).toContain('halben Räumweg der Kfz-Ströme');
490 // Die Untergrenze gilt nur noch fuer den Raeumweg (Befund 9);
491 // seit 5.43.0 setzt der Einstieg ueberhaupt keinen Einfahrweg
492 // mehr an, und die Hilfe sagt warum.
493 expect(HILFE.raeumweg.woher).toContain(`(mindestens ${FURT_MINDESTWEGE.raeumweg} m)`);
494 expect(HILFE.raeumweg.woher).toContain('Einfahrweg 0 m');
495 });
496 });