lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests ui stilleErsetzungenAnzeige.test.ts
| 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 | }); |