N5: Der Fokus geht nach dem Blättern über restoreFocus (workersFocus.js) mit preventScroll an die Seitenzeile zurück, aber nur, wenn er wirklich verloren ging (body, nirgends, alter Knopf ausgehängt). Hat der Nutzer inzwischen Suchfeld oder Raumfilter angesteuert, bleibt er dort. N6: Scheitert eine Anfrage, steht unter der Meldung „Erneut versuchen“; der Knopf stellt genau diese Anfrage (Raum, Suche, Seite oder Eintrag) noch einmal und die Seitenzeile bekommt danach den Fokus zurück. CSS: .wk-bl-retry, am Handy 40 px. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
98 lines
7.6 KiB
JavaScript
98 lines
7.6 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { readFileSync, readdirSync } from 'node:fs'
|
|
|
|
test('workers.css: nur wk-Klassen, @font-face, keine Hex-Farben, reduzierte Bewegung', () => {
|
|
const css = readFileSync(new URL('../../src/styles/workers.css', import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '') // Kommentare vorher entfernen
|
|
for (const m of css.matchAll(/([^{}]+)\{/g)) { const pre = m[1].trim(); if (pre.startsWith('@')) continue; for (const sel of pre.split(',')) assert.ok(sel.trim().startsWith('.wk-'), sel) } // jede Regel beginnt mit einer wk-Klasse (Nachfahren dürfen globale Klassen nutzen)
|
|
assert.match(css, /@font-face\s*\{[^}]*font-family:\s*"Pixelify Sans"[^}]*PixelifySans-Variable\.woff2[^}]*font-display:\s*swap/s); assert.doesNotMatch(css, /#[0-9a-fA-F]{3,8}\b/)
|
|
assert.match(css, /prefers-reduced-motion/); assert.match(css, /@media \(max-width: 1100px\)/); assert.match(css, /@media \(max-width: 860px\)/)
|
|
})
|
|
test('jede im JSX verwendete wk-Klasse ist definiert', () => {
|
|
const css = readFileSync(new URL('../../src/styles/workers.css', import.meta.url), 'utf8'); const dir = new URL('../../src/components/workers/', import.meta.url)
|
|
for (const f of readdirSync(dir).filter((x) => x.endsWith('.jsx'))) for (const m of readFileSync(new URL(f, dir), 'utf8').matchAll(/(?<![\w-])wk-[a-z0-9-]+/g)) assert.ok(css.includes('.' + m[0]), `${f}: ${m[0]}`) // Lookbehind: das Attribut data-wk-worker ist keine Klasse
|
|
})
|
|
test('workers.css: Zoomknöpfe unten rechts (oben läge die Reihe über dem Schild der Kommandobrücke), Meldungen halten Abstand zu den Kennzahlen', () => {
|
|
const css = readFileSync(new URL('../../src/styles/workers.css', import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '')
|
|
const rule = (selector) => css.match(new RegExp(`${selector.replace(/[.~]/g, '\\$&')}\\s*\\{([^}]*)\\}`))?.[1] ?? ''
|
|
assert.match(rule('.wk-zoom'), /bottom:\s*8px/); assert.match(rule('.wk-zoom'), /right:\s*8px/); assert.doesNotMatch(rule('.wk-zoom'), /\btop:/)
|
|
assert.match(rule('.wk-toolbar ~ .msg'), /margin-bottom:\s*16px/)
|
|
})
|
|
test('workers.css: die Karte bleibt beim Anspringen unter der festen Leiste der Handy-Ansicht (scroll-margin-top)', () => {
|
|
const css = readFileSync(new URL('../../src/styles/workers.css', import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '')
|
|
assert.match(css, /\.wk-card\s*\{[^}]*scroll-margin-top:\s*12px/)
|
|
const phone = css.slice(css.indexOf('@media (max-width: 860px)'))
|
|
assert.match(phone, /\.wk-card\s*\{\s*scroll-margin-top:\s*64px/) // 52 px Leiste + Luft
|
|
})
|
|
test('workers.css: die globale Regel .page section + section (36 px) verlängert den Abstand der Panel-Abschnitte nicht (W4)', () => {
|
|
const global = readFileSync(new URL('../../src/styles/global.css', import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '')
|
|
const css = readFileSync(new URL('../../src/styles/workers.css', import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '')
|
|
assert.match(global, /\.page section \+ section\s*\{\s*margin-top:\s*36px/) // die Regel, gegen die das Panel sich behaupten muss: Spezifität (0,1,2)
|
|
const m = css.match(/(\.wk-prospect [^{}]*\.wk-prospect-section \+ \.wk-prospect-section)\s*\{([^}]*)\}/)
|
|
assert.ok(m, 'Regel für zwei benachbarte Abschnitte im Panel')
|
|
assert.match(m[2], /margin-top:\s*0\b/)
|
|
// Spezifität: zwei Klassen (0,2,0) schlagen schon (0,1,2); eine einzelne Klasse (0,1,0) verlöre
|
|
assert.ok((m[1].match(/\.[\w-]+/g) || []).length >= 2, 'mindestens zwei Klassen')
|
|
// der Abstand der Abschnitte kommt aus gap, nicht aus einem eigenen margin-top
|
|
assert.match(css, /\.wk-prospect\s*\{[^}]*gap:\s*16px/)
|
|
assert.doesNotMatch(css.match(/\.wk-prospect-section\s*\{([^}]*)\}/)[1], /margin-top/)
|
|
})
|
|
|
|
// ---- Bot-Leads (Endprüfung P6: C5, W1, W2) ----
|
|
const stripped = (file) => readFileSync(new URL('../../src/styles/' + file, import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '')
|
|
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
// Rumpf der Regel mit genau diesem Selektor am Anfang der Regel (weder Teil eines längeren Selektors noch einer Gruppe mit Komma)
|
|
const body = (css, selector) => css.match(new RegExp(`(?:^|[{}])\\s*${escapeRe(selector)}\\s*\\{([^}]*)\\}`))?.[1] ?? null
|
|
const decl = (rule, prop) => rule?.match(new RegExp(`(?:^|[;\\s])${prop}\\s*:\\s*([^;]+)`))?.[1].trim() ?? null
|
|
// Spezifität (ids, Klassen/Attribute/Pseudoklassen, Elemente); Vergleich von links nach rechts
|
|
const specificity = (selector) => {
|
|
const s = selector.replace(/\[[^\]]*\]/g, '.a')
|
|
const ids = (s.match(/#[\w-]+/g) || []).length
|
|
const classes = (s.match(/\.[\w-]+|:(?!:)[\w-]+/g) || []).length
|
|
const elements = (s.replace(/\.[\w-]+|#[\w-]+|:+[\w-]+/g, ' ').match(/(?:^|[\s>+~])[a-z][\w-]*/gi) || []).length
|
|
return [ids, classes, elements]
|
|
}
|
|
const beats = (a, b) => { for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i]; return false }
|
|
const luminance = (hex) => {
|
|
const c = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255).map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4))
|
|
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
|
|
}
|
|
const contrast = (a, b) => { const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x); return (hi + 0.05) / (lo + 0.05) }
|
|
const token = (name) => stripped('global.css').match(new RegExp(`${name}:\\s*(#[0-9a-fA-F]{6})`))?.[1]
|
|
|
|
test('workers.css: gekürzte Zeilen der Bot-Leads bleiben lesbar (Text >= 4,5:1 auf dem Seiten- und dem Zeilengrund), grau über --text-2 statt --text-3 (C5)', () => {
|
|
const css = stripped('workers.css')
|
|
const rule = body(css, '.wk-bl-cut td')
|
|
assert.ok(rule, 'Regel .wk-bl-cut td')
|
|
assert.equal(decl(rule, 'color'), 'var(--text-2)')
|
|
assert.doesNotMatch(css, /\.wk-bl-cut[^{]*\{[^}]*--text-3/) // keine Regel der gekürzten Zeile greift zu --text-3
|
|
for (const ground of ['--bg', '--bg-subtle']) {
|
|
const ratio = contrast(token('--text-2'), token(ground))
|
|
assert.ok(ratio >= 4.5, `${ground}: ${ratio.toFixed(2)}:1`)
|
|
}
|
|
})
|
|
|
|
test('workers.css: am Handy sind die Blätterknöpfe, „Alle anzeigen“ und „Erneut versuchen“ der Bot-Leads 40 px hoch, ohne die globale .btn-Regel zu ändern (C5, W1)', () => {
|
|
const css = stripped('workers.css')
|
|
const phone = css.slice(css.indexOf('@media (max-width: 860px)'))
|
|
for (const selector of ['.wk-bl-pager .btn', '.wk-bl-single .btn', '.wk-bl-retry .btn']) {
|
|
assert.equal(decl(body(phone, selector), 'min-height'), '40px', selector)
|
|
// min-height schlägt height: 32px der globalen .btn-Regel, und die Spezifität (0,2,0) schlägt .btn (0,1,0): kein !important nötig
|
|
assert.ok(beats(specificity(selector), specificity('.btn')), selector)
|
|
assert.doesNotMatch(body(phone, selector), /!important/)
|
|
}
|
|
assert.equal(decl(body(stripped('global.css'), '.btn'), 'height'), '32px') // das, wogegen die Knöpfe sich behaupten
|
|
assert.equal(body(css, '.btn'), null) // keine globale Änderung aus der Hustler-Datei
|
|
})
|
|
|
|
test('workers.css: der Ort in der Bot-Leads-Tabelle bricht nicht mitten im Wort (break-word schlägt die globale Regel mit anywhere) (W2)', () => {
|
|
const css = stripped('workers.css')
|
|
const global = stripped('global.css')
|
|
const wanted = '.wk-bl .wk-bl-table td .cell-sub'
|
|
const globalSelector = '.table td .cell-sub'
|
|
assert.equal(decl(body(global, globalSelector), 'overflow-wrap'), 'anywhere') // die globale Regel, die der Bot-Leads-Ort nicht erben soll
|
|
assert.equal(decl(body(css, wanted), 'overflow-wrap'), 'break-word')
|
|
// streng größer, nicht gleich: bei Gleichstand entschiede die Reihenfolge der Stylesheets im Bündel
|
|
assert.ok(beats(specificity(wanted), specificity(globalSelector)), `${wanted} ${specificity(wanted)} gegen ${specificity(globalSelector)}`)
|
|
})
|