lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests ui regelwertBeschriftung.test.ts
| 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 | createStandardIntersectionProject, |
| 9 | } from '@/domain/model/factory'; |
| 10 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 11 | import { RILSA_ENGSTELLE, UEBERFAHRZEIT_ANSATZ_LABELS } from '@/domain/rilsa/constants'; |
| 12 | import type { Project } from '@/domain/model/project'; |
| 13 | import type { ViewDefinition } from '@/ui/shell'; |
| 14 | import { conflictsView } from '@/ui/views/conflictsView'; |
| 15 | |
| 16 | /** |
| 17 | * Befunde zur Fassung 5.8.0. |
| 18 | * |
| 19 | * Befund 30 Das Konfliktfenster beschriftete die drei Felder |
| 20 | * Raeumgeschwindigkeit, Einfahrgeschwindigkeit und Ueberfahrzeit mit |
| 21 | * "Regelwert <Zahl>" und las die Zahl aus dem RECHENERGEBNIS. Bei |
| 22 | * gesetzter Handeingabe nannte das Feld damit die eigene Eingabe des |
| 23 | * Bearbeiters "Regelwert"; die Bezugsgroesse, gegen die er sie |
| 24 | * abwaegen soll, war im Fenster nicht mehr zu sehen. Bei der |
| 25 | * Einfahrgeschwindigkeit widersprach das Fenster unmittelbar der |
| 26 | * Warnung derselben Rechnung zwei Bloecke tiefer. |
| 27 | * Befund 58 Die Zeile "Ueberfahrzeit tue" im Rechenweg wies eine VERWORFENE |
| 28 | * Handeingabe weiterhin als "von Hand eingetragen" aus, obwohl mit |
| 29 | * dem Regelwert gerechnet wurde. |
| 30 | */ |
| 31 | |
| 32 | const FESTES_DATUM = new Date('2026-01-01T00:00:00Z'); |
| 33 | |
| 34 | function zeichne( |
| 35 | view: ViewDefinition, |
| 36 | project: Project, |
| 37 | ): { wurzel: HTMLElement; aufraeumen: () => void } { |
| 38 | const wurzel = document.createElement('div'); |
| 39 | document.body.append(wurzel); |
| 40 | const abmelden = view.render(wurzel, { |
| 41 | store: new ProjectStore(project), |
| 42 | refresh: () => {}, |
| 43 | navigate: () => {}, |
| 44 | target: null, |
| 45 | }); |
| 46 | return { |
| 47 | wurzel, |
| 48 | aufraeumen: () => { |
| 49 | abmelden(); |
| 50 | wurzel.remove(); |
| 51 | }, |
| 52 | }; |
| 53 | } |
| 54 | |
| 55 | /** Sichtbarer Text einer Beschriftung - ohne den Hilfeknopf. */ |
| 56 | function beschriftungstext(label: HTMLLabelElement): string { |
| 57 | return [...label.childNodes] |
| 58 | .filter((knoten) => !(knoten instanceof Element && knoten.classList.contains('hilfe-knopf'))) |
| 59 | .map((knoten) => knoten.textContent ?? '') |
| 60 | .join('') |
| 61 | .trim(); |
| 62 | } |
| 63 | |
| 64 | function feldZu(wurzel: ParentNode, beschriftung: string): HTMLElement | null { |
| 65 | const label = [...wurzel.querySelectorAll('label')].find( |
| 66 | (l) => beschriftungstext(l) === beschriftung, |
| 67 | ); |
| 68 | const id = label?.getAttribute('for'); |
| 69 | if (id === undefined || id === null || id === '') return null; |
| 70 | return wurzel.querySelector<HTMLElement>(`[id="${id}"]`); |
| 71 | } |
| 72 | |
| 73 | function hinweisZu(wurzel: ParentNode, beschriftung: string): string { |
| 74 | const feld = feldZu(wurzel, beschriftung); |
| 75 | expect(feld, `Das Feld "${beschriftung}" fehlt im Fenster`).not.toBeNull(); |
| 76 | const id = feld!.getAttribute('aria-describedby'); |
| 77 | if (id === null) return ''; |
| 78 | return id |
| 79 | .split(' ') |
| 80 | .map((teil) => wurzel.querySelector(`[id="${teil}"]`)?.textContent ?? '') |
| 81 | .join(' '); |
| 82 | } |
| 83 | |
| 84 | /** Erlaeuterungsspalte einer Zeile des Rechenwegs. */ |
| 85 | function rechenwegZeile(fenster: ParentNode, bezeichnung: string): string { |
| 86 | const zeile = [...fenster.querySelectorAll('tbody tr')].find( |
| 87 | (tr) => tr.children[0]?.textContent?.trim() === bezeichnung, |
| 88 | ); |
| 89 | if (!zeile) throw new Error(`Zeile "${bezeichnung}" fehlt im Rechenweg`); |
| 90 | return zeile.children[2]?.textContent?.trim() ?? ''; |
| 91 | } |
| 92 | |
| 93 | function oeffneBeziehung(wurzel: HTMLElement, project: Project, fromName: string): ParentNode { |
| 94 | const von = project.signalGroups.find((g) => g.name === fromName)!; |
| 95 | const konflikt = project.conflicts.find((c) => c.fromId === von.id)!; |
| 96 | const zeile = project.signalGroups.findIndex((g) => g.id === konflikt.fromId); |
| 97 | const spalte = project.signalGroups.findIndex((g) => g.id === konflikt.toId); |
| 98 | const zellen = [...wurzel.querySelectorAll<HTMLButtonElement>('.matrix-zelle')]; |
| 99 | const n = project.signalGroups.length; |
| 100 | const versatz = spalte > zeile ? spalte - 1 : spalte; |
| 101 | zellen[zeile * (n - 1) + versatz]?.click(); |
| 102 | const fenster = document.querySelector('.dialog'); |
| 103 | expect(fenster, `Das Fenster zu ${fromName} ist nicht offen`).not.toBeNull(); |
| 104 | return fenster as ParentNode; |
| 105 | } |
| 106 | |
| 107 | /** Knotenpunkt, dessen erste Signalgruppe rechts abbiegt. */ |
| 108 | function mitAbbieger(aenderung: Partial<Project['conflicts'][number]> = {}): Project { |
| 109 | const basis = createStandardIntersectionProject('Beschriftung', FESTES_DATUM); |
| 110 | const erste = basis.signalGroups[0]!; |
| 111 | return { |
| 112 | ...basis, |
| 113 | signalGroups: basis.signalGroups.map((g) => |
| 114 | g.id === erste.id ? { ...g, movement: 'rechts' as const } : g, |
| 115 | ), |
| 116 | conflicts: basis.conflicts.map((c) => (c.fromId === erste.id ? { ...c, ...aenderung } : c)), |
| 117 | }; |
| 118 | } |
| 119 | |
| 120 | afterEach(() => { |
| 121 | document.body.replaceChildren(); |
| 122 | }); |
| 123 | |
| 124 | describe('Befund 30 - "Regelwert" ist nicht die eigene Eingabe', () => { |
| 125 | it('nennt bei eingetragener Raeumgeschwindigkeit den Regelwert der Fahrbeziehung', () => { |
| 126 | const project = mitAbbieger({ clearingSpeedOverride: 9 }); |
| 127 | const { wurzel, aufraeumen } = zeichne(conflictsView, project); |
| 128 | try { |
| 129 | const fenster = oeffneBeziehung(wurzel, project, 'K1'); |
| 130 | const hinweis = hinweisZu(fenster, 'Räumgeschwindigkeit'); |
| 131 | expect(hinweis).toContain('Angesetzt 9 m/s'); |
| 132 | expect(hinweis).toContain('von Hand'); |
| 133 | expect(hinweis).toContain('Regelwert 7 m/s'); |
| 134 | expect(hinweis).not.toContain('Regelwert 9'); |
| 135 | } finally { |
| 136 | aufraeumen(); |
| 137 | } |
| 138 | }); |
| 139 | |
| 140 | it('nennt bei eingetragener Einfahrgeschwindigkeit den Regelwert des einfahrenden Stroms', () => { |
| 141 | const project = mitAbbieger({ enteringSpeedOverride: 5 }); |
| 142 | const { wurzel, aufraeumen } = zeichne(conflictsView, project); |
| 143 | try { |
| 144 | const fenster = oeffneBeziehung(wurzel, project, 'K1'); |
| 145 | const hinweis = hinweisZu(fenster, 'Einfahrgeschwindigkeit'); |
| 146 | expect(hinweis).toContain('Angesetzt 5 m/s'); |
| 147 | expect(hinweis).toContain('Regelwert 11,1 m/s'); |
| 148 | expect(hinweis).not.toContain('Regelwert 5'); |
| 149 | } finally { |
| 150 | aufraeumen(); |
| 151 | } |
| 152 | }); |
| 153 | |
| 154 | it('nennt bei eingetragener Ueberfahrzeit den Wert des Ansatzes ohne diesen Eintrag', () => { |
| 155 | /* |
| 156 | * Seit Fassung 5.43.0 (Befund 17) nicht mehr "Regelwert": |
| 157 | * Die Gelbzeit ist der Ansatz dieses Programms, und beim Abbieger setzt |
| 158 | * die RiLSA 2 s, nicht 3 s. Die Bezugsgroesse steht trotzdem da - darum |
| 159 | * ging es in Befund 30. |
| 160 | */ |
| 161 | const project = mitAbbieger({ crossingTimeOverride: 6 }); |
| 162 | const { wurzel, aufraeumen } = zeichne(conflictsView, project); |
| 163 | try { |
| 164 | const fenster = oeffneBeziehung(wurzel, project, 'K1'); |
| 165 | const hinweis = hinweisZu(fenster, 'Überfahrzeit'); |
| 166 | expect(hinweis).toContain('Angesetzt 6 s'); |
| 167 | expect(hinweis).toContain('ohne diesen Eintrag 3 s'); |
| 168 | expect(hinweis).not.toContain('ohne diesen Eintrag 6'); |
| 169 | expect(hinweis).not.toContain('Regelwert 6'); |
| 170 | } finally { |
| 171 | aufraeumen(); |
| 172 | } |
| 173 | }); |
| 174 | |
| 175 | it('sagt bei einer verworfenen Ueberfahrzeit, dass der Eintrag nicht gilt', () => { |
| 176 | const project = mitAbbieger({ crossingTimeOverride: 0 }); |
| 177 | const { wurzel, aufraeumen } = zeichne(conflictsView, project); |
| 178 | try { |
| 179 | const fenster = oeffneBeziehung(wurzel, project, 'K1'); |
| 180 | const hinweis = hinweisZu(fenster, 'Überfahrzeit'); |
| 181 | expect(hinweis).toContain('verworfen'); |
| 182 | expect(hinweis).not.toContain('Regelwert 0'); |
| 183 | } finally { |
| 184 | aufraeumen(); |
| 185 | } |
| 186 | }); |
| 187 | |
| 188 | it('beschriftet die Felder ohne Handeingabe weiterhin mit dem Regelwert', () => { |
| 189 | const project = mitAbbieger(); |
| 190 | const { wurzel, aufraeumen } = zeichne(conflictsView, project); |
| 191 | try { |
| 192 | const fenster = oeffneBeziehung(wurzel, project, 'K1'); |
| 193 | expect(hinweisZu(fenster, 'Räumgeschwindigkeit')).toContain('Regelwert 7 m/s'); |
| 194 | expect(hinweisZu(fenster, 'Einfahrgeschwindigkeit')).toContain('Regelwert 11,1 m/s'); |
| 195 | // Die Ueberfahrzeit des Abbiegers ist im Gelbzeitansatz kein Regelwert |
| 196 | // (Fassung 5.43.0, Befund 17), aber weiter angesetzt genannt. |
| 197 | expect(hinweisZu(fenster, 'Überfahrzeit')).toContain( |
| 198 | 'Angesetzt 3 s – Gelbzeit (Ansatz dieses Programms)', |
| 199 | ); |
| 200 | } finally { |
| 201 | aufraeumen(); |
| 202 | } |
| 203 | }); |
| 204 | }); |
| 205 | |
| 206 | describe('Befund 58 - der Rechenweg nennt keine Herkunft, die es nicht gibt', () => { |
| 207 | it('weist eine verworfene Ueberfahrzeit nicht als von Hand eingetragen aus', () => { |
| 208 | // Eingetragen sind 0 s, gerechnet wird mit der Gelbzeit von 3 s. |
| 209 | const project = mitAbbieger({ crossingTimeOverride: 0 }); |
| 210 | const { wurzel, aufraeumen } = zeichne(conflictsView, project); |
| 211 | try { |
| 212 | const fenster = oeffneBeziehung(wurzel, project, 'K1'); |
| 213 | const erlaeuterung = rechenwegZeile(fenster, 'Überfahrzeit tü'); |
| 214 | expect(erlaeuterung).toContain('verworfen'); |
| 215 | expect(erlaeuterung).not.toBe('an dieser Beziehung von Hand eingetragen'); |
| 216 | } finally { |
| 217 | aufraeumen(); |
| 218 | } |
| 219 | }); |
| 220 | |
| 221 | it('nennt eine uebernommene Ueberfahrzeit weiterhin als Handeingabe', () => { |
| 222 | const project = mitAbbieger({ crossingTimeOverride: 4 }); |
| 223 | const { wurzel, aufraeumen } = zeichne(conflictsView, project); |
| 224 | try { |
| 225 | const fenster = oeffneBeziehung(wurzel, project, 'K1'); |
| 226 | expect(rechenwegZeile(fenster, 'Überfahrzeit tü')).toBe( |
| 227 | 'an dieser Beziehung von Hand eingetragen', |
| 228 | ); |
| 229 | } finally { |
| 230 | aufraeumen(); |
| 231 | } |
| 232 | }); |
| 233 | }); |
| 234 | |
| 235 | /* |
| 236 | * NACHTRAG (Fassung 5.43.0): Die Zeile "Ueberfahrzeit tue" nannte an |
| 237 | * einer EINSTREIFIGEN Verkehrsfuehrung weiter die Beschriftung des |
| 238 | * Kfz-Ansatzes, obwohl der Fachkern dort nach RiLSA 2015, Abschnitt 5.2.2 mit |
| 239 | * festen 4 s rechnet - der Ansatz erreicht das Verfahren gar nicht. Die drei |
| 240 | * Felder darueber holen ihren Bezugswert seit dem Befund vom 11.09.2026 aus |
| 241 | * `bezugswerte` und kennen das Verfahren; diese Zeile nicht. |
| 242 | */ |
| 243 | describe('Fassung 5.43.0 - der Rechenweg nennt das Verfahren der Engstelle', () => { |
| 244 | /** Zwei Richtungen einer Baustellenampel, 200 m Engstelle, 50 km/h. */ |
| 245 | function baustelle(aenderung: Partial<Project['conflicts'][number]> = {}): Project { |
| 246 | const basis = createEmptyProject('Baustelle', FESTES_DATUM, 'einstreifig'); |
| 247 | const a = createSignalGroup({ name: 'K1', mode: 'kfz', vZul: 50, index: 0 }); |
| 248 | const b = createSignalGroup({ name: 'K2', mode: 'kfz', vZul: 50, index: 1 }); |
| 249 | const p1 = createPhase('Seite A', [a.id]); |
| 250 | const p2 = createPhase('Seite B', [b.id]); |
| 251 | const beziehung = (von: string, nach: string) => ({ |
| 252 | ...createConflict(von, nach), |
| 253 | clearingDistance: 200, |
| 254 | enteringDistance: 0, |
| 255 | ...aenderung, |
| 256 | }); |
| 257 | return { |
| 258 | ...basis, |
| 259 | signalGroups: [a, b], |
| 260 | conflicts: [beziehung(a.id, b.id), beziehung(b.id, a.id)], |
| 261 | phases: [p1, p2], |
| 262 | program: { ...basis.program, phaseOrder: [p1.id, p2.id] }, |
| 263 | }; |
| 264 | } |
| 265 | |
| 266 | it('nennt den Regelwert des Abschnitts 5.2.2 statt des Kfz-Ansatzes', () => { |
| 267 | const project = baustelle(); |
| 268 | // Vorbedingung: Hier rechnet wirklich das zweite Verfahren. |
| 269 | const plan = buildSignalPlan(project); |
| 270 | expect( |
| 271 | [...plan.intergreens.values()].every((i) => i.calculation?.verfahren === 'engstelle'), |
| 272 | 'Vorbedingung: das Engstellenverfahren greift', |
| 273 | ).toBe(true); |
| 274 | |
| 275 | const { wurzel, aufraeumen } = zeichne(conflictsView, project); |
| 276 | try { |
| 277 | const fenster = oeffneBeziehung(wurzel, project, 'K1'); |
| 278 | expect(rechenwegZeile(fenster, 'Überfahrzeit tü'), 'die Zahl daneben').toBe( |
| 279 | `Regelwert der Engstelle: tü = ${RILSA_ENGSTELLE.ueberfahrzeit} s ` + |
| 280 | '(RiLSA 2015, Abschnitt 5.2.2)', |
| 281 | ); |
| 282 | expect(rechenwegZeile(fenster, 'Überfahrzeit tü')).not.toContain( |
| 283 | UEBERFAHRZEIT_ANSATZ_LABELS.gelbzeit, |
| 284 | ); |
| 285 | } finally { |
| 286 | aufraeumen(); |
| 287 | } |
| 288 | }); |
| 289 | |
| 290 | it('sagt dort auch, dass eine eingetragene Ueberfahrzeit nicht eingeht', () => { |
| 291 | // Das Verfahren liest `crossingTimeOverride` nicht; bis hierher hiess eine |
| 292 | // Eingabe von 6 s daneben "verworfen", als haette eine Pruefung sie |
| 293 | // abgelehnt. |
| 294 | const project = baustelle({ crossingTimeOverride: 6 }); |
| 295 | const { wurzel, aufraeumen } = zeichne(conflictsView, project); |
| 296 | try { |
| 297 | const zeile = rechenwegZeile(oeffneBeziehung(wurzel, project, 'K1'), 'Überfahrzeit tü'); |
| 298 | expect(zeile).toContain('gehen in dieses Verfahren nicht ein'); |
| 299 | expect(zeile).toContain('6 s'); |
| 300 | } finally { |
| 301 | aufraeumen(); |
| 302 | } |
| 303 | }); |
| 304 | |
| 305 | it('laesst die Zeile am Knotenpunkt unveraendert', () => { |
| 306 | // Gegenprobe: Dieselbe Geometrie am Knotenpunkt nennt weiter den Ansatz. |
| 307 | const project: Project = { ...baustelle(), anlagenart: 'knotenpunkt' }; |
| 308 | const { wurzel, aufraeumen } = zeichne(conflictsView, project); |
| 309 | try { |
| 310 | expect(rechenwegZeile(oeffneBeziehung(wurzel, project, 'K1'), 'Überfahrzeit tü')).toBe( |
| 311 | UEBERFAHRZEIT_ANSATZ_LABELS.gelbzeit, |
| 312 | ); |
| 313 | } finally { |
| 314 | aufraeumen(); |
| 315 | } |
| 316 | }); |
| 317 | |
| 318 | /* |
| 319 | * NACHTRAG (Befund 3): Der Rechenweg war berichtigt, der FELDHINWEIS |
| 320 | * desselben Feldes nicht. Er sagte weiter genau die beiden Wortlaute, die der |
| 321 | * Rechenweg als falsch ausweist - gemessen an diesem Projekt: mit einer |
| 322 | * eingetragenen Ueberfahrzeit von 4 s "Angesetzt 4 s - von Hand eingetragen; |
| 323 | * Regelwert 4 s", mit 6 s "Regelwert 4 s - die Handeingabe 6 s ist |
| 324 | * verworfen". Zwei Aussagen ueber denselben Eintrag im selben Fenster, und |
| 325 | * eine davon die verworfene. |
| 326 | * |
| 327 | * NUR DER SATZTEIL ZUM EINTRAG: Dass der Hinweis dort "Regelwert 4 s" sagt, |
| 328 | * ist eigens bewacht (konfliktfensterUeberfahrzeit.test.ts, Fassung 5.5.0, |
| 329 | * Befund F4, samt dem Gleichlauf mit dem Platzhalter des leeren Feldes). Der Bezugswert bleibt |
| 330 | * also stehen; falsch war, was DANEBEN ueber die Eingabe behauptet wurde. |
| 331 | */ |
| 332 | it('nennt am Feld selbst keine Wirkung einer eingetragenen Ueberfahrzeit', () => { |
| 333 | for (const eintrag of [4, 6]) { |
| 334 | const project = baustelle({ crossingTimeOverride: eintrag }); |
| 335 | const { wurzel, aufraeumen } = zeichne(conflictsView, project); |
| 336 | try { |
| 337 | const hinweis = hinweisZu(oeffneBeziehung(wurzel, project, 'K1'), 'Überfahrzeit'); |
| 338 | expect(hinweis, `Eintrag ${eintrag} s`).toContain('gehen in dieses Verfahren nicht ein'); |
| 339 | expect(hinweis, 'der Eintrag wird genannt').toContain(`${eintrag} s`); |
| 340 | // Der Bezugswert bleibt daneben stehen. |
| 341 | expect(hinweis, 'der Regelwert des Verfahrens').toContain( |
| 342 | `Regelwert ${RILSA_ENGSTELLE.ueberfahrzeit} s`, |
| 343 | ); |
| 344 | expect(hinweis, 'die Herleitung, die nicht stattgefunden hat').not.toContain( |
| 345 | 'von Hand eingetragen', |
| 346 | ); |
| 347 | expect(hinweis, 'die Pruefung, die es nicht gab').not.toContain('verworfen'); |
| 348 | } finally { |
| 349 | aufraeumen(); |
| 350 | } |
| 351 | } |
| 352 | }); |
| 353 | |
| 354 | it('haengt ohne Eintrag nichts an den Hinweis', () => { |
| 355 | // Gegenprobe: Der Zusatz kommt nur, wo wirklich etwas eingetragen ist - |
| 356 | // dieselbe Bedingung wie im Rechenweg. |
| 357 | const project = baustelle(); |
| 358 | const { wurzel, aufraeumen } = zeichne(conflictsView, project); |
| 359 | try { |
| 360 | expect(hinweisZu(oeffneBeziehung(wurzel, project, 'K1'), 'Überfahrzeit')).toContain( |
| 361 | `Regelwert ${RILSA_ENGSTELLE.ueberfahrzeit} s`, |
| 362 | ); |
| 363 | expect(hinweisZu(oeffneBeziehung(wurzel, project, 'K1'), 'Überfahrzeit')).not.toContain( |
| 364 | 'nicht ein', |
| 365 | ); |
| 366 | } finally { |
| 367 | aufraeumen(); |
| 368 | } |
| 369 | }); |
| 370 | |
| 371 | it('laesst den Feldhinweis am Knotenpunkt unveraendert', () => { |
| 372 | // Gegenprobe: Dort ist die Handeingabe wirksam, und dort heisst sie so. |
| 373 | const project: Project = { |
| 374 | ...baustelle({ crossingTimeOverride: 4 }), |
| 375 | anlagenart: 'knotenpunkt', |
| 376 | }; |
| 377 | const { wurzel, aufraeumen } = zeichne(conflictsView, project); |
| 378 | try { |
| 379 | const hinweis = hinweisZu(oeffneBeziehung(wurzel, project, 'K1'), 'Überfahrzeit'); |
| 380 | expect(hinweis).toContain('von Hand eingetragen'); |
| 381 | expect(hinweis).not.toContain('nicht ein'); |
| 382 | } finally { |
| 383 | aufraeumen(); |
| 384 | } |
| 385 | }); |
| 386 | }); |