lsa-planer

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

/ tests ui fahrzeugart.test.ts

5,8 KB Rohdatei
tests/ui/fahrzeugart.test.ts — 137 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import * as actions from '@/app/actions';
3 import { ProjectStore } from '@/app/store';
4 import { createStandardIntersectionProject } from '@/domain/model/factory';
5 import { RILSA_DEFAULTS, SAFETY_FLOORS } from '@/domain/rilsa/constants';
6 import type { Project } from '@/domain/model/project';
7 import type { VehicleClass } from '@/domain/rilsa/types';
8 import type { ViewContext } from '@/ui/shell';
9 import { signalGroupsView } from '@/ui/views/signalGroupsView';
10
11 /*
12 * Befund 4 zur Fassung 5.8.0.
13 *
14 * Die Spalte "Fahrzeugart" bot an JEDER Signalgruppe alle sieben Arten an,
15 * auch "ohne Längenzuschlag" (0 m) und "Fahrrad" (0 m). An einem Kfz- oder
16 * OePNV-Strom setzt das den Raeumweg ohne Fahrzeuglaenge an: Bei Raeumweg 22 m,
17 * Einfahrweg 8 m und einem linksabbiegenden Kfz-Strom fiel die Zwischenzeit von
18 * 7 s auf 6 s - die harte Schranke SAFETY_FLOORS.minVehicleLengthKfz = 6 m, die
19 * die Vorgabenverwaltung mit "Ein kuerzerer Laengenzuschlag als 6 m verkuerzt
20 * den Raeumweg unzulaessig" erzwingt, war ueber ein Auswahlfeld umgehbar. Eine
21 * Meldung gab es dazu nirgends.
22 *
23 * Zweiter, unauffaelligerer Bedienweg: Wer eine Fussgaengergruppe auf
24 * "Kraftfahrzeuge" umstellt, behielt die Fahrzeugart "ohne Laengenzuschlag",
25 * ohne die Spalte je angefasst zu haben.
26 */
27
28 let aufraeumen: (() => void) | null = null;
29 afterEach(() => {
30 aufraeumen?.();
31 aufraeumen = null;
32 document.body.replaceChildren();
33 });
34
35 function zeichne(project: Project): { wurzel: HTMLElement; store: ProjectStore } {
36 const wurzel = document.createElement('div');
37 document.body.append(wurzel);
38 const store = new ProjectStore(project);
39 const context: ViewContext = {
40 store,
41 refresh: () => {},
42 navigate: () => {},
43 target: null,
44 };
45 const dispose = signalGroupsView.render(wurzel, context);
46 aufraeumen = () => {
47 dispose();
48 wurzel.remove();
49 };
50 return { wurzel, store };
51 }
52
53 /** Das Auswahlfeld einer Spalte in der Zeile einer Signalgruppe. */
54 function feldDerZeile(wurzel: HTMLElement, spalte: string, zeile: string): HTMLSelectElement {
55 const feld = wurzel.querySelector<HTMLSelectElement>(`select[aria-label="${spalte}, ${zeile}"]`);
56 expect(feld, `Kein Feld "${spalte}" in der Zeile "${zeile}"`).not.toBeNull();
57 return feld as HTMLSelectElement;
58 }
59
60 const werteDer = (feld: HTMLSelectElement): VehicleClass[] =>
61 [...feld.options].map((option) => option.value as VehicleClass);
62
63 /** Projekt mit einer OePNV-Signalgruppe (Bezeichnung B1). */
64 function mitOepnv(): Project {
65 return actions.addSignalGroup(createStandardIntersectionProject(), 'oepnv');
66 }
67
68 describe('Fahrzeugart an Kfz- und OePNV-Signalgruppen (Befund 4)', () => {
69 for (const [verkehrsart, zeile, projekt] of [
70 ['Kraftfahrzeuge', 'K1', createStandardIntersectionProject],
71 ['ÖPNV', 'B1', mitOepnv],
72 ] as const) {
73 it(`bietet an ${verkehrsart} keine Art unterhalb der Schranke von 6 m an`, () => {
74 const { wurzel } = zeichne(projekt());
75 const werte = werteDer(feldDerZeile(wurzel, 'Fahrzeugart', zeile));
76
77 // Beide Arten tragen 0 m Laengenzuschlag - der Raeumweg bliebe ohne
78 // Fahrzeuglaenge, und die Zwischenzeit faellt um rund eine Sekunde.
79 expect(werte, 'Fahrzeugart ohne Längenzuschlag wählbar').not.toContain('keine');
80 expect(werte, 'Fahrzeugart Fahrrad wählbar').not.toContain('rad');
81 expect(werte).toContain('pkw');
82
83 // Die Schranke selbst ist der Massstab, nicht eine Liste von Namen: Wer
84 // einen Laengenzuschlag ueber die Vorgabenverwaltung aendert, aendert
85 // damit auch, was hier waehlbar ist.
86 for (const art of werte) {
87 expect(
88 RILSA_DEFAULTS.vehicleLength[art],
89 `Fahrzeugart "${art}" unterschreitet die Schranke`,
90 ).toBeGreaterThanOrEqual(SAFETY_FLOORS.minVehicleLengthKfz);
91 }
92 });
93 }
94
95 it('laesst an einer Fussgaengergruppe weiter jede Art zu', () => {
96 // Nach oben ist derselbe Widerspruch nur unwirtschaftlich, nicht unsicher;
97 // die Liste bleibt dort unveraendert.
98 const { wurzel } = zeichne(createStandardIntersectionProject());
99 expect(werteDer(feldDerZeile(wurzel, 'Fahrzeugart', 'F1'))).toContain('keine');
100 });
101
102 it('setzt beim Wechsel von Fussgaenger auf Kraftfahrzeuge eine zulaessige Art', () => {
103 const { wurzel, store } = zeichne(createStandardIntersectionProject());
104 const verkehrsart = feldDerZeile(wurzel, 'Verkehrsart', 'F1');
105
106 verkehrsart.value = 'kfz';
107 verkehrsart.dispatchEvent(new Event('change'));
108
109 const gruppe = store.getProject().signalGroups.find((g) => g.name === 'F1');
110 expect(gruppe?.mode).toBe('kfz');
111 // Vorher blieb hier 'keine' stehen: ein Kfz-Strom mit 0 m Laengenzuschlag,
112 // ohne dass der Bearbeiter die Spalte Fahrzeugart je angefasst haette.
113 expect(gruppe?.vehicleClass).toBe('pkw');
114 });
115
116 it('zeigt eine im Projekt stehende unzulaessige Art an und kennzeichnet sie', () => {
117 // Seit der Fassung 5.9.0 prueft das Schema die Fahrzeugart auch gegen
118 // die Verkehrsart und ersetzt eine unzulaessige mit Meldung; ueber eine
119 // geladene Datei ist dieser Zustand damit nicht mehr erreichbar. Ueber ein
120 // von Hand zusammengesetztes Projekt schon - und dann darf die Anzeige ihn
121 // weder verschweigen noch stillschweigend gegen eine andere Art
122 // austauschen. Das Projekt entsteht hier deshalb ohne `parseProject`.
123 const basis = createStandardIntersectionProject();
124 const projekt: Project = {
125 ...basis,
126 signalGroups: basis.signalGroups.map((g) =>
127 g.name === 'K1' ? { ...g, vehicleClass: 'keine' } : g,
128 ),
129 };
130 const { wurzel } = zeichne(projekt);
131 const feld = feldDerZeile(wurzel, 'Fahrzeugart', 'K1');
132
133 expect(feld.value, 'Die Anzeige zeigt eine andere Art als die gespeicherte').toBe('keine');
134 const gewaehlt = feld.options[feld.selectedIndex];
135 expect(gewaehlt?.textContent ?? '').toContain('unzulässig');
136 });
137 });