lsa-planer

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

/ tests ui phasenbezeichnung.test.ts

5,2 KB Rohdatei
tests/ui/phasenbezeichnung.test.ts — 124 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { ProjectStore } from '@/app/store';
3 import { createStandardIntersectionProject } from '@/domain/model/factory';
4 import { mitBewahrtemFokus } from '@/ui/fokus';
5 import type { Project } from '@/domain/model/project';
6 import type { ViewContext } from '@/ui/shell';
7 import { phasesView } from '@/ui/views/phasesView';
8
9 /**
10 * Befund 26 zur Fassung 5.10.0.
11 *
12 * Der Empfaenger des Bezeichnungsfeldes schrieb den neuen Namen in den
13 * Speicher, rief aber kein `context.refresh()`. Der Speicher allein zeichnet
14 * nichts neu - `store.subscribe` bedient in der Huelle nur `updateChrome`
15 * (Kopfzeile, Fenstertitel, Abzeichen, Statusleiste). Alles, was aus
16 * `phase.name` gebildet war, blieb deshalb auf dem alten Namen stehen: die
17 * Ueberschrift der Karte, die Namen der Schaltflaechen ↑, ↓ und ×, der Titel
18 * der Loeschrueckfrage und der Uebergangshinweis auf der Karte der
19 * VORHERGEHENDEN Phase.
20 *
21 * Schwer wiegt die Rueckfrage: `removePhase` sagt im Kommentar zu, sie trage
22 * "dieselbe Bezeichnung wie auf der Schaltflaeche" - und nannte damit vor
23 * einem zerstoerenden Schritt einen Namen, den es nicht mehr gibt.
24 *
25 * Die Nachbaransicht macht es richtig: signalGroupsView fasst jede Aenderung -
26 * auch den Namen - in `change()` zusammen, das nach `store.update` auffrischt.
27 */
28
29 const STICHTAG = new Date('2026-01-01T00:00:00Z');
30
31 let aufraeumen: (() => void) | null = null;
32 afterEach(() => {
33 aufraeumen?.();
34 aufraeumen = null;
35 document.body.replaceChildren();
36 });
37
38 /** Ansicht mit demselben Neuaufbau, den die Huelle fuer `refresh` vorsieht. */
39 function zeichne(project: Project): { wurzel: HTMLElement; store: ProjectStore } {
40 const wurzel = document.createElement('div');
41 document.body.append(wurzel);
42 const store = new ProjectStore(project);
43 let dispose: () => void = () => {};
44 const context: ViewContext = {
45 store,
46 refresh: () => {
47 mitBewahrtemFokus(wurzel, () => {
48 dispose();
49 wurzel.replaceChildren();
50 dispose = phasesView.render(wurzel, context);
51 });
52 },
53 navigate: () => {},
54 target: null,
55 };
56 dispose = phasesView.render(wurzel, context);
57 aufraeumen = () => {
58 dispose();
59 wurzel.remove();
60 };
61 return { wurzel, store };
62 }
63
64 /** Das Bezeichnungsfeld der n-ten Phase - ueber seinen zugaenglichen Namen. */
65 function bezeichnungsfeld(wurzel: HTMLElement, nummer: number): HTMLInputElement {
66 const feld = [...wurzel.querySelectorAll<HTMLInputElement>('input[type="text"]')].find(
67 (i) => i.getAttribute('aria-label') === `Bezeichnung der ${nummer}. Phase`,
68 );
69 expect(feld, `Kein Feld "Bezeichnung der ${nummer}. Phase"`).toBeDefined();
70 return feld as HTMLInputElement;
71 }
72
73 /** Namen aller Schaltflaechen der Ansicht. */
74 const knopfnamen = (wurzel: HTMLElement): string[] =>
75 [...wurzel.querySelectorAll('button')].map((b) => b.getAttribute('aria-label') ?? '');
76
77 /** Fette Kopfzeile jeder Uebergangskarte. */
78 const uebergangskoepfe = (wurzel: HTMLElement): string[] =>
79 [...wurzel.querySelectorAll('.hinweisbalken strong')].map((s) => s.textContent ?? '');
80
81 /** Traegt die Bezeichnung ein und verlaesst das Feld (Tabulator oder Klick). */
82 function trageEin(feld: HTMLInputElement, wert: string): void {
83 feld.value = wert;
84 feld.dispatchEvent(new Event('change', { bubbles: true }));
85 }
86
87 describe('Befund 26 - Umbenennen einer Phase frischt die Ansicht auf', () => {
88 it('zieht Ueberschrift und Schaltflaechennamen nach', () => {
89 const { wurzel, store } = zeichne(createStandardIntersectionProject('Umbenennen', STICHTAG));
90 trageEin(bezeichnungsfeld(wurzel, 1), 'Fußgänger Nord');
91
92 expect(store.getProject().phases[0]?.name).toBe('Fußgänger Nord');
93 expect([...wurzel.querySelectorAll('h3')].map((h) => h.textContent)).toContain(
94 '1. Fußgänger Nord',
95 );
96 expect(knopfnamen(wurzel)).toContain('1. Phase "Fußgänger Nord" löschen');
97 expect(knopfnamen(wurzel)).toContain('1. Phase "Fußgänger Nord" nach hinten');
98 expect(knopfnamen(wurzel).some((n) => n.includes('Phase 1 - Nord/Süd'))).toBe(false);
99 });
100
101 it('zieht den Uebergangshinweis auf der Karte der vorhergehenden Phase nach', () => {
102 const { wurzel } = zeichne(createStandardIntersectionProject('Umbenennen', STICHTAG));
103 trageEin(bezeichnungsfeld(wurzel, 2), 'Ost/West mit Furt');
104
105 const koepfe = uebergangskoepfe(wurzel);
106 expect(koepfe.some((k) => k.startsWith('Übergang zu "Ost/West mit Furt": '))).toBe(true);
107 expect(koepfe.some((k) => k.includes('Phase 2 - Ost/West'))).toBe(false);
108 });
109
110 it('laesst den Fokus im Bezeichnungsfeld stehen', () => {
111 // Der Neuaufbau ist an dieser Stelle unschaedlich: Das Ereignis ist
112 // `change` und nicht `input`, und `mitBewahrtemFokus` fuehrt Fokus und
113 // Textmarke nach. Ohne diesen Fall bliebe offen, ob die Auffrischung die
114 // Eingabe stoert.
115 const { wurzel } = zeichne(createStandardIntersectionProject('Umbenennen', STICHTAG));
116 const feld = bezeichnungsfeld(wurzel, 1);
117 feld.focus();
118 trageEin(feld, 'Nordzufahrt');
119
120 const jetzt = document.activeElement as HTMLElement | null;
121 expect(jetzt?.getAttribute('aria-label')).toBe('Bezeichnung der 1. Phase');
122 expect((jetzt as HTMLInputElement | null)?.value).toBe('Nordzufahrt');
123 });
124 });