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 type { Project } from '@/domain/model/project'; /* * Befund 37 zur Fassung 5.9.0, am fertigen Ausdruck. * * Die Wache in tests/render/steuerzeichen.test.ts haelt den Vertrag der * Zeichenflaeche fest. Dieser Fall zeigt die Wirkung dort, wo sie zaehlt: im * Inhaltsstrom des PDF. Ein Signalgruppenname mit \n - ueber eine Projektdatei * erreichbar, weil `str` in schema.ts Zeichenketten ungefiltert durchreicht - * wurde von jsPDF in ZWEI Textzeilen zerlegt, waehrend `planHeight` je * Signalgruppe genau eine Zeile rechnet. Die zweite Zeile stand damit in der * Beschriftungsspalte der naechsten Signalgruppe. * * Gegen den Altstand fehlt "K1 Nord" im Inhaltsstrom; dort stehen stattdessen * "K1" und "Nord" als zwei aufeinanderfolgende Textzeilen. */ const STICHTAG = new Date('2026-01-01T12:00:00Z'); /** Jede einzelne Textzeile des PDF, unverbunden - der Umbruch ist der Befund. */ function pdfZeilen(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; } /** Knotenpunkt, dessen erste Signalgruppe einen Zeilenumbruch im Namen traegt. */ function mitUmbruchImNamen(): Project { const basis = createStandardIntersectionProject('Zeichenfläche', STICHTAG); return { ...basis, signalGroups: basis.signalGroups.map((g, index) => index === 0 ? { ...g, name: 'K1\nNord' } : g, ), }; } describe('Befund 37 - der gezeichnete Signalzeitenplan bleibt einzeilig', () => { it('setzt die Beschriftung als EINE Textzeile in den Inhaltsstrom', () => { const projekt = mitUmbruchImNamen(); const plan = buildSignalPlan(projekt); const bericht = validateProject(projekt, plan, STICHTAG); const zeilen = pdfZeilen( buildProjectPdf(projekt, plan, bericht, DEFAULT_PDF_OPTIONS, STICHTAG), ); // Vorbedingung: Der Name traegt den Umbruch wirklich bis in den Plan. expect(plan.groups[0]?.name).toBe('K1\nNord'); expect(zeilen).toContain('K1 Nord'); }); });