lsa-planer

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

/ tests ui fertigInDerLeiste.test.ts

7,7 KB Rohdatei
tests/ui/fertigInDerLeiste.test.ts — 192 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { projectView } from '@/ui/views/projectView';
3 import { lageplanView } from '@/ui/views/lageplanView';
4 import { signalGroupsView } from '@/ui/views/signalGroupsView';
5 import { compatibilityView } from '@/ui/views/compatibilityView';
6 import { phasesView } from '@/ui/views/phasesView';
7 import { conflictsView } from '@/ui/views/conflictsView';
8 import { planView } from '@/ui/views/planView';
9 import { simulationView } from '@/ui/views/simulationView';
10 import { reportView } from '@/ui/views/reportView';
11 import { exportView } from '@/ui/views/exportView';
12 import { settingsView } from '@/ui/views/settingsView';
13 import { vergleichView } from '@/ui/views/vergleichView';
14 import { koordinierungView } from '@/ui/views/koordinierungView';
15 import { naechsterSchritt } from '@/ui/naechsterSchritt';
16 import { buildSignalPlan } from '@/domain/plan/signalPlan';
17 import { validateProject } from '@/domain/validation';
18 import { createEmptyProject, createStandardIntersectionProject } from '@/domain/model/factory';
19 import type { ViewDefinition } from '@/ui/shell';
20 import type { Project } from '@/domain/model/project';
21 import type { AppState } from '@/app/store';
22
23 /**
24 * Der Haken "erledigt" an den Navigationseintraegen.
25 *
26 * WARUM ER NICHT AUS `naechsterSchritt` KOMMT. Die Kette sagt, was JETZT OFFEN
27 * ist, und das ist etwas anderes als "fertig". Ihr Zweig zur Vertraeglichkeit
28 * greift erst ab zwei Signalgruppen; bei einer einzigen waere die Ansicht
29 * "nicht offen" und trotzdem nicht erledigt. Ihr Zweig zu den Zwischenzeiten
30 * greift nur, wenn es ueberhaupt Konflikte gibt. Wer aus "nicht offen" ein
31 * "erledigt" macht, setzt Haken an Ansichten, an denen niemand war - und ein
32 * Haken, der luegt, ist schlechter als kein Haken.
33 *
34 * JEDE ANSICHT SAGT ES DESHALB SELBST, wie beim Abzeichen daneben. Das ist eine
35 * zweite Stelle neben der Kette, und zwei Stellen fuer verwandte Aussagen
36 * laufen auseinander. GENAU DAGEGEN ist diese Datei geschrieben: Sie haelt die
37 * beiden gegeneinander, ueber alle Ansichten und ueber mehrere
38 * Bearbeitungsstaende hinweg.
39 *
40 * Die tragende Zusicherung lautet: **Was erledigt ist, darf die Kette nicht
41 * zugleich als naechsten Schritt nennen.** Dass eine Ansicht "erledigt" traegt
42 * und der Hinweis darunter im selben Augenblick dorthin zeigt, waere derselbe
43 * Widerspruch wie ein Sprungknopf auf die eigene Seite.
44 */
45
46 const FESTES_DATUM = new Date('2026-01-01T00:00:00Z');
47
48 /** Dieselbe Reihenfolge, in der main.ts die Ansichten anmeldet. */
49 const ANSICHTEN: readonly ViewDefinition[] = [
50 projectView,
51 lageplanView,
52 signalGroupsView,
53 compatibilityView,
54 phasesView,
55 conflictsView,
56 planView,
57 simulationView,
58 koordinierungView,
59 vergleichView,
60 reportView,
61 exportView,
62 settingsView,
63 ];
64
65 /** Die Ansichten, die eine Antwort geben - hier festgehalten, nicht gezaehlt. */
66 const MIT_ANTWORT = [
67 'signalgruppen',
68 'vertraeglichkeit',
69 'phasen',
70 'zwischenzeiten',
71 'pruefung',
72 ] as const;
73
74 function zustand(project: Project): AppState {
75 const plan = buildSignalPlan(project);
76 return {
77 project,
78 plan,
79 report: validateProject(project, plan, FESTES_DATUM),
80 dirty: false,
81 filePath: null,
82 canUndo: false,
83 canRedo: false,
84 undoLabel: null,
85 redoLabel: null,
86 };
87 }
88
89 /** Bearbeitungsstaende, ueber die geprueft wird. */
90 function staende(): { name: string; state: AppState }[] {
91 const leer = createEmptyProject('Leer', FESTES_DATUM);
92 const voll = createStandardIntersectionProject('Musterkreuzung', FESTES_DATUM);
93 return [
94 { name: 'leeres Projekt', state: zustand(leer) },
95 { name: 'Musterknotenpunkt', state: zustand(voll) },
96 {
97 name: 'Musterknotenpunkt ohne Phasen',
98 state: zustand({ ...voll, phases: [], program: { ...voll.program, phaseOrder: [] } }),
99 },
100 {
101 name: 'Musterknotenpunkt ohne Konflikte',
102 state: zustand({ ...voll, conflicts: [] }),
103 },
104 ];
105 }
106
107 describe('Der Haken sagt nur, was die Ansicht belegen kann', () => {
108 it('geben genau die vorgesehenen Ansichten eine Antwort', () => {
109 /*
110 * Sachstand. Ohne diesen Fall koennte `fertig` ueberall fehlen, und alles
111 * Weitere liefe ueber eine leere Menge - gruen, ohne etwas zu bewachen.
112 */
113 const mitAntwort = ANSICHTEN.filter((v) => v.fertig !== undefined).map((v) => v.id);
114 expect(mitAntwort.sort(), 'Ansichten mit einer Fertig-Zusage').toEqual([...MIT_ANTWORT].sort());
115 });
116
117 it('schweigen die uebrigen, statt "nicht fertig" zu behaupten', () => {
118 /*
119 * Der Unterschied ist wesentlich: `undefined` heisst "dazu sage ich
120 * nichts", `false` hiesse "noch nicht fertig". Fuer den Lageplan (wahlfrei),
121 * die Ergebnisansichten und die Vorgaben ist das Zweite falsch.
122 */
123 for (const view of ANSICHTEN) {
124 if ((MIT_ANTWORT as readonly string[]).includes(view.id)) continue;
125 expect(view.fertig, `${view.id} sollte keine Fertig-Zusage tragen`).toBeUndefined();
126 }
127 });
128 });
129
130 describe('Erledigt und der naechste Schritt widersprechen sich nie', () => {
131 it.each(staende())('$name', ({ state }) => {
132 const schritt = naechsterSchritt(state);
133 const fertige = ANSICHTEN.filter((v) => v.fertig?.(state) === true).map((v) => v.id);
134 expect(
135 fertige,
136 `die Kette zeigt auf "${schritt.ansicht}", und dieselbe Ansicht gilt als erledigt`,
137 ).not.toContain(schritt.ansicht);
138 });
139
140 it('gilt keine Ansicht als erledigt, an der noch nichts geschehen ist', () => {
141 // Am leeren Projekt ist nichts getan - kein einziger Haken darf stehen.
142 const leer = zustand(createEmptyProject('Leer', FESTES_DATUM));
143 const fertige = ANSICHTEN.filter((v) => v.fertig?.(leer) === true).map((v) => v.id);
144 expect(fertige, 'Haken am leeren Projekt').toEqual([]);
145 });
146
147 it('gilt am fertigen Musterknotenpunkt die Erfassung als erledigt', () => {
148 /*
149 * Die Gegenprobe: Waeren die Zusagen zu streng, staende nie ein Haken, und
150 * die Faelle darueber blieben gruen, ohne etwas zu bewachen.
151 */
152 const voll = zustand(createStandardIntersectionProject('Musterkreuzung', FESTES_DATUM));
153 const fertige = ANSICHTEN.filter((v) => v.fertig?.(voll) === true).map((v) => v.id);
154 expect(fertige, 'die Erfassung des Musterknotenpunkts').toEqual(
155 expect.arrayContaining(['signalgruppen', 'vertraeglichkeit', 'phasen', 'zwischenzeiten']),
156 );
157 });
158
159 it('nimmt eine geleerte Angabe den Haken wieder weg', () => {
160 // Ohne diesen Fall koennte `fertig` schlicht `true` zurueckgeben.
161 const voll = createStandardIntersectionProject('Musterkreuzung', FESTES_DATUM);
162 const ohnePhasen = zustand({
163 ...voll,
164 phases: [],
165 program: { ...voll.program, phaseOrder: [] },
166 });
167 expect(phasesView.fertig?.(ohnePhasen), 'Phasen ohne eine einzige Phase').toBe(false);
168
169 const ohneKonflikte = zustand({ ...voll, conflicts: [] });
170 expect(
171 conflictsView.fertig?.(ohneKonflikte),
172 'Zwischenzeiten ohne eine einzige Konfliktbeziehung',
173 ).toBe(false);
174 expect(
175 compatibilityView.fertig?.(ohneKonflikte),
176 'Vertraeglichkeit ohne eine einzige Konfliktbeziehung',
177 ).toBe(false);
178 });
179
180 it('bleibt der Pruefbericht offen, solange eine Warnung dasteht', () => {
181 /*
182 * Warnungen sperren den Export nicht, sind aber fachlich zu wuerdigen. Ein
183 * Haken neben "4 Warnungen" waere eine Aufforderung, sie zu uebergehen.
184 */
185 const voll = zustand(createStandardIntersectionProject('Musterkreuzung', FESTES_DATUM));
186 if (voll.report.warningCount === 0 && voll.report.errorCount === 0) {
187 expect(reportView.fertig?.(voll), 'ohne Beanstandung ist der Bericht erledigt').toBe(true);
188 return;
189 }
190 expect(reportView.fertig?.(voll), 'mit Beanstandung gilt der Bericht als offen').toBe(false);
191 });
192 });