// @vitest-environment node /** * Einstellbare Textabstände (WCAG 1.4.12). * * **Der Anlass.** Das Kriterium verlangt, dass sich Text mit weiterem Zeilen-, * Wort- und Zeichenabstand darstellen lässt, ohne dass Inhalt verlorengeht. Im * Browser erfüllt man das, indem nutzereigene Stilblätter toleriert werden; * der eigene Prüfplan (Z. 177) stellt den Fall mit einem * Entwicklerwerkzeug-Stilblatt nach. * * In der ausgelieferten Anwendung gibt es beides nicht – weder Erweiterungen * noch Entwicklerwerkzeuge, die sollen laut `docs/stand.md` sogar aus dem * Auslieferungsmenü verschwinden. Der Einzige, der die Abstände ändern kann, * ist die Anwendung selbst, und sie bot es bis 0.22.0 nicht an. Formal ist das * kein Verstoß (in einer geschlossenen Umgebung misst 1.4.12 die Toleranz * gegenüber Overrides, die es hier nicht geben kann) – praktisch ist es eine * Lücke bei genau denen, für die das Kriterium geschrieben wurde. */ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import { TEXTABSTAENDE, TEXTABSTAND_BEZEICHNUNG, TEXTABSTAND_ERKLAERUNG, TEXTABSTAND_STANDARD, istTextabstand, textabstandAnsage, textabstandBereinigen, } from '../src/shared/textabstand'; const wurzel = fileURLToPath(new URL('..', import.meta.url)); function stil(datei: string): string { return readFileSync(join(wurzel, 'src', 'renderer', 'src', 'styles', datei), 'utf8'); } const TOKENS_CSS = stil('tokens.css'); const GLOBAL_CSS = stil('global.css'); describe('Die Stufen', () => { it('beginnt bei der gewohnten Darstellung', () => { /* Wer nichts einstellt, soll nichts merken. Eine Anwendung, die von sich aus weiter setzt, hätte eine Meinung, die ihr nicht zusteht. */ expect(TEXTABSTAND_STANDARD).toBe('normal'); expect(TEXTABSTAENDE[0]).toBe('normal'); }); it('benennt und erklärt jede Stufe', () => { for (const stufe of TEXTABSTAENDE) { expect(TEXTABSTAND_BEZEICHNUNG[stufe].trim().length).toBeGreaterThan(0); expect(TEXTABSTAND_ERKLAERUNG[stufe].trim().length).toBeGreaterThan(0); } }); it('macht aus Unbrauchbarem die Vorgabe', () => { /* Ein Wert aus einer früheren Fassung oder von Hand in die Datei geschrieben darf die Darstellung nicht in einen Zustand bringen, den es nicht gibt. */ expect(textabstandBereinigen('gibt-es-nicht')).toBe(TEXTABSTAND_STANDARD); expect(textabstandBereinigen(undefined)).toBe(TEXTABSTAND_STANDARD); expect(textabstandBereinigen(42)).toBe(TEXTABSTAND_STANDARD); expect(textabstandBereinigen('weit')).toBe('weit'); }); it('erkennt gültige Stufen', () => { expect(istTextabstand('sehr-weit')).toBe(true); expect(istTextabstand('mittel')).toBe(false); }); it('sagt die gewählte Stufe an', () => { expect(textabstandAnsage('weit')).toContain('Weit'); }); }); describe('Das Stilblatt setzt die Stufen wirklich um', () => { it('führt für jede Stufe außer der Vorgabe einen eigenen Block', () => { for (const stufe of TEXTABSTAENDE.filter((s) => s !== TEXTABSTAND_STANDARD)) { expect(TOKENS_CSS, `Stufe ohne Regelblock: ${stufe}`).toContain( `:root[data-textabstand='${stufe}']`, ); } }); it('vergrößert Zeilen-, Wort- und Zeichenabstand mit jeder Stufe', () => { /* Drei Stufen, die sich nur im Namen unterscheiden, wären eine Attrappe einer Einstellung. */ const werte = (stufe: string): Record => { const anfang = TOKENS_CSS.indexOf(`:root[data-textabstand='${stufe}']`); const block = TOKENS_CSS.slice(anfang, TOKENS_CSS.indexOf('}', anfang)); return Object.fromEntries( [...block.matchAll(/--([a-z-]+):\s*([\d.]+)(em|rem)?/gu)].map((t) => [ t[1] ?? '', t[2] ?? '', ]), ); }; const weit = werte('weit'); const sehrWeit = werte('sehr-weit'); for (const eigenschaft of ['zeilenhoehe-text', 'wortabstand', 'zeichenabstand']) { const a = Number(weit[eigenschaft]); const b = Number(sehrWeit[eigenschaft]); expect(a, `${eigenschaft} fehlt in „weit“`).toBeGreaterThan(0); expect(b, `${eigenschaft}: „sehr weit“ ist nicht weiter als „weit“`).toBeGreaterThan(a); } }); it('erreicht die Messwerte des Kriteriums', () => { /* 1.4.12 nennt Wortabstand 0,16 em und Zeichenabstand 0,12 em als Mindestmaß. Die weiteste Stufe muss dort ankommen – sonst wäre die Einstellung eine Geste. */ const block = TOKENS_CSS.slice(TOKENS_CSS.indexOf(":root[data-textabstand='sehr-weit']")); const wort = /--wortabstand:\s*([\d.]+)em/u.exec(block); const zeichen = /--zeichenabstand:\s*([\d.]+)em/u.exec(block); expect(Number(wort?.[1])).toBeGreaterThanOrEqual(0.16); expect(Number(zeichen?.[1])).toBeGreaterThanOrEqual(0.12); }); it('reicht Wort- und Zeichenabstand an den Textkörper durch', () => { /* Ohne diese beiden Zeilen wären die Variablen gesetzt und wirkungslos. */ const body = GLOBAL_CSS.slice( GLOBAL_CSS.indexOf('body {'), GLOBAL_CSS.indexOf('}', GLOBAL_CSS.indexOf('body {')), ); expect(body).toMatch(/word-spacing:\s*var\(--wortabstand\)/u); expect(body).toMatch(/letter-spacing:\s*var\(--zeichenabstand\)/u); }); it('lässt den Feinsatz der Überschriften nicht gegen die Einstellung stehen', () => { /* Der negative Zeichenabstand von h1 und h2 steht hart im Stilblatt. Bliebe er unangetastet, zöge die Einstellung überall – nur nicht in den Überschriften, wo mehr Luft am meisten hilft. */ for (const ueberschrift of ['h1 {', 'h2 {']) { const anfang = GLOBAL_CSS.indexOf(ueberschrift); const block = GLOBAL_CSS.slice(anfang, GLOBAL_CSS.indexOf('}', anfang)); expect(block, `${ueberschrift} übersteuert den Zeichenabstand nicht`).toMatch( /letter-spacing:\s*max\([^)]*var\(--zeichenabstand\)\)/u, ); } }); it('gibt der Vorgabe eine rechenbare Länge statt „normal“', () => { /* `max(-0.015em, normal)` wäre ungültig – die ganze Angabe fiele aus, und der Feinsatz der Überschriften verschwände stillschweigend. */ const wurzelblock = TOKENS_CSS.slice(TOKENS_CSS.indexOf(':root {')); expect(wurzelblock).toMatch(/--zeichenabstand:\s*0em/u); expect(wurzelblock).toMatch(/--wortabstand:\s*0em/u); }); });