import { describe, expect, it } from 'vitest'; import { csvVermerke, intergreenCsv, intergreenDetailCsv, phasesCsv, reportCsv, signalGroupsCsv, } from '@/services/export/csv'; import { UMLAUFZEIT_ERSATZWERT_VERMERK, buildSignalPlan } from '@/domain/plan/signalPlan'; import { validateProject } from '@/domain/validation'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import type { Project } from '@/domain/model/project'; /* * Tabellenausgabe zu den Befunden C7 und C11. * * C7: Die vier Plan-Tabellen nehmen einen Pruefkontext entgegen und schreiben * bei Warnungen einen Vermerk in ihre LETZTE Zeile - die Kopfzeile bleibt * Zeile 1 (Begruendung in csv.ts, csvVermerke). Ohne Kontext und ohne * Warnungen gibt es keinen Vermerk; der Pruefbericht selbst bekommt nie einen. * C11: Ist die Umlaufzeit nur ein Ersatzwert (uebersaettigt), tragen die * Tabellen, deren Zahlen an ihr haengen (Signalgruppen, Phasen), den * Ersatzwert-Vermerk - auch ohne Pruefkontext. * * Gegen den Altstand schlagen die Faelle fehl: Dort nahmen die Funktionen * keinen Kontext an, und keine Datei trug einen Vermerk. */ const STICHTAG = new Date('2026-01-01T12:00:00Z'); /** Zeilen und Spalten, ohne Bytefolgemarke - derselbe Leser wie in csv.test.ts. */ function tabelle(csv: string): string[][] { const text = csv.replace(/^/, ''); const zeilen: string[][] = []; let zeile: string[] = []; let feld = ''; let inAnfuehrung = false; for (let i = 0; i < text.length; i += 1) { const z = text[i]!; if (inAnfuehrung) { if (z === '"') { if (text[i + 1] === '"') { feld += '"'; i += 1; } else inAnfuehrung = false; } else feld += z; continue; } if (z === '"') inAnfuehrung = true; else if (z === ';') { zeile.push(feld); feld = ''; } else if (z === '\r') continue; else if (z === '\n') { zeile.push(feld); zeilen.push(zeile); zeile = []; feld = ''; } else feld += z; } if (feld !== '' || zeile.length > 0) { zeile.push(feld); zeilen.push(zeile); } return zeilen; } function projekt(): Project { return createStandardIntersectionProject('Vermerke', STICHTAG); } /** Alle Kfz-Verkehrsstaerken so hoch, dass die Anlage uebersaettigt ist. */ function uebersaettigt(): Project { const p = projekt(); return { ...p, demands: p.demands.map((d) => ({ ...d, volume: 1500 })) }; } const letzte = (zeilen: string[][]): string => zeilen[zeilen.length - 1]!.join(';'); describe('Plan-Tabellen: Vermerk bei Warnungen (Befund C7)', () => { const alle = [ ['Signalgruppen', signalGroupsCsv], ['Zwischenzeitenmatrix', intergreenCsv], ['Zwischenzeiten mit Rechenweg', intergreenDetailCsv], ['Phasen', phasesCsv], ] as const; it('schreibt den Vermerk als letzte Zeile und laesst die Kopfzeile in Zeile 1', () => { const p = projekt(); const plan = buildSignalPlan(p); const bericht = validateProject(p, plan, STICHTAG); expect(bericht.warningCount, 'Vorbedingung: die Vorlage hat Warnungen').toBeGreaterThan(0); expect(bericht.exportBlocked).toBe(false); for (const [name, csv] of alle) { const ohne = tabelle(csv(p, plan)); const mit = tabelle(csv(p, plan, bericht)); // Kopfzeile unveraendert in Zeile 1 - die Datei bleibt auswertbar. expect(mit[0], name).toEqual(ohne[0]); // Genau eine Zeile mehr, und die steht am Ende. expect(mit.length, name).toBe(ohne.length + 1); expect(mit.slice(0, -1), name).toEqual(ohne); expect(letzte(mit), name).toBe( `Hinweis: Prüfbericht meldet ${bericht.warningCount} Warnungen – siehe Prüfbericht`, ); } }); it('formuliert eine einzelne Warnung im Singular', () => { expect(csvVermerke(null, { warningCount: 1 })).toEqual([ ['Hinweis: Prüfbericht meldet 1 Warnung – siehe Prüfbericht'], ]); }); it('schreibt ohne Warnungen und ohne Kontext keinen Vermerk', () => { const p = projekt(); const plan = buildSignalPlan(p); expect(csvVermerke(plan)).toEqual([]); expect(csvVermerke(plan, { warningCount: 0 })).toEqual([]); for (const [name, csv] of alle) { const zeilen = tabelle(csv(p, plan, { warningCount: 0 })); expect(letzte(zeilen), name).not.toContain('Hinweis:'); } }); it('gibt dem Pruefbericht selbst keinen Vermerk - er IST der Kontext', () => { const p = projekt(); const plan = buildSignalPlan(p); const bericht = validateProject(p, plan, STICHTAG); const zeilen = tabelle(reportCsv(bericht)); expect(zeilen[0]).toEqual([ 'Art', 'Bereich', 'Beanstandung', 'Beschreibung', 'Zu tun', 'Fundstelle', ]); expect(zeilen.length - 1).toBe(bericht.findings.length); expect(letzte(zeilen)).not.toContain('Hinweis: Prüfbericht'); }); }); describe('Plan-Tabellen: Ersatzwert-Vermerk bei Uebersaettigung (Befund C11)', () => { it('vermerkt in Signalgruppen- und Phasentabelle, dass die Umlaufzeit ein Ersatzwert ist', () => { const p = uebersaettigt(); const plan = buildSignalPlan(p); expect(plan.cycleResult?.bounded, 'Vorbedingung').toBe('uebersaettigt'); const erwartet = `Hinweis: Umlaufzeit ${plan.cycleTime} s ${UMLAUFZEIT_ERSATZWERT_VERMERK}; ` + 'alle umlaufabhängigen Werte dieser Tabelle beruhen auf dem Ersatzwert'; // Auch OHNE Pruefkontext: Der Ersatzwert steht im Plan, nicht im Bericht. for (const csv of [signalGroupsCsv, phasesCsv]) { const zeilen = tabelle(csv(p, plan)); expect(letzte(zeilen)).toBe(erwartet); expect(zeilen[0]).not.toContain('Hinweis:'); } // Zwischenzeiten haengen nicht an der Umlaufzeit - kein Vermerk. for (const csv of [intergreenCsv, intergreenDetailCsv]) { expect(letzte(tabelle(csv(p, plan)))).not.toContain('Ersatzwert'); } }); it('setzt bei Fehlern, Warnungen UND Ersatzwert alle Vermerke ans Ende - Fehler, Warnungen, Ersatzwert', () => { const p = uebersaettigt(); const plan = buildSignalPlan(p); const bericht = validateProject(p, plan, STICHTAG); expect(bericht.warningCount).toBeGreaterThan(0); // Uebersaettigung ist ein Fehler: Die Oberflaeche gaebe die Tabelle nicht // aus (Sperre); wer sie unmittelbar erzeugt, bekommt den Fehler-Vermerk. expect(bericht.errorCount).toBeGreaterThan(0); const zeilen = tabelle(signalGroupsCsv(p, plan, bericht)); const drittletzte = zeilen[zeilen.length - 3]!.join(';'); const vorletzte = zeilen[zeilen.length - 2]!.join(';'); expect(drittletzte).toMatch( /^Hinweis: Prüfbericht meldet \d+ Fehler – die Tabelle ist keine freigegebene Planunterlage/, ); expect(vorletzte).toMatch(/^Hinweis: Prüfbericht meldet \d+ Warnungen/); expect(letzte(zeilen)).toContain(UMLAUFZEIT_ERSATZWERT_VERMERK); // Und davor stehen genau die Datenzeilen. expect(zeilen.length - 4).toBe(p.signalGroups.length); }); it('schreibt den Fehler-Vermerk nur mit Fehlerzahl im Kontext - und im Singular bei einem', () => { expect(csvVermerke(null, { warningCount: 0, errorCount: 1 })).toEqual([ [ 'Hinweis: Prüfbericht meldet 1 Fehler – die Tabelle ist keine freigegebene Planunterlage; siehe Prüfbericht', ], ]); expect(csvVermerke(null, { warningCount: 0, errorCount: 0 })).toEqual([]); // Ohne Fehlerzahl (nur Warnungen bekannt) kein Fehler-Vermerk. expect(csvVermerke(null, { warningCount: 0 })).toEqual([]); }); it('nennt den Vermerk mit demselben Wortlaut wie der Fachkern', () => { const p = uebersaettigt(); const plan = buildSignalPlan(p); const [zeile] = csvVermerke(plan); expect(zeile).toBeDefined(); expect(zeile![0]).toContain('nicht bemessbar (übersättigt)'); expect(zeile![0]).toContain('Ersatzwert'); }); });