Files
Webklar-Kundenbereich/test/knEfixCss.test.js
T
ClaudeandClaude Sonnet 5.5 f8962ffa50 Kundenansicht neu E-Fix M4: Mitarbeiter-Banner verschiebt Blatt und Menü nicht mehr unter den Kopf
Der Banner stand im Fluss (relative) und schob den Kopf nach unten, das am Handy feste Blatt begann aber fest bei der Kopfhöhe: es deckte den
unteren Teil des Kopfs samt Profilbild ab (Konto und Abmelden nicht erreichbar); am Desktop blieb das Menü bei fest 36 px stehen, der Banner
ist aber 40 px hoch und beim Scrollen blieb eine Lücke. Jetzt bleibt der Banner oben stehen (sticky), huelle.js setzt --kn-banner-h auf seine
Höhe (auch bei Umbruch am Handy), und Kopf, Menü, Blatt und Konto-Popover beginnen darunter. Werkzeug-Ansicht kn-huelle-banner-blatt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:43:20 +00:00

90 lines
6.2 KiB
JavaScript

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(/<p[^>]*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'/)
})