import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { lies, handyTeil } from './support/knHuelle.js' // Abschlussprüfung Phase E (Fix-Welle): Pins für die CSS-Befunde der neuen Seite. Sie lesen nur Quelltext, kein Browser. const HUELLE = lies('huelle.css') const regel = (css, selektor) => { const m = css.match(new RegExp(`(?:^|\\n)${selektor.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\{([^}]*)\\}`)) return m ? m[1] : '' } test('F2: die Sperre der Mitarbeiter-Kundenansicht (.kb-view-overlay) ist in der neuen Seite eine echte Sperre', () => { const o = regel(HUELLE, '.kb-view-overlay') assert.match(o, /position:\s*fixed/) assert.match(o, /inset:\s*0\b/) assert.match(o, /z-index:\s*(\d{3,})/) // über Konto (60), Leiste (40) und Banner assert.ok(Number(o.match(/z-index:\s*(\d+)/)?.[1]) > 60) assert.match(o, /background:\s*var\(--kn-bg\)/) // deckend weiß, kein Glas, kein rgba mit Durchsicht assert.doesNotMatch(o, /rgba|transparent|opacity/) const karte = regel(HUELLE, '.kb-view-overlay-card') assert.match(karte, /max-width:\s*\d+px/) const knopf = regel(HUELLE, '.kb-view-overlay-card button') assert.match(knopf, /min-height:\s*44px/) assert.match(knopf, /background:\s*var\(--kn-accent\)/) assert.match(regel(HUELLE, '.kb-view-overlay-card p'), /font-size:\s*\d+px/) }) test('F3: Fokusring der Shell-Reiter liegt innen (Spezifität über .kn [tabindex]:focus-visible aus module.css, 0-3-0)', () => { const BASIS = lies('basis.css') assert.match(HUELLE, /\n\.kn \.kn-t\[role="tab"\]:focus-visible\s*\{[^}]*outline-offset:\s*-2px/) assert.match(BASIS, /\n\.kn \.kn-reiter-t\[role="tab"\]:focus-visible\s*\{[^}]*outline-offset:\s*-2px/) // keine Regel mit niedrigerer Spezifität, die den Versatz setzt assert.doesNotMatch(HUELLE, /\n\.kn-t:focus-visible/) assert.doesNotMatch(BASIS, /\n\.kn-reiter-t:focus-visible/) // der aktive Reiter liegt auf dem schwarzen Gleiter: heller Innenring dort assert.match(HUELLE, /\.kn \.kn-balken:has\(\.kn-t:focus-visible\) \.kn-gleiter\s*\{[^}]*outline:\s*2px solid #fff[^}]*outline-offset:\s*-3px/) }) test('F3: Pflicht- und Zahlungslinks haben am Handy eine Tippfläche von 44 px', () => { const handy = handyTeil(lies('ansichten.css')) assert.match(handy, /\.kn-kuendigen a,\s*\.kn-widerruf a,\s*\.kn-links a,\s*\.kn-rechnung-offen a\s*\{[^}]*display:\s*inline-flex[^}]*min-height:\s*44px/) }) // F3: Safe-Area links und rechts (Querformat am iPhone, viewport-fit=cover): jeder feste Seitenrand der Hülle und der öffentlichen Seiten // wird mit max(Wert, env(safe-area-inset-left|right)) gebaut const alleRegeln = (css) => [...css.matchAll(/([^{}]+)\{([^{}]*)\}/g)].map((m) => ({ selektoren: m[1].replace(/\/\*[\s\S]*?\*\//g, '').split(',').map((x) => x.trim()), text: m[2] })) const hatInsets = (css, selektor) => alleRegeln(css).some((r) => r.selektoren.includes(selektor) && /env\(safe-area-inset-left\b/.test(r.text) && /env\(safe-area-inset-right\b/.test(r.text)) test('F3: Seitenränder der Hülle berücksichtigen die Safe-Area links und rechts', () => { for (const sel of ['.kn-kopf', '.kn-haupt', '.kn-blatt', '.kn-streifen', '.kn-konto', '#kn-fehler', '.kn-rw', '.kn-banner', '.kb-view-overlay']) assert.ok(hatInsets(HUELLE, sel), `huelle.css: ${sel}`) }) test('F3: Seitenränder der öffentlichen Seiten und des Chats berücksichtigen die Safe-Area links und rechts', () => { const oeff = lies('oeffentlich/oeffentlich.css') for (const sel of ['.kn-login-kopf', '.kn-login-haupt', '.kn-recht main', '.ab-seite .ab-innen']) assert.ok(hatInsets(oeff, sel), `oeffentlich.css: ${sel}`) assert.ok(hatInsets(lies('chat-composer.css'), '.chat-ov-top'), 'chat-composer.css: .chat-ov-top') }) // M3: Fehlermeldungen, die erst nach einer Aktion erscheinen, sind Live-Regionen (role="alert"), damit Screenreader sie vorlesen const quelle = (datei) => readFileSync(new URL(`../public/neu/${datei}`, import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '') test('M3: versteckte Fehlerzeilen der Bestellseiten, des Wechsels, des Speichers, der Gutschrift und der Zahlungsart tragen role="alert"', () => { for (const datei of ['ansicht-bestellen.js', 'ansicht-wechsel.js', 'ansicht-speicher.js', 'ansicht-gutschrift.js', 'vertraege-abo.js']) { const q = quelle(datei) // jedes Attributobjekt eines Elements mit der Fehlerklasse, das versteckt starten kann (hidden), hat eine Rolle const objekte = [...q.matchAll(/\{[^{}]*(?:K\.fehler|'kn-fehler'|fehlerKlasse)[^{}]*\}/g)].map((m) => m[0]).filter((o) => /\bhidden\b/.test(o)) assert.ok(objekte.length > 0, `${datei}: keine versteckte Fehlerzeile gefunden`) for (const o of objekte) assert.match(o, /role:\s*'alert'/, `${datei}: ${o}`) } // der Fehler der Wechsel-Vorschau steht nicht versteckt, wird aber nach dem Laden eingesetzt: auch er ist eine Live-Region assert.match(quelle('ansicht-wechsel.js'), /h\('p', \{ class: K\.fehler, role: 'alert' \}, lauf\.error\)/) }) test('M3: die Fehlertexte der öffentlichen Seiten (Login, Kündigen, Widerrufen) sind Live-Regionen', () => { for (const datei of ['login.html', 'kuendigen.html', 'widerrufen.html']) { const html = readFileSync(new URL(`../public/neu/oeffentlich/${datei}`, import.meta.url), 'utf8') const zeile = html.match(/
]*id="(?:login|form)-error"[^>]*>/)?.[0] ?? '' assert.match(zeile, /role="alert"/, `${datei}: ${zeile}`) } }) test('M4: der Mitarbeiter-Banner steht oben (sticky), Kopf, Menü, Blatt und Konto beginnen um seine Höhe (--kn-banner-h) darunter', () => { assert.match(HUELLE, /\.kn \{[^}]*--kn-banner-h:\s*0px/) assert.match(HUELLE, /\.kn-banner \{[^}]*position:\s*sticky;[^}]*top:\s*0/) assert.match(HUELLE, /\.kn-menue \{[^}]*inset:\s*var\(--kn-banner-h\) auto 0 0/) assert.match(HUELLE, /\.kn-kopf \{[^}]*position:\s*sticky;\s*top:\s*var\(--kn-banner-h\)/) assert.match(HUELLE, /\.kn-blatt \{[^}]*inset:\s*calc\(var\(--kn-banner-h\) \+ var\(--kn-kopf-h\) \+ env\(safe-area-inset-top\)\)/) assert.match(HUELLE, /\.kn-konto \{[^}]*top:\s*calc\(var\(--kn-banner-h\) \+ 56px\)/) assert.doesNotMatch(HUELLE, /\.kn-banner:not\(\[hidden\]\) ~ \.kn-huelle \.kn-menue/, 'die feste Höhe 36 px ist weg') const js = quelle('huelle.js') assert.match(js, /setProperty\?\.\('--kn-banner-h'/) }) test('M7: Bewegung reduziert nimmt auch die Fahrt der Fahrstuhl-Zeilen (Spezifität gleich, später im Blatt); #fortschritt hat scroll-margin-top für den haftenden Kopf', () => { const reduziert = HUELLE.slice(HUELLE.indexOf('@media (prefers-reduced-motion: reduce)')) assert.match(reduziert, /\.kn-leiste-reel\[data-etage="offen"\] #kn-zeile-haupt,\s*\.kn-leiste-reel:not\(\[data-etage="offen"\]\) #kn-zeile-etage\s*\{\s*transition:\s*none/) const A = lies('ansichten.css') assert.match(A, /\n\[data-abschnitt="aktivitaet"\] \{ scroll-margin-top: 16px/) assert.match(handyTeil(A), /\[data-abschnitt="aktivitaet"\] \{ scroll-margin-top: calc\(var\(--kn-banner-h\) \+ var\(--kn-kopf-h\) \+ env\(safe-area-inset-top\) \+ 8px\)/) })