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 { createStandardIntersectionProject } from '@/domain/model/factory'; import { toWinAnsi } from '@/render/pdfSurface'; import type { Lageplan, Planlinie } from '@/domain/geometrie/lageplan'; import type { Project } from '@/domain/model/project'; /* * BEFUNDE 21 UND 22: Zwei Verweise standen unbedingt da und schickten den * Pruefer in Abschnitte, die die Ausgabe nicht enthaelt. * * Befund 21 - "Grundlagen der Kennwerte": Der Satz "Dieses Projekt rechnet und * bewertet nach dem HBS 2015 (Verfahren siehe Abschnitt Leistungsfaehigkeit)" * wurde auch dann gedruckt, wenn der Abschnitt "Leistungsfaehigkeit" abgewaehlt * war. Auf Blatt 2 derselben Datei weist die Uebersicht "Bestandteile der * Anordnungsunterlage" ihn zugleich als "abgewählt - in dieser Ausgabe nicht * enthalten" aus. * * Befund 22 - "Planungsgrundlage": Der Satz verwies auf die Spalte "Herkunft * der Wege" im Rechenweg der Zwischenzeiten. Die Spalte entsteht nur unter * `includeCalculationDetails && includeIntergreenMatrix`; ohne sie blieb * "Herkunft der Wege" genau einmal im Dokument stehen - naemlich im Verweis * selbst. * * Beides ist dasselbe Muster, das dieselbe Datei an sechs anderen Stellen * bereits bedingt fuehrt ("Ein Verweis auf einen abgewaehlten Abschnitt waere * ein Verweis ins Leere", pdf.ts zu planVerweisRumpf). */ const FEST = 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(' ').replace(/\s+/g, ' '); } const gedruckt = (text: string): string => toWinAnsi(text).replace(/\s+/g, ' '); function fahrlinie(): Planlinie { return { id: 'l1', name: 'l1', mode: 'kfz', movement: 'geradeaus', breiteMeter: 3.25, punkte: [ { x: 10, y: 10 }, { x: 10, y: 90 }, ], haltlinieId: null, startT: 0.5, signalGroupId: null, }; } /** Ein masstaeblich auswertbarer Lageplan - sonst greift der andere Satzzweig. */ const AUSWERTBAR: Lageplan = { arbeitsbereiche: [], signalgeber: [], bild: { datenUrl: 'data:image/png;base64,AAAA', breite: 1000, hoehe: 1000, herkunft: 'Luftbild NRW - Kante 100 m', geladenAm: '2026-03-04T09:15:00.000Z', }, kalibrierung: { von: { x: 0, y: 0 }, bis: { x: 100, y: 0 }, laengeMeter: 10, herkunft: 'georeferenziert', }, haltlinien: [], linien: [fahrlinie()], }; function ausdruck(optionen: Partial, mitLageplan = false): string { const basis = createStandardIntersectionProject('Hauptstrasse / Bahnhofstrasse', FEST); const project: Project = mitLageplan ? { ...basis, lageplan: AUSWERTBAR } : basis; const plan = buildSignalPlan(project); return pdfText( buildProjectPdf( project, plan, validateProject(project, plan, FEST), { ...DEFAULT_PDF_OPTIONS, ...optionen }, FEST, ), ); } describe('Befund 21 - Verweis der Grundlagen auf die Leistungsfaehigkeit', () => { it('verweist bei abgewaehlter Leistungsfaehigkeit nicht auf deren Abschnitt', () => { const text = ausdruck({ includePerformance: false, includeSources: true }); expect(text, 'Vorbedingung: der Satz selbst steht weiter da').toContain( gedruckt('Dieses Projekt rechnet und bewertet nach dem HBS 2015'), ); expect(text, 'Vorbedingung: die Bestandteileuebersicht weist die Abwahl aus').toContain( gedruckt('abgewählt'), ); expect(text, 'der Verweis geht ins Leere').not.toContain( gedruckt('Verfahren siehe Abschnitt Leistungsfähigkeit'), ); expect(text, 'stattdessen wird die Abwahl an der Stelle benannt').toContain( gedruckt( 'der Abschnitt "Leistungsfähigkeit", der das Verfahren zeigt, ist für diese Ausgabe abgewählt', ), ); }); it('behaelt den Verweis, solange die Leistungsfaehigkeit im Dokument steht', () => { const text = ausdruck({ includePerformance: true, includeSources: true }); expect(text).toContain( gedruckt('Dieses Projekt rechnet und bewertet nach dem HBS 2015 (Verfahren siehe Abschnitt'), ); }); }); describe('Befund 22 - Verweis der Planungsgrundlage auf "Herkunft der Wege"', () => { for (const [name, optionen] of [ ['Rechenweg abgewaehlt', { includeCalculationDetails: false }], ['Zwischenzeitenmatrix abgewaehlt', { includeIntergreenMatrix: false }], ] as const) { it(`verweist bei "${name}" nicht auf die Spalte "Herkunft der Wege"`, () => { const text = ausdruck(optionen, true); expect(text, 'Vorbedingung: der Lageplan ist auswertbar').toContain( gedruckt('Der Lageplan ist maßstäblich auswertbar'), ); expect(text, 'Vorbedingung: die Zusage zum Mitfuehren bleibt').toContain( gedruckt('führt das Projekt je Beziehung mit'), ); expect(text, 'der Verweis auf die Spalte geht ins Leere').not.toContain( gedruckt( 'ausgewiesen wird es in der Spalte "Herkunft der Wege" im Rechenweg der Zwischenzeiten', ), ); expect(text, 'stattdessen wird die Luecke ausgewiesen').toContain( gedruckt( 'der Rechenweg der Zwischenzeiten, der es in der Spalte "Herkunft der Wege" Beziehung ' + 'für Beziehung ausweist, ist für diese Ausgabe abgewählt', ), ); }); } it('behaelt den Verweis, solange der Rechenweg der Zwischenzeiten im Dokument steht', () => { const text = ausdruck({}, true); expect(text).toContain( gedruckt( 'ausgewiesen wird es in der Spalte "Herkunft der Wege" im Rechenweg der Zwischenzeiten', ), ); }); });