lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain zeichenNichtDruckbar.test.ts
| 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 | }); |