import { afterEach, describe, expect, it } from 'vitest'; import { ProjectStore } from '@/app/store'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { CYCLE_METHOD_LABELS } from '@/domain/rilsa/cycle'; import type { Project } from '@/domain/model/project'; import type { ViewContext, ViewDefinition } from '@/ui/shell'; import { phasesView } from '@/ui/views/phasesView'; /** * Die Anzeige des Verfahrensvergleichs. * * `compareCycleTimeMethods` lag seit 5.0.0 geprueft im Fachkern und hatte * keinen Aufrufer: Wer wissen wollte, was ein anderer Ansatz aus denselben * Eingaben macht, musste die Verfahren nacheinander durchschalten. Seit dieser * Fassung stehen die vier Zahlen unter der Auswahlliste. * * WAS DIESE FAELLE BEWACHEN, ist nicht die Tabelle als solche, sondern die * beiden Zusagen daneben: dass die Anzeige KEINE Empfehlung ausspricht und dass * sie sagt, woher der genannte Wert kommt. Ohne sie liest ein Bearbeiter vier * Zahlen und nimmt die kleinste. */ const STICHTAG = new Date('2026-01-01T00:00:00Z'); let aufraeumen: (() => void) | null = null; afterEach(() => { aufraeumen?.(); aufraeumen = null; document.body.replaceChildren(); }); function zeichne(view: ViewDefinition, project: Project): HTMLElement { const wurzel = document.createElement('div'); document.body.append(wurzel); const store = new ProjectStore(project); const context: ViewContext = { store, refresh: () => {}, navigate: () => {}, target: null, }; const dispose = view.render(wurzel, context); aufraeumen = () => { dispose(); wurzel.remove(); }; return wurzel; } /** Standardknotenpunkt mit Verkehrsstaerken - sonst gibt es nichts zu rechnen. */ function mitVerkehr(method: Project['program']['method']): Project { const roh = createStandardIntersectionProject('Vergleich', STICHTAG); return { ...roh, program: { ...roh.program, method, manualCycleTime: method === 'manuell' ? 80 : null }, demands: roh.signalGroups .filter((g) => g.mode === 'kfz') .map((g) => ({ signalGroupId: g.id, volume: 600, heavyVehicleShare: 0.05 })), }; } /** Die Tabelle des Vergleichs - ueber ihre Beschriftung gefunden. */ function vergleichstabelle(wurzel: HTMLElement): HTMLTableElement { const treffer = [...wurzel.querySelectorAll('table')].filter((t) => (t.querySelector('caption')?.textContent ?? '').includes('Vergleich der Verfahren'), ); expect(treffer.length, 'genau eine Vergleichstabelle').toBe(1); return treffer[0] as HTMLTableElement; } function zeilentexte(tabelle: HTMLTableElement): string[] { return [...tabelle.querySelectorAll('tbody tr')].map((tr) => [...tr.querySelectorAll('td')].map((td) => td.textContent ?? '').join(' | '), ); } describe('Phasenansicht - die vier Rechenansaetze nebeneinander', () => { it('zeigt alle vier Verfahren, nicht nur das gewaehlte', () => { const zeilen = zeilentexte(vergleichstabelle(zeichne(phasesView, mitVerkehr('webster')))); expect(zeilen.length, 'vier Zeilen').toBe(4); for (const method of ['webster', 'akcelik', 'hbs', 'hcm'] as const) { expect( zeilen.some((z) => z.startsWith(CYCLE_METHOD_LABELS[method])), CYCLE_METHOD_LABELS[method], ).toBe(true); } }); it('kennzeichnet das gewaehlte Verfahren im Text und nicht nur farblich', () => { // Eine Sprachausgabe liest keine Hintergrundfarbe vor. Stuende die Auswahl // allein in der Klasse, waere die Tafel fuer sie vier gleichrangige Zahlen. const zeilen = zeilentexte(vergleichstabelle(zeichne(phasesView, mitVerkehr('hbs')))); const markiert = zeilen.filter((z) => z.includes('(gewählt)')); expect(markiert.length, 'genau eine Zeile ist gekennzeichnet').toBe(1); expect(markiert[0]).toContain('HBS'); }); it('steht auch bei fester Vorgabe da', () => { /* * Gerade dort ist der Vergleich die Auskunft, die fehlte: Wer die * Umlaufzeit von Hand setzt, hatte bis hierher keinen Anhaltspunkt, ob sie * zu den Verkehrsstaerken passt. */ const zeilen = zeilentexte(vergleichstabelle(zeichne(phasesView, mitVerkehr('manuell')))); expect(zeilen.length).toBe(4); // Keines der vier Verfahren ist gewaehlt - die feste Vorgabe ist keines. expect(zeilen.some((z) => z.includes('(gewählt)'))).toBe(false); }); it('spricht keine Empfehlung aus und sagt, woher der Wert kommt', () => { const wurzel = zeichne(phasesView, mitVerkehr('webster')); const text = wurzel.textContent ?? ''; expect(text, 'die Zusage, dass es keine Empfehlung ist').toContain('keine Empfehlung'); expect(text, 'die Wahl bleibt beim Planer').toContain('entscheidet der Planer'); expect(text, 'die Regel ist kein Regelwerkswert').toContain('keinem Regelwerk'); expect(text, 'die fremden Beanstandungen gehen nicht in den Bericht').toContain( 'nicht im Prüfbericht', ); }); it('nennt den Rechenwert eines nicht bemessbaren Verfahrens nicht als Zahl', () => { /* * Bei Uebersaettigung ist der Rechenwert unendlich, und `cycleTime` traegt * nur die Obergrenze als Ersatzwert. Eine Zahl in der Spalte "Rechenwert" * stuende dort zum Schein. * * DIESER FALL BEWACHT DIE ZUSAGE, NICHT EINE EIGENE ABFRAGE: Den Strich * schreibt `fmt.seconds` fuer jeden nicht endlichen Wert. In der Ansicht * stand davor einmal eine eigene Pruefung auf `Number.isFinite`, die * dasselbe ein zweites Mal tat - sie ist entfernt, weil keine Mutation an * ihr auffiel. Wer die Zelle kuenftig ohne den Formatierer baut, faellt * hier auf. */ const roh = createStandardIntersectionProject('Uebersaettigt', STICHTAG); const projekt: Project = { ...roh, program: { ...roh.program, method: 'webster' }, demands: roh.signalGroups .filter((g) => g.mode === 'kfz') .map((g) => ({ signalGroupId: g.id, volume: 1200, heavyVehicleShare: 0 })), }; const zeilen = zeilentexte(vergleichstabelle(zeichne(phasesView, projekt))); const hbs = zeilen.find((z) => z.startsWith('HBS')) ?? ''; expect(hbs, 'HBS ist nicht bemessbar').toContain('nicht bemessbar'); expect(hbs, 'kein Rechenwert, sondern ein Strich').toContain('–'); }); });