Files
Webklar-Kundenbereich/test/knFunktionenStatik.test.js

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