import { afterEach, describe, expect, it } from 'vitest'; import { ProjectStore } from '@/app/store'; import { createConflict, createEmptyProject, createPhase, createSignalGroup, } from '@/domain/model/factory'; import type { Project } from '@/domain/model/project'; import type { ViewDefinition } from '@/ui/shell'; import { conflictsView } from '@/ui/views/conflictsView'; /** * Das Zwischenzeitfenster einer einstreifigen Verkehrsfuehrung. * * ANLASS (Befund aus dem Betrieb, 11.09.2026, mit Bildschirmfoto). Unter dem * Feld "Raeumgeschwindigkeit" stand "Regelwert 10 m/s", waehrend der Rechenweg * zwei Zeilen tiefer im selben Fenster "Raeumgeschwindigkeit vr 11,11 m/s" * auswies. An einer Engstelle rechnet der Fachkern nach RiLSA 2015, * Abschnitt 5.2.2 - mit der Raeumgeschwindigkeitsstaffel, nicht mit dem * Vorgabenwert der Knotenpunktrechnung. Die Beschriftung kannte die Anlagenart * nicht. * * DAS IST DIE UNSICHERE RICHTUNG. Der genannte Wert ist die Groesse, gegen die * der Bearbeiter seine Handeingabe abwaegt. 10 m/s sieht strenger aus als die * 11,11 m/s des Verfahrens; eine Eingabe daneben erschien harmloser, als sie * ist. * * Dieselbe Datei bewacht die beiden Nachbarfelder: Die Ueberfahrzeit steht an * der Engstelle mit 4 s fest, und die Einfahrgeschwindigkeit geht in gar keine * Zahl ein - dort darf kein Regelwert stehen, sondern der Grund. */ 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 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'); if (beschrieben === null || beschrieben === undefined) return ''; return beschrieben .split(' ') .map((teil) => wurzel.querySelector(`[id="${teil}"]`)?.textContent ?? '') .join(' '); } /** Erlaeuterungsspalte einer Zeile des Rechenwegs. */ function rechenwegWert(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() ?? ''; } /** * Zwei Kfz-Stroeme ueber eine einstreifige Engstelle, 44 m Haltlinienabstand, * 50 km/h zulaessig - der Aufbau des gemeldeten Falls. */ function baustelle(anlagenart: 'einstreifig' | 'knotenpunkt'): Project { const basis = createEmptyProject('Engstelle', FESTES_DATUM, anlagenart); 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]); return { ...basis, signalGroups: [a, b], conflicts: [ { ...createConflict(a.id, b.id), clearingDistance: 44, enteringDistance: 0 }, { ...createConflict(b.id, a.id), clearingDistance: 44, enteringDistance: 0 }, ], phases: [p1, p2], program: { ...basis.program, phaseOrder: [p1.id, p2.id] }, }; } function oeffneErsteBeziehung(wurzel: HTMLElement): ParentNode { const zellen = [...wurzel.querySelectorAll('.matrix-zelle')]; expect(zellen.length, 'Die Matrix hat keine Zellen').toBeGreaterThan(0); zellen[0]?.click(); const fenster = document.querySelector('.dialog'); expect(fenster, 'Das Zwischenzeitfenster ist nicht offen').not.toBeNull(); return fenster as ParentNode; } afterEach(() => { document.body.replaceChildren(); }); describe('An der Engstelle nennt das Fenster die Werte des Abschnitts 5.2', () => { it('nennt unter dem Feld dieselbe Raeumgeschwindigkeit, mit der es rechnet', () => { // Der gemeldete Widerspruch, in einem Fall: Beschriftung gegen Rechenweg. const { wurzel, aufraeumen } = zeichne(conflictsView, baustelle('einstreifig')); try { const fenster = oeffneErsteBeziehung(wurzel); expect(hinweisZu(fenster, 'Räumgeschwindigkeit')).toContain('11,11 m/s'); expect(rechenwegWert(fenster, 'Räumgeschwindigkeit vr')).toContain('11,11'); } finally { aufraeumen(); } }); it('nennt nicht mehr den Regelwert der Knotenpunktrechnung', () => { const { wurzel, aufraeumen } = zeichne(conflictsView, baustelle('einstreifig')); try { const fenster = oeffneErsteBeziehung(wurzel); expect(hinweisZu(fenster, 'Räumgeschwindigkeit')).not.toContain('10 m/s'); } finally { aufraeumen(); } }); it('nennt statt einer Einfahrgeschwindigkeit den Grund, warum keine wirkt', () => { // Ein Regelwert unter einem Feld ohne Wirkung laedt dazu ein, daran zu // drehen. Hier steht deshalb der Abschnitt, nicht die Zahl. const { wurzel, aufraeumen } = zeichne(conflictsView, baustelle('einstreifig')); try { const hinweis = hinweisZu(oeffneErsteBeziehung(wurzel), 'Einfahrgeschwindigkeit'); expect(hinweis).toContain('Ohne Wirkung'); expect(hinweis).toContain('5.2.2'); expect(hinweis).not.toContain('Regelwert'); } finally { aufraeumen(); } }); it('nennt die feste Ueberfahrzeit von 4 s', () => { const { wurzel, aufraeumen } = zeichne(conflictsView, baustelle('einstreifig')); try { expect(hinweisZu(oeffneErsteBeziehung(wurzel), 'Überfahrzeit')).toContain('4 s'); } finally { aufraeumen(); } }); }); describe('Am Knotenpunkt bleibt das Fenster, wie es war', () => { it('nennt dort weiterhin den Regelwert der Knotenpunktrechnung', () => { // Dieselbe Geometrie, andere Anlagenart. Ohne diesen Fall waere nicht // gezeigt, dass die Berichtigung nur die Engstelle betrifft. const { wurzel, aufraeumen } = zeichne(conflictsView, baustelle('knotenpunkt')); try { const fenster = oeffneErsteBeziehung(wurzel); expect(hinweisZu(fenster, 'Räumgeschwindigkeit')).toContain('10 m/s'); expect(hinweisZu(fenster, 'Einfahrgeschwindigkeit')).toContain('Regelwert'); } finally { aufraeumen(); } }); });