lsa-planer

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

/ tests ui verborgenBleibtVerborgen.test.ts

5,1 KB Rohdatei
tests/ui/verborgenBleibtVerborgen.test.ts — 132 Zeilen
1 import { readFileSync } from 'node:fs';
2 import { afterEach, describe, expect, it } from 'vitest';
3 import { ProjectStore } from '@/app/store';
4 import { createStandardIntersectionProject } from '@/domain/model/factory';
5 import { exportView, RECHENWEG_SCHALTER } from '@/ui/views/exportView';
6
7 /**
8 * Was `hidden` traegt, ist auch verborgen.
9 *
10 * BEFUND AUS DEM BETRIEB (11.09.2026, mit Bildschirmfoto): In der Ansicht
11 * "Ausgabe" stand der Warnbalken "Rechenweg abgewaehlt" dauerhaft da - auch bei
12 * gesetztem Haekchen, also waehrend der Rechenweg gedruckt wurde. Die Ansicht
13 * blendet ihn ueber das HTML-Merkmal `hidden` aus, und ihre Logik dabei ist
14 * richtig. Nur wirkt das Merkmal allein ueber die Regel `[hidden] { display:
15 * none }` des Browsers, und `.hinweisbalken { display: flex }` schlaegt sie:
16 * Eine Klassenregel hat hoehere Spezifitaet als die Regel eines
17 * Merkmalsselektors aus der Vorgabestilvorlage.
18 *
19 * WARUM DIE STILVORLAGE HIER GEPRUEFT WIRD UND NICHT DIE ANZEIGE. Unter jsdom
20 * gibt es kein Stylesheet - `getComputedStyle` liefert dort nichts, woran sich
21 * dieser Fehler zeigen liesse. Nachweisbar ist er nur an den beiden Tatsachen,
22 * aus denen er folgt: Der Balken traegt eine Klasse mit eigenem `display`, und
23 * die Stilvorlage stellt `[hidden]` darueber. Faellt eine der beiden weg, ist
24 * der Fehler zurueck.
25 */
26
27 const STIL = readFileSync('src/styles/app.css', 'utf8');
28 const FESTES_DATUM = new Date('2026-01-01T00:00:00Z');
29
30 let aufraeumen: () => void = () => {};
31
32 function zeichne() {
33 const wurzel = document.createElement('div');
34 document.body.append(wurzel);
35 const abmelden = exportView.render(wurzel, {
36 store: new ProjectStore(createStandardIntersectionProject('Ausgabe', FESTES_DATUM)),
37 refresh: () => {},
38 navigate: () => {},
39 target: null,
40 });
41 aufraeumen = () => {
42 abmelden();
43 wurzel.remove();
44 };
45 return wurzel;
46 }
47
48 afterEach(() => {
49 aufraeumen();
50 document.body.replaceChildren();
51 });
52
53 /** Die Regel eines Selektors, roh aus der Stilvorlage. */
54 function regel(selektor: string): string | null {
55 const treffer = new RegExp(
56 `(^|\\})\\s*${selektor.replace(/[.[\]*]/g, (z) => `\\${z}`)}\\s*\\{([^}]*)\\}`,
57 'm',
58 ).exec(STIL);
59 return treffer?.[2] ?? null;
60 }
61
62 describe('Die Stilvorlage stellt hidden ueber jede Klassenregel', () => {
63 it('gibt es die Regel ueberhaupt', () => {
64 expect(regel('[hidden]'), 'keine Regel fuer [hidden] in app.css').not.toBeNull();
65 });
66
67 it('setzt sie display auf none', () => {
68 expect(regel('[hidden]')).toMatch(/display:\s*none/);
69 });
70
71 it('setzt sie es mit !important', () => {
72 // Ohne das gewinnt jede Klassenregel mit eigenem `display`, und genau daran
73 // scheiterte der Warnbalken der Ausgabe.
74 expect(regel('[hidden]')).toMatch(/display:\s*none\s*!important/);
75 });
76 });
77
78 describe('Warum es diese Regel braucht', () => {
79 it('setzt der Hinweisbalken ein eigenes display', () => {
80 // Die Tatsache, aus der der Fehler folgte. Faellt sie eines Tages weg, ist
81 // die Regel oben trotzdem richtig - aber dieser Fall sagt dann, dass der
82 // Anlass ein anderer geworden ist.
83 expect(regel('.hinweisbalken')).toMatch(/display:\s*flex/);
84 });
85
86 it('traegt der Warnbalken der Ausgabe genau diese Klasse', () => {
87 const wurzel = zeichne();
88 const balken = [...wurzel.querySelectorAll('.hinweisbalken')].find((k) =>
89 (k.textContent ?? '').includes('Rechenweg abgewählt'),
90 );
91 expect(balken, 'Der Warnbalken fehlt in der Ansicht').not.toBeUndefined();
92 });
93 });
94
95 describe('Der Warnbalken folgt dem Haekchen', () => {
96 function balkenUndSchalter(wurzel: ParentNode) {
97 const balken = [...wurzel.querySelectorAll<HTMLElement>('.hinweisbalken')].find((k) =>
98 (k.textContent ?? '').includes('Rechenweg abgewählt'),
99 );
100 // Die Beschriftung ist ein Geschwister des Feldes, kein Elternteil - der
101 // Weg fuehrt deshalb ueber `for` und die Kennung.
102 const beschriftung = [...wurzel.querySelectorAll('label')].find((l) =>
103 (l.textContent ?? '').includes(RECHENWEG_SCHALTER),
104 );
105 const schalter = wurzel.querySelector<HTMLInputElement>(
106 `input[id="${beschriftung?.getAttribute('for') ?? ''}"]`,
107 );
108 return { balken, schalter };
109 }
110
111 it('ist er verborgen, solange der Rechenweg ausgewaehlt ist', () => {
112 // Der Regelfall: Das Haekchen steht, der Rechenweg wird gedruckt, und die
113 // Warnung davor hat nichts zu suchen.
114 const { balken, schalter } = balkenUndSchalter(zeichne());
115 expect(schalter?.checked, 'Der Rechenweg ist nicht vorausgewaehlt').toBe(true);
116 expect(balken?.hidden).toBe(true);
117 });
118
119 it('erscheint er beim Abwaehlen und verschwindet beim Wiederanwaehlen', () => {
120 const wurzel = zeichne();
121 const { balken, schalter } = balkenUndSchalter(wurzel);
122 expect(schalter, 'Kein Schalter fuer den Rechenweg').not.toBeNull();
123
124 schalter!.checked = false;
125 schalter!.dispatchEvent(new Event('change', { bubbles: true }));
126 expect(balken?.hidden).toBe(false);
127
128 schalter!.checked = true;
129 schalter!.dispatchEvent(new Event('change', { bubbles: true }));
130 expect(balken?.hidden).toBe(true);
131 });
132 });