import { readFileSync } from 'node:fs'; import { inflateSync } from 'node:zlib'; import { describe, expect, it } from 'vitest'; import { buildProjectPdf, DEFAULT_PDF_OPTIONS } from '@/services/export/pdf'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { validateProject } from '@/domain/validation'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { toWinAnsi } from '@/render/pdfSurface'; import type { Project } from '@/domain/model/project'; /* * Befund 34 zur Fassung 5.9.0, der Rest im Ausdruck. * * Der gedruckte Erlaeuterungsabsatz ist berichtigt (bewertung.ts, * schwerverkehrSatz: die Spalte fuehrt qS0 · n · fA / fSV), und die * gleichlautende Zusage in csv.ts ist nachgezogen. Der Docblock von * `schwerverkehrZelle` in pdf.ts trug sie weiter im alten Wortlaut: "Mit dem * Faktor daneben laesst sich die Spalte 'Saettigungsverkehrsstaerke' aus dem * Regelwert je Fahrstreifen nachrechnen: qS = 3600 / (fSV * tB)". * * Sie ist doppelt widerlegt: Die Spalte fuehrt zusaetzlich die Fahrstreifenzahl * und - bei links und rechts abbiegenden Stroemen - die Abminderung fA, und das * Zeichen qS steht in derselben Zeile einmal fuer den Wert je Fahrstreifen und * einmal fuer den des ganzen Stroms. Eine Zusage im Kommentar ist Teil des * Programms; wer das Verhalten aendert, aendert den Kommentar daneben. * * Der Docblock nennt jetzt dieselbe Kette wie der gedruckte Absatz und sagt, * WO n und die Fahrbeziehung in der Unterlage stehen: in der * Signalgruppentabelle. Eine zweite Fahrstreifenspalte in der Leistungstabelle * traegt nicht - ohne die Fahrbeziehung derselben Zeile waere die Kette dort * ohnehin nicht geschlossen, und die Angabe stuende ein zweites Mal in der * Unterlage. * * NACHGEBESSERT: Diese Begruendung gilt nur, solange die Signalgruppentabelle * gedruckt wird. Sie ist abwaehlbar (exportView.ts, `includeSignalGroups`), und * dann stand die Kette "qS0 · n · fA / fSV" in einer Unterlage, in der weder * "Fahrstr." noch "Fahrbeziehung" vorkam - aus keiner gedruckten Groesse zu * schliessen. Derselbe Fall ist in dieser Datei beim Anfahransatz bereits * geloest (Befund 61 zur Fassung 5.8.0): Der Absatz nennt die Abwahl und traegt * die Werte selbst nach. Der Erlaeuterungsabsatz der Leistungstabelle tut das * jetzt ebenso. * * MUTATIONSNACHWEIS, nachgemessen an zwei zurueckgesetzten Staenden: * * - Docblock im alten Wortlaut: 2 von 5 Faellen fallen - der erste und der * zweite. * - Ohne den Nachsatz des Erlaeuterungsabsatzes * (`fahrstreifenHerkunftSatz`, pdf.ts): 2 von 5 fallen - der dritte und * der vierte. * * Der fuenfte ist in beiden Staenden gruen: Er haelt fest, dass die Angabe bei * gedruckter Signalgruppentabelle NICHT ein zweites Mal in der Unterlage * steht, und bewacht damit die Gegenrichtung. */ const STICHTAG = new Date('2026-01-01T12:00:00Z'); const QUELLE = readFileSync('src/services/export/pdf.ts', 'utf8'); /** Der Docblock unmittelbar vor einer Funktion. */ function docblock(quelle: string, funktion: string): string { const stelle = quelle.indexOf(`function ${funktion}(`); expect(stelle, `${funktion} steht in der Quelle`).toBeGreaterThan(0); const davor = quelle.slice(0, stelle); const anfang = davor.lastIndexOf('/**'); const ende = davor.lastIndexOf('*/'); expect(anfang, `${funktion} hat einen Docblock`).toBeGreaterThan(0); expect(ende).toBeGreaterThan(anfang); return davor.slice(anfang, ende + 2); } /** 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(' ').replace(/\s+/g, ' '); } function ausdruck(project: Project, optionen = DEFAULT_PDF_OPTIONS): string { const plan = buildSignalPlan(project); return pdfText( buildProjectPdf(project, plan, validateProject(project, plan, STICHTAG), optionen, STICHTAG), ); } describe('Befund 34 - die Zusage am Ausdruck nennt die vollständige Kette', () => { it('trägt die widerlegte Rechnung nicht mehr', () => { const block = docblock(QUELLE, 'schwerverkehrZelle'); // Der alte Wortlaut, Zeichen fuer Zeichen: "Mit dem Faktor daneben laesst // sich die Spalte ... nachrechnen: qS = 3600 / (fSV * tB)". Was der // Docblock heute davon nennt, nennt er als das, was hier STAND. expect(block).not.toContain('qS = 3600 / (fSV * tB)'); expect(block).not.toContain('laesst sich die Spalte'); }); it('nennt dieselbe Kette wie der gedruckte Absatz und wo ihre Glieder stehen', () => { const block = docblock(QUELLE, 'schwerverkehrZelle'); expect(block).toContain('qS0 · n · fA / fSV'); // n und die Fahrbeziehung, aus der fA folgt, stehen in der // Signalgruppentabelle - deshalb braucht die Leistungstabelle keine // zweite Fahrstreifenspalte. expect(block).toContain('Signalgruppentabelle'); }); it('druckt die Kette im Erläuterungsabsatz und die Glieder in der Signalgruppentabelle', () => { // Zusammenhangspruefung: Der Kommentar sagt zu, was die Unterlage haelt. const text = ausdruck(createStandardIntersectionProject('Schwerverkehr', STICHTAG)); expect(text).toContain(toWinAnsi('qS0 · n · fA / fSV')); expect(text).toContain('Fahrstr.'); expect(text).toContain('Fahrbeziehung'); // Und er sagt auch, WO sie stehen - sonst muesste der Leser die Kette // selbst in der Unterlage suchen. expect(text).toContain(toWinAnsi('Spalten "Fahrstr." und "Fahrbeziehung"')); }); it('nennt n und die Fahrbeziehung selbst, wenn die Signalgruppentabelle abgewählt ist', () => { /* * Der Haken "Signalgruppen" in der Ausgabeansicht (exportView.ts). Ohne * die Tabelle steht die Kette qS0 · n · fA / fSV in einer Unterlage, in * der n und die Fahrbeziehung nirgends vorkommen. Dasselbe Muster wie beim * Anfahransatz (Befund 61 zur Fassung 5.8.0): Die Abwahl wird genannt, * die Werte werden nachgetragen. */ const projekt = createStandardIntersectionProject('Schwerverkehr', STICHTAG); const text = ausdruck(projekt, { ...DEFAULT_PDF_OPTIONS, includeSignalGroups: false }); expect(text, 'Vorbedingung: die Spalte "Fahrstr." gibt es nicht mehr').not.toContain( 'Fahrstr.', ); expect(text).toContain(toWinAnsi('ist für diese Ausgabe abgewählt')); for (const gruppe of projekt.signalGroups.filter((g) => g.mode === 'kfz')) { expect(text, gruppe.name).toContain( toWinAnsi(`"${gruppe.name}" ${gruppe.lanes} Fahrstreifen, geradeaus`), ); } }); it('trägt bei gedruckter Signalgruppentabelle keine zweite Fahrstreifenangabe nach', () => { // Die Gegenprobe: Steht die Tabelle in der Unterlage, bleibt die Angabe an // ihrem einen Ort - sonst stuende sie zweimal da. const text = ausdruck(createStandardIntersectionProject('Schwerverkehr', STICHTAG)); expect(text).not.toContain(toWinAnsi('Fahrstreifen, geradeaus')); }); });