lsa-planer

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

/ tests ui hilfetexte.test.ts

7,9 KB Rohdatei
tests/ui/hilfetexte.test.ts — 192 Zeilen
1 import { readFileSync, readdirSync } from 'node:fs';
2 import { join } from 'node:path';
3 import { describe, expect, it } from 'vitest';
4 import { HILFE, glossarEintraege } from '@/ui/help/texte';
5 import type { HilfeSchluessel, Hilfetext } from '@/ui/help/texte';
6 import { parseProject } from '@/domain/model/schema';
7 import { buildSignalPlan } from '@/domain/plan/signalPlan';
8 import { compareCycleTimeMethods } from '@/domain/rilsa/cycle';
9 import { RILSA_DEFAULTS } from '@/domain/rilsa/constants';
10
11 /*
12 * Fassung 5.10.0 - Hilfetexte sind Zusagen.
13 *
14 * Die Sammlung in src/ui/help/texte.ts wird dem Anwender am Feld und im
15 * Glossar ausgegeben; sie leitet seine Handmessung an. Ein Satz, der etwas
16 * anderes sagt als der Fachkern rechnet, ist deshalb kein Schoenheitsfehler,
17 * sondern eine falsche Anweisung. Diese Datei bewacht fuenf solcher Saetze.
18 */
19
20 const DATUM = new Date('2026-01-01T00:00:00Z');
21 const BEISPIELDATEI = 'beispiele/vierarmiger-knotenpunkt.lsap';
22
23 const SCHLUESSEL = Object.keys(HILFE) as HilfeSchluessel[];
24
25 /** Ein Eintrag als Hilfetext - so sind auch die freiwilligen Felder lesbar. */
26 function eintrag(schluessel: HilfeSchluessel): Hilfetext {
27 return HILFE[schluessel];
28 }
29
30 /** Alle Textfelder eines Eintrags in einer Zeichenkette. */
31 function ganzerText(schluessel: HilfeSchluessel): string {
32 const e = eintrag(schluessel);
33 return [e.wasIstDas, e.woher ?? '', e.ueblich ?? '', e.wennFalsch ?? ''].join(' ');
34 }
35
36 describe('Furt mit Mittelinsel: nur der Raeumweg geht ueber die ganze Breite (Befund 15)', () => {
37 /*
38 * intergreen.ts rechnet raw = tue + tr - te mit te = se / ve. Ein zu gross
39 * angesetzter Einfahrweg VERKUERZT die Zwischenzeit - die gefaehrliche
40 * Richtung. Der Einfahrweg endet an der vorderen Kante des
41 * Konfliktbereichs (types.ts, vermessung.ts), an einer Furt also nach null
42 * bis wenigen Metern.
43 */
44 it('weist den Einfahrweg nicht ueber die ganze Furtbreite an', () => {
45 const woher = HILFE.mittelinsel.woher;
46 expect(woher).not.toContain('Einfahrweg werden über die ganze Furtbreite');
47 expect(woher).toContain('Räumweg wird über die ganze Furtbreite');
48 });
49
50 it('sagt, wo der Einfahrweg des Querenden endet', () => {
51 expect(HILFE.mittelinsel.woher).toContain('vorderen Kante des Konfliktbereichs');
52 });
53
54 it('beschraenkt auch die uebliche Angabe auf den Raeumweg', () => {
55 // "Fuer die Eingabe bleibt es bei der vollen Furtbreite" schloss den
56 // Einfahrweg mit ein und nahm die Berichtigung im Feld darueber zurueck.
57 expect(HILFE.mittelinsel.ueblich).not.toContain('Für die Eingabe bleibt es');
58 expect(HILFE.mittelinsel.ueblich).toContain('Räumweg');
59 });
60
61 it('laesst in der ganzen Sammlung keinen Satz stehen, der beides zusammenwirft', () => {
62 // Die Gegenprobe steht im Eintrag "Einfahrweg" selbst: "Im Zweifel also
63 // knapp messen." Kein zweiter Eintrag darf das Gegenteil anweisen.
64 expect(HILFE.einfahrweg.wennFalsch).toContain('knapp messen');
65 for (const schluessel of SCHLUESSEL) {
66 for (const satz of ganzerText(schluessel).split(/(?<=[.!?])\s+/)) {
67 if (!satz.includes('Einfahrweg')) continue;
68 expect(satz, schluessel).not.toMatch(/ganze[nr]? Furtbreite/);
69 }
70 }
71 });
72 });
73
74 describe('Zusatzeinrichtung fuer Blinde: was eine aeltere Projektdatei erwartet (Befund 43)', () => {
75 it('sagt einer aelteren Projektdatei keine Unveraenderlichkeit zu', () => {
76 const ueblich = HILFE.blindenzusatz.ueblich;
77 expect(ueblich).not.toContain('ändert sich also nicht');
78 expect(ueblich).not.toContain('so, wie es vor diesem');
79 });
80
81 it('nennt die Verschiebung, die das Einlesen bewirkt', () => {
82 const ueblich = HILFE.blindenzusatz.ueblich;
83 expect(ueblich).toContain('längere Mindestfreigabezeiten');
84 expect(ueblich).toContain('Umlaufzeit');
85 });
86
87 it('deckt sich mit dem, was die Beispieldatei aus Schema 7 wirklich ergibt', () => {
88 // Belegstelle, nicht Wache: Sie zeigt, dass die neue Aussage stimmt.
89 const { project } = parseProject(JSON.parse(readFileSync(BEISPIELDATEI, 'utf8')), DATUM);
90 for (const gruppe of project.signalGroups.filter((g) => g.mode === 'fuss')) {
91 expect(gruppe.blindenzusatz).toBeUndefined();
92 }
93 const fussgruppen = buildSignalPlan(project).groups.filter((g) => g.mode === 'fuss');
94 expect(fussgruppen.length).toBeGreaterThan(0);
95 for (const gruppe of fussgruppen) {
96 expect(gruppe.times.minGreen).toBeGreaterThan(RILSA_DEFAULTS.minGreen.fuss);
97 }
98 });
99 });
100
101 describe('Kein Hilfeeintrag ohne Zugang (Befund 62)', () => {
102 /** Alle Quelltextdateien ausser der Sammlung selbst. */
103 function quelltext(verzeichnis: string, gesammelt: string[] = []): string[] {
104 for (const eintragImOrdner of readdirSync(verzeichnis, { withFileTypes: true })) {
105 const pfad = join(verzeichnis, eintragImOrdner.name);
106 if (eintragImOrdner.isDirectory()) quelltext(pfad, gesammelt);
107 else if (pfad.endsWith('.ts') && !pfad.replace(/\\/g, '/').endsWith('src/ui/help/texte.ts'))
108 gesammelt.push(pfad);
109 }
110 return gesammelt;
111 }
112
113 it('verlinkt die feste Freigabezeit einer Phase vom Eintrag Freigabezeit aus', () => {
114 expect(HILFE.freigabezeit.siehe).toContain('festeFreigabezeit');
115 });
116
117 it('erreicht jeden Eintrag ueber Glossar, Verweis oder Bedienfeld', () => {
118 const quellen = quelltext('src')
119 .map((pfad) => readFileSync(pfad, 'utf8'))
120 .join('\n');
121 const verlinkt = new Set<string>();
122 for (const schluessel of SCHLUESSEL) {
123 for (const verweis of eintrag(schluessel).siehe ?? []) {
124 if (verweis !== schluessel) verlinkt.add(verweis);
125 }
126 }
127 const imGlossar = new Set<string>(glossarEintraege().map((e) => e.schluessel));
128
129 for (const schluessel of SCHLUESSEL) {
130 const amBedienfeld = quellen.includes(`'${schluessel}'`);
131 expect(
132 imGlossar.has(schluessel) || verlinkt.has(schluessel) || amBedienfeld,
133 `${schluessel} ist von keiner Stelle des Programms aus erreichbar`,
134 ).toBe(true);
135 }
136 });
137 });
138
139 describe('Rechtsgrundlagen: wo die eigene Fundstelle eingetragen wird (Befund 63)', () => {
140 it('schickt nicht in das Fundstellenverzeichnis, das nur anzeigt', () => {
141 expect(HILFE.rechtsgrundlagen.woher).not.toContain('tragen Sie dort ein');
142 });
143
144 it('nennt den Ort, an dem das Eingabefeld wirklich steht', () => {
145 const woher = HILFE.rechtsgrundlagen.woher;
146 expect(woher).toContain('Vorgaben');
147 expect(woher).toContain('Herkunft');
148 });
149 });
150
151 describe('Verfahren zur Umlaufzeitermittlung: Einleitung und Aufzaehlung (Befund 64)', () => {
152 const ZAHLWOERTER: Readonly<Record<string, number>> = {
153 zwei: 2,
154 drei: 3,
155 vier: 4,
156 fünf: 5,
157 sechs: 6,
158 };
159
160 function punkte(): readonly string[] {
161 return HILFE.verfahren.wasIstDas.split('\n').filter((zeile) => zeile.startsWith('• '));
162 }
163
164 it('nennt in der Einleitung keine Zahl, die von der Aufzaehlung abweicht', () => {
165 const einleitung = HILFE.verfahren.wasIstDas.split('\n')[0] ?? '';
166 for (const [wort, zahl] of Object.entries(ZAHLWOERTER)) {
167 if (!new RegExp(`\\b${wort}\\b`, 'i').test(einleitung)) continue;
168 expect(zahl, `Einleitung nennt "${wort}", aufgezählt sind ${punkte().length}`).toBe(
169 punkte().length,
170 );
171 }
172 });
173
174 it('fuehrt jeden Rechenansatz des Fachkerns als eigenen Punkt, dazu die feste Vorgabe', () => {
175 // Die Ansaetze kommen aus dem Fachkern, nicht aus einer zweiten Liste:
176 // Wer dort ein Verfahren ergaenzt, bringt diesen Fall zu Fall.
177 const beschriftung: Readonly<Record<string, string>> = {
178 webster: 'Webster',
179 akcelik: 'Akçelik',
180 hbs: 'HBS',
181 hcm: 'HCM',
182 };
183 const ansaetze = Object.keys(
184 compareCycleTimeMethods({ lostTime: 12, criticalFlowRatios: [0.3, 0.3] }).results,
185 );
186 for (const ansatz of ansaetze) {
187 expect(HILFE.verfahren.wasIstDas).toContain(`• ${beschriftung[ansatz] ?? ansatz}`);
188 }
189 expect(HILFE.verfahren.wasIstDas).toContain('• Feste Vorgabe');
190 expect(punkte()).toHaveLength(ansaetze.length + 1);
191 });
192 });