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 { ASSISTENT_VORGABE, projektAusAngaben } from '@/ui/assistent'; import type { Project } from '@/domain/model/project'; /* * STUFE 1 DES SIGNALLAGEPLAN-ENTWURFS: DIE FREMDEN UNTERLAGEN BEKOMMEN EINEN * NAMEN, DIE LUECKE BLEIBT EINE LUECKE. * * Die Uebersicht "Bestandteile der Anordnungsunterlage" sagt seit Befund C14, * was diese Unterlage NICHT enthaelt - den bemassten Signallageplan und den * Verkehrszeichenplan. Das war richtig und blieb doch die halbe Auskunft: Wer * die Unterlage einreicht, legt einen Verkehrszeichenplan daneben, und der * Sachbearbeiter musste die Zuordnung aus dem Anschreiben rekonstruieren. * * DIE GEFAHR DABEI IST DIE UMGEKEHRTE. Ein Verweis an der falschen Stelle * liest sich wie ein Haken: Stuende die Kennung vorn, uebersaehe ein Pruefer * beim Ueberfliegen das "NICHT Bestandteil". Diese Datei bewacht deshalb nicht * nur, DASS der Verweis erscheint, sondern auch, dass die Zeile weiterhin mit * der Absage beginnt. * * ZWEITE GEFAHR: die Regelplannummer. Das Programm hat die RSA 21 nicht im * Original; jede Nummer, die es selbst vorschluege, waere der Fall des * Befunds D4 (geratene Fundstellennummern) - derselbe, aus dem die Typklassenmeldung * in 5.41.0 ihren Vorbehalt bekam. Die Nummer kommt vom Anwender, und der * Ausdruck sagt das. */ const DATUM = new Date('2026-09-12T09:00:00Z'); /** Der sichtbare Text des Ausdrucks, aus den Textbefehlen des PDF gelesen. */ 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 stueck of inhalt.matchAll(/\((?:\\.|[^\\()])*\)\s*Tj/g)) { teile.push(stueck[0].slice(1, stueck[0].lastIndexOf(')'))); } } return teile.join(' ').replace(/\s+/g, ' '); } /** Eine echte Engstelle aus dem gefuehrten Einstieg, mit geaenderten Projektdaten. */ function ausdruck(meta: Partial): string { const basis = projektAusAngaben( { ...ASSISTENT_VORGABE, form: 'einstreifig' }, 'Arbeitsstelle Musterweg', DATUM, ); const project: Project = { ...basis, meta: { ...basis.meta, ...meta } }; const plan = buildSignalPlan(project); const report = validateProject(project, plan, DATUM); return pdfText(buildProjectPdf(project, plan, report, DEFAULT_PDF_OPTIONS, DATUM)); } describe('Verweise auf Unterlagen, die dieses Programm nicht erzeugt', () => { it('nennt den Verkehrszeichenplan beim Namen, sobald einer eingetragen ist', () => { expect(ausdruck({ verkehrszeichenplan: 'VZP 2027-14, Stand 03.05.2027' })).toContain( 'VZP 2027-14, Stand 03.05.2027', ); }); it('sagt trotz Verweis weiterhin, dass der Plan NICHT Bestandteil ist', () => { /* * Der Kern der Uebersicht. Aus "fehlt" darf nicht "enthalten" werden, * sondern nur "fehlt hier, liegt als X bei" - und die Absage steht vorn, * weil sie sonst beim Ueberfliegen untergeht. */ const gedruckt = ausdruck({ verkehrszeichenplan: 'VZP 2027-14' }); const verweis = gedruckt.indexOf('bezieht sich auf'); expect(verweis, 'der Verweis fehlt').toBeGreaterThanOrEqual(0); const absage = gedruckt.lastIndexOf('NICHT Bestandteil', verweis); expect(absage, 'vor dem Verweis steht keine Absage').toBeGreaterThanOrEqual(0); // In derselben Zelle und nicht irgendwo davor: Die Absage gehoert an den // Anfang der Zeile, die den Verweis traegt. expect(verweis - absage, 'die Absage steht zu weit vor dem Verweis').toBeLessThan(120); expect(gedruckt.slice(verweis, verweis + 40)).toContain('VZP 2027-14'); }); it('sagt ohne Eintrag genau das, was es vorher sagte', () => { // Gegenprobe: Wer nichts eintraegt, bekommt keine neue Zeile und keinen // halben Satz, sondern die bekannte Absage. const gedruckt = ausdruck({}); expect(gedruckt).toContain('NICHT Bestandteil dieser Unterlage'); expect(gedruckt).not.toContain('bezieht sich auf'); }); it('druckt Regelplan und Zustimmung nur, wenn sie eingetragen sind', () => { const ohne = ausdruck({}); expect(ohne).not.toContain('Regelplan der RSA 21'); expect(ohne).not.toContain('Zustimmung des Straßenbaulastträgers'); const mit = ausdruck({ regelplan: 'B I/5', zustimmungBaulasttraeger: 'LBM Az. 4711-2027' }); expect(mit).toContain('B I/5'); expect(mit).toContain('LBM Az. 4711-2027'); }); it('weist die Regelplannummer als Angabe des Planers aus', () => { /* * Ohne diesen Zusatz laese sich die Zeile als Aussage des Programms ueber * die RSA 21 - und das Programm hat sie nicht im Original. Es gibt die * Nummer unveraendert wieder und stellt sich nicht dahinter. */ expect(ausdruck({ regelplan: 'B I/5' })).toContain('Angabe des Planers'); }); });