lsa-planer

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

/ tests domain validation.test.ts

8,8 KB Rohdatei
tests/domain/validation.test.ts — 215 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { validateProject } from '@/domain/validation/engine';
3 import { buildSignalPlan } from '@/domain/plan/signalPlan';
4 import { createEmptyProject, createStandardIntersectionProject } from '@/domain/model/factory';
5 import type { Project } from '@/domain/model/project';
6
7 const FIXED_DATE = new Date('2026-01-01T00:00:00Z');
8
9 function complete(): Project {
10 const base = createStandardIntersectionProject('Prueffall', FIXED_DATE);
11 return {
12 ...base,
13 meta: {
14 ...base.meta,
15 projectNumber: 'VP-2026-001',
16 client: 'Stadt Musterhausen',
17 planner: 'O. Willerding',
18 location: 'Hauptstrasse / Bahnhofstrasse',
19 },
20 };
21 }
22
23 describe('Pruefung des Projekts', () => {
24 it('meldet bei einem vollstaendigen Regelknotenpunkt keinen Fehler', () => {
25 const report = validateProject(complete(), undefined, FIXED_DATE);
26 const errors = report.findings.filter((f) => f.severity === 'fehler');
27 expect(errors, errors.map((e) => `${e.rule}: ${e.message}`).join('\n')).toHaveLength(0);
28 expect(report.exportBlocked).toBe(false);
29 });
30
31 it('sperrt den Export, sobald ein Fehler vorliegt', () => {
32 const report = validateProject(createEmptyProject('Leer', FIXED_DATE), undefined, FIXED_DATE);
33 expect(report.errorCount).toBeGreaterThan(0);
34 expect(report.exportBlocked).toBe(true);
35 });
36
37 it('meldet fehlende Projektangaben', () => {
38 const base = createStandardIntersectionProject('X', FIXED_DATE);
39 const report = validateProject(base, undefined, FIXED_DATE);
40 expect(report.findings.some((f) => f.rule === 'projektdaten.client')).toBe(true);
41 expect(report.findings.some((f) => f.rule === 'projektdaten.planner')).toBe(true);
42 });
43
44 /*
45 * Die fehlende Bezeichnung (Befund B3 zur Zwischenzeitenmatrix). Vor der
46 * Regel `signalgruppen.name-leer` fiel eine EINZELNE Signalgruppe ohne
47 * Bezeichnung durch die Pruefung: Gemeldet wurde sie erst, wenn eine zweite
48 * ebenfalls keine trug - und auch dann nur als Doppelung, weil beide auf den
49 * Schluessel '' abgebildet wurden.
50 */
51 describe('Signalgruppe ohne Bezeichnung', () => {
52 /** Der Standardknotenpunkt, dessen i-te Signalgruppe `name` traegt. */
53 function mitNamen(i: number, name: string): Project {
54 const base = complete();
55 return {
56 ...base,
57 signalGroups: base.signalGroups.map((g, index) => (index === i ? { ...g, name } : g)),
58 };
59 }
60
61 it('meldet eine einzelne Signalgruppe ohne Bezeichnung als Fehler', () => {
62 const report = validateProject(mitNamen(2, ''), undefined, FIXED_DATE);
63 const leer = report.findings.filter((f) => f.rule === 'signalgruppen.name-leer');
64 expect(leer).toHaveLength(1);
65 expect(leer[0]!.severity).toBe('fehler');
66 // Die Stellung ist das einzige Merkmal, ueber das sie ansprechbar ist.
67 expect(leer[0]!.title).toContain('Signalgruppe 3');
68 expect(report.exportBlocked).toBe(true);
69 });
70
71 it('behandelt eine Bezeichnung aus lauter Leerzeichen wie eine fehlende', () => {
72 const report = validateProject(mitNamen(0, ' '), undefined, FIXED_DATE);
73 expect(report.findings.filter((f) => f.rule === 'signalgruppen.name-leer')).toHaveLength(1);
74 });
75
76 it('meldet zwei unbenannte Gruppen zweimal als fehlend und nicht als Doppelung', () => {
77 const base = complete();
78 const project: Project = {
79 ...base,
80 signalGroups: base.signalGroups.map((g, index) =>
81 index === 1 || index === 3 ? { ...g, name: '' } : g,
82 ),
83 };
84 const report = validateProject(project, undefined, FIXED_DATE);
85 expect(report.findings.filter((f) => f.rule === 'signalgruppen.name-leer')).toHaveLength(2);
86 /*
87 * Und KEINE Doppelung: Zwei Signalgruppen ohne Bezeichnung sind nicht
88 * doppelt benannt, sondern zweimal unbenannt. Der frueher gemeldete
89 * Befund trug den Titel 'Bezeichnung "" mehrfach vergeben'.
90 */
91 expect(report.findings.filter((f) => f.rule === 'signalgruppen.name-doppelt')).toHaveLength(
92 0,
93 );
94 });
95
96 it('laesst die Doppelungsregel fuer benannte Gruppen unberuehrt', () => {
97 const base = complete();
98 const project: Project = {
99 ...base,
100 signalGroups: base.signalGroups.map((g, index) =>
101 index === 1 ? { ...g, name: base.signalGroups[0]!.name } : g,
102 ),
103 };
104 const report = validateProject(project, undefined, FIXED_DATE);
105 expect(report.findings.filter((f) => f.rule === 'signalgruppen.name-doppelt')).toHaveLength(
106 1,
107 );
108 expect(report.findings.filter((f) => f.rule === 'signalgruppen.name-leer')).toHaveLength(0);
109 });
110
111 it('meldet nichts, solange jede Signalgruppe eine Bezeichnung traegt', () => {
112 const report = validateProject(complete(), undefined, FIXED_DATE);
113 expect(report.findings.filter((f) => f.rule === 'signalgruppen.name-leer')).toHaveLength(0);
114 });
115 });
116
117 it('erkennt doppelte Signalgruppenbezeichnungen', () => {
118 const base = complete();
119 const project: Project = {
120 ...base,
121 signalGroups: base.signalGroups.map((g, i) =>
122 i === 1 ? { ...g, name: base.signalGroups[0]!.name } : g,
123 ),
124 };
125 const report = validateProject(project, undefined, FIXED_DATE);
126 expect(report.findings.some((f) => f.rule === 'signalgruppen.name-doppelt')).toBe(true);
127 });
128
129 it('erkennt feindliche Signalgruppen in derselben Phase', () => {
130 const base = complete();
131 // K1 und K3 stehen in Konflikt - hier bewusst in dieselbe Phase gelegt.
132 const k1 = base.signalGroups.find((g) => g.name === 'K1')!;
133 const k3 = base.signalGroups.find((g) => g.name === 'K3')!;
134 const project: Project = {
135 ...base,
136 phases: base.phases.map((p, i) => (i === 0 ? { ...p, signalGroupIds: [k1.id, k3.id] } : p)),
137 };
138
139 const report = validateProject(project, undefined, FIXED_DATE);
140 expect(report.findings.some((f) => f.rule === 'phasen.feindlich-in-phase')).toBe(true);
141 expect(report.exportBlocked).toBe(true);
142 });
143
144 it('meldet einen fehlenden Raeumweg', () => {
145 const base = complete();
146 const project: Project = {
147 ...base,
148 conflicts: base.conflicts.map((c, i) => (i === 0 ? { ...c, clearingDistance: 0 } : c)),
149 };
150 const report = validateProject(project, undefined, FIXED_DATE);
151 expect(report.findings.some((f) => f.rule === 'konflikte.raeumweg-fehlt')).toBe(true);
152 });
153
154 it('meldet eine fehlende Gegenrichtung der Konfliktbeziehung', () => {
155 const base = complete();
156 const project: Project = { ...base, conflicts: base.conflicts.slice(0, 1) };
157 const report = validateProject(project, undefined, FIXED_DATE);
158 expect(report.findings.some((f) => f.rule === 'konflikte.gegenrichtung-fehlt')).toBe(true);
159 });
160
161 it('meldet Ueberlastung, wenn die Nachfrage die Kapazitaet uebersteigt', () => {
162 const base = complete();
163 const project: Project = {
164 ...base,
165 demands: base.demands.map((d) => ({ ...d, volume: 2500 })),
166 };
167 const report = validateProject(project, undefined, FIXED_DATE);
168 expect(
169 report.findings.some(
170 (f) =>
171 f.rule === 'leistungsfaehigkeit.ueberlastet' || f.rule === 'signalplan.uebersaettigt',
172 ),
173 ).toBe(true);
174 });
175
176 it('weist eine unzulaessige Vorgabe als Fehler aus', () => {
177 const base = complete();
178 const project: Project = {
179 ...base,
180 settings: { ...base.settings, rilsa: { redYellow: 0, clearingSpeedKfzStraight: 20 } },
181 };
182 const report = validateProject(project, undefined, FIXED_DATE);
183 const settingErrors = report.findings.filter(
184 (f) => f.category === 'vorgaben' && f.severity === 'fehler',
185 );
186 expect(settingErrors.length).toBeGreaterThan(0);
187 });
188
189 it('gibt zu jeder Meldung eine Fundstelle und einen Korrekturvorschlag an', () => {
190 const report = validateProject(createEmptyProject('Leer', FIXED_DATE), undefined, FIXED_DATE);
191 for (const finding of report.findings) {
192 expect(finding.reference).not.toBe('');
193 expect(finding.suggestion).not.toBe('');
194 expect(finding.title).not.toBe('');
195 }
196 });
197
198 it('sortiert Fehler vor Warnungen vor Hinweisen', () => {
199 const report = validateProject(createEmptyProject('Leer', FIXED_DATE), undefined, FIXED_DATE);
200 const order = { fehler: 0, warnung: 1, hinweis: 2 } as const;
201 for (let i = 1; i < report.findings.length; i += 1) {
202 expect(order[report.findings[i]!.severity]).toBeGreaterThanOrEqual(
203 order[report.findings[i - 1]!.severity],
204 );
205 }
206 });
207
208 it('nimmt einen bereits berechneten Plan entgegen, statt ihn erneut aufzubauen', () => {
209 const project = complete();
210 const plan = buildSignalPlan(project);
211 const a = validateProject(project, plan, FIXED_DATE);
212 const b = validateProject(project, undefined, FIXED_DATE);
213 expect(a.findings.map((f) => f.rule)).toEqual(b.findings.map((f) => f.rule));
214 });
215 });