import { afterEach, describe, expect, it } from 'vitest'; import { ProjectStore } from '@/app/store'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { SERVICE_LEVEL_SCALES } from '@/domain/rilsa/constants'; import { PRUEFSTAND_LABELS, QUELLEN } from '@/domain/rilsa/quellen'; import { settingFieldsByQuelle } from '@/domain/rilsa/settings'; import type { Anlagenart } from '@/domain/rilsa/types'; import { stufentafelSatz } from '@/services/export/bewertung'; import { settingsView } from '@/ui/views/settingsView'; /** * Fundstellenverzeichnis der Vorgabenverwaltung. * * Das Verzeichnis entsteht aus den Vorgabenfeldern (settingFieldsByQuelle). * Die Stufentafeln der Qualitaetsstufen sind kein Vorgabenfeld - die Quelle * 'hbs-qualitaetsstufen' erschien deshalb NIRGENDS, obwohl gerade ihre * OePNV- und Fuss/Rad-Tafeln nur ueber Sekundaerquellen belegt und am Original * abzugleichen sind. Gegen den Altstand schlaegt dieser Test fehl. * * NACHGEFUEHRT (Fassung 5.5.0, Frage E5, Stand 15.08.2026): Der Pruefstand dieser Quelle * ist von 'abzugleichen' auf 'gesichert' gewechselt - alle drei Tafeln liegen * jetzt in zwei herausgeberseitigen Wiedergaben vor (FGSV-Einfuehrungs- * kolloquium 2015 und BASt-Bericht V 400, 2025). Der Fall prueft deshalb nicht * mehr einen festen Pruefstand, sondern dass das Verzeichnis DENSELBEN * ausweist, den der Fachkern fuehrt: Genau der Auseinanderlauf zwischen * Quelle und Anzeige ist es, was hier auffallen soll. */ const FESTES_DATUM = new Date('2026-01-01T00:00:00Z'); function oeffneVerzeichnis(anlagenart: Anlagenart = 'knotenpunkt'): { dialog: HTMLElement; aufraeumen: () => void; } { const wurzel = document.createElement('div'); document.body.append(wurzel); const store = new ProjectStore({ ...createStandardIntersectionProject('Verzeichnis', FESTES_DATUM), anlagenart, }); const abmelden = settingsView.render(wurzel, { store, refresh: () => {}, navigate: () => {}, target: null, }); const knopf = [...wurzel.querySelectorAll('button')].find( (b) => b.textContent?.trim() === 'Fundstellenverzeichnis', ); if (!knopf) throw new Error('Schaltflaeche "Fundstellenverzeichnis" fehlt'); knopf.click(); const dialog = document.querySelector('[role="dialog"]'); if (!dialog) throw new Error('Das Verzeichnis hat sich nicht geoeffnet'); return { dialog, aufraeumen: () => { abmelden(); wurzel.remove(); }, }; } afterEach(() => { document.body.replaceChildren(); }); describe('Fundstellenverzeichnis', () => { it('fuehrt die Stufentafeln der Qualitaetsstufen mit Herkunft, Werten und ihrem Pruefstand', () => { const quelle = QUELLEN['hbs-qualitaetsstufen']; // Sie haengt an keinem Vorgabenfeld - sonst braeuchte es den eigenen // Eintrag nicht. expect(settingFieldsByQuelle().map((g) => g.schluessel)).not.toContain('hbs-qualitaetsstufen'); const { dialog, aufraeumen } = oeffneVerzeichnis(); try { const abschnitt = [...dialog.querySelectorAll('h3')] .find((h) => h.textContent === `${quelle.kurz} – ${quelle.sachgebiet}`) ?.closest('section.karte'); expect(abschnitt, 'Abschnitt zu den Qualitaetsstufen fehlt').not.toBeNull(); expect(abschnitt).toBeDefined(); const text = abschnitt!.textContent ?? ''; expect(text).toContain(quelle.herausgeber); expect(text).toContain(quelle.verwendung); expect(text).toContain(quelle.hinweis!); expect(text).toContain( 'Stufentafeln der Qualitätsstufen (Kfz/ÖPNV: mittlere Wartezeit; Fuß/Rad: maximale Wartezeit)', ); // Die Werte, mit denen bewertet wird - aus den Konstanten. expect(text).toContain( `Stufentafel Kfz (mittlere Wartezeit): ${stufentafelSatz(SERVICE_LEVEL_SCALES.HBS.kfz)}; F bei Überlastung (q > C)`, ); expect(text).toContain( `Stufentafel ÖPNV (mittlere Wartezeit): ${stufentafelSatz(SERVICE_LEVEL_SCALES.HBS.oepnv)}`, ); expect(text).toContain( `Stufentafel Fußgänger und Radverkehr (maximale Wartezeit): ${stufentafelSatz(SERVICE_LEVEL_SCALES.HBS.fussRad)}`, ); // Der Pruefstand als Marke, sichtbar wie bei jeder anderen Quelle - und // zwar der des Fachkerns. Eine hier festgeschriebene Stufe wuerde eine // Anzeige durchgehen lassen, die von der Quelle abweicht. const marke = abschnitt!.querySelector(`.marke-${quelle.pruefstand}`); expect(marke, 'Pruefstandsmarke fehlt').not.toBeNull(); expect(marke!.textContent).toBe(PRUEFSTAND_LABELS[quelle.pruefstand]); } finally { aufraeumen(); } }); it('zeigt weiterhin jede Quelle mit Vorgabenfeld', () => { const { dialog, aufraeumen } = oeffneVerzeichnis(); try { const ueberschriften = [...dialog.querySelectorAll('h3')].map((h) => h.textContent ?? ''); for (const gruppe of settingFieldsByQuelle()) { expect(ueberschriften).toContain(`${gruppe.quelle.kurz} – ${gruppe.quelle.sachgebiet}`); } } finally { aufraeumen(); } }); /* * VwV-StVO und RSA 21: Beide standen in QUELLEN, aber an keinem * Vorgabenfeld - und damit in keinem Verzeichnis. Die VwV-StVO gilt fuer jede * Anlagenart, die RSA nur bei einstreifiger Verkehrsfuehrung. */ it('fuehrt die VwV-StVO mit 70 km/h, Rot-Gelb und Gelbzeitstaffel - auch am Knotenpunkt', () => { const quelle = QUELLEN['vwv-stvo']; expect(settingFieldsByQuelle().map((g) => g.schluessel)).not.toContain('vwv-stvo'); const { dialog, aufraeumen } = oeffneVerzeichnis(); try { const abschnitt = [...dialog.querySelectorAll('h3')] .find((h) => h.textContent === `${quelle.kurz} – ${quelle.sachgebiet}`) ?.closest('section.karte'); expect(abschnitt, 'Abschnitt VwV-StVO fehlt').toBeDefined(); const text = abschnitt!.textContent ?? ''; expect(text).toContain(quelle.verwendung); expect(text).toContain('Zulässige Höchstgeschwindigkeit für den Betrieb einer LSA'); expect(text).toContain('bis 70 km/h (Rn. 10)'); expect(text).toContain('1 s, höchstens 2 s (Rn. 17)'); expect(text).toContain('3 s bis 50 km/h, 4 s bis 60 km/h, 5 s bis 70 km/h (Rn. 17)'); // Am Knotenpunkt keine RSA: Ihre Anhaltswerte gehen dort in keine Meldung ein. const rsa = QUELLEN['rsa-arbeitsstellen']; expect( [...dialog.querySelectorAll('h3')].some( (h) => h.textContent === `${rsa.kurz} – ${rsa.sachgebiet}`, ), ).toBe(false); } finally { aufraeumen(); } }); it('fuehrt bei einstreifiger Fuehrung die RSA 21 mit ihren Anhaltswerten und der Praxiszeile 400/600 m', () => { const quelle = QUELLEN['rsa-arbeitsstellen']; const { dialog, aufraeumen } = oeffneVerzeichnis('einstreifig'); try { const abschnitt = [...dialog.querySelectorAll('h3')] .find((h) => h.textContent === `${quelle.kurz} – ${quelle.sachgebiet}`) ?.closest('section.karte'); expect(abschnitt, 'Abschnitt RSA 21 fehlt').toBeDefined(); const text = abschnitt!.textContent ?? ''; expect(text).toContain('bis 50 m'); expect(text).toContain('über 500 Kfz/h'); expect(text).toContain('in der Regel 50 km/h'); expect(text).toContain('Engstellenlänge 400 m / 600 m'); expect(text).toContain('KEIN Wert der RSA'); expect(abschnitt!.querySelector('.marke-abzugleichen')).not.toBeNull(); } finally { aufraeumen(); } }); });