150 lines
7.7 KiB
JavaScript
150 lines
7.7 KiB
JavaScript
import { test } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { readFileSync, readdirSync } from 'node:fs'
|
||
|
||
// Prüftest für public/neu/module.css (P4 Tasks 9 bis 13): jede Regel steht unter .kn, nur Farben der Palette, keine Verläufe, kein Glas,
|
||
// kein Dunkelmodus, keine Animation; für jedes Präfix aus FERTIG ist jede Klasse der alten CSS-Datei unter .kn gestaltet.
|
||
|
||
const PUBLIC = new URL('../public/', import.meta.url)
|
||
const lies = (datei) => readFileSync(new URL(datei, PUBLIC), 'utf8')
|
||
const neuDateien = () => readdirSync(new URL('neu/', PUBLIC)).filter((f) => f.endsWith('.css')).sort()
|
||
const neuCss = () => neuDateien().map((f) => lies(`neu/${f}`)).join('\n')
|
||
const MODUL = () => lies('neu/module.css')
|
||
const jsModule = () => [
|
||
...readdirSync(PUBLIC).filter((f) => f.endsWith('.js')).map((f) => lies(f)),
|
||
...readdirSync(new URL('neu/', PUBLIC)).filter((f) => f.endsWith('.js')).map((f) => lies(`neu/${f}`)),
|
||
].join('\n')
|
||
|
||
const ohneKommentare = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '')
|
||
|
||
/** [{ selektor, koerper }] – verschachtelte @media/@supports flach, @keyframes und @font-face ohne Schritte. */
|
||
function regeln(css) {
|
||
const text = ohneKommentare(css)
|
||
const lies2 = (quelle) => {
|
||
const aus = []
|
||
let i = 0
|
||
while (i < quelle.length) {
|
||
const auf = quelle.indexOf('{', i)
|
||
if (auf === -1) break
|
||
const kopf = quelle.slice(i, auf).trim()
|
||
let tiefe = 1
|
||
let j = auf + 1
|
||
while (j < quelle.length && tiefe > 0) {
|
||
if (quelle[j] === '{') tiefe += 1
|
||
else if (quelle[j] === '}') tiefe -= 1
|
||
j += 1
|
||
}
|
||
const innen = quelle.slice(auf + 1, j - 1)
|
||
if (/^@(media|supports|container|layer)\b/.test(kopf)) aus.push(...lies2(innen))
|
||
else if (!kopf.startsWith('@')) aus.push({ selektor: kopf, koerper: innen })
|
||
i = j
|
||
}
|
||
return aus
|
||
}
|
||
return lies2(text)
|
||
}
|
||
|
||
/** Selektoren einer Regelliste; Kommalisten aufgespalten, Klammer-bewusst (:is(), :not(), [attr="a,b"]). */
|
||
function selektoren(css) {
|
||
const aus = []
|
||
for (const { selektor } of regeln(css)) {
|
||
let tiefe = 0
|
||
let quelle = ''
|
||
let anfuehrung = null
|
||
for (const z of selektor) {
|
||
if (anfuehrung) { quelle += z; if (z === anfuehrung) anfuehrung = null; continue }
|
||
if (z === '"' || z === "'") { anfuehrung = z; quelle += z; continue }
|
||
if (z === '(' || z === '[') tiefe += 1
|
||
if (z === ')' || z === ']') tiefe -= 1
|
||
if (z === ',' && tiefe === 0) { aus.push(quelle.trim()); quelle = ''; continue }
|
||
quelle += z
|
||
}
|
||
if (quelle.trim()) aus.push(quelle.trim())
|
||
}
|
||
return aus
|
||
}
|
||
|
||
/** Klassen mit Präfix aus den Selektoren (nur echte Klassen: ein Punkt direkt davor, nie `.kn-fn-…` für `fn`). */
|
||
function klassen(css, praefix) {
|
||
const treffer = new Set()
|
||
const muster = new RegExp(`\\.(${praefix}-[A-Za-z0-9_-]+)`, 'g')
|
||
for (const s of selektoren(css)) for (const m of s.matchAll(muster)) treffer.add(m[1])
|
||
return treffer
|
||
}
|
||
|
||
const FERTIG = ['tm'] // Präfixe mit geprüfter Abdeckung; Task 11 bis 13 ergänzen sie
|
||
const UNGENUTZT = new Set([]) // alte Klassen, die in keiner JS-Datei vorkommen; der Test prüft das
|
||
const ALT = { tm: 'termine.css', sh: 'shop.css', bk: 'buchungen.css', zt: 'zeiten.css', an: 'analyse.css', hs: 'analyse.css', nw: 'funktionen.css' }
|
||
const FN_NEWS = ['fn-project', 'fn-booking', 'fn-price', 'fn-estimate', 'fn-desc-link', 'fn-consent', 'fn-thanks', 'fn-status', 'fn-facts', 'fn-pill', 'fn-pill-setup']
|
||
const ALT_KLASSEN = ['kb-panel', 'kb-btn', 'kb-btn-primary', 'kb-btn-outline', 'kb-empty', 'kb-link', 'kb-checks', 'abo-actions', 'abo-btn', 'hidden', 'muted', 'error']
|
||
const PALETTE = new Set(['#fff', '#ffffff', '#1a1f36', '#5e6678', '#8a93a3', '#ebeef1', '#d8dde3', '#f6f8fa', '#f8f9fb', '#111', '#111111', '#414552', '#e0f2fe', '#075985', '#fcedb9', '#a82c00', '#efeafe', '#5b21b6', '#d7f7c2', '#006908', '#ffe7f2', '#b3093c', '#c0123c'])
|
||
const ZETTEL = /#fff6d6|#ddeffb|#ffe1d9/i
|
||
const RING = new Set(['60,66,87,.16', '60,66,87,0.16', '0,0,0,.06', '0,0,0,0.06'])
|
||
const HANDY = '@media (max-width: 880px), (pointer: coarse) and (max-height: 500px)'
|
||
|
||
test('Hilfen: Regeln, Selektoren und Klassen werden richtig gelesen', () => {
|
||
const css = '/* x */ .a, .b:not(.c, .d) { color: #fff } @media (x) { .e { margin: 0 } } @keyframes k { from { opacity: 0 } } .f[data-x="a,b"] { top: 0 }'
|
||
assert.deepEqual(regeln(css).map((r) => r.selektor), ['.a, .b:not(.c, .d)', '.e', '.f[data-x="a,b"]'])
|
||
assert.deepEqual(selektoren(css), ['.a', '.b:not(.c, .d)', '.e', '.f[data-x="a,b"]'])
|
||
assert.deepEqual([...klassen('.kn .tm-a, .kn .kn-tm-b, .tm-c-d:hover { }', 'tm')].sort(), ['tm-a', 'tm-c-d'])
|
||
assert.deepEqual([...klassen('.kn-fn-x, .fn-y { }', 'fn')], ['fn-y'])
|
||
})
|
||
|
||
test('module.css: jede Regel steht unter .kn und hängt nicht an der alten Seite', () => {
|
||
const alle = selektoren(MODUL())
|
||
assert.ok(alle.length > 0, 'die Datei hat Regeln')
|
||
for (const s of alle) assert.match(s, /^\.kn(\s|[.:#[>~+]|$)/, s)
|
||
assert.doesNotMatch(MODUL(), /#view-|--kb-|\.kb-page/)
|
||
})
|
||
|
||
test('module.css: Verbote (Lila, Teal, Verlauf, Glas, Dunkel, Gewicht 800, Animation)', () => {
|
||
assert.doesNotMatch(MODUL(), /#9d3dfe|#d4b4ff|157,\s*61,\s*254|#2dd4bf|#5eead4|gradient|backdrop-filter|color-scheme:\s*dark|prefers-color-scheme|font-weight:\s*(800|900)|@keyframes/)
|
||
})
|
||
|
||
test('module.css: Farben nur aus der Palette der Übersicht; Zettelfarben nur in .nw-paper-*; rgba nur der Ring-Schatten', () => {
|
||
for (const { selektor, koerper } of regeln(MODUL())) {
|
||
for (const [, hex] of koerper.matchAll(/(#[0-9a-fA-F]{3,8})\b/g)) assert.ok(PALETTE.has(hex.toLowerCase()) || (ZETTEL.test(hex) && /\.nw-paper-/.test(selektor)), `${selektor}: ${hex}`)
|
||
for (const [, wert] of koerper.matchAll(/rgba?\(([^)]*)\)/g)) assert.ok(RING.has(wert.replace(/\s/g, '')), `${selektor}: rgba(${wert})`)
|
||
}
|
||
})
|
||
|
||
test('module.css: Fokus schwarz, Handy-Regeln wörtlich mit 44px, reduzierte Bewegung', () => {
|
||
const css = MODUL()
|
||
assert.match(css, /:focus-visible[^{]*\{[^}]*outline:\s*2px solid (#111|var\(--kn-[a-z-]+\))/)
|
||
assert.ok(css.includes(HANDY) && /min-height:\s*44px/.test(css.slice(css.indexOf(HANDY))))
|
||
assert.match(css, /prefers-reduced-motion:\s*reduce/)
|
||
})
|
||
|
||
test('module.css: Animationen höchstens als transition bis 150 ms', () => {
|
||
for (const { selektor, koerper } of regeln(MODUL())) {
|
||
assert.doesNotMatch(koerper, /\banimation(-name)?\s*:/, selektor)
|
||
for (const [, wert] of koerper.matchAll(/transition(?:-duration)?:\s*([^;]*)/g)) {
|
||
for (const [, zahl, einheit] of wert.matchAll(/(\d*\.?\d+)(ms|s)\b/g)) assert.ok((einheit === 's' ? Number(zahl) * 1000 : Number(zahl)) <= 150, `${selektor}: ${wert}`)
|
||
}
|
||
}
|
||
})
|
||
|
||
test('module.css: jedes var(--kn-…) ist in public/neu/*.css definiert', () => {
|
||
const definiert = new Set([...neuCss().matchAll(/(--kn-[a-z0-9-]+)\s*:/g)].map((m) => m[1]))
|
||
for (const [, name] of MODUL().matchAll(/var\((--kn-[a-z0-9-]+)/g)) assert.ok(definiert.has(name), name)
|
||
})
|
||
|
||
test('Alt-Klassen der Module sind unter .kn gestaltet (in einer Datei von public/neu)', () => {
|
||
const sel = selektoren(neuCss()).filter((s) => s.startsWith('.kn'))
|
||
for (const k of ALT_KLASSEN) assert.ok(sel.some((s) => new RegExp(`\\.${k}(?![a-z0-9-])`).test(s)), k)
|
||
})
|
||
|
||
test('UNGENUTZT enthält nur Klassen, die kein JS nennt', () => {
|
||
const js = jsModule()
|
||
for (const k of UNGENUTZT) assert.ok(!js.includes(k), k)
|
||
})
|
||
|
||
for (const praefix of FERTIG) {
|
||
test(`Abdeckung ${praefix}: jede Klasse der alten CSS-Datei ist unter .kn gestaltet`, () => {
|
||
const alt = praefix === 'fn' ? FN_NEWS : [...klassen(lies(ALT[praefix]), praefix)]
|
||
const neu = klassen(MODUL(), praefix)
|
||
assert.ok(alt.length > 0, 'es gibt alte Klassen')
|
||
assert.deepEqual(alt.filter((k) => !neu.has(k) && !UNGENUTZT.has(k)).sort(), [])
|
||
})
|
||
}
|