lsa-planer

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

/ tests ui uebergangszeile.test.ts

6,3 KB Rohdatei
tests/ui/uebergangszeile.test.ts — 153 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { ProjectStore } from '@/app/store';
3 import { buildSignalPlan } from '@/domain/plan/signalPlan';
4 import { createStandardIntersectionProject } from '@/domain/model/factory';
5 import { orderedPhases, type Project } from '@/domain/model/project';
6 import { phasesView } from '@/ui/views/phasesView';
7
8 /**
9 * Befunde 25 und 56 zur Fassung 5.10.0 - die Zeile unter jeder
10 * Uebergangszeit der Ansicht "Phasen".
11 *
12 * BEFUND 25. Der Satz "Keine feindliche Beziehung erfasst." wurde aus
13 * `transition.critical === null` abgeleitet. `critical` bleibt aber auch dann
14 * null, wenn ALLE Beziehungen erfasst sind und rechnerisch 0 s ergeben: Der
15 * Planaufbau setzt es nur bei `value > duration`, und `duration` startet bei 0
16 * (plan/signalPlan.ts). Genau diese Unterscheidung kann der Planaufbau nach
17 * eigener Aussage nicht treffen - dort wurde aus diesem Grund schon einmal
18 * eine Meldung mit derselben Bedeutung entfernt. Die Ansicht behauptete sie
19 * weiter, und zwar ausgerechnet dort, wo eine erfasste Beziehung auf 0 s
20 * zusammengefallen ist.
21 *
22 * BEFUND 56. Der Zielname stand ungefiltert in Anfuehrungszeichen; eine Phase
23 * ohne Bezeichnung ergab `Übergang zu "": 10 s`. Ueberall sonst in derselben
24 * Datei ist der Rueckfall gesetzt.
25 */
26
27 const STICHTAG = new Date('2026-01-01T00:00:00Z');
28
29 let aufraeumen: (() => void) | null = null;
30 afterEach(() => {
31 aufraeumen?.();
32 aufraeumen = null;
33 document.body.replaceChildren();
34 });
35
36 function zeichne(project: Project): HTMLElement {
37 const wurzel = document.createElement('div');
38 document.body.append(wurzel);
39 const schliessen = phasesView.render(wurzel, {
40 store: new ProjectStore(project),
41 refresh: () => {},
42 navigate: () => {},
43 target: null,
44 });
45 aufraeumen = () => {
46 schliessen();
47 wurzel.remove();
48 };
49 return wurzel;
50 }
51
52 /** Fette Ueberschrift und Erlaeuterung jeder Uebergangskarte, in Kartenfolge. */
53 function uebergaenge(wurzel: HTMLElement): { kopf: string; hinweis: string }[] {
54 return [...wurzel.querySelectorAll('.hinweisbalken')].map((balken) => ({
55 kopf: (balken.querySelector('strong')?.textContent ?? '').trim(),
56 hinweis: (balken.querySelector('p')?.textContent ?? '').trim(),
57 }));
58 }
59
60 /**
61 * Standardknotenpunkt, bei dem jede Zwischenzeit rechnerisch 0 s ergibt.
62 *
63 * Kurzer Konfliktbereich, weit zurueckliegende Haltlinie - derselbe Aufbau wie
64 * `alleNull()` in tests/domain/berichtWidersprueche.test.ts, der dort belegt,
65 * dass der Pruefbericht den Uebergang dann nicht als unerfasst beanstandet. Alle sechzehn Beziehungen bleiben erfasst
66 * (zwoelf bis 5.42.1; seit 5.43.0 erfasst die Vorlage jede Furt auch gegen den
67 * Strom, der sie beim Ausfahren quert - Gruppe Furt).
68 */
69 function alleZwischenzeitenNull(): Project {
70 const roh = createStandardIntersectionProject('Nullzwischenzeiten', STICHTAG);
71 return {
72 ...roh,
73 conflicts: roh.conflicts.map((c) => ({ ...c, clearingDistance: 3, enteringDistance: 60 })),
74 };
75 }
76
77 describe('Befund 25 - die Uebergangszeile spricht nur ueber das Rechenergebnis', () => {
78 it('behauptet bei erfassten Beziehungen mit 0 s nicht, es sei keine erfasst', () => {
79 const projekt = alleZwischenzeitenNull();
80
81 // Voraussetzungen des Ausloesers - sonst pruefte der Fall etwas anderes.
82 const plan = buildSignalPlan(projekt);
83 expect(projekt.conflicts.length, 'Es sind Konfliktbeziehungen erfasst').toBe(16);
84 expect([...plan.intergreens.values()].every((i) => i.value === 0)).toBe(true);
85 expect(plan.transitions.every((t) => t.duration === 0 && t.critical === null)).toBe(true);
86
87 const zeilen = uebergaenge(zeichne(projekt));
88 expect(zeilen.length, 'Der Standardknotenpunkt hat zwei Uebergaenge').toBe(2);
89 for (const zeile of zeilen) {
90 expect(zeile.hinweis).toBe(
91 'Keine Beziehung mit einer Zwischenzeit über 0 s; ob Konflikte erfasst sind, ' +
92 'sagt der Prüfbericht.',
93 );
94 }
95 });
96
97 it('sagt dasselbe, wenn wirklich keine Beziehung erfasst ist', () => {
98 // Der Satz spricht ueber das Rechenergebnis. Ueber die Erfassung sagt er
99 // in beiden Faellen nichts - zustaendig ist der Pruefbericht, der die
100 // erfassten Beziehungen kennt (zwischenzeiten.uebergang-ungesichert).
101 const roh = createStandardIntersectionProject('Ohne Beziehungen', STICHTAG);
102 const zeilen = uebergaenge(zeichne({ ...roh, conflicts: [] }));
103 expect(zeilen.length).toBe(2);
104 for (const zeile of zeilen) {
105 expect(zeile.hinweis).toContain('Keine Beziehung mit einer Zwischenzeit über 0 s');
106 expect(zeile.hinweis).not.toContain('erfasst.');
107 }
108 });
109
110 it('nennt weiterhin die massgebende Beziehung, wo es eine gibt', () => {
111 const projekt = createStandardIntersectionProject('Regelfall', STICHTAG);
112 const plan = buildSignalPlan(projekt);
113 expect(plan.transitions.every((t) => t.critical !== null)).toBe(true);
114
115 for (const zeile of uebergaenge(zeichne(projekt))) {
116 expect(zeile.hinweis).toContain('Maßgebend:');
117 expect(zeile.hinweis).toContain('räumt');
118 }
119 });
120 });
121
122 describe('Befund 56 - eine Phase ohne Bezeichnung als Ziel des Uebergangs', () => {
123 /** Standardknotenpunkt, bei dem die zweite Phase keine Bezeichnung traegt. */
124 function zweitePhaseOhneNamen(): Project {
125 const roh = createStandardIntersectionProject('Ohne Bezeichnung', STICHTAG);
126 const zweite = orderedPhases(roh)[1]!;
127 return {
128 ...roh,
129 phases: roh.phases.map((p) => (p.id === zweite.id ? { ...p, name: '' } : p)),
130 };
131 }
132
133 it('setzt kein leeres Anfuehrungszeichenpaar in die Uebergangszeile', () => {
134 const zeilen = uebergaenge(zeichne(zweitePhaseOhneNamen()));
135 for (const zeile of zeilen) {
136 expect(zeile.kopf).not.toContain('""');
137 }
138 });
139
140 it('nennt sie so, wie die Ueberschrift ihrer Karte sie nennt', () => {
141 const wurzel = zeichne(zweitePhaseOhneNamen());
142 const ueberschriften = [...wurzel.querySelectorAll('h3')].map((h) => h.textContent);
143 expect(ueberschriften).toContain('2. Phase (ohne Bezeichnung)');
144
145 const zeilen = uebergaenge(wurzel);
146 expect(zeilen[0]?.kopf.startsWith('Übergang zu 2. Phase (ohne Bezeichnung): ')).toBe(true);
147 });
148
149 it('laesst die Zeile mit Bezeichnung unveraendert', () => {
150 const zeilen = uebergaenge(zeichne(createStandardIntersectionProject('Regelfall', STICHTAG)));
151 expect(zeilen[0]?.kopf.startsWith('Übergang zu "Phase 2 - Ost/West": ')).toBe(true);
152 });
153 });