lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests ui fundstellenverzeichnis.test.ts
| 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 | }); |