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>
90 lines
6.2 KiB
JavaScript
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'/)
|
|
})
|