import { describe, expect, it } from 'vitest'; import { projectView } from '@/ui/views/projectView'; import { signalGroupsView } from '@/ui/views/signalGroupsView'; import { compatibilityView, zaehleFeindlichePaare } 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 * as actions from '@/app/actions'; import { createEmptyProject } from '@/domain/model/factory'; import { ASSISTENT_VORGABE, projektAusAngaben } from '@/ui/assistent'; import type { Project } from '@/domain/model/project'; import type { AppState } from '@/app/store'; import type { ViewDefinition } from '@/ui/shell'; const FIXED_DATE = new Date('2026-01-01T00:00:00Z'); /** Dieselbe Reihenfolge, in der main.ts die Ansichten anmeldet. */ const REIHENFOLGE: readonly ViewDefinition[] = [ projectView, signalGroupsView, compatibilityView, phasesView, conflictsView, planView, simulationView, koordinierungView, vergleichView, reportView, exportView, settingsView, ]; function zustand(project: Project): AppState { const plan = buildSignalPlan(project); return { project, plan, report: validateProject(project, plan, FIXED_DATE), dirty: false, filePath: null, canUndo: false, canRedo: false, undoLabel: null, redoLabel: null, }; } describe('Reihenfolge der Ansichten', () => { it('folgt der fachlichen Abhaengigkeit', () => { const ids = REIHENFOLGE.map((v) => v.id); // Signalgruppen vor allem, was auf ihnen aufbaut. expect(ids.indexOf('signalgruppen')).toBeLessThan(ids.indexOf('vertraeglichkeit')); // Vertraeglichkeit VOR den Phasen: sonst kann nicht geprueft werden, ob // zwei feindliche Gruppen in derselben Phase liegen. expect(ids.indexOf('vertraeglichkeit')).toBeLessThan(ids.indexOf('phasen')); // Vermassen ist Detailarbeit und folgt der Struktur. expect(ids.indexOf('phasen')).toBeLessThan(ids.indexOf('zwischenzeiten')); // Ergebnisse nach allen Eingaben. expect(ids.indexOf('zwischenzeiten')).toBeLessThan(ids.indexOf('signalplan')); expect(ids.indexOf('signalplan')).toBeLessThan(ids.indexOf('pruefung')); expect(ids.indexOf('pruefung')).toBeLessThan(ids.indexOf('export')); }); it('gliedert die Navigation in zusammenhaengende Bloecke', () => { const gruppen = REIHENFOLGE.map((v) => v.group); expect(new Set(gruppen)).toEqual( new Set(['Erfassung', 'Ergebnis', 'Abschluss', 'Einstellungen']), ); // Eine Gruppe darf nicht unterbrochen werden, sonst zerfaellt die // Ueberschrift in der Navigation in mehrere Bloecke. const bloecke: string[] = []; for (const gruppe of gruppen) { if (bloecke[bloecke.length - 1] !== gruppe) bloecke.push(gruppe); } expect(bloecke).toEqual(['Erfassung', 'Ergebnis', 'Abschluss', 'Einstellungen']); }); it('vergibt eindeutige Kennungen und Beschriftungen', () => { const ids = REIHENFOLGE.map((v) => v.id); const labels = REIHENFOLGE.map((v) => v.label); expect(new Set(ids).size).toBe(ids.length); expect(new Set(labels).size).toBe(labels.length); for (const view of REIHENFOLGE) { expect(view.description.length, view.id).toBeGreaterThan(20); } }); it('fuehrt der naechste Schritt durch dieselbe Reihenfolge', () => { const ids = REIHENFOLGE.map((v) => v.id); let project = createEmptyProject('Ablauf', FIXED_DATE); const besucht: string[] = []; // Leeres Projekt -> Signalgruppen besucht.push(naechsterSchritt(zustand(project)).ansicht); // Zwei Signalgruppen -> Vertraeglichkeit project = actions.addSignalGroup(actions.addSignalGroup(project, 'kfz'), 'kfz'); besucht.push(naechsterSchritt(zustand(project)).ansicht); // Konflikt erfasst, aber keine Phase -> Phasen const [a, b] = project.signalGroups; project = actions.toggleConflictPair(project, a!.id, b!.id); project = { ...project, conflicts: project.conflicts.map((c) => ({ ...c, clearingDistance: 20, enteringDistance: 8, })), }; besucht.push(naechsterSchritt(zustand(project)).ansicht); expect(besucht).toEqual(['signalgruppen', 'vertraeglichkeit', 'phasen']); // Die Fuehrung darf nie rueckwaerts durch die Navigation springen. const positionen = besucht.map((id) => ids.indexOf(id)); for (let i = 1; i < positionen.length; i += 1) { expect(positionen[i]!).toBeGreaterThan(positionen[i - 1]!); } }); }); describe('Vertraeglichkeit', () => { it('zaehlt Paare ungerichtet', () => { let project = createEmptyProject('Paare', FIXED_DATE); project = actions.addSignalGroup(actions.addSignalGroup(project, 'kfz'), 'kfz'); const [a, b] = project.signalGroups; expect(zaehleFeindlichePaare(project)).toBe(0); project = actions.toggleConflictPair(project, a!.id, b!.id); // Zwei gerichtete Beziehungen, aber nur ein Paar. expect(project.conflicts).toHaveLength(2); expect(zaehleFeindlichePaare(project)).toBe(1); project = actions.toggleConflictPair(project, a!.id, b!.id); expect(zaehleFeindlichePaare(project)).toBe(0); }); it('stimmt mit dem Ergebnis des Assistenten ueberein', () => { const projekt = projektAusAngaben(ASSISTENT_VORGABE, 'x', FIXED_DATE); // Jede erfasste Beziehung muss eine Gegenrichtung haben - sonst waere die // paarweise Darstellung nicht deckungsgleich mit den Daten. for (const c of projekt.conflicts) { expect( projekt.conflicts.some((g) => g.fromId === c.toId && g.toId === c.fromId), `${c.fromId} -> ${c.toId} ohne Gegenrichtung`, ).toBe(true); } expect(zaehleFeindlichePaare(projekt) * 2).toBe(projekt.conflicts.length); }); });