lsa-planer

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

/ tests ui zeilenkopf.test.ts

6,4 KB Rohdatei
tests/ui/zeilenkopf.test.ts — 145 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { ProjectStore } from '@/app/store';
3 import { createStandardIntersectionProject } from '@/domain/model/factory';
4 import type { Project } from '@/domain/model/project';
5 import type { ViewDefinition } from '@/ui/shell';
6 import { planView } from '@/ui/views/planView';
7 import { simulationView } from '@/ui/views/simulationView';
8
9 /**
10 * Befund 46 zur Fassung 5.10.0 - Zeilenkoepfe in den beiden breiten
11 * Texttabellen.
12 *
13 * "Signalzeiten je Signalgruppe" (elf Spalten) und "Rückstau und Wartezeit je
14 * Signalgruppe" (neun Spalten) fuehrten die Signalgruppe in einem `td`. Damit
15 * gab es in diesen Tabellen kein einziges `tbody th`, und `beschrifteTabelle()`
16 * hatte nichts, dem es `scope="row"` geben konnte: Eine Sprachausgabe im
17 * Tabellenmodus sagt dann je Zelle nur den Spaltenkopf und den Wert an - die
18 * Signalgruppe, auf die sich alle uebrigen Werte der Zeile beziehen, wird nie
19 * wiederholt. Die beiden Matrizen loesen dieselbe Aufgabe seit jeher mit einem
20 * `th` links in jeder Zeile (conflictsView, compatibilityView); die Begruendung
21 * steht in src/ui/dom.ts bei `beschrifteTabelle`.
22 *
23 * Bei "Signalzeiten je Signalgruppe" wiegt das doppelt: Sie ist der
24 * Alternativinhalt, auf den der Beschreibungstext der Zeichenflaeche verweist
25 * ("Dieselben Angaben stehen als Zahlen in der Tabelle ...").
26 *
27 * WARUM DER BESTEHENDE PRUEFSTAND DAS NICHT BEMERKT HAT: Der Fall in
28 * tests/ui/barrierefreiheit.test.ts filtert vorhandene `th` ohne `scope`. Eine
29 * Tabelle ganz ohne Zeilenkopf liefert eine leere Trefferliste und laeuft gruen
30 * durch. Hier wird deshalb die Zahl der Zeilenkoepfe gegen die Zahl der Zeilen
31 * gehalten.
32 */
33
34 const STICHTAG = new Date('2026-01-01T00:00:00Z');
35
36 let aufraeumen: (() => void) | null = null;
37 afterEach(() => {
38 aufraeumen?.();
39 aufraeumen = null;
40 document.body.replaceChildren();
41 });
42
43 function zeichne(view: ViewDefinition, project: Project): HTMLElement {
44 const wurzel = document.createElement('div');
45 document.body.append(wurzel);
46 const schliessen = view.render(wurzel, {
47 store: new ProjectStore(project),
48 refresh: () => {},
49 navigate: () => {},
50 target: null,
51 });
52 aufraeumen = () => {
53 schliessen();
54 wurzel.remove();
55 };
56 return wurzel;
57 }
58
59 /** Tabelle ueber ihre Beschriftung - sie ist ihr zugaenglicher Name. */
60 function tabelle(wurzel: HTMLElement, beschriftung: string): HTMLTableElement {
61 const treffer = [...wurzel.querySelectorAll('table')].find(
62 (t) => (t.querySelector('caption')?.textContent ?? '').trim() === beschriftung,
63 );
64 expect(treffer, `Keine Tabelle "${beschriftung}"`).toBeDefined();
65 return treffer as HTMLTableElement;
66 }
67
68 /**
69 * Prueft, dass JEDE Koerperzeile einen Zeilenkopf mit dem erwarteten Text
70 * traegt - und zwar als erste Zelle.
71 */
72 function pruefeZeilenkoepfe(tab: HTMLTableElement, namen: readonly string[]): void {
73 const zeilen = [...tab.querySelectorAll<HTMLTableRowElement>('tbody tr')];
74 expect(zeilen.length, 'Die Tabelle hat keine Koerperzeile').toBe(namen.length);
75 expect(tab.querySelectorAll('tbody th').length, 'Jede Zeile braucht genau einen Zeilenkopf').toBe(
76 namen.length,
77 );
78 zeilen.forEach((tr, index) => {
79 const erste = tr.children[0];
80 expect(erste?.tagName, `Zeile ${index + 1}`).toBe('TH');
81 expect(erste?.getAttribute('scope'), `Zeile ${index + 1}`).toBe('row');
82 expect((erste?.textContent ?? '').trim()).toBe(namen[index]);
83 });
84 }
85
86 describe('Befund 46 - die Signalgruppe ist der Zeilenkopf ihrer Zeile', () => {
87 it('fuehrt sie im Signalzeitenplan als th scope="row"', () => {
88 const projekt = createStandardIntersectionProject('Zeilenkopf', STICHTAG);
89 const wurzel = zeichne(planView, projekt);
90 pruefeZeilenkoepfe(
91 tabelle(wurzel, 'Signalzeiten je Signalgruppe'),
92 projekt.signalGroups.map((g) => g.name),
93 );
94 });
95
96 it('fuehrt sie in der Simulation als th scope="row"', () => {
97 const projekt = createStandardIntersectionProject('Zeilenkopf', STICHTAG);
98 const wurzel = zeichne(simulationView, projekt);
99 const tab = tabelle(wurzel, 'Rückstau und Wartezeit je Signalgruppe');
100 // Simuliert werden nur Gruppen mit Saettigungsverkehrsstaerke; welche das
101 // sind, sagt die Tabelle selbst - gemessen wird der Zeilenbezug, nicht die
102 // Auswahl.
103 const namen = [...tab.querySelectorAll<HTMLTableRowElement>('tbody tr')].map((tr) =>
104 (tr.children[0]?.textContent ?? '').trim(),
105 );
106 expect(namen.length).toBeGreaterThan(0);
107 pruefeZeilenkoepfe(tab, namen);
108 });
109
110 it('setzt am Zeilenkopf die inline erreichbaren Kopfzeileneigenschaften zurueck', () => {
111 /*
112 * `th { position: sticky; top: 0; z-index: 1; font-size: 0.75rem;
113 * font-weight: 600; color: gedaempft; background: gedaempft;
114 * vertical-align: bottom }` in app.css ist fuer SPALTENkoepfe gedacht und
115 * griffe im Koerper mit: Der Signalgruppenname stuende klein, gedaempft
116 * und klebend in der Zeile. ZEILENKOPF_STIL setzt davon genau fuenf
117 * Eigenschaften zurueck - hier sind alle fuenf gemessen, damit keine von
118 * ihnen unbemerkt aus der Konstante faellt.
119 *
120 * NICHT GEMESSEN, WEIL NICHT GELEISTET: Ein Inline-Stil kennt weder
121 * `:hover` noch `:last-child`. `font-weight: 600` bleibt wirksam, die
122 * Zeilenhervorhebung `tbody tr:hover td` laesst die erste Zelle aus, und
123 * `tbody tr:last-child td { border-bottom: none }` erreicht den
124 * Zeilenkopf nicht. Diese drei Punkte sind erst mit einer Regel
125 * `tbody th` in src/styles/app.css zu haben; die Begruendung steht bei
126 * ZEILENKOPF_STIL. Der Testname sagt deshalb "inline erreichbar" und
127 * nicht "sieht nicht wie ein Spaltenkopf aus".
128 */
129 const projekt = createStandardIntersectionProject('Zeilenkopf', STICHTAG);
130 for (const [view, beschriftung] of [
131 [planView, 'Signalzeiten je Signalgruppe'],
132 [simulationView, 'Rückstau und Wartezeit je Signalgruppe'],
133 ] as const) {
134 const wurzel = zeichne(view, projekt);
135 const kopf = tabelle(wurzel, beschriftung).querySelector<HTMLElement>('tbody th');
136 expect(kopf?.style.position, beschriftung).toBe('static');
137 expect(kopf?.style.fontSize, beschriftung).toBe('inherit');
138 expect(kopf?.style.color, beschriftung).toBe('inherit');
139 expect(kopf?.style.background, beschriftung).toBe('transparent');
140 expect(kopf?.style.verticalAlign, beschriftung).toBe('inherit');
141 aufraeumen?.();
142 aufraeumen = null;
143 }
144 });
145 });