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 55 zur Fassung 5.9.0, dieselbe Stelle im Ausdruck. * * In der Tabellenausgabe ist der englische Dezimalpunkt der Fahrstreifenzahl * behoben (csv.ts, `fmt.numShort(group.lanes)`; Wache * tests/export/fahrstreifenkomma.test.ts). Die Signalgruppentabelle * des Ausdrucks fuehrte weiter `String(group.lanes)` und druckte damit "2.5". * * `num()` in schema.ts rundet `lanes` seit der Fassung 5.10.0 auf eine * ganze Zahl AB und meldet die Ersetzung (`ganzzahlAbrunden`; Wache * tests/domain/fahrstreifenzahl.test.ts) - aus einer Projektdatei kommt * eine 2,5 also nicht mehr durch. Der Fall laesst sich hier trotzdem bauen: * Diese Datei setzt das Projekt unmittelbar zusammen, ohne `parseProject`, und * `SignalGroup.lanes` traegt im Typ keine Ganzzahlschranke. Bewacht wird die * Schreibweise und nicht der Weg, auf dem der Wert entsteht: Jede andere Zahl * der Unterlage steht in deutscher Schreibweise - diese eine nicht. Der * Ausdruck ist die Unterlage, die zur Behoerde geht; "2.5" liest sich dort je * nach Gewohnheit als 25. * * MUTATIONSNACHWEIS: Gegen den Altstand faellt der erste Fall. Der zweite haelt * fest, dass sich an ganzen Fahrstreifenzahlen nichts aendert, und ist in * beiden Staenden gruen. */ const STICHTAG = new Date('2026-01-01T12:00:00Z'); /** * Die einzelnen Textstuecke des PDF; die Inhaltsstroeme sind komprimiert. * * Stueckweise und nicht als ein Text: Eine Tabellenzelle ist ein Stueck, und * nur so laesst sich pruefen, dass sie GENAU "2,5" lautet. */ function pdfStuecke(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; } function ausdruck(project: Project): Uint8Array { const plan = buildSignalPlan(project); return buildProjectPdf( project, plan, validateProject(project, plan, STICHTAG), DEFAULT_PDF_OPTIONS, STICHTAG, ); } /** Knotenpunkt, dessen erste Signalgruppe eine gebrochene Fahrstreifenzahl traegt. */ function mitGebrochenerFahrstreifenzahl(): Project { const basis = createStandardIntersectionProject('Fahrstreifen', STICHTAG); return { ...basis, signalGroups: basis.signalGroups.map((g, index) => (index === 0 ? { ...g, lanes: 2.5 } : g)), }; } describe('Befund 55 - die Fahrstreifenspalte des Ausdrucks trägt das deutsche Dezimalkomma', () => { it('druckt 2,5 statt 2.5', () => { const stuecke = pdfStuecke(ausdruck(mitGebrochenerFahrstreifenzahl())); expect(stuecke, 'Vorbedingung: der Ausdruck traegt Text').not.toHaveLength(0); expect(stuecke).toContain('2,5'); expect(stuecke).not.toContain('2.5'); }); it('lässt ganze Fahrstreifenzahlen unverändert', () => { const projekt = createStandardIntersectionProject('Fahrstreifen', STICHTAG); const stuecke = pdfStuecke(ausdruck(projekt)); for (const gruppe of projekt.signalGroups) { expect(stuecke, gruppe.name).toContain(String(gruppe.lanes)); } }); it('nennt die Fahrstreifenzahl weiterhin in der Signalgruppentabelle', () => { // Zusammenhangspruefung: Die Spalte bleibt, sie wechselt nur die // Schreibweise. Ohne sie fehlte n in der Kette qS0 · n · fA / fSV. const stuecke = pdfStuecke(ausdruck(mitGebrochenerFahrstreifenzahl())); expect(stuecke).toContain('Fahrstr.'); }); });