lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain fahrstreifenzahl.test.ts
| 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 | }); |