lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests ui ruecknahmetaste.test.ts
| 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 | }); |