lsa-planer

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

/ tests ui vertraeglichkeitFokus.test.ts

5,0 KB Rohdatei
tests/ui/vertraeglichkeitFokus.test.ts — 130 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import * as actions from '@/app/actions';
3 import { ProjectStore } from '@/app/store';
4 import { createStandardIntersectionProject } from '@/domain/model/factory';
5 import type { Project } from '@/domain/model/project';
6 import type { ViewContext } from '@/ui/shell';
7 import { compatibilityView } from '@/ui/views/compatibilityView';
8
9 /*
10 * Punkt X.4 - bestaendiger Fokusschluessel fuer die Vertraeglichkeitsmatrix.
11 *
12 * In dieser Matrix IST die Beschriftung des Knopfes der Zustand ("feindlich"
13 * oder "verträglich"), und `ariaLabel` nennt denselben Zustand. Der allgemeine
14 * Schluessel aus `benenneBedienelemente()` setzt sich unter anderem aus dem
15 * zugaenglichen Namen zusammen - er kippt damit bei genau dem Klick, der den
16 * Neuaufbau ausloest, und die Fokusrettung faellt fuer bis zu 120 Knoepfe auf
17 * den schwaecheren Strukturweg zurueck.
18 *
19 * GEPRUEFT WIRD DER SCHLUESSEL, NICHT DAS SICHTBARE ERGEBNIS: Sichtbar geht der
20 * Fokus heute nicht verloren, weil die Struktur der Ansicht beim Umschalten
21 * gleich bleibt und der Strukturweg deshalb greift. Die Zusage, die hier haelt,
22 * ist die des Schluessels selbst.
23 */
24
25 let aufraeumen: (() => void) | null = null;
26 afterEach(() => {
27 aufraeumen?.();
28 aufraeumen = null;
29 document.body.replaceChildren();
30 });
31
32 interface Ansicht {
33 readonly wurzel: HTMLElement;
34 readonly store: ProjectStore;
35 readonly neuZeichnen: () => void;
36 }
37
38 /**
39 * Zeichnet die Ansicht und verdrahtet `refresh` mit einem echten Neuaufbau -
40 * so, wie die Schale es tut. Ohne das bliebe die Matrix nach dem Klick stehen,
41 * und der Schluessel waere gar nicht neu vergeben.
42 */
43 function zeichne(project: Project): Ansicht {
44 const wurzel = document.createElement('div');
45 document.body.append(wurzel);
46 const store = new ProjectStore(project);
47 let schliessen: (() => void) | null = null;
48
49 const neuZeichnen = (): void => {
50 schliessen?.();
51 schliessen = compatibilityView.render(wurzel, context);
52 };
53 const context: ViewContext = {
54 store,
55 refresh: neuZeichnen,
56 navigate: () => {},
57 target: null,
58 };
59
60 neuZeichnen();
61 aufraeumen = () => {
62 schliessen?.();
63 wurzel.remove();
64 };
65 return { wurzel, store, neuZeichnen };
66 }
67
68 /** Der Umschaltknopf der Zelle in Zeile `i`, Spalte `j` der Matrix. */
69 function knopfAn(wurzel: HTMLElement, i: number, j: number): HTMLButtonElement {
70 const zeile = wurzel.querySelectorAll<HTMLTableRowElement>('table.matrix tbody tr')[i];
71 expect(zeile, `Keine Matrixzeile ${i}`).toBeDefined();
72 // Erste Zelle der Zeile ist der Zeilenkopf, danach je Signalgruppe eine.
73 const zelle = zeile?.children[1 + j];
74 const knopf = zelle?.querySelector('button') ?? null;
75 expect(knopf, `Kein Umschaltknopf in Zeile ${i}, Spalte ${j}`).not.toBeNull();
76 return knopf as HTMLButtonElement;
77 }
78
79 const fokus = (knopf: HTMLElement): string => knopf.getAttribute('data-fokus') ?? '';
80
81 describe('X.4: Fokusschluessel der Vertraeglichkeitsmatrix', () => {
82 it('ueberlebt den Zustandswechsel, den der Klick selbst ausloest', () => {
83 const { wurzel } = zeichne(createStandardIntersectionProject());
84 const vorher = knopfAn(wurzel, 0, 1);
85 const schluesselVorher = fokus(vorher);
86 expect(schluesselVorher, 'Der Knopf traegt gar keinen Fokusschluessel').not.toBe('');
87 const zustandVorher = (vorher.textContent ?? '').trim();
88
89 vorher.click();
90
91 const nachher = knopfAn(wurzel, 0, 1);
92 expect(
93 (nachher.textContent ?? '').trim(),
94 'der Klick hat den Zustand nicht umgeschaltet',
95 ).not.toBe(zustandVorher);
96 expect(fokus(nachher), 'der Schluessel kippt mit dem Zustand').toBe(schluesselVorher);
97 });
98
99 it('haengt an den Kennungen der beiden Signalgruppen, nicht an ihren Bezeichnungen', () => {
100 const { wurzel, store, neuZeichnen } = zeichne(createStandardIntersectionProject());
101 const gruppen = store.getProject().signalGroups;
102 const zeile = gruppen[0];
103 const spalte = gruppen[1];
104 expect(zeile, 'zu wenige Signalgruppen').toBeDefined();
105 expect(spalte, 'zu wenige Signalgruppen').toBeDefined();
106
107 const schluesselVorher = fokus(knopfAn(wurzel, 0, 1));
108 expect(schluesselVorher).toContain(zeile?.id ?? '');
109 expect(schluesselVorher).toContain(spalte?.id ?? '');
110
111 // Bezeichnungen sind freier Text und jederzeit aenderbar; das Paar bleibt
112 // dasselbe.
113 store.update((p) => actions.updateSignalGroup(p, zeile?.id ?? '', { name: 'Nord links' }), {
114 label: 'Bezeichnung geändert',
115 });
116 neuZeichnen();
117
118 expect(fokus(knopfAn(wurzel, 0, 1)), 'der Schluessel haengt an der Bezeichnung').toBe(
119 schluesselVorher,
120 );
121 });
122
123 it('vergibt fuer jedes Paar einen eigenen Schluessel', () => {
124 const { wurzel } = zeichne(createStandardIntersectionProject());
125 const alle = [...wurzel.querySelectorAll<HTMLElement>('table.matrix [data-fokus]')].map(fokus);
126
127 expect(alle.length, 'keine Knoepfe mit Fokusschluessel gefunden').toBeGreaterThan(1);
128 expect(new Set(alle).size, `Doppelte Schluessel: ${alle.join(' / ')}`).toBe(alle.length);
129 });
130 });