lsa-planer

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

/ tests ui ruecknahmetaste.test.ts

5,7 KB Rohdatei
tests/ui/ruecknahmetaste.test.ts — 172 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import * as actions from '@/app/actions';
3 import { ProjectStore } from '@/app/store';
4 import type { Lageplan } from '@/domain/geometrie/lageplan';
5 import { ASSISTENT_VORGABE, projektAusAngaben } from '@/ui/assistent';
6 import { lageplanView } from '@/ui/views/lageplanView';
7
8 /**
9 * Befund 54: Die Rueckschritttaste wirkte nur beim Werkzeug "Fahrlinie".
10 *
11 * WAS DER ANWENDER SONST ERLEBT
12 *
13 * Er waehlt "Massstab", setzt mit der Leertaste den ersten Punkt der
14 * Bezugsstrecke und merkt, dass er danebengegriffen hat. Er drueckt
15 * Rueckschritt - nichts geschieht, und nichts wird angesagt. Der naechste
16 * Leertastendruck schliesst die Strecke zu genau dem Punkt, den er
17 * zuruecknehmen wollte. Dasselbe beim Werkzeug "Haltlinie".
18 *
19 * Zugesagt wird die Taste ohne jede Einschraenkung: im vorgelesenen Hinweis der
20 * Zeichenflaeche, in der aufklappbaren Tastenbelegung darunter und im
21 * Hilfefenster. Uebrig blieb nur Escape - das verwirft aber die ganze
22 * Messung UND schaltet auf "Ansehen", ist also kein Schritt zurueck.
23 *
24 * WCAG 2.1 Erfolgskriterium 4.1.3 "Statusmeldungen": Eine zurueckgenommene
25 * Marke, die nicht angesagt wird, ist auf einer Zeichenflaeche nicht
26 * wahrnehmbar.
27 */
28
29 const FESTES_DATUM = '2026-01-01T00:00:00.000Z';
30
31 function planOhneMassstab(): Lageplan {
32 return {
33 arbeitsbereiche: [],
34 signalgeber: [],
35 bild: {
36 datenUrl: 'data:image/png;base64,iVBORw0KGgo=',
37 breite: 800,
38 hoehe: 600,
39 herkunft: 'Prüfbild',
40 geladenAm: FESTES_DATUM,
41 },
42 kalibrierung: null,
43 haltlinien: [],
44 linien: [],
45 };
46 }
47
48 /** Derselbe Plan, nachdem der Massstab steht: 100 Bildpunkte auf 20 m. */
49 function planMitMassstab(): Lageplan {
50 return {
51 ...planOhneMassstab(),
52 kalibrierung: {
53 von: { x: 0, y: 0 },
54 bis: { x: 100, y: 0 },
55 laengeMeter: 20,
56 herkunft: 'gemessen',
57 },
58 };
59 }
60
61 interface Ansicht {
62 readonly store: ProjectStore;
63 readonly wurzel: HTMLElement;
64 readonly leinwand: HTMLCanvasElement;
65 readonly aufraeumen: () => void;
66 }
67
68 function baueAnsicht(lageplan: Lageplan): Ansicht {
69 const store = new ProjectStore(projektAusAngaben({ ...ASSISTENT_VORGABE }, FESTES_DATUM));
70 store.update((p) => actions.setLageplan(p, lageplan), { label: 'Pruefplan' });
71
72 const wurzel = document.createElement('div');
73 document.body.append(wurzel);
74 const schliessen = lageplanView.render(wurzel, {
75 store,
76 refresh: () => {},
77 navigate: () => {},
78 target: null,
79 });
80
81 const leinwand = wurzel.querySelector('canvas');
82 if (leinwand === null) throw new Error('Die Ansicht hat keine Zeichenflaeche.');
83
84 return {
85 store,
86 wurzel,
87 leinwand,
88 aufraeumen: () => {
89 leinwand.blur();
90 schliessen();
91 wurzel.remove();
92 },
93 };
94 }
95
96 afterEach(() => {
97 document.body.replaceChildren();
98 });
99
100 function waehleWerkzeug(ansicht: Ansicht, beschriftung: string): void {
101 const knopf = [
102 ...ansicht.wurzel.querySelectorAll<HTMLButtonElement>('.lageplan-werkzeuge button'),
103 ].find((b) => (b.textContent ?? '').trim() === beschriftung);
104 if (knopf === undefined) throw new Error(`Kein Werkzeug "${beschriftung}"`);
105 knopf.click();
106 }
107
108 /** Tippt auf das Element, das gerade den Fokus hat; false heisst abgefangen. */
109 function tippe(key: string, male = 1): boolean {
110 let zugestellt = true;
111 for (let i = 0; i < male; i += 1) {
112 const ziel = (document.activeElement as HTMLElement | null) ?? document.body;
113 zugestellt = ziel.dispatchEvent(
114 new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }),
115 );
116 }
117 return zugestellt;
118 }
119
120 function ansage(ansicht: Ansicht): string {
121 return ansicht.wurzel.querySelector('.lageplan-rahmen [role="status"]')?.textContent ?? '';
122 }
123
124 describe('Rückschritttaste beim Werkzeug "Maßstab"', () => {
125 it('nimmt den ersten Punkt der Maßstabsstrecke zurück und sagt es an', () => {
126 const ansicht = baueAnsicht(planOhneMassstab());
127 try {
128 waehleWerkzeug(ansicht, 'Maßstab');
129 ansicht.leinwand.focus();
130 tippe(' ');
131 expect(ansage(ansicht)).toContain('Erster Punkt der Maßstabsstrecke gesetzt');
132
133 // Abgefangen werden muss die Taste auch: Sonst blaettert der Browser
134 // unter der Zeichenflaeche zurueck.
135 expect(tippe('Backspace'), 'Rückschritt nicht abgefangen').toBe(false);
136 expect(ansage(ansicht)).toContain('zurückgenommen');
137
138 // Der Nachweis, dass wirklich nichts mehr steht: Der naechste
139 // Leertastendruck beginnt von vorn, statt die Strecke zu schliessen.
140 tippe('ArrowRight', 20);
141 tippe(' ');
142 expect(ansage(ansicht)).toContain('Erster Punkt der Maßstabsstrecke gesetzt');
143 expect(document.querySelector('.dialog'), 'Fenster nach nur einem Punkt').toBeNull();
144 expect(ansicht.store.getProject().lageplan.kalibrierung).toBeNull();
145 } finally {
146 ansicht.aufraeumen();
147 }
148 });
149 });
150
151 describe('Rückschritttaste beim Werkzeug "Haltlinie"', () => {
152 it('nimmt den ersten Fahrbahnrand zurück und sagt es an', () => {
153 const ansicht = baueAnsicht(planMitMassstab());
154 try {
155 waehleWerkzeug(ansicht, 'Haltlinie');
156 ansicht.leinwand.focus();
157 tippe(' ');
158 expect(ansage(ansicht)).toContain('Erster Punkt der Haltlinie gesetzt');
159
160 expect(tippe('Backspace'), 'Rückschritt nicht abgefangen').toBe(false);
161 expect(ansage(ansicht)).toContain('zurückgenommen');
162
163 tippe('ArrowDown', 12);
164 tippe(' ');
165 expect(ansage(ansicht)).toContain('Erster Punkt der Haltlinie gesetzt');
166 expect(document.querySelector('.dialog'), 'Fenster nach nur einem Punkt').toBeNull();
167 expect(ansicht.store.getProject().lageplan.haltlinien).toHaveLength(0);
168 } finally {
169 ansicht.aufraeumen();
170 }
171 });
172 });