119 lines
6.4 KiB
JavaScript
119 lines
6.4 KiB
JavaScript
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)
|
||
}
|
||
})
|