lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests export wegherkunftOhneDatum.test.ts
| 1 | import { inflateSync } from 'node:zlib'; |
| 2 | import { describe, expect, it } from 'vitest'; |
| 3 | import { wegherkunftZelle } from '@/services/export/wegherkunft'; |
| 4 | import { buildProjectPdf, DEFAULT_PDF_OPTIONS } from '@/services/export/pdf'; |
| 5 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 6 | import { validateProject } from '@/domain/validation'; |
| 7 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 8 | import type { Conflict, Project, Wegnachweis } from '@/domain/model/project'; |
| 9 | import * as fmt from '@/ui/format'; |
| 10 | |
| 11 | /* |
| 12 | * Herkunft der Wege ohne ueberlieferten Zeitpunkt (Befund 52). |
| 13 | * |
| 14 | * `wegherkunftZelle` gab bei herkunft = 'lageplan' und uebernommenAm = null die |
| 15 | * blanke Beschriftung "Aus dem Lageplan vermessen" aus. Der Erlaeuterungsabsatz |
| 16 | * unter der Tabelle (src/services/export/pdf.ts) sagt daneben unbedingt zu, die |
| 17 | * Angabe nenne den Zeitpunkt der Uebernahme - fuer diese Zeile eine falsche |
| 18 | * Aussage der Planunterlage ueber ihre eigene Tabelle. Der Zustand entsteht |
| 19 | * beim Einlesen einer fremd erzeugten Datei und ist dort ausdruecklich |
| 20 | * vorgesehen (parseWegnachweis in src/domain/model/schema.ts sammelt nur eine |
| 21 | * Warnung und laesst den Nachweis stehen). |
| 22 | * |
| 23 | * Der Zeitstempel hat einen benannten Zweck: Er soll gegen |
| 24 | * `lageplan.bild.geladenAm` gehalten werden. Fehlt er wortlos, kann ein Pruefer |
| 25 | * nicht unterscheiden, ob er nie ueberliefert wurde oder ob der Druck ihn |
| 26 | * verschluckt hat. Die Zelle sagt es deshalb selbst. |
| 27 | */ |
| 28 | |
| 29 | const DATUM = new Date('2026-01-01T12:00:00Z'); |
| 30 | |
| 31 | function beziehung(wegnachweis: Wegnachweis): Conflict { |
| 32 | const basis = createStandardIntersectionProject('Testknoten', DATUM).conflicts[0]; |
| 33 | expect(basis, 'Vorbedingung: der Regelknotenpunkt fuehrt Beziehungen').toBeDefined(); |
| 34 | return { ...basis!, wegnachweis }; |
| 35 | } |
| 36 | |
| 37 | /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */ |
| 38 | function pdfText(bytes: Uint8Array): string { |
| 39 | const roh = new TextDecoder('latin1').decode(bytes); |
| 40 | const teile: string[] = []; |
| 41 | const suche = /stream\r?\n/g; |
| 42 | let treffer: RegExpExecArray | null; |
| 43 | |
| 44 | while ((treffer = suche.exec(roh)) !== null) { |
| 45 | const start = treffer.index + treffer[0].length; |
| 46 | const ende = roh.indexOf('endstream', start); |
| 47 | if (ende < 0) continue; |
| 48 | let inhalt: string; |
| 49 | try { |
| 50 | inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1'); |
| 51 | } catch { |
| 52 | continue; |
| 53 | } |
| 54 | for (const stueck of inhalt.matchAll(/\((?:\\.|[^\\()])*\)\s*Tj/g)) { |
| 55 | teile.push(stueck[0].slice(1, stueck[0].lastIndexOf(')'))); |
| 56 | } |
| 57 | } |
| 58 | // Lange Absaetze und Tabellenzellen sind auf Zeilen umbrochen; fuer die Suche |
| 59 | // wieder zusammenziehen. Klammern und Schraegstriche stehen im Inhaltsstrom |
| 60 | // maskiert - die Zelle traegt Klammern, also wird die Maskierung entfernt. |
| 61 | return teile |
| 62 | .join(' ') |
| 63 | .replace(/\\([()\\])/g, '$1') |
| 64 | .replace(/\s+/g, ' '); |
| 65 | } |
| 66 | |
| 67 | function ausdruck(project: Project): string { |
| 68 | const plan = buildSignalPlan(project); |
| 69 | const report = validateProject(project, plan, DATUM); |
| 70 | return pdfText( |
| 71 | buildProjectPdf( |
| 72 | project, |
| 73 | plan, |
| 74 | report, |
| 75 | { ...DEFAULT_PDF_OPTIONS, includeCalculationDetails: true }, |
| 76 | DATUM, |
| 77 | ), |
| 78 | ); |
| 79 | } |
| 80 | |
| 81 | describe('Herkunft der Wege: Vermessung ohne ueberlieferten Zeitpunkt', () => { |
| 82 | it('vermerkt in der Zelle, dass der Zeitpunkt der Uebernahme nicht ueberliefert ist', () => { |
| 83 | const zelle = wegherkunftZelle(beziehung({ herkunft: 'lageplan', uebernommenAm: null })); |
| 84 | |
| 85 | expect(zelle).toBe('Aus dem Lageplan vermessen (Zeitpunkt nicht überliefert)'); |
| 86 | // Zuvor stand hier die blanke Beschriftung - und der Absatz daneben |
| 87 | // behauptete, sie nenne den Zeitpunkt. |
| 88 | expect(zelle).not.toBe('Aus dem Lageplan vermessen'); |
| 89 | }); |
| 90 | |
| 91 | it('nennt den Zeitpunkt unveraendert, wenn er ueberliefert ist', () => { |
| 92 | const am = '2026-03-04T09:15:00.000Z'; |
| 93 | const zelle = wegherkunftZelle(beziehung({ herkunft: 'lageplan', uebernommenAm: am })); |
| 94 | |
| 95 | expect(zelle).toBe(`Aus dem Lageplan vermessen (${fmt.dateTime(am)})`); |
| 96 | }); |
| 97 | |
| 98 | it('laesst die beiden anderen Herkunftsstufen unberuehrt', () => { |
| 99 | expect(wegherkunftZelle(beziehung({ herkunft: 'handeingabe', uebernommenAm: null }))).toBe( |
| 100 | 'Von Hand eingetragen', |
| 101 | ); |
| 102 | expect(wegherkunftZelle(beziehung({ herkunft: 'unbestimmt', uebernommenAm: null }))).toBe( |
| 103 | 'Herkunft nicht nachgewiesen', |
| 104 | ); |
| 105 | }); |
| 106 | |
| 107 | it('traegt den Vermerk bis auf das Papier, wo der Erlaeuterungsabsatz den Zeitpunkt zusagt', () => { |
| 108 | const basis = createStandardIntersectionProject('Testknoten', DATUM); |
| 109 | const erste = basis.conflicts[0]; |
| 110 | expect(erste).toBeDefined(); |
| 111 | const project: Project = { |
| 112 | ...basis, |
| 113 | conflicts: basis.conflicts.map((c) => |
| 114 | c.id === erste!.id |
| 115 | ? { ...c, wegnachweis: { herkunft: 'lageplan', uebernommenAm: null } } |
| 116 | : c, |
| 117 | ), |
| 118 | }; |
| 119 | |
| 120 | const text = ausdruck(project); |
| 121 | |
| 122 | // Der Absatz sagt den Zeitpunkt weiterhin unbedingt zu ... |
| 123 | expect(text).toContain('"Aus dem Lageplan vermessen" nennt den Zeitpunkt der Übernahme'); |
| 124 | // ... die Zeile, fuer die er nicht ueberliefert ist, sagt es selbst. |
| 125 | expect(text).toContain('(Zeitpunkt nicht überliefert)'); |
| 126 | }); |
| 127 | }); |