import { afterEach, describe, expect, it } from 'vitest'; import { ProjectStore } from '@/app/store'; import { createConflict, createEmptyProject, createPhase, createSignalGroup, createStandardIntersectionProject, } from '@/domain/model/factory'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { RILSA_ENGSTELLE, UEBERFAHRZEIT_ANSATZ_LABELS } from '@/domain/rilsa/constants'; import type { Project } from '@/domain/model/project'; import type { ViewDefinition } from '@/ui/shell'; import { conflictsView } from '@/ui/views/conflictsView'; /** * Befunde zur Fassung 5.8.0. * * Befund 30 Das Konfliktfenster beschriftete die drei Felder * Raeumgeschwindigkeit, Einfahrgeschwindigkeit und Ueberfahrzeit mit * "Regelwert " und las die Zahl aus dem RECHENERGEBNIS. Bei * gesetzter Handeingabe nannte das Feld damit die eigene Eingabe des * Bearbeiters "Regelwert"; die Bezugsgroesse, gegen die er sie * abwaegen soll, war im Fenster nicht mehr zu sehen. Bei der * Einfahrgeschwindigkeit widersprach das Fenster unmittelbar der * Warnung derselben Rechnung zwei Bloecke tiefer. * Befund 58 Die Zeile "Ueberfahrzeit tue" im Rechenweg wies eine VERWORFENE * Handeingabe weiterhin als "von Hand eingetragen" aus, obwohl mit * dem Regelwert gerechnet wurde. */ const FESTES_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 feldZu(wurzel: ParentNode, beschriftung: string): HTMLElement | null { const label = [...wurzel.querySelectorAll('label')].find( (l) => beschriftungstext(l) === beschriftung, ); const id = label?.getAttribute('for'); if (id === undefined || id === null || id === '') return null; return wurzel.querySelector(`[id="${id}"]`); } function hinweisZu(wurzel: ParentNode, beschriftung: string): string { const feld = feldZu(wurzel, beschriftung); expect(feld, `Das Feld "${beschriftung}" fehlt im Fenster`).not.toBeNull(); const id = feld!.getAttribute('aria-describedby'); if (id === null) return ''; return id .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[2]?.textContent?.trim() ?? ''; } function oeffneBeziehung(wurzel: HTMLElement, project: Project, fromName: string): ParentNode { const von = project.signalGroups.find((g) => g.name === fromName)!; const konflikt = project.conflicts.find((c) => c.fromId === von.id)!; 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 Fenster zu ${fromName} ist nicht offen`).not.toBeNull(); return fenster as ParentNode; } /** Knotenpunkt, dessen erste Signalgruppe rechts abbiegt. */ function mitAbbieger(aenderung: Partial = {}): Project { const basis = createStandardIntersectionProject('Beschriftung', FESTES_DATUM); const erste = basis.signalGroups[0]!; return { ...basis, signalGroups: basis.signalGroups.map((g) => g.id === erste.id ? { ...g, movement: 'rechts' as const } : g, ), conflicts: basis.conflicts.map((c) => (c.fromId === erste.id ? { ...c, ...aenderung } : c)), }; } afterEach(() => { document.body.replaceChildren(); }); describe('Befund 30 - "Regelwert" ist nicht die eigene Eingabe', () => { it('nennt bei eingetragener Raeumgeschwindigkeit den Regelwert der Fahrbeziehung', () => { const project = mitAbbieger({ clearingSpeedOverride: 9 }); const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, 'K1'); const hinweis = hinweisZu(fenster, 'Räumgeschwindigkeit'); expect(hinweis).toContain('Angesetzt 9 m/s'); expect(hinweis).toContain('von Hand'); expect(hinweis).toContain('Regelwert 7 m/s'); expect(hinweis).not.toContain('Regelwert 9'); } finally { aufraeumen(); } }); it('nennt bei eingetragener Einfahrgeschwindigkeit den Regelwert des einfahrenden Stroms', () => { const project = mitAbbieger({ enteringSpeedOverride: 5 }); const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, 'K1'); const hinweis = hinweisZu(fenster, 'Einfahrgeschwindigkeit'); expect(hinweis).toContain('Angesetzt 5 m/s'); expect(hinweis).toContain('Regelwert 11,1 m/s'); expect(hinweis).not.toContain('Regelwert 5'); } finally { aufraeumen(); } }); it('nennt bei eingetragener Ueberfahrzeit den Wert des Ansatzes ohne diesen Eintrag', () => { /* * Seit Fassung 5.43.0 (Befund 17) nicht mehr "Regelwert": * Die Gelbzeit ist der Ansatz dieses Programms, und beim Abbieger setzt * die RiLSA 2 s, nicht 3 s. Die Bezugsgroesse steht trotzdem da - darum * ging es in Befund 30. */ const project = mitAbbieger({ crossingTimeOverride: 6 }); const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, 'K1'); const hinweis = hinweisZu(fenster, 'Überfahrzeit'); expect(hinweis).toContain('Angesetzt 6 s'); expect(hinweis).toContain('ohne diesen Eintrag 3 s'); expect(hinweis).not.toContain('ohne diesen Eintrag 6'); expect(hinweis).not.toContain('Regelwert 6'); } finally { aufraeumen(); } }); it('sagt bei einer verworfenen Ueberfahrzeit, dass der Eintrag nicht gilt', () => { const project = mitAbbieger({ crossingTimeOverride: 0 }); const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, 'K1'); const hinweis = hinweisZu(fenster, 'Überfahrzeit'); expect(hinweis).toContain('verworfen'); expect(hinweis).not.toContain('Regelwert 0'); } finally { aufraeumen(); } }); it('beschriftet die Felder ohne Handeingabe weiterhin mit dem Regelwert', () => { const project = mitAbbieger(); const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, 'K1'); expect(hinweisZu(fenster, 'Räumgeschwindigkeit')).toContain('Regelwert 7 m/s'); expect(hinweisZu(fenster, 'Einfahrgeschwindigkeit')).toContain('Regelwert 11,1 m/s'); // Die Ueberfahrzeit des Abbiegers ist im Gelbzeitansatz kein Regelwert // (Fassung 5.43.0, Befund 17), aber weiter angesetzt genannt. expect(hinweisZu(fenster, 'Überfahrzeit')).toContain( 'Angesetzt 3 s – Gelbzeit (Ansatz dieses Programms)', ); } finally { aufraeumen(); } }); }); describe('Befund 58 - der Rechenweg nennt keine Herkunft, die es nicht gibt', () => { it('weist eine verworfene Ueberfahrzeit nicht als von Hand eingetragen aus', () => { // Eingetragen sind 0 s, gerechnet wird mit der Gelbzeit von 3 s. const project = mitAbbieger({ crossingTimeOverride: 0 }); const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, 'K1'); const erlaeuterung = rechenwegZeile(fenster, 'Überfahrzeit tü'); expect(erlaeuterung).toContain('verworfen'); expect(erlaeuterung).not.toBe('an dieser Beziehung von Hand eingetragen'); } finally { aufraeumen(); } }); it('nennt eine uebernommene Ueberfahrzeit weiterhin als Handeingabe', () => { const project = mitAbbieger({ crossingTimeOverride: 4 }); const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, 'K1'); expect(rechenwegZeile(fenster, 'Überfahrzeit tü')).toBe( 'an dieser Beziehung von Hand eingetragen', ); } finally { aufraeumen(); } }); }); /* * NACHTRAG (Fassung 5.43.0): Die Zeile "Ueberfahrzeit tue" nannte an * einer EINSTREIFIGEN Verkehrsfuehrung weiter die Beschriftung des * Kfz-Ansatzes, obwohl der Fachkern dort nach RiLSA 2015, Abschnitt 5.2.2 mit * festen 4 s rechnet - der Ansatz erreicht das Verfahren gar nicht. Die drei * Felder darueber holen ihren Bezugswert seit dem Befund vom 11.09.2026 aus * `bezugswerte` und kennen das Verfahren; diese Zeile nicht. */ describe('Fassung 5.43.0 - der Rechenweg nennt das Verfahren der Engstelle', () => { /** Zwei Richtungen einer Baustellenampel, 200 m Engstelle, 50 km/h. */ function baustelle(aenderung: Partial = {}): Project { const basis = createEmptyProject('Baustelle', FESTES_DATUM, 'einstreifig'); const a = createSignalGroup({ name: 'K1', mode: 'kfz', vZul: 50, index: 0 }); const b = createSignalGroup({ name: 'K2', mode: 'kfz', vZul: 50, index: 1 }); const p1 = createPhase('Seite A', [a.id]); const p2 = createPhase('Seite B', [b.id]); const beziehung = (von: string, nach: string) => ({ ...createConflict(von, nach), clearingDistance: 200, enteringDistance: 0, ...aenderung, }); return { ...basis, signalGroups: [a, b], conflicts: [beziehung(a.id, b.id), beziehung(b.id, a.id)], phases: [p1, p2], program: { ...basis.program, phaseOrder: [p1.id, p2.id] }, }; } it('nennt den Regelwert des Abschnitts 5.2.2 statt des Kfz-Ansatzes', () => { const project = baustelle(); // Vorbedingung: Hier rechnet wirklich das zweite Verfahren. const plan = buildSignalPlan(project); expect( [...plan.intergreens.values()].every((i) => i.calculation?.verfahren === 'engstelle'), 'Vorbedingung: das Engstellenverfahren greift', ).toBe(true); const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, 'K1'); expect(rechenwegZeile(fenster, 'Überfahrzeit tü'), 'die Zahl daneben').toBe( `Regelwert der Engstelle: tü = ${RILSA_ENGSTELLE.ueberfahrzeit} s ` + '(RiLSA 2015, Abschnitt 5.2.2)', ); expect(rechenwegZeile(fenster, 'Überfahrzeit tü')).not.toContain( UEBERFAHRZEIT_ANSATZ_LABELS.gelbzeit, ); } finally { aufraeumen(); } }); it('sagt dort auch, dass eine eingetragene Ueberfahrzeit nicht eingeht', () => { // Das Verfahren liest `crossingTimeOverride` nicht; bis hierher hiess eine // Eingabe von 6 s daneben "verworfen", als haette eine Pruefung sie // abgelehnt. const project = baustelle({ crossingTimeOverride: 6 }); const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const zeile = rechenwegZeile(oeffneBeziehung(wurzel, project, 'K1'), 'Überfahrzeit tü'); expect(zeile).toContain('gehen in dieses Verfahren nicht ein'); expect(zeile).toContain('6 s'); } finally { aufraeumen(); } }); it('laesst die Zeile am Knotenpunkt unveraendert', () => { // Gegenprobe: Dieselbe Geometrie am Knotenpunkt nennt weiter den Ansatz. const project: Project = { ...baustelle(), anlagenart: 'knotenpunkt' }; const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { expect(rechenwegZeile(oeffneBeziehung(wurzel, project, 'K1'), 'Überfahrzeit tü')).toBe( UEBERFAHRZEIT_ANSATZ_LABELS.gelbzeit, ); } finally { aufraeumen(); } }); /* * NACHTRAG (Befund 3): Der Rechenweg war berichtigt, der FELDHINWEIS * desselben Feldes nicht. Er sagte weiter genau die beiden Wortlaute, die der * Rechenweg als falsch ausweist - gemessen an diesem Projekt: mit einer * eingetragenen Ueberfahrzeit von 4 s "Angesetzt 4 s - von Hand eingetragen; * Regelwert 4 s", mit 6 s "Regelwert 4 s - die Handeingabe 6 s ist * verworfen". Zwei Aussagen ueber denselben Eintrag im selben Fenster, und * eine davon die verworfene. * * NUR DER SATZTEIL ZUM EINTRAG: Dass der Hinweis dort "Regelwert 4 s" sagt, * ist eigens bewacht (konfliktfensterUeberfahrzeit.test.ts, Fassung 5.5.0, * Befund F4, samt dem Gleichlauf mit dem Platzhalter des leeren Feldes). Der Bezugswert bleibt * also stehen; falsch war, was DANEBEN ueber die Eingabe behauptet wurde. */ it('nennt am Feld selbst keine Wirkung einer eingetragenen Ueberfahrzeit', () => { for (const eintrag of [4, 6]) { const project = baustelle({ crossingTimeOverride: eintrag }); const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const hinweis = hinweisZu(oeffneBeziehung(wurzel, project, 'K1'), 'Überfahrzeit'); expect(hinweis, `Eintrag ${eintrag} s`).toContain('gehen in dieses Verfahren nicht ein'); expect(hinweis, 'der Eintrag wird genannt').toContain(`${eintrag} s`); // Der Bezugswert bleibt daneben stehen. expect(hinweis, 'der Regelwert des Verfahrens').toContain( `Regelwert ${RILSA_ENGSTELLE.ueberfahrzeit} s`, ); expect(hinweis, 'die Herleitung, die nicht stattgefunden hat').not.toContain( 'von Hand eingetragen', ); expect(hinweis, 'die Pruefung, die es nicht gab').not.toContain('verworfen'); } finally { aufraeumen(); } } }); it('haengt ohne Eintrag nichts an den Hinweis', () => { // Gegenprobe: Der Zusatz kommt nur, wo wirklich etwas eingetragen ist - // dieselbe Bedingung wie im Rechenweg. const project = baustelle(); const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { expect(hinweisZu(oeffneBeziehung(wurzel, project, 'K1'), 'Überfahrzeit')).toContain( `Regelwert ${RILSA_ENGSTELLE.ueberfahrzeit} s`, ); expect(hinweisZu(oeffneBeziehung(wurzel, project, 'K1'), 'Überfahrzeit')).not.toContain( 'nicht ein', ); } finally { aufraeumen(); } }); it('laesst den Feldhinweis am Knotenpunkt unveraendert', () => { // Gegenprobe: Dort ist die Handeingabe wirksam, und dort heisst sie so. const project: Project = { ...baustelle({ crossingTimeOverride: 4 }), anlagenart: 'knotenpunkt', }; const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const hinweis = hinweisZu(oeffneBeziehung(wurzel, project, 'K1'), 'Überfahrzeit'); expect(hinweis).toContain('von Hand eingetragen'); expect(hinweis).not.toContain('nicht ein'); } finally { aufraeumen(); } }); });