import { afterEach, describe, expect, it } from 'vitest'; import { applyTheme, effektivesErscheinungsbild } from '@/app/theme'; /** * Befund L14 (EN 301 549 Kap. 11.7). * * Die Zeichenflaechen (`planView`, `simulationView`, `lageplanView`) lesen ihre * Farbwahl ausschliesslich aus `data-erscheinungsbild`. Ein `canvas` wird von * `forced-colors: active` nicht erfasst - das Betriebssystem ersetzt dort keine * Farbe. Im Altbestand wurde das Attribut allein aus der Einstellung und * `prefers-color-scheme` gebildet; bei ausdruecklicher Wahl "Hell" und dunklem * Kontrastdesign stand eine weisse Zeichnung in einer schwarzen Oberflaeche. */ interface Abfrage { matches: boolean; empfaenger: (() => void)[]; } const abfragen = new Map(); function stelleMedienabfragen(kontrastdesign: boolean, systemDunkel: boolean): void { abfragen.clear(); abfragen.set('(forced-colors: active)', { matches: kontrastdesign, empfaenger: [] }); abfragen.set('(prefers-color-scheme: dark)', { matches: systemDunkel, empfaenger: [] }); Object.defineProperty(window, 'matchMedia', { configurable: true, writable: true, value: (text: string) => { const zustand = abfragen.get(text) ?? { matches: false, empfaenger: [] }; abfragen.set(text, zustand); return { get matches() { return zustand.matches; }, media: text, addEventListener: (_art: string, empfaenger: () => void) => { zustand.empfaenger.push(empfaenger); }, removeEventListener: (_art: string, empfaenger: () => void) => { const stelle = zustand.empfaenger.indexOf(empfaenger); if (stelle >= 0) zustand.empfaenger.splice(stelle, 1); }, }; }, }); } function schalte(abfrage: string, matches: boolean): void { const zustand = abfragen.get(abfrage); if (zustand === undefined) throw new Error(`Abfrage ${abfrage} wurde nie gestellt.`); zustand.matches = matches; for (const empfaenger of [...zustand.empfaenger]) empfaenger(); } function erscheinungsbild(): string | undefined { return document.documentElement.dataset['erscheinungsbild']; } afterEach(() => { delete document.documentElement.dataset['erscheinungsbild']; abfragen.clear(); Reflect.deleteProperty(window, 'matchMedia'); }); describe('Erscheinungsbild: Kontrastdesign des Betriebssystems', () => { it('folgt bei aktivem Kontrastdesign dem System, nicht der eigenen Wahl', () => { expect(effektivesErscheinungsbild('hell', true, true)).toBe('dunkel'); expect(effektivesErscheinungsbild('dunkel', true, false)).toBe('hell'); }); it('lässt die eigene Wahl gelten, solange kein Kontrastdesign aktiv ist', () => { expect(effektivesErscheinungsbild('hell', false, true)).toBe('hell'); expect(effektivesErscheinungsbild('dunkel', false, false)).toBe('dunkel'); }); it('folgt bei "System" der Systemvorgabe', () => { expect(effektivesErscheinungsbild('system', false, true)).toBe('dunkel'); expect(effektivesErscheinungsbild('system', false, false)).toBe('hell'); }); it('setzt das Attribut auf "dunkel", obwohl "Hell" gewählt ist', () => { // Genau der Fall des Befunds: dunkles Kontrastdesign, Wahl "Hell". stelleMedienabfragen(true, true); applyTheme('hell'); expect(erscheinungsbild()).toBe('dunkel'); }); it('nimmt das Kontrastdesign auch dann an, wenn es erst später eingeschaltet wird', () => { stelleMedienabfragen(false, true); applyTheme('hell'); expect(erscheinungsbild()).toBe('hell'); schalte('(forced-colors: active)', true); expect(erscheinungsbild()).toBe('dunkel'); }); it('folgt bei "System" weiterhin dem Wechsel der Systemvorgabe', () => { stelleMedienabfragen(false, false); applyTheme('system'); expect(erscheinungsbild()).toBe('hell'); schalte('(prefers-color-scheme: dark)', true); expect(erscheinungsbild()).toBe('dunkel'); }); it('meldet die Empfänger des vorigen Aufrufs ab', () => { // Sonst sammelten sich bei jedem Wechsel des Erscheinungsbilds weitere an, // und der aelteste setzte das Attribut nach dem juengsten wieder zurueck. stelleMedienabfragen(false, false); applyTheme('system'); applyTheme('dunkel'); expect(abfragen.get('(prefers-color-scheme: dark)')?.empfaenger).toHaveLength(1); schalte('(prefers-color-scheme: dark)', true); expect(erscheinungsbild()).toBe('dunkel'); schalte('(prefers-color-scheme: dark)', false); expect(erscheinungsbild()).toBe('dunkel'); }); it('bricht nicht ab, wenn die Umgebung keine Medienabfragen kennt', () => { // `applyTheme` laeuft als eine der ersten Anweisungen des Programmstarts. Reflect.deleteProperty(window, 'matchMedia'); expect(() => { applyTheme('dunkel'); }).not.toThrow(); expect(erscheinungsbild()).toBe('dunkel'); }); });