import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync, statSync, readdirSync, existsSync } from 'node:fs' import sharp from 'sharp' 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]}`) }) test('Bilder: fünf webp-Dateien, 960×600, höchstens 120 KB; analyse.webp, falls da, ebenso', async () => { const PFLICHT = ['news', 'termine', 'shop', 'unterkunft', 'zeiten'] const ordner = new URL('../public/neu/bilder/', import.meta.url) assert.ok(existsSync(ordner), 'public/neu/bilder/ fehlt') const da = readdirSync(ordner).sort() for (const d of da) assert.ok([...PFLICHT, 'analyse'].map((k) => `${k}.webp`).includes(d), `unerwartete Datei ${d}`) for (const key of PFLICHT) assert.ok(da.includes(`${key}.webp`), `${key}.webp fehlt`) for (const d of da) { const pfad = new URL(d, ordner).pathname const meta = await sharp(pfad).metadata() assert.deepEqual([meta.format, meta.width, meta.height], ['webp', 960, 600], d) assert.ok(statSync(pfad).size <= 122880, `${d} größer als 120 KB`) } }) test('Bilder: jede Bildadresse des Katalogs zeigt auf eine Datei unter public/neu/bilder/ (Testdaten)', async () => { const { daten } = await import('./support/knFunktionenDaten.js') for (const e of daten().katalog) { const m = /^\/neu\/bilder\/([a-z0-9-]+)\.webp$/.exec(e.bild) assert.ok(m, e.key) if (e.key !== 'analyse') assert.ok(existsSync(new URL(`../public/neu/bilder/${m[1]}.webp`, import.meta.url)), `${e.key}: Bild fehlt`) // analyse.webp kommt mit P4 (VF2) } })