lsa-planer

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

/ tests ui verkehrsstaerkeLeeren.test.ts

10,7 KB Rohdatei
tests/ui/verkehrsstaerkeLeeren.test.ts — 273 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { ProjectStore } from '@/app/store';
3 import {
4 createEmptyProject,
5 createSignalGroup,
6 createStandardIntersectionProject,
7 } from '@/domain/model/factory';
8 import type { Project } from '@/domain/model/project';
9 import { einstreifigBefunde } from '@/domain/validation/rules';
10 import { mitBewahrtemFokus } from '@/ui/fokus';
11 import type { ViewContext } from '@/ui/shell';
12 import { signalGroupsView } from '@/ui/views/signalGroupsView';
13
14 /**
15 * Befund 58 zur Fassung 5.9.0 (Datenverlust).
16 *
17 * WAS DER ANWENDER SONST ERLEBT
18 *
19 * Er traegt bei einer Kfz-Gruppe Verkehrsstaerke 620, Schwerverkehrsanteil
20 * 12 % und Lastzuege am Schwerverkehr 40 % ein. Dann leert er das Feld
21 * "Verkehrsstärke" (markieren, Entf, Tabulator) - und beide Nachbarfelder sind
22 * mit verschwunden: Das leere Feld fuehrte auf `removeDemand`, und das entfernt
23 * den GANZEN Datensatz. Traegt er die Verkehrsstaerke wieder ein, steht der
24 * Schwerverkehrsanteil auf 0 und die Aufteilung ist fort; gerechnet wird mit
25 * fSV = 1 statt mit den erfassten Werten. Die Kapazitaet faellt zu gross aus,
26 * der Auslastungsgrad zu klein - die unsichere Richtung der Bemessung, und
27 * kein Pruefbericht meldet es.
28 *
29 * Beide Angaben haengen fachlich nicht an der Verkehrsstaerke; das Programm
30 * sieht das selbst so: `setLastzugAnteil` legt eigens einen Datensatz mit
31 * `volume: 0` an, damit eine Aufteilung OHNE Verkehrsstaerke gehalten werden
32 * kann, und das Nachbarfeld derselben Zeile leert nicht, sondern setzt.
33 *
34 * Richtig ist deshalb: `volume: 0` - im Fachkern durchgaengig "keine Angabe" -
35 * und entfernt wird der Datensatz nur, wenn ausser der Verkehrsstaerke nichts
36 * erfasst ist.
37 *
38 * DAZU DIE ANZEIGE: Wird der Datensatz behalten, muss das Feld
39 * nach dem Neuaufbau LEER bleiben. `volume: 0` als Ziffer zu zeichnen setzte
40 * eine "0" in ein Feld, das der Anwender gerade geleert hat - eine gezaehlte
41 * Null, die niemand gezaehlt hat. Die Ansicht baut sich nach jeder Eingabe neu
42 * auf; die Prueffaelle hier tun das ebenso (`refresh` zeichnet wirklich neu).
43 */
44
45 const FESTES_DATUM = new Date('2026-01-01T00:00:00Z');
46
47 let aufraeumen: (() => void) | null = null;
48 afterEach(() => {
49 aufraeumen?.();
50 aufraeumen = null;
51 document.body.replaceChildren();
52 });
53
54 interface Ansicht {
55 readonly wurzel: HTMLElement;
56 readonly store: ProjectStore;
57 }
58
59 /**
60 * Ansicht mit dem Neuaufbau, den die Shell fuer `refresh` vorsieht
61 * (src/ui/shell.ts, `neuAufbauen`): aufraeumen, leeren, neu zeichnen, Fokus
62 * bewahren. Ein `refresh: () => {}` liesse gerade das ungeprueft, was der
63 * Anwender nach seiner Eingabe zu sehen bekommt.
64 */
65 function zeichne(project: Project): Ansicht {
66 const wurzel = document.createElement('div');
67 document.body.append(wurzel);
68 const store = new ProjectStore(project);
69 let abmelden: () => void = () => {};
70 const context: ViewContext = {
71 store,
72 refresh: () => {
73 mitBewahrtemFokus(wurzel, () => {
74 abmelden();
75 wurzel.replaceChildren();
76 abmelden = signalGroupsView.render(wurzel, context);
77 });
78 },
79 navigate: () => {},
80 target: null,
81 };
82 abmelden = signalGroupsView.render(wurzel, context);
83 aufraeumen = () => {
84 abmelden();
85 wurzel.remove();
86 };
87 return { wurzel, store };
88 }
89
90 /** Kennung der Gruppe "K1" - die traegt im Regelknotenpunkt eine Verkehrsstaerke. */
91 function k1(project: Project): string {
92 const gruppe = project.signalGroups.find((g) => g.name === 'K1');
93 if (gruppe === undefined) throw new Error('Keine Signalgruppe "K1"');
94 return gruppe.id;
95 }
96
97 /** Das Feld "Verkehrsstärke" der Gruppe - im gerade gezeichneten Stand. */
98 function verkehrsstaerkefeld(ansicht: Ansicht, groupId: string): HTMLInputElement {
99 const feld = ansicht.wurzel.querySelector<HTMLInputElement>(
100 `[data-fokus="signalgruppe|${groupId}|verkehrsstaerke"]`,
101 );
102 if (feld === null) throw new Error('Kein Feld "Verkehrsstärke"');
103 return feld;
104 }
105
106 /** Leert das Feld "Verkehrsstärke" der Gruppe, wie es der Bedienschritt tut. */
107 function leereVerkehrsstaerke(ansicht: Ansicht, groupId: string): void {
108 const feld = verkehrsstaerkefeld(ansicht, groupId);
109 feld.value = '';
110 feld.dispatchEvent(new Event('change', { bubbles: true }));
111 }
112
113 /** Einstreifige Fuehrung mit einer Kfz-Gruppe und einer Verkehrsstaerke. */
114 function einstreifigMitEinerStaerke(): { readonly projekt: Project; readonly gruppeId: string } {
115 const basis = createEmptyProject('Arbeitsstelle', FESTES_DATUM, 'einstreifig');
116 const gruppe = createSignalGroup({ name: 'A1', mode: 'kfz', index: 0 });
117 return {
118 projekt: {
119 ...basis,
120 signalGroups: [gruppe],
121 demands: [{ signalGroupId: gruppe.id, volume: 620, heavyVehicleShare: 0 }],
122 },
123 gruppeId: gruppe.id,
124 };
125 }
126
127 /** Der Satz der RSA-Anordnungskriterien zur Verkehrsstaerke. */
128 function anordnungskriterien(projekt: Project): string {
129 const befund = einstreifigBefunde(projekt).find(
130 (f) => f.rule === 'einstreifig.rsa-anordnungskriterien',
131 );
132 if (befund === undefined) throw new Error('Kein Befund "einstreifig.rsa-anordnungskriterien"');
133 return befund.message;
134 }
135
136 describe('Signalgruppen: das Leeren der Verkehrsstaerke laesst die Zaehlangaben stehen', () => {
137 it('behaelt Schwerverkehrsanteil und Lastzuganteil', () => {
138 const basis = createStandardIntersectionProject('Verkehrsstärke', FESTES_DATUM);
139 const id = k1(basis);
140 const projekt: Project = {
141 ...basis,
142 demands: basis.demands.map((d) =>
143 d.signalGroupId === id
144 ? { ...d, volume: 620, heavyVehicleShare: 0.12, lastzugAnteil: 0.4 }
145 : d,
146 ),
147 };
148
149 const ansicht = zeichne(projekt);
150 leereVerkehrsstaerke(ansicht, id);
151
152 const nachher = ansicht.store.getProject().demands.find((d) => d.signalGroupId === id);
153 expect(nachher, 'der ganze Datensatz ist fort').toBeDefined();
154 expect(nachher?.volume, '"keine Angabe" ist die Verkehrsstaerke 0').toBe(0);
155 expect(nachher?.heavyVehicleShare, 'der erfasste Schwerverkehrsanteil ist fort').toBe(0.12);
156 expect(nachher?.lastzugAnteil, 'die erfasste Aufteilung ist fort').toBe(0.4);
157 });
158
159 it('behaelt auch einen Lastzuganteil ohne Schwerverkehrsanteil', () => {
160 const basis = createStandardIntersectionProject('Verkehrsstärke', FESTES_DATUM);
161 const id = k1(basis);
162 const projekt: Project = {
163 ...basis,
164 demands: basis.demands.map((d) =>
165 d.signalGroupId === id
166 ? { ...d, volume: 620, heavyVehicleShare: 0, lastzugAnteil: 0.4 }
167 : d,
168 ),
169 };
170
171 const ansicht = zeichne(projekt);
172 leereVerkehrsstaerke(ansicht, id);
173 const nachher = ansicht.store.getProject().demands.find((d) => d.signalGroupId === id);
174 expect(nachher?.lastzugAnteil).toBe(0.4);
175 });
176
177 it('laesst das geleerte Feld leer und schreibt keine 0 hinein', () => {
178 // Der behaltene Datensatz traegt `volume: 0`. Zeichnete die Zelle diesen
179 // Wert als Ziffer, stuende nach dem Neuaufbau eine "0" in dem Feld, das der
180 // Anwender gerade geleert hat - und der Bildschirm behauptete eine
181 // Zaehlung, die es nicht gibt.
182 const basis = createStandardIntersectionProject('Verkehrsstärke', FESTES_DATUM);
183 const id = k1(basis);
184 const projekt: Project = {
185 ...basis,
186 demands: basis.demands.map((d) =>
187 d.signalGroupId === id ? { ...d, volume: 620, heavyVehicleShare: 0.12 } : d,
188 ),
189 };
190
191 const ansicht = zeichne(projekt);
192 expect(verkehrsstaerkefeld(ansicht, id).value, 'Vorbedingung: die Zahl steht im Feld').toBe(
193 '620',
194 );
195
196 leereVerkehrsstaerke(ansicht, id);
197
198 expect(
199 ansicht.store.getProject().demands.find((d) => d.signalGroupId === id)?.volume,
200 'Vorbedingung: der Datensatz bleibt mit volume 0 stehen',
201 ).toBe(0);
202 expect(
203 verkehrsstaerkefeld(ansicht, id).value,
204 'im geleerten Feld steht nach dem Neuaufbau eine Zahl',
205 ).toBe('');
206 });
207
208 it('zeichnet auch einen Datensatz aus dem Schwerverkehrsanteil allein mit leerem Feld', () => {
209 // Denselben Datensatz legt `setDemand` an, sobald nur der
210 // Schwerverkehrsanteil ausgefuellt wird - auch dort hat niemand eine
211 // Verkehrsstaerke eingetragen.
212 const basis = createStandardIntersectionProject('Verkehrsstärke', FESTES_DATUM);
213 const id = k1(basis);
214 const projekt: Project = {
215 ...basis,
216 demands: basis.demands.map((d) =>
217 d.signalGroupId === id ? { ...d, volume: 0, heavyVehicleShare: 0.12 } : d,
218 ),
219 };
220
221 const ansicht = zeichne(projekt);
222 expect(verkehrsstaerkefeld(ansicht, id).value).toBe('');
223 });
224
225 it('entfernt den Datensatz, wenn ausser der Verkehrsstaerke nichts erfasst ist', () => {
226 // Sonst bliebe ein Datensatz ohne jede Angabe stehen, und an zwei Stellen
227 // zaehlt der DATENSATZ und nicht seine Zahl: `einstreifigBefunde`
228 // (domain/validation/rules.ts) nimmt jeden Datensatz einer Kfz- oder
229 // OePNV-Gruppe als erfasste Verkehrsstaerke, und die Fehlermeldung an den
230 // Hauptprozess (src/main.ts) meldet `demands.length` als Zahl der
231 // Verkehrsstaerken. Die Fuehrung (naechsterSchritt.ts) ist NICHT betroffen:
232 // Sie zaehlt nur Datensaetze mit einer Zahl groesser 0 (Befund 48 zur
233 // Fassung 5.8.0, bewacht in tests/ui/verkehrsstaerkeNull).
234 const basis = createStandardIntersectionProject('Verkehrsstärke', FESTES_DATUM);
235 const id = k1(basis);
236 const projekt: Project = {
237 ...basis,
238 demands: basis.demands.map((d) =>
239 d.signalGroupId === id ? { ...d, volume: 620, heavyVehicleShare: 0 } : d,
240 ),
241 };
242
243 const ansicht = zeichne(projekt);
244 leereVerkehrsstaerke(ansicht, id);
245 expect(
246 ansicht.store.getProject().demands.some((d) => d.signalGroupId === id),
247 'ein leerer Datensatz bleibt zurueck',
248 ).toBe(false);
249 });
250
251 it('haelt den Pruefbericht bei "Verkehrsstärken sind nicht erfasst"', () => {
252 // Der Beleg fuer den Zweig darueber, an der Stelle, die den Unterschied
253 // druckt: Bliebe der leere Datensatz stehen, schriebe der Pruefbericht der
254 // einstreifigen Fuehrung "Die erfassten Verkehrsstärken ergeben zusammen
255 // 0 Kfz/h und liegen damit unter dem Richtwert" - eine Zaehlung mit dem
256 // Ergebnis null statt einer fehlenden Zaehlung.
257 const { projekt, gruppeId } = einstreifigMitEinerStaerke();
258
259 expect(anordnungskriterien(projekt), 'Vorbedingung: die Zahl steht im Bericht').toContain(
260 '620 Kfz/h',
261 );
262
263 const ansicht = zeichne(projekt);
264 leereVerkehrsstaerke(ansicht, gruppeId);
265
266 const satz = anordnungskriterien(ansicht.store.getProject());
267 expect(satz, 'der Bericht zaehlt eine Verkehrsstaerke, die niemand erfasst hat').toContain(
268 'Verkehrsstärken sind nicht erfasst',
269 );
270 // Nicht auf "0 Kfz/h" pruefen: Der Richtwert "500 Kfz/h" steht im selben Satz.
271 expect(satz).not.toContain('ergeben zusammen');
272 });
273 });