import { afterEach, describe, expect, it } from 'vitest'; import { ProjectStore } from '@/app/store'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { buildSignalPlan, intergreenKey } from '@/domain/plan/signalPlan'; import { raeumbeziehungSatz } from '@/domain/rilsa/ansaetze'; import { RILSA_DEFAULTS } from '@/domain/rilsa/constants'; import type { Conflict, Project } from '@/domain/model/project'; import type { ViewDefinition } from '@/ui/shell'; import { conflictsView } from '@/ui/views/conflictsView'; /* * Der massgebende Strom im KONFLIKTFENSTER. * * Das Fenster bildete die Entscheidung, mit welcher Fahrbeziehung eine * Beziehung raeumt, ein zweites Mal nach und nahm dabei `from.movement`. An * einer Sammelsignalgruppe "geradeaus UND rechts" widersprach es damit dem * Rechenweg, den es selbst zwei Zeilen tiefer zeigt: Ueber der Zeile * "Räumgeschwindigkeit vr 7,00 m/s" stand "Regelwert 10 m/s". */ const DATUM = new Date('2026-01-01T00:00:00Z'); function zeichne( view: ViewDefinition, project: Project, ): { wurzel: HTMLElement; aufraeumen: () => void } { const wurzel = document.createElement('div'); document.body.append(wurzel); const abmelden = view.render(wurzel, { store: new ProjectStore(project), refresh: () => {}, navigate: () => {}, target: null, }); return { wurzel, aufraeumen: () => { abmelden(); wurzel.remove(); }, }; } /** Sichtbarer Text einer Beschriftung - ohne den Hilfeknopf. */ function beschriftungstext(label: HTMLLabelElement): string { return [...label.childNodes] .filter((knoten) => !(knoten instanceof Element && knoten.classList.contains('hilfe-knopf'))) .map((knoten) => knoten.textContent ?? '') .join('') .trim(); } function hinweisZu(wurzel: ParentNode, beschriftung: string): string { const label = [...wurzel.querySelectorAll('label')].find( (l) => beschriftungstext(l) === beschriftung, ); const id = label?.getAttribute('for'); expect(id, `Das Feld "${beschriftung}" fehlt im Fenster`).toBeTruthy(); const feld = wurzel.querySelector(`[id="${id ?? ''}"]`); const beschrieben = feld?.getAttribute('aria-describedby') ?? null; if (beschrieben === null) return ''; return beschrieben .split(' ') .map((teil) => wurzel.querySelector(`[id="${teil}"]`)?.textContent ?? '') .join(' '); } /** Erlaeuterungsspalte einer Zeile des Rechenwegs. */ function rechenwegZeile(fenster: ParentNode, bezeichnung: string): string { const zeile = [...fenster.querySelectorAll('tbody tr')].find( (tr) => tr.children[0]?.textContent?.trim() === bezeichnung, ); if (!zeile) throw new Error(`Zeile "${bezeichnung}" fehlt im Rechenweg`); return zeile.children[1]?.textContent?.trim() ?? ''; } /** Der ganze sichtbare Text des Fensters. */ function fenstertext(fenster: ParentNode): string { return ((fenster as HTMLElement).textContent ?? '').replace(/\s+/g, ' '); } function oeffneBeziehung(wurzel: HTMLElement, project: Project, konflikt: Conflict): ParentNode { const zeile = project.signalGroups.findIndex((g) => g.id === konflikt.fromId); const spalte = project.signalGroups.findIndex((g) => g.id === konflikt.toId); const zellen = [...wurzel.querySelectorAll('.matrix-zelle')]; const n = project.signalGroups.length; const versatz = spalte > zeile ? spalte - 1 : spalte; zellen[zeile * (n - 1) + versatz]?.click(); const fenster = document.querySelector('.dialog'); expect(fenster, 'Das Konfliktfenster ist nicht offen').not.toBeNull(); return fenster as ParentNode; } /** * Standardknotenpunkt, an dem "K1" geradeaus UND rechts fuehrt: Die Gruppe * bleibt bei 'geradeaus', die Beziehung K1 -> K3 traegt die Fahrbeziehung * ihres massgebenden Stroms. */ function sammelgruppe(zusatz: Partial = {}): Project { const basis = createStandardIntersectionProject('Sammelgruppe', DATUM); const k1 = basis.signalGroups.find((g) => g.name === 'K1')!; const k3 = basis.signalGroups.find((g) => g.name === 'K3')!; return { ...basis, conflicts: basis.conflicts.map((c) => c.fromId === k1.id && c.toId === k3.id ? { ...c, raeumbeziehung: 'rechts' as const, ...zusatz } : c, ), }; } function beziehung(project: Project): Conflict { return project.conflicts.find((c) => c.raeumbeziehung !== undefined)!; } afterEach(() => { document.body.replaceChildren(); }); describe('N1/4: Das Konfliktfenster nennt den Regelwert des massgebenden Stroms', () => { it('beschriftet die Raeumgeschwindigkeit mit dem Wert, mit dem der Plan rechnet', () => { const project = sammelgruppe(); const konflikt = beziehung(project); const gerechnet = buildSignalPlan(project).intergreens.get( intergreenKey(konflikt.fromId, konflikt.toId), ); expect(gerechnet?.calculation?.clearingSpeed).toBe(RILSA_DEFAULTS.clearingSpeed.kfzTurning); const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, konflikt); expect(hinweisZu(fenster, 'Räumgeschwindigkeit')).toContain('Regelwert 7 m/s'); expect(hinweisZu(fenster, 'Räumgeschwindigkeit')).not.toContain('Regelwert 10 m/s'); // Und der Rechenweg im selben Fenster sagt dasselbe. expect(rechenwegZeile(fenster, 'Räumgeschwindigkeit vr')).toContain('7,00 m/s'); } finally { aufraeumen(); } }); it('zeigt den engen Innenradius als wirksam, wo der Plan mit ihm rechnet', () => { const project = sammelgruppe({ engerRadius: true }); const konflikt = beziehung(project); const gerechnet = buildSignalPlan(project).intergreens.get( intergreenKey(konflikt.fromId, konflikt.toId), ); expect(gerechnet?.calculation?.clearingSpeed).toBe( RILSA_DEFAULTS.clearingSpeed.kfzTurningEngerRadius, ); const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, konflikt); expect(hinweisZu(fenster, 'Räumgeschwindigkeit')).toContain('Angesetzt 5 m/s'); expect(hinweisZu(fenster, 'Räumgeschwindigkeit')).toContain('enger Innenradius'); // Das Kaestchen darf nicht behaupten, der Strom biege nicht ab. expect(fenstertext(fenster)).not.toContain('ist kein abbiegender Kraftfahrzeugstrom'); } finally { aufraeumen(); } }); it('nennt beim Ansatz "feste Ueberfahrzeit" den Wert des abbiegenden Stroms', () => { const basis = sammelgruppe({ crossingTimeOverride: 4 }); const project: Project = { ...basis, settings: { ...basis.settings, ueberfahrzeitAnsatz: 'fest' }, }; const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, beziehung(project)); const hinweis = hinweisZu(fenster, 'Überfahrzeit'); expect(hinweis).toContain('Angesetzt 4 s'); // "Regelwert" seit Fassung 5.43.0 (Befund 17): Der feste // Ansatz ist der der RiLSA 2015. Hier stand "ohne diesen Eintrag". expect(hinweis).toContain(`Regelwert ${RILSA_DEFAULTS.crossingTime.kfzAbbiegend} s`); expect(hinweis).not.toContain(`Regelwert ${RILSA_DEFAULTS.crossingTime.kfzGeradeaus} s`); } finally { aufraeumen(); } }); it('sagt im Kopf des Fensters, welcher Strom den Raeumweg faehrt', () => { /* * Sonst steht "K1 (Kfz, geradeaus) räumt" ueber einer Zeile mit 7 m/s, * und nichts im Fenster erklaert den Unterschied. */ const project = sammelgruppe(); const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, beziehung(project)); expect(fenstertext(fenster)).toContain( 'Den Räumweg dieser Beziehung fährt ein Strom der Signalgruppe „K1“ mit der ' + 'Fahrbeziehung „rechts abbiegend“; geräumt wird mit ihr.', ); } finally { aufraeumen(); } }); it('setzt denselben Satz in den Kopf, den der Pruefbericht fuehrt', () => { /* * NACHTRAG (Fassung 5.11.0): Der Satz steht einmal (`raeumbeziehungSatz`, * domain/rilsa/ansaetze.ts) und wird von Konfliktfenster und Pruefbericht * geteilt. Zwei Fassungen desselben Sachverhalts liefen beim naechsten Wort * auseinander - genau das war der Befund, aus dem diese Gruppe entstanden * ist. */ const project = sammelgruppe(); const konflikt = beziehung(project); const from = project.signalGroups.find((g) => g.id === konflikt.fromId)!; const satz = raeumbeziehungSatz(from, konflikt); expect(satz).not.toBe(''); const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { expect(fenstertext(oeffneBeziehung(wurzel, project, konflikt))).toContain(satz); } finally { aufraeumen(); } }); it('bleibt ohne eigene Fahrbeziehung beim Regelwert der Signalgruppe', () => { // Gegenprobe: Ohne `raeumbeziehung` gilt die Fahrbeziehung der Gruppe. const basis = sammelgruppe(); const konflikt = beziehung(basis); const project: Project = { ...basis, conflicts: basis.conflicts.map((c) => { const { raeumbeziehung: _weg, ...ohne } = c; return ohne; }), }; const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, konflikt); expect(hinweisZu(fenster, 'Räumgeschwindigkeit')).toContain('Regelwert 10 m/s'); expect(fenstertext(fenster)).not.toContain('Den Räumweg dieser Beziehung fährt'); } finally { aufraeumen(); } }); });