lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests ui fahrzeugartSchranke.test.ts
| 1 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; |
| 2 | import { ProjectStore } from '@/app/store'; |
| 3 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 4 | import type { RilsaDefaults } from '@/domain/rilsa/constants'; |
| 5 | import type { Project } from '@/domain/model/project'; |
| 6 | import type { TrafficMode, VehicleClass } from '@/domain/rilsa/types'; |
| 7 | import type { ViewContext } from '@/ui/shell'; |
| 8 | |
| 9 | /* |
| 10 | * Punkt X.1 - dieselbe Schranke stand zweimal. |
| 11 | * |
| 12 | * `istKraftfahrzeugstrom` und `laengenzuschlagGenuegt` fuehrte die |
| 13 | * Signalgruppenansicht als eigene Fassungen, obwohl beide seit der |
| 14 | * Fassung 5.9.0 in `@/domain/rilsa/intergreen` stehen und dort von Einlesen und |
| 15 | * Zwischenzeitrechnung benutzt werden. Zwei Fassungen derselben Schranke laufen |
| 16 | * auseinander - die des Fachkerns war bereits strenger (sie prueft den |
| 17 | * hinterlegten Zuschlag zusaetzlich mit `isFiniteNumber`). |
| 18 | * |
| 19 | * WIE HIER GEMESSEN WIRD: Nicht am Quelltext, sondern an der Wirkung. Der |
| 20 | * Fachkern wird durch eine gesteuerte Fassung ersetzt; benutzt die Ansicht ihre |
| 21 | * eigene, bleibt die Steuerung wirkungslos, und die Faelle sind rot. Der |
| 22 | * Unterschied der beiden Fassungen selbst (`isFiniteNumber`) ist ueber die |
| 23 | * Vorgabenverwaltung nicht erreichbar - sie kappt jede Laengenangabe auf ihren |
| 24 | * Wertebereich - und taugt deshalb nicht als Massstab. |
| 25 | */ |
| 26 | |
| 27 | const steuerung = vi.hoisted(() => ({ |
| 28 | genuegt: null as ((art: string) => boolean) | null, |
| 29 | kfzStrom: null as ((mode: string) => boolean) | null, |
| 30 | })); |
| 31 | |
| 32 | vi.mock('@/domain/rilsa/intergreen', async () => { |
| 33 | const echt = await vi.importActual<typeof import('@/domain/rilsa/intergreen')>( |
| 34 | '@/domain/rilsa/intergreen', |
| 35 | ); |
| 36 | return { |
| 37 | ...echt, |
| 38 | laengenzuschlagGenuegt: (art: VehicleClass, defaults?: RilsaDefaults): boolean => |
| 39 | steuerung.genuegt === null |
| 40 | ? echt.laengenzuschlagGenuegt(art, defaults) |
| 41 | : steuerung.genuegt(art), |
| 42 | istKraftfahrzeugstrom: (mode: TrafficMode): boolean => |
| 43 | steuerung.kfzStrom === null ? echt.istKraftfahrzeugstrom(mode) : steuerung.kfzStrom(mode), |
| 44 | }; |
| 45 | }); |
| 46 | |
| 47 | const { signalGroupsView } = await import('@/ui/views/signalGroupsView'); |
| 48 | |
| 49 | let aufraeumen: (() => void) | null = null; |
| 50 | |
| 51 | beforeEach(() => { |
| 52 | steuerung.genuegt = null; |
| 53 | steuerung.kfzStrom = null; |
| 54 | }); |
| 55 | |
| 56 | afterEach(() => { |
| 57 | aufraeumen?.(); |
| 58 | aufraeumen = null; |
| 59 | document.body.replaceChildren(); |
| 60 | }); |
| 61 | |
| 62 | function zeichne(project: Project): { wurzel: HTMLElement; store: ProjectStore } { |
| 63 | const wurzel = document.createElement('div'); |
| 64 | document.body.append(wurzel); |
| 65 | const store = new ProjectStore(project); |
| 66 | const context: ViewContext = { |
| 67 | store, |
| 68 | refresh: () => {}, |
| 69 | navigate: () => {}, |
| 70 | target: null, |
| 71 | }; |
| 72 | const schliessen = signalGroupsView.render(wurzel, context); |
| 73 | aufraeumen = () => { |
| 74 | schliessen(); |
| 75 | wurzel.remove(); |
| 76 | }; |
| 77 | return { wurzel, store }; |
| 78 | } |
| 79 | |
| 80 | function feldDerZeile(wurzel: HTMLElement, spalte: string, zeile: string): HTMLSelectElement { |
| 81 | const feld = wurzel.querySelector<HTMLSelectElement>(`select[aria-label="${spalte}, ${zeile}"]`); |
| 82 | expect(feld, `Kein Feld "${spalte}" in der Zeile "${zeile}"`).not.toBeNull(); |
| 83 | return feld as HTMLSelectElement; |
| 84 | } |
| 85 | |
| 86 | const werteDer = (feld: HTMLSelectElement): VehicleClass[] => |
| 87 | [...feld.options].map((option) => option.value as VehicleClass); |
| 88 | |
| 89 | describe('X.1: die Signalgruppenansicht holt die Schranke aus dem Fachkern', () => { |
| 90 | it('bietet an einem Kfz-Strom an, was `laengenzuschlagGenuegt` des Fachkerns zulaesst', () => { |
| 91 | steuerung.genuegt = () => true; |
| 92 | const { wurzel } = zeichne(createStandardIntersectionProject()); |
| 93 | |
| 94 | const werte = werteDer(feldDerZeile(wurzel, 'Fahrzeugart', 'K1')); |
| 95 | expect(werte, 'die Ansicht entscheidet mit einer eigenen Schranke').toContain('keine'); |
| 96 | expect(werte).toContain('rad'); |
| 97 | }); |
| 98 | |
| 99 | it('nimmt jede Art aus der Liste, sobald der Fachkern keine mehr zulaesst', () => { |
| 100 | steuerung.genuegt = () => false; |
| 101 | const { wurzel } = zeichne(createStandardIntersectionProject()); |
| 102 | |
| 103 | const feld = feldDerZeile(wurzel, 'Fahrzeugart', 'K1'); |
| 104 | // Uebrig bleibt allein der gespeicherte Wert - er darf nicht verschwinden, |
| 105 | // sonst zeigte das Auswahlfeld eine andere Art an als die gerechnete. |
| 106 | expect(werteDer(feld)).toEqual(['pkw']); |
| 107 | expect(feld.options[0]?.textContent ?? '').toContain('unzulässig'); |
| 108 | }); |
| 109 | |
| 110 | it('fragt auch `istKraftfahrzeugstrom` beim Fachkern nach', () => { |
| 111 | steuerung.kfzStrom = () => false; |
| 112 | const { wurzel } = zeichne(createStandardIntersectionProject()); |
| 113 | |
| 114 | // Kein Kraftfahrzeugstrom heisst: volle Liste, ohne jede Einschraenkung. |
| 115 | expect(werteDer(feldDerZeile(wurzel, 'Fahrzeugart', 'K1'))).toContain('keine'); |
| 116 | }); |
| 117 | |
| 118 | it('nimmt beim Wechsel der Verkehrsart dieselbe Schranke des Fachkerns', () => { |
| 119 | steuerung.genuegt = () => true; |
| 120 | const { wurzel, store } = zeichne(createStandardIntersectionProject()); |
| 121 | const verkehrsart = feldDerZeile(wurzel, 'Verkehrsart', 'F1'); |
| 122 | |
| 123 | verkehrsart.value = 'kfz'; |
| 124 | verkehrsart.dispatchEvent(new Event('change')); |
| 125 | |
| 126 | // Die Fussgaengergruppe traegt "keine"; genuegt der Zuschlag nach dem |
| 127 | // Fachkern, bleibt die Art stehen - der Rueckfall auf die Standardart darf |
| 128 | // nicht aus einer zweiten Schranke der Ansicht kommen. |
| 129 | expect(store.getProject().signalGroups.find((g) => g.name === 'F1')?.vehicleClass).toBe( |
| 130 | 'keine', |
| 131 | ); |
| 132 | }); |
| 133 | }); |