import { afterEach, describe, expect, it } from 'vitest'; import { ProjectStore } from '@/app/store'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { orderedPhases, type Project } from '@/domain/model/project'; import { phasesView } from '@/ui/views/phasesView'; /** * Befunde 25 und 56 zur Fassung 5.10.0 - die Zeile unter jeder * Uebergangszeit der Ansicht "Phasen". * * BEFUND 25. Der Satz "Keine feindliche Beziehung erfasst." wurde aus * `transition.critical === null` abgeleitet. `critical` bleibt aber auch dann * null, wenn ALLE Beziehungen erfasst sind und rechnerisch 0 s ergeben: Der * Planaufbau setzt es nur bei `value > duration`, und `duration` startet bei 0 * (plan/signalPlan.ts). Genau diese Unterscheidung kann der Planaufbau nach * eigener Aussage nicht treffen - dort wurde aus diesem Grund schon einmal * eine Meldung mit derselben Bedeutung entfernt. Die Ansicht behauptete sie * weiter, und zwar ausgerechnet dort, wo eine erfasste Beziehung auf 0 s * zusammengefallen ist. * * BEFUND 56. Der Zielname stand ungefiltert in Anfuehrungszeichen; eine Phase * ohne Bezeichnung ergab `Übergang zu "": 10 s`. Ueberall sonst in derselben * Datei ist der Rueckfall gesetzt. */ const STICHTAG = new Date('2026-01-01T00:00:00Z'); let aufraeumen: (() => void) | null = null; afterEach(() => { aufraeumen?.(); aufraeumen = null; document.body.replaceChildren(); }); function zeichne(project: Project): HTMLElement { const wurzel = document.createElement('div'); document.body.append(wurzel); const schliessen = phasesView.render(wurzel, { store: new ProjectStore(project), refresh: () => {}, navigate: () => {}, target: null, }); aufraeumen = () => { schliessen(); wurzel.remove(); }; return wurzel; } /** Fette Ueberschrift und Erlaeuterung jeder Uebergangskarte, in Kartenfolge. */ function uebergaenge(wurzel: HTMLElement): { kopf: string; hinweis: string }[] { return [...wurzel.querySelectorAll('.hinweisbalken')].map((balken) => ({ kopf: (balken.querySelector('strong')?.textContent ?? '').trim(), hinweis: (balken.querySelector('p')?.textContent ?? '').trim(), })); } /** * Standardknotenpunkt, bei dem jede Zwischenzeit rechnerisch 0 s ergibt. * * Kurzer Konfliktbereich, weit zurueckliegende Haltlinie - derselbe Aufbau wie * `alleNull()` in tests/domain/berichtWidersprueche.test.ts, der dort belegt, * dass der Pruefbericht den Uebergang dann nicht als unerfasst beanstandet. Alle sechzehn Beziehungen bleiben erfasst * (zwoelf bis 5.42.1; seit 5.43.0 erfasst die Vorlage jede Furt auch gegen den * Strom, der sie beim Ausfahren quert - Gruppe Furt). */ function alleZwischenzeitenNull(): Project { const roh = createStandardIntersectionProject('Nullzwischenzeiten', STICHTAG); return { ...roh, conflicts: roh.conflicts.map((c) => ({ ...c, clearingDistance: 3, enteringDistance: 60 })), }; } describe('Befund 25 - die Uebergangszeile spricht nur ueber das Rechenergebnis', () => { it('behauptet bei erfassten Beziehungen mit 0 s nicht, es sei keine erfasst', () => { const projekt = alleZwischenzeitenNull(); // Voraussetzungen des Ausloesers - sonst pruefte der Fall etwas anderes. const plan = buildSignalPlan(projekt); expect(projekt.conflicts.length, 'Es sind Konfliktbeziehungen erfasst').toBe(16); expect([...plan.intergreens.values()].every((i) => i.value === 0)).toBe(true); expect(plan.transitions.every((t) => t.duration === 0 && t.critical === null)).toBe(true); const zeilen = uebergaenge(zeichne(projekt)); expect(zeilen.length, 'Der Standardknotenpunkt hat zwei Uebergaenge').toBe(2); for (const zeile of zeilen) { expect(zeile.hinweis).toBe( 'Keine Beziehung mit einer Zwischenzeit über 0 s; ob Konflikte erfasst sind, ' + 'sagt der Prüfbericht.', ); } }); it('sagt dasselbe, wenn wirklich keine Beziehung erfasst ist', () => { // Der Satz spricht ueber das Rechenergebnis. Ueber die Erfassung sagt er // in beiden Faellen nichts - zustaendig ist der Pruefbericht, der die // erfassten Beziehungen kennt (zwischenzeiten.uebergang-ungesichert). const roh = createStandardIntersectionProject('Ohne Beziehungen', STICHTAG); const zeilen = uebergaenge(zeichne({ ...roh, conflicts: [] })); expect(zeilen.length).toBe(2); for (const zeile of zeilen) { expect(zeile.hinweis).toContain('Keine Beziehung mit einer Zwischenzeit über 0 s'); expect(zeile.hinweis).not.toContain('erfasst.'); } }); it('nennt weiterhin die massgebende Beziehung, wo es eine gibt', () => { const projekt = createStandardIntersectionProject('Regelfall', STICHTAG); const plan = buildSignalPlan(projekt); expect(plan.transitions.every((t) => t.critical !== null)).toBe(true); for (const zeile of uebergaenge(zeichne(projekt))) { expect(zeile.hinweis).toContain('Maßgebend:'); expect(zeile.hinweis).toContain('räumt'); } }); }); describe('Befund 56 - eine Phase ohne Bezeichnung als Ziel des Uebergangs', () => { /** Standardknotenpunkt, bei dem die zweite Phase keine Bezeichnung traegt. */ function zweitePhaseOhneNamen(): Project { const roh = createStandardIntersectionProject('Ohne Bezeichnung', STICHTAG); const zweite = orderedPhases(roh)[1]!; return { ...roh, phases: roh.phases.map((p) => (p.id === zweite.id ? { ...p, name: '' } : p)), }; } it('setzt kein leeres Anfuehrungszeichenpaar in die Uebergangszeile', () => { const zeilen = uebergaenge(zeichne(zweitePhaseOhneNamen())); for (const zeile of zeilen) { expect(zeile.kopf).not.toContain('""'); } }); it('nennt sie so, wie die Ueberschrift ihrer Karte sie nennt', () => { const wurzel = zeichne(zweitePhaseOhneNamen()); const ueberschriften = [...wurzel.querySelectorAll('h3')].map((h) => h.textContent); expect(ueberschriften).toContain('2. Phase (ohne Bezeichnung)'); const zeilen = uebergaenge(wurzel); expect(zeilen[0]?.kopf.startsWith('Übergang zu 2. Phase (ohne Bezeichnung): ')).toBe(true); }); it('laesst die Zeile mit Bezeichnung unveraendert', () => { const zeilen = uebergaenge(zeichne(createStandardIntersectionProject('Regelfall', STICHTAG))); expect(zeilen[0]?.kopf.startsWith('Übergang zu "Phase 2 - Ost/West": ')).toBe(true); }); });