import { afterEach, describe, expect, it } from 'vitest'; import { Shell, abzeicheninhalt, abzeichenwort, type Abzeichen } from '@/ui/shell'; import { ProjectStore } from '@/app/store'; import { createEmptyProject } from '@/domain/model/factory'; import { zugaenglicherName } from './zugaenglicherName'; /** * Das Abzeichen an den Navigationseintraegen * (Befund M11). * * Das Abzeichen zeigte nur eine Zahl. Ob sie Fehler oder Warnungen zaehlte, * stand ausschliesslich in der Hintergrundfarbe - und die beiden Toene stehen * zueinander bei 1,10:1 (hell #b3261e gegen #8a5a00) beziehungsweise 1,47:1 * (dunkel #ff8a80 gegen #ffc65c). Bei Farbfehlsichtigkeit, im * Schwarzweissdruck der Oberflaeche und auf einem schlecht eingestellten * Schirm ist das dasselbe Feld; das verletzt 1.4.1 (Stufe A). Fuer die * Sprachausgabe war es noch deutlicher: der Knopf hiess gemessen * "5 Phasen 1" - zwei nackte Zahlen um einen Namen herum. * * Gegen den Altstand schlagen alle Faelle fehl: Dort war der Inhalt des * Abzeichens ausschliesslich `String(value.count)`. */ const FESTES_DATUM = new Date('2026-01-01T00:00:00Z'); let laufend: Shell | null = null; afterEach(() => { laufend?.stop(); laufend = null; document.body.replaceChildren(); }); /** Eine Shell mit drei Ansichten: Fehler, Warnung, nichts. */ function baueShell(): HTMLElement { const wurzel = document.createElement('div'); document.body.append(wurzel); const store = new ProjectStore(createEmptyProject('Musterkreuzung', FESTES_DATUM)); const shell = new Shell(wurzel, store, '5.6.0'); const ansicht = (id: string, label: string, badge: Abzeichen | null) => ({ id, label, group: 'Prüfung', description: `Nur zum Aufbau der Shell: ${label}`, badge: () => badge, render: () => () => {}, }); shell.addView(ansicht('mit-fehlern', 'Phasen', { count: 1, kind: 'fehler' })); shell.addView(ansicht('mit-warnungen', 'Prüfbericht', { count: 3, kind: 'warnung' })); shell.addView(ansicht('ohne', 'Einstellungen', null)); shell.start('ohne'); laufend = shell; return wurzel; } function knopf(wurzel: HTMLElement, label: string): HTMLButtonElement { const treffer = [...wurzel.querySelectorAll('button.nav-knopf')].find( (b) => b.querySelector('.nav-beschriftung')?.textContent === label, ); expect(treffer, `Navigationseintrag "${label}"`).toBeDefined(); return treffer!; } describe('Navigationsabzeichen: Fehler und Warnung ohne Farbe unterscheidbar', () => { it('setzt am Fehlerabzeichen ein Ausrufezeichen und am Warnungsabzeichen keines', () => { // Dasselbe Formmerkmal benutzt die Zwischenzeitenmatrix seit Fassung 5.4.0 // ("0 !" statt einer blanken 0). const wurzel = baueShell(); const fehler = knopf(wurzel, 'Phasen').querySelector('.nav-abzeichen')!; const warnung = knopf(wurzel, 'Prüfbericht').querySelector('.nav-abzeichen')!; expect(fehler.querySelector('.nav-abzeichen-zeichen')?.textContent).toBe('!'); expect(warnung.querySelector('.nav-abzeichen-zeichen')).toBeNull(); // Die Zahl selbst bleibt, wo sie war. expect(fehler.querySelector('.nav-abzeichen-zahl')?.textContent).toBe('1'); expect(warnung.querySelector('.nav-abzeichen-zahl')?.textContent).toBe('3'); }); it('unterscheidet die beiden Abzeichen auch ohne Stilblatt und ohne Farbe', () => { // Die eigentliche Zusage: Nimmt man Klassen und Farben weg - Ausdruck in // Schwarzweiss, Kontrastdarstellung, Farbfehlsichtigkeit -, muessen sich // die beiden Abzeichen immer noch unterscheiden. const wurzel = baueShell(); const sichtbar = (label: string): string => { const abzeichen = knopf(wurzel, label).querySelector('.nav-abzeichen')!; return [...abzeichen.children] .filter((kind) => !kind.classList.contains('nur-vorlesen')) .map((kind) => kind.textContent) .join(''); }; expect(sichtbar('Phasen')).toBe('1!'); expect(sichtbar('Prüfbericht')).toBe('3'); }); it('haelt das Ausrufezeichen aus dem Vorlesetext heraus und setzt das Wort daneben', () => { const wurzel = baueShell(); const fehler = knopf(wurzel, 'Phasen'); const warnung = knopf(wurzel, 'Prüfbericht'); // Das Zeichen ersetzt die Farbe, es ist kein Wort - eine Sprachausgabe // soll es nicht mitlesen. expect(fehler.querySelector('.nav-abzeichen-zeichen')?.getAttribute('aria-hidden')).toBe( 'true', ); // Der zugaengliche Name eines Knopfes ist sein Textinhalt. Er darf nicht // mehr auf einer nackten Zahl enden. expect(zugaenglicherName(fehler)).toContain('Fehler'); expect(zugaenglicherName(warnung)).toContain('Warnungen'); expect(zugaenglicherName(fehler)).not.toMatch(/\d\s*$/); expect(zugaenglicherName(warnung)).not.toMatch(/\d\s*$/); }); it('nennt Schritt, Ansicht und Abzeichen getrennt voneinander', () => { /* * NACHTRAG (Fassung 5.7.0, Rest zu Befund M11): Gemessen hiess der * Knopf "1Phasen1 Fehler" - die Navigationsnummer klebte vorn am Namen, * die Zahl des Abzeichens hinten. Der Name eines Knopfes reiht die Texte * seiner Kinder ohne Trennung aneinander; wer zuhoert, bekam drei Angaben * als ein Wort. * * Die Nummer ist deshalb `aria-hidden` und wird ausgeschrieben ersetzt. * Ueber die Listenposition allein waere sie nicht zu ersetzen: Sie laeuft * ueber alle Gruppen durch, die Liste beginnt in jeder Gruppe neu. */ const wurzel = baueShell(); const fehler = knopf(wurzel, 'Phasen'); expect(zugaenglicherName(fehler)).toBe('Schritt 1, Phasen, 1 Fehler'); expect(zugaenglicherName(knopf(wurzel, 'Prüfbericht'))).toBe( 'Schritt 2, Prüfbericht, 3 Warnungen', ); // Ohne Abzeichen bleibt es bei Schritt und Ansicht. expect(zugaenglicherName(knopf(wurzel, 'Einstellungen'))).toBe('Schritt 3, Einstellungen'); // Die sichtbare Nummer bleibt sichtbar - sie zeigt die // Bearbeitungsreihenfolge. expect(fehler.querySelector('.nav-nummer')?.textContent).toBe('1'); }); it('nennt die Mehrzahl richtig', () => { expect(abzeichenwort({ count: 1, kind: 'warnung' })).toBe('Warnung'); expect(abzeichenwort({ count: 2, kind: 'warnung' })).toBe('Warnungen'); // "Fehler" hat im Deutschen keine eigene Mehrzahlform. expect(abzeichenwort({ count: 1, kind: 'fehler' })).toBe('Fehler'); expect(abzeichenwort({ count: 4, kind: 'fehler' })).toBe('Fehler'); }); it('laesst das Abzeichen leer und verborgen, wenn nichts zu melden ist', () => { const wurzel = baueShell(); const ohne = knopf(wurzel, 'Einstellungen').querySelector('.nav-abzeichen')!; expect(ohne.hidden).toBe(true); expect(ohne.textContent).toBe(''); expect(ohne.className).toBe('nav-abzeichen'); }); it('baut den Inhalt in der Reihenfolge Zahl, Zeichen, Wort auf', () => { // Die Reihenfolge bestimmt, was die Sprachausgabe hoert: erst der Wert, // dann seine Art - nicht umgekehrt. const teile = abzeicheninhalt({ count: 2, kind: 'fehler' }); expect(teile.map((t) => t.className)).toEqual([ 'nav-abzeichen-zahl', 'nav-abzeichen-zeichen', 'nur-vorlesen', ]); expect(teile.map((t) => t.textContent).join('')).toBe('2!, 2 Fehler'); // Die sichtbare Zahl steht nicht zusaetzlich im Namen: Sonst klebte sie am // Ansichtsnamen davor ("Phasen1 Fehler", Rest zu M11). expect(teile[0]!.getAttribute('aria-hidden')).toBe('true'); }); });