lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests export csvVermerk.test.ts

7,7 KB Rohdatei
tests/export/csvVermerk.test.ts — 205 Zeilen
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 });