import { inflateSync } from 'node:zlib'; import { describe, expect, it } from 'vitest'; import { buildProjectPdf, DEFAULT_PDF_OPTIONS, type PdfOptions } from '@/services/export/pdf'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { validateProject } from '@/domain/validation'; import { createSignalGroup, createStandardIntersectionProject } from '@/domain/model/factory'; import { SERVICE_LEVEL_SCALES } from '@/domain/rilsa/constants'; import { PRUEFSTAND_LABELS, QUELLEN } from '@/domain/rilsa/quellen'; import { quellenOhneFeld, stufentafelSatz } from '@/services/export/bewertung'; import type { Project } from '@/domain/model/project'; import * as fmt from '@/ui/format'; /* * Befunde B1 bis B4: Der Leistungsabschnitt des Ausdrucks behauptete "Bewertung * nach HBS 2015", waehrend fuer beide Einstellungen dieselbe HCM-Formel * gerechnet und nach der Kfz-Tafel des HBS 2001 bewertet wurde; Fussgaenger * fehlten mit der Begruendung, ihre Leistungsfaehigkeit sei "nicht * massgebend" - das HBS 2015 bewertet sie nach der maximalen Wartezeit. Diese * Tests sichern, dass der Ausdruck nur noch sagt, was gerechnet wurde. */ const STICHTAG = 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 t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) { teile.push(t[1]!.replace(/\\([()\\])/g, '$1')); } } return teile.join('\n'); } /** * Nur der Leistungsabschnitt, ohne das Fundstellenverzeichnis: Dort steht * "HBS 2015" zu Recht als Herkunft der Saettigungsverkehrsstaerke - auch wenn * nach HCM bewertet wird. Die Behauptung, um die es hier geht, ist die * "Bewertung nach ...". */ const NUR_LEISTUNG: PdfOptions = { ...DEFAULT_PDF_OPTIONS, includeSignalGroups: false, includeIntergreenMatrix: false, includePhases: false, includePlan: false, includeReport: false, includeSources: false, }; /** Fliesstext ohne Zeilenumbrueche - der Ausdruck bricht Saetze fuer den Satzspiegel um. */ function ausdruck(project: Project, options: PdfOptions = NUR_LEISTUNG): string { const plan = buildSignalPlan(project); return pdfText( buildProjectPdf(project, plan, validateProject(project, plan, STICHTAG), options), ).replace(/\s+/g, ' '); } function vollstaendig(verfahren: 'HBS' | 'HCM' = 'HBS'): Project { const base = createStandardIntersectionProject('Bewertung', STICHTAG); return { ...base, meta: { ...base.meta, planner: 'O. Willerding' }, settings: { ...base.settings, serviceLevelScale: verfahren }, }; } describe('Ausdruck nennt das Bewertungsverfahren, das gerechnet wurde', () => { it('sagt "Bewertung nach HBS 2015" nur beim HBS-Verfahren, sonst "Bewertung nach HCM"', () => { const hbs = ausdruck(vollstaendig('HBS')); expect(hbs).toContain('Bewertung nach HBS 2015'); expect(hbs).not.toContain('Bewertung nach HCM'); // Die Formelkette, die tatsaechlich gerechnet wird - nicht die HCM-Formel // unter fremder Ueberschrift (Befund B2). expect(hbs).toContain('tA = tF + 1 s'); expect(hbs).toContain('tW = tW,G + tW,R'); expect(hbs).toContain('Instationaritätsfaktor fin = 1,1'); expect(hbs).not.toContain('d1 + d2'); const hcm = ausdruck(vollstaendig('HCM')); expect(hcm).toContain('Bewertung nach HCM'); expect(hcm).not.toContain('Bewertung nach HBS 2015'); /* * Ohne Fundstellenverzeichnis darf "HBS 2015" im HCM-Ausdruck nur dort * vorkommen, wo es die HERKUNFT einer Eingangsgroesse ist - nie als * Bewertungsverfahren. Zwei solche Stellen gibt es: * * - die ehrliche Anmerkung zur Fuss/Rad-Bewertung (Befund B4), * - der Rechenweg der Saettigungsverkehrsstaerke (Fassung 5.5.0, Frage E6): Der * Anpassungsfaktor fSV stammt auch in einem HCM-Projekt aus dem * HBS 2015, weil die Saettigungsverkehrsstaerke es tut. Bis zum * 15.08.2026 stand an dieser Stelle "Pkw-Aequivalent 2,0" - der Wert * des HCM 2010, gedruckt unter der Ueberschrift HBS. Die Nennung des * Regelwerks ist hier also gerade die Behebung des Befunds und nicht * sein Rueckfall. */ const ohneHerkunftsangaben = hcm .replace(/nach der das HBS 2015 bewerten würde/g, '') .replace(/Der Schwerverkehr geht nach HBS 2015 über den Anpassungsfaktor fSV/g, ''); expect(ohneHerkunftsangaben).not.toContain('HBS 2015'); expect(hcm).toContain('d = d1 + d2'); expect(hcm).toContain('T = 0,25 h'); expect(hcm).not.toContain('tW,R'); }); it('druckt die Kfz-Stufentafel des HBS 2015 aus den Konstanten - E ohne Obergrenze, F bei q > C', () => { // Befund B1: Bis dahin stand die Tafel des HBS 2001 (E bis 100 s, F // darueber) unter der Ueberschrift "HBS 2015". Die gedruckte Tafel kommt // jetzt aus denselben Konstanten wie die Bewertung. const text = ausdruck(vollstaendig('HBS')); expect(text).toContain('A bis 20 s, B bis 35 s, C bis 50 s, D bis 70 s, E darüber'); expect(text).toContain('Stufe F bei Überlastung (q > C)'); expect(text).not.toContain('E bis 100 s'); // Gegenprobe gegen die Konstante selbst. const e = SERVICE_LEVEL_SCALES.HBS.kfz.find((s) => s.grade === 'E')!; expect(Number.isFinite(e.maxDelay)).toBe(false); }); it('fuehrt Fussgaenger mit der maximalen Wartezeit auf und streicht den falschen Satz', () => { // Befund B4: Der alte Satz "Fussgaengersignalgruppen sind nicht // aufgefuehrt, da ihre Leistungsfaehigkeit ... nicht massgebend ist" war // falsch - das HBS 2015 bewertet sie nach der laengsten Sperrzeit. const projekt = vollstaendig('HBS'); const plan = buildSignalPlan(projekt); const text = ausdruck(projekt); expect(text).not.toContain('nicht aufgeführt'); expect(text).not.toContain('nicht maßgebend'); expect(text).toContain('Fußgänger und Radverkehr'); expect(text).toContain('Längste Sperrzeit = maximale Wartezeit'); expect(text).toContain('nach der maximalen Wartezeit bewertet'); expect(text).toContain('A bis 30 s, B bis 40 s, C bis 55 s, D bis 70 s, E bis 85 s, F darüber'); // Die gedruckte maximale Wartezeit ist die laengste Sperrzeit der Gruppe - // und die Stufe dazu ist die des Fachkerns. const fuss = plan.groups.filter((g) => g.mode === 'fuss'); expect(fuss.length).toBeGreaterThan(0); for (const gruppe of fuss) { expect(gruppe.delay, gruppe.name).not.toBeNull(); const maximal = gruppe.delay!.maximumDelay; expect(text, gruppe.name).toContain(`${maximal.toFixed(1).replace('.', ',')} s`); expect(gruppe.delay!.serviceLevel, gruppe.name).not.toBeNull(); expect(gruppe.delay!.serviceLevel!.kriterium).toBe('maximale-wartezeit'); } }); it('vergibt nach HCM fuer Fussgaenger keine Stufe und sagt das', () => { const projekt = vollstaendig('HCM'); const plan = buildSignalPlan(projekt); for (const gruppe of plan.groups.filter((g) => g.mode === 'fuss')) { expect(gruppe.delay?.serviceLevel ?? null, gruppe.name).toBeNull(); } const text = ausdruck(projekt); expect(text).toContain('Fußgänger und Radverkehr'); expect(text).toContain('Nach HCM wird für Fußgänger und Radverkehr keine Stufe vergeben'); // Auch ohne Stufe steht die maximale Wartezeit da. expect(text).toContain('Längste Sperrzeit = maximale Wartezeit'); }); it('kennzeichnet die Stufe F bei Ueberlastung als "überlastet"', () => { // Nach HBS 2015 haengt F fuer Kfz an q > C, nicht an der Wartezeit. Ein // blosses "F" neben einer Wartezeit liesse sich mit der gedruckten Tafel // nicht in Einklang bringen. const roh = vollstaendig('HBS'); const projekt: Project = { ...roh, demands: roh.demands.map((d, i) => (i === 0 ? { ...d, volume: 3000 } : d)), }; const plan = buildSignalPlan(projekt); const ueberlastet = plan.groups.filter( (g) => g.delay?.serviceLevel?.kriterium === 'ueberlastung', ); expect(ueberlastet.length, 'die Vorbereitung muss eine Ueberlastung erzeugen').toBeGreaterThan( 0, ); const text = ausdruck(projekt); expect(text).toContain('F (überlastet)'); }); it('druckt auf Wunsch den Rechenweg der Wartezeit mit den Zwischenwerten', () => { // Grundsatz: tW muss sich aus den gedruckten Groessen // nachrechnen lassen. Der Rechenweg wird wie bei den Zwischenzeiten ueber // includeCalculationDetails zugeschaltet. const projekt = vollstaendig('HBS'); const plan = buildSignalPlan(projekt); // Seit Befund C15 ist der Rechenweg voreingestellt; die Abwahl muss hier // ausdruecklich gesetzt werden. Die Zeile stand bis dahin ohne Zusatz da - // sie las die Voreinstellung mit. const ohne = ausdruck(projekt, { ...NUR_LEISTUNG, includeCalculationDetails: false }); expect(ohne).not.toContain('Rechenweg Wartezeit'); const mit = ausdruck(projekt, { ...NUR_LEISTUNG, includeCalculationDetails: true }); expect(mit).toContain('Rechenweg Wartezeit'); expect(mit).toContain('NGE'); expect(mit).toContain('tW,G'); expect(mit).toContain('tW,R'); expect(mit).toContain('Wurzel aus'); // Die Zwischenwerte des Fachkerns stehen im Ausdruck. const k1 = plan.groups.find((g) => g.mode === 'kfz' && g.delay !== null)!; const rw = k1.delay!.rechenweg; expect(rw.reststau).not.toBeNull(); expect(mit).toContain(`${k1.delay!.uniformDelay.toFixed(1).replace('.', ',')} s`); expect(mit).toContain(rw.reststau!.toFixed(2).replace('.', ',')); // Nach HCM stehen die HCM-Beiwerte im Rechenweg, nicht NGE. const hcm = ausdruck(vollstaendig('HCM'), { ...NUR_LEISTUNG, includeCalculationDetails: true }); expect(hcm).toContain('Rechenweg Wartezeit'); expect(hcm).toContain('d1'); expect(hcm).toContain('k = 0,5'); expect(hcm).not.toContain('NGE'); }); it('druckt die Abflusszeit, aus der sich die Kapazitaet nachrechnen laesst', () => { // C = qS · tA/tU - ohne tA in der Tabelle ginge die Kapazitaet neben der // Freigabezeit nicht auf (Befund B2, tA = tF + 1 s). const projekt = vollstaendig('HBS'); const plan = buildSignalPlan(projekt); const text = ausdruck(projekt); expect(text).toContain('Abflusszeit tA'); const k1 = plan.groups.find((g) => g.mode === 'kfz' && g.capacity !== null)!; expect(k1.capacity!.abflusszeit).toBeCloseTo(k1.totalGreen + 1, 3); expect(text).toContain(`${k1.capacity!.abflusszeit.toFixed(1).replace('.', ',')} s`); }); it('druckt den Auslastungsgrad mit drei Nachkommastellen', () => { // Mit zwei Stellen stand bei q = 244 und C = 243 "1,00" neben "F // (ueberlastet)" - die gedruckte Zahl widersprach der gedruckten Stufe. // Drei Stellen wie im Rechenweg und im Pruefbericht. const projekt = vollstaendig('HBS'); const plan = buildSignalPlan(projekt); const text = ausdruck(projekt); const kfz = plan.groups.filter((g) => g.mode === 'kfz' && g.capacity !== null); expect(kfz.length).toBeGreaterThan(0); for (const gruppe of kfz) { expect(gruppe.capacity!.degreeOfSaturation, gruppe.name).toBeDefined(); const drei = fmt.ratio(gruppe.capacity!.degreeOfSaturation!, 3); expect(drei, gruppe.name).toMatch(/^\d,\d{3}$/); expect(text, gruppe.name).toContain(drei); } // Der Fall des Befunds: q knapp ueber C darf nicht als "1,00" erscheinen. // Der Umlauf haengt an der Nachfrage (Webster); deshalb wird die // Verkehrsstaerke aus dem Plan abgeleitet und der Plan neu gebaut. const k1 = kfz[0]!; const knapp: Project = { ...projekt, demands: projekt.demands.map((d) => d.signalGroupId === k1.groupId ? { ...d, volume: k1.capacity!.capacity + 1 } : d, ), }; const neu = buildSignalPlan(knapp).groups.find((g) => g.groupId === k1.groupId)!; const x = neu.capacity!.degreeOfSaturation!; const gedruckt = ausdruck(knapp); if (x > 1 && x < 1.005) { expect(fmt.ratio(x, 2), 'Vorbedingung des Befunds').toBe('1,00'); expect(gedruckt).toContain(fmt.ratio(x, 3)); expect(gedruckt).toContain('F (überlastet)'); } else { // Der Neuaufbau hat den Umlauf verschoben; dann gilt nur die Stellenzahl. expect(gedruckt).toContain(fmt.ratio(x, 3)); } }); }); describe('Signalgruppentabelle: Verkehrsstaerke nur fuer Kfz und OePNV', () => { it('druckt fuer eine Radgruppe mit gespeicherter Verkehrsstaerke "–"', () => { // Altdatei aus der Zeit vor Fassung 5.4.0 (Befund B4): Fuer // eine Radgruppe ist eine Verkehrsstaerke gespeichert. Sie geht in keine // Rechnung ein, und der Pruefbericht sagt das - im Ausdruck darf sie // deshalb nicht als Eingangsgroesse erscheinen. const roh = vollstaendig('HBS'); const rad = createSignalGroup({ name: 'R9', mode: 'rad', index: 9 }); const projekt: Project = { ...roh, signalGroups: [...roh.signalGroups, rad], demands: [...roh.demands, { signalGroupId: rad.id, volume: 777, heavyVehicleShare: 0 }], }; const text = ausdruck(projekt, { ...NUR_LEISTUNG, includeSignalGroups: true, includePerformance: false, }); expect(text).toContain('R9'); expect(text).not.toContain('777 Fz/h'); // Die Kfz-Verkehrsstaerken stehen weiterhin da. expect(text).toContain(`${fmt.vehiclesPerHour(roh.demands[0]!.volume)}`); }); }); const NUR_GRUNDLAGEN: PdfOptions = { ...NUR_LEISTUNG, includePerformance: false, includeSources: true, }; describe('Grundlagen der Kennwerte', () => { it('sagt bei HCM, dass dieses Projekt nach dem HCM bewertet und aus dem HBS nur Eingangsgroessen nimmt', () => { // Die Rechtsgrundlagentafel nennt das HBS 2015 allgemein als Grundlage // von Kapazitaet, Wartezeit und Qualitaetsstufen. Ohne den Satz zum // Projekt hielte der Leser einer HCM-Bewertung die ganze Bewertung fuer // eine nach HBS 2015. const hcm = ausdruck(vollstaendig('HCM'), NUR_GRUNDLAGEN); expect(hcm).toContain('Dieses Projekt bewertet nach dem HCM (Highway Capacity Manual)'); expect(hcm).toContain('Wartezeit d = d1 + d2, Level of Service'); expect(hcm).toContain( 'Aus dem HBS 2015 stammen hier nur Sättigungsverkehrsstärke und Abbiegerfaktor', ); expect(hcm).not.toContain('rechnet und bewertet nach dem HBS 2015'); const hbs = ausdruck(vollstaendig('HBS'), NUR_GRUNDLAGEN); expect(hbs).toContain('Dieses Projekt rechnet und bewertet nach dem HBS 2015'); expect(hbs).not.toContain('Dieses Projekt bewertet nach dem HCM'); }); it('fuehrt die Stufentafeln der Qualitaetsstufen im Fundstellenverzeichnis mit Pruefstand', () => { // Die Quelle 'hbs-qualitaetsstufen' hat kein Vorgabenfeld und fehlte im // Verzeichnis deshalb ganz: Die gedruckten Tafeln hatten weder Herkunft // noch Pruefstand, obwohl OePNV- und Fuss/Rad-Tafel nur ueber // Sekundaerquellen belegt sind. const quelle = QUELLEN['hbs-qualitaetsstufen']; /* * PRUEFSTAND SEIT DEM 15.08.2026 "gesichert" (Fassung 5.5.0, Frage E5): Hier stand * "abzugleichen", weil die OePNV- und die Fuss/Rad-Tafel nur ueber * Sekundaerquellen belegt waren. Inzwischen liegen zwei * HERAUSGEBERSEITIGE Wiedergaben derselben gedruckten HBS-Tabelle vor - * der Vortrag des Verfassers von Kapitel S4 beim FGSV-Einfuehrungs- * kolloquium 2015 und Tab. 2-2 des BASt-Berichts V 400 (2025). Der Test * haelt weiter den Pruefstand der Quelle gegen den gedruckten fest; die * Zusicherung ist also nicht schwaecher geworden, sondern folgt der * Beleglage. */ expect(quelle.pruefstand, 'Vorbedingung: die Tafeln sind zweifach belegt').toBe('gesichert'); const text = ausdruck(vollstaendig('HBS'), NUR_GRUNDLAGEN); expect(text).toContain('Fundstellenverzeichnis'); expect(text).toContain( 'Stufentafeln der Qualitätsstufen (Kfz/ÖPNV: mittlere Wartezeit; Fuß/Rad: maximale Wartezeit)', ); expect(text).toContain(quelle.sachgebiet); // Die Werte selbst, aus den Konstanten - und die Stufe F mit ihrem Grund. expect(text).toContain( `Stufentafel Kfz (mittlere Wartezeit): ${stufentafelSatz(SERVICE_LEVEL_SCALES.HBS.kfz)}`, ); 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)}`, ); expect(text).toContain('F bei Überlastung (q > C)'); // Der Hinweis mit der Beleglage steht dabei; der Pruefstand ist derselbe // wie in der Quelle. Der Hinweis nennt jetzt beide Herausgeberquellen und // sagt zugleich, was sie NICHT sind - zwei Wiedergaben eines Originals // sind kein zweiter Zeuge fuer das Original (Fassung 5.5.0, Frage E5). expect(text).toContain('Zu beachten: Alle drei Stufentafeln'); expect(text).toContain('zwei Wiedergaben eines Originals'); expect(text).toContain(PRUEFSTAND_LABELS[quelle.pruefstand]); // Die Liste der feldlosen Quellen kennt genau diese Quelle. expect(quellenOhneFeld().map((q) => q.schluessel)).toContain('hbs-qualitaetsstufen'); }); }); describe('HCM-Projekt: Nachweis der Kennwerte ohne HBS-Bewertungsetikett', () => { it('fuehrt die HCM-Tafel statt der HBS-Tafeln und vermerkt wirkungslose HBS-Wartezeitkennwerte', () => { // Im HCM-Projekt gehen die HBS-Tafeln und der Instationaritaetsfaktor in // keine Zahl ein. Das Verzeichnis druckte sie trotzdem als verwendet // (Befund F6). const projekt: Project = { ...vollstaendig('HCM'), settings: { ...vollstaendig('HCM').settings, rilsa: { instationaritaetsfaktor: 1.2 } }, }; const text = ausdruck(projekt, NUR_GRUNDLAGEN); expect(text).toContain('Level of Service (Kfz/ÖPNV: mittlere Wartezeit; Fuß/Rad: keine Tafel)'); expect(text).toContain( `Stufentafel Kfz und ÖPNV (mittlere Wartezeit): ${stufentafelSatz(SERVICE_LEVEL_SCALES.HCM.kfz)}`, ); expect(text).not.toContain('Stufentafeln der Qualitätsstufen (Kfz/ÖPNV'); expect(text).toContain( 'Instationaritätsfaktor (HBS 2015) (ohne Wirkung: Bewertungsverfahren HCM)', ); // Die Abweichungstabelle verlangt fuer den wirkungslosen Wert keine Begruendung. expect(text).toContain( 'Instationaritätsfaktor (HBS 2015) (ohne Wirkung: Bewertungsverfahren HCM)', ); // Die HCM-Tafel statt der HBS-Tafeln; die VwV-StVO steht unabhaengig vom // Verfahren dabei, die RSA nur bei einstreifiger Fuehrung. // Die Geometriefaktoren stehen seit 5.30.0 unabhaengig vom Verfahren // dabei: Sie wirken auf jede Saettigungsverkehrsstaerke, und wo keine // Zufahrt vermessen ist, steht der Faktor 1 fuer eine ANGENOMMENE // Standardbedingung - genau das gehoert in den Nachweis. expect(quellenOhneFeld('HCM').map((q) => q.schluessel)).toEqual([ 'hcm-verfahren', 'vwv-stvo', 'hbs-geometriefaktoren', // Seit 5.31.0: die Laenge je wartendem Fahrzeug, aus der die // gedruckte Rueckstaulaenge entsteht. 'praxis-stauraum-je-fahrzeug', ]); expect(quellenOhneFeld('HCM').some((q) => q.schluessel === 'hbs-qualitaetsstufen')).toBe(false); expect(QUELLEN['hcm-verfahren'].pruefstand).toBe('fachliteratur'); }); it('vermerkt im HBS-Projekt keinen Kennwert als verfahrensbedingt wirkungslos', () => { const text = ausdruck(vollstaendig('HBS'), NUR_GRUNDLAGEN); // Der verfahrensbedingte Vermerk gilt nur dem HBS-Wartezeitverfahren; im // HBS-Projekt wirkt es, also darf er nirgends stehen. expect(text).not.toContain('ohne Wirkung: Bewertungsverfahren HCM'); expect(text).not.toContain('Instationaritätsfaktor (HBS 2015) (ohne Wirkung'); // Befund C20: Die Progressionsgeschwindigkeiten sind dagegen UNABHAENGIG // vom Bewertungsverfahren wirkungslos, solange kein Strassenzug erfasst ist // (seit 5.35.0 haengt der Vermerk daran und nicht mehr an der Fassung). // Dieser Vermerk steht deshalb auch hier, und zwar mit seinem eigenen // Grund statt mit dem des Bewertungsverfahrens. expect(text).toContain( 'Progressionsgeschwindigkeit (ohne Wirkung: keine Koordinierung erfasst)', ); }); it('nennt in der Abweichungstabelle den eigenen Grund der Koordinierung, nicht das Bewertungsverfahren', () => { // Befund C20, Folgefehler im Ausdruck: Die Bedingung fuer den Zusatz // fragte bereits den Fachkern, der Zusatztext selbst stand fest auf // " (ohne Wirkung: Bewertungsverfahren HCM)". Die Zeile der // Progressionsgeschwindigkeit behauptete damit in einem HBS-Projekt ein // Bewertungsverfahren als Grund, das mit ihr nichts zu tun hat. const roh = vollstaendig('HBS'); const projekt: Project = { ...roh, settings: { ...roh.settings, rilsa: { ...roh.settings.rilsa, coordinationDefaultSpeed: 70 } }, }; const text = ausdruck(projekt, NUR_GRUNDLAGEN); expect(text).toContain('Abweichungen von den Regelwerten'); // Kennwert | Regelwert | Angesetzt | Einheit - mit dem Grund an der Zeile. expect(text).toMatch( /Progressionsgeschwindigkeit \(ohne Wirkung: keine Koordinierung erfasst\)\s+50\s+70\s+km\/h/, ); expect(text).not.toContain('Bewertungsverfahren HCM'); }); });