import { afterEach, describe, expect, it } from 'vitest'; import * as actions from '@/app/actions'; import { ProjectStore } from '@/app/store'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { RILSA_DEFAULTS, SAFETY_FLOORS } from '@/domain/rilsa/constants'; import type { Project } from '@/domain/model/project'; import type { VehicleClass } from '@/domain/rilsa/types'; import type { ViewContext } from '@/ui/shell'; import { signalGroupsView } from '@/ui/views/signalGroupsView'; /* * Befund 4 zur Fassung 5.8.0. * * Die Spalte "Fahrzeugart" bot an JEDER Signalgruppe alle sieben Arten an, * auch "ohne Längenzuschlag" (0 m) und "Fahrrad" (0 m). An einem Kfz- oder * OePNV-Strom setzt das den Raeumweg ohne Fahrzeuglaenge an: Bei Raeumweg 22 m, * Einfahrweg 8 m und einem linksabbiegenden Kfz-Strom fiel die Zwischenzeit von * 7 s auf 6 s - die harte Schranke SAFETY_FLOORS.minVehicleLengthKfz = 6 m, die * die Vorgabenverwaltung mit "Ein kuerzerer Laengenzuschlag als 6 m verkuerzt * den Raeumweg unzulaessig" erzwingt, war ueber ein Auswahlfeld umgehbar. Eine * Meldung gab es dazu nirgends. * * Zweiter, unauffaelligerer Bedienweg: Wer eine Fussgaengergruppe auf * "Kraftfahrzeuge" umstellt, behielt die Fahrzeugart "ohne Laengenzuschlag", * ohne die Spalte je angefasst zu haben. */ let aufraeumen: (() => void) | null = null; afterEach(() => { aufraeumen?.(); aufraeumen = null; document.body.replaceChildren(); }); function zeichne(project: Project): { wurzel: HTMLElement; store: ProjectStore } { const wurzel = document.createElement('div'); document.body.append(wurzel); const store = new ProjectStore(project); const context: ViewContext = { store, refresh: () => {}, navigate: () => {}, target: null, }; const dispose = signalGroupsView.render(wurzel, context); aufraeumen = () => { dispose(); wurzel.remove(); }; return { wurzel, store }; } /** Das Auswahlfeld einer Spalte in der Zeile einer Signalgruppe. */ function feldDerZeile(wurzel: HTMLElement, spalte: string, zeile: string): HTMLSelectElement { const feld = wurzel.querySelector(`select[aria-label="${spalte}, ${zeile}"]`); expect(feld, `Kein Feld "${spalte}" in der Zeile "${zeile}"`).not.toBeNull(); return feld as HTMLSelectElement; } const werteDer = (feld: HTMLSelectElement): VehicleClass[] => [...feld.options].map((option) => option.value as VehicleClass); /** Projekt mit einer OePNV-Signalgruppe (Bezeichnung B1). */ function mitOepnv(): Project { return actions.addSignalGroup(createStandardIntersectionProject(), 'oepnv'); } describe('Fahrzeugart an Kfz- und OePNV-Signalgruppen (Befund 4)', () => { for (const [verkehrsart, zeile, projekt] of [ ['Kraftfahrzeuge', 'K1', createStandardIntersectionProject], ['ÖPNV', 'B1', mitOepnv], ] as const) { it(`bietet an ${verkehrsart} keine Art unterhalb der Schranke von 6 m an`, () => { const { wurzel } = zeichne(projekt()); const werte = werteDer(feldDerZeile(wurzel, 'Fahrzeugart', zeile)); // Beide Arten tragen 0 m Laengenzuschlag - der Raeumweg bliebe ohne // Fahrzeuglaenge, und die Zwischenzeit faellt um rund eine Sekunde. expect(werte, 'Fahrzeugart ohne Längenzuschlag wählbar').not.toContain('keine'); expect(werte, 'Fahrzeugart Fahrrad wählbar').not.toContain('rad'); expect(werte).toContain('pkw'); // Die Schranke selbst ist der Massstab, nicht eine Liste von Namen: Wer // einen Laengenzuschlag ueber die Vorgabenverwaltung aendert, aendert // damit auch, was hier waehlbar ist. for (const art of werte) { expect( RILSA_DEFAULTS.vehicleLength[art], `Fahrzeugart "${art}" unterschreitet die Schranke`, ).toBeGreaterThanOrEqual(SAFETY_FLOORS.minVehicleLengthKfz); } }); } it('laesst an einer Fussgaengergruppe weiter jede Art zu', () => { // Nach oben ist derselbe Widerspruch nur unwirtschaftlich, nicht unsicher; // die Liste bleibt dort unveraendert. const { wurzel } = zeichne(createStandardIntersectionProject()); expect(werteDer(feldDerZeile(wurzel, 'Fahrzeugart', 'F1'))).toContain('keine'); }); it('setzt beim Wechsel von Fussgaenger auf Kraftfahrzeuge eine zulaessige Art', () => { const { wurzel, store } = zeichne(createStandardIntersectionProject()); const verkehrsart = feldDerZeile(wurzel, 'Verkehrsart', 'F1'); verkehrsart.value = 'kfz'; verkehrsart.dispatchEvent(new Event('change')); const gruppe = store.getProject().signalGroups.find((g) => g.name === 'F1'); expect(gruppe?.mode).toBe('kfz'); // Vorher blieb hier 'keine' stehen: ein Kfz-Strom mit 0 m Laengenzuschlag, // ohne dass der Bearbeiter die Spalte Fahrzeugart je angefasst haette. expect(gruppe?.vehicleClass).toBe('pkw'); }); it('zeigt eine im Projekt stehende unzulaessige Art an und kennzeichnet sie', () => { // Seit der Fassung 5.9.0 prueft das Schema die Fahrzeugart auch gegen // die Verkehrsart und ersetzt eine unzulaessige mit Meldung; ueber eine // geladene Datei ist dieser Zustand damit nicht mehr erreichbar. Ueber ein // von Hand zusammengesetztes Projekt schon - und dann darf die Anzeige ihn // weder verschweigen noch stillschweigend gegen eine andere Art // austauschen. Das Projekt entsteht hier deshalb ohne `parseProject`. const basis = createStandardIntersectionProject(); const projekt: Project = { ...basis, signalGroups: basis.signalGroups.map((g) => g.name === 'K1' ? { ...g, vehicleClass: 'keine' } : g, ), }; const { wurzel } = zeichne(projekt); const feld = feldDerZeile(wurzel, 'Fahrzeugart', 'K1'); expect(feld.value, 'Die Anzeige zeigt eine andere Art als die gespeicherte').toBe('keine'); const gewaehlt = feld.options[feld.selectedIndex]; expect(gewaehlt?.textContent ?? '').toContain('unzulässig'); }); });