lsa-planer

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

/ tests domain fahrstreifenzahl.test.ts

7,8 KB Rohdatei
tests/domain/fahrstreifenzahl.test.ts — 183 Zeilen
1 import { readFileSync } from 'node:fs';
2 import { describe, expect, it } from 'vitest';
3 import { parseProject } from '@/domain/model/schema';
4 import { createStandardIntersectionProject } from '@/domain/model/factory';
5
6 /*
7 * Fassung 5.10.0 - die Fahrstreifenzahl darf gebrochen sein.
8 *
9 * `num()` prueft `lanes` nur gegen 1 bis 8. Eine Projektdatei mit
10 * "lanes": 2.5 wurde ohne ein Wort uebernommen, ging mit 2,5 in
11 * `saturationFlow` ein und stand so in Tabelle und Ausdruck. Befund 55 hat den
12 * Wert zuletzt nur noch in deutscher Schreibweise gedruckt - eine unzulaessige
13 * Angabe in gepflegter Form.
14 *
15 * Ein Fahrstreifen ist eine ANZAHL. Ein halber Fahrstreifen ist keine Planung,
16 * sondern ein verdorbener Eintrag, und die Grenze zieht dieselbe Stelle, die
17 * schon 1 bis 8 zieht: das Einlesen.
18 *
19 * SO WEIT REICHT ES, NICHT WEITER. Fuer jede Projektdatei ist der Fall damit
20 * geschlossen. Ueber die Oberflaeche entsteht ein gebrochener Wert weiterhin -
21 * `numberCell` in src/ui/inputs.ts begrenzt nur `min` und `max` (dort bewusst
22 * so: `step` ist die Sprungweite der Pfeiltasten, kein Raster), eine getippte
23 * 2,5 geht also unbeanstandet in den Speicher, in `saturationFlow`, in die
24 * Tabelle und in den Ausdruck. Zu schliessen ist das in `checkSignalGroups` in
25 * src/domain/validation/engine.ts, die nicht auf der Liste dieser Gruppe steht.
26 *
27 * DIE RICHTUNG IST ABWAERTS. Die Fahrstreifenzahl geht ueber
28 * `saturationFlow` in die Kapazitaet ein; aufrunden gaebe Kapazitaet, die
29 * nicht da ist, und liesse den Plan besser aussehen, als er ist. Abrunden
30 * nimmt Kapazitaet weg - die sichere Richtung -, und gemeldet wird es
31 * ohnehin.
32 *
33 * GEGEN DEN ALTSTAND (5.9.0) schlagen fuenf Faelle fehl: die vier zum Einlesen
34 * und der zum Vorbehalt im Kommentar an der Zufahrt. Die uebrigen vier - drei
35 * Gegenproben und der Blick in engine.ts - bestehen dort ebenso.
36 */
37
38 const DATUM = new Date('2026-01-01T00:00:00Z');
39
40 /** Das Projekt als schlichte Daten - so, wie es aus einer Datei kaeme. */
41 function alsDatei(): Record<string, unknown> {
42 return JSON.parse(
43 JSON.stringify(createStandardIntersectionProject('Fahrstreifen', DATUM)),
44 ) as Record<string, unknown>;
45 }
46
47 /** Dieselbe Datei, aber die erste Signalgruppe traegt die uebergebene Zahl. */
48 function mitGruppenwert(wert: unknown): ReturnType<typeof parseProject> {
49 const roh = alsDatei();
50 const gruppen = roh['signalGroups'] as Record<string, unknown>[];
51 return parseProject({
52 ...roh,
53 signalGroups: gruppen.map((g, index) => (index === 0 ? { ...g, lanes: wert } : g)),
54 });
55 }
56
57 /** Dieselbe Datei, aber die erste Zufahrt traegt die uebergebene Zahl. */
58 function mitZufahrtswert(wert: unknown): ReturnType<typeof parseProject> {
59 const roh = alsDatei();
60 const knoten = roh['intersection'] as Record<string, unknown>;
61 const arme = knoten['arms'] as Record<string, unknown>[];
62 return parseProject({
63 ...roh,
64 intersection: {
65 ...knoten,
66 arms: arme.map((a, index) => (index === 0 ? { ...a, lanes: wert } : a)),
67 },
68 });
69 }
70
71 describe('Fahrstreifenzahl der Signalgruppe', () => {
72 it('bringt 2,5 auf eine ganze Zahl und meldet die Ersetzung', () => {
73 const { project, issues, repaired } = mitGruppenwert(2.5);
74
75 expect(project.signalGroups[0]?.lanes).toBe(2);
76 const meldung = issues.find((i) => i.path === 'signalGroups[0].lanes');
77 expect(meldung, 'Meldung zur ersetzten Fahrstreifenzahl').toBeDefined();
78 expect(meldung?.severity).toBe('warnung');
79 expect(meldung?.message).toContain('Fahrstreifenzahl');
80 expect(meldung?.message).toContain('ganze Zahl');
81 // Die Datei ist verändert worden; der Anwender muss das beim Speichern
82 // wissen.
83 expect(repaired).toBe(true);
84 });
85
86 it('rundet ab und nicht auf - 7,9 wird 7', () => {
87 // Aufrunden gaebe einen Fahrstreifen mehr Kapazitaet, als die Zufahrt hat.
88 // Das ist die unsichere Richtung, auch wenn 7,9 nach 8 aussieht.
89 const { project } = mitGruppenwert(7.9);
90
91 expect(project.signalGroups[0]?.lanes).toBe(7);
92 });
93
94 it('nimmt die Zahl auch aus einer Zeichenkette und rundet sie ab', () => {
95 // parseNumber liest "2,5" mit deutschem Komma; die Ganzzahlpruefung darf
96 // hinter dieser Umwandlung stehen, nicht davor.
97 const { project, issues } = mitGruppenwert('2,5');
98
99 expect(project.signalGroups[0]?.lanes).toBe(2);
100 expect(issues.some((i) => i.path === 'signalGroups[0].lanes')).toBe(true);
101 });
102 });
103
104 describe('Fahrstreifenzahl der Zufahrt', () => {
105 it('bringt 3,5 auf eine ganze Zahl und meldet die Ersetzung', () => {
106 const { project, issues } = mitZufahrtswert(3.5);
107
108 expect(project.intersection.arms[0]?.lanes).toBe(3);
109 const meldung = issues.find((i) => i.path === 'intersection.arms[0].lanes');
110 expect(meldung, 'Meldung zur ersetzten Fahrstreifenzahl').toBeDefined();
111 expect(meldung?.severity).toBe('warnung');
112 expect(meldung?.message).toContain('ganze Zahl');
113 });
114 });
115
116 describe('So weit reicht die Behebung', () => {
117 /*
118 * Der Kommentar an der Zufahrt las sich als abgeschlossener Fall. Er ist es
119 * nur fuer den Einlesepfad: `numberCell` in src/ui/inputs.ts begrenzt eine
120 * getippte Zahl allein gegen `min` und `max`, eine 2,5 aus der Oberflaeche
121 * steht danach unbeanstandet im Speicher. Solange das so ist, sagt der
122 * Kommentar es auch.
123 */
124 const SCHEMA = readFileSync('src/domain/model/schema.ts', 'utf8');
125 const ENGINE = readFileSync('src/domain/validation/engine.ts', 'utf8');
126
127 it('nennt an der Zufahrt alle drei geschlossenen Wege', () => {
128 /*
129 * Frueher stand hier, der Kommentar muesse den OFFENEN Weg ueber die
130 * Oberflaeche nennen: Das Fahrstreifenfeld der Zufahrt
131 * (src/ui/views/projectView.ts) trug die Ganzzahlgrenze als einziges der
132 * drei Felder nicht. Diese Luecke ist geschlossen; die Zusicherung haette
133 * sonst einen Vorbehalt festgehalten, den es nicht mehr gibt.
134 *
135 * Was der Kommentar weiterhin sagen muss: dass die Grenze an jeder der
136 * drei Stellen greift und dass der Pruefbericht sie nach wie vor nicht
137 * prueft - der Fall darunter ist die Gegenprobe dazu.
138 */
139 const zufahrt = SCHEMA.slice(
140 SCHEMA.indexOf('Eine Anzahl, keine Messgroesse'),
141 SCHEMA.indexOf('`intersection.arms[${index}].lanes`'),
142 );
143
144 expect(zufahrt, 'Fundstelle des Zufahrtskommentars').not.toBe('');
145 expect(zufahrt).toContain('alle drei Wege');
146 expect(zufahrt).toContain('engine.ts');
147 });
148
149 it('haelt fest, dass der Pruefbericht die Fahrstreifenzahl noch nicht prueft', () => {
150 // Die Gegenprobe zum Vorbehalt: Sobald eine Regel dazukommt, faellt dieser
151 // Fall - und mit ihm der Satz, der den Weg als offen beschreibt.
152 expect(ENGINE).not.toContain('lanes');
153 });
154 });
155
156 describe('Gegenproben - was sich nicht aendern darf', () => {
157 it('laesst eine ganze Fahrstreifenzahl unberuehrt und schweigt dazu', () => {
158 const { project, issues } = mitGruppenwert(3);
159
160 expect(project.signalGroups[0]?.lanes).toBe(3);
161 expect(issues.filter((i) => i.path === 'signalGroups[0].lanes')).toHaveLength(0);
162 });
163
164 it('meldet einen Wert unter dem Mindestwert weiterhin nur einmal', () => {
165 // 0,5 liegt unter 1. Die Begrenzung hebt auf 1 an - und 1 ist bereits
166 // ganz. Zwei Meldungen zu einem Eintrag waeren eine zu viel.
167 const { project, issues } = mitGruppenwert(0.5);
168
169 expect(project.signalGroups[0]?.lanes).toBe(1);
170 const meldungen = issues.filter((i) => i.path === 'signalGroups[0].lanes');
171 expect(meldungen).toHaveLength(1);
172 expect(meldungen[0]?.message).toContain('Mindestwert');
173 });
174
175 it('meldet einen Wert ueber dem Hoechstwert weiterhin nur einmal', () => {
176 const { project, issues } = mitGruppenwert(8.5);
177
178 expect(project.signalGroups[0]?.lanes).toBe(8);
179 const meldungen = issues.filter((i) => i.path === 'signalGroups[0].lanes');
180 expect(meldungen).toHaveLength(1);
181 expect(meldungen[0]?.message).toContain('Höchstwert');
182 });
183 });