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