lsa-planer

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

/ tests ui pruefberichtKopf.test.ts

7,1 KB Rohdatei
tests/ui/pruefberichtKopf.test.ts — 185 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { ProjectStore } from '@/app/store';
3 import { buildSignalPlan } from '@/domain/plan/signalPlan';
4 import { validateProject } from '@/domain/validation';
5 import { createSignalGroup, createStandardIntersectionProject } from '@/domain/model/factory';
6 import type { Project } from '@/domain/model/project';
7 import { nurHinweiseUeberschrift } from '@/ui/inputs';
8 import type { ViewDefinition } from '@/ui/shell';
9 import { reportView } from '@/ui/views/reportView';
10
11 /**
12 * Pruefbericht, Befunde zur Fassung 5.10.0.
13 *
14 * BEFUND 28: Der Zusammenfassungsbalken kannte nur `errorCount` und
15 * `warningCount`. Bei ausschliesslich Hinweisen sagte er "Keine
16 * Beanstandungen" und "Alle geprüften Regeln sind eingehalten", waehrend
17 * derselbe Bildschirm darunter "Hinweise (2)", die Gruppe "Projektdaten (2)"
18 * und im Ansageraum "Alle Beanstandungen – 2 von 2 Einträgen" fuehrte. Der
19 * zweite Satz war dabei nicht nur unscharf, sondern falsch: zwei gepruefte
20 * Regeln haben angeschlagen.
21 *
22 * BEFUND 58: "Keine Fehler, aber 1 Warnungen" - die Zahl stand unbesehen vor
23 * der Mehrzahlform, obwohl das Projekt die Fallunterscheidung mit
24 * `abzeichenwort` (src/ui/shell.ts) bereits fuehrt.
25 *
26 * BEFUND 59: Der Sprungknopf setzte `finding.target.label` ungeprueft zwischen
27 * die Anfuehrungszeichen. Fuer eine Signalgruppe oder Phase ohne Bezeichnung
28 * hiess er 'Zu ""' und nannte kein Ziel mehr, waehrend derselbe Bericht eine
29 * Zeile darueber 'Zu "(ohne Bezeichnung)"' zeigt (engine.ts setzt den
30 * Rueckfall nur bei 'signalgruppen.name-leer').
31 */
32
33 const FESTES_DATUM = new Date('2026-01-01T00:00:00Z');
34
35 function zeichne(
36 view: ViewDefinition,
37 project: Project,
38 ): { wurzel: HTMLElement; aufraeumen: () => void } {
39 const wurzel = document.createElement('div');
40 document.body.append(wurzel);
41 const store = new ProjectStore(project);
42 const aufraeumen = view.render(wurzel, {
43 store,
44 refresh: () => {},
45 navigate: () => {},
46 target: null,
47 });
48 return {
49 wurzel,
50 aufraeumen: () => {
51 aufraeumen();
52 wurzel.remove();
53 },
54 };
55 }
56
57 function mitMeta(p: Project, meta: Partial<Project['meta']>): Project {
58 return { ...p, meta: { ...p.meta, ...meta } };
59 }
60
61 /** Alle vier Pflichtangaben gefuellt: es bleiben genau zwei Hinweise. */
62 function nurHinweise(): Project {
63 const p = mitMeta(createStandardIntersectionProject('Nur Hinweise', FESTES_DATUM), {
64 projectNumber: '2026-001',
65 client: 'Stadt',
66 planner: 'Bearbeiterin',
67 location: 'Hauptstraße',
68 });
69 const bericht = validateProject(p, buildSignalPlan(p), FESTES_DATUM);
70 expect([bericht.errorCount, bericht.warningCount, bericht.hintCount]).toEqual([0, 0, 2]);
71 return p;
72 }
73
74 /** Genau eine offene Warnung: nur der Ort fehlt. */
75 function eineWarnung(): Project {
76 const p = mitMeta(createStandardIntersectionProject('Eine Warnung', FESTES_DATUM), {
77 projectNumber: '2026-002',
78 client: 'Stadt',
79 planner: 'Bearbeiterin',
80 });
81 const bericht = validateProject(p, buildSignalPlan(p), FESTES_DATUM);
82 expect(bericht.errorCount).toBe(0);
83 expect(bericht.warningCount).toBe(1);
84 return p;
85 }
86
87 /**
88 * Eine zusaetzliche Signalgruppe ohne Bezeichnung und ohne Phase - der
89 * kuerzeste Bedienweg zu einem Befundziel mit leerem Namen
90 * ('signalgruppen.ohne-phase', engine.ts).
91 */
92 function gruppeOhneBezeichnung(): Project {
93 const p = createStandardIntersectionProject('Ohne Bezeichnung', FESTES_DATUM);
94 const arm = p.intersection.arms[0];
95 if (arm === undefined) throw new Error('Vorlage ohne Knotenpunktarm');
96 const gruppe = createSignalGroup({ name: '', mode: 'kfz', armId: arm.id, index: 9 });
97 const projekt: Project = { ...p, signalGroups: [...p.signalGroups, gruppe] };
98 const bericht = validateProject(projekt, buildSignalPlan(projekt), FESTES_DATUM);
99 const leer = bericht.findings.filter((f) => f.target !== null && f.target.label === '');
100 expect(leer.length, 'Vorbedingung: mindestens ein Befundziel ohne Namen').toBeGreaterThan(0);
101 return projekt;
102 }
103
104 function kopf(wurzel: ParentNode): string {
105 return wurzel.querySelector<HTMLElement>('.hinweisbalken')?.textContent ?? '';
106 }
107
108 /** Die Ueberschrift des Zusammenfassungsbalkens - die fette Zeile darin. */
109 function kopfzeile(wurzel: ParentNode): string {
110 return wurzel.querySelector<HTMLElement>('.hinweisbalken strong')?.textContent ?? '';
111 }
112
113 function knopfbeschriftungen(wurzel: ParentNode): string[] {
114 return [...wurzel.querySelectorAll<HTMLButtonElement>('button')].map((b) =>
115 (b.textContent ?? '').trim(),
116 );
117 }
118
119 afterEach(() => {
120 document.body.replaceChildren();
121 });
122
123 describe('Prüfbericht: Kopfzeile, Mehrzahl und Sprungziel', () => {
124 it('nennt Hinweise im Kopf, statt sie zu bestreiten (Befund 28)', () => {
125 const { wurzel, aufraeumen } = zeichne(reportView, nurHinweise());
126 try {
127 const text = kopf(wurzel);
128 expect(text, 'der Kopf bestreitet die Liste darunter').not.toContain('Keine Beanstandungen');
129 expect(text, 'zwei Regeln haben angeschlagen').not.toContain(
130 'Alle geprüften Regeln sind eingehalten',
131 );
132 expect(text).toContain('2 Hinweise');
133 // Und zwar mit dem Wortlaut, den auch die Ausgabe-Ansicht fuehrt: Er
134 // steht an EINER Stelle (`nurHinweiseUeberschrift` in src/ui/inputs.ts)
135 // und nicht zweimal getrennt zusammengesetzt.
136 expect(kopfzeile(wurzel)).toBe(nurHinweiseUeberschrift(2));
137 // Der Bildschirm darunter fuehrt dieselben zwei Eintraege.
138 expect(knopfbeschriftungen(wurzel)).toContain('Hinweise (2)');
139 expect(wurzel.textContent).toContain('Projektdaten (2)');
140 } finally {
141 aufraeumen();
142 }
143 });
144
145 it('behält bei einem Bericht ohne jeden Eintrag den Satz "Keine Beanstandungen"', () => {
146 const p = mitMeta(createStandardIntersectionProject('Vollständig', FESTES_DATUM), {
147 projectNumber: '2026-003',
148 client: 'Stadt',
149 planner: 'Bearbeiterin',
150 location: 'Hauptstraße',
151 einsatzzeiten: 'werktags 6-20 Uhr',
152 betriebsverantwortlicher: 'Betrieb, Firma, 0800 1',
153 });
154 const bericht = validateProject(p, buildSignalPlan(p), FESTES_DATUM);
155 expect(bericht.findings.length, 'Vorbedingung: kein einziger Eintrag').toBe(0);
156 const { wurzel, aufraeumen } = zeichne(reportView, p);
157 try {
158 expect(kopf(wurzel)).toContain('Keine Beanstandungen');
159 expect(kopf(wurzel)).toContain('Alle geprüften Regeln sind eingehalten');
160 } finally {
161 aufraeumen();
162 }
163 });
164
165 it('bildet die Einzahl bei genau einer Warnung (Befund 58)', () => {
166 const { wurzel, aufraeumen } = zeichne(reportView, eineWarnung());
167 try {
168 expect(kopf(wurzel)).toContain('Keine Fehler, aber 1 Warnung');
169 expect(kopf(wurzel)).not.toContain('1 Warnungen');
170 } finally {
171 aufraeumen();
172 }
173 });
174
175 it('nennt im Sprungknopf ein Ziel, auch wenn die Bezeichnung leer ist (Befund 59)', () => {
176 const { wurzel, aufraeumen } = zeichne(reportView, gruppeOhneBezeichnung());
177 try {
178 const beschriftungen = knopfbeschriftungen(wurzel);
179 expect(beschriftungen, 'ein Knopf ohne Ziel im Namen').not.toContain('Zu ""');
180 expect(beschriftungen).toContain('Zu "(ohne Bezeichnung)"');
181 } finally {
182 aufraeumen();
183 }
184 });
185 });