lsa-planer

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

/ tests ui fahrstreifenzahl.test.ts

9,8 KB Rohdatei
tests/ui/fahrstreifenzahl.test.ts — 244 Zeilen
1 import { readFileSync } from 'node:fs';
2 import { afterEach, describe, expect, it } from 'vitest';
3 import { ProjectStore } from '@/app/store';
4 import { createStandardIntersectionProject } from '@/domain/model/factory';
5 import { bereichsHinweis, numberCell } from '@/ui/inputs';
6 import { mitBewahrtemFokus } from '@/ui/fokus';
7 import type { ViewContext } from '@/ui/shell';
8 import { signalGroupsView } from '@/ui/views/signalGroupsView';
9
10 /*
11 * Fassung 5.9.0 - die getippte Fahrstreifenzahl.
12 *
13 * WAS DER ANWENDER SONST ERLEBT: Er tippt "2,5" in die Spalte Fahrstreifen.
14 * Das Feld nimmt die Zahl an, `onChange` reicht 2,5 weiter, das Feld behaelt
15 * "2.5", es gibt keine Kennzeichnung und keine Meldung. Ab da rechnet
16 * `saturationFlow` mit zweieinhalb Fahrstreifen, und die Zahl steht in Tabelle
17 * und Ausdruck. Der Einlesepfad zieht diese Grenze seit R4.2 - die Bedienung
18 * nicht, und derselbe Eintrag bekam zwei Antworten.
19 *
20 * WARUM NICHT UEBER `step`: Die Schrittweite ist an jedem Feld dieses Programms
21 * die Sprungweite der Pfeiltasten und kein Raster; das steht in `inputs.ts`
22 * (`bereichsHinweis`) und in `phasesView.ts` (`umlaufGrenzen`) ausdruecklich so
23 * da. Wer sie durchsetzte, verwuerfe eine gezaehlte Verkehrsstaerke von
24 * 620 Fz/h (Feld mit `step: 50`), eine von Hand gesetzte Umlaufzeit von 78 s
25 * (`step: 5`) und einen abgemessenen Raeumweg von 12,34 m (`step: 0,1`).
26 * Ganzzahlig ist nicht die Schrittweite, sondern die GROESSE - eine Anzahl.
27 * Die letzten beiden Faelle dieser Datei bewachen genau das.
28 */
29
30 afterEach(() => {
31 document.body.replaceChildren();
32 });
33
34 const ANZAHLFELD = { min: 1, max: 8, step: 1, ganzzahlAbrunden: true } as const;
35
36 function letzteWarnung(): string {
37 const meldungen = [...document.querySelectorAll('.meldung-warnung')];
38 return meldungen[meldungen.length - 1]?.textContent ?? '';
39 }
40
41 function warnungen(): number {
42 return document.querySelectorAll('.meldung-warnung').length;
43 }
44
45 /** Ein Zahlenfeld im Baum, das seine Eingabe wie in der Tabelle bekommt. */
46 function tippe(
47 eingabe: string,
48 optionen: Parameters<typeof numberCell>[2],
49 start: number | null = 2,
50 ): { feld: HTMLInputElement; gemeldet: number | null | undefined } {
51 let gemeldet: number | null | undefined;
52 const feld = numberCell(
53 start,
54 (wert) => {
55 gemeldet = wert;
56 },
57 optionen,
58 );
59 feld.setAttribute('aria-label', 'Fahrstreifen');
60 document.body.append(feld);
61 feld.value = eingabe;
62 feld.dispatchEvent(new Event('change'));
63 return { feld, gemeldet };
64 }
65
66 describe('Eine Anzahl nimmt keine Nachkommastelle an', () => {
67 it('macht aus der getippten 2,5 eine 2 und sagt es', () => {
68 const { feld, gemeldet } = tippe('2.5', ANZAHLFELD);
69
70 expect(gemeldet, 'der Aufrufer bekommt die ganze Zahl').toBe(2);
71 expect(feld.value, 'und dieselbe Zahl steht im Feld').toBe('2');
72 expect(feld.getAttribute('aria-invalid')).toBe('true');
73 const meldung = letzteWarnung();
74 expect(meldung).toContain('Fahrstreifen');
75 expect(meldung).toContain('2,5');
76 expect(meldung).toContain('keine ganze Zahl');
77 expect(meldung, 'kein englischer Dezimalpunkt im vorgelesenen Satz').not.toMatch(/\d\.\d/);
78 });
79
80 it('rundet ab und nicht auf - 7,9 wird 7', () => {
81 // Dieselbe Richtung wie beim Einlesen: Die Fahrstreifenzahl geht ueber
82 // `saturationFlow` in die Kapazitaet ein; aufrunden gaebe
83 // Leistungsfaehigkeit, die es nicht gibt.
84 const { feld, gemeldet } = tippe('7.9', ANZAHLFELD);
85
86 expect(gemeldet).toBe(7);
87 expect(feld.value).toBe('7');
88 expect(letzteWarnung()).toContain('abgerundet');
89 });
90
91 it('sagt den Bereich vor der Eingabe an', () => {
92 expect(bereichsHinweis(ANZAHLFELD)).toContain('Nur ganze Zahlen.');
93 });
94
95 it('meldet einen Wert unter dem Mindestwert nur einmal', () => {
96 // 0,5 liegt unter 1. Die Begrenzung hebt auf 1 an - und 1 ist bereits ganz.
97 const { feld, gemeldet } = tippe('0.5', ANZAHLFELD);
98
99 expect(gemeldet).toBe(1);
100 expect(feld.value).toBe('1');
101 expect(warnungen(), 'eine Eingabe, eine Meldung').toBe(1);
102 expect(letzteWarnung()).toContain('außerhalb');
103 });
104
105 it('bleibt auch bei einem gebrochenen Mindestwert im Bereich', () => {
106 // Ein gebrochenes `min` hat an einer Anzahl nichts zu suchen, waere aber
107 // eintragbar. Dann gilt die kleinste ganze Zahl im Bereich, und die
108 // Meldung sagt nicht "abgerundet".
109 const { feld, gemeldet } = tippe('1.7', { min: 1.5, max: 8, ganzzahlAbrunden: true });
110
111 expect(gemeldet).toBe(2);
112 expect(feld.value).toBe('2');
113 expect(letzteWarnung()).toContain('kleinste zulässige ganze Wert');
114 });
115
116 it('laesst eine ganze Zahl unberuehrt und schweigt dazu', () => {
117 const { feld, gemeldet } = tippe('3', ANZAHLFELD);
118
119 expect(gemeldet).toBe(3);
120 expect(feld.value).toBe('3');
121 expect(feld.hasAttribute('aria-invalid')).toBe(false);
122 expect(warnungen()).toBe(0);
123 });
124 });
125
126 describe('Gegenproben - die Schrittweite bleibt die Sprungweite der Pfeiltasten', () => {
127 it('nimmt einen abgemessenen Räumweg von 12,34 m unverändert an', () => {
128 // Feld aus dem Konfliktfenster: min 0, max 3000, step 0,1.
129 const { feld, gemeldet } = tippe('12.34', { min: 0, max: 3000, step: 0.1, einheit: 'm' }, 22);
130
131 expect(gemeldet).toBe(12.34);
132 expect(feld.value).toBe('12.34');
133 expect(warnungen()).toBe(0);
134 });
135
136 it('nimmt eine gezählte Verkehrsstärke von 620 Fz/h unverändert an', () => {
137 // Feld aus dem Assistenten: min 0, max 3000, step 50. Ein Raster wuerde
138 // aus der Zaehlung 600 oder 650 machen.
139 const { feld, gemeldet } = tippe('620', { min: 0, max: 3000, step: 50, einheit: 'Fz/h' }, 0);
140
141 expect(gemeldet).toBe(620);
142 expect(feld.value).toBe('620');
143 expect(warnungen()).toBe(0);
144 });
145
146 it('nennt die Schrittweite weiterhin als Bedienhinweis und nicht als Bedingung', () => {
147 const satz = bereichsHinweis({ min: 0, max: 3000, step: 50, einheit: 'Fz/h' });
148
149 expect(satz).toContain('Pfeiltasten ändern in Schritten von 50 Fz/h.');
150 expect(satz).not.toContain('Nur ganze Zahlen.');
151 });
152 });
153
154 /* --- Dieselbe Grenze in der Signalgruppentabelle -------------------------- */
155
156 const FESTES_DATUM = new Date('2026-01-01T00:00:00Z');
157
158 describe('Signalgruppentabelle: die Spalte Fahrstreifen', () => {
159 it('legt keine gebrochene Fahrstreifenzahl im Speicher ab', () => {
160 const projekt = createStandardIntersectionProject('Fahrstreifen', FESTES_DATUM);
161 const wurzel = document.createElement('div');
162 document.body.append(wurzel);
163 const store = new ProjectStore(projekt);
164 let abmelden: () => void = () => {};
165 const context: ViewContext = {
166 store,
167 refresh: () => {
168 mitBewahrtemFokus(wurzel, () => {
169 abmelden();
170 wurzel.replaceChildren();
171 abmelden = signalGroupsView.render(wurzel, context);
172 });
173 },
174 navigate: () => {},
175 target: null,
176 };
177 abmelden = signalGroupsView.render(wurzel, context);
178
179 const feld = wurzel.querySelector<HTMLInputElement>(
180 'input[type="number"][aria-label^="Anzahl der Fahrstreifen"]',
181 );
182 expect(feld, 'Vorbedingung: die Tabelle hat eine Spalte Fahrstreifen').not.toBeNull();
183 feld!.value = '2.5';
184 feld!.dispatchEvent(new Event('change', { bubbles: true }));
185
186 expect(store.getProject().signalGroups[0]?.lanes).toBe(2);
187 abmelden();
188 });
189 });
190
191 /* --- Was der Kommentar am Einlesen noch offen nennt ----------------------- */
192
193 describe('Der Vorbehalt an der Zufahrt nennt den Stand nach S1.2', () => {
194 /*
195 * Der Kommentar vor `lanes` in `parseArms` beschrieb den Weg ueber die
196 * Oberflaeche als ganz offen und verwies auf eine Regel in engine.ts. Beides
197 * stimmt seit S1.2 nicht mehr: Die Spalte der Signalgruppentabelle zieht die
198 * Grenze jetzt am Feld, und eine Regel im Pruefbericht ist nicht mehr der
199 * gemeinte Weg. Ein Vorbehalt, der auf die falsche Datei zeigt, schickt den
200 * naechsten Leser in die Irre - deshalb steht er hier unter Aufsicht.
201 *
202 * NACHTRAG: Der dritte Fall dieses Blocks war die Gegenprobe zum letzten
203 * offenen Weg - dem Fahrstreifenfeld der Zufahrt - und sagte selbst voraus:
204 * "Sobald `ganzzahlAbrunden` auch dort steht, faellt dieser Fall." Genau das
205 * ist eingetreten. Er prueft jetzt das Gegenteil, und der zweite Fall
206 * verlangt vom Kommentar nicht mehr den Verweis auf die offene Stelle,
207 * sondern die Aussage, dass alle drei Wege geschlossen sind.
208 */
209 const SCHEMA = readFileSync('src/domain/model/schema.ts', 'utf8');
210 const PROJECTVIEW = readFileSync('src/ui/views/projectView.ts', 'utf8');
211
212 const zufahrtskommentar = SCHEMA.slice(
213 SCHEMA.indexOf('Eine Anzahl, keine Messgroesse'),
214 SCHEMA.indexOf('`intersection.arms[${index}].lanes`'),
215 );
216
217 it('weist alle drei Wege als geschlossen aus und nicht mehr auf eine Regel im Pruefbericht', () => {
218 expect(zufahrtskommentar, 'Fundstelle des Zufahrtskommentars').not.toBe('');
219 expect(zufahrtskommentar).toContain('alle drei Wege');
220 expect(zufahrtskommentar, 'der Vorschlag von damals ist nicht der Weg von heute').not.toContain(
221 'checkSignalGroups',
222 );
223 });
224
225 it('behauptet nicht mehr, das Zahlenfeld begrenze nur min und max', () => {
226 expect(zufahrtskommentar).not.toContain('begrenzt nur min und max');
227 expect(zufahrtskommentar, 'die Spalte der Signalgruppe ist geschlossen').toContain(
228 'ganzzahlAbrunden',
229 );
230 });
231
232 it('trifft zu: auch das Zufahrtsfeld traegt die Grenze jetzt', () => {
233 // Die Gegenprobe zur Aussage des Kommentars. Faellt die Option an der
234 // Zufahrt wieder heraus, faellt dieser Fall - und der Kommentar darueber
235 // wuerde einen geschlossenen Weg behaupten, den es nicht gibt.
236 const zufahrtsfeld = PROJECTVIEW.slice(
237 PROJECTVIEW.indexOf('arm.lanes'),
238 PROJECTVIEW.indexOf('arm.vZul'),
239 );
240
241 expect(zufahrtsfeld, 'Fundstelle des Fahrstreifenfelds der Zufahrt').not.toBe('');
242 expect(zufahrtsfeld).toContain('ganzzahlAbrunden: true');
243 });
244 });