import { afterEach, describe, expect, it } from 'vitest'; import { ProjectStore } from '@/app/store'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { ENGER_RADIUS_LABEL, HBS_PKW_GLEICHWERTE, RILSA_DEFAULTS, UEBERFAHRZEIT_ANSATZ_LABELS, } from '@/domain/rilsa/constants'; import { QUELLEN } from '@/domain/rilsa/quellen'; import { gelbzeitstaffelSatz } from '@/services/export/bewertung'; import type { Project } from '@/domain/model/project'; import type { UeberfahrzeitAnsatz } from '@/domain/rilsa/types'; import type { ViewDefinition } from '@/ui/shell'; import { HILFE, glossarEintraege, glossarSuche } from '@/ui/help/texte'; import { conflictsView } from '@/ui/views/conflictsView'; import { projectView } from '@/ui/views/projectView'; import { settingsView } from '@/ui/views/settingsView'; import { schwerverkehrsanteilHinweis, signalGroupsView } from '@/ui/views/signalGroupsView'; /** * Was die Oberflaeche ueber die beiden neuen Rechenansaetze und ueber den * Schwerverkehr sagt (Fassung 5.5.0, Fragen E1, E2, E5 und E6). * * Der gemeinsame Nenner aller Faelle hier ist der Grundsatz des Programms: Ein * Wert darf nur dann wie ein Regelwerkswert dastehen, wenn er belegt ist. * * - E2: Die Ueberfahrzeit des Kraftfahrzeugverkehrs haengt in diesem Programm * an der Gelbzeit. Dass sie das in der RiLSA NICHT tut, ist aus den * Hamburger Zwischenzeitentafeln nachgerechnet; der RiLSA-Wortlaut zu den * Faellen 1 und 2 ist oeffentlich nicht abgleichbar. Deshalb ein WAEHLBARER * Ansatz mit der Vorgabe auf dem heutigen, konservativen Stand - und mit dem * ehrlichen Hinweis, dass die Wahl 'fest' die Zwischenzeiten VERKUERZT. * - E1: Die 5 m/s bei engem Innenradius stehen auf einer einzigen * Quellenlinie. Sie sind ein Merkmal der einzelnen Konfliktbeziehung, stehen * AUS und tragen ihre Beleglage in der Beschriftung. * - E5: Der Pruefstand der HBS-Qualitaetsstufen ist auf 'gesichert' gewechselt; * die Auslastungsgrade 0,85/0,95 bleiben Praxiswerte. * - E6: Der Schwerverkehr geht ueber den Anpassungsfaktor fSV des HBS 2015 in * die Saettigungsverkehrsstaerke ein, nicht mehr ueber den HCM-Wert 2,0. * * Gegen den Stand vor dem 15.08.2026 schlagen diese Faelle fehl: Dort gab es * weder die Auswahl noch das Kaestchen, die Vorgabenansicht kannte die drei * bedingten Kennwerte nicht, und die Hilfe erklaerte den Schwerverkehr mit * "wie etwa zwei Pkw". */ const FESTES_DATUM = new Date('2026-01-01T00:00:00Z'); /** Deutsche Schreibweise wie in der Oberflaeche - fuer die Erwartungswerte. */ function deutsch(wert: number, stellen = 1): string { return new Intl.NumberFormat('de-DE', { minimumFractionDigits: stellen, maximumFractionDigits: stellen, }).format(wert); } function zeichne( view: ViewDefinition, project: Project, ): { wurzel: HTMLElement; store: ProjectStore; aufraeumen: () => void } { const wurzel = document.createElement('div'); document.body.append(wurzel); const store = new ProjectStore(project); const abmelden = view.render(wurzel, { store, refresh: () => {}, navigate: () => {}, target: null, }); return { wurzel, store, aufraeumen: () => { abmelden(); wurzel.remove(); }, }; } /** * Sichtbarer Text einer Beschriftung - ohne den Hilfeknopf. * * `labelled()` im Zwischenzeitfenster haengt den Knopf IN die Beschriftung; ihr * `textContent` lautet dann "Raeumgeschwindigkeit?". Dieselbe Filterung nimmt * dom.ts an den Spaltenkoepfen vor, aus demselben Grund. */ function beschriftungstext(label: HTMLLabelElement): string { return [...label.childNodes] .filter((knoten) => !(knoten instanceof Element && knoten.classList.contains('hilfe-knopf'))) .map((knoten) => knoten.textContent ?? '') .join('') .trim(); } /** Bedienelement zu einer Beschriftung - ueber `for`, nicht ueber die Lage. */ function feldZu(wurzel: ParentNode, beschriftung: string): HTMLElement | null { const label = [...wurzel.querySelectorAll('label')].find( (l) => beschriftungstext(l) === beschriftung, ); const id = label?.getAttribute('for'); if (id === undefined || id === null || id === '') return null; return wurzel.querySelector(`[id="${id}"]`); } /** Der Feldhinweis, der zu einem Bedienelement vorgelesen wird. */ function hinweisZu(wurzel: ParentNode, feld: HTMLElement): string { const id = feld.getAttribute('aria-describedby'); if (id === null) return ''; return id .split(' ') .map((teil) => wurzel.querySelector(`[id="${teil}"]`)?.textContent ?? '') .join(' '); } /** Projekt mit abweichenden Vorgaben und/oder Ueberfahrzeit-Ansatz. */ function projekt( options: { ansatz?: UeberfahrzeitAnsatz; rilsa?: Record; } = {}, ): Project { const basis = createStandardIntersectionProject('Ersatzbelege', FESTES_DATUM); return { ...basis, settings: { ...basis.settings, rilsa: options.rilsa ?? {}, ueberfahrzeitAnsatz: options.ansatz ?? basis.settings.ueberfahrzeitAnsatz, }, }; } /** * Knotenpunkt, dessen erste Signalgruppe rechts abbiegt. * * Der Regelknotenpunkt der Vorlage faehrt durchweg geradeaus - dort wirkt der * enge Innenradius nirgends, und genau das ist einer der Faelle unten. */ function mitAbbieger(): Project { const basis = projekt(); const ersteGruppe = basis.signalGroups[0]!; return { ...basis, signalGroups: basis.signalGroups.map((gruppe) => gruppe.id === ersteGruppe.id ? { ...gruppe, movement: 'rechts' as const } : gruppe, ), }; } /** Das Bearbeitungsfenster der ersten Beziehung, deren Zeile `fromId` raeumt. */ function oeffneBeziehung(wurzel: HTMLElement, project: Project, fromName: string): ParentNode { const von = project.signalGroups.find((g) => g.name === fromName)!; const konflikt = project.conflicts.find((c) => c.fromId === von.id)!; const zeile = project.signalGroups.findIndex((g) => g.id === konflikt.fromId); const spalte = project.signalGroups.findIndex((g) => g.id === konflikt.toId); const zellen = [...wurzel.querySelectorAll('.matrix-zelle')]; // Die Matrix ist quadratisch ohne Diagonale: je Zeile n-1 Zellen. const n = project.signalGroups.length; const versatz = spalte > zeile ? spalte - 1 : spalte; zellen[zeile * (n - 1) + versatz]?.click(); const fenster = document.querySelector('.dialog'); expect(fenster, `Das Fenster zu ${fromName} ist nicht offen`).not.toBeNull(); return fenster as ParentNode; } /** Abschnitt der Vorgabenansicht, an seiner Ueberschrift gesucht. */ function abschnitt(wurzel: HTMLElement, ueberschrift: string): HTMLElement { const kopf = [...wurzel.querySelectorAll('h2, h3')].find( (h) => h.textContent?.trim() === ueberschrift, ); if (!kopf) throw new Error(`Abschnitt "${ueberschrift}" fehlt`); return kopf.closest('section.karte') as HTMLElement; } /** Zeile der Kennwerttabelle, an ihrer Bezeichnung gesucht. */ function kennwertzeile(karte: HTMLElement, bezeichnung: string): HTMLTableRowElement { const zeile = [...karte.querySelectorAll('tbody tr')].find( (tr) => tr.children[0]?.textContent?.trim() === bezeichnung, ); if (!zeile) throw new Error(`Kennwert "${bezeichnung}" fehlt`); return zeile as HTMLTableRowElement; } afterEach(() => { document.body.replaceChildren(); }); describe('E2 - Auswahl des Ueberfahrzeit-Ansatzes in den Projektdaten', () => { const BESCHRIFTUNG = 'Überfahrzeit des Kraftfahrzeugverkehrs'; it('fuehrt beide Ansaetze mit den Beschriftungen des Fachkerns', () => { const { wurzel, aufraeumen } = zeichne(projectView, projekt()); try { const feld = feldZu(wurzel, BESCHRIFTUNG); expect(feld, 'Die Auswahl des Ansatzes fehlt in den Projektdaten').not.toBeNull(); const auswahl = feld as HTMLSelectElement; expect([...auswahl.options].map((o) => o.value)).toEqual(['gelbzeit', 'fest']); // Aus constants.ts und nicht hier nachgebaut: Bildschirm, Pruefbericht // und Ausdruck muessen dieselbe Beschriftung fuehren - samt dem Zusatz, // der die feste Ueberfahrzeit als Regelwert der RiLSA 2015 und die // Kopplung an die Gelbzeit als Ansatz dieses Programms kennzeichnet. expect([...auswahl.options].map((o) => o.textContent)).toEqual([ UEBERFAHRZEIT_ANSATZ_LABELS.gelbzeit, UEBERFAHRZEIT_ANSATZ_LABELS.fest, ]); // Seit 5.27.0 nennt die Beschriftung die Fundstelle: Der feste Ansatz // IST der Regelwerksansatz (RiLSA 2015, Abschnitt 2.5.2). expect(UEBERFAHRZEIT_ANSATZ_LABELS.fest).toContain('RiLSA 2015'); expect(UEBERFAHRZEIT_ANSATZ_LABELS.fest).not.toContain('nicht abgleichbar'); } finally { aufraeumen(); } }); it('ist mit dem konservativen Ansatz vorbelegt', () => { const vorlage = createStandardIntersectionProject('Vorgabe', FESTES_DATUM); expect(vorlage.settings.ueberfahrzeitAnsatz).toBe('gelbzeit'); const { wurzel, aufraeumen } = zeichne(projectView, vorlage); try { expect((feldZu(wurzel, BESCHRIFTUNG) as HTMLSelectElement).value).toBe('gelbzeit'); } finally { aufraeumen(); } }); it('nennt am Feld die Verkuerzung und die Herkunft der festen Werte', () => { const { wurzel, aufraeumen } = zeichne(projectView, projekt()); try { const hinweis = hinweisZu(wurzel, feldZu(wurzel, BESCHRIFTUNG)!); expect(hinweis).toContain('VERKÜRZT'); // Seit 5.27.0 nennt der Hinweis die Fundstelle statt der Leitfaeden. expect(hinweis).toContain('Regelwerte der RiLSA 2015 (Abschnitt 2.5.2)'); expect(hinweis).not.toContain('nicht abgleichbar'); // Die Vorgabe ist als solche benannt - sonst liest sich die Auswahl wie // zwei gleichrangige Rechenwege. expect(hinweis).toContain('Vorgabe ist'); } finally { aufraeumen(); } }); it('nennt die WIRKSAMEN Ueberfahrzeiten, nicht die Regelwerte', () => { // Befund C21: Beide Werte sind ueber die Vorgabenverwaltung einstellbar. // Ein fest geschriebenes "3 s geradeaus" waere falsch, sobald jemand sie // aendert. const { wurzel, aufraeumen } = zeichne( projectView, projekt({ rilsa: { crossingTimeKfzGeradeaus: 4, crossingTimeKfzAbbiegend: 3 } }), ); try { const hinweis = hinweisZu(wurzel, feldZu(wurzel, BESCHRIFTUNG)!); expect(hinweis).toContain('4 s'); expect(hinweis).toContain('3 s'); /* * Auch die GROESSENORDNUNG der Verkuerzung folgt den Vorgaben * (Fassung 5.5.0, Befund B5): Sie stand fest geschrieben als "beim * Abbiegen um bis zu 3 s je Uebergang" - eine Zahl, die nur fuer die * Regelwerte 3 s/2 s gilt. Mit 4 s und 3 s sind es 1 s geradeaus und 2 s * abbiegend (gegen die groesste Gelbzeit von 5 s). */ expect(hinweis).toContain('beim Abbiegen um bis zu 2 s'); expect(hinweis).toContain('geradeaus um bis zu 1 s'); expect(hinweis).not.toContain('bis zu 3 s'); } finally { aufraeumen(); } }); /* * Die Umkehrung desselben Befundes: Eine feste Ueberfahrzeit OBERHALB jeder * Gelbzeit verkuerzt nichts. Der Ansatz bleibt begruendungspflichtig, aber * das Wort "VERKUERZT" waere dann schlicht falsch. */ it('behauptet keine Verkuerzung, wenn die festen Werte laenger sind als jede Gelbzeit', () => { const { wurzel, aufraeumen } = zeichne( projectView, projekt({ rilsa: { crossingTimeKfzGeradeaus: 6, crossingTimeKfzAbbiegend: 6 } }), ); try { const hinweis = hinweisZu(wurzel, feldZu(wurzel, BESCHRIFTUNG)!); expect(hinweis).not.toContain('VERKÜRZT'); expect(hinweis).toContain('nirgends kürzer'); } finally { aufraeumen(); } }); it('warnt sichtbar, solange die feste Ueberfahrzeit gewaehlt ist', () => { const { wurzel, aufraeumen } = zeichne(projectView, projekt({ ansatz: 'fest' })); try { const balken = [...wurzel.querySelectorAll('.hinweisbalken-warnung')].find((b) => b.textContent?.includes('Feste Überfahrzeit gewählt'), ); expect(balken, 'Der Warnbalken zur festen Überfahrzeit fehlt').toBeDefined(); const text = balken!.textContent ?? ''; expect(text).toContain('kürzer'); /* * NACHGEZOGEN (5.27.0): Bis dahin verlangte der Balken die Saetze "kein * belegter Regelwerkswert" und den Rat, die Wahl mit der * Strassenverkehrsbehoerde abzustimmen. Die beiden Zahlen SIND der * Regelwerkswert (RiLSA 2015, Abschnitt 2.5.2); zu begruenden ist nicht * mehr die Herkunft, sondern die Wahl gegenueber der laengeren Vorgabe * dieses Programms. Die Untergrenze bleibt im Balken - jetzt als * eingebaute Regel statt als fehlendes Sicherheitsnetz. */ expect(text).toContain('Regelwerte der RiLSA 2015 (Abschnitt 2.5.2'); expect(text).not.toContain('kein belegter Regelwerkswert'); expect(text).toContain('Planunterlagen'); expect(text).toContain('Untergrenze'); expect(text).toContain('ist eingebaut'); expect(text).not.toContain('ist nicht eingebaut'); // Die Gelbzeitstaffel, gegen die verkuerzt wird, steht mit ihren Zahlen // dabei - und zwar in derselben Fassung wie im Fundstellenverzeichnis // (Befund C21), nicht als zweite handgeschriebene Zahlenreihe. expect(text).toContain(gelbzeitstaffelSatz(RILSA_DEFAULTS)); } finally { aufraeumen(); } }); it('warnt im Regelfall nicht', () => { const { wurzel, aufraeumen } = zeichne(projectView, projekt()); try { const balken = [...wurzel.querySelectorAll('.hinweisbalken-warnung')].filter((b) => b.textContent?.includes('Überfahrzeit'), ); expect(balken, 'Der Regelfall braucht keine Warnung').toHaveLength(0); } finally { aufraeumen(); } }); it('uebernimmt die Wahl in das Projekt', () => { const { wurzel, store, aufraeumen } = zeichne(projectView, projekt()); try { const auswahl = feldZu(wurzel, BESCHRIFTUNG) as HTMLSelectElement; auswahl.value = 'fest'; auswahl.dispatchEvent(new Event('change')); expect(store.getProject().settings.ueberfahrzeitAnsatz).toBe('fest'); // Und zurueck - der Weg darf nicht einbahnig sein. const zweite = feldZu(wurzel, BESCHRIFTUNG) as HTMLSelectElement; zweite.value = 'gelbzeit'; zweite.dispatchEvent(new Event('change')); expect(store.getProject().settings.ueberfahrzeitAnsatz).toBe('gelbzeit'); } finally { aufraeumen(); } }); }); describe('E1 - Kontrollkaestchen "enger Innenradius" je Konfliktbeziehung', () => { it('steht an einer Beziehung mit abbiegendem Kfz-Raeumstrom, mit ehrlicher Beschriftung', () => { const project = mitAbbieger(); const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, 'K1'); const kaestchen = feldZu(fenster, ENGER_RADIUS_LABEL); expect(kaestchen, 'Das Kästchen fehlt am abbiegenden Kfz-Strom').not.toBeNull(); expect((kaestchen as HTMLInputElement).checked, 'Vorgabe ist AUS').toBe(false); // Die Beschriftung ist die Aussage: Wer sie liest, weiss ohne Hilfeseite, // dass hier kein Regelwerkswert steht. expect(ENGER_RADIUS_LABEL).toContain('unter 10 m'); expect(ENGER_RADIUS_LABEL).toContain('RiLSA 2015, Abschnitt 2.5.2, Fall 2'); expect(ENGER_RADIUS_LABEL).not.toContain('Praxisansatz'); } finally { aufraeumen(); } }); it('steht nicht an einer Beziehung ohne abbiegenden Kfz-Raeumstrom', () => { const project = projekt(); const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, 'K1'); expect( feldZu(fenster, ENGER_RADIUS_LABEL), 'Geradeaus gibt es keinen Innenradius - ein Kästchen ohne Wirkung wäre eine Falle', ).toBeNull(); } finally { aufraeumen(); } }); it('nennt die WIRKSAMEN Raeumgeschwindigkeiten beider Faelle', () => { const project = mitAbbieger(); const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, 'K1'); const hinweis = hinweisZu(fenster, feldZu(fenster, ENGER_RADIUS_LABEL)!); expect(hinweis).toContain( `${deutsch(RILSA_DEFAULTS.clearingSpeed.kfzTurningEngerRadius)} m/s`, ); expect(hinweis).toContain(`${deutsch(RILSA_DEFAULTS.clearingSpeed.kfzTurning)} m/s`); expect(hinweis).toContain('länger'); // Die Schwelle steht nur in der Beschriftung - das Programm kennt keinen // Abbiegeradius. Das darf die Ansicht nicht verschweigen. expect(hinweis).toContain('prüft sie nicht nach'); } finally { aufraeumen(); } }); it('nennt eine geaenderte Vorgabe statt des Regelwerts', () => { const basis = mitAbbieger(); const project: Project = { ...basis, settings: { ...basis.settings, rilsa: { clearingSpeedKfzTurningEngerRadius: 6 } }, }; const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, 'K1'); const hinweis = hinweisZu(fenster, feldZu(fenster, ENGER_RADIUS_LABEL)!); expect(hinweis).toContain('6,0 m/s'); expect(hinweis).not.toContain('5,0 m/s'); } finally { aufraeumen(); } }); it('schreibt das Merkmal in die Konfliktbeziehung', () => { const project = mitAbbieger(); const { wurzel, store, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, 'K1'); const kaestchen = feldZu(fenster, ENGER_RADIUS_LABEL) as HTMLInputElement; kaestchen.checked = true; kaestchen.dispatchEvent(new Event('change')); const konflikt = store .getProject() .conflicts.find((c) => c.fromId === project.signalGroups[0]!.id)!; expect(konflikt.engerRadius).toBe(true); // Das Merkmal ist eine Angabe zur Oertlichkeit, keine ersetzte // Messgroesse: Der Herkunftsnachweis der Wege bleibt stehen (Muster // haltVorKnoten). expect(konflikt.wegnachweis.herkunft).toBe( project.conflicts.find((c) => c.id === konflikt.id)!.wegnachweis.herkunft, ); } finally { aufraeumen(); } }); it('zeigt ein gesetztes, aber wirkungsloses Merkmal weiter an und sagt es', () => { /* * Der Fall entsteht aus einer Datei oder dadurch, dass die Fahrbeziehung * der Signalgruppe nachtraeglich auf "geradeaus" gestellt wird. Waere das * Kaestchen dann ausgeblendet, gaebe es keine Stelle mehr, an der sich die * Warnung des Pruefberichts abstellen liesse - und im Ausdruck stuende ein * Merkmal, das die Ansicht nicht zeigt. */ const basis = projekt(); const project: Project = { ...basis, conflicts: basis.conflicts.map((c, index) => (index === 0 ? { ...c, engerRadius: true } : c)), }; const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, 'K1'); const kaestchen = feldZu(fenster, ENGER_RADIUS_LABEL); expect(kaestchen, 'Ein gesetztes Merkmal muss sichtbar bleiben').not.toBeNull(); expect((kaestchen as HTMLInputElement).checked).toBe(true); expect(hinweisZu(fenster, kaestchen as HTMLElement)).toContain('Ohne Wirkung'); } finally { aufraeumen(); } }); it('nennt die angesetzte Raeumgeschwindigkeit nicht mehr "Regelwert", wo der Ansatz wirkt', () => { /* * Der stillste Fehler dieses Fensters: Die Zeile "Regelwert 5 m/s" steht * unmittelbar am Feld und behauptet damit ueber eine Zahl, die auf einer * einzigen Quellenlinie steht, sie sei der Wert des Regelwerks. */ const basis = mitAbbieger(); const project: Project = { ...basis, conflicts: basis.conflicts.map((c) => c.fromId === basis.signalGroups[0]!.id ? { ...c, engerRadius: true } : c, ), }; const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, 'K1'); const hinweis = hinweisZu(fenster, feldZu(fenster, 'Räumgeschwindigkeit')!); expect(hinweis).toContain('Angesetzt'); expect(hinweis).toContain('enger Innenradius'); expect(hinweis).not.toContain('Regelwert'); } finally { aufraeumen(); } }); it('nennt sie am Regelfall weiterhin Regelwert', () => { const project = mitAbbieger(); const { wurzel, aufraeumen } = zeichne(conflictsView, project); try { const fenster = oeffneBeziehung(wurzel, project, 'K1'); expect(hinweisZu(fenster, feldZu(fenster, 'Räumgeschwindigkeit')!)).toContain('Regelwert'); } finally { aufraeumen(); } }); }); describe('E1/E2 - die drei bedingten Kennwerte in der Vorgabenansicht', () => { const ENGER_RADIUS_KENNWERT = 'Räumgeschwindigkeit Kfz abbiegend bei engem Innenradius'; const GERADEAUS = 'Überfahrzeit Kfz geradeaus (Ansatz „feste Überfahrzeit")'; const ABBIEGEND = 'Überfahrzeit Kfz abbiegend (Ansatz „feste Überfahrzeit")'; it('fuehrt sie im Abschnitt Zwischenzeiten', () => { const { wurzel, aufraeumen } = zeichne(settingsView, projekt()); try { const karte = abschnitt(wurzel, 'Zwischenzeiten'); const zeilen = [...karte.querySelectorAll('tbody tr')].map( (tr) => tr.children[0]?.textContent?.trim() ?? '', ); expect(zeilen).toContain(ENGER_RADIUS_KENNWERT); expect(zeilen).toContain(GERADEAUS); expect(zeilen).toContain(ABBIEGEND); // Ohne diesen Satz stuenden drei Kennwerte zwischen lauter wirksamen, // die nur unter einer Bedingung in eine Zahl eingehen. expect(karte.textContent).toContain('wirken nur unter einer Bedingung'); } finally { aufraeumen(); } }); it('nennt die Herkunft der drei Werte - Praxisansatz, nicht RiLSA', () => { const { wurzel, aufraeumen } = zeichne(settingsView, projekt()); try { const karte = abschnitt(wurzel, 'Zwischenzeiten'); for (const bezeichnung of [ENGER_RADIUS_KENNWERT, GERADEAUS, ABBIEGEND]) { const herkunft = kennwertzeile(karte, bezeichnung).children[1]?.textContent ?? ''; expect(herkunft, bezeichnung).toContain(QUELLEN['praxis-enger-radius'].kurz); } } finally { aufraeumen(); } }); it('vermerkt die festen Ueberfahrzeiten beim Ansatz "Gelbzeit" als ohne Wirkung', () => { const { wurzel, aufraeumen } = zeichne(settingsView, projekt()); try { const karte = abschnitt(wurzel, 'Zwischenzeiten'); for (const bezeichnung of [GERADEAUS, ABBIEGEND]) { expect(kennwertzeile(karte, bezeichnung).textContent, bezeichnung).toContain( 'ohne Wirkung: Überfahrzeit = Gelbzeit', ); } /* * UND DEN ENGEN INNENRADIUS EBENSO (Fassung 5.5.0, Befund B6): Traegt * keine Beziehung das Merkmal, geht auch dieser Kennwert in keine Zahl * ein. Hier stand die Gegenprobe "nicht pauschal wirkungslos" - richtig * gedacht, aber sie deckte eine Ungleichbehandlung zu: Der eine bedingte * Kennwert bekam einen Vermerk, der andere unter derselben Bedingung * keinen. */ expect(kennwertzeile(karte, ENGER_RADIUS_KENNWERT).textContent).toContain( 'ohne Wirkung: kein enger Innenradius gesetzt', ); } finally { aufraeumen(); } }); it('nimmt den Vermerk zum engen Innenradius zurueck, sobald eine Beziehung ihn traegt', () => { const basis = mitAbbieger(); const project: Project = { ...basis, conflicts: basis.conflicts.map((c, i) => (i === 0 ? { ...c, engerRadius: true } : c)), }; const { wurzel, aufraeumen } = zeichne(settingsView, project); try { const karte = abschnitt(wurzel, 'Zwischenzeiten'); expect(kennwertzeile(karte, ENGER_RADIUS_KENNWERT).textContent).not.toContain('ohne Wirkung'); } finally { aufraeumen(); } }); it('nimmt den Vermerk zurueck, sobald der Ansatz gewaehlt ist', () => { const { wurzel, aufraeumen } = zeichne(settingsView, projekt({ ansatz: 'fest' })); try { const karte = abschnitt(wurzel, 'Zwischenzeiten'); for (const bezeichnung of [GERADEAUS, ABBIEGEND]) { expect(kennwertzeile(karte, bezeichnung).textContent, bezeichnung).not.toContain( 'ohne Wirkung', ); } } finally { aufraeumen(); } }); it('kennzeichnet eine wirkungslose Abweichung auch im Abweichungsbalken', () => { const { wurzel, aufraeumen } = zeichne( settingsView, projekt({ rilsa: { crossingTimeKfzGeradeaus: 4 } }), ); try { const balken = [...wurzel.querySelectorAll('.hinweisbalken-warnung')].find((b) => b.textContent?.includes('weichen vom Regelwerk ab'), ); const eintrag = [...balken!.querySelectorAll('li')].find((li) => li.textContent?.includes('Überfahrzeit Kfz geradeaus'), ); expect(eintrag, 'Die Abweichung fehlt im Balken').toBeDefined(); expect(eintrag!.textContent).toContain('(ohne Wirkung: Überfahrzeit = Gelbzeit)'); } finally { aufraeumen(); } }); it('fuehrt denselben Vermerk im Fundstellenverzeichnis wie im Ausdruck', () => { const { wurzel, aufraeumen } = zeichne(settingsView, projekt()); try { [...wurzel.querySelectorAll('button')] .find((b) => b.textContent?.trim() === 'Fundstellenverzeichnis')! .click(); const dialog = document.querySelector('[role="dialog"]')!; const eintrag = [...dialog.querySelectorAll('p')].find((p) => p.textContent?.includes('Überfahrzeit Kfz geradeaus'), ); expect(eintrag, 'Die festen Überfahrzeiten fehlen im Verzeichnis').toBeDefined(); expect(eintrag!.textContent).toContain('(ohne Wirkung: Überfahrzeit = Gelbzeit)'); } finally { aufraeumen(); } }); it('verlangt die Begruendung, sobald der Ansatz gewaehlt ist', () => { const { wurzel, aufraeumen } = zeichne( settingsView, projekt({ ansatz: 'fest', rilsa: { crossingTimeKfzGeradeaus: 4 } }), ); try { const balken = [...wurzel.querySelectorAll('.hinweisbalken-warnung')].find((b) => b.textContent?.includes('weichen vom Regelwerk ab'), ); const eintrag = [...balken!.querySelectorAll('li')].find((li) => li.textContent?.includes('Überfahrzeit Kfz geradeaus'), ); expect(eintrag!.textContent).not.toContain('ohne Wirkung'); } finally { aufraeumen(); } }); }); describe('E6 - was die Signalgruppenansicht ueber den Schwerverkehr sagt', () => { it('nennt Anpassungsfaktor und Pkw-Gleichwert aus den Konstanten', () => { const hinweis = schwerverkehrsanteilHinweis(); const gleichwert = HBS_PKW_GLEICHWERTE.schwerverkehrPauschal; expect(hinweis).toContain(`1 + ${deutsch(gleichwert - 1)} · Anteil`); expect(hinweis).toContain(`Pkw-Gleichwert ${deutsch(gleichwert)}`); expect(hinweis).toContain('HBS 2015'); // Der Pauschalwert ist als solcher benannt - samt seiner Grenze. expect(hinweis).toContain('unbekannter Aufteilung'); expect(hinweis).toContain('Lastzügen'); }); it('behauptet kein Pkw-Aequivalent von 2,0 mehr', () => { // Die 2,0 sind der Wert des HCM 2010 und standen bis zum 15.08.2026 unter // der HBS-Quelle - dieselbe Fehlerklasse wie Befund B3 der Fassung 5.4.0. expect(schwerverkehrsanteilHinweis()).not.toContain('2,0'); expect(HBS_PKW_GLEICHWERTE.schwerverkehrPauschal).not.toBe(2); }); it('stellt den Satz unter die Tabelle, ohne den Feldern ihren Namen zu nehmen', () => { const { wurzel, aufraeumen } = zeichne(signalGroupsView, projekt()); try { expect(wurzel.textContent).toContain(schwerverkehrsanteilHinweis()); /* * Die Gegenprobe zum naheliegenden Fehler: Derselbe Satz als `title` am * Eingabefeld haette ihm seinen Namen genommen - benenneBedienelemente() * ueberspringt jedes Feld mit `title`, und statt "Schwerverkehrsanteil in * Prozent, K1" hiesse das Feld nach dem Hinweistext. */ const felder = [...wurzel.querySelectorAll('input[type="number"]')]; expect(felder.some((f) => f.getAttribute('title') === schwerverkehrsanteilHinweis())).toBe( false, ); expect( felder.some((f) => (f.getAttribute('aria-label') ?? '').startsWith('Schwerverkehrsanteil in Prozent'), ), 'Das Feld hat seinen Namen aus Spalte und Zeile behalten', ).toBe(true); } finally { aufraeumen(); } }); }); describe('Hilfetexte zu den Ersatzbelegen', () => { it('erklaert den engen Innenradius in einem eigenen Eintrag', () => { const eintrag = HILFE.engerRadius; expect(eintrag.wasIstDas).toContain('5,0 m/s'); expect(eintrag.wasIstDas).toContain('7,0 m/s'); // Seit 5.27.0 die Fundstelle statt der Luecke - und der Vermerk, dass das // Programm den Radius nach wie vor nicht pruefen kann. expect(eintrag.wasIstDas).toContain('Abschnitt 2.5.2'); expect(eintrag.wasIstDas).toContain('den Radius prüfen'); expect(eintrag.wasIstDas).not.toContain('PRAXISANSATZ'); // Und der Grund, warum es trotzdem waehlbar ist: die sichere Richtung. expect(eintrag.wennFalsch).toBeDefined(); expect(eintrag.wennFalsch).toContain('verlängert es die Zwischenzeit'); expect(eintrag.woher).toContain('prüft sie nicht nach'); }); it('haengt den Eintrag dort an, wo das Merkmal begegnet', () => { expect(HILFE.raeumgeschwindigkeit.siehe ?? []).toContain('engerRadius'); expect(HILFE.raeumgeschwindigkeit.woher).toContain('enger Innenradius'); expect(glossarEintraege().map((e) => e.schluessel)).toContain('engerRadius'); expect(glossarSuche('Innenradius').map((e) => e.schluessel)).toContain('engerRadius'); }); it('erklaert die Ueberfahrzeit als Wahl zwischen zwei Ansaetzen', () => { const eintrag = HILFE.ueberfahrzeit; expect(eintrag.wasIstDas).toContain('Vorgabe'); expect(eintrag.wasIstDas).toContain('feste Überfahrzeit'); expect(eintrag.wasIstDas).toContain('VERKÜRZT'); // Der tragende Beweis gehoert in die Hilfe, nicht nur in den Quelltext. // Seit 5.27.0 die Fundstelle statt des Umkehrschlusses aus den // Zwischenzeitentafeln des Hamburger LSA-Leitfadens. expect(eintrag.woher).toContain('Abschnitt 2.5.2'); expect(eintrag.woher).not.toContain('60-km/h-Spalte'); // Die frueher offene Frage ist beantwortet. expect(eintrag.woher).not.toContain('am Regelwerk zu klären'); }); it('sagt, dass die Untergrenze der RiLSA eingebaut ist - und fuer wen', () => { /* * UMGEDREHT MIT 5.27.0. Bis dahin verlangte dieser Fall den Satz "NICHT * EINGEBAUT" samt der drei einander widersprechenden Praxisfassungen. Die * RiLSA 2015 nennt in Abschnitt 2.5.2 eine, und sie ist eingebaut. */ const wennFalsch = HILFE.ueberfahrzeit.wennFalsch; expect(wennFalsch).toContain('EINGEBAUT ist seit der Fassung 5.27.0'); // Die alte Aussage steht noch im Text, aber nur als Bericht ueber den // frueheren Stand - also HINTER dem Satz, der sie widerruft. expect(wennFalsch.indexOf('NICHT EINGEBAUT')).toBeGreaterThan( wennFalsch.indexOf('EINGEBAUT ist seit der Fassung 5.27.0'), ); expect(wennFalsch).toContain('Abschnitt 2.5.2'); expect(wennFalsch).toContain('Gelbzeit zuzüglich einer Sekunde'); // Und fuer wen sie NICHT gilt - sonst wird sie auf Rad und Fuss uebertragen. expect(wennFalsch).toContain('Für Rad, Fußgänger und ÖPNV gilt sie nicht'); }); it('erklaert den Schwerverkehr mit den Pkw-Gleichwerten des HBS', () => { const eintrag = HILFE.schwerverkehrsanteil; expect(eintrag.wasIstDas).toContain('1,75'); expect(eintrag.wasIstDas).toContain('2,50'); expect(eintrag.wasIstDas).toContain('1,90'); expect(eintrag.wasIstDas).toContain('fSV = 1 + 0,90 · Anteil'); // Der alte Satz behauptete das Falsche ueber das Regelwerk. expect(eintrag.wasIstDas).not.toContain('wie etwa zwei Pkw'); }); it('nennt die Fehlerrichtung des Pauschalwerts und die Umstellung', () => { const wennFalsch = HILFE.schwerverkehrsanteil.wennFalsch; expect(wennFalsch).toContain('Lastzügen'); expect(wennFalsch).toContain('unsicheren Seite'); expect(wennFalsch).toContain('HCM 2010'); /* * Die Groessenordnung der Umstellung - jetzt mit der richtigen Obergrenze * (Fassung 5.5.0, Befund F1): Der Zuwachs waechst streng monoton mit dem * Schwerverkehrsanteil bis 5,3 % und war mit "bis zu 2,3 %" als Obergrenze * ausgewiesen. */ expect(wennFalsch).toContain('5,3 Prozent'); expect(wennFalsch).toContain('2,4 Prozent'); expect(wennFalsch).not.toContain('2,3 Prozent'); // Und die groessere, unsichere Richtung mit ihrer Zahl (Fassung 5.5.0, Befund F2). expect(wennFalsch).toContain('10,2 Prozent'); expect(wennFalsch).toContain('31,6 Prozent'); }); it('nennt die Standardbedingungen der 2000 Kfz/h und was nicht gerechnet wird', () => { const woher = HILFE.saettigungsverkehrsstaerke.woher; expect(woher).toContain('Standardbedingungen'); expect(woher).toContain('20 m'); expect(woher).toContain('3,00 m'); expect(woher).toContain('NICHT ab'); expect(woher).toContain('zu groß'); }); it('fuehrt die Qualitaetsstufen mit dem Pruefstand, den das Verzeichnis fuehrt', () => { // Waechter: Faellt der Pruefstand der Quelle zurueck, muss dieser Satz // mitgehen - sonst behauptet die Hilfe einen Beleg, den das // Fundstellenverzeichnis daneben nicht mehr fuehrt. expect(QUELLEN['hbs-qualitaetsstufen'].pruefstand).toBe('gesichert'); const woher = HILFE.qualitaetsstufe.woher; expect(woher).toContain('zweifach herausgeberseitig belegt'); expect(woher).toContain('V 400'); expect(woher).toContain('gesichert'); // Und die Einschraenkung, die dazu festgehalten ist. expect(woher).toContain('dieselbe gedruckte Tabelle'); }); it('fuehrt die Auslastungsgrade als Praxiswerte, nicht als HBS-Werte', () => { const ueblich = HILFE.auslastungsgrad.ueblich; expect(ueblich).toContain('Praxiswerte dieses Programms'); expect(ueblich).toContain('q > C'); }); });