lsa-planer

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

/ tests ui fundstellenverzeichnis.test.ts

7,4 KB Rohdatei
tests/ui/fundstellenverzeichnis.test.ts — 173 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { ProjectStore } from '@/app/store';
3 import { createStandardIntersectionProject } from '@/domain/model/factory';
4 import { SERVICE_LEVEL_SCALES } from '@/domain/rilsa/constants';
5 import { PRUEFSTAND_LABELS, QUELLEN } from '@/domain/rilsa/quellen';
6 import { settingFieldsByQuelle } from '@/domain/rilsa/settings';
7 import type { Anlagenart } from '@/domain/rilsa/types';
8 import { stufentafelSatz } from '@/services/export/bewertung';
9 import { settingsView } from '@/ui/views/settingsView';
10
11 /**
12 * Fundstellenverzeichnis der Vorgabenverwaltung.
13 *
14 * Das Verzeichnis entsteht aus den Vorgabenfeldern (settingFieldsByQuelle).
15 * Die Stufentafeln der Qualitaetsstufen sind kein Vorgabenfeld - die Quelle
16 * 'hbs-qualitaetsstufen' erschien deshalb NIRGENDS, obwohl gerade ihre
17 * OePNV- und Fuss/Rad-Tafeln nur ueber Sekundaerquellen belegt und am Original
18 * abzugleichen sind. Gegen den Altstand schlaegt dieser Test fehl.
19 *
20 * NACHGEFUEHRT (Fassung 5.5.0, Frage E5, Stand 15.08.2026): Der Pruefstand dieser Quelle
21 * ist von 'abzugleichen' auf 'gesichert' gewechselt - alle drei Tafeln liegen
22 * jetzt in zwei herausgeberseitigen Wiedergaben vor (FGSV-Einfuehrungs-
23 * kolloquium 2015 und BASt-Bericht V 400, 2025). Der Fall prueft deshalb nicht
24 * mehr einen festen Pruefstand, sondern dass das Verzeichnis DENSELBEN
25 * ausweist, den der Fachkern fuehrt: Genau der Auseinanderlauf zwischen
26 * Quelle und Anzeige ist es, was hier auffallen soll.
27 */
28
29 const FESTES_DATUM = new Date('2026-01-01T00:00:00Z');
30
31 function oeffneVerzeichnis(anlagenart: Anlagenart = 'knotenpunkt'): {
32 dialog: HTMLElement;
33 aufraeumen: () => void;
34 } {
35 const wurzel = document.createElement('div');
36 document.body.append(wurzel);
37 const store = new ProjectStore({
38 ...createStandardIntersectionProject('Verzeichnis', FESTES_DATUM),
39 anlagenart,
40 });
41 const abmelden = settingsView.render(wurzel, {
42 store,
43 refresh: () => {},
44 navigate: () => {},
45 target: null,
46 });
47 const knopf = [...wurzel.querySelectorAll('button')].find(
48 (b) => b.textContent?.trim() === 'Fundstellenverzeichnis',
49 );
50 if (!knopf) throw new Error('Schaltflaeche "Fundstellenverzeichnis" fehlt');
51 knopf.click();
52 const dialog = document.querySelector<HTMLElement>('[role="dialog"]');
53 if (!dialog) throw new Error('Das Verzeichnis hat sich nicht geoeffnet');
54 return {
55 dialog,
56 aufraeumen: () => {
57 abmelden();
58 wurzel.remove();
59 },
60 };
61 }
62
63 afterEach(() => {
64 document.body.replaceChildren();
65 });
66
67 describe('Fundstellenverzeichnis', () => {
68 it('fuehrt die Stufentafeln der Qualitaetsstufen mit Herkunft, Werten und ihrem Pruefstand', () => {
69 const quelle = QUELLEN['hbs-qualitaetsstufen'];
70 // Sie haengt an keinem Vorgabenfeld - sonst braeuchte es den eigenen
71 // Eintrag nicht.
72 expect(settingFieldsByQuelle().map((g) => g.schluessel)).not.toContain('hbs-qualitaetsstufen');
73
74 const { dialog, aufraeumen } = oeffneVerzeichnis();
75 try {
76 const abschnitt = [...dialog.querySelectorAll('h3')]
77 .find((h) => h.textContent === `${quelle.kurz} – ${quelle.sachgebiet}`)
78 ?.closest('section.karte');
79 expect(abschnitt, 'Abschnitt zu den Qualitaetsstufen fehlt').not.toBeNull();
80 expect(abschnitt).toBeDefined();
81 const text = abschnitt!.textContent ?? '';
82
83 expect(text).toContain(quelle.herausgeber);
84 expect(text).toContain(quelle.verwendung);
85 expect(text).toContain(quelle.hinweis!);
86 expect(text).toContain(
87 'Stufentafeln der Qualitätsstufen (Kfz/ÖPNV: mittlere Wartezeit; Fuß/Rad: maximale Wartezeit)',
88 );
89 // Die Werte, mit denen bewertet wird - aus den Konstanten.
90 expect(text).toContain(
91 `Stufentafel Kfz (mittlere Wartezeit): ${stufentafelSatz(SERVICE_LEVEL_SCALES.HBS.kfz)}; F bei Überlastung (q > C)`,
92 );
93 expect(text).toContain(
94 `Stufentafel ÖPNV (mittlere Wartezeit): ${stufentafelSatz(SERVICE_LEVEL_SCALES.HBS.oepnv)}`,
95 );
96 expect(text).toContain(
97 `Stufentafel Fußgänger und Radverkehr (maximale Wartezeit): ${stufentafelSatz(SERVICE_LEVEL_SCALES.HBS.fussRad)}`,
98 );
99 // Der Pruefstand als Marke, sichtbar wie bei jeder anderen Quelle - und
100 // zwar der des Fachkerns. Eine hier festgeschriebene Stufe wuerde eine
101 // Anzeige durchgehen lassen, die von der Quelle abweicht.
102 const marke = abschnitt!.querySelector<HTMLElement>(`.marke-${quelle.pruefstand}`);
103 expect(marke, 'Pruefstandsmarke fehlt').not.toBeNull();
104 expect(marke!.textContent).toBe(PRUEFSTAND_LABELS[quelle.pruefstand]);
105 } finally {
106 aufraeumen();
107 }
108 });
109
110 it('zeigt weiterhin jede Quelle mit Vorgabenfeld', () => {
111 const { dialog, aufraeumen } = oeffneVerzeichnis();
112 try {
113 const ueberschriften = [...dialog.querySelectorAll('h3')].map((h) => h.textContent ?? '');
114 for (const gruppe of settingFieldsByQuelle()) {
115 expect(ueberschriften).toContain(`${gruppe.quelle.kurz} – ${gruppe.quelle.sachgebiet}`);
116 }
117 } finally {
118 aufraeumen();
119 }
120 });
121
122 /*
123 * VwV-StVO und RSA 21: Beide standen in QUELLEN, aber an keinem
124 * Vorgabenfeld - und damit in keinem Verzeichnis. Die VwV-StVO gilt fuer jede
125 * Anlagenart, die RSA nur bei einstreifiger Verkehrsfuehrung.
126 */
127 it('fuehrt die VwV-StVO mit 70 km/h, Rot-Gelb und Gelbzeitstaffel - auch am Knotenpunkt', () => {
128 const quelle = QUELLEN['vwv-stvo'];
129 expect(settingFieldsByQuelle().map((g) => g.schluessel)).not.toContain('vwv-stvo');
130 const { dialog, aufraeumen } = oeffneVerzeichnis();
131 try {
132 const abschnitt = [...dialog.querySelectorAll('h3')]
133 .find((h) => h.textContent === `${quelle.kurz} – ${quelle.sachgebiet}`)
134 ?.closest('section.karte');
135 expect(abschnitt, 'Abschnitt VwV-StVO fehlt').toBeDefined();
136 const text = abschnitt!.textContent ?? '';
137 expect(text).toContain(quelle.verwendung);
138 expect(text).toContain('Zulässige Höchstgeschwindigkeit für den Betrieb einer LSA');
139 expect(text).toContain('bis 70 km/h (Rn. 10)');
140 expect(text).toContain('1 s, höchstens 2 s (Rn. 17)');
141 expect(text).toContain('3 s bis 50 km/h, 4 s bis 60 km/h, 5 s bis 70 km/h (Rn. 17)');
142 // Am Knotenpunkt keine RSA: Ihre Anhaltswerte gehen dort in keine Meldung ein.
143 const rsa = QUELLEN['rsa-arbeitsstellen'];
144 expect(
145 [...dialog.querySelectorAll('h3')].some(
146 (h) => h.textContent === `${rsa.kurz} – ${rsa.sachgebiet}`,
147 ),
148 ).toBe(false);
149 } finally {
150 aufraeumen();
151 }
152 });
153
154 it('fuehrt bei einstreifiger Fuehrung die RSA 21 mit ihren Anhaltswerten und der Praxiszeile 400/600 m', () => {
155 const quelle = QUELLEN['rsa-arbeitsstellen'];
156 const { dialog, aufraeumen } = oeffneVerzeichnis('einstreifig');
157 try {
158 const abschnitt = [...dialog.querySelectorAll('h3')]
159 .find((h) => h.textContent === `${quelle.kurz} – ${quelle.sachgebiet}`)
160 ?.closest('section.karte');
161 expect(abschnitt, 'Abschnitt RSA 21 fehlt').toBeDefined();
162 const text = abschnitt!.textContent ?? '';
163 expect(text).toContain('bis 50 m');
164 expect(text).toContain('über 500 Kfz/h');
165 expect(text).toContain('in der Regel 50 km/h');
166 expect(text).toContain('Engstellenlänge 400 m / 600 m');
167 expect(text).toContain('KEIN Wert der RSA');
168 expect(abschnitt!.querySelector('.marke-abzugleichen')).not.toBeNull();
169 } finally {
170 aufraeumen();
171 }
172 });
173 });