lsa-planer

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

/ tests ui umlaufzeitvergleichAnsicht.test.ts

6,1 KB Rohdatei
tests/ui/umlaufzeitvergleichAnsicht.test.ts — 147 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { ProjectStore } from '@/app/store';
3 import { createStandardIntersectionProject } from '@/domain/model/factory';
4 import { CYCLE_METHOD_LABELS } from '@/domain/rilsa/cycle';
5 import type { Project } from '@/domain/model/project';
6 import type { ViewContext, ViewDefinition } from '@/ui/shell';
7 import { phasesView } from '@/ui/views/phasesView';
8
9 /**
10 * Die Anzeige des Verfahrensvergleichs.
11 *
12 * `compareCycleTimeMethods` lag seit 5.0.0 geprueft im Fachkern und hatte
13 * keinen Aufrufer: Wer wissen wollte, was ein anderer Ansatz aus denselben
14 * Eingaben macht, musste die Verfahren nacheinander durchschalten. Seit dieser
15 * Fassung stehen die vier Zahlen unter der Auswahlliste.
16 *
17 * WAS DIESE FAELLE BEWACHEN, ist nicht die Tabelle als solche, sondern die
18 * beiden Zusagen daneben: dass die Anzeige KEINE Empfehlung ausspricht und dass
19 * sie sagt, woher der genannte Wert kommt. Ohne sie liest ein Bearbeiter vier
20 * Zahlen und nimmt die kleinste.
21 */
22
23 const STICHTAG = new Date('2026-01-01T00:00:00Z');
24
25 let aufraeumen: (() => void) | null = null;
26 afterEach(() => {
27 aufraeumen?.();
28 aufraeumen = null;
29 document.body.replaceChildren();
30 });
31
32 function zeichne(view: ViewDefinition, project: Project): HTMLElement {
33 const wurzel = document.createElement('div');
34 document.body.append(wurzel);
35 const store = new ProjectStore(project);
36 const context: ViewContext = {
37 store,
38 refresh: () => {},
39 navigate: () => {},
40 target: null,
41 };
42 const dispose = view.render(wurzel, context);
43 aufraeumen = () => {
44 dispose();
45 wurzel.remove();
46 };
47 return wurzel;
48 }
49
50 /** Standardknotenpunkt mit Verkehrsstaerken - sonst gibt es nichts zu rechnen. */
51 function mitVerkehr(method: Project['program']['method']): Project {
52 const roh = createStandardIntersectionProject('Vergleich', STICHTAG);
53 return {
54 ...roh,
55 program: { ...roh.program, method, manualCycleTime: method === 'manuell' ? 80 : null },
56 demands: roh.signalGroups
57 .filter((g) => g.mode === 'kfz')
58 .map((g) => ({ signalGroupId: g.id, volume: 600, heavyVehicleShare: 0.05 })),
59 };
60 }
61
62 /** Die Tabelle des Vergleichs - ueber ihre Beschriftung gefunden. */
63 function vergleichstabelle(wurzel: HTMLElement): HTMLTableElement {
64 const treffer = [...wurzel.querySelectorAll('table')].filter((t) =>
65 (t.querySelector('caption')?.textContent ?? '').includes('Vergleich der Verfahren'),
66 );
67 expect(treffer.length, 'genau eine Vergleichstabelle').toBe(1);
68 return treffer[0] as HTMLTableElement;
69 }
70
71 function zeilentexte(tabelle: HTMLTableElement): string[] {
72 return [...tabelle.querySelectorAll('tbody tr')].map((tr) =>
73 [...tr.querySelectorAll('td')].map((td) => td.textContent ?? '').join(' | '),
74 );
75 }
76
77 describe('Phasenansicht - die vier Rechenansaetze nebeneinander', () => {
78 it('zeigt alle vier Verfahren, nicht nur das gewaehlte', () => {
79 const zeilen = zeilentexte(vergleichstabelle(zeichne(phasesView, mitVerkehr('webster'))));
80 expect(zeilen.length, 'vier Zeilen').toBe(4);
81 for (const method of ['webster', 'akcelik', 'hbs', 'hcm'] as const) {
82 expect(
83 zeilen.some((z) => z.startsWith(CYCLE_METHOD_LABELS[method])),
84 CYCLE_METHOD_LABELS[method],
85 ).toBe(true);
86 }
87 });
88
89 it('kennzeichnet das gewaehlte Verfahren im Text und nicht nur farblich', () => {
90 // Eine Sprachausgabe liest keine Hintergrundfarbe vor. Stuende die Auswahl
91 // allein in der Klasse, waere die Tafel fuer sie vier gleichrangige Zahlen.
92 const zeilen = zeilentexte(vergleichstabelle(zeichne(phasesView, mitVerkehr('hbs'))));
93 const markiert = zeilen.filter((z) => z.includes('(gewählt)'));
94 expect(markiert.length, 'genau eine Zeile ist gekennzeichnet').toBe(1);
95 expect(markiert[0]).toContain('HBS');
96 });
97
98 it('steht auch bei fester Vorgabe da', () => {
99 /*
100 * Gerade dort ist der Vergleich die Auskunft, die fehlte: Wer die
101 * Umlaufzeit von Hand setzt, hatte bis hierher keinen Anhaltspunkt, ob sie
102 * zu den Verkehrsstaerken passt.
103 */
104 const zeilen = zeilentexte(vergleichstabelle(zeichne(phasesView, mitVerkehr('manuell'))));
105 expect(zeilen.length).toBe(4);
106 // Keines der vier Verfahren ist gewaehlt - die feste Vorgabe ist keines.
107 expect(zeilen.some((z) => z.includes('(gewählt)'))).toBe(false);
108 });
109
110 it('spricht keine Empfehlung aus und sagt, woher der Wert kommt', () => {
111 const wurzel = zeichne(phasesView, mitVerkehr('webster'));
112 const text = wurzel.textContent ?? '';
113 expect(text, 'die Zusage, dass es keine Empfehlung ist').toContain('keine Empfehlung');
114 expect(text, 'die Wahl bleibt beim Planer').toContain('entscheidet der Planer');
115 expect(text, 'die Regel ist kein Regelwerkswert').toContain('keinem Regelwerk');
116 expect(text, 'die fremden Beanstandungen gehen nicht in den Bericht').toContain(
117 'nicht im Prüfbericht',
118 );
119 });
120
121 it('nennt den Rechenwert eines nicht bemessbaren Verfahrens nicht als Zahl', () => {
122 /*
123 * Bei Uebersaettigung ist der Rechenwert unendlich, und `cycleTime` traegt
124 * nur die Obergrenze als Ersatzwert. Eine Zahl in der Spalte "Rechenwert"
125 * stuende dort zum Schein.
126 *
127 * DIESER FALL BEWACHT DIE ZUSAGE, NICHT EINE EIGENE ABFRAGE: Den Strich
128 * schreibt `fmt.seconds` fuer jeden nicht endlichen Wert. In der Ansicht
129 * stand davor einmal eine eigene Pruefung auf `Number.isFinite`, die
130 * dasselbe ein zweites Mal tat - sie ist entfernt, weil keine Mutation an
131 * ihr auffiel. Wer die Zelle kuenftig ohne den Formatierer baut, faellt
132 * hier auf.
133 */
134 const roh = createStandardIntersectionProject('Uebersaettigt', STICHTAG);
135 const projekt: Project = {
136 ...roh,
137 program: { ...roh.program, method: 'webster' },
138 demands: roh.signalGroups
139 .filter((g) => g.mode === 'kfz')
140 .map((g) => ({ signalGroupId: g.id, volume: 1200, heavyVehicleShare: 0 })),
141 };
142 const zeilen = zeilentexte(vergleichstabelle(zeichne(phasesView, projekt)));
143 const hbs = zeilen.find((z) => z.startsWith('HBS')) ?? '';
144 expect(hbs, 'HBS ist nicht bemessbar').toContain('nicht bemessbar');
145 expect(hbs, 'kein Rechenwert, sondern ein Strich').toContain('–');
146 });
147 });