- gelöscht: components/workers/engine/* (10 Dateien), WorkersStage, WorkersBoard, WorkerCard, WorkerList, WorkersKpis, die neun Engine-Tests samt workersSmoke (211 Tests) und die Helfer engineVm.mjs und fakeCtx.mjs (nur die Engine-Tests brauchten sie) - workers.css (310 -> 60 Zeilen) behält nur Einstellungen-Dialog und Interessenten-Panel; @font-face für Pixelify Sans steht jetzt in hustler/schrift.css - workersComponents: kein innerHTML und kein Kanvas rekursiv unter components/workers/**, jeder Knopf mit type, Links nur aus geprüften Feldern (externe mit noopener), keine Anmeldung in den Bildschirmen, alte Dateien weg und nirgends mehr importiert - workersCss: jede wk-Klasse im Code hat eine Regel, keine Regel ohne Nutzer; workersFont: Pixelify Sans genau einmal in schrift.css - hustlerAssets: Pixelify Sans in schrift.css erlaubt; hustlerDialog: Link-Ziele nur aus safeLink, Knöpfe mit type; hustlerSeite: Laden/Fehler/bereit am selben Einhängepunkt Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
82 lines
5.2 KiB
JavaScript
82 lines
5.2 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { readFileSync, readdirSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
// workers.css ist seit T11 (Pixelwelt) auf die weißen Bausteine gekürzt, die noch gebraucht werden: Einstellungen-Dialog (WorkersSettingsDialog)
|
|
// und Interessenten-Panel (ProspectPanel). Hochhaus, Pult und Keller haben ihre Stile in src/styles/hustler/ (eigene Tests: hustler*.test.mjs).
|
|
const stylesDir = fileURLToPath(new URL('../../src/styles/', import.meta.url))
|
|
const srcDir = fileURLToPath(new URL('../../src/', import.meta.url))
|
|
const stripped = (file) => readFileSync(path.join(stylesDir, file), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '') // Kommentare vorher entfernen
|
|
const walk = (dir) => readdirSync(dir, { withFileTypes: true }).flatMap((e) => (e.isDirectory() ? walk(path.join(dir, e.name)) : [path.join(dir, e.name)]))
|
|
const ohneKommentare = (code) => code.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|\s)\/\/.*$/gm, '$1')
|
|
|
|
// Klassen der Regeln (jede Regel, auch in @media) und Klassen im Code (ohne Kommentare; `wk-set-${name}` ist eine Kennung, keine Klasse;
|
|
// das Attribut data-wk-worker hat ein Zeichen davor und zählt nicht)
|
|
const definierteKlassen = (css) => new Set([...css.matchAll(/\.(wk-[a-z0-9-]+)/g)].map((m) => m[1]))
|
|
const benutzteKlassen = () => {
|
|
const out = new Map()
|
|
for (const f of walk(srcDir).filter((x) => /\.jsx?$/.test(x))) {
|
|
for (const m of ohneKommentare(readFileSync(f, 'utf8')).matchAll(/(?<![\w-])wk-[a-z0-9-]*[a-z0-9](?![\w-])/g)) {
|
|
if (!out.has(m[0])) out.set(m[0], new Set())
|
|
out.get(m[0]).add(path.relative(srcDir, f))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
test('workers.css: nur wk-Klassen, keine Hex-Farben, keine Schrift, Bilder oder Importe, ein Handy-Block', () => {
|
|
const css = stripped('workers.css')
|
|
let regeln = 0
|
|
for (const m of css.matchAll(/([^{}]+)\{/g)) {
|
|
const pre = m[1].trim()
|
|
if (pre.startsWith('@')) continue
|
|
regeln += 1
|
|
for (const sel of pre.split(',')) assert.ok(sel.trim().startsWith('.wk-'), sel) // jede Regel beginnt mit einer wk-Klasse (Nachfahren dürfen globale Klassen nutzen)
|
|
}
|
|
assert.ok(regeln >= 15, `nur ${regeln} Regeln gelesen`)
|
|
assert.doesNotMatch(css, /#[0-9a-fA-F]{3,8}\b/)
|
|
assert.doesNotMatch(css, /@font-face|@import|url\(/) // die Schrift steht in hustler/schrift.css (workersFont.test.mjs)
|
|
assert.match(css, /@media \(max-width: 860px\)/)
|
|
assert.doesNotMatch(css, /@media \(max-width: 1100px\)/)
|
|
// keine Bewegung in dieser Datei, also braucht sie auch kein prefers-reduced-motion
|
|
assert.doesNotMatch(css, /\b(?:animation|transition)\s*:|@keyframes/)
|
|
})
|
|
|
|
test('jede wk-Klasse im Code hat eine Regel in workers.css, und keine Regel steht ohne Nutzer', () => {
|
|
const definiert = definierteKlassen(stripped('workers.css'))
|
|
const benutzt = benutzteKlassen()
|
|
assert.ok(definiert.size >= 10 && benutzt.size >= 10, `${definiert.size} Regel-Klassen, ${benutzt.size} benutzte Klassen`)
|
|
for (const [klasse, dateien] of benutzt) assert.ok(definiert.has(klasse), `${klasse} (${[...dateien].join(', ')}) hat keine Regel`)
|
|
for (const klasse of definiert) assert.ok(benutzt.has(klasse), `.${klasse} wird nirgends im Code benutzt`)
|
|
// die Nutzer sind genau die zwei weißen Dialoge
|
|
const nutzer = new Set([...benutzt.values()].flatMap((s) => [...s]))
|
|
assert.deepEqual([...nutzer].sort(), ['components/workers/ProspectPanel.jsx', 'components/workers/WorkersSettingsDialog.jsx'])
|
|
})
|
|
|
|
test('workers.css: die globale Regel .page section + section (36 px) verlängert den Abstand der Panel-Abschnitte nicht (W4)', () => {
|
|
const global = stripped('global.css')
|
|
const css = stripped('workers.css')
|
|
assert.match(global, /\.page section \+ section\s*\{\s*margin-top:\s*36px/) // die Regel, gegen die das Panel sich behaupten muss: Spezifität (0,1,2)
|
|
const m = css.match(/(\.wk-prospect [^{}]*\.wk-prospect-section \+ \.wk-prospect-section)\s*\{([^}]*)\}/)
|
|
assert.ok(m, 'Regel für zwei benachbarte Abschnitte im Panel')
|
|
assert.match(m[2], /margin-top:\s*0\b/)
|
|
// Spezifität: zwei Klassen (0,2,0) schlagen schon (0,1,2); eine einzelne Klasse (0,1,0) verlöre
|
|
assert.ok((m[1].match(/\.[\w-]+/g) || []).length >= 2, 'mindestens zwei Klassen')
|
|
// der Abstand der Abschnitte kommt aus gap, nicht aus einem eigenen margin-top
|
|
assert.match(css, /\.wk-prospect\s*\{[^}]*gap:\s*16px/)
|
|
assert.doesNotMatch(css.match(/\.wk-prospect-section\s*\{([^}]*)\}/)[1], /margin-top/)
|
|
})
|
|
|
|
test('workers.css: Einstellungen-Dialog behält Trennlinien und den roten Block „Versand gestoppt“ (Tokens aus global.css)', () => {
|
|
const css = stripped('workers.css')
|
|
const global = stripped('global.css')
|
|
const rule = (selector) => css.match(new RegExp(`${selector.replace(/[.~]/g, '\\$&')}\\s*\\{([^}]*)\\}`))?.[1] ?? ''
|
|
assert.match(rule('.wk-set-group'), /border-top:\s*1px solid var\(--line\)/)
|
|
assert.match(rule('.wk-halt'), /border-top:\s*1px solid var\(--danger\)/); assert.match(rule('.wk-halt'), /border-bottom:\s*1px solid var\(--danger\)/)
|
|
assert.match(rule('.wk-halt .wk-set-title'), /color:\s*var\(--red-fg\)/)
|
|
// jedes verwendete Token gibt es in global.css
|
|
for (const m of css.matchAll(/var\((--[\w-]+)\)/g)) assert.match(global, new RegExp(`${m[1]}\\s*:`), m[1])
|
|
})
|