lsa-planer

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

/ tests ui vertraeglichkeitVerwirft.test.ts

7,9 KB Rohdatei
tests/ui/vertraeglichkeitVerwirft.test.ts — 227 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 { Conflict, Project } from '@/domain/model/project';
5 import { compatibilityView } from '@/ui/views/compatibilityView';
6
7 /**
8 * Befund 35 zur Fassung 5.9.0 (Datenverlust).
9 *
10 * WAS DER ANWENDER SONST ERLEBT
11 *
12 * Er hat das Paar K1/K3 vermasst - Raeumweg 25 m - und "enger Innenradius"
13 * angehakt. In der Ansicht "Verträglichkeit" klickt er die Zelle einmal
14 * (Zustand "verträglich") und, weil das ein Versehen war, gleich noch einmal.
15 * `toggleConflictPair` entfernt beim ersten Klick BEIDE gerichteten
16 * Datensaetze, der zweite legt ueber `createConflict` zwei frische an:
17 * Raeumweg 0, Einfahrweg 0, alle Ueberschreibungen null, die von Hand
18 * abgestimmte Zwischenzeit fort, Bemerkung leer, `engerRadius` und
19 * `haltVorKnoten` weggelassen. Nichts hat es angesagt.
20 *
21 * Die Wege faellt der Pruefbericht spaeter auf (`konflikte.raeumweg-fehlt`,
22 * Fehler mit Exportsperre). Den Verlust von `engerRadius` faengt niemand auf,
23 * und er geht in die gefaehrliche Richtung: Ohne das Merkmal raeumt der
24 * Abbieger mit 7,0 statt 5,0 m/s - bei s0 = 25 m rund 1,8 s kuerzere
25 * Zwischenzeit, nachdem der Bearbeiter den angemahnten Raeumweg wieder
26 * eingetragen hat. Ein Merkmal ueber die Oertlichkeit laesst sich aus den
27 * Daten nicht rekonstruieren.
28 *
29 * Die Nachbarschaltflaeche "Alle aufheben" fragt fuer denselben Vorgang ueber
30 * alle Paare und traegt `variant: 'gefahr'`; beim Loeschen einer Signalgruppe
31 * nennt das Programm sogar die Zahl der betroffenen Konfliktbeziehungen. Nur
32 * die Zelle verwarf ohne Rueckfrage.
33 */
34
35 const FESTES_DATUM = new Date('2026-01-01T00:00:00Z');
36
37 afterEach(() => {
38 document.body.replaceChildren();
39 });
40
41 interface Ansicht {
42 readonly wurzel: HTMLElement;
43 readonly store: ProjectStore;
44 readonly aufraeumen: () => void;
45 }
46
47 function zeichne(project: Project): Ansicht {
48 const wurzel = document.createElement('div');
49 document.body.append(wurzel);
50 const store = new ProjectStore(project);
51 const abmelden = compatibilityView.render(wurzel, {
52 store,
53 refresh: () => {},
54 navigate: () => {},
55 target: null,
56 });
57 return {
58 wurzel,
59 store,
60 aufraeumen: () => {
61 abmelden();
62 wurzel.remove();
63 },
64 };
65 }
66
67 async function mikroschritt(): Promise<void> {
68 await new Promise<void>((fertig) => {
69 queueMicrotask(fertig);
70 });
71 }
72
73 /** Der Umschaltknopf der Zelle in Zeile `i`, Spalte `j` der oberen Dreiecksmatrix. */
74 function zelle(wurzel: HTMLElement, i: number, j: number): HTMLButtonElement {
75 const zeile = wurzel.querySelectorAll<HTMLTableRowElement>('table.matrix tbody tr')[i];
76 // Erste Zelle der Zeile ist der Zeilenkopf, danach je Signalgruppe eine.
77 const knopf = zeile?.children[1 + j]?.querySelector('button') ?? null;
78 if (knopf === null) throw new Error(`Kein Umschaltknopf in Zeile ${i}, Spalte ${j}`);
79 return knopf;
80 }
81
82 function rueckfrage(): HTMLElement | null {
83 return document.querySelector<HTMLElement>('.dialog-hintergrund .dialog');
84 }
85
86 function dialogKnopf(beschriftung: string): HTMLButtonElement {
87 const knopf = [
88 ...document.querySelectorAll<HTMLButtonElement>('.dialog-hintergrund .dialog-aktionen button'),
89 ].find((b) => (b.textContent ?? '').trim() === beschriftung);
90 if (knopf === undefined) throw new Error(`Keine Schaltflaeche "${beschriftung}"`);
91 return knopf;
92 }
93
94 /** Beide gerichteten Beziehungen des Paares. */
95 function beziehungen(project: Project, aId: string, bId: string): Conflict[] {
96 return project.conflicts.filter(
97 (c) => (c.fromId === aId && c.toId === bId) || (c.fromId === bId && c.toId === aId),
98 );
99 }
100
101 /** Regelknotenpunkt, an dessen Paar K1/K3 der enge Innenradius haengt. */
102 function mitMerkmal(): { projekt: Project; k1: string; k3: string } {
103 const basis = createStandardIntersectionProject('Verträglichkeit', FESTES_DATUM);
104 const k1 = basis.signalGroups.find((g) => g.name === 'K1')!.id;
105 const k3 = basis.signalGroups.find((g) => g.name === 'K3')!.id;
106 return {
107 k1,
108 k3,
109 projekt: {
110 ...basis,
111 conflicts: basis.conflicts.map((c) =>
112 c.fromId === k1 && c.toId === k3
113 ? { ...c, clearingDistance: 25, engerRadius: true, manualIntergreen: 7 }
114 : c,
115 ),
116 },
117 };
118 }
119
120 /** Dieselbe Beziehung ohne jede erfasste Angabe - so, wie `createConflict` sie anlegt. */
121 function ohneAngaben(): { projekt: Project; k1: string; k3: string } {
122 const { projekt, k1, k3 } = mitMerkmal();
123 const geleert = (c: Conflict): Conflict => {
124 const { engerRadius: _radius, haltVorKnoten: _halt, ...rest } = c;
125 return { ...rest, clearingDistance: 0, enteringDistance: 0, manualIntergreen: null, note: '' };
126 };
127 return {
128 k1,
129 k3,
130 projekt: {
131 ...projekt,
132 conflicts: projekt.conflicts.map((c) =>
133 (c.fromId === k1 && c.toId === k3) || (c.fromId === k3 && c.toId === k1) ? geleert(c) : c,
134 ),
135 },
136 };
137 }
138
139 describe('Verträglichkeitsmatrix: das Aufheben eines vermassten Paares fragt zurueck', () => {
140 it('nennt vor dem Verwerfen, was an dem Paar haengt, und laesst es bis dahin stehen', () => {
141 const { projekt, k1, k3 } = mitMerkmal();
142 const ansicht = zeichne(projekt);
143 try {
144 // Zeile 0 ist K1, Spalte 2 ist K3.
145 zelle(ansicht.wurzel, 0, 2).click();
146
147 const dialog = rueckfrage();
148 expect(dialog, 'Die Zelle verwirft ohne Rueckfrage').not.toBeNull();
149 const text = dialog?.textContent ?? '';
150 expect(text, 'die Rueckfrage nennt das Paar nicht').toContain('K1');
151 expect(text).toContain('K3');
152 expect(text, 'die Rueckfrage nennt das gefaehrlichste Merkmal nicht').toContain(
153 'enger Innenradius',
154 );
155 expect(text).toContain('Räumweg');
156 expect(text).toContain('Zwischenzeit');
157
158 // Solange die Rueckfrage steht, ist nichts verworfen.
159 expect(beziehungen(ansicht.store.getProject(), k1, k3)).toHaveLength(2);
160 } finally {
161 ansicht.aufraeumen();
162 }
163 });
164
165 it('laesst die Angaben unberuehrt, wenn die Rueckfrage abgebrochen wird', async () => {
166 const { projekt, k1, k3 } = mitMerkmal();
167 const ansicht = zeichne(projekt);
168 try {
169 zelle(ansicht.wurzel, 0, 2).click();
170 dialogKnopf('Abbrechen').click();
171 await mikroschritt();
172 await mikroschritt();
173
174 const paar = beziehungen(ansicht.store.getProject(), k1, k3);
175 expect(paar).toHaveLength(2);
176 expect(
177 paar.some((c) => c.engerRadius === true),
178 'der enge Innenradius ist fort',
179 ).toBe(true);
180 } finally {
181 ansicht.aufraeumen();
182 }
183 });
184
185 it('hebt das Paar auf, wenn der Bearbeiter zustimmt', async () => {
186 const { projekt, k1, k3 } = mitMerkmal();
187 const ansicht = zeichne(projekt);
188 try {
189 zelle(ansicht.wurzel, 0, 2).click();
190 dialogKnopf('Aufheben').click();
191 await mikroschritt();
192 await mikroschritt();
193
194 expect(beziehungen(ansicht.store.getProject(), k1, k3)).toHaveLength(0);
195 } finally {
196 ansicht.aufraeumen();
197 }
198 });
199
200 it('fragt nicht, wenn an dem Paar nichts erfasst ist', () => {
201 const { projekt, k1, k3 } = ohneAngaben();
202 const ansicht = zeichne(projekt);
203 try {
204 zelle(ansicht.wurzel, 0, 2).click();
205
206 expect(rueckfrage(), 'eine Rueckfrage ohne Anlass').toBeNull();
207 expect(beziehungen(ansicht.store.getProject(), k1, k3)).toHaveLength(0);
208 } finally {
209 ansicht.aufraeumen();
210 }
211 });
212
213 it('fragt nicht beim Erfassen eines Konflikts', () => {
214 const { projekt, k1 } = mitMerkmal();
215 const k2 = projekt.signalGroups.find((g) => g.name === 'K2')!.id;
216 const ansicht = zeichne(projekt);
217 try {
218 // Zeile 0 ist K1, Spalte 1 ist K2 - dieses Paar ist verträglich.
219 zelle(ansicht.wurzel, 0, 1).click();
220
221 expect(rueckfrage()).toBeNull();
222 expect(beziehungen(ansicht.store.getProject(), k1, k2)).toHaveLength(2);
223 } finally {
224 ansicht.aufraeumen();
225 }
226 });
227 });