lsa-planer

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

/ tests ui anzeigenEhrlichkeit.test.ts

21,8 KB Rohdatei
tests/ui/anzeigenEhrlichkeit.test.ts — 523 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { ProjectStore } from '@/app/store';
3 import { createPhase, createStandardIntersectionProject } from '@/domain/model/factory';
4 import { RILSA_DEFAULTS, type RilsaDefaults } from '@/domain/rilsa/constants';
5 import { QUELLEN } from '@/domain/rilsa/quellen';
6 import { resolveRilsaSettings } from '@/domain/rilsa/settings';
7 import type { Project } from '@/domain/model/project';
8 import type { ViewDefinition } from '@/ui/shell';
9 import { buildSignalPlan } from '@/domain/plan/signalPlan';
10 import { gelbzeitstaffelSatz } from '@/services/export/bewertung';
11 import { FREIGABEFENSTER_FUSSNOTE, freigabefensterZelle } from '@/services/export/csv';
12 import { ASSISTENT_VORGABE, fuelleEngstellenkasten, projektAusAngaben } from '@/ui/assistent';
13 import { planView } from '@/ui/views/planView';
14 import { projectView } from '@/ui/views/projectView';
15 import { settingsView } from '@/ui/views/settingsView';
16 import { erhoehterZeitbedarfBeschriftung, signalGroupsView } from '@/ui/views/signalGroupsView';
17 import { simulationView } from '@/ui/views/simulationView';
18
19 /**
20 * Was die Oberflaeche ueber ihre eigenen Zahlen behauptet (Befunde C20, C21,
21 * D1 und D5).
22 *
23 * - C20: Die Vorgabenansicht schrieb "Diese Kennwerte gehen unmittelbar in
24 * jede Berechnung ein" und fuehrte darunter drei Koordinierungs-Kennwerte,
25 * die in keine Zahl eingehen.
26 * - C21: Die Beschriftung des erhoehten Zeitbedarfs nannte fest "1,0 m/s statt
27 * 1,2 m/s" - beide Werte sind ueber die Vorgabenverwaltung einstellbar.
28 * - D1: Die Engstellen-Vorschau des Assistenten rechnete mit den Regelwerten
29 * statt mit den Kennwerten der Planung.
30 * - D5: "Mittlere Wartezeit" heisst in der Simulation und im Signalzeitenplan
31 * nicht dasselbe - die Simulation wichtet mit den angekommenen Fahrzeugen,
32 * der Plan mittelt ungewichtet je Signalgruppe. Beide Zahlen stehen unter
33 * derselben Ueberschrift und weichen voneinander ab.
34 *
35 * Gegen den Altstand schlagen diese Faelle fehl: Dort gab es weder den
36 * Wirkungsvermerk noch die Mittelungsbasis der Simulation, die Beschriftung
37 * stand fest im Quelltext, und fuelleEngstellenkasten nahm keine Kennwerte
38 * entgegen.
39 */
40
41 const FESTES_DATUM = new Date('2026-01-01T00:00:00Z');
42
43 function zeichne(
44 view: ViewDefinition,
45 project: Project,
46 ): { wurzel: HTMLElement; aufraeumen: () => void } {
47 const wurzel = document.createElement('div');
48 document.body.append(wurzel);
49 const store = new ProjectStore(project);
50 const abmelden = view.render(wurzel, {
51 store,
52 refresh: () => {},
53 navigate: () => {},
54 target: null,
55 });
56 return {
57 wurzel,
58 aufraeumen: () => {
59 abmelden();
60 wurzel.remove();
61 },
62 };
63 }
64
65 function mitVorgaben(vorgaben: Record<string, number>): Project {
66 const basis = createStandardIntersectionProject('Anzeige', FESTES_DATUM);
67 return { ...basis, settings: { ...basis.settings, rilsa: vorgaben } };
68 }
69
70 /** Kennzahl der Kennzahlenleiste, an ihrer Beschriftung gesucht. */
71 function kennzahl(wurzel: HTMLElement, beschriftung: string): HTMLElement {
72 const knoten = [...wurzel.querySelectorAll<HTMLElement>('.kennzahl')].find(
73 (k) => k.querySelector('dt')?.textContent === beschriftung,
74 );
75 if (!knoten) throw new Error(`Kennzahl "${beschriftung}" fehlt`);
76 return knoten;
77 }
78
79 /** Abschnitt der Vorgabenansicht, an seiner Ueberschrift gesucht. */
80 function abschnitt(wurzel: HTMLElement, ueberschrift: string): HTMLElement {
81 const kopf = [...wurzel.querySelectorAll('h2, h3')].find(
82 (h) => h.textContent?.trim() === ueberschrift,
83 );
84 if (!kopf) throw new Error(`Abschnitt "${ueberschrift}" fehlt`);
85 const karte = kopf.closest('section.karte');
86 if (!karte) throw new Error(`Abschnitt "${ueberschrift}" liegt auf keiner Karte`);
87 return karte as HTMLElement;
88 }
89
90 afterEach(() => {
91 document.body.replaceChildren();
92 });
93
94 describe('C20 - Vorgabenansicht kennzeichnet die Koordinierung als wirkungslos', () => {
95 it('behauptet im Kopf nicht mehr, jeder Kennwert gehe in jede Berechnung ein', () => {
96 const { wurzel, aufraeumen } = zeichne(settingsView, mitVorgaben({}));
97 try {
98 const kopf = wurzel.querySelector('.ansicht-kopf')!;
99 expect(kopf.textContent).not.toContain('gehen unmittelbar in jede Berechnung ein');
100 expect(kopf.textContent).toContain('sofern der Abschnitt nichts anderes vermerkt');
101 } finally {
102 aufraeumen();
103 }
104 });
105
106 it('fuehrt die drei Kennwerte in einem eigenen Abschnitt mit sichtbarem Vermerk', () => {
107 const { wurzel, aufraeumen } = zeichne(settingsView, mitVorgaben({}));
108 try {
109 const karte = abschnitt(wurzel, 'Koordinierung (wirkt in der Ansicht „Koordinierung")');
110 const balken = karte.querySelector('.hinweisbalken');
111 expect(balken, 'Der Vermerk steht nicht als Balken über der Tabelle').not.toBeNull();
112 // Seit 5.35.0 haengt der Vermerk am Projekt: Dieses hat keinen
113 // Strassenzug erfasst, also gilt er.
114 expect(balken!.textContent).toContain('Ohne erfassten Straßenzug ohne Wirkung');
115 expect(balken!.textContent).toContain('ein Versatz gilt zwischen Anlagen');
116 expect(balken!.textContent).toContain('Erfassen Sie unter „Koordinierung"');
117
118 // Und die Felder stehen wirklich in DIESEM Abschnitt, nicht zwischen den
119 // Kennwerten der Umlaufzeit.
120 const zeilen = [...karte.querySelectorAll('tbody tr')].map(
121 (tr) => tr.children[0]?.textContent ?? '',
122 );
123 expect(zeilen).toEqual([
124 'Kleinste Progressionsgeschwindigkeit',
125 'Größte Progressionsgeschwindigkeit',
126 'Progressionsgeschwindigkeit',
127 ]);
128 } finally {
129 aufraeumen();
130 }
131 });
132
133 it('kennzeichnet eine abweichende Progressionsgeschwindigkeit im Abweichungsbalken', () => {
134 const { wurzel, aufraeumen } = zeichne(
135 settingsView,
136 mitVorgaben({ coordinationDefaultSpeed: 70 }),
137 );
138 try {
139 const balken = [...wurzel.querySelectorAll('.hinweisbalken-warnung')].find((b) =>
140 b.textContent?.includes('weichen vom Regelwerk ab'),
141 );
142 expect(balken, 'Abweichungsbalken fehlt').toBeDefined();
143 // Nicht mehr pauschal "Jede Abweichung ist zu begruenden".
144 expect(balken!.textContent).toContain('Jede wirksame Abweichung');
145 const eintrag = [...balken!.querySelectorAll('li')].find((li) =>
146 li.textContent?.startsWith('Progressionsgeschwindigkeit'),
147 );
148 expect(
149 eintrag,
150 'Die Abweichung fehlt in der Liste - sie darf nicht verschwiegen werden',
151 ).toBeDefined();
152 expect(eintrag!.textContent).toContain('70 km/h statt 50 km/h');
153 expect(eintrag!.textContent).toContain('(ohne Wirkung: keine Koordinierung erfasst)');
154 } finally {
155 aufraeumen();
156 }
157 });
158
159 it('haengt den Vermerk auch an die Kennwertliste im Fundstellenverzeichnis', () => {
160 const { wurzel, aufraeumen } = zeichne(settingsView, mitVorgaben({}));
161 try {
162 const knopf = [...wurzel.querySelectorAll('button')].find(
163 (b) => b.textContent?.trim() === 'Fundstellenverzeichnis',
164 )!;
165 knopf.click();
166 const dialog = document.querySelector<HTMLElement>('[role="dialog"]')!;
167 const quelle = QUELLEN['rilsa-koordinierung'];
168 const karte = [...dialog.querySelectorAll('h3')]
169 .find((h) => h.textContent === `${quelle.kurz} – ${quelle.sachgebiet}`)
170 ?.closest('section.karte');
171 expect(karte, 'Abschnitt zur Koordinierung fehlt im Verzeichnis').toBeTruthy();
172 expect(karte!.textContent).toContain(
173 'Progressionsgeschwindigkeit (ohne Wirkung: keine Koordinierung erfasst)',
174 );
175 } finally {
176 aufraeumen();
177 }
178 });
179
180 it('nimmt den Vermerk zurück, sobald ein Straßenzug erfasst ist', () => {
181 /*
182 * DIE GEGENPROBE, und sie ist die eigentliche Zusage: Ein
183 * Balken, der "wirkungslos" sagt, waehrend die Zahl in Reisezeit, Versatz
184 * und Bandbreite eingeht, ist genau der Fehler, den Befund C20 in der
185 * Gegenrichtung geruegt hat.
186 */
187 const roh = mitVorgaben({ coordinationDefaultSpeed: 70 });
188 const mitStrassenzug: Project = {
189 ...roh,
190 koordinierung: {
191 progressionsgeschwindigkeit: null,
192 hauptrichtung: null,
193 gegenrichtung: null,
194 eigenerAbstand: 0,
195 platz: 0,
196 nachbarn: [
197 {
198 id: 'knoten-west',
199 name: 'Nachbaranlage',
200 abstand: 300,
201 freigabeBeginn: 0,
202 freigabeDauer: 30,
203 },
204 ],
205 },
206 };
207 const { wurzel, aufraeumen } = zeichne(settingsView, mitStrassenzug);
208 try {
209 const karte = abschnitt(wurzel, 'Koordinierung (wirkt in der Ansicht „Koordinierung")');
210 expect(karte.querySelector('.hinweisbalken')).toBeNull();
211 const balken = [...wurzel.querySelectorAll('.hinweisbalken-warnung')].find((b) =>
212 b.textContent?.includes('weichen vom Regelwerk ab'),
213 );
214 const eintrag = [...(balken?.querySelectorAll('li') ?? [])].find((li) =>
215 li.textContent?.startsWith('Progressionsgeschwindigkeit'),
216 );
217 expect(eintrag!.textContent).not.toContain('ohne Wirkung');
218 } finally {
219 aufraeumen();
220 }
221 });
222
223 /*
224 * Der Vermerk hing an der ZUSAMMENGEFUEGTEN Liste und las sich, als betraefe
225 * er nur den letzten Wert. Der Ausdruck setzt ihn an jede Zeile; am
226 * Bildschirm muss dasselbe stehen.
227 */
228 it('setzt den Vermerk an jeden Kennwert, nicht nur an den letzten', () => {
229 const { wurzel, aufraeumen } = zeichne(settingsView, mitVorgaben({}));
230 try {
231 const knopf = [...wurzel.querySelectorAll('button')].find(
232 (b) => b.textContent?.trim() === 'Fundstellenverzeichnis',
233 )!;
234 knopf.click();
235 const dialog = document.querySelector<HTMLElement>('[role="dialog"]')!;
236 const quelle = QUELLEN['rilsa-koordinierung'];
237 const karte = [...dialog.querySelectorAll('h3')]
238 .find((h) => h.textContent === `${quelle.kurz} – ${quelle.sachgebiet}`)!
239 .closest('section.karte')!;
240 const liste = [...karte.querySelectorAll('p')]
241 .find((p) => p.textContent?.startsWith('Betroffene Kennwerte:'))!
242 .textContent.replace('Betroffene Kennwerte: ', '');
243 const kennwerte = liste.split(', ');
244 expect(kennwerte.length, 'Die Koordinierung hat mehr als einen Kennwert').toBeGreaterThan(1);
245 for (const kennwert of kennwerte) {
246 expect(kennwert).toContain('(ohne Wirkung: keine Koordinierung erfasst)');
247 }
248 } finally {
249 aufraeumen();
250 }
251 });
252 });
253
254 describe('C21 - Feldhinweise nennen den wirksamen Wert', () => {
255 it('bildet die Beschriftung des erhoehten Zeitbedarfs aus den Kennwerten', () => {
256 expect(erhoehterZeitbedarfBeschriftung(RILSA_DEFAULTS)).toBe(
257 'Furt mit erhöhtem Zeitbedarf (1,0 m/s statt 1,2 m/s)',
258 );
259 const geaendert = resolveRilsaSettings(
260 { clearingSpeedFussReducedMobility: 0.8, clearingSpeedFuss: 1.1 },
261 undefined,
262 'knotenpunkt',
263 ).defaults;
264 expect(erhoehterZeitbedarfBeschriftung(geaendert)).toBe(
265 'Furt mit erhöhtem Zeitbedarf (0,8 m/s statt 1,1 m/s)',
266 );
267 });
268
269 it('zeigt an der Furt die Vorgabe des Projekts, nicht den Regelwert', () => {
270 const { wurzel, aufraeumen } = zeichne(
271 signalGroupsView,
272 mitVorgaben({ clearingSpeedFussReducedMobility: 0.8 }),
273 );
274 try {
275 const kaestchen = [
276 ...wurzel.querySelectorAll<HTMLInputElement>('input[type="checkbox"]'),
277 ].filter((i) => i.title.startsWith('Furt mit erhöhtem Zeitbedarf'));
278 expect(kaestchen.length, 'Keine Fußgängergruppe in der Tabelle').toBeGreaterThan(0);
279 for (const feld of kaestchen) {
280 expect(feld.title).toBe('Furt mit erhöhtem Zeitbedarf (0,8 m/s statt 1,2 m/s)');
281 /*
282 * Die vorgelesene BESCHREIBUNG traegt denselben Wert.
283 *
284 * Hier stand `expect(feld.getAttribute('aria-label')).toBe(feld.title)`.
285 * Gemeint war mit C21 der wirksame Kennwert im Hinweis - festgehalten
286 * hat der strenge Vergleich zusaetzlich, dass der Erklaersatz der NAME
287 * des Feldes ist. Das war falsches Verhalten: In jeder Furtzeile hiess
288 * das Kaestchen gleich, und keiner der Namen sagte, um welche Furt es
289 * geht (Befund 18 zur Fassung 5.10.0). `aria-label` ERSETZT
290 * den Namen; der Erklaersatz gehoert nach `aria-describedby`, den Namen
291 * vergibt `beschrifteTabelle()` aus Spaltenkopf und Zeile. Geprueft
292 * wird deshalb weiterhin der Wert - nur an der Stelle, an der er jetzt
293 * steht. Dass der Name die Zeile nennt, sichert
294 * tests/ui/kaestchennamen.test.ts.
295 */
296 const beschreibung = wurzel.querySelector(
297 `[id="${feld.getAttribute('aria-describedby') ?? ''}"]`,
298 );
299 expect(beschreibung?.textContent).toBe(feld.title);
300 expect(feld.title).not.toContain('1,0 m/s');
301 }
302 } finally {
303 aufraeumen();
304 }
305 });
306
307 /*
308 * Die zweite in C21 ausdruecklich genannte Stelle: der Hinweis am vZul-Feld.
309 * Er trug die Gelbstaffel als feste Zahlenreihe, obwohl yellowKfzUpTo50,
310 * yellowKfzUpTo60 und yellowKfzAbove60 Vorgabenfelder sind.
311 */
312 it('setzt die Gelbzeitstaffel aus den Kennwerten zusammen', () => {
313 expect(gelbzeitstaffelSatz(RILSA_DEFAULTS)).toBe(
314 '3 s bis 50 km/h, 4 s bis 60 km/h, 5 s bis 70 km/h',
315 );
316 const geaendert = resolveRilsaSettings(
317 { yellowKfzUpTo50: 4 },
318 undefined,
319 'knotenpunkt',
320 ).defaults;
321 expect(gelbzeitstaffelSatz(geaendert)).toBe(
322 '4 s bis 50 km/h, 4 s bis 60 km/h, 5 s bis 70 km/h',
323 );
324 });
325
326 it('nennt am vZul-Feld die vorgegebene Gelbzeit, nicht die Regelstaffel', () => {
327 const { wurzel, aufraeumen } = zeichne(projectView, mitVorgaben({ yellowKfzUpTo50: 4 }));
328 try {
329 const feld = [...wurzel.querySelectorAll<HTMLElement>('div.feld')].find(
330 (f) => f.querySelector('label')?.textContent === 'Zulässige Höchstgeschwindigkeit',
331 );
332 expect(feld, 'Feld "Zulässige Höchstgeschwindigkeit" fehlt').toBeDefined();
333 const text = feld!.querySelector('.feld-hinweis')?.textContent ?? '';
334 // Der Wortlaut davor lautete "Bestimmt die Gelbzeit: ...". Er schrieb dem
335 // Knotenpunktwert eine Wirkung zu, die er nicht hat (Befund 23 zur
336 // Fassung 5.10.0) - die Gelbzeit folgt `SignalGroup.vZul`. Was
337 // dieser Prueffall begruendet, bleibt unberuehrt: Die Staffel stammt aus
338 // den aufgeloesten Vorgaben, nicht aus einer festen Zahlenreihe.
339 expect(text).toContain('4 s bis 50 km/h');
340 // Genau die Behauptung, die der Plan mit dieser Vorgabe nicht mehr deckt.
341 expect(text).not.toContain('bis 50 km/h 3 s');
342 } finally {
343 aufraeumen();
344 }
345 });
346 });
347
348 /*
349 * C22 auf dem Bildschirm: Die Signalzeitentabelle der Ansicht
350 * "Signalzeitenplan" fuehrte weiterhin "Freigabe von"/"Freigabe bis" aus
351 * greens[0] neben einer Freigabezeit, die die Summe aller Fenster nannte -
352 * waehrend Ausdruck und Tabellenausgabe bereits alle Fenster zeigten. Wer beide
353 * nebeneinanderlegt, liest die Abweichung als Fehler in der Schaltung.
354 */
355 describe('C22 - Signalzeitentabelle am Bildschirm nennt alle Freigabefenster', () => {
356 /*
357 * Vierphasiger Umlauf wie in tests/export/pdfFreigabefenster.test.ts: K1/K2 in
358 * Phase 1 und 3, K3/K4 in Phase 2 und 4. Vier Phasen und nicht drei - bei
359 * drei waeren Phase 1 und 3 ueber das Umlaufende benachbart, und der
360 * Planaufbau fasste beide zu EINEM Fenster zusammen.
361 */
362 function mitZweiFenstern(): Project {
363 const roh = createStandardIntersectionProject('Zwei Fenster', FESTES_DATUM);
364 const id = (name: string): string => roh.signalGroups.find((g) => g.name === name)!.id;
365 const nordSued = [id('K1'), id('K2'), id('F1')];
366 const ostWest = [id('K3'), id('K4'), id('F2')];
367 const phasen = [
368 createPhase('Phase 1 - Nord/Süd', nordSued),
369 createPhase('Phase 2 - Ost/West', ostWest),
370 createPhase('Phase 3 - Nord/Süd', nordSued),
371 createPhase('Phase 4 - Ost/West', ostWest),
372 ];
373 return {
374 ...roh,
375 phases: phasen,
376 program: { ...roh.program, phaseOrder: phasen.map((p) => p.id) },
377 // Vier Phasen kosten doppelt so viele Uebergaenge; mit den
378 // Verkehrsstaerken der Vorlage stuenden die Freigabezeiten auf einem
379 // Ersatzwert. Geprueft wird die Fensterspalte, nicht die Uebersaettigung.
380 demands: roh.demands.map((d) => ({ ...d, volume: Math.round(d.volume / 2) })),
381 };
382 }
383
384 it('legt der Pruefung wirklich eine Gruppe mit zwei Fenstern vor', () => {
385 const plan = buildSignalPlan(mitZweiFenstern());
386 expect(plan.groups.some((g) => g.greens.length > 1)).toBe(true);
387 });
388
389 it('fuehrt eine Fensterspalte und eine Summenspalte statt "von"/"bis"', () => {
390 const { wurzel, aufraeumen } = zeichne(planView, mitZweiFenstern());
391 try {
392 const kopfzeilen = [...wurzel.querySelectorAll<HTMLElement>('th')].map(
393 (th) => th.textContent,
394 );
395 expect(kopfzeilen).toContain('Freigabefenster (von–bis) [s]');
396 expect(kopfzeilen).toContain('Freigabezeit (Summe)');
397 expect(kopfzeilen).not.toContain('Freigabe von');
398 expect(kopfzeilen).not.toContain('Freigabe bis');
399 } finally {
400 aufraeumen();
401 }
402 });
403
404 it('zeigt dieselbe Zeichenkette wie Ausdruck und Tabellenausgabe', () => {
405 const projekt = mitZweiFenstern();
406 const plan = buildSignalPlan(projekt);
407 const mehrfach = plan.groups.find((g) => g.greens.length > 1)!;
408 const erwartet = freigabefensterZelle(mehrfach.greens, plan.cycleTime);
409 expect(erwartet).toContain(';');
410
411 const { wurzel, aufraeumen } = zeichne(planView, projekt);
412 try {
413 // Die erste Zelle der Zeile - seit Befund 46 ist sie der Zeilenkopf
414 // (`th scope="row"`) und kein `td` mehr. Gesucht wird die Zeile, geprueft
415 // die Fensterspalte; die Auszeichnung der ersten Zelle sichert
416 // tests/ui/zeilenkopf.test.ts.
417 const zeile = [...wurzel.querySelectorAll<HTMLElement>('tbody tr')].find((tr) =>
418 tr.children[0]?.textContent?.includes(mehrfach.name),
419 );
420 expect(zeile, `Zeile zu ${mehrfach.name} fehlt`).toBeDefined();
421 const zellen = [...zeile!.querySelectorAll('td')].map((td) => td.textContent);
422 expect(zellen).toContain(erwartet);
423 } finally {
424 aufraeumen();
425 }
426 });
427
428 it('stellt die Fussnote unter die Tabelle', () => {
429 const { wurzel, aufraeumen } = zeichne(planView, mitZweiFenstern());
430 try {
431 expect(wurzel.textContent).toContain(FREIGABEFENSTER_FUSSNOTE);
432 } finally {
433 aufraeumen();
434 }
435 });
436 });
437
438 describe('D1 - Engstellen-Vorschau des Assistenten rechnet mit den Kennwerten der Planung', () => {
439 const kasten = (laenge: number, defaults?: RilsaDefaults): HTMLElement => {
440 const knoten = document.createElement('div');
441 const angaben = {
442 ...ASSISTENT_VORGABE,
443 form: 'einstreifig' as const,
444 engstellenlaenge: laenge,
445 };
446 // Ohne dritten Wert: derselbe Aufruf wie vor dem Befund - die Vorschau
447 // muss dann die Kennwerte der neu anzulegenden Planung nehmen.
448 if (defaults === undefined) fuelleEngstellenkasten(knoten, angaben);
449 else fuelleEngstellenkasten(knoten, angaben, defaults);
450 return knoten;
451 };
452
453 it('nennt mit dem Regelwert 40 s Zwischenzeit bei 400 m', () => {
454 // Seit 5.27.0 nach RiLSA 2015, Abschnitt 5.2.2: 4 + 400/40 * 3,6 = 40 s
455 // (bis 5.26.0 nach der Knotenpunktformel 3 + 406/10 = 43,6 -> 44 s).
456 expect(kasten(400).textContent).toContain('Die Zwischenzeit beträgt je Fahrtrichtung 40 s');
457 });
458
459 it('aendert die Vorschau, wenn die Raeumgeschwindigkeit vorgegeben ist', () => {
460 const langsam = resolveRilsaSettings(
461 { clearingSpeedKfzStraight: 8 },
462 undefined,
463 'einstreifig',
464 ).defaults;
465 const text = kasten(400, langsam).textContent ?? '';
466 expect(text).toContain('Die Zwischenzeit beträgt je Fahrtrichtung 54 s');
467 // Umlauf 2 x 54 + 2 x 5 = 118 s, Wartezeit 2 x 54 + 5 = 113 s.
468 expect(text).toContain('118 s');
469 expect(text).toContain('113 s');
470 expect(text).not.toContain('40 s');
471 });
472 });
473
474 describe('D5 - Mittlere Wartezeit nennt ihre Mittelungsbasis', () => {
475 function mitVerkehr(): Project {
476 return projektAusAngaben(
477 { ...ASSISTENT_VORGABE, fussgaenger: true, verkehrsstaerke: 400 },
478 'Mittelung',
479 FESTES_DATUM,
480 );
481 }
482
483 it('sagt im Signalzeitenplan, dass ungewichtet je Signalgruppe gemittelt wird', () => {
484 const { wurzel, aufraeumen } = zeichne(planView, mitVerkehr());
485 try {
486 const knoten = kennzahl(wurzel, 'Mittlere Wartezeit');
487 const fussnote = knoten.querySelector('.kennzahl-fussnote')!;
488 expect(fussnote.textContent).toContain('ungewichtetes Mittel je Signalgruppe');
489 // Und dass die Simulation eine andere Basis hat - sonst sieht der
490 // Unterschied zwischen beiden Zahlen nach einem Rechenfehler aus.
491 expect(fussnote.textContent).toContain('Simulation');
492 expect(fussnote.textContent).toContain('angekommenen Fahrzeugen');
493 } finally {
494 aufraeumen();
495 }
496 });
497
498 it('sagt in der Simulation, dass mit den angekommenen Fahrzeugen gewichtet wird', () => {
499 const { wurzel, aufraeumen } = zeichne(simulationView, mitVerkehr());
500 try {
501 const knoten = kennzahl(wurzel, 'Mittlere Wartezeit');
502 const fussnote = knoten.querySelector('.kennzahl-fussnote')!;
503 expect(fussnote, 'Die Kennzahl steht ohne Mittelungsbasis da').not.toBeNull();
504 expect(fussnote.textContent).toContain('verkehrsstärkegewichtet');
505 expect(fussnote.textContent).toContain('ungewichtet je Signalgruppe');
506 } finally {
507 aufraeumen();
508 }
509 });
510
511 it('nennt auch in der Tabelle je Signalgruppe, worueber gemittelt wird', () => {
512 const { wurzel, aufraeumen } = zeichne(simulationView, mitVerkehr());
513 try {
514 const kopf = [...wurzel.querySelectorAll<HTMLElement>('th')].find(
515 (th) => th.textContent === 'Mittlere Wartezeit',
516 );
517 expect(kopf, 'Spalte fehlt').toBeDefined();
518 expect(kopf!.title).toBe('Mittel über die bisher angekommenen Fahrzeuge dieser Signalgruppe');
519 } finally {
520 aufraeumen();
521 }
522 });
523 });