import { afterEach, describe, expect, it } from 'vitest'; import * as actions from '@/app/actions'; import { ProjectStore } from '@/app/store'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import type { Project } from '@/domain/model/project'; import type { ViewContext } from '@/ui/shell'; import { compatibilityView } from '@/ui/views/compatibilityView'; /* * Punkt X.4 - bestaendiger Fokusschluessel fuer die Vertraeglichkeitsmatrix. * * In dieser Matrix IST die Beschriftung des Knopfes der Zustand ("feindlich" * oder "verträglich"), und `ariaLabel` nennt denselben Zustand. Der allgemeine * Schluessel aus `benenneBedienelemente()` setzt sich unter anderem aus dem * zugaenglichen Namen zusammen - er kippt damit bei genau dem Klick, der den * Neuaufbau ausloest, und die Fokusrettung faellt fuer bis zu 120 Knoepfe auf * den schwaecheren Strukturweg zurueck. * * GEPRUEFT WIRD DER SCHLUESSEL, NICHT DAS SICHTBARE ERGEBNIS: Sichtbar geht der * Fokus heute nicht verloren, weil die Struktur der Ansicht beim Umschalten * gleich bleibt und der Strukturweg deshalb greift. Die Zusage, die hier haelt, * ist die des Schluessels selbst. */ let aufraeumen: (() => void) | null = null; afterEach(() => { aufraeumen?.(); aufraeumen = null; document.body.replaceChildren(); }); interface Ansicht { readonly wurzel: HTMLElement; readonly store: ProjectStore; readonly neuZeichnen: () => void; } /** * Zeichnet die Ansicht und verdrahtet `refresh` mit einem echten Neuaufbau - * so, wie die Schale es tut. Ohne das bliebe die Matrix nach dem Klick stehen, * und der Schluessel waere gar nicht neu vergeben. */ function zeichne(project: Project): Ansicht { const wurzel = document.createElement('div'); document.body.append(wurzel); const store = new ProjectStore(project); let schliessen: (() => void) | null = null; const neuZeichnen = (): void => { schliessen?.(); schliessen = compatibilityView.render(wurzel, context); }; const context: ViewContext = { store, refresh: neuZeichnen, navigate: () => {}, target: null, }; neuZeichnen(); aufraeumen = () => { schliessen?.(); wurzel.remove(); }; return { wurzel, store, neuZeichnen }; } /** Der Umschaltknopf der Zelle in Zeile `i`, Spalte `j` der Matrix. */ function knopfAn(wurzel: HTMLElement, i: number, j: number): HTMLButtonElement { const zeile = wurzel.querySelectorAll('table.matrix tbody tr')[i]; expect(zeile, `Keine Matrixzeile ${i}`).toBeDefined(); // Erste Zelle der Zeile ist der Zeilenkopf, danach je Signalgruppe eine. const zelle = zeile?.children[1 + j]; const knopf = zelle?.querySelector('button') ?? null; expect(knopf, `Kein Umschaltknopf in Zeile ${i}, Spalte ${j}`).not.toBeNull(); return knopf as HTMLButtonElement; } const fokus = (knopf: HTMLElement): string => knopf.getAttribute('data-fokus') ?? ''; describe('X.4: Fokusschluessel der Vertraeglichkeitsmatrix', () => { it('ueberlebt den Zustandswechsel, den der Klick selbst ausloest', () => { const { wurzel } = zeichne(createStandardIntersectionProject()); const vorher = knopfAn(wurzel, 0, 1); const schluesselVorher = fokus(vorher); expect(schluesselVorher, 'Der Knopf traegt gar keinen Fokusschluessel').not.toBe(''); const zustandVorher = (vorher.textContent ?? '').trim(); vorher.click(); const nachher = knopfAn(wurzel, 0, 1); expect( (nachher.textContent ?? '').trim(), 'der Klick hat den Zustand nicht umgeschaltet', ).not.toBe(zustandVorher); expect(fokus(nachher), 'der Schluessel kippt mit dem Zustand').toBe(schluesselVorher); }); it('haengt an den Kennungen der beiden Signalgruppen, nicht an ihren Bezeichnungen', () => { const { wurzel, store, neuZeichnen } = zeichne(createStandardIntersectionProject()); const gruppen = store.getProject().signalGroups; const zeile = gruppen[0]; const spalte = gruppen[1]; expect(zeile, 'zu wenige Signalgruppen').toBeDefined(); expect(spalte, 'zu wenige Signalgruppen').toBeDefined(); const schluesselVorher = fokus(knopfAn(wurzel, 0, 1)); expect(schluesselVorher).toContain(zeile?.id ?? ''); expect(schluesselVorher).toContain(spalte?.id ?? ''); // Bezeichnungen sind freier Text und jederzeit aenderbar; das Paar bleibt // dasselbe. store.update((p) => actions.updateSignalGroup(p, zeile?.id ?? '', { name: 'Nord links' }), { label: 'Bezeichnung geändert', }); neuZeichnen(); expect(fokus(knopfAn(wurzel, 0, 1)), 'der Schluessel haengt an der Bezeichnung').toBe( schluesselVorher, ); }); it('vergibt fuer jedes Paar einen eigenen Schluessel', () => { const { wurzel } = zeichne(createStandardIntersectionProject()); const alle = [...wurzel.querySelectorAll('table.matrix [data-fokus]')].map(fokus); expect(alle.length, 'keine Knoepfe mit Fokusschluessel gefunden').toBeGreaterThan(1); expect(new Set(alle).size, `Doppelte Schluessel: ${alle.join(' / ')}`).toBe(alle.length); }); });