import { afterEach, describe, expect, it } from 'vitest'; import { ProjectStore } from '@/app/store'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { validateProject } from '@/domain/validation'; import { createSignalGroup, createStandardIntersectionProject } from '@/domain/model/factory'; import type { Project } from '@/domain/model/project'; import { nurHinweiseUeberschrift } from '@/ui/inputs'; import type { ViewDefinition } from '@/ui/shell'; import { reportView } from '@/ui/views/reportView'; /** * Pruefbericht, Befunde zur Fassung 5.10.0. * * BEFUND 28: Der Zusammenfassungsbalken kannte nur `errorCount` und * `warningCount`. Bei ausschliesslich Hinweisen sagte er "Keine * Beanstandungen" und "Alle geprüften Regeln sind eingehalten", waehrend * derselbe Bildschirm darunter "Hinweise (2)", die Gruppe "Projektdaten (2)" * und im Ansageraum "Alle Beanstandungen – 2 von 2 Einträgen" fuehrte. Der * zweite Satz war dabei nicht nur unscharf, sondern falsch: zwei gepruefte * Regeln haben angeschlagen. * * BEFUND 58: "Keine Fehler, aber 1 Warnungen" - die Zahl stand unbesehen vor * der Mehrzahlform, obwohl das Projekt die Fallunterscheidung mit * `abzeichenwort` (src/ui/shell.ts) bereits fuehrt. * * BEFUND 59: Der Sprungknopf setzte `finding.target.label` ungeprueft zwischen * die Anfuehrungszeichen. Fuer eine Signalgruppe oder Phase ohne Bezeichnung * hiess er 'Zu ""' und nannte kein Ziel mehr, waehrend derselbe Bericht eine * Zeile darueber 'Zu "(ohne Bezeichnung)"' zeigt (engine.ts setzt den * Rueckfall nur bei 'signalgruppen.name-leer'). */ 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 store = new ProjectStore(project); const aufraeumen = view.render(wurzel, { store, refresh: () => {}, navigate: () => {}, target: null, }); return { wurzel, aufraeumen: () => { aufraeumen(); wurzel.remove(); }, }; } function mitMeta(p: Project, meta: Partial): Project { return { ...p, meta: { ...p.meta, ...meta } }; } /** Alle vier Pflichtangaben gefuellt: es bleiben genau zwei Hinweise. */ function nurHinweise(): Project { const p = mitMeta(createStandardIntersectionProject('Nur Hinweise', FESTES_DATUM), { projectNumber: '2026-001', client: 'Stadt', planner: 'Bearbeiterin', location: 'Hauptstraße', }); const bericht = validateProject(p, buildSignalPlan(p), FESTES_DATUM); expect([bericht.errorCount, bericht.warningCount, bericht.hintCount]).toEqual([0, 0, 2]); return p; } /** Genau eine offene Warnung: nur der Ort fehlt. */ function eineWarnung(): Project { const p = mitMeta(createStandardIntersectionProject('Eine Warnung', FESTES_DATUM), { projectNumber: '2026-002', client: 'Stadt', planner: 'Bearbeiterin', }); const bericht = validateProject(p, buildSignalPlan(p), FESTES_DATUM); expect(bericht.errorCount).toBe(0); expect(bericht.warningCount).toBe(1); return p; } /** * Eine zusaetzliche Signalgruppe ohne Bezeichnung und ohne Phase - der * kuerzeste Bedienweg zu einem Befundziel mit leerem Namen * ('signalgruppen.ohne-phase', engine.ts). */ function gruppeOhneBezeichnung(): Project { const p = createStandardIntersectionProject('Ohne Bezeichnung', FESTES_DATUM); const arm = p.intersection.arms[0]; if (arm === undefined) throw new Error('Vorlage ohne Knotenpunktarm'); const gruppe = createSignalGroup({ name: '', mode: 'kfz', armId: arm.id, index: 9 }); const projekt: Project = { ...p, signalGroups: [...p.signalGroups, gruppe] }; const bericht = validateProject(projekt, buildSignalPlan(projekt), FESTES_DATUM); const leer = bericht.findings.filter((f) => f.target !== null && f.target.label === ''); expect(leer.length, 'Vorbedingung: mindestens ein Befundziel ohne Namen').toBeGreaterThan(0); return projekt; } function kopf(wurzel: ParentNode): string { return wurzel.querySelector('.hinweisbalken')?.textContent ?? ''; } /** Die Ueberschrift des Zusammenfassungsbalkens - die fette Zeile darin. */ function kopfzeile(wurzel: ParentNode): string { return wurzel.querySelector('.hinweisbalken strong')?.textContent ?? ''; } function knopfbeschriftungen(wurzel: ParentNode): string[] { return [...wurzel.querySelectorAll('button')].map((b) => (b.textContent ?? '').trim(), ); } afterEach(() => { document.body.replaceChildren(); }); describe('Prüfbericht: Kopfzeile, Mehrzahl und Sprungziel', () => { it('nennt Hinweise im Kopf, statt sie zu bestreiten (Befund 28)', () => { const { wurzel, aufraeumen } = zeichne(reportView, nurHinweise()); try { const text = kopf(wurzel); expect(text, 'der Kopf bestreitet die Liste darunter').not.toContain('Keine Beanstandungen'); expect(text, 'zwei Regeln haben angeschlagen').not.toContain( 'Alle geprüften Regeln sind eingehalten', ); expect(text).toContain('2 Hinweise'); // Und zwar mit dem Wortlaut, den auch die Ausgabe-Ansicht fuehrt: Er // steht an EINER Stelle (`nurHinweiseUeberschrift` in src/ui/inputs.ts) // und nicht zweimal getrennt zusammengesetzt. expect(kopfzeile(wurzel)).toBe(nurHinweiseUeberschrift(2)); // Der Bildschirm darunter fuehrt dieselben zwei Eintraege. expect(knopfbeschriftungen(wurzel)).toContain('Hinweise (2)'); expect(wurzel.textContent).toContain('Projektdaten (2)'); } finally { aufraeumen(); } }); it('behält bei einem Bericht ohne jeden Eintrag den Satz "Keine Beanstandungen"', () => { const p = mitMeta(createStandardIntersectionProject('Vollständig', FESTES_DATUM), { projectNumber: '2026-003', client: 'Stadt', planner: 'Bearbeiterin', location: 'Hauptstraße', einsatzzeiten: 'werktags 6-20 Uhr', betriebsverantwortlicher: 'Betrieb, Firma, 0800 1', }); const bericht = validateProject(p, buildSignalPlan(p), FESTES_DATUM); expect(bericht.findings.length, 'Vorbedingung: kein einziger Eintrag').toBe(0); const { wurzel, aufraeumen } = zeichne(reportView, p); try { expect(kopf(wurzel)).toContain('Keine Beanstandungen'); expect(kopf(wurzel)).toContain('Alle geprüften Regeln sind eingehalten'); } finally { aufraeumen(); } }); it('bildet die Einzahl bei genau einer Warnung (Befund 58)', () => { const { wurzel, aufraeumen } = zeichne(reportView, eineWarnung()); try { expect(kopf(wurzel)).toContain('Keine Fehler, aber 1 Warnung'); expect(kopf(wurzel)).not.toContain('1 Warnungen'); } finally { aufraeumen(); } }); it('nennt im Sprungknopf ein Ziel, auch wenn die Bezeichnung leer ist (Befund 59)', () => { const { wurzel, aufraeumen } = zeichne(reportView, gruppeOhneBezeichnung()); try { const beschriftungen = knopfbeschriftungen(wurzel); expect(beschriftungen, 'ein Knopf ohne Ziel im Namen').not.toContain('Zu ""'); expect(beschriftungen).toContain('Zu "(ohne Bezeichnung)"'); } finally { aufraeumen(); } }); });