lsa-planer

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

/ tests ui fahrzeugartSchranke.test.ts

5,2 KB Rohdatei
tests/ui/fahrzeugartSchranke.test.ts — 133 Zeilen
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 });