Files
Webklar-Kundenbereich/test/knAnsichtStil.test.js
T
ClaudeandClaude Sonnet 5.5 29ca12a409 Kundenansicht neu E-Fix M7: Bewegung reduziert ohne Rest-Fahrt der Leisten-Zeilen, #fortschritt unter dem haftenden Kopf
- Die Regel der zwei Fahrstuhl-Zeilen (#kn-zeile-haupt, #kn-zeile-etage, Spezifität 1-2-0) schlug das .kn-rw { transition: none } der Reduced-Motion-
  Regel: mit "Bewegung reduzieren" glitt die alte Zeile noch 550 ms durch die neue (beide Texte überlagert). Jetzt steht im Reduced-Motion-Block
  dieselbe Selektorliste mit transition: none.
- #fortschritt (scrollIntoView block start) ließ den Abschnitt "Was wir zuletzt erledigt haben" hinter dem haftenden Kopf beginnen: scroll-margin-top
  mit Kopfhöhe, Bannerhöhe, Safe-Area und 8 px (Desktop 16 px). Die Gutschrift-Etage scrollt im Blatt, dessen Oberkante schon unter dem Kopf liegt: dort
  ist nichts nötig.
Prüfung: Werkzeug-Ansichten kn-huelle-reduziert (jetzt auch die Zeilen bei offener Etage) und kn-p2-website-fortschritt, Pins in test/knEfixCss.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:46:15 +00:00

37 lines
3.8 KiB
JavaScript

import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync, existsSync } from 'node:fs'
import { K } from '../public/neu/ansicht-hilfen.js'
const css = (f) => readFileSync(new URL(`../public/neu/${f}`, import.meta.url), 'utf8')
const lese = (p) => readFileSync(new URL(`../public/${p}`, import.meta.url), 'utf8')
const A = css('ansichten.css'); const O = css('oeffentlich/oeffentlich.css'); const C = css('chat-composer.css'); const BASIS = css('basis.css'); const TOKENS = css('tokens.css')
const MODULE = ['ansicht-hilfen', 'rechnung-offen', 'install-hinweis', 'website-details', 'ansicht-website', 'ansicht-chat', 'ansicht-vertraege', 'vertraege-rechnungen', 'vertraege-abo', 'ansicht-stunden', 'ansicht-speicher', 'ansicht-bestellen', 'ansicht-wechsel', 'ansicht-gutschrift', 'ansicht-passwort', 'bestell-bausteine']
const existiert = (n) => existsSync(new URL(`../public/neu/${n}.js`, import.meta.url))
const lies = (n) => css(`${n}.js`).replace(/\/\*[\s\S]*?\*\//g, '')
const VERBOTEN = /linear-gradient|radial-gradient|conic-gradient|backdrop-filter|prefers-color-scheme:\s*dark|font-weight:\s*800|#9d3dfe|rgba?\(\s*157\s*,\s*61/i
test('Stil: keine verbotenen Mittel (Verläufe, Glas, Dunkelmodus, Lila, Gewicht 800), auch nicht in der Kopie des Composer-CSS', () => { for (const [n, c] of [['ansichten.css', A], ['oeffentlich.css', O], ['chat-composer.css', C]]) assert.doesNotMatch(c, VERBOTEN, n) })
test('Stil: Farbliterale nur in Custom-Property-Deklarationen, jedes benutzte Token ist definiert', () => {
for (const [n, c] of [['ansichten.css', A], ['oeffentlich.css', O]]) for (const z of c.split('\n')) if (!/^\s*--kn-[\w-]+:/.test(z)) assert.doesNotMatch(z, /#[0-9a-f]{3,8}\b|rgba?\(/i, `${n}: ${z.trim()}`)
const definiert = new Set([...(TOKENS + A + O + css('huelle.css')).matchAll(/(--kn-[\w-]+)\s*:/g)].map((m) => m[1])) // die Maße der Hülle (--kn-kopf-h, --kn-banner-h) stehen in huelle.css
for (const m of (A + O).matchAll(/var\((--kn-[\w-]+)/g)) assert.ok(definiert.has(m[1]), m[1])
})
test('Stil: jede kn-Klasse der P2-Module hat eine Regel', () => {
const klassen = new Set(); for (const n of MODULE.filter(existiert)) for (const m of lies(n).matchAll(/class:\s*'([^']+)'/g)) m[1].split(/\s+/).filter((k) => k.startsWith('kn-')).forEach((k) => klassen.add(k))
for (const wert of Object.values(K).flatMap((v) => (typeof v === 'string' ? [v] : Object.values(v)))) wert.split(/\s+/).filter((k) => k.startsWith('kn-')).forEach((k) => klassen.add(k))
for (const k of klassen) assert.match(BASIS + A, new RegExp(`\\.${k}(?![\\w-])`), `Klasse ohne Regel: ${k}`)
})
test('Stil: Aufträge-Abschnitt, Chat-Variablen, hidden und Handy-Abfrage', () => {
const ab = new Set([...(lese('auftrag-dom.js') + lese('auftrag-portal.js')).matchAll(/\bab-[a-z0-9]+(?:-[a-z0-9]+)*/g)].map((m) => m[0]))
for (const k of [...ab, 'ab-pille-warn', 'ab-pille-ok', 'ab-pille-rot'].filter((x) => x !== 'ab-portal')) assert.match(A, new RegExp(`\\.kn \\.ab-portal[^{]*\\.${k}(?![\\w-])`), k)
for (const v of ['accent', 'accent-hover', 'accent-soft', 'accent-line', 'accent-text', 'on-accent', 'line', 'muted', 'surface']) assert.match(A, new RegExp(`\\.kn-chat[^{]*\\{[^}]*--chat-${v}:`), v)
assert.match(A, /\.kn \.hidden\s*\{[^}]*display:\s*none/); assert.ok(A.includes('(max-width: 880px), (pointer: coarse) and (max-height: 500px)'))
})
test('Stil: Bestellseiten am Handy (F1): Beschriftung läuft in der Zeile mit, die Einzelheiten stehen unter dem Knopf', () => {
const block = A.slice(A.lastIndexOf('(max-width: 880px), (pointer: coarse) and (max-height: 500px)'))
assert.match(block, /\.kn-block-zeile\s*\{[^}]*display:\s*block/)
assert.match(block, /\.kn-block dt,\s*\.kn-block dd\s*\{[^}]*display:\s*inline/)
assert.match(block, /\.kn-block dt::after\s*\{[^}]*content:\s*": "/)
assert.match(A, /\.kn-einzelheiten\s*\{/)
})