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