lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests ui fuehrungPlatzUndZiel.test.ts
| 1 | import { afterEach, describe, expect, it } from 'vitest'; |
| 2 | import { Shell } from '@/ui/shell'; |
| 3 | import { ProjectStore } from '@/app/store'; |
| 4 | import { createEmptyProject, createSignalGroup } from '@/domain/model/factory'; |
| 5 | import { naechsterSchritt } from '@/ui/naechsterSchritt'; |
| 6 | import type { AppState } from '@/app/store'; |
| 7 | |
| 8 | /** |
| 9 | * Der Fuehrungshinweis: wo er steht und wohin er zeigt. |
| 10 | * |
| 11 | * DREI BEFUNDE AUS DER DURCHSICHT DER OBERFLAECHE, alle am selben Kasten: |
| 12 | * |
| 13 | * 1. ER STAND UEBER DER ANSICHT. Das Erste auf einer Seite sagt, WO MAN IST, |
| 14 | * nicht, wo man hin soll - die Ueberschrift der Ansicht ist der Anker. |
| 15 | * Wer mit Sprachausgabe arbeitet, begann jede Ansicht mit einem Satz ueber |
| 16 | * eine ANDERE Ansicht. Er steht jetzt darunter, dort, wo die Arbeit dieser |
| 17 | * Seite endet. |
| 18 | * |
| 19 | * 2. ER ZEIGTE AUF DIE SEITE, AUF DER MAN STAND. `naechsterSchritt` bekommt |
| 20 | * nur den Zustand und kennt die offene Ansicht nicht; keine Bedingung |
| 21 | * unterdrueckte den Sprungknopf auf seinem eigenen Ziel. Sieben der elf |
| 22 | * Ansichten koennen sich selbst als Ziel nennen, und kein Test beruehrte |
| 23 | * den Fall. Der Knopf war dabei nicht bloss wirkungslos: `navigate` |
| 24 | * zeichnet die Ansicht vollstaendig neu, setzt den Fokus auf die |
| 25 | * Ueberschrift, den Rollbalken auf null und verwirft den ansichtseigenen |
| 26 | * Zustand - wer im Lageplan gezeichnet hatte, verlor seine Stelle. |
| 27 | * |
| 28 | * 3. DIE LEISTE TRUG DIE AUSKUNFT NICHT. Sie kannte "aktiv" und |
| 29 | * "beanstandet", und ein Abzeichen bilden ueberhaupt nur 6 der 11 |
| 30 | * Eintraege - gerade in den ersten beiden Schritten blieb sie stumm, |
| 31 | * waehrend der Kasten dorthin zeigte. |
| 32 | * |
| 33 | * WAS HIER NICHT GEPRUEFT WIRD: ob der Kasten schoen aussieht. Geprueft wird |
| 34 | * die Reihenfolge im Baum, das Vorhandensein des Knopfes und die Marke - alles |
| 35 | * Dinge, die sich messen lassen. |
| 36 | */ |
| 37 | |
| 38 | const FESTES_DATUM = new Date('2026-01-01T00:00:00Z'); |
| 39 | |
| 40 | let laufend: Shell | null = null; |
| 41 | |
| 42 | afterEach(() => { |
| 43 | laufend?.stop(); |
| 44 | laufend = null; |
| 45 | document.body.replaceChildren(); |
| 46 | }); |
| 47 | |
| 48 | /** |
| 49 | * Eine Shell mit den beiden Ansichten, die der erste Schritt betrifft. |
| 50 | * |
| 51 | * Ein leeres Projekt hat keine Signalgruppe; `naechsterSchritt` zeigt dann auf |
| 52 | * 'signalgruppen' (naechsterSchritt.ts, erster Zweig). Mit einer zweiten |
| 53 | * Ansicht daneben laesst sich beides pruefen: der Fall "ich stehe auf dem Ziel" |
| 54 | * und der Fall "ich stehe woanders". |
| 55 | */ |
| 56 | function baueShell(start: string): { |
| 57 | wurzel: HTMLElement; |
| 58 | shell: Shell; |
| 59 | store: ProjectStore; |
| 60 | } { |
| 61 | const wurzel = document.createElement('div'); |
| 62 | document.body.append(wurzel); |
| 63 | |
| 64 | const store = new ProjectStore(createEmptyProject('Musterkreuzung', FESTES_DATUM)); |
| 65 | const shell = new Shell(wurzel, store, '5.17.0'); |
| 66 | const ansicht = (id: string, label: string) => ({ |
| 67 | id, |
| 68 | label, |
| 69 | group: 'Erfassung', |
| 70 | description: `Nur zum Aufbau der Shell: ${label}`, |
| 71 | // Dieselbe Zusage wie die echte Signalgruppenansicht - hier reicht sie fuer |
| 72 | // alle drei, weil nur EINE davon sie je erfuellt: Ohne Signalgruppe ist sie |
| 73 | // ueberall falsch, mit einer ueberall wahr, und der Fall unten prueft |
| 74 | // beides. |
| 75 | fertig: (zustand: AppState) => zustand.project.signalGroups.length > 0, |
| 76 | render: () => () => {}, |
| 77 | }); |
| 78 | shell.addView(ansicht('signalgruppen', 'Signalgruppen')); |
| 79 | shell.addView(ansicht('projekt', 'Projekt')); |
| 80 | // Die dritte Ansicht ist das Ziel, sobald eine Signalgruppe da ist - ohne |
| 81 | // sie liesse sich nur zeigen, dass die Marke verschwindet, nicht, dass sie |
| 82 | // wandert. |
| 83 | shell.addView(ansicht('phasen', 'Phasen')); |
| 84 | shell.start(start); |
| 85 | laufend = shell; |
| 86 | return { wurzel, shell, store }; |
| 87 | } |
| 88 | |
| 89 | /** Der Kasten selbst. */ |
| 90 | function fuehrung(wurzel: HTMLElement): HTMLElement { |
| 91 | const knoten = wurzel.querySelector<HTMLElement>('.fuehrung'); |
| 92 | expect(knoten, 'der Fuehrungshinweis fehlt ganz').not.toBeNull(); |
| 93 | return knoten as HTMLElement; |
| 94 | } |
| 95 | |
| 96 | describe('Der Fuehrungshinweis steht unter der Ansicht', () => { |
| 97 | it('zeigt ein leeres Projekt ueberhaupt auf die Signalgruppen', () => { |
| 98 | /* |
| 99 | * Sachstand zuerst. Zeigte der erste Zweig eines Tages woandershin, prueften |
| 100 | * die Faelle darunter etwas anderes als das, was sie zu pruefen glauben - |
| 101 | * und blieben womoeglich gruen, ohne es zu merken. |
| 102 | */ |
| 103 | const store = new ProjectStore(createEmptyProject('Musterkreuzung', FESTES_DATUM)); |
| 104 | expect(naechsterSchritt(store.getState()).ansicht, 'Ziel bei leerem Projekt').toBe( |
| 105 | 'signalgruppen', |
| 106 | ); |
| 107 | }); |
| 108 | |
| 109 | it('steht er im Baum HINTER dem Ansichtsbehaelter', () => { |
| 110 | const { wurzel } = baueShell('projekt'); |
| 111 | const halter = wurzel.querySelector('.fuehrung-halter'); |
| 112 | const ansichten = wurzel.querySelector('.ansicht-halter'); |
| 113 | expect(halter, 'der Halter des Hinweises fehlt').not.toBeNull(); |
| 114 | expect(ansichten, 'der Ansichtsbehaelter fehlt').not.toBeNull(); |
| 115 | |
| 116 | /* |
| 117 | * `compareDocumentPosition` statt eines Vergleichs der Kindindizes: Der |
| 118 | * Vergleich gilt auch dann, wenn zwischen beiden eines Tages etwas |
| 119 | * Drittes steht. DOCUMENT_POSITION_FOLLOWING (4) heisst: der Halter kommt |
| 120 | * NACH dem Behaelter. |
| 121 | */ |
| 122 | const lage = (ansichten as Element).compareDocumentPosition(halter as Element); |
| 123 | expect( |
| 124 | lage & Node.DOCUMENT_POSITION_FOLLOWING, |
| 125 | 'der Hinweis steht vor der Ansicht statt dahinter', |
| 126 | ).toBeGreaterThan(0); |
| 127 | }); |
| 128 | |
| 129 | it('ist er nicht das erste Element des Inhaltsbereichs', () => { |
| 130 | // Die Gegenprobe zum Fall darueber, an derselben Stelle von der anderen |
| 131 | // Seite: Wer den Inhalt liest, trifft zuerst auf die Ansicht. |
| 132 | const { wurzel } = baueShell('projekt'); |
| 133 | const inhalt = wurzel.querySelector('main.inhalt'); |
| 134 | expect(inhalt, 'der Inhaltsbereich fehlt').not.toBeNull(); |
| 135 | expect( |
| 136 | (inhalt as HTMLElement).firstElementChild?.className, |
| 137 | 'der Hinweis steht an erster Stelle im Inhalt', |
| 138 | ).not.toContain('fuehrung'); |
| 139 | }); |
| 140 | }); |
| 141 | |
| 142 | describe('Der Fuehrungshinweis zeigt nie auf die eigene Seite', () => { |
| 143 | it('traegt er auf einer anderen Ansicht einen Sprungknopf', () => { |
| 144 | // Sachstand: Ohne diesen Fall koennte der Knopf ueberall fehlen, und der |
| 145 | // Fall darunter waere gruen, ohne etwas zu bewachen. |
| 146 | const { wurzel } = baueShell('projekt'); |
| 147 | const knopf = fuehrung(wurzel).querySelector('button'); |
| 148 | expect(knopf, 'auf einer fremden Ansicht fehlt der Sprungknopf').not.toBeNull(); |
| 149 | expect(knopf?.textContent, 'Beschriftung des Sprungknopfes').toContain('Signalgruppen'); |
| 150 | }); |
| 151 | |
| 152 | it('traegt er auf der Zielansicht KEINEN Sprungknopf', () => { |
| 153 | const { wurzel } = baueShell('signalgruppen'); |
| 154 | expect( |
| 155 | fuehrung(wurzel).querySelector('button'), |
| 156 | 'der Hinweis schickt den Anwender dorthin, wo er steht', |
| 157 | ).toBeNull(); |
| 158 | }); |
| 159 | |
| 160 | it('bleibt der Satz auf der Zielansicht stehen', () => { |
| 161 | /* |
| 162 | * Der Knopf faellt weg, die Anweisung nicht: Auf der Signalgruppenansicht |
| 163 | * ist "Legen Sie die Signalgruppen an." genau richtig - nur der Weg dorthin |
| 164 | * ist keiner. Ohne diesen Fall waere das Beheben durch blosses Ausblenden |
| 165 | * des ganzen Kastens ununterscheidbar. |
| 166 | */ |
| 167 | const { wurzel } = baueShell('signalgruppen'); |
| 168 | const text = fuehrung(wurzel).textContent ?? ''; |
| 169 | expect(text, 'die Anweisung ist mit dem Knopf verschwunden').toContain('Signalgruppen'); |
| 170 | expect(text, 'die Marke kuendigt einen Schritt an, der hier zu tun ist').toContain('Hier'); |
| 171 | expect(text, 'die Marke behauptet weiter einen naechsten Schritt').not.toContain( |
| 172 | 'Nächster Schritt', |
| 173 | ); |
| 174 | }); |
| 175 | |
| 176 | it('wechselt er mit der Ansicht', () => { |
| 177 | // Derselbe Zustand, zwei Ansichten, zwei Ergebnisse - das ist der Kern: |
| 178 | // Der Kasten haengt nicht mehr allein am Projekt, sondern auch am Standort. |
| 179 | const { wurzel, shell } = baueShell('projekt'); |
| 180 | expect(fuehrung(wurzel).querySelector('button'), 'Ausgangslage').not.toBeNull(); |
| 181 | shell.navigate('signalgruppen'); |
| 182 | expect(fuehrung(wurzel).querySelector('button'), 'nach dem Wechsel auf das Ziel').toBeNull(); |
| 183 | shell.navigate('projekt'); |
| 184 | expect(fuehrung(wurzel).querySelector('button'), 'nach dem Wechsel zurueck').not.toBeNull(); |
| 185 | }); |
| 186 | }); |
| 187 | |
| 188 | describe('Die Leiste markiert den naechsten Schritt', () => { |
| 189 | /** Der Navigationsknopf zu einer Ansicht. */ |
| 190 | function navKnopf(wurzel: HTMLElement, label: string): HTMLElement { |
| 191 | const knoepfe = [...wurzel.querySelectorAll<HTMLElement>('.nav-knopf')]; |
| 192 | const treffer = knoepfe.find( |
| 193 | (k) => k.querySelector('.nav-beschriftung')?.textContent === label, |
| 194 | ); |
| 195 | expect(treffer, `Navigationseintrag "${label}"`).toBeDefined(); |
| 196 | return treffer as HTMLElement; |
| 197 | } |
| 198 | |
| 199 | it('markiert sie den Eintrag, auf den der Hinweis zeigt', () => { |
| 200 | const { wurzel } = baueShell('projekt'); |
| 201 | const marke = navKnopf(wurzel, 'Signalgruppen').querySelector<HTMLElement>('.nav-naechster'); |
| 202 | expect(marke, 'die Marke fehlt im Eintrag').not.toBeNull(); |
| 203 | expect(marke?.hidden, 'die Marke ist verborgen, obwohl der Eintrag dran ist').toBe(false); |
| 204 | }); |
| 205 | |
| 206 | it('sagt sie es auch der Sprachausgabe', () => { |
| 207 | /* |
| 208 | * Der Punkt traegt `aria-hidden` - Farbe und Zeichen allein sind fuer eine |
| 209 | * Sprachausgabe nichts. Wer nur den Punkt umschaltet, zeigt die Marke dem |
| 210 | * Auge und verschweigt sie dem Ohr. |
| 211 | */ |
| 212 | const knoten = navKnopf(baueShell('projekt').wurzel, 'Signalgruppen'); |
| 213 | const text = knoten.querySelector<HTMLElement>('.nav-naechster-text'); |
| 214 | expect(text, 'der vorgelesene Zusatz fehlt').not.toBeNull(); |
| 215 | expect(text?.hidden, 'der Zusatz bleibt verborgen, waehrend der Punkt sichtbar ist').toBe( |
| 216 | false, |
| 217 | ); |
| 218 | expect(text?.textContent, 'Wortlaut des Zusatzes').toContain('als Nächstes dran'); |
| 219 | }); |
| 220 | |
| 221 | it('markiert sie einen anderen Eintrag nicht', () => { |
| 222 | const knoten = navKnopf(baueShell('projekt').wurzel, 'Projekt'); |
| 223 | expect( |
| 224 | knoten.querySelector<HTMLElement>('.nav-naechster')?.hidden, |
| 225 | 'ein Eintrag ohne Anlass traegt die Marke', |
| 226 | ).toBe(true); |
| 227 | }); |
| 228 | |
| 229 | it('zieht sie mit, wenn sich der Zustand ohne Ansichtswechsel aendert', () => { |
| 230 | /* |
| 231 | * DIESE LUECKE HAT DER MUTATIONSNACHWEIS GEFUNDEN. Die Faelle darueber |
| 232 | * pruefen nur nach `start()` und `navigate()` - beide laufen ueber |
| 233 | * `zeichneAnsicht`, wo `markiereNaechsten` ein zweites Mal steht. Nimmt man |
| 234 | * den Aufruf aus `updateChrome` heraus, blieben sie alle gruen. GEMESSEN |
| 235 | * mit genau dieser Mutation. |
| 236 | * |
| 237 | * Der ungeprueft gebliebene Weg ist zugleich der haeufige: Wer eine |
| 238 | * Signalgruppe anlegt, wechselt dabei die Ansicht nicht - der naechste |
| 239 | * Schritt ist danach trotzdem ein anderer. |
| 240 | */ |
| 241 | const { wurzel, store } = baueShell('projekt'); |
| 242 | expect( |
| 243 | navKnopf(wurzel, 'Signalgruppen').querySelector<HTMLElement>('.nav-naechster')?.hidden, |
| 244 | 'Ausgangslage: die Marke steht bei den Signalgruppen', |
| 245 | ).toBe(false); |
| 246 | |
| 247 | store.update( |
| 248 | (projekt) => ({ |
| 249 | ...projekt, |
| 250 | signalGroups: [createSignalGroup({ name: 'K1', mode: 'kfz' })], |
| 251 | }), |
| 252 | { label: 'Signalgruppe angelegt' }, |
| 253 | ); |
| 254 | |
| 255 | // Mit einer Signalgruppe und ohne Phase zeigt die Kette auf die Phasen. |
| 256 | expect(naechsterSchritt(store.getState()).ansicht, 'Ziel nach der Aenderung').toBe('phasen'); |
| 257 | expect( |
| 258 | navKnopf(wurzel, 'Signalgruppen').querySelector<HTMLElement>('.nav-naechster')?.hidden, |
| 259 | 'die Marke steht noch bei den Signalgruppen', |
| 260 | ).toBe(true); |
| 261 | expect( |
| 262 | navKnopf(wurzel, 'Phasen').querySelector<HTMLElement>('.nav-naechster')?.hidden, |
| 263 | 'die Marke ist den Phasen nicht gefolgt', |
| 264 | ).toBe(false); |
| 265 | }); |
| 266 | |
| 267 | it('markiert sie nicht die Ansicht, auf der man steht', () => { |
| 268 | /* |
| 269 | * Dort sagt `aria-current` bereits, wo man ist. "Hier" und "als Naechstes |
| 270 | * hierhin" nebeneinander waere dieselbe Verdopplung, die der Sprungknopf |
| 271 | * auf der eigenen Seite war. |
| 272 | */ |
| 273 | const knoten = navKnopf(baueShell('signalgruppen').wurzel, 'Signalgruppen'); |
| 274 | expect( |
| 275 | knoten.querySelector<HTMLElement>('.nav-naechster')?.hidden, |
| 276 | 'die Leiste kuendigt an, wohin man schon gegangen ist', |
| 277 | ).toBe(true); |
| 278 | expect( |
| 279 | knoten.querySelector<HTMLElement>('.nav-naechster-text')?.hidden, |
| 280 | 'der vorgelesene Zusatz bleibt stehen', |
| 281 | ).toBe(true); |
| 282 | }); |
| 283 | }); |
| 284 | |
| 285 | describe('Der Haken "erledigt" in der Leiste', () => { |
| 286 | /** Der Navigationsknopf zu einer Ansicht. */ |
| 287 | function navKnopf(wurzel: HTMLElement, label: string): HTMLElement { |
| 288 | const knoepfe = [...wurzel.querySelectorAll<HTMLElement>('.nav-knopf')]; |
| 289 | const treffer = knoepfe.find( |
| 290 | (k) => k.querySelector('.nav-beschriftung')?.textContent === label, |
| 291 | ); |
| 292 | expect(treffer, `Navigationseintrag "${label}"`).toBeDefined(); |
| 293 | return treffer as HTMLElement; |
| 294 | } |
| 295 | |
| 296 | it('steht am leeren Projekt an keinem Eintrag', () => { |
| 297 | // Sachstand: Ohne Signalgruppe ist die Zusage der Testansichten ueberall |
| 298 | // falsch. Stuende hier ein Haken, waere der Fall darunter wertlos. |
| 299 | const { wurzel } = baueShell('projekt'); |
| 300 | for (const label of ['Projekt', 'Signalgruppen', 'Phasen']) { |
| 301 | expect( |
| 302 | navKnopf(wurzel, label).querySelector<HTMLElement>('.nav-fertig')?.hidden, |
| 303 | `Haken an "${label}" am leeren Projekt`, |
| 304 | ).toBe(true); |
| 305 | } |
| 306 | }); |
| 307 | |
| 308 | it('erscheint er, sobald die Zusage erfuellt ist', () => { |
| 309 | const { wurzel, store } = baueShell('projekt'); |
| 310 | store.update( |
| 311 | (projekt) => ({ |
| 312 | ...projekt, |
| 313 | signalGroups: [createSignalGroup({ name: 'K1', mode: 'kfz' })], |
| 314 | }), |
| 315 | { label: 'Signalgruppe angelegt' }, |
| 316 | ); |
| 317 | const knoten = navKnopf(wurzel, 'Signalgruppen'); |
| 318 | expect(knoten.querySelector<HTMLElement>('.nav-fertig')?.hidden, 'Haken sichtbar').toBe(false); |
| 319 | expect( |
| 320 | knoten.querySelector<HTMLElement>('.nav-fertig-text')?.hidden, |
| 321 | 'der vorgelesene Zusatz bleibt verborgen', |
| 322 | ).toBe(false); |
| 323 | expect( |
| 324 | knoten.querySelector<HTMLElement>('.nav-fertig-text')?.textContent, |
| 325 | 'Wortlaut des Zusatzes', |
| 326 | ).toContain('erledigt'); |
| 327 | }); |
| 328 | |
| 329 | it('schweigt er an dem Eintrag, der als Naechstes dran ist', () => { |
| 330 | /* |
| 331 | * "Erledigt" und "als Nächstes dran" am selben Eintrag waeren ein |
| 332 | * Widerspruch. Der Haken ist die schwaechere der beiden Aussagen und |
| 333 | * weicht. |
| 334 | * |
| 335 | * Gebaut wird die Lage ueber die Phasen: Mit einer Signalgruppe zeigt die |
| 336 | * Kette dorthin, und die Testansicht "Phasen" traegt dieselbe Zusage wie |
| 337 | * alle - sie waere also ohne diese Regel zugleich erledigt und dran. |
| 338 | */ |
| 339 | const { wurzel, store } = baueShell('projekt'); |
| 340 | store.update( |
| 341 | (projekt) => ({ |
| 342 | ...projekt, |
| 343 | signalGroups: [createSignalGroup({ name: 'K1', mode: 'kfz' })], |
| 344 | }), |
| 345 | { label: 'Signalgruppe angelegt' }, |
| 346 | ); |
| 347 | expect(naechsterSchritt(store.getState()).ansicht, 'Ziel nach der Aenderung').toBe('phasen'); |
| 348 | |
| 349 | const knoten = navKnopf(wurzel, 'Phasen'); |
| 350 | expect( |
| 351 | knoten.querySelector<HTMLElement>('.nav-naechster')?.hidden, |
| 352 | 'die Marke fehlt am naechsten Schritt', |
| 353 | ).toBe(false); |
| 354 | expect( |
| 355 | knoten.querySelector<HTMLElement>('.nav-fertig')?.hidden, |
| 356 | 'derselbe Eintrag traegt zugleich einen Haken', |
| 357 | ).toBe(true); |
| 358 | }); |
| 359 | }); |
| 360 | |
| 361 | describe('Aendert sich der naechste Schritt, sagt es die Sprachausgabe', () => { |
| 362 | /** Der Ansageraum der Shell. */ |
| 363 | function ansage(wurzel: HTMLElement): HTMLElement { |
| 364 | const knoten = wurzel.querySelector<HTMLElement>('[role="status"]'); |
| 365 | expect(knoten, 'der Ansageraum fehlt').not.toBeNull(); |
| 366 | return knoten as HTMLElement; |
| 367 | } |
| 368 | |
| 369 | it('nennt sie den Schritt beim ersten Aufbau', () => { |
| 370 | const { wurzel } = baueShell('projekt'); |
| 371 | expect(ansage(wurzel).textContent, 'Ansage beim Start').toContain( |
| 372 | 'Nächster Schritt: Legen Sie die Signalgruppen an.', |
| 373 | ); |
| 374 | }); |
| 375 | |
| 376 | it('nennt sie den neuen Schritt, wenn er sich aendert', () => { |
| 377 | /* |
| 378 | * DER BEFUND, GEGEN DEN DIESER FALL GESCHRIEBEN IST: Der naechste Schritt |
| 379 | * aenderte sich stumm. Wer eine Signalgruppe anlegte, hoerte, dass zwei |
| 380 | * Fehler dazugekommen sind, aber nicht, dass jetzt die Phasen dran sind - |
| 381 | * die sichtbarste Auskunft der Oberflaeche war die einzige, die nie |
| 382 | * angesagt wurde. |
| 383 | */ |
| 384 | const { wurzel, store } = baueShell('projekt'); |
| 385 | store.update( |
| 386 | (projekt) => ({ |
| 387 | ...projekt, |
| 388 | signalGroups: [createSignalGroup({ name: 'K1', mode: 'kfz' })], |
| 389 | }), |
| 390 | { label: 'Signalgruppe angelegt' }, |
| 391 | ); |
| 392 | expect(ansage(wurzel).textContent, 'Ansage nach der Aenderung').toContain( |
| 393 | 'Nächster Schritt: Bilden Sie die Phasen.', |
| 394 | ); |
| 395 | }); |
| 396 | |
| 397 | it('wiederholt sie den Schritt nicht, wenn nur der Zustand sich aendert', () => { |
| 398 | /* |
| 399 | * Der Grund fuer zwei getrennte Staende. Haengte der Schritt an derselben |
| 400 | * Zeichenkette wie der Zustand, laese die Sprachausgabe ihn bei jeder |
| 401 | * geaenderten Zahl noch einmal mit vor - und wer ihn hoeren will, hoerte |
| 402 | * ihn im Rauschen nicht. |
| 403 | */ |
| 404 | const { wurzel, store } = baueShell('projekt'); |
| 405 | /* |
| 406 | * GEMESSEN WIRD, OB NEU GESCHRIEBEN WURDE, nicht, was dasteht. Ein |
| 407 | * Ansageraum behaelt seinen Text, solange niemand ihn tauscht - eine |
| 408 | * Sprachausgabe liest ihn aber nur bei der AENDERUNG vor. Der erste Anlauf |
| 409 | * dieses Falles suchte im Text nach "Nächster Schritt:" und fand die |
| 410 | * Ansage vom Programmstart wieder; er war rot, obwohl nichts gesagt worden |
| 411 | * war. |
| 412 | */ |
| 413 | expect(ansage(wurzel).textContent, 'Ausgangslage: es wurde schon etwas gesagt').not.toBe(''); |
| 414 | |
| 415 | /* |
| 416 | * GEMESSEN WIRD DER SCHREIBVORGANG, nicht der Wert. Der zweite Anlauf |
| 417 | * dieses Falles verglich den Text vorher und nachher - und blieb gruen, als |
| 418 | * die Mutation "alles bei jeder Aenderung wiederholen" den Ansageraum |
| 419 | * unbedingt neu beschrieb: Derselbe Text, geschrieben wie ungeschrieben, |
| 420 | * sieht gleich aus. Eine Sprachausgabe liest ihn aber vor, sobald er |
| 421 | * gesetzt wird. |
| 422 | * |
| 423 | * `textContent = ...` ersetzt die Kindknoten. Der Textknoten selbst ist |
| 424 | * damit der Zeuge: Ist es derselbe wie vorher, wurde nicht geschrieben. |
| 425 | */ |
| 426 | const knoten = ansage(wurzel).firstChild; |
| 427 | expect(knoten, 'der Ansageraum hat keinen Textknoten').not.toBeNull(); |
| 428 | |
| 429 | // Der Projektname geht weder in den Zustandssatz noch in den Schritt ein. |
| 430 | store.update((projekt) => ({ ...projekt, meta: { ...projekt.meta, name: 'Anders' } }), { |
| 431 | label: 'Name geaendert', |
| 432 | }); |
| 433 | expect(ansage(wurzel).firstChild, 'der Ansageraum wurde neu beschrieben').toBe(knoten); |
| 434 | }); |
| 435 | |
| 436 | it('sagt sie beim Schrittwechsel den Schritt und nicht die halbe Oberflaeche', () => { |
| 437 | // Die Gegenprobe zum Fall darueber: Dass NICHTS gesagt wird, ist nur dann |
| 438 | // ein Verdienst, wenn im richtigen Fall etwas gesagt wird. |
| 439 | const { wurzel, store } = baueShell('projekt'); |
| 440 | const vorher = ansage(wurzel).textContent ?? ''; |
| 441 | store.update( |
| 442 | (projekt) => ({ |
| 443 | ...projekt, |
| 444 | signalGroups: [createSignalGroup({ name: 'K1', mode: 'kfz' })], |
| 445 | }), |
| 446 | { label: 'Signalgruppe angelegt' }, |
| 447 | ); |
| 448 | expect(ansage(wurzel).textContent, 'die Ansage blieb stehen').not.toBe(vorher); |
| 449 | expect(ansage(wurzel).textContent, 'der neue Schritt fehlt').toContain( |
| 450 | 'Nächster Schritt: Bilden Sie die Phasen.', |
| 451 | ); |
| 452 | }); |
| 453 | }); |
| 454 | |
| 455 | describe('Der Fokus ueberlebt eine Zustandsaenderung auf dem Sprungknopf', () => { |
| 456 | it('bleibt er auf dem Knopf, wenn sich der Zustand aendert', () => { |
| 457 | /* |
| 458 | * Der Hinweis wird bei JEDER Zustandsaenderung neu gebaut - auch beim |
| 459 | * Tippen in einem Feld. Ohne Fokusrettung ersetzt `render` die Kinder, und |
| 460 | * der Fokus faellt auf den Seitenkoerper zurueck: Wer den Sprungknopf mit |
| 461 | * der Tastatur erreicht hatte, stand danach wieder am Anfang. |
| 462 | */ |
| 463 | const { wurzel, store } = baueShell('projekt'); |
| 464 | const knopf = wurzel.querySelector<HTMLButtonElement>('.fuehrung button'); |
| 465 | expect(knopf, 'der Sprungknopf fehlt').not.toBeNull(); |
| 466 | (knopf as HTMLButtonElement).focus(); |
| 467 | expect(document.activeElement, 'Ausgangslage: der Knopf hat den Fokus').toBe(knopf); |
| 468 | |
| 469 | store.update((projekt) => ({ ...projekt, meta: { ...projekt.meta, name: 'Anders' } }), { |
| 470 | label: 'Name geaendert', |
| 471 | }); |
| 472 | |
| 473 | const danach = wurzel.querySelector<HTMLButtonElement>('.fuehrung button'); |
| 474 | expect(danach, 'nach der Aenderung fehlt der Knopf').not.toBeNull(); |
| 475 | expect(document.activeElement, 'der Fokus ist vom Knopf gefallen').toBe(danach); |
| 476 | }); |
| 477 | }); |