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(/(? { 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)}`) })