lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests export csvVermerk.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { |
| 3 | csvVermerke, |
| 4 | intergreenCsv, |
| 5 | intergreenDetailCsv, |
| 6 | phasesCsv, |
| 7 | reportCsv, |
| 8 | signalGroupsCsv, |
| 9 | } from '@/services/export/csv'; |
| 10 | import { UMLAUFZEIT_ERSATZWERT_VERMERK, buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 11 | import { validateProject } from '@/domain/validation'; |
| 12 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 13 | import type { Project } from '@/domain/model/project'; |
| 14 | |
| 15 | /* |
| 16 | * Tabellenausgabe zu den Befunden C7 und C11. |
| 17 | * |
| 18 | * C7: Die vier Plan-Tabellen nehmen einen Pruefkontext entgegen und schreiben |
| 19 | * bei Warnungen einen Vermerk in ihre LETZTE Zeile - die Kopfzeile bleibt |
| 20 | * Zeile 1 (Begruendung in csv.ts, csvVermerke). Ohne Kontext und ohne |
| 21 | * Warnungen gibt es keinen Vermerk; der Pruefbericht selbst bekommt nie einen. |
| 22 | * C11: Ist die Umlaufzeit nur ein Ersatzwert (uebersaettigt), tragen die |
| 23 | * Tabellen, deren Zahlen an ihr haengen (Signalgruppen, Phasen), den |
| 24 | * Ersatzwert-Vermerk - auch ohne Pruefkontext. |
| 25 | * |
| 26 | * Gegen den Altstand schlagen die Faelle fehl: Dort nahmen die Funktionen |
| 27 | * keinen Kontext an, und keine Datei trug einen Vermerk. |
| 28 | */ |
| 29 | |
| 30 | const STICHTAG = new Date('2026-01-01T12:00:00Z'); |
| 31 | |
| 32 | /** Zeilen und Spalten, ohne Bytefolgemarke - derselbe Leser wie in csv.test.ts. */ |
| 33 | function tabelle(csv: string): string[][] { |
| 34 | const text = csv.replace(/^/, ''); |
| 35 | const zeilen: string[][] = []; |
| 36 | let zeile: string[] = []; |
| 37 | let feld = ''; |
| 38 | let inAnfuehrung = false; |
| 39 | for (let i = 0; i < text.length; i += 1) { |
| 40 | const z = text[i]!; |
| 41 | if (inAnfuehrung) { |
| 42 | if (z === '"') { |
| 43 | if (text[i + 1] === '"') { |
| 44 | feld += '"'; |
| 45 | i += 1; |
| 46 | } else inAnfuehrung = false; |
| 47 | } else feld += z; |
| 48 | continue; |
| 49 | } |
| 50 | if (z === '"') inAnfuehrung = true; |
| 51 | else if (z === ';') { |
| 52 | zeile.push(feld); |
| 53 | feld = ''; |
| 54 | } else if (z === '\r') continue; |
| 55 | else if (z === '\n') { |
| 56 | zeile.push(feld); |
| 57 | zeilen.push(zeile); |
| 58 | zeile = []; |
| 59 | feld = ''; |
| 60 | } else feld += z; |
| 61 | } |
| 62 | if (feld !== '' || zeile.length > 0) { |
| 63 | zeile.push(feld); |
| 64 | zeilen.push(zeile); |
| 65 | } |
| 66 | return zeilen; |
| 67 | } |
| 68 | |
| 69 | function projekt(): Project { |
| 70 | return createStandardIntersectionProject('Vermerke', STICHTAG); |
| 71 | } |
| 72 | |
| 73 | /** Alle Kfz-Verkehrsstaerken so hoch, dass die Anlage uebersaettigt ist. */ |
| 74 | function uebersaettigt(): Project { |
| 75 | const p = projekt(); |
| 76 | return { ...p, demands: p.demands.map((d) => ({ ...d, volume: 1500 })) }; |
| 77 | } |
| 78 | |
| 79 | const letzte = (zeilen: string[][]): string => zeilen[zeilen.length - 1]!.join(';'); |
| 80 | |
| 81 | describe('Plan-Tabellen: Vermerk bei Warnungen (Befund C7)', () => { |
| 82 | const alle = [ |
| 83 | ['Signalgruppen', signalGroupsCsv], |
| 84 | ['Zwischenzeitenmatrix', intergreenCsv], |
| 85 | ['Zwischenzeiten mit Rechenweg', intergreenDetailCsv], |
| 86 | ['Phasen', phasesCsv], |
| 87 | ] as const; |
| 88 | |
| 89 | it('schreibt den Vermerk als letzte Zeile und laesst die Kopfzeile in Zeile 1', () => { |
| 90 | const p = projekt(); |
| 91 | const plan = buildSignalPlan(p); |
| 92 | const bericht = validateProject(p, plan, STICHTAG); |
| 93 | expect(bericht.warningCount, 'Vorbedingung: die Vorlage hat Warnungen').toBeGreaterThan(0); |
| 94 | expect(bericht.exportBlocked).toBe(false); |
| 95 | |
| 96 | for (const [name, csv] of alle) { |
| 97 | const ohne = tabelle(csv(p, plan)); |
| 98 | const mit = tabelle(csv(p, plan, bericht)); |
| 99 | // Kopfzeile unveraendert in Zeile 1 - die Datei bleibt auswertbar. |
| 100 | expect(mit[0], name).toEqual(ohne[0]); |
| 101 | // Genau eine Zeile mehr, und die steht am Ende. |
| 102 | expect(mit.length, name).toBe(ohne.length + 1); |
| 103 | expect(mit.slice(0, -1), name).toEqual(ohne); |
| 104 | expect(letzte(mit), name).toBe( |
| 105 | `Hinweis: Prüfbericht meldet ${bericht.warningCount} Warnungen – siehe Prüfbericht`, |
| 106 | ); |
| 107 | } |
| 108 | }); |
| 109 | |
| 110 | it('formuliert eine einzelne Warnung im Singular', () => { |
| 111 | expect(csvVermerke(null, { warningCount: 1 })).toEqual([ |
| 112 | ['Hinweis: Prüfbericht meldet 1 Warnung – siehe Prüfbericht'], |
| 113 | ]); |
| 114 | }); |
| 115 | |
| 116 | it('schreibt ohne Warnungen und ohne Kontext keinen Vermerk', () => { |
| 117 | const p = projekt(); |
| 118 | const plan = buildSignalPlan(p); |
| 119 | expect(csvVermerke(plan)).toEqual([]); |
| 120 | expect(csvVermerke(plan, { warningCount: 0 })).toEqual([]); |
| 121 | for (const [name, csv] of alle) { |
| 122 | const zeilen = tabelle(csv(p, plan, { warningCount: 0 })); |
| 123 | expect(letzte(zeilen), name).not.toContain('Hinweis:'); |
| 124 | } |
| 125 | }); |
| 126 | |
| 127 | it('gibt dem Pruefbericht selbst keinen Vermerk - er IST der Kontext', () => { |
| 128 | const p = projekt(); |
| 129 | const plan = buildSignalPlan(p); |
| 130 | const bericht = validateProject(p, plan, STICHTAG); |
| 131 | const zeilen = tabelle(reportCsv(bericht)); |
| 132 | expect(zeilen[0]).toEqual([ |
| 133 | 'Art', |
| 134 | 'Bereich', |
| 135 | 'Beanstandung', |
| 136 | 'Beschreibung', |
| 137 | 'Zu tun', |
| 138 | 'Fundstelle', |
| 139 | ]); |
| 140 | expect(zeilen.length - 1).toBe(bericht.findings.length); |
| 141 | expect(letzte(zeilen)).not.toContain('Hinweis: Prüfbericht'); |
| 142 | }); |
| 143 | }); |
| 144 | |
| 145 | describe('Plan-Tabellen: Ersatzwert-Vermerk bei Uebersaettigung (Befund C11)', () => { |
| 146 | it('vermerkt in Signalgruppen- und Phasentabelle, dass die Umlaufzeit ein Ersatzwert ist', () => { |
| 147 | const p = uebersaettigt(); |
| 148 | const plan = buildSignalPlan(p); |
| 149 | expect(plan.cycleResult?.bounded, 'Vorbedingung').toBe('uebersaettigt'); |
| 150 | const erwartet = |
| 151 | `Hinweis: Umlaufzeit ${plan.cycleTime} s ${UMLAUFZEIT_ERSATZWERT_VERMERK}; ` + |
| 152 | 'alle umlaufabhängigen Werte dieser Tabelle beruhen auf dem Ersatzwert'; |
| 153 | |
| 154 | // Auch OHNE Pruefkontext: Der Ersatzwert steht im Plan, nicht im Bericht. |
| 155 | for (const csv of [signalGroupsCsv, phasesCsv]) { |
| 156 | const zeilen = tabelle(csv(p, plan)); |
| 157 | expect(letzte(zeilen)).toBe(erwartet); |
| 158 | expect(zeilen[0]).not.toContain('Hinweis:'); |
| 159 | } |
| 160 | // Zwischenzeiten haengen nicht an der Umlaufzeit - kein Vermerk. |
| 161 | for (const csv of [intergreenCsv, intergreenDetailCsv]) { |
| 162 | expect(letzte(tabelle(csv(p, plan)))).not.toContain('Ersatzwert'); |
| 163 | } |
| 164 | }); |
| 165 | |
| 166 | it('setzt bei Fehlern, Warnungen UND Ersatzwert alle Vermerke ans Ende - Fehler, Warnungen, Ersatzwert', () => { |
| 167 | const p = uebersaettigt(); |
| 168 | const plan = buildSignalPlan(p); |
| 169 | const bericht = validateProject(p, plan, STICHTAG); |
| 170 | expect(bericht.warningCount).toBeGreaterThan(0); |
| 171 | // Uebersaettigung ist ein Fehler: Die Oberflaeche gaebe die Tabelle nicht |
| 172 | // aus (Sperre); wer sie unmittelbar erzeugt, bekommt den Fehler-Vermerk. |
| 173 | expect(bericht.errorCount).toBeGreaterThan(0); |
| 174 | const zeilen = tabelle(signalGroupsCsv(p, plan, bericht)); |
| 175 | const drittletzte = zeilen[zeilen.length - 3]!.join(';'); |
| 176 | const vorletzte = zeilen[zeilen.length - 2]!.join(';'); |
| 177 | expect(drittletzte).toMatch( |
| 178 | /^Hinweis: Prüfbericht meldet \d+ Fehler – die Tabelle ist keine freigegebene Planunterlage/, |
| 179 | ); |
| 180 | expect(vorletzte).toMatch(/^Hinweis: Prüfbericht meldet \d+ Warnungen/); |
| 181 | expect(letzte(zeilen)).toContain(UMLAUFZEIT_ERSATZWERT_VERMERK); |
| 182 | // Und davor stehen genau die Datenzeilen. |
| 183 | expect(zeilen.length - 4).toBe(p.signalGroups.length); |
| 184 | }); |
| 185 | |
| 186 | it('schreibt den Fehler-Vermerk nur mit Fehlerzahl im Kontext - und im Singular bei einem', () => { |
| 187 | expect(csvVermerke(null, { warningCount: 0, errorCount: 1 })).toEqual([ |
| 188 | [ |
| 189 | 'Hinweis: Prüfbericht meldet 1 Fehler – die Tabelle ist keine freigegebene Planunterlage; siehe Prüfbericht', |
| 190 | ], |
| 191 | ]); |
| 192 | expect(csvVermerke(null, { warningCount: 0, errorCount: 0 })).toEqual([]); |
| 193 | // Ohne Fehlerzahl (nur Warnungen bekannt) kein Fehler-Vermerk. |
| 194 | expect(csvVermerke(null, { warningCount: 0 })).toEqual([]); |
| 195 | }); |
| 196 | |
| 197 | it('nennt den Vermerk mit demselben Wortlaut wie der Fachkern', () => { |
| 198 | const p = uebersaettigt(); |
| 199 | const plan = buildSignalPlan(p); |
| 200 | const [zeile] = csvVermerke(plan); |
| 201 | expect(zeile).toBeDefined(); |
| 202 | expect(zeile![0]).toContain('nicht bemessbar (übersättigt)'); |
| 203 | expect(zeile![0]).toContain('Ersatzwert'); |
| 204 | }); |
| 205 | }); |