lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests ui erscheinungsbildKontrastdesign.test.ts

4,8 KB Rohdatei
tests/ui/erscheinungsbildKontrastdesign.test.ts — 130 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { applyTheme, effektivesErscheinungsbild } from '@/app/theme';
3
4 /**
5 * Befund L14 (EN 301 549 Kap. 11.7).
6 *
7 * Die Zeichenflaechen (`planView`, `simulationView`, `lageplanView`) lesen ihre
8 * Farbwahl ausschliesslich aus `data-erscheinungsbild`. Ein `canvas` wird von
9 * `forced-colors: active` nicht erfasst - das Betriebssystem ersetzt dort keine
10 * Farbe. Im Altbestand wurde das Attribut allein aus der Einstellung und
11 * `prefers-color-scheme` gebildet; bei ausdruecklicher Wahl "Hell" und dunklem
12 * Kontrastdesign stand eine weisse Zeichnung in einer schwarzen Oberflaeche.
13 */
14
15 interface Abfrage {
16 matches: boolean;
17 empfaenger: (() => void)[];
18 }
19
20 const abfragen = new Map<string, Abfrage>();
21
22 function stelleMedienabfragen(kontrastdesign: boolean, systemDunkel: boolean): void {
23 abfragen.clear();
24 abfragen.set('(forced-colors: active)', { matches: kontrastdesign, empfaenger: [] });
25 abfragen.set('(prefers-color-scheme: dark)', { matches: systemDunkel, empfaenger: [] });
26
27 Object.defineProperty(window, 'matchMedia', {
28 configurable: true,
29 writable: true,
30 value: (text: string) => {
31 const zustand = abfragen.get(text) ?? { matches: false, empfaenger: [] };
32 abfragen.set(text, zustand);
33 return {
34 get matches() {
35 return zustand.matches;
36 },
37 media: text,
38 addEventListener: (_art: string, empfaenger: () => void) => {
39 zustand.empfaenger.push(empfaenger);
40 },
41 removeEventListener: (_art: string, empfaenger: () => void) => {
42 const stelle = zustand.empfaenger.indexOf(empfaenger);
43 if (stelle >= 0) zustand.empfaenger.splice(stelle, 1);
44 },
45 };
46 },
47 });
48 }
49
50 function schalte(abfrage: string, matches: boolean): void {
51 const zustand = abfragen.get(abfrage);
52 if (zustand === undefined) throw new Error(`Abfrage ${abfrage} wurde nie gestellt.`);
53 zustand.matches = matches;
54 for (const empfaenger of [...zustand.empfaenger]) empfaenger();
55 }
56
57 function erscheinungsbild(): string | undefined {
58 return document.documentElement.dataset['erscheinungsbild'];
59 }
60
61 afterEach(() => {
62 delete document.documentElement.dataset['erscheinungsbild'];
63 abfragen.clear();
64 Reflect.deleteProperty(window, 'matchMedia');
65 });
66
67 describe('Erscheinungsbild: Kontrastdesign des Betriebssystems', () => {
68 it('folgt bei aktivem Kontrastdesign dem System, nicht der eigenen Wahl', () => {
69 expect(effektivesErscheinungsbild('hell', true, true)).toBe('dunkel');
70 expect(effektivesErscheinungsbild('dunkel', true, false)).toBe('hell');
71 });
72
73 it('lässt die eigene Wahl gelten, solange kein Kontrastdesign aktiv ist', () => {
74 expect(effektivesErscheinungsbild('hell', false, true)).toBe('hell');
75 expect(effektivesErscheinungsbild('dunkel', false, false)).toBe('dunkel');
76 });
77
78 it('folgt bei "System" der Systemvorgabe', () => {
79 expect(effektivesErscheinungsbild('system', false, true)).toBe('dunkel');
80 expect(effektivesErscheinungsbild('system', false, false)).toBe('hell');
81 });
82
83 it('setzt das Attribut auf "dunkel", obwohl "Hell" gewählt ist', () => {
84 // Genau der Fall des Befunds: dunkles Kontrastdesign, Wahl "Hell".
85 stelleMedienabfragen(true, true);
86 applyTheme('hell');
87 expect(erscheinungsbild()).toBe('dunkel');
88 });
89
90 it('nimmt das Kontrastdesign auch dann an, wenn es erst später eingeschaltet wird', () => {
91 stelleMedienabfragen(false, true);
92 applyTheme('hell');
93 expect(erscheinungsbild()).toBe('hell');
94
95 schalte('(forced-colors: active)', true);
96 expect(erscheinungsbild()).toBe('dunkel');
97 });
98
99 it('folgt bei "System" weiterhin dem Wechsel der Systemvorgabe', () => {
100 stelleMedienabfragen(false, false);
101 applyTheme('system');
102 expect(erscheinungsbild()).toBe('hell');
103
104 schalte('(prefers-color-scheme: dark)', true);
105 expect(erscheinungsbild()).toBe('dunkel');
106 });
107
108 it('meldet die Empfänger des vorigen Aufrufs ab', () => {
109 // Sonst sammelten sich bei jedem Wechsel des Erscheinungsbilds weitere an,
110 // und der aelteste setzte das Attribut nach dem juengsten wieder zurueck.
111 stelleMedienabfragen(false, false);
112 applyTheme('system');
113 applyTheme('dunkel');
114 expect(abfragen.get('(prefers-color-scheme: dark)')?.empfaenger).toHaveLength(1);
115
116 schalte('(prefers-color-scheme: dark)', true);
117 expect(erscheinungsbild()).toBe('dunkel');
118 schalte('(prefers-color-scheme: dark)', false);
119 expect(erscheinungsbild()).toBe('dunkel');
120 });
121
122 it('bricht nicht ab, wenn die Umgebung keine Medienabfragen kennt', () => {
123 // `applyTheme` laeuft als eine der ersten Anweisungen des Programmstarts.
124 Reflect.deleteProperty(window, 'matchMedia');
125 expect(() => {
126 applyTheme('dunkel');
127 }).not.toThrow();
128 expect(erscheinungsbild()).toBe('dunkel');
129 });
130 });