lsa-planer

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

/ tests ui hilfe.test.ts

10,7 KB Rohdatei
tests/ui/hilfe.test.ts — 274 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { HILFE, glossarEintraege, glossarSuche, istHilfeSchluessel } from '@/ui/help/texte';
3 import { ASSISTENT_VORGABE, projektAusAngaben } from '@/ui/assistent';
4 import { naechsterSchritt } from '@/ui/naechsterSchritt';
5 import { buildSignalPlan } from '@/domain/plan/signalPlan';
6 import { validateProject } from '@/domain/validation';
7 import { isGreenAt } from '@/domain/plan/aspects';
8 import { createEmptyProject } from '@/domain/model/factory';
9 import type { Project } from '@/domain/model/project';
10 import type { AppState } from '@/app/store';
11
12 const FIXED_DATE = new Date('2026-01-01T00:00:00Z');
13
14 function zustand(project: Project): AppState {
15 const plan = buildSignalPlan(project);
16 return {
17 project,
18 plan,
19 report: validateProject(project, plan, FIXED_DATE),
20 dirty: false,
21 filePath: null,
22 canUndo: false,
23 canRedo: false,
24 undoLabel: null,
25 redoLabel: null,
26 };
27 }
28
29 describe('Fachliche Hilfe', () => {
30 it('erklaert jeden Begriff vollstaendig', () => {
31 for (const [schluessel, eintrag] of Object.entries(HILFE)) {
32 expect(eintrag.titel, schluessel).not.toBe('');
33 expect(eintrag.kurz, schluessel).not.toBe('');
34 // Der Kurztext soll ein Satz sein, kein Absatz.
35 expect(eintrag.kurz.length, schluessel).toBeLessThan(140);
36 expect(eintrag.wasIstDas.length, schluessel).toBeGreaterThan(60);
37 }
38 });
39
40 it('loest alle Querverweise auf', () => {
41 for (const [schluessel, eintrag] of Object.entries(HILFE)) {
42 for (const verweis of eintrag.siehe ?? []) {
43 expect(istHilfeSchluessel(verweis), `${schluessel} verweist auf ${verweis}`).toBe(true);
44 }
45 }
46 });
47
48 it('verweist nicht auf sich selbst', () => {
49 for (const [schluessel, eintrag] of Object.entries(HILFE)) {
50 expect(eintrag.siehe ?? [], schluessel).not.toContain(schluessel);
51 }
52 });
53
54 it('sagt bei sicherheitsrelevanten Groessen, was eine falsche Angabe bewirkt', () => {
55 const kritisch = [
56 'zwischenzeit',
57 'raeumweg',
58 'einfahrweg',
59 'verkehrsart',
60 'fahrbeziehung',
61 'gelbzeit',
62 'konfliktbeziehung',
63 'phase',
64 ] as const;
65 for (const schluessel of kritisch) {
66 const eintrag = HILFE[schluessel];
67 expect(eintrag.wennFalsch, schluessel).toBeDefined();
68 expect(eintrag.wennFalsch.length, schluessel).toBeGreaterThan(40);
69 }
70 });
71
72 it('erklaert bei Eingabefeldern, woher der Wert kommt', () => {
73 // 'bewertungsverfahren' und 'instationaritaetsfaktor' kamen mit
74 // Fassung 5.4.0 (Befund B2) hinzu: Beide sind Einstellungen, die
75 // in Kapazitaet und Wartezeit eingehen - wer sie setzt, muss wissen, woher
76 // der Wert kommt (Zaehldaten bzw. FGSV-Dialog zum HBS 2015).
77 const eingabefelder = [
78 'raeumweg',
79 'einfahrweg',
80 'verkehrsstaerke',
81 'zufahrt',
82 'fahrstreifen',
83 'bewertungsverfahren',
84 'instationaritaetsfaktor',
85 ] as const;
86 for (const schluessel of eingabefelder) {
87 // toBeDefined() allein liesse eine leere Zeichenkette durch - der Test
88 // darueber prueft an derselben Struktur bereits eine Mindestlaenge.
89 const woher = HILFE[schluessel].woher;
90 expect(woher, schluessel).toBeDefined();
91 expect(woher.trim().length, schluessel).toBeGreaterThan(40);
92 }
93 });
94
95 it('fuehrt die Fachbegriffe im Glossar, Bedienfelder nicht', () => {
96 const schluessel = glossarEintraege().map((e) => e.schluessel);
97 expect(schluessel).toContain('zwischenzeit');
98 expect(schluessel).toContain('zufahrt');
99 expect(schluessel).not.toContain('manuelleZwischenzeit');
100 });
101
102 it('findet Begriffe ueber die Suche', () => {
103 expect(glossarSuche('Räumweg').map((e) => e.schluessel)).toContain('raeumweg');
104 expect(glossarSuche('Haltlinie').length).toBeGreaterThan(0);
105 expect(glossarSuche('xyzabc')).toHaveLength(0);
106 expect(glossarSuche('').length).toBe(glossarEintraege().length);
107 });
108 });
109
110 /**
111 * Befund D2: Das Programm rechnet jede Furt als durchgehende Furt; eine
112 * progressive (zweistufige) Signalisierung der Furtabschnitte bildet es nicht
113 * ab. Das ist die konservative Seite - und trotzdem eine Grenze des
114 * Leistungsumfangs, die in der Hilfe stehen muss. Gegen den Altstand schlaegt
115 * jeder Fall fehl: Dort gab es das Wort "Mittelinsel" in der Hilfe nicht.
116 */
117 describe('Furt mit Mittelinsel (Befund D2)', () => {
118 it('erklaert, was gerechnet wird und was nicht', () => {
119 const eintrag = HILFE.mittelinsel;
120 expect(eintrag.wasIstDas).toContain('durchgehende Furt');
121 expect(eintrag.wasIstDas).toContain('von Bordkante zu Bordkante');
122 // Beides gehoert zusammen: die konservative Richtung UND die Luecke.
123 expect(eintrag.wasIstDas).toContain('sicheren Seite');
124 expect(eintrag.wasIstDas).toContain('Nicht abgebildet ist die progressive Signalisierung');
125 expect(eintrag.wasIstDas).toContain(
126 'zeitliche Abstimmung zwischen ihnen ist außerhalb dieses Programms zu planen',
127 );
128 // Seit 5.38.0 prueft der Bericht EINEN Punkt davon; der Eintrag darf das
129 // nicht laenger pauschal verneinen.
130 expect(eintrag.wasIstDas).toContain('warnt der Prüfbericht');
131 });
132
133 it('sagt, was eine falsche Annahme kostet', () => {
134 // Der teure Irrtum ist nicht "zu lange Freigabe", sondern der halbierte
135 // Raeumweg: Er verkuerzt die Zwischenzeit fuer einen Querenden, der nach
136 // diesem Plan in einem Zug durchgehen darf.
137 expect(HILFE.mittelinsel.wennFalsch).toBeDefined();
138 expect(HILFE.mittelinsel.wennFalsch).toContain('Sicherheitsrelevant');
139 expect(HILFE.mittelinsel.wennFalsch).toContain('Zwischenzeit wird zu kurz');
140 });
141
142 it('steht im Glossar und ist ueber die Suche zu finden', () => {
143 expect(glossarEintraege().map((e) => e.schluessel)).toContain('mittelinsel');
144 expect(glossarSuche('Mittelinsel').map((e) => e.schluessel)).toContain('mittelinsel');
145 });
146
147 it('haengt an den Eintraegen, an denen die Furtmasse entstehen', () => {
148 // Wer den Raeumweg misst oder die Mindestfreigabezeit liest, muss auf den
149 // Eintrag stossen - sonst findet ihn nur, wer ihn schon kennt.
150 for (const schluessel of ['raeumweg', 'mindestfreigabezeit', 'blindenzusatz'] as const) {
151 expect(HILFE[schluessel].siehe ?? [], schluessel).toContain('mittelinsel');
152 }
153 expect(HILFE.raeumweg.woher).toContain(
154 'Auch bei einer Mittelinsel wird über die ganze Breite gemessen',
155 );
156 expect(HILFE.mindestfreigabezeit.wasIstDas).toContain('mit ihrer ganzen Länge anzusetzen');
157 });
158 });
159
160 describe('Gefuehrter Einstieg', () => {
161 it('erzeugt aus den Vorgaben einen schaltbaren Plan ohne Fehler', () => {
162 const projekt = projektAusAngaben(ASSISTENT_VORGABE, 'Assistent', FIXED_DATE);
163 const plan = buildSignalPlan(projekt);
164 const bericht = validateProject(projekt, plan, FIXED_DATE);
165
166 expect(plan.feasible).toBe(true);
167 expect(plan.cycleTime).toBeGreaterThan(0);
168 const fachfehler = bericht.findings.filter(
169 (f) => f.severity === 'fehler' && f.category !== 'projektdaten',
170 );
171 expect(fachfehler, fachfehler.map((f) => `${f.rule}: ${f.message}`).join('\n')).toHaveLength(0);
172 });
173
174 it('gibt feindliche Signalgruppen nie gleichzeitig frei', () => {
175 const projekt = projektAusAngaben(
176 { ...ASSISTENT_VORGABE, rad: true, linksabbieger: true },
177 'Assistent',
178 FIXED_DATE,
179 );
180 const plan = buildSignalPlan(projekt);
181
182 for (const konflikt of projekt.conflicts) {
183 const a = plan.groups.find((g) => g.groupId === konflikt.fromId)!;
184 const b = plan.groups.find((g) => g.groupId === konflikt.toId)!;
185 for (let t = 0; t < plan.cycleTime; t += 0.5) {
186 expect(
187 isGreenAt(a, t, plan.cycleTime) && isGreenAt(b, t, plan.cycleTime),
188 `${a.name} und ${b.name} bei t=${t}s`,
189 ).toBe(false);
190 }
191 }
192 });
193
194 it('legt fuer die Einmuendung drei Zufahrten an', () => {
195 const projekt = projektAusAngaben(
196 { ...ASSISTENT_VORGABE, form: 'einmuendung' },
197 'Einmuendung',
198 FIXED_DATE,
199 );
200 expect(projekt.intersection.arms).toHaveLength(3);
201 expect(buildSignalPlan(projekt).feasible).toBe(true);
202 });
203
204 it('legt Fussgaengerfurten nur auf Wunsch an', () => {
205 const mit = projektAusAngaben(ASSISTENT_VORGABE, 'x', FIXED_DATE);
206 const ohne = projektAusAngaben({ ...ASSISTENT_VORGABE, fussgaenger: false }, 'x', FIXED_DATE);
207 expect(mit.signalGroups.some((g) => g.mode === 'fuss')).toBe(true);
208 expect(ohne.signalGroups.some((g) => g.mode === 'fuss')).toBe(false);
209 });
210
211 it('uebernimmt die Verkehrsstaerke nur, wenn sie angegeben ist', () => {
212 const ohne = projektAusAngaben(ASSISTENT_VORGABE, 'x', FIXED_DATE);
213 const mit = projektAusAngaben({ ...ASSISTENT_VORGABE, verkehrsstaerke: 400 }, 'x', FIXED_DATE);
214 expect(ohne.demands).toHaveLength(0);
215 expect(mit.demands.length).toBeGreaterThan(0);
216 expect(mit.demands.every((d) => d.volume === 400)).toBe(true);
217 });
218
219 it('vergibt eindeutige Bezeichnungen', () => {
220 const projekt = projektAusAngaben(
221 { ...ASSISTENT_VORGABE, rad: true, linksabbieger: true },
222 'x',
223 FIXED_DATE,
224 );
225 const namen = projekt.signalGroups.map((g) => g.name);
226 expect(new Set(namen).size).toBe(namen.length);
227 });
228 });
229
230 describe('Naechster Arbeitsschritt', () => {
231 it('fuehrt vom leeren Projekt bis zur Ausgabe', () => {
232 const leer = naechsterSchritt(zustand(createEmptyProject('Leer', FIXED_DATE)));
233 expect(leer.ansicht).toBe('signalgruppen');
234 expect(leer.text).toContain('Signalgruppen');
235
236 const fertig = zustand({
237 ...projektAusAngaben({ ...ASSISTENT_VORGABE, verkehrsstaerke: 400 }, 'x', FIXED_DATE),
238 meta: {
239 ...createEmptyProject('x', FIXED_DATE).meta,
240 name: 'Vollstaendig',
241 projectNumber: 'VP-1',
242 client: 'Stadt',
243 planner: 'Willerding',
244 location: 'Hauptstrasse',
245 },
246 });
247 const schritt = naechsterSchritt(fertig);
248 // Kein Fehler mehr offen - es geht nur noch um Warnungen oder die Ausgabe.
249 expect(schritt.dringlichkeit).not.toBe('fehler');
250 });
251
252 it('nennt fehlende Raeumwege vor allem anderen', () => {
253 const projekt = projektAusAngaben(ASSISTENT_VORGABE, 'x', FIXED_DATE);
254 const ohneWege: Project = {
255 ...projekt,
256 conflicts: projekt.conflicts.map((c) => ({ ...c, clearingDistance: 0 })),
257 };
258 const schritt = naechsterSchritt(zustand(ohneWege));
259 expect(schritt.ansicht).toBe('zwischenzeiten');
260 expect(schritt.text).toContain('Räumwege');
261 expect(schritt.dringlichkeit).toBe('fehler');
262 });
263
264 it('gibt zu jedem Schritt eine fachliche Begruendung', () => {
265 for (const projekt of [
266 createEmptyProject('Leer', FIXED_DATE),
267 projektAusAngaben(ASSISTENT_VORGABE, 'x', FIXED_DATE),
268 ]) {
269 const schritt = naechsterSchritt(zustand(projekt));
270 expect(schritt.grund.length).toBeGreaterThan(30);
271 expect(schritt.ziel).not.toBe('');
272 }
273 });
274 });