import { inflateSync } from 'node:zlib'; import { describe, expect, it } from 'vitest'; import { buildProjectPdf, DEFAULT_PDF_OPTIONS, MITTELINSEL_VERMERK, furtregelAbsatz, } from '@/services/export/pdf'; import { FREIGABEFENSTER_FUSSNOTE, freigabefensterZelle, reportCsv, signalGroupsCsv, } from '@/services/export/csv'; import { quellenOhneFeld } from '@/services/export/bewertung'; import { ANFORDERUNGSBETRIEB_VORBEHALT } from '@/domain/validation/rules'; import { ANLAGENART_GRENZEN, RILSA_DEFAULTS, type RilsaDefaults } from '@/domain/rilsa/constants'; import { engstellenFolgen, umlaufBeiFreigabe } from '@/domain/rilsa/engstelle'; import { resolveRilsaSettings } from '@/domain/rilsa/settings'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { validateProject } from '@/domain/validation'; import { createPhase, createStandardIntersectionProject } from '@/domain/model/factory'; import { toWinAnsi } from '@/render/pdfSurface'; import type { Project } from '@/domain/model/project'; /** * Freigabefenster in Ausdruck und Tabellenausgabe (Befund C22), der * Mittelinsel-Vermerk (Befund D2) und der Vorbehalt zum Anforderungsbetrieb * (Befund D3) - alle drei in den Ausgaben geprueft, weil dort steht, was der * Anwender aus der Hand gibt. * * C22: Die Signalzeitentabelle des Ausdrucks fuellte "Freigabe von" und * "Freigabe bis" aus greens[0] - dem ersten Fenster -, waehrend "Freigabezeit" * die Summe aller Fenster nannte. Eine Signalgruppe in zwei getrennten Phasen * verlor damit ihr zweites Fenster, und die beiden Spalten widersprachen * einander. Gegen den Altstand schlagen die Faelle fehl: Dort gab es die Spalte * "Freigabefenster" weder im Ausdruck noch in der Tabellenausgabe, die * Summenspalte hiess nicht Summe, und die Fussnote fehlte. * * D2: Der Furtregel-Absatz sagte nichts darueber, dass eine Furt mit * Mittelinsel als durchgehende Furt gerechnet wird. * * D3: Das Programm empfahl, die Furt "auf Anforderung" freizugeben - einen * Betrieb, den es selbst nicht abbildet und nicht prueft. */ const FIXED_DATE = new Date('2026-01-01T12:00:00Z'); /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */ function pdfText(bytes: Uint8Array): string { const roh = new TextDecoder('latin1').decode(bytes); const teile: string[] = []; const suche = /stream\r?\n/g; let treffer: RegExpExecArray | null; while ((treffer = suche.exec(roh)) !== null) { const start = treffer.index + treffer[0].length; const ende = roh.indexOf('endstream', start); if (ende < 0) continue; let inhalt: string; try { inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1'); } catch { continue; } for (const stueck of inhalt.matchAll(/\((?:\\.|[^\\()])*\)\s*Tj/g)) { teile.push(stueck[0].slice(1, stueck[0].lastIndexOf(')'))); } } return teile .join(' ') .replace(/\\([()])/g, '$1') .replace(/\s+/g, ' '); } function ausdruck(project: Project): string { const plan = buildSignalPlan(project); const report = validateProject(project, plan, FIXED_DATE); return pdfText(buildProjectPdf(project, plan, report, DEFAULT_PDF_OPTIONS, FIXED_DATE)); } /** * Semikolongetrennte Tabellenausgabe in Zeilen und Zellen zerlegen. * * Die Trennung achtet auf Anfuehrungszeichen: Die Fensterzelle enthaelt selbst * ein Semikolon ("12–40; 60–72") und wird von der Ausgabe deshalb umschlossen. * Ein naives split(';') zerrisse genau die Zelle, um die es hier geht. */ function tabelle(csv: string): string[][] { return csv .replace(/^/, '') .split('\r\n') .map((zeile) => { const zellen: string[] = []; let aktuell = ''; let inAnfuehrung = false; for (let i = 0; i < zeile.length; i += 1) { const zeichen = zeile[i]!; if (zeichen === '"') { if (inAnfuehrung && zeile[i + 1] === '"') { aktuell += '"'; i += 1; } else { inAnfuehrung = !inAnfuehrung; } } else if (zeichen === ';' && !inAnfuehrung) { zellen.push(aktuell); aktuell = ''; } else { aktuell += zeichen; } } zellen.push(aktuell); return zellen; }); } /** * Wie der Text im PDF wirklich steht. * * Die PDF-Standardschriften kennen kein Halbgeviertstrich; toWinAnsi ersetzt * ihn durch den Bindestrich. Ein Vergleich mit der Zeichenkette der * Tabellenausgabe muss dieselbe Ersetzung durchlaufen, sonst prueft er nichts. */ function imAusdruck(text: string): string { return toWinAnsi(text); } function spalte(zeilen: string[][], name: string): string[] { const index = zeilen[0]!.indexOf(name); expect(index, `Spalte "${name}"`).toBeGreaterThanOrEqual(0); return zeilen.slice(1).map((zeile) => zeile[index] ?? ''); } /** * Vierphasiger Umlauf, in dem K1/K2 in Phase 1 und 3 und K3/K4 in Phase 2 und 4 * freigegeben sind. * * Vier Phasen und nicht drei: Bei drei Phasen waeren Phase 1 und Phase 3 ueber * das Umlaufende BENACHBART, und der Planaufbau fasste beide zu einem einzigen * Fenster zusammen (cyclicRuns). Erst der Wechsel 1-2-3-4 erzeugt zwei wirklich * getrennte Fenster - genau der Fall, den die alte Tabelle verschwieg. */ function zweiFenster(): Project { const roh = createStandardIntersectionProject('Zwei Fenster', FIXED_DATE); const id = (name: string) => roh.signalGroups.find((g) => g.name === name)!.id; const nordSued = [id('K1'), id('K2'), id('F1')]; const ostWest = [id('K3'), id('K4'), id('F2')]; const phasen = [ createPhase('Phase 1 - Nord/Süd', nordSued), createPhase('Phase 2 - Ost/West', ostWest), createPhase('Phase 3 - Nord/Süd', nordSued), createPhase('Phase 4 - Ost/West', ostWest), ]; return { ...roh, phases: phasen, program: { ...roh.program, phaseOrder: phasen.map((p) => p.id) }, // Vier Phasen kosten doppelt so viele Uebergaenge; mit den Verkehrsstaerken // der Vorlage waere der Umlauf uebersaettigt und die Freigabezeiten stuenden // auf einem Ersatzwert. Der Fall soll die Fensterspalte pruefen, nicht die // Uebersaettigung. demands: roh.demands.map((d) => ({ ...d, volume: Math.round(d.volume / 2) })), }; } describe('Freigabefenster: alle Fenster statt nur des ersten (Befund C22)', () => { it('legt der Pruefung wirklich zwei getrennte Fenster vor', () => { // Vorbedingung des ganzen Falls: Ohne zwei Fenster prueft nichts darunter // etwas. K1 ist in Phase 1 und 3, F1 ebenso. const plan = buildSignalPlan(zweiFenster()); const k1 = plan.groups.find((g) => g.name === 'K1')!; expect(k1.greens).toHaveLength(2); // Die Summe ist die Summe beider Fenster - nicht die Dauer des ersten. const summe = k1.greens[0]!.duration + k1.greens[1]!.duration; expect(k1.totalGreen).toBeCloseTo(summe, 3); expect(k1.greens[0]!.duration).toBeLessThan(k1.totalGreen); }); it('nennt in der Zelle beide Fenster, getrennt durch Strichpunkt', () => { const plan = buildSignalPlan(zweiFenster()); const k1 = plan.groups.find((g) => g.name === 'K1')!; const zelle = freigabefensterZelle(k1.greens, plan.cycleTime); expect(zelle.split('; ')).toHaveLength(2); for (const fenster of k1.greens) { const ende = (fenster.start + fenster.duration) % plan.cycleTime; expect(zelle).toContain( `${fenster.start.toFixed(1).replace(/\.0$/, '').replace('.', ',')}–` + `${ende.toFixed(1).replace(/\.0$/, '').replace('.', ',')}`, ); } }); it('gibt bei einem Fenster genau ein Intervall und ohne Fenster einen Strich', () => { const plan = buildSignalPlan(createStandardIntersectionProject('Zwei Phasen', FIXED_DATE)); const k1 = plan.groups.find((g) => g.name === 'K1')!; expect(k1.greens).toHaveLength(1); expect(freigabefensterZelle(k1.greens, plan.cycleTime)).not.toContain(';'); expect(freigabefensterZelle([], plan.cycleTime)).toBe('–'); }); it('nennt die Dauerfreigabe als ganzen Umlauf statt als "0–0"', () => { // Ein Fenster ueber den ganzen Umlauf faellt beim Rueckfall in den Umlauf // auf das Ende 0 zurueck; der Altbestand druckte dafuer "0 s" und "0 s". expect(freigabefensterZelle([{ start: 0, duration: 80 }], 80)).toBe('0–80 (ganzer Umlauf)'); }); it('druckt im Ausdruck die Spalte, die Summenbezeichnung und die Fussnote', () => { const text = ausdruck(zweiFenster()); expect(text).toContain(imAusdruck('Freigabefenster (von–bis) [s]')); expect(text).toContain('Freigabezeit (Summe)'); // Der Altstand fuehrte zwei Spalten aus dem ersten Fenster - im flachen // Text des Ausdrucks standen ihre Koepfe nebeneinander. Auf "Freigabe von" // allein laesst sich nicht pruefen: Der Pruefbericht schreibt "die Freigabe // von K1" in seine Meldungen. expect(text).not.toContain('Freigabe von Freigabe bis'); expect(text).toContain( imAusdruck('Die Freigabezeit ist die Summe aller Fenster, nicht die Dauer des ersten.'), ); }); it('druckt beide Fenster der Signalgruppe in den Ausdruck', () => { const projekt = zweiFenster(); const plan = buildSignalPlan(projekt); const k1 = plan.groups.find((g) => g.name === 'K1')!; const zelle = freigabefensterZelle(k1.greens, plan.cycleTime); expect(zelle).toContain('; '); expect(ausdruck(projekt)).toContain(imAusdruck(zelle)); }); it('fuehrt in der Tabellenausgabe Fenster und Summe getrennt', () => { const projekt = zweiFenster(); const plan = buildSignalPlan(projekt); const zeilen = tabelle(signalGroupsCsv(projekt, plan)); const namen = spalte(zeilen, 'Signalgruppe'); const fenster = spalte(zeilen, 'Freigabefenster [s]'); const summen = spalte(zeilen, 'Freigabezeit (Summe aller Fenster) [s]'); const i = namen.indexOf('K1'); expect(i).toBeGreaterThanOrEqual(0); const k1 = plan.groups.find((g) => g.name === 'K1')!; expect(fenster[i]).toBe(freigabefensterZelle(k1.greens, plan.cycleTime)); expect(fenster[i]!.split('; ')).toHaveLength(2); expect(Number(summen[i]!.replace(',', '.'))).toBeCloseTo(k1.totalGreen, 1); }); it('zeigt in Ausdruck und Tabellenausgabe dieselbe Zeichenkette', () => { // Wer beide Unterlagen nebeneinanderlegt, vergleicht Zeile fuer Zeile; zwei // Schreibweisen derselben Fenster waeren ein neuer Widerspruch. const projekt = zweiFenster(); const plan = buildSignalPlan(projekt); const zeilen = tabelle(signalGroupsCsv(projekt, plan)); const namen = spalte(zeilen, 'Signalgruppe'); const fenster = spalte(zeilen, 'Freigabefenster [s]'); const text = ausdruck(projekt); for (const [i, name] of namen.entries()) { const gruppe = plan.groups.find((g) => g.name === name); if (gruppe === undefined || gruppe.greens.length === 0) continue; expect(fenster[i], name).toBe(freigabefensterZelle(gruppe.greens, plan.cycleTime)); expect(text, name).toContain(imAusdruck(fenster[i]!)); } }); it('haelt die Fussnote und den Spaltennamen der Tabellenausgabe beisammen', () => { expect(FREIGABEFENSTER_FUSSNOTE).toContain('Summe aller Fenster'); expect(FREIGABEFENSTER_FUSSNOTE).toContain('getrennten Phasen'); }); /* * Zwei Randfaelle, die der erste Entwurf offen liess. */ it('ordnet die Fenster nach Umlaufposition, nicht nach Speicherreihenfolge', () => { // greens entsteht aus der Phasenfolge und ist nicht sortiert. Eine // Signalzeitentabelle wird gegen den Umlauf gelesen; eine Zeile, die // rueckwaerts springt, liest sich wie ein Fehler in der Schaltung. expect( freigabefensterZelle( [ { start: 116, duration: 28 }, { start: 56, duration: 28 }, ], 120, ), ).toBe('56–84; 116–24'); }); it('nennt auch die Signalgruppentabelle ihre Freigabespalte eine Summe', () => { // Die Spalte druckt totalGreen, also ebenfalls die Summe aller Fenster, // sagte das aber nicht - und die C22-Fussnote steht auf einer anderen // Seite. expect(ausdruck(zweiFenster())).toContain(imAusdruck('Freigabe im Plan (Summe)')); }); it('nennt ein Fenster am Umlaufende "60–80" und nicht "60–0"', () => { // Dieselbe Modulo-Null wie bei der Dauerfreigabe, nur an einem Fenster, // das nicht den ganzen Umlauf belegt: "60–0" behauptete ein Fenster, das // rueckwaerts laeuft, statt eines, das bis zum Umlaufende reicht. expect(freigabefensterZelle([{ start: 60, duration: 20 }], 80)).toBe('60–80'); // Der echte Ueberlauf bleibt, wie er war - das Ende liegt dann vor dem // Beginn, und die Fussnote sagt es. expect(freigabefensterZelle([{ start: 116, duration: 28 }], 120)).toBe('116–24'); }); }); describe('Furt mit Mittelinsel wird als durchgehende Furt gerechnet (Befund D2)', () => { it('vermerkt es im Furtregel-Absatz', () => { const absatz = furtregelAbsatz( buildSignalPlan(createStandardIntersectionProject('Furt', FIXED_DATE)), )!; expect(absatz).not.toBeNull(); expect(absatz).toContain(MITTELINSEL_VERMERK); }); it('sagt, WOMIT gerechnet wurde, und behauptet nichts ueber die Eingabe', () => { // BERICHTIGT 5.38.0. Hier stand die Erwartung, der Vermerk nenne die // Rechnung "konservativ". Das war eine Zusicherung ueber eine EINGABE: // Gerechnet wird mit dem je Konfliktbeziehung erfassten Raeumweg, und ob // der bis zur gegenueberliegenden Bordkante reicht oder an der Insel endet, // kann dieser Plan nicht feststellen. Wer die Furt nur bis zur Insel // vermass, bekam die kuerzeren Zeiten UND einen gedruckten Satz, der sie // konservativ nannte - in einer Unterlage, die zur Anordnung geht. expect(MITTELINSEL_VERMERK).toContain('erfassten Räumweg'); expect(MITTELINSEL_VERMERK).toContain('kann dieser Plan nicht feststellen'); expect(MITTELINSEL_VERMERK).toContain('progressive (zweistufige) Signalisierung'); expect(MITTELINSEL_VERMERK).toContain('bildet das Programm nicht ab'); // Die zurueckgenommene Zusicherung darf nicht wiederkommen. expect(MITTELINSEL_VERMERK).not.toContain('konservativ'); }); it('nennt alle drei Groessen, die der halbe Weg verkuerzt', () => { // NACHGEBESSERT 5.38.1: Die erste Berichtigung liess die RAEUMZEIT fallen - // sie verband den erfassten Weg nur noch mit der Mindestfreigabezeit. // Derselbe halbierte Weg traegt aber auch jede Zwischenzeit der Gruppe. for (const groesse of ['Räumzeit', 'Zwischenzeit', 'Furtbedarf']) { expect(MITTELINSEL_VERMERK, groesse).toContain(groesse); } }); it('sagt, in welche Richtung die Vereinfachung geht, und unter welcher Bedingung', () => { // Ebenfalls 5.38.1: Wer nur das PDF vor sich hat, konnte der ersten // Berichtigung nicht mehr entnehmen, ob die Vereinfachung zu seinen Gunsten // oder zu seinen Lasten geht. expect(MITTELINSEL_VERMERK).toContain('auf der sicheren Seite'); expect(MITTELINSEL_VERMERK).toContain('fallen alle drei zu kurz aus'); }); it('verspricht keine Furtlaenge, die es nicht immer gibt', () => { // Und noch einmal 5.38.1: "die daraus gebildete Furtlaenge steht oben bei // jeder Gruppe" war die naechste unbelegbare Zusage - fuer eine Gruppe ohne // erfassten Raeumweg druckt derselbe Absatz "Furtlaenge unbekannt". expect(MITTELINSEL_VERMERK).not.toContain('steht oben bei jeder Gruppe'); expect(MITTELINSEL_VERMERK).toContain('oder dass'); }); it('steht im Ausdruck, sobald eine Fussgaengergruppe vorkommt', () => { const text = ausdruck(createStandardIntersectionProject('Furt', FIXED_DATE)); expect(text).toContain( imAusdruck('Eine Furt mit Mittelinsel ist über die Insel hinweg zu vermessen'), ); expect(text).toContain('bildet das Programm nicht ab'); }); }); /** * Fussgaengerschutzanlage mit vier Phasen - der Umlauf ist uebersaettigt, und * der Pruefbericht raet zum Anforderungsbetrieb (signalplan.uebersaettigt). */ function fsaUebersaettigt(): Project { return { ...zweiFensterOhneEntlastung(), anlagenart: 'fussgaengerschutzanlage' }; } /** Wie zweiFenster(), aber mit den Verkehrsstaerken der Vorlage. */ function zweiFensterOhneEntlastung(): Project { const roh = createStandardIntersectionProject('Anforderung', FIXED_DATE); const id = (name: string) => roh.signalGroups.find((g) => g.name === name)!.id; const nordSued = [id('K1'), id('K2'), id('F1')]; const ostWest = [id('K3'), id('K4'), id('F2')]; const phasen = [ createPhase('Phase 1', nordSued), createPhase('Phase 2', ostWest), createPhase('Phase 3', nordSued), createPhase('Phase 4', ostWest), ]; return { ...roh, phases: phasen, program: { ...roh.program, phaseOrder: phasen.map((p) => p.id) }, }; } /** * Fussgaengerschutzanlage mit langer Wartezeit - der Pruefbericht raet ebenfalls * zum Anforderungsbetrieb, aber aus einer anderen Regel (wartezeitBefunde in * rules.ts statt uebersaettigtVorschlag in engine.ts). Beide Wege muessen den * Vorbehalt tragen; er steht deshalb einmal als Konstante. */ function fsaLangeWartezeit(): Project { return { ...createStandardIntersectionProject('Wartezeit', FIXED_DATE), anlagenart: 'fussgaengerschutzanlage', }; } describe('Anforderungsbetrieb ist nicht abgebildet (Befund D3)', () => { it('haengt den Vorbehalt an den Vorschlag bei Übersättigung', () => { const projekt = fsaUebersaettigt(); const bericht = validateProject(projekt, buildSignalPlan(projekt), FIXED_DATE); const befund = bericht.findings.find((f) => f.rule === 'signalplan.uebersaettigt'); expect(befund, 'Fall erreicht die Regel').toBeDefined(); expect(befund!.suggestion).toContain('auf Anforderung'); expect(befund!.suggestion).toContain(ANFORDERUNGSBETRIEB_VORBEHALT); }); it('haengt den Vorbehalt an den Vorschlag bei zu langer Wartezeit', () => { const projekt = fsaLangeWartezeit(); const bericht = validateProject(projekt, buildSignalPlan(projekt), FIXED_DATE); const befund = bericht.findings.find((f) => f.rule.startsWith('signalplan.wartezeit')); expect(befund, 'Fall erreicht die Regel').toBeDefined(); expect(befund!.suggestion).toContain('auf Anforderung'); expect(befund!.suggestion).toContain(ANFORDERUNGSBETRIEB_VORBEHALT); }); it('laesst keinen Anforderungs-Vorschlag ohne Vorbehalt durch', () => { // Die eigentliche Zusage: Nicht "an zwei Stellen steht es", sondern "es // gibt keine Stelle ohne". Ein neuer Vorschlagstext faellt hier auf. let gepruefte = 0; for (const projekt of [fsaUebersaettigt(), fsaLangeWartezeit(), zweiFensterOhneEntlastung()]) { const bericht = validateProject(projekt, buildSignalPlan(projekt), FIXED_DATE); for (const finding of bericht.findings) { if (!finding.suggestion.includes('auf Anforderung')) continue; gepruefte += 1; expect(finding.suggestion, finding.rule).toContain(ANFORDERUNGSBETRIEB_VORBEHALT); } } expect(gepruefte, 'die Schleife prüft wirklich etwas').toBeGreaterThan(1); }); it('nennt Ruecksprung, Alles-Rot und Anforderungslogik - und dass nichts davon geprueft wird', () => { expect(ANFORDERUNGSBETRIEB_VORBEHALT).toContain('Rücksprung'); expect(ANFORDERUNGSBETRIEB_VORBEHALT).toContain('Alles-Rot'); expect(ANFORDERUNGSBETRIEB_VORBEHALT).toContain('Anforderungslogik'); expect(ANFORDERUNGSBETRIEB_VORBEHALT).toContain('weder gerechnet noch geprüft'); // Befund D4: keine Abschnittsnummer, nur das Regelwerk. expect(ANFORDERUNGSBETRIEB_VORBEHALT).not.toMatch(/Abschnitt \d/); }); it('traegt den Vorbehalt in Ausdruck und Pruefbericht-Tabelle', () => { const projekt = fsaLangeWartezeit(); const plan = buildSignalPlan(projekt); const bericht = validateProject(projekt, plan, FIXED_DATE); expect(reportCsv(bericht)).toContain(ANFORDERUNGSBETRIEB_VORBEHALT); expect(ausdruck(projekt)).toContain(imAusdruck(ANFORDERUNGSBETRIEB_VORBEHALT)); }); }); /* * D1 im Ausdruck: Der Schranken-Absatz der einstreifigen Verkehrsfuehrung * rechnete Zwischenzeit und Umlaufzeit mit den Regelwerten statt mit den * Vorgaben des Plans - und genau diese beiden Zahlen sind dort die BEGRUENDUNG * der Umlaufzeitobergrenze. Ein Pruefer, der sie nachrechnet, fand mit den * Kennwerten desselben Dokuments etwas anderes. */ describe('Herleitung der Umlaufzeitobergrenze rechnet mit den Vorgaben des Plans (Hinweis D1)', () => { function einstreifig(vorgaben: Record): Project { const roh = createStandardIntersectionProject('Engstelle', FIXED_DATE); return { ...roh, anlagenart: 'einstreifig', settings: { ...roh.settings, rilsa: vorgaben }, }; } /** Die gerechneten Zahlen des Absatzes, aus derselben Quelle wie der Plan. */ function erwartet(vorgaben: Record): { zwischenzeit: number; umlauf: number } { const defaults = resolveRilsaSettings(vorgaben, undefined, 'einstreifig').defaults; const laenge = ANLAGENART_GRENZEN.einstreifig.maxClearingDistance; return { zwischenzeit: engstellenFolgen(laenge, 50, defaults).zwischenzeit, umlauf: umlaufBeiFreigabe(laenge, 50, 20, defaults), }; } it('nennt mit den Regelwerten die Zahlen des Regelwerts', () => { const soll = erwartet({}); const text = ausdruck(einstreifig({})); expect(text).toContain(`${soll.zwischenzeit} s Zwischenzeit`); expect(text).toContain(`${soll.umlauf} s Umlaufzeit`); }); it('folgt einer abweichenden Raeumgeschwindigkeit in beide Zahlen', () => { const vorgaben = { clearingSpeedKfzStraight: 8 }; const regel = erwartet({}); const soll = erwartet(vorgaben); // Vorbedingung: Ohne Unterschied prueft der Fall nichts. expect(soll.zwischenzeit).not.toBe(regel.zwischenzeit); expect(soll.umlauf).not.toBe(regel.umlauf); const text = ausdruck(einstreifig(vorgaben)); expect(text).toContain(`${soll.zwischenzeit} s Zwischenzeit`); expect(text).toContain(`${soll.umlauf} s Umlaufzeit`); // Und nicht mehr die Regelwertzahlen, die im Plan daneben nicht vorkommen. expect(text).not.toContain(`${regel.zwischenzeit} s Zwischenzeit`); expect(text).not.toContain(`${regel.umlauf} s Umlaufzeit`); }); }); /* * C21 im gedruckten Nachweis: Der VwV-StVO-Eintrag des * Fundstellenverzeichnisses nannte Rot-Gelb-Zeit und Gelbzeitstaffel als * Regelwerte - unter einer Ueberschrift, die "keine einstellbaren Vorgaben" * behauptete, obwohl beide Vorgabenfelder sind. */ describe('Fundstellenverzeichnis nennt die wirksame Gelb- und Rot-Gelb-Zeit (Befund C21)', () => { const eintrag = (defaults = RILSA_DEFAULTS) => quellenOhneFeld('HBS', 'knotenpunkt', defaults).find((e) => e.schluessel === 'vwv-stvo')!; function kennwert(defaults: RilsaDefaults, label: string): string { return eintrag(defaults).kennwerte.find((k) => k.label.startsWith(label))!.wert; } it('behauptet in der Ueberschrift nicht mehr, hier gebe es keine Vorgaben', () => { expect(eintrag().bezeichnung).not.toContain('keine einstellbaren Vorgaben'); expect(eintrag().bezeichnung).toContain('einstellbar'); }); it('nennt mit den Regelwerten den Regelwert ohne Zusatz', () => { expect(kennwert(RILSA_DEFAULTS, 'Gelbzeit')).toBe( '3 s bis 50 km/h, 4 s bis 60 km/h, 5 s bis 70 km/h (Rn. 17)', ); expect(kennwert(RILSA_DEFAULTS, 'Rot-Gelb-Zeit')).not.toContain('abweichend'); }); it('nennt bei abweichender Vorgabe den wirksamen Wert UND den Regelwert', () => { const geaendert = resolveRilsaSettings( { yellowKfzUpTo50: 4, redYellow: 2 }, undefined, 'knotenpunkt', ).defaults; const gelb = kennwert(geaendert, 'Gelbzeit'); expect(gelb).toContain('4 s bis 50 km/h'); expect(gelb).toContain('abweichend vom Regelwert 3 s bis 50 km/h'); const rotGelb = kennwert(geaendert, 'Rot-Gelb-Zeit'); expect(rotGelb).toContain('2 s – abweichend vom Regelwert 1 s'); }); it('druckt den wirksamen Wert auch wirklich in den Ausdruck', () => { const roh = createStandardIntersectionProject('Gelbstaffel', FIXED_DATE); const projekt: Project = { ...roh, settings: { ...roh.settings, rilsa: { yellowKfzUpTo50: 4 } }, }; expect(ausdruck(projekt)).toContain(imAusdruck('abweichend vom Regelwert 3 s bis 50 km/h')); }); });