lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests export druckzeitpunkt.test.ts

3,7 KB Rohdatei
tests/export/druckzeitpunkt.test.ts — 96 Zeilen
1 import { inflateSync } from 'node:zlib';
2 import { describe, expect, it } from 'vitest';
3 import { buildProjectPdf, DEFAULT_PDF_OPTIONS } from '@/services/export/pdf';
4 import { buildSignalPlan } from '@/domain/plan/signalPlan';
5 import { validateProject } from '@/domain/validation';
6 import { createStandardIntersectionProject } from '@/domain/model/factory';
7 import { toWinAnsi } from '@/render/pdfSurface';
8 import * as fmt from '@/ui/format';
9 import type { Project } from '@/domain/model/project';
10
11 /*
12 * Befund 62: "Erstellt am" bezeichnete in derselben Unterlage zwei
13 * verschiedene Zeitpunkte - in der Fusszeile jeder Seite den Zeitpunkt der
14 * Ausgabe, auf dem Deckblatt das Anlagedatum des Projekts. Daneben stand auf
15 * demselben Deckblatt "Ausgedruckt am" mit genau dem Wert, den die Fusszeile
16 * "Erstellt am" nannte.
17 *
18 * Was der Anwender sonst erlebt: Er legt ein Projekt im August an, druckt es
19 * im September, und die Unterlage traegt auf Blatt 1 zwei verschiedene
20 * Erstellungsdaten. Welches die Behoerde in ihre Anordnung uebernimmt, bleibt
21 * offen.
22 */
23
24 const ANGELEGT = new Date('2026-08-14T09:00:00Z');
25 const AUSGEDRUCKT = new Date('2026-09-03T20:15:00Z');
26
27 function entpacke(roh: string, von: number): string | null {
28 const anfang = roh.indexOf('stream', von);
29 if (anfang < 0) return null;
30 const start = anfang + (roh.startsWith('stream\r\n', anfang) ? 8 : 7);
31 const ende = roh.indexOf('endstream', start);
32 if (ende < 0) return null;
33 try {
34 return inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1');
35 } catch {
36 return null;
37 }
38 }
39
40 /** Alle Textstuecke der Datei, in der Reihenfolge des Inhaltsstroms. */
41 function textstuecke(bytes: Uint8Array): readonly string[] {
42 const roh = new TextDecoder('latin1').decode(bytes);
43 const raus: string[] = [];
44 for (const treffer of roh.matchAll(/\/Type \/Page\b[^>]*?\/Contents (\d+) 0 R/g)) {
45 const objekt = roh.indexOf(`\n${treffer[1]!} 0 obj`);
46 const inhalt = objekt < 0 ? null : entpacke(roh, objekt);
47 if (inhalt === null) continue;
48 for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) {
49 raus.push(t[1]!.replace(/\\([()\\])/g, '$1'));
50 }
51 }
52 return raus;
53 }
54
55 function unterlage(): Uint8Array {
56 const basis = createStandardIntersectionProject('Musterkreuzung', ANGELEGT);
57 const projekt: Project = {
58 ...basis,
59 meta: { ...basis.meta, planner: 'O. Willerding' },
60 };
61 const plan = buildSignalPlan(projekt);
62 return buildProjectPdf(
63 projekt,
64 plan,
65 validateProject(projekt, plan, AUSGEDRUCKT),
66 DEFAULT_PDF_OPTIONS,
67 AUSGEDRUCKT,
68 );
69 }
70
71 describe('Zeitangaben der Unterlage', () => {
72 it('nennt den Zeitpunkt der Ausgabe in der Fusszeile "Ausgedruckt am", nicht "Erstellt am"', () => {
73 const stuecke = textstuecke(unterlage());
74 const fusszeilen = stuecke.filter((s) => s.includes('Bearbeiter:'));
75
76 expect(fusszeilen.length, 'Vorbedingung: jede Seite traegt eine Fusszeile').toBeGreaterThan(3);
77 for (const zeile of fusszeilen) {
78 expect(zeile.startsWith(toWinAnsi('Ausgedruckt am '))).toBe(true);
79 }
80 });
81
82 it('traegt das Anlagedatum unter derselben Beschriftung wie die Oberflaeche', () => {
83 const stuecke = textstuecke(unterlage());
84
85 // Die Ansicht "Projekt" nennt dasselbe Feld "Angelegt am".
86 expect(stuecke).toContain(toWinAnsi('Angelegt am'));
87 expect(stuecke).toContain(toWinAnsi(fmt.dateTime(ANGELEGT.toISOString())));
88 });
89
90 it('benutzt "Erstellt am" nirgends mehr - die Beschriftung war doppelt belegt', () => {
91 const stuecke = textstuecke(unterlage());
92 const treffer = stuecke.filter((s) => s.includes(toWinAnsi('Erstellt am')));
93
94 expect(treffer, `noch vorhanden: ${treffer.join(' | ')}`).toEqual([]);
95 });
96 });