import { describe, expect, it } from 'vitest'; import { projectView } from '@/ui/views/projectView'; import { lageplanView } from '@/ui/views/lageplanView'; import { signalGroupsView } from '@/ui/views/signalGroupsView'; import { compatibilityView } from '@/ui/views/compatibilityView'; import { phasesView } from '@/ui/views/phasesView'; import { conflictsView } from '@/ui/views/conflictsView'; import { planView } from '@/ui/views/planView'; import { simulationView } from '@/ui/views/simulationView'; import { reportView } from '@/ui/views/reportView'; import { exportView } from '@/ui/views/exportView'; import { settingsView } from '@/ui/views/settingsView'; import { vergleichView } from '@/ui/views/vergleichView'; import { koordinierungView } from '@/ui/views/koordinierungView'; import { naechsterSchritt } from '@/ui/naechsterSchritt'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { validateProject } from '@/domain/validation'; import { createEmptyProject, createStandardIntersectionProject } from '@/domain/model/factory'; import type { ViewDefinition } from '@/ui/shell'; import type { Project } from '@/domain/model/project'; import type { AppState } from '@/app/store'; /** * Der Haken "erledigt" an den Navigationseintraegen. * * WARUM ER NICHT AUS `naechsterSchritt` KOMMT. Die Kette sagt, was JETZT OFFEN * ist, und das ist etwas anderes als "fertig". Ihr Zweig zur Vertraeglichkeit * greift erst ab zwei Signalgruppen; bei einer einzigen waere die Ansicht * "nicht offen" und trotzdem nicht erledigt. Ihr Zweig zu den Zwischenzeiten * greift nur, wenn es ueberhaupt Konflikte gibt. Wer aus "nicht offen" ein * "erledigt" macht, setzt Haken an Ansichten, an denen niemand war - und ein * Haken, der luegt, ist schlechter als kein Haken. * * JEDE ANSICHT SAGT ES DESHALB SELBST, wie beim Abzeichen daneben. Das ist eine * zweite Stelle neben der Kette, und zwei Stellen fuer verwandte Aussagen * laufen auseinander. GENAU DAGEGEN ist diese Datei geschrieben: Sie haelt die * beiden gegeneinander, ueber alle Ansichten und ueber mehrere * Bearbeitungsstaende hinweg. * * Die tragende Zusicherung lautet: **Was erledigt ist, darf die Kette nicht * zugleich als naechsten Schritt nennen.** Dass eine Ansicht "erledigt" traegt * und der Hinweis darunter im selben Augenblick dorthin zeigt, waere derselbe * Widerspruch wie ein Sprungknopf auf die eigene Seite. */ const FESTES_DATUM = new Date('2026-01-01T00:00:00Z'); /** Dieselbe Reihenfolge, in der main.ts die Ansichten anmeldet. */ const ANSICHTEN: readonly ViewDefinition[] = [ projectView, lageplanView, signalGroupsView, compatibilityView, phasesView, conflictsView, planView, simulationView, koordinierungView, vergleichView, reportView, exportView, settingsView, ]; /** Die Ansichten, die eine Antwort geben - hier festgehalten, nicht gezaehlt. */ const MIT_ANTWORT = [ 'signalgruppen', 'vertraeglichkeit', 'phasen', 'zwischenzeiten', 'pruefung', ] as const; function zustand(project: Project): AppState { const plan = buildSignalPlan(project); return { project, plan, report: validateProject(project, plan, FESTES_DATUM), dirty: false, filePath: null, canUndo: false, canRedo: false, undoLabel: null, redoLabel: null, }; } /** Bearbeitungsstaende, ueber die geprueft wird. */ function staende(): { name: string; state: AppState }[] { const leer = createEmptyProject('Leer', FESTES_DATUM); const voll = createStandardIntersectionProject('Musterkreuzung', FESTES_DATUM); return [ { name: 'leeres Projekt', state: zustand(leer) }, { name: 'Musterknotenpunkt', state: zustand(voll) }, { name: 'Musterknotenpunkt ohne Phasen', state: zustand({ ...voll, phases: [], program: { ...voll.program, phaseOrder: [] } }), }, { name: 'Musterknotenpunkt ohne Konflikte', state: zustand({ ...voll, conflicts: [] }), }, ]; } describe('Der Haken sagt nur, was die Ansicht belegen kann', () => { it('geben genau die vorgesehenen Ansichten eine Antwort', () => { /* * Sachstand. Ohne diesen Fall koennte `fertig` ueberall fehlen, und alles * Weitere liefe ueber eine leere Menge - gruen, ohne etwas zu bewachen. */ const mitAntwort = ANSICHTEN.filter((v) => v.fertig !== undefined).map((v) => v.id); expect(mitAntwort.sort(), 'Ansichten mit einer Fertig-Zusage').toEqual([...MIT_ANTWORT].sort()); }); it('schweigen die uebrigen, statt "nicht fertig" zu behaupten', () => { /* * Der Unterschied ist wesentlich: `undefined` heisst "dazu sage ich * nichts", `false` hiesse "noch nicht fertig". Fuer den Lageplan (wahlfrei), * die Ergebnisansichten und die Vorgaben ist das Zweite falsch. */ for (const view of ANSICHTEN) { if ((MIT_ANTWORT as readonly string[]).includes(view.id)) continue; expect(view.fertig, `${view.id} sollte keine Fertig-Zusage tragen`).toBeUndefined(); } }); }); describe('Erledigt und der naechste Schritt widersprechen sich nie', () => { it.each(staende())('$name', ({ state }) => { const schritt = naechsterSchritt(state); const fertige = ANSICHTEN.filter((v) => v.fertig?.(state) === true).map((v) => v.id); expect( fertige, `die Kette zeigt auf "${schritt.ansicht}", und dieselbe Ansicht gilt als erledigt`, ).not.toContain(schritt.ansicht); }); it('gilt keine Ansicht als erledigt, an der noch nichts geschehen ist', () => { // Am leeren Projekt ist nichts getan - kein einziger Haken darf stehen. const leer = zustand(createEmptyProject('Leer', FESTES_DATUM)); const fertige = ANSICHTEN.filter((v) => v.fertig?.(leer) === true).map((v) => v.id); expect(fertige, 'Haken am leeren Projekt').toEqual([]); }); it('gilt am fertigen Musterknotenpunkt die Erfassung als erledigt', () => { /* * Die Gegenprobe: Waeren die Zusagen zu streng, staende nie ein Haken, und * die Faelle darueber blieben gruen, ohne etwas zu bewachen. */ const voll = zustand(createStandardIntersectionProject('Musterkreuzung', FESTES_DATUM)); const fertige = ANSICHTEN.filter((v) => v.fertig?.(voll) === true).map((v) => v.id); expect(fertige, 'die Erfassung des Musterknotenpunkts').toEqual( expect.arrayContaining(['signalgruppen', 'vertraeglichkeit', 'phasen', 'zwischenzeiten']), ); }); it('nimmt eine geleerte Angabe den Haken wieder weg', () => { // Ohne diesen Fall koennte `fertig` schlicht `true` zurueckgeben. const voll = createStandardIntersectionProject('Musterkreuzung', FESTES_DATUM); const ohnePhasen = zustand({ ...voll, phases: [], program: { ...voll.program, phaseOrder: [] }, }); expect(phasesView.fertig?.(ohnePhasen), 'Phasen ohne eine einzige Phase').toBe(false); const ohneKonflikte = zustand({ ...voll, conflicts: [] }); expect( conflictsView.fertig?.(ohneKonflikte), 'Zwischenzeiten ohne eine einzige Konfliktbeziehung', ).toBe(false); expect( compatibilityView.fertig?.(ohneKonflikte), 'Vertraeglichkeit ohne eine einzige Konfliktbeziehung', ).toBe(false); }); it('bleibt der Pruefbericht offen, solange eine Warnung dasteht', () => { /* * Warnungen sperren den Export nicht, sind aber fachlich zu wuerdigen. Ein * Haken neben "4 Warnungen" waere eine Aufforderung, sie zu uebergehen. */ const voll = zustand(createStandardIntersectionProject('Musterkreuzung', FESTES_DATUM)); if (voll.report.warningCount === 0 && voll.report.errorCount === 0) { expect(reportView.fertig?.(voll), 'ohne Beanstandung ist der Bericht erledigt').toBe(true); return; } expect(reportView.fertig?.(voll), 'mit Beanstandung gilt der Bericht als offen').toBe(false); }); });