lsa-planer

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

/ tests ui hoechstfreigabezeit.test.ts

6,8 KB Rohdatei
tests/ui/hoechstfreigabezeit.test.ts — 178 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { ProjectStore } from '@/app/store';
3 import { createStandardIntersectionProject } from '@/domain/model/factory';
4 import type { Project } from '@/domain/model/project';
5 import { RILSA_DEFAULTS } from '@/domain/rilsa/constants';
6 import { mitBewahrtemFokus } from '@/ui/fokus';
7 import type { ViewContext } from '@/ui/shell';
8 import { signalGroupsView } from '@/ui/views/signalGroupsView';
9
10 /**
11 * Folge aus den Befunden 36 und 42 zur Fassung 5.9.0.
12 *
13 * Die Hoechstfreigabezeit je Signalgruppe (`maxGreenOverride`) wirkt seit
14 * diesen Befunden in der Freigabezeitverteilung (`phaseMaxGreen` in
15 * domain/plan/signalPlan.ts) - ein Eingabefeld hatte sie nicht. Sie konnte nur
16 * aus einer Projektdatei oder aus dem Aufstieg von 4.x kommen; ueber die
17 * Bedienung war sie weder zu setzen noch zu entfernen. Damit lief zugleich der
18 * Rat der Regel `freigabezeit-rest-umgelegt` ins Leere: "... erfassen Sie die
19 * Verkehrsstärken der maßgebenden Signalgruppen oder heben Sie deren
20 * Höchstfreigabezeit an."
21 *
22 * DIE GRENZEN DES FELDES: Eine Hoechstfreigabezeit unter der
23 * Mindestfreigabezeit ist ein Widerspruch. Der Fachkern faengt ihn ab - er
24 * rechnet mit der Mindestfreigabezeit und meldet die Vorgabe als wirkungslos
25 * ('hoechstfreigabezeit-unter-mindestfreigabezeit'). Das Feld nimmt deshalb
26 * dieselbe Untergrenze wie das Feld der Mindestfreigabezeit daneben: den Wert,
27 * der bei leerem Feld gilt. Was zwischen dieser Untergrenze und der
28 * Mindestfreigabezeit der PHASE liegt, bleibt eintragbar - dort ist die
29 * Meldung des Pruefberichts die richtige Antwort, nicht eine Sperre am Feld.
30 */
31
32 const FESTES_DATUM = new Date('2026-01-01T00:00:00Z');
33
34 let aufraeumen: (() => void) | null = null;
35 afterEach(() => {
36 aufraeumen?.();
37 aufraeumen = null;
38 document.body.replaceChildren();
39 });
40
41 interface Ansicht {
42 readonly wurzel: HTMLElement;
43 readonly store: ProjectStore;
44 }
45
46 function zeichne(project: Project): Ansicht {
47 const wurzel = document.createElement('div');
48 document.body.append(wurzel);
49 const store = new ProjectStore(project);
50 let abmelden: () => void = () => {};
51 const context: ViewContext = {
52 store,
53 refresh: () => {
54 mitBewahrtemFokus(wurzel, () => {
55 abmelden();
56 wurzel.replaceChildren();
57 abmelden = signalGroupsView.render(wurzel, context);
58 });
59 },
60 navigate: () => {},
61 target: null,
62 };
63 abmelden = signalGroupsView.render(wurzel, context);
64 aufraeumen = () => {
65 abmelden();
66 wurzel.remove();
67 };
68 return { wurzel, store };
69 }
70
71 function k1(project: Project): string {
72 const gruppe = project.signalGroups.find((g) => g.name === 'K1');
73 if (gruppe === undefined) throw new Error('Keine Signalgruppe "K1"');
74 return gruppe.id;
75 }
76
77 function feld(ansicht: Ansicht, groupId: string, name: string): HTMLInputElement {
78 const knoten = ansicht.wurzel.querySelector<HTMLInputElement>(
79 `[data-fokus="signalgruppe|${groupId}|${name}"]`,
80 );
81 if (knoten === null) throw new Error(`Kein Feld "${name}"`);
82 return knoten;
83 }
84
85 function trageEin(eingabe: HTMLInputElement, wert: string): void {
86 eingabe.value = wert;
87 eingabe.dispatchEvent(new Event('change', { bubbles: true }));
88 }
89
90 function hoechstfreigabe(ansicht: Ansicht, groupId: string): number | null {
91 return (
92 ansicht.store.getProject().signalGroups.find((g) => g.id === groupId)?.maxGreenOverride ?? null
93 );
94 }
95
96 describe('Signalgruppen: die Höchstfreigabezeit ist eintragbar', () => {
97 it('zeigt ein leeres Feld mit dem Regelwert als Platzhalter', () => {
98 const projekt = createStandardIntersectionProject('Höchstfreigabe', FESTES_DATUM);
99 const ansicht = zeichne(projekt);
100 const eingabe = feld(ansicht, k1(projekt), 'hoechstfreigabe');
101
102 expect(eingabe.value).toBe('');
103 expect(eingabe.placeholder).toBe(String(RILSA_DEFAULTS.maxGreen));
104 });
105
106 it('übernimmt einen eingetragenen Wert in die Signalgruppe', () => {
107 const projekt = createStandardIntersectionProject('Höchstfreigabe', FESTES_DATUM);
108 const ansicht = zeichne(projekt);
109 const id = k1(projekt);
110
111 trageEin(feld(ansicht, id, 'hoechstfreigabe'), '45');
112
113 expect(hoechstfreigabe(ansicht, id)).toBe(45);
114 // Nach dem Neuaufbau steht der Wert im Feld.
115 expect(feld(ansicht, id, 'hoechstfreigabe').value).toBe('45');
116 });
117
118 it('entfernt die Vorgabe wieder, wenn das Feld geleert wird', () => {
119 const basis = createStandardIntersectionProject('Höchstfreigabe', FESTES_DATUM);
120 const id = k1(basis);
121 const projekt: Project = {
122 ...basis,
123 signalGroups: basis.signalGroups.map((g) =>
124 g.id === id ? { ...g, maxGreenOverride: 40 } : g,
125 ),
126 };
127 const ansicht = zeichne(projekt);
128 expect(
129 feld(ansicht, id, 'hoechstfreigabe').value,
130 'Vorbedingung: die Vorgabe steht im Feld',
131 ).toBe('40');
132
133 trageEin(feld(ansicht, id, 'hoechstfreigabe'), '');
134
135 expect(hoechstfreigabe(ansicht, id)).toBeNull();
136 });
137
138 it('hebt eine Eingabe unter der Mindestfreigabezeit auf diese an', () => {
139 const projekt = createStandardIntersectionProject('Höchstfreigabe', FESTES_DATUM);
140 const ansicht = zeichne(projekt);
141 const id = k1(projekt);
142 const untergrenze = Number(feld(ansicht, id, 'mindestfreigabe').min);
143 expect(untergrenze, 'Vorbedingung: das Nachbarfeld nennt eine Untergrenze').toBeGreaterThan(0);
144
145 trageEin(feld(ansicht, id, 'hoechstfreigabe'), '2');
146
147 expect(hoechstfreigabe(ansicht, id)).toBe(untergrenze);
148 });
149
150 it('sagt das Feld ausgeschrieben an wie das Nachbarfeld', () => {
151 // Der Spaltenkopf ist abgekuerzt, damit die Spalte schmal bleibt; den Namen
152 // des Bedienelements bildet `beschrifteTabelle` aus ihm. Ohne Eintrag in
153 // der Karte `ausgeschrieben` hiesse das Feld einer Sprachausgabe
154 // "Höchstfreigabe, K1 ..." - eine Abkuerzung ohne Einheit, waehrend das
155 // unmittelbare Nachbarfeld derselben Zeile ausgeschrieben angesagt wird.
156 const projekt = createStandardIntersectionProject('Höchstfreigabe', FESTES_DATUM);
157 const ansicht = zeichne(projekt);
158 const id = k1(projekt);
159 const nachbar = feld(ansicht, id, 'mindestfreigabe').getAttribute('aria-label') ?? '';
160 expect(nachbar, 'Vorbedingung: das Nachbarfeld wird ausgeschrieben angesagt').toMatch(
161 /^Mindestfreigabezeit in Sekunden, /,
162 );
163
164 const name = feld(ansicht, id, 'hoechstfreigabe').getAttribute('aria-label') ?? '';
165
166 expect(name).toBe(nachbar.replace('Mindestfreigabezeit', 'Höchstfreigabezeit'));
167 });
168
169 it('trägt die Spalte im Tabellenkopf', () => {
170 const projekt = createStandardIntersectionProject('Höchstfreigabe', FESTES_DATUM);
171 const ansicht = zeichne(projekt);
172 const koepfe = [...ansicht.wurzel.querySelectorAll('thead th')].map(
173 (th) => th.textContent?.replace('?', '').trim() ?? '',
174 );
175
176 expect(koepfe).toContain('Höchstfreigabe');
177 });
178 });