Funktionen neu: Aussehen (weiß, Pillen, 44-px-Zeilen) mit Quelltext-Prüfung

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 18:14:08 +00:00
1 parent 36890c6734
commit 9ca7900213
2 files changed
+179 -2

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
const lies = (pfad) => readFileSync(new URL(`../${pfad}`, import.meta.url), 'utf8')
const ohneKommentare = (t) => t.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1')
const JS = ['public/neu/ansicht-funktionen.js', 'public/neu/funktion-einschalten.js']
const css = lies('public/neu/funktionen.css')
test('funktionen.css: Pillen-Farben aus den Global Constraints', () => {
const PILLEN = { grau: ['#ebeef1', '#414552'], blau: ['#e0f2fe', '#075985'], orange: ['#fcedb9', '#a82c00'], violett: ['#efeafe', '#5b21b6'], gruen: ['#d7f7c2', '#006908'], rot: ['#ffe7f2', '#b3093c'] }
for (const [ton, [flaeche, schrift]] of Object.entries(PILLEN)) {
const regel = (css.match(new RegExp(`\\.kn-fn-pille--${ton}\\s*\\{([^}]*)\\}`)) ?? [, ''])[1].toLowerCase()
assert.ok(regel.includes(flaeche) && regel.includes(schrift), ton)
}
})
test('funktionen.css: verbotene Mittel fehlen, Radien nur 6 und 12 px, Tap-Ziel 44 px', () => {
assert.doesNotMatch(css, /gradient|backdrop-filter|glow|prefers-color-scheme\s*:\s*dark|#9d3dfe|rgba\(\s*157/i)
assert.doesNotMatch(css, /font-weight\s*:\s*(800|900)/)
for (const m of css.matchAll(/border-radius\s*:\s*([^;}]+)/g)) assert.match(m[1].trim(), /^(0|6px|12px|999px|50%)$/, m[0])
assert.match((css.match(/\.kn-fn-zeile-knopf\s*\{([^}]*)\}/) ?? [, ''])[1], /min-height\s*:\s*44px/)
assert.match(css, /@media\s*\(min-width:\s*881px\)/)
})
test('funktionen.css: Knöpfe 44 px hoch, Primärknopf #111 mit weißer Schrift, gesperrt grau; keine Schatten außer dem Ring; keine Animation', () => {
const regel = (selektor) => (css.match(new RegExp(`${selektor.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\{([^}]*)\\}`)) ?? [, ''])[1]
const knopf = regel('.kn-fn-knopf')
assert.match(knopf, /min-height\s*:\s*44px/)
assert.match(knopf, /#111/i)
assert.match(knopf, /#fff/i)
const gesperrt = (css.match(/\.kn-fn-knopf:disabled[^{]*\{([^}]*)\}/) ?? [, ''])[1]
assert.match(gesperrt, /#8a93a3/i)
assert.match(gesperrt, /#f8f9fb/i)
assert.match(regel('.kn-fn-knopf--zweit'), /0 0 0 1px rgba\(60,\s*66,\s*87,\s*\.16\)/)
assert.match(regel('.kn-fn-meldung'), /#c0123c/i)
for (const m of css.matchAll(/box-shadow\s*:\s*([^;}]+)/g)) assert.match(m[1], /^(none|var\(--kn-ring,\s*0 0 0 1px rgba\(60,\s*66,\s*87,\s*\.16\),\s*0 1px 1px rgba\(0,\s*0,\s*0,\s*\.06\)\))$/, m[0])
assert.doesNotMatch(css, /@keyframes|animation\s*:|transition\s*:/)
})
test('funktionen.css: das Bild ist 16:10 mit Radius 12 px, Entdecken hat keinen Rahmen', () => {
const bild = (css.match(/\.kn-fn-bild\s*\{([^}]*)\}/) ?? [, ''])[1]
assert.match(bild, /aspect-ratio\s*:\s*16\s*\/\s*10/)
assert.match(bild, /border-radius\s*:\s*12px/)
const eintrag = (css.match(/\.kn-fn-eintrag\s*\{([^}]*)\}/) ?? [, ''])[1]
assert.doesNotMatch(eintrag, /\bborder\s*:|box-shadow|border-radius/)
})
test('jede kn-fn-Klasse der JS-Dateien ist im CSS gestaltet', () => {
for (const datei of JS) for (const k of new Set(ohneKommentare(lies(datei)).match(/kn-fn-[a-z0-9-]+/g) ?? [])) assert.ok(css.includes(`.${k}`), `${datei}: .${k} fehlt im CSS`)
})
// entfernt var(…) samt verschachtelter Klammern: danach darf kein Farbliteral mehr übrig sein (Ausnahme: weiße Schrift auf dem Primärknopf)
function ohneVar(text) {
let aus = ''
for (let i = 0; i < text.length; i += 1) {
if (!text.startsWith('var(', i)) { aus += text[i]; continue }
let tiefe = 0
let j = i + 3
for (; j < text.length; j += 1) {
if (text[j] === '(') tiefe += 1
else if (text[j] === ')') { tiefe -= 1; if (tiefe === 0) break }
}
i = j
}
return aus
}
test('funktionen.css: jedes benutzte Token ist in tokens.css definiert, Farbliterale nur als Fallback von var()', () => {
const tokens = lies('public/neu/tokens.css')
for (const m of css.matchAll(/var\((--kn-[\w-]+)/g)) assert.ok(tokens.includes(`${m[1]}:`), m[1])
assert.doesNotMatch(ohneVar(css.replace(/\/\*[\s\S]*?\*\//g, '')), /#(?!fff\b)[0-9a-f]{3,8}\b|rgba?\(/i)
})
for (const datei of JS) {
test(`${datei}: kein innerHTML, nur relative Importe, keine Preise, keine Sie-Anrede`, () => {
const code = ohneKommentare(lies(datei))
assert.doesNotMatch(code, /innerHTML|insertAdjacentHTML|outerHTML|document\.write|\beval\(|new Function/)
for (const m of code.matchAll(/(?:from|import\()\s*'([^']+)'/g)) assert.match(m[1], /^\.{1,2}\//, m[1])
assert.doesNotMatch(code, /€|\bEuro\b/)
assert.doesNotMatch(code, /\b(Sie|Ihre[rnms]?|Ihnen|Ihr)\b/)
})
}
test('beide Module laden unter Node ohne DOM (kein Zugriff beim Import)', async () => {
assert.equal(typeof globalThis.document, 'undefined')
await import('../public/neu/ansicht-funktionen.js')
await import('../public/neu/funktion-einschalten.js')
})
test('Module der Funktionen: keine Hash-Namen außer den vorhandenen, kein Hash mit Großbuchstaben', () => {
for (const datei of JS) for (const m of ohneKommentare(lies(datei)).matchAll(/'#([A-Za-z-]+)/g)) assert.match(m[1], /^[a-z-]+$/, `${datei}: #${m[1]}`)
})