waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ app tests textabstand.test.ts

6,3 KB Rohdatei
app/tests/textabstand.test.ts — 156 Zeilen
1 // @vitest-environment node
2 /**
3 * Einstellbare Textabstände (WCAG 1.4.12).
4 *
5 * **Der Anlass.** Das Kriterium verlangt, dass sich Text mit weiterem Zeilen-,
6 * Wort- und Zeichenabstand darstellen lässt, ohne dass Inhalt verlorengeht. Im
7 * Browser erfüllt man das, indem nutzereigene Stilblätter toleriert werden;
8 * der eigene Prüfplan (Z. 177) stellt den Fall mit einem
9 * Entwicklerwerkzeug-Stilblatt nach.
10 *
11 * In der ausgelieferten Anwendung gibt es beides nicht – weder Erweiterungen
12 * noch Entwicklerwerkzeuge, die sollen laut `docs/stand.md` sogar aus dem
13 * Auslieferungsmenü verschwinden. Der Einzige, der die Abstände ändern kann,
14 * ist die Anwendung selbst, und sie bot es bis 0.22.0 nicht an. Formal ist das
15 * kein Verstoß (in einer geschlossenen Umgebung misst 1.4.12 die Toleranz
16 * gegenüber Overrides, die es hier nicht geben kann) – praktisch ist es eine
17 * Lücke bei genau denen, für die das Kriterium geschrieben wurde.
18 */
19
20 import { readFileSync } from 'node:fs';
21 import { join } from 'node:path';
22 import { fileURLToPath } from 'node:url';
23
24 import { describe, expect, it } from 'vitest';
25
26 import {
27 TEXTABSTAENDE,
28 TEXTABSTAND_BEZEICHNUNG,
29 TEXTABSTAND_ERKLAERUNG,
30 TEXTABSTAND_STANDARD,
31 istTextabstand,
32 textabstandAnsage,
33 textabstandBereinigen,
34 } from '../src/shared/textabstand';
35
36 const wurzel = fileURLToPath(new URL('..', import.meta.url));
37
38 function stil(datei: string): string {
39 return readFileSync(join(wurzel, 'src', 'renderer', 'src', 'styles', datei), 'utf8');
40 }
41
42 const TOKENS_CSS = stil('tokens.css');
43 const GLOBAL_CSS = stil('global.css');
44
45 describe('Die Stufen', () => {
46 it('beginnt bei der gewohnten Darstellung', () => {
47 /* Wer nichts einstellt, soll nichts merken. Eine Anwendung, die von sich
48 aus weiter setzt, hätte eine Meinung, die ihr nicht zusteht. */
49 expect(TEXTABSTAND_STANDARD).toBe('normal');
50 expect(TEXTABSTAENDE[0]).toBe('normal');
51 });
52
53 it('benennt und erklärt jede Stufe', () => {
54 for (const stufe of TEXTABSTAENDE) {
55 expect(TEXTABSTAND_BEZEICHNUNG[stufe].trim().length).toBeGreaterThan(0);
56 expect(TEXTABSTAND_ERKLAERUNG[stufe].trim().length).toBeGreaterThan(0);
57 }
58 });
59
60 it('macht aus Unbrauchbarem die Vorgabe', () => {
61 /* Ein Wert aus einer früheren Fassung oder von Hand in die Datei
62 geschrieben darf die Darstellung nicht in einen Zustand bringen, den
63 es nicht gibt. */
64 expect(textabstandBereinigen('gibt-es-nicht')).toBe(TEXTABSTAND_STANDARD);
65 expect(textabstandBereinigen(undefined)).toBe(TEXTABSTAND_STANDARD);
66 expect(textabstandBereinigen(42)).toBe(TEXTABSTAND_STANDARD);
67 expect(textabstandBereinigen('weit')).toBe('weit');
68 });
69
70 it('erkennt gültige Stufen', () => {
71 expect(istTextabstand('sehr-weit')).toBe(true);
72 expect(istTextabstand('mittel')).toBe(false);
73 });
74
75 it('sagt die gewählte Stufe an', () => {
76 expect(textabstandAnsage('weit')).toContain('Weit');
77 });
78 });
79
80 describe('Das Stilblatt setzt die Stufen wirklich um', () => {
81 it('führt für jede Stufe außer der Vorgabe einen eigenen Block', () => {
82 for (const stufe of TEXTABSTAENDE.filter((s) => s !== TEXTABSTAND_STANDARD)) {
83 expect(TOKENS_CSS, `Stufe ohne Regelblock: ${stufe}`).toContain(
84 `:root[data-textabstand='${stufe}']`,
85 );
86 }
87 });
88
89 it('vergrößert Zeilen-, Wort- und Zeichenabstand mit jeder Stufe', () => {
90 /* Drei Stufen, die sich nur im Namen unterscheiden, wären eine Attrappe
91 einer Einstellung. */
92 const werte = (stufe: string): Record<string, string> => {
93 const anfang = TOKENS_CSS.indexOf(`:root[data-textabstand='${stufe}']`);
94 const block = TOKENS_CSS.slice(anfang, TOKENS_CSS.indexOf('}', anfang));
95 return Object.fromEntries(
96 [...block.matchAll(/--([a-z-]+):\s*([\d.]+)(em|rem)?/gu)].map((t) => [
97 t[1] ?? '',
98 t[2] ?? '',
99 ]),
100 );
101 };
102
103 const weit = werte('weit');
104 const sehrWeit = werte('sehr-weit');
105
106 for (const eigenschaft of ['zeilenhoehe-text', 'wortabstand', 'zeichenabstand']) {
107 const a = Number(weit[eigenschaft]);
108 const b = Number(sehrWeit[eigenschaft]);
109 expect(a, `${eigenschaft} fehlt in „weit“`).toBeGreaterThan(0);
110 expect(b, `${eigenschaft}: „sehr weit“ ist nicht weiter als „weit“`).toBeGreaterThan(a);
111 }
112 });
113
114 it('erreicht die Messwerte des Kriteriums', () => {
115 /* 1.4.12 nennt Wortabstand 0,16 em und Zeichenabstand 0,12 em als
116 Mindestmaß. Die weiteste Stufe muss dort ankommen – sonst wäre die
117 Einstellung eine Geste. */
118 const block = TOKENS_CSS.slice(TOKENS_CSS.indexOf(":root[data-textabstand='sehr-weit']"));
119 const wort = /--wortabstand:\s*([\d.]+)em/u.exec(block);
120 const zeichen = /--zeichenabstand:\s*([\d.]+)em/u.exec(block);
121
122 expect(Number(wort?.[1])).toBeGreaterThanOrEqual(0.16);
123 expect(Number(zeichen?.[1])).toBeGreaterThanOrEqual(0.12);
124 });
125
126 it('reicht Wort- und Zeichenabstand an den Textkörper durch', () => {
127 /* Ohne diese beiden Zeilen wären die Variablen gesetzt und wirkungslos. */
128 const body = GLOBAL_CSS.slice(
129 GLOBAL_CSS.indexOf('body {'),
130 GLOBAL_CSS.indexOf('}', GLOBAL_CSS.indexOf('body {')),
131 );
132 expect(body).toMatch(/word-spacing:\s*var\(--wortabstand\)/u);
133 expect(body).toMatch(/letter-spacing:\s*var\(--zeichenabstand\)/u);
134 });
135
136 it('lässt den Feinsatz der Überschriften nicht gegen die Einstellung stehen', () => {
137 /* Der negative Zeichenabstand von h1 und h2 steht hart im Stilblatt.
138 Bliebe er unangetastet, zöge die Einstellung überall – nur nicht in
139 den Überschriften, wo mehr Luft am meisten hilft. */
140 for (const ueberschrift of ['h1 {', 'h2 {']) {
141 const anfang = GLOBAL_CSS.indexOf(ueberschrift);
142 const block = GLOBAL_CSS.slice(anfang, GLOBAL_CSS.indexOf('}', anfang));
143 expect(block, `${ueberschrift} übersteuert den Zeichenabstand nicht`).toMatch(
144 /letter-spacing:\s*max\([^)]*var\(--zeichenabstand\)\)/u,
145 );
146 }
147 });
148
149 it('gibt der Vorgabe eine rechenbare Länge statt „normal“', () => {
150 /* `max(-0.015em, normal)` wäre ungültig – die ganze Angabe fiele aus,
151 und der Feinsatz der Überschriften verschwände stillschweigend. */
152 const wurzelblock = TOKENS_CSS.slice(TOKENS_CSS.indexOf(':root {'));
153 expect(wurzelblock).toMatch(/--zeichenabstand:\s*0em/u);
154 expect(wurzelblock).toMatch(/--wortabstand:\s*0em/u);
155 });
156 });