Files
Webklar-Kundenbereich/test/kundenansichtNeuModulCss.test.js
2026-10-16 18:58:57 +00:00

150 lines
7.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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', 'sh', 'bk', 'zt', 'fn', 'nw', 'an', 'hs'] // Präfixe mit geprüfter Abdeckung (alle)
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(), [])
})
}