lsa-planer

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

/ tests ui navigationsabzeichen.test.ts

7,4 KB Rohdatei
tests/ui/navigationsabzeichen.test.ts — 173 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { Shell, abzeicheninhalt, abzeichenwort, type Abzeichen } from '@/ui/shell';
3 import { ProjectStore } from '@/app/store';
4 import { createEmptyProject } from '@/domain/model/factory';
5 import { zugaenglicherName } from './zugaenglicherName';
6
7 /**
8 * Das Abzeichen an den Navigationseintraegen
9 * (Befund M11).
10 *
11 * Das Abzeichen zeigte nur eine Zahl. Ob sie Fehler oder Warnungen zaehlte,
12 * stand ausschliesslich in der Hintergrundfarbe - und die beiden Toene stehen
13 * zueinander bei 1,10:1 (hell #b3261e gegen #8a5a00) beziehungsweise 1,47:1
14 * (dunkel #ff8a80 gegen #ffc65c). Bei Farbfehlsichtigkeit, im
15 * Schwarzweissdruck der Oberflaeche und auf einem schlecht eingestellten
16 * Schirm ist das dasselbe Feld; das verletzt 1.4.1 (Stufe A). Fuer die
17 * Sprachausgabe war es noch deutlicher: der Knopf hiess gemessen
18 * "5 Phasen 1" - zwei nackte Zahlen um einen Namen herum.
19 *
20 * Gegen den Altstand schlagen alle Faelle fehl: Dort war der Inhalt des
21 * Abzeichens ausschliesslich `String(value.count)`.
22 */
23
24 const FESTES_DATUM = new Date('2026-01-01T00:00:00Z');
25
26 let laufend: Shell | null = null;
27
28 afterEach(() => {
29 laufend?.stop();
30 laufend = null;
31 document.body.replaceChildren();
32 });
33
34 /** Eine Shell mit drei Ansichten: Fehler, Warnung, nichts. */
35 function baueShell(): HTMLElement {
36 const wurzel = document.createElement('div');
37 document.body.append(wurzel);
38
39 const store = new ProjectStore(createEmptyProject('Musterkreuzung', FESTES_DATUM));
40 const shell = new Shell(wurzel, store, '5.6.0');
41 const ansicht = (id: string, label: string, badge: Abzeichen | null) => ({
42 id,
43 label,
44 group: 'Prüfung',
45 description: `Nur zum Aufbau der Shell: ${label}`,
46 badge: () => badge,
47 render: () => () => {},
48 });
49 shell.addView(ansicht('mit-fehlern', 'Phasen', { count: 1, kind: 'fehler' }));
50 shell.addView(ansicht('mit-warnungen', 'Prüfbericht', { count: 3, kind: 'warnung' }));
51 shell.addView(ansicht('ohne', 'Einstellungen', null));
52 shell.start('ohne');
53 laufend = shell;
54 return wurzel;
55 }
56
57 function knopf(wurzel: HTMLElement, label: string): HTMLButtonElement {
58 const treffer = [...wurzel.querySelectorAll<HTMLButtonElement>('button.nav-knopf')].find(
59 (b) => b.querySelector('.nav-beschriftung')?.textContent === label,
60 );
61 expect(treffer, `Navigationseintrag "${label}"`).toBeDefined();
62 return treffer!;
63 }
64
65 describe('Navigationsabzeichen: Fehler und Warnung ohne Farbe unterscheidbar', () => {
66 it('setzt am Fehlerabzeichen ein Ausrufezeichen und am Warnungsabzeichen keines', () => {
67 // Dasselbe Formmerkmal benutzt die Zwischenzeitenmatrix seit Fassung 5.4.0
68 // ("0 !" statt einer blanken 0).
69 const wurzel = baueShell();
70 const fehler = knopf(wurzel, 'Phasen').querySelector<HTMLElement>('.nav-abzeichen')!;
71 const warnung = knopf(wurzel, 'Prüfbericht').querySelector<HTMLElement>('.nav-abzeichen')!;
72
73 expect(fehler.querySelector('.nav-abzeichen-zeichen')?.textContent).toBe('!');
74 expect(warnung.querySelector('.nav-abzeichen-zeichen')).toBeNull();
75
76 // Die Zahl selbst bleibt, wo sie war.
77 expect(fehler.querySelector('.nav-abzeichen-zahl')?.textContent).toBe('1');
78 expect(warnung.querySelector('.nav-abzeichen-zahl')?.textContent).toBe('3');
79 });
80
81 it('unterscheidet die beiden Abzeichen auch ohne Stilblatt und ohne Farbe', () => {
82 // Die eigentliche Zusage: Nimmt man Klassen und Farben weg - Ausdruck in
83 // Schwarzweiss, Kontrastdarstellung, Farbfehlsichtigkeit -, muessen sich
84 // die beiden Abzeichen immer noch unterscheiden.
85 const wurzel = baueShell();
86 const sichtbar = (label: string): string => {
87 const abzeichen = knopf(wurzel, label).querySelector<HTMLElement>('.nav-abzeichen')!;
88 return [...abzeichen.children]
89 .filter((kind) => !kind.classList.contains('nur-vorlesen'))
90 .map((kind) => kind.textContent)
91 .join('');
92 };
93 expect(sichtbar('Phasen')).toBe('1!');
94 expect(sichtbar('Prüfbericht')).toBe('3');
95 });
96
97 it('haelt das Ausrufezeichen aus dem Vorlesetext heraus und setzt das Wort daneben', () => {
98 const wurzel = baueShell();
99 const fehler = knopf(wurzel, 'Phasen');
100 const warnung = knopf(wurzel, 'Prüfbericht');
101
102 // Das Zeichen ersetzt die Farbe, es ist kein Wort - eine Sprachausgabe
103 // soll es nicht mitlesen.
104 expect(fehler.querySelector('.nav-abzeichen-zeichen')?.getAttribute('aria-hidden')).toBe(
105 'true',
106 );
107
108 // Der zugaengliche Name eines Knopfes ist sein Textinhalt. Er darf nicht
109 // mehr auf einer nackten Zahl enden.
110 expect(zugaenglicherName(fehler)).toContain('Fehler');
111 expect(zugaenglicherName(warnung)).toContain('Warnungen');
112 expect(zugaenglicherName(fehler)).not.toMatch(/\d\s*$/);
113 expect(zugaenglicherName(warnung)).not.toMatch(/\d\s*$/);
114 });
115
116 it('nennt Schritt, Ansicht und Abzeichen getrennt voneinander', () => {
117 /*
118 * NACHTRAG (Fassung 5.7.0, Rest zu Befund M11): Gemessen hiess der
119 * Knopf "1Phasen1 Fehler" - die Navigationsnummer klebte vorn am Namen,
120 * die Zahl des Abzeichens hinten. Der Name eines Knopfes reiht die Texte
121 * seiner Kinder ohne Trennung aneinander; wer zuhoert, bekam drei Angaben
122 * als ein Wort.
123 *
124 * Die Nummer ist deshalb `aria-hidden` und wird ausgeschrieben ersetzt.
125 * Ueber die Listenposition allein waere sie nicht zu ersetzen: Sie laeuft
126 * ueber alle Gruppen durch, die Liste beginnt in jeder Gruppe neu.
127 */
128 const wurzel = baueShell();
129 const fehler = knopf(wurzel, 'Phasen');
130
131 expect(zugaenglicherName(fehler)).toBe('Schritt 1, Phasen, 1 Fehler');
132 expect(zugaenglicherName(knopf(wurzel, 'Prüfbericht'))).toBe(
133 'Schritt 2, Prüfbericht, 3 Warnungen',
134 );
135 // Ohne Abzeichen bleibt es bei Schritt und Ansicht.
136 expect(zugaenglicherName(knopf(wurzel, 'Einstellungen'))).toBe('Schritt 3, Einstellungen');
137
138 // Die sichtbare Nummer bleibt sichtbar - sie zeigt die
139 // Bearbeitungsreihenfolge.
140 expect(fehler.querySelector('.nav-nummer')?.textContent).toBe('1');
141 });
142
143 it('nennt die Mehrzahl richtig', () => {
144 expect(abzeichenwort({ count: 1, kind: 'warnung' })).toBe('Warnung');
145 expect(abzeichenwort({ count: 2, kind: 'warnung' })).toBe('Warnungen');
146 // "Fehler" hat im Deutschen keine eigene Mehrzahlform.
147 expect(abzeichenwort({ count: 1, kind: 'fehler' })).toBe('Fehler');
148 expect(abzeichenwort({ count: 4, kind: 'fehler' })).toBe('Fehler');
149 });
150
151 it('laesst das Abzeichen leer und verborgen, wenn nichts zu melden ist', () => {
152 const wurzel = baueShell();
153 const ohne = knopf(wurzel, 'Einstellungen').querySelector<HTMLElement>('.nav-abzeichen')!;
154 expect(ohne.hidden).toBe(true);
155 expect(ohne.textContent).toBe('');
156 expect(ohne.className).toBe('nav-abzeichen');
157 });
158
159 it('baut den Inhalt in der Reihenfolge Zahl, Zeichen, Wort auf', () => {
160 // Die Reihenfolge bestimmt, was die Sprachausgabe hoert: erst der Wert,
161 // dann seine Art - nicht umgekehrt.
162 const teile = abzeicheninhalt({ count: 2, kind: 'fehler' });
163 expect(teile.map((t) => t.className)).toEqual([
164 'nav-abzeichen-zahl',
165 'nav-abzeichen-zeichen',
166 'nur-vorlesen',
167 ]);
168 expect(teile.map((t) => t.textContent).join('')).toBe('2!, 2 Fehler');
169 // Die sichtbare Zahl steht nicht zusaetzlich im Namen: Sonst klebte sie am
170 // Ansichtsnamen davor ("Phasen1 Fehler", Rest zu M11).
171 expect(teile[0]!.getAttribute('aria-hidden')).toBe('true');
172 });
173 });