lsa-planer

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

/ tests domain zeichenNichtDruckbar.test.ts

7,7 KB Rohdatei
tests/domain/zeichenNichtDruckbar.test.ts — 167 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { aufDruckzeichen, nichtDruckbareZeichen, zeichenKennung } from '@/domain/druckzeichen';
3 import { toWinAnsi } from '@/render/pdfSurface';
4 import { validateProject } from '@/domain/validation/engine';
5 import { createStandardIntersectionProject } from '@/domain/model/factory';
6 import type { Project } from '@/domain/model/project';
7
8 /**
9 * Was der Ausdruck nicht setzen kann, wird gesagt.
10 *
11 * DER BEFUND. Die PDF-Standardschriften beherrschen Latin-1. Was darueber
12 * hinausgeht, ersetzt das Programm lesbar, wo es geht - ein Pfeil wird zu
13 * "->", ein Haken zu "ja" -, und entfernt den Rest. Das Entfernen ist die
14 * richtige Wahl; ein Fragezeichen mitten im Ortsnamen waere schlechter. STILL
15 * war es falsch: Ein Bearbeiter namens "Łukasz" stand in der abgegebenen
16 * Planunterlage als "ukasz", und niemand erfuhr davon. Betroffen sind gerade
17 * die Buchstaben, die in einem deutschen Planungsvorgang wirklich vorkommen -
18 * polnische, tuerkische, tschechische und ungarische Namen von Personen und
19 * Firmen.
20 *
21 * Aufgefallen ist das bei einer Durchsicht vor der Auslieferung, nicht an einem
22 * Test. Ueberall sonst sagt dieses Programm, wenn es einen Wert ersetzt oder
23 * aufrundet; an dieser einen Stelle schwieg es.
24 *
25 * WAS DIESE DATEI BEWACHT: dass der Vorrat an EINER Stelle steht und beide
26 * Seiten - Ausdruck und Pruefbericht - denselben benutzen, dass die Ersetzungen
27 * weiterhin greifen und nicht als Verlust gemeldet werden, und dass die
28 * Meldung die Stelle und das Zeichen benennt.
29 *
30 * MUTATIONSNACHWEIS (10.09.2026, fuenf Mutationen): Fassung 5.26.0.
31 */
32
33 const DATUM = new Date('2026-09-10T10:00:00Z');
34
35 /** Die Standardvorlage mit einem geaenderten Textfeld. */
36 function mitText(feld: 'planner' | 'location' | 'name', wert: string): Project {
37 const basis = createStandardIntersectionProject('Zeichen', DATUM);
38 return { ...basis, meta: { ...basis.meta, [feld]: wert } };
39 }
40
41 const befund = (project: Project) =>
42 validateProject(project, undefined, DATUM).findings.find(
43 (f) => f.rule === 'projektdaten.zeichen-nicht-druckbar',
44 );
45
46 describe('Der Zeichenvorrat steht an einer Stelle', () => {
47 it('benutzt der Ausdruck denselben wie der Pruefbericht', () => {
48 /*
49 * Der Kern der Aenderung. Vorher stand die Tafel in render/pdfSurface.ts,
50 * und der Pruefbericht kannte sie gar nicht. Zwei Listen liefen
51 * auseinander, sobald jemand eine von beiden ergaenzt haette.
52 */
53 for (const probe of ['Łukasz', 'Straße', 'A → B', 'Ω', '😀', 'Şişli', 'Plzeň']) {
54 expect(toWinAnsi(probe), probe).toBe(aufDruckzeichen(probe));
55 }
56 });
57
58 it('meldet nichts, was der Ausdruck ersetzen kann', () => {
59 // Ein Pfeil ist kein Verlust: Er wird zu "->", und das steht im Ausdruck.
60 expect(nichtDruckbareZeichen('A → B'), 'Pfeil').toEqual([]);
61 expect(aufDruckzeichen('A → B')).toBe('A -> B');
62 expect(nichtDruckbareZeichen('bis 5 ≤ 7'), 'Vergleichszeichen').toEqual([]);
63 expect(nichtDruckbareZeichen('Straße Grünstadt'), 'Umlaute und Eszett').toEqual([]);
64 });
65
66 it('nennt jedes Zeichen genau einmal und in der Reihenfolge des Textes', () => {
67 /*
68 * "Łódź" zweimal: Gemeldet werden Ł und ź, jedes einmal - das ó nicht, es
69 * liegt in Latin-1 und wird gedruckt. Genau diese Grenze soll der Fall
70 * festhalten; beim Schreiben hatte ich das ó zuerst mitgezaehlt und der
71 * Fall hat es berichtigt.
72 */
73 expect(nichtDruckbareZeichen('Łódź Łódź')).toEqual(['Ł', 'ź']);
74 expect(aufDruckzeichen('Łódź'), 'so steht es im Ausdruck').toBe('ód');
75 });
76
77 it('gibt dem Zeichen eine Kennung, die sich drucken laesst', () => {
78 /*
79 * Ohne sie beklagte die Meldung etwas Unsichtbares: Sie steht im
80 * Pruefbericht, der Bericht geht in die Planunterlage, und die kann das
81 * beanstandete Zeichen gerade nicht setzen.
82 */
83 expect(zeichenKennung('Ł')).toBe('U+0141');
84 expect(zeichenKennung('ł')).toBe('U+0142');
85 expect(aufDruckzeichen(zeichenKennung('Ł')), 'die Kennung selbst ist druckbar').toBe('U+0141');
86 });
87 });
88
89 describe('Die Regel projektdaten.zeichen-nicht-druckbar', () => {
90 it('meldet den Bearbeiter, dessen Name im Ausdruck verstuemmelt wuerde', () => {
91 const gefunden = befund(mitText('planner', 'Łukasz Nowak'));
92 expect(gefunden, 'die Meldung').toBeDefined();
93 expect(gefunden!.severity).toBe('warnung');
94 expect(gefunden!.message, 'die Stelle').toContain('Bearbeiter');
95 expect(gefunden!.message, 'das Zeichen').toContain('U+0141');
96 expect(gefunden!.message, 'die Folge').toContain('ersatzlos');
97 // Und die Gegenprobe zur Sache selbst: So sähe es im Ausdruck aus.
98 expect(aufDruckzeichen('Łukasz Nowak')).toBe('ukasz Nowak');
99 });
100
101 it('schweigt bei einem Text, den der Ausdruck ganz setzen kann', () => {
102 expect(befund(mitText('planner', 'Jürgen Groß')), 'Umlaute sind kein Fall').toBeUndefined();
103 expect(befund(createStandardIntersectionProject('Zeichen', DATUM))).toBeUndefined();
104 });
105
106 it('fasst mehrere betroffene Stellen in einer Meldung zusammen', () => {
107 /*
108 * Wer einen Namen aus einer fremden Quelle einfuegt, trifft es meist an
109 * mehreren Stellen zugleich. Zehn gleichlautende Zeilen verdeckten die
110 * uebrigen Befunde.
111 */
112 const basis = createStandardIntersectionProject('Zeichen', DATUM);
113 const projekt: Project = {
114 ...basis,
115 meta: { ...basis.meta, planner: 'Łukasz', location: 'Plzeň' },
116 };
117 const alle = validateProject(projekt, undefined, DATUM).findings.filter(
118 (f) => f.rule === 'projektdaten.zeichen-nicht-druckbar',
119 );
120 expect(alle, 'genau eine Meldung').toHaveLength(1);
121 expect(alle[0]!.message).toContain('Bearbeiter');
122 expect(alle[0]!.message).toContain('Ort und Straßenzug');
123 });
124
125 it('nennt dieselbe Art von Stelle nicht zweimal', () => {
126 /*
127 * Zwei Konfliktbeziehungen tragen dieselbe Bezeichnung der Stelle. Ohne
128 * die Entdopplung stuende sie zweimal im selben Satz, und bei einem
129 * Knotenpunkt mit zweiundsiebzig Beziehungen zweiundsiebzigmal.
130 *
131 * DIESER FALL IST NACHGETRAGEN: Eine Mutation, die die Entdopplung
132 * entfernt, blieb im ersten Anlauf gruen - die uebrigen Faelle benutzen
133 * lauter verschiedene Stellen und pruefen sie deshalb nicht.
134 */
135 const basis = createStandardIntersectionProject('Zeichen', DATUM);
136 const projekt: Project = {
137 ...basis,
138 conflicts: basis.conflicts.map((c, i) => (i < 2 ? { ...c, note: 'Bogen ➜ Ω' } : c)),
139 };
140 const gefunden = validateProject(projekt, undefined, DATUM).findings.find(
141 (f) => f.rule === 'projektdaten.zeichen-nicht-druckbar',
142 )!;
143 const treffer = gefunden.message.match(/Bemerkung einer Konfliktbeziehung/gu) ?? [];
144 expect(treffer, 'die Stelle steht genau einmal im Satz').toHaveLength(1);
145 });
146
147 it('sieht auch die Namen der Signalgruppen an', () => {
148 const basis = createStandardIntersectionProject('Zeichen', DATUM);
149 const projekt: Project = {
150 ...basis,
151 signalGroups: basis.signalGroups.map((g, i) => (i === 0 ? { ...g, name: 'K1 ➜ Ost' } : g)),
152 };
153 const gefunden = validateProject(projekt, undefined, DATUM).findings.find(
154 (f) => f.rule === 'projektdaten.zeichen-nicht-druckbar',
155 );
156 expect(gefunden, 'die Meldung').toBeDefined();
157 expect(gefunden!.message).toContain('Signalgruppe');
158 });
159
160 it('traegt eine eigene Ueberschrift und einen eigenen Rat', () => {
161 // Sonst faellt die Regel im Bericht auf die Sammeltexte zurueck.
162 const gefunden = befund(mitText('name', 'Knoten Ω'))!;
163 expect(gefunden.title).toBe('Zeichen fehlen im Ausdruck');
164 expect(gefunden.suggestion).not.toBe('');
165 expect(gefunden.reference, 'kein Regelwerk vorgeben').toContain('keine Regelwerksfrage');
166 });
167 });