import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { SCHRIFT_STANDARD, SCHRIFT_STUFEN, gueltigeStufe, liesSchriftgroesse, nachbarstufe, schreibeSchriftgroesse, schriftsicherung, } from '../../electron/schriftgroesse'; /** * Befund L11 (EN 301 549 Kap. 11.7). * * Der Altbestand hatte weder Stufen noch eine Sicherung - dieses Modul gab es * nicht. Geprueft wird deshalb nicht nur, dass es die Stufen gibt, sondern die * beiden Eigenschaften, an denen der Befund haengt: Die Einstellung ueberlebt * den Programmstart, und ein unbrauchbarer Dateiinhalt haelt die Anwendung nicht * auf. */ describe('Schriftgroesse: Stufen', () => { it('enthält die von WCAG 1.4.4 geforderten 200 % und die im Befund genannten 175 %', () => { expect(SCHRIFT_STUFEN).toContain(200); expect(SCHRIFT_STUFEN).toContain(175); expect(SCHRIFT_STUFEN).toContain(SCHRIFT_STANDARD); }); it('steigt lückenlos an und lässt auch kleiner als 100 % zu', () => { const sortiert = [...SCHRIFT_STUFEN].sort((a, b) => a - b); expect(SCHRIFT_STUFEN).toEqual(sortiert); expect(new Set(SCHRIFT_STUFEN).size).toBe(SCHRIFT_STUFEN.length); expect(Math.min(...SCHRIFT_STUFEN)).toBeLessThan(SCHRIFT_STANDARD); }); it('rundet Zwischenwerte auf die nächstgelegene Stufe', () => { // Ein Zwischenwert entsteht durch Strg+Mausrad oder durch Handarbeit an der // Datei. Er darf nicht bestehen bleiben - sonst zeigt die Auswahlmarke im // Menue auf eine Stufe, die gar nicht gilt. expect(gueltigeStufe(160)).toBe(150); expect(gueltigeStufe(170)).toBe(175); expect(gueltigeStufe(5)).toBe(Math.min(...SCHRIFT_STUFEN)); expect(gueltigeStufe(5000)).toBe(Math.max(...SCHRIFT_STUFEN)); }); it('weist alles zurück, was keine brauchbare Zahl ist', () => { expect(gueltigeStufe('175')).toBe(SCHRIFT_STANDARD); expect(gueltigeStufe(null)).toBe(SCHRIFT_STANDARD); expect(gueltigeStufe(undefined)).toBe(SCHRIFT_STANDARD); expect(gueltigeStufe(Number.NaN)).toBe(SCHRIFT_STANDARD); expect(gueltigeStufe(Number.POSITIVE_INFINITY)).toBe(SCHRIFT_STANDARD); expect(gueltigeStufe({ prozent: 175 })).toBe(SCHRIFT_STANDARD); }); it('geht eine Stufe weiter und bleibt an den Enden stehen', () => { const kleinste = Math.min(...SCHRIFT_STUFEN); const groesste = Math.max(...SCHRIFT_STUFEN); expect(nachbarstufe(100, 1)).toBe(125); expect(nachbarstufe(125, -1)).toBe(100); expect(nachbarstufe(groesste, 1)).toBe(groesste); expect(nachbarstufe(kleinste, -1)).toBe(kleinste); }); it('führt von 100 % in fünf Schritten mindestens auf 200 %', () => { let stufe = SCHRIFT_STANDARD; for (let i = 0; i < 5; i += 1) stufe = nachbarstufe(stufe, 1); expect(stufe).toBeGreaterThanOrEqual(200); }); }); describe('Schriftgroesse: Sicherung', () => { let ordner = ''; let datei = ''; beforeEach(() => { ordner = mkdtempSync(path.join(tmpdir(), 'lsa-ansicht-')); datei = path.join(ordner, 'ansicht.json'); }); afterEach(() => { rmSync(ordner, { recursive: true, force: true }); }); it('gibt die Vorgabe zurück, solange nichts gesichert wurde', () => { expect(liesSchriftgroesse(datei)).toBe(SCHRIFT_STANDARD); }); it('überlebt den Programmstart - das ist der Kern des Befunds', () => { schreibeSchriftgroesse(datei, 175); expect(liesSchriftgroesse(datei)).toBe(175); }); it('behält fremde Schlüssel der Datei', () => { // Fenstergroesse und Fensterlage gehoeren spaeter in dieselbe Datei; sie // duerfen beim Sichern der Schriftgroesse nicht verlorengehen. writeFileSync(datei, JSON.stringify({ fensterbreite: 1200 }), 'utf8'); schreibeSchriftgroesse(datei, 150); const gelesen: unknown = JSON.parse(readFileSync(datei, 'utf8')); expect(gelesen).toMatchObject({ fensterbreite: 1200, schriftgroesseProzent: 150 }); }); it('schreibt ohne Byte-Reihenfolge-Marke und mit abschließendem Zeilenende', () => { schreibeSchriftgroesse(datei, 125); const roh = readFileSync(datei); expect(roh[0]).not.toBe(0xef); expect(roh.toString('utf8').endsWith('\n')).toBe(true); }); it('hält bei beschädigtem Inhalt an der Vorgabe fest, statt zu scheitern', () => { writeFileSync(datei, '{ das ist kein JSON', 'utf8'); expect(liesSchriftgroesse(datei)).toBe(SCHRIFT_STANDARD); writeFileSync(datei, '[]', 'utf8'); expect(liesSchriftgroesse(datei)).toBe(SCHRIFT_STANDARD); writeFileSync(datei, JSON.stringify({ schriftgroesseProzent: 'sehr groß' }), 'utf8'); expect(liesSchriftgroesse(datei)).toBe(SCHRIFT_STANDARD); }); it('scheitert nicht, wenn der Ort nicht beschreibbar ist', () => { const unerreichbar = path.join(ordner, 'gibt-es-nicht', 'ansicht.json'); expect(() => { schreibeSchriftgroesse(unerreichbar, 175); }).not.toThrow(); expect(liesSchriftgroesse(unerreichbar)).toBe(SCHRIFT_STANDARD); }); }); /** * Verzoegerte Sicherung (Befund B5). * * `zoom-changed` feuert bei jeder Raste von Strg+Mausrad, und jede Raste lief * in ein `writeFileSync` im HAUPTPROZESS - also in den Prozess, der die Fenster * bedient. Auf einem servergespeicherten Benutzerprofil blockiert das die * Oberflaeche pro Raste. Geschrieben wurde ausserdem auch dann, wenn sich gar * nichts geaendert hatte: An beiden Enden der Stufenliste gibt `nachbarstufe` * denselben Wert zurueck. */ describe('Schriftgroesse: Sicherung wartet die Ruhezeit ab', () => { let ordner = ''; let datei = ''; beforeEach(() => { vi.useFakeTimers(); ordner = mkdtempSync(path.join(tmpdir(), 'lsa-sicherung-')); datei = path.join(ordner, 'ansicht.json'); }); afterEach(() => { vi.useRealTimers(); rmSync(ordner, { recursive: true, force: true }); }); /** Wie oft die Datei seit dem letzten Aufruf neu geschrieben wurde. */ function schreibungen(): number { try { readFileSync(datei, 'utf8'); return 1; } catch { return 0; } } it('macht aus einem Dutzend Rasten eine einzige Schreibung', () => { const sicherung = schriftsicherung(datei, 100, 750); for (const stufe of [125, 150, 175, 200, 250]) sicherung.plane(stufe); expect(schreibungen(), 'schon vor der Ruhezeit geschrieben').toBe(0); vi.advanceTimersByTime(800); expect(liesSchriftgroesse(datei)).toBe(250); }); it('schreibt gar nicht, wenn sich die Stufe nicht aendert', () => { // An beiden Enden der Liste gibt `nachbarstufe` denselben Wert zurueck - // wer am Anschlag weiterdreht, loeste bis hierher Schreibung um Schreibung // aus. const sicherung = schriftsicherung(datei, 250, 750); for (let i = 0; i < 12; i += 1) sicherung.plane(nachbarstufe(250, 1)); vi.advanceTimersByTime(2000); expect(schreibungen(), 'ohne Aenderung geschrieben').toBe(0); }); it('verwirft eine Aenderung, die vor Ablauf wieder zurueckgenommen wird', () => { const sicherung = schriftsicherung(datei, 100, 750); sicherung.plane(150); vi.advanceTimersByTime(300); sicherung.plane(100); vi.advanceTimersByTime(2000); expect(schreibungen(), 'hin und zurueck ist keine Aenderung').toBe(0); }); it('schreibt beim Programmende sofort, was noch aussteht', () => { // Eine Einstellung, die eine halbe Sekunde vor dem Beenden getroffen wurde, // darf nicht verlorengehen. const sicherung = schriftsicherung(datei, 100, 750); sicherung.plane(175); sicherung.jetzt(); expect(liesSchriftgroesse(datei)).toBe(175); // Und ein zweites `jetzt()` schreibt nicht noch einmal. rmSync(datei); sicherung.jetzt(); expect(schreibungen()).toBe(0); }); it('haelt die Buchfuehrung ueber mehrere Vorgaenge hinweg', () => { const sicherung = schriftsicherung(datei, 100, 750); sicherung.plane(150); vi.advanceTimersByTime(800); expect(liesSchriftgroesse(datei)).toBe(150); // Derselbe Wert noch einmal: keine zweite Schreibung. rmSync(datei); sicherung.plane(150); vi.advanceTimersByTime(800); expect(schreibungen()).toBe(0); sicherung.plane(200); vi.advanceTimersByTime(800); expect(liesSchriftgroesse(datei)).toBe(200); }); });