Hustler-Pixelwelt T1+T2: Import der Zeichenfläche (Bilder, CSS mit hk-Vorsatz, Geometrie), helles Haus mit grauen Raum-Ebenen, Press Start 2P selbst gehostet
- scripts/hustler-import.mjs liest pixelwelt-v2 (+ pixelwelt-woms/haus) und schreibt src/assets/hustler, src/styles/hustler/{hochhaus,keller,pult}.css und src/lib/hustlerWelt.js; idempotent
- haus.png mit allen Räumen hell, acht graue Ebenen für die Such-Räume (GRAUE_RAEUME, .hk-raum-grau-<raum>)
- Press Start 2P lateinisch aus @fontsource/press-start-2p@5.2.7 (OFL), noch nirgends importiert
- 40 neue Tests (Assets, Geometrie, CSS-Umformung)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6e24868995
commit
8bd365b1fc
195 files changed
+2697
No files matched your search
@@ -0,0 +1,221 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
||||
import { createHash } from 'node:crypto'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
// Prüft die vom Import-Skript erzeugten Dateien (src/styles/hustler, src/assets/hustler, public/fonts).
|
||||
// Die Prüfung der Ergebnisse ist bewusst unabhängig vom Umformer im Skript geschrieben.
|
||||
|
||||
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const ASSETS = path.join(REPO, 'src/assets/hustler')
|
||||
const BILDSCHIRME = [
|
||||
{ name: 'hochhaus', wurzel: 'hk-hochhaus', kf: 'hk-h-', datei: path.join(REPO, 'src/styles/hustler/hochhaus.css') },
|
||||
{ name: 'keller', wurzel: 'hk-keller', kf: 'hk-k-', datei: path.join(REPO, 'src/styles/hustler/keller.css') },
|
||||
{ name: 'pult', wurzel: 'hk-pult', kf: 'hk-p-', datei: path.join(REPO, 'src/styles/hustler/pult.css') },
|
||||
]
|
||||
const ANIMATIONS_WOERTER = new Set(['none', 'infinite', 'linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out', 'step-start', 'step-end', 'normal', 'reverse', 'alternate', 'alternate-reverse', 'forwards', 'backwards', 'both', 'running', 'paused', 'initial', 'inherit', 'unset', 'revert'])
|
||||
|
||||
const lies = (datei) => readFileSync(datei, 'utf8')
|
||||
const ohneKommentare = (css) => css.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
||||
|
||||
// → { regeln: [{ selektor, koerper, kontext }], keyframes: [name] }
|
||||
function zerlege(css) {
|
||||
const text = ohneKommentare(css)
|
||||
const regeln = []
|
||||
const keyframes = []
|
||||
let i = 0
|
||||
function liste(kontext) {
|
||||
while (i < text.length) {
|
||||
while (i < text.length && /\s/.test(text[i])) i += 1
|
||||
if (i >= text.length) return
|
||||
if (text[i] === '}') { i += 1; return }
|
||||
const start = i
|
||||
while (i < text.length && !'{;}'.includes(text[i])) i += 1
|
||||
const kopf = text.slice(start, i).trim()
|
||||
assert.equal(text[i], '{', `Regel ohne Block: ${kopf.slice(0, 60)}`)
|
||||
i += 1
|
||||
if (kopf.startsWith('@keyframes')) {
|
||||
keyframes.push(kopf.slice('@keyframes'.length).trim())
|
||||
let tiefe = 1
|
||||
while (tiefe > 0 && i < text.length) {
|
||||
if (text[i] === '{') tiefe += 1
|
||||
else if (text[i] === '}') tiefe -= 1
|
||||
i += 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (kopf.startsWith('@')) { liste(kopf); continue }
|
||||
const b = i
|
||||
while (i < text.length && text[i] !== '}') i += 1
|
||||
regeln.push({ selektor: kopf, koerper: text.slice(b, i), kontext })
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
liste('')
|
||||
return { regeln, keyframes }
|
||||
}
|
||||
|
||||
// Teilt an Kommas/Leerzeichen auf oberster Ebene (außerhalb von Klammern und Zeichenketten)
|
||||
function teile(s, trenner) {
|
||||
const aus = []
|
||||
let tiefe = 0
|
||||
let quote = null
|
||||
let aktuell = ''
|
||||
for (const c of s) {
|
||||
if (quote) { aktuell += c; if (c === quote) quote = null; continue }
|
||||
if (c === '"' || c === "'") { quote = c; aktuell += c; continue }
|
||||
if (c === '(' || c === '[') tiefe += 1
|
||||
if (c === ')' || c === ']') tiefe -= 1
|
||||
if (tiefe === 0 && trenner.test(c)) { if (aktuell) aus.push(aktuell); aktuell = ''; continue }
|
||||
aktuell += c
|
||||
}
|
||||
if (aktuell) aus.push(aktuell)
|
||||
return aus
|
||||
}
|
||||
|
||||
function dateienUnter(ordner) {
|
||||
const liste = []
|
||||
for (const name of readdirSync(ordner)) {
|
||||
const voll = path.join(ordner, name)
|
||||
if (statSync(voll).isDirectory()) liste.push(...dateienUnter(voll))
|
||||
else liste.push(voll)
|
||||
}
|
||||
return liste
|
||||
}
|
||||
|
||||
const css = Object.fromEntries(BILDSCHIRME.map((b) => [b.name, lies(b.datei)]))
|
||||
const zerlegt = Object.fromEntries(BILDSCHIRME.map((b) => [b.name, zerlege(css[b.name])]))
|
||||
|
||||
test('Kopfkommentar: jede CSS-Datei sagt, dass sie erzeugt ist', () => {
|
||||
for (const b of BILDSCHIRME) {
|
||||
assert.match(css[b.name].split('\n')[0], /^\/\* Erzeugt von scripts\/hustler-import\.mjs aus .+, nicht von Hand ändern\. \*\/$/, b.name)
|
||||
}
|
||||
})
|
||||
|
||||
test('kein /_blob/, kein googleapis, keine @font-face/@import in den erzeugten CSS-Dateien', () => {
|
||||
for (const b of BILDSCHIRME) {
|
||||
const t = ohneKommentare(css[b.name])
|
||||
assert.ok(!t.includes('/_blob/'), `${b.name}: /_blob/`)
|
||||
assert.ok(!/googleapis/i.test(t), `${b.name}: googleapis`)
|
||||
assert.ok(!/@font-face/i.test(t), `${b.name}: @font-face`)
|
||||
assert.ok(!/@import/i.test(t), `${b.name}: @import`)
|
||||
assert.ok(zerlegt[b.name].regeln.length > 50, `${b.name}: zu wenige Regeln gelesen`)
|
||||
}
|
||||
})
|
||||
|
||||
test('jede url(...) zeigt auf eine vorhandene Datei im Asset-Ordner (relativer Pfad)', () => {
|
||||
for (const b of BILDSCHIRME) {
|
||||
const urls = [...ohneKommentare(css[b.name]).matchAll(/url\(\s*([^)]*?)\s*\)/g)].map((m) => m[1].replace(/^["']|["']$/g, ''))
|
||||
assert.ok(urls.length > 10, `${b.name}: zu wenige url(...)`)
|
||||
for (const u of urls) {
|
||||
assert.match(u, /^\.\.\/\.\.\/assets\/hustler\/[\w./-]+\.png$/, `${b.name}: ${u}`)
|
||||
const ziel = path.resolve(path.dirname(b.datei), u)
|
||||
assert.ok(ziel.startsWith(ASSETS + path.sep), `${b.name}: ${u} liegt außerhalb von src/assets/hustler`)
|
||||
assert.ok(existsSync(ziel) && statSync(ziel).isFile(), `${b.name}: ${u} fehlt`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('jede Datei unter src/assets/hustler wird von mindestens einer CSS-Datei benutzt', () => {
|
||||
const benutzt = new Set()
|
||||
for (const b of BILDSCHIRME) {
|
||||
for (const m of ohneKommentare(css[b.name]).matchAll(/url\(\s*([^)]*?)\s*\)/g)) {
|
||||
benutzt.add(path.resolve(path.dirname(b.datei), m[1].replace(/^["']|["']$/g, '')))
|
||||
}
|
||||
}
|
||||
const alle = dateienUnter(ASSETS)
|
||||
assert.ok(alle.length > 100, `nur ${alle.length} Dateien`)
|
||||
for (const d of alle) {
|
||||
assert.ok(d.endsWith('.png'), `keine PNG: ${d}`)
|
||||
assert.ok(benutzt.has(d), `unbenutzt: ${path.relative(REPO, d)}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('jede Klasse in einem Selektor beginnt mit hk-', () => {
|
||||
for (const b of BILDSCHIRME) {
|
||||
for (const r of zerlegt[b.name].regeln) {
|
||||
const ohneStrings = r.selektor.replace(/"[^"]*"|'[^']*'/g, '""').replace(/\[[^\]]*\]/g, '[]')
|
||||
const klassen = [...ohneStrings.matchAll(/\.(-?[A-Za-z_][\w-]*)/g)].map((m) => m[1])
|
||||
for (const k of klassen) {
|
||||
assert.ok(k.startsWith('hk-'), `${b.name}: .${k} in „${r.selektor.slice(0, 80)}“`)
|
||||
assert.ok(!k.startsWith('hk-hk-'), `${b.name}: doppelt vorangestellt: .${k}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('jede Regel liegt unter ihrer Wurzelklasse', () => {
|
||||
for (const b of BILDSCHIRME) {
|
||||
for (const r of zerlegt[b.name].regeln) {
|
||||
for (const teil of teile(r.selektor, /,/)) {
|
||||
const t = teil.trim()
|
||||
assert.ok(t.startsWith(`.${b.wurzel}`), `${b.name}: „${t.slice(0, 80)}“ beginnt nicht mit .${b.wurzel}`)
|
||||
const danach = t[b.wurzel.length + 1]
|
||||
assert.ok(danach === undefined || ' .:[>+~'.includes(danach), `${b.name}: „${t.slice(0, 80)}“ nennt eine andere Klasse mit gleichem Anfang`)
|
||||
assert.ok(!/(^|[\s>+~(,])(?:html|body|:root)(?![\w-])/i.test(t), `${b.name}: html/body/:root übrig in „${t.slice(0, 80)}“`)
|
||||
}
|
||||
}
|
||||
assert.ok(zerlegt[b.name].regeln.some((r) => r.selektor === `.${b.wurzel}`), `${b.name}: keine Regel für die Wurzel selbst (früher body)`)
|
||||
}
|
||||
})
|
||||
|
||||
test('jeder Keyframe-Name trägt den Vorsatz seines Bildschirms und ist nur einmal definiert', () => {
|
||||
const alle = new Set()
|
||||
for (const b of BILDSCHIRME) {
|
||||
const { keyframes } = zerlegt[b.name]
|
||||
assert.ok(keyframes.length > 10, `${b.name}: nur ${keyframes.length} Keyframes`)
|
||||
assert.equal(new Set(keyframes).size, keyframes.length, `${b.name}: Keyframe doppelt`)
|
||||
for (const k of keyframes) {
|
||||
assert.ok(k.startsWith(b.kf), `${b.name}: ${k} ohne ${b.kf}`)
|
||||
assert.ok(!alle.has(k), `${k} kommt in zwei Bildschirmen vor`)
|
||||
alle.add(k)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('jede in animation/animation-name genannte Bezeichnung ist ein definierter Keyframe oder ein Schlüsselwort', () => {
|
||||
let geprueft = 0
|
||||
for (const b of BILDSCHIRME) {
|
||||
const definiert = new Set(zerlegt[b.name].keyframes)
|
||||
for (const r of zerlegt[b.name].regeln) {
|
||||
for (const deklaration of teile(r.koerper, /;/)) {
|
||||
const k = deklaration.indexOf(':')
|
||||
if (k < 0) continue
|
||||
const eigenschaft = deklaration.slice(0, k).trim().toLowerCase()
|
||||
if (!/^(?:-webkit-)?animation(?:-name)?$/.test(eigenschaft)) continue
|
||||
const wert = deklaration.slice(k + 1).replace(/!important/i, ' ')
|
||||
for (const eintrag of teile(wert, /,/)) {
|
||||
for (const wort of teile(eintrag, /\s/)) {
|
||||
if (wort.includes('(') || /^[+-]?[\d.]+(?:ms|s)?$/.test(wort)) continue
|
||||
if (ANIMATIONS_WOERTER.has(wort)) continue
|
||||
assert.ok(definiert.has(wort), `${b.name}: animation nennt „${wort}“, aber es gibt keinen Keyframe ${wort}`)
|
||||
geprueft += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.ok(geprueft > 80, `nur ${geprueft} Animationsnamen gefunden`)
|
||||
})
|
||||
|
||||
test('schrift.css bindet Press Start 2P selbst gehostet ein (Datei vorhanden, OFL-Text)', () => {
|
||||
const schrift = ohneKommentare(lies(path.join(REPO, 'src/styles/hustler/schrift.css')))
|
||||
assert.match(schrift, /@font-face/)
|
||||
assert.match(schrift, /font-family:\s*["']Press Start 2P["']/)
|
||||
assert.match(schrift, /font-display:\s*swap/)
|
||||
const m = schrift.match(/url\(\s*["']?(\/fonts\/[\w.-]+\.woff2)["']?\s*\)/)
|
||||
assert.ok(m, 'keine woff2-Adresse unter /fonts/')
|
||||
assert.equal(m[1], '/fonts/PressStart2P-Regular.woff2')
|
||||
const datei = path.join(REPO, 'public', m[1])
|
||||
assert.ok(existsSync(datei), `${m[1]} fehlt in public/`)
|
||||
assert.equal(readFileSync(datei).subarray(0, 4).toString('latin1'), 'wOF2')
|
||||
// lateinischer Schnitt aus @fontsource/press-start-2p@5.2.7 (files/press-start-2p-latin-400-normal.woff2): Ziffern, Umlaute, ß, € (kein U+2212)
|
||||
assert.equal(createHash('sha256').update(readFileSync(datei)).digest('hex'), 'afec86997fdaf54af1f59358fa2c1e2a0f1d04146edad18e5cd141d0384a7548')
|
||||
assert.ok(!/googleapis|gstatic/.test(schrift))
|
||||
assert.ok(!/Pixelify/i.test(schrift), 'Pixelify Sans ist schon in workers.css eingebunden, nicht doppelt')
|
||||
const lizenz = lies(path.join(REPO, 'public/fonts/OFL-PressStart2P.txt'))
|
||||
assert.match(lizenz, /^Copyright 2012 The Press Start 2P Project Authors \(cody@zone38\.net\), with Reserved Font Name "Press Start 2P"\.?$/m)
|
||||
assert.match(lizenz, /SIL OPEN FONT LICENSE Version 1\.1/)
|
||||
})
|
||||
@@ -0,0 +1,294 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
// Umformer und Planer aus scripts/hustler-import.mjs: CSS-Zerlegung (Kommentare, Zeichenketten, verschachtelte
|
||||
// @media, @keyframes), Selektoren, Animationsnamen, Blob-Adressen, Fehlermeldungen und Wiederholbarkeit.
|
||||
|
||||
const { wandleCss, wandleSelektor, baue, pruefsumme, schreibe, ImportFehler } = await import('../../scripts/hustler-import.mjs')
|
||||
|
||||
const V2 = process.env.HUSTLER_V2 || '/root/reports/workers-2026-10-16/pixelwelt-v2'
|
||||
const hatV2 = existsSync(path.join(V2, 'entwurf/Main.dc.html'))
|
||||
|
||||
// Löscht nur Ordner, die dieser Test selbst unter os.tmpdir() mit dem Vorsatz hustler- angelegt hat
|
||||
function loesche(ordner) {
|
||||
assert.equal(path.dirname(ordner), os.tmpdir(), 'nur Ordner direkt unter dem Temp-Verzeichnis')
|
||||
assert.ok(path.basename(ordner).startsWith('hustler-'), 'nur selbst angelegte Ordner')
|
||||
rmSync(ordner, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
const BLOB = (id) => `../../assets/hustler/${id}.png`
|
||||
const umformen = (css, extra = {}) => wandleCss(css, { wurzel: 'hk-t', kfVorsatz: 'hk-x-', blobPfad: BLOB, ...extra })
|
||||
const nur = (css, extra) => umformen(css, extra).css
|
||||
|
||||
test('Wurzel: body, html und :root werden zur Wurzelklasse, doppelte fallen zusammen', () => {
|
||||
assert.equal(nur('body{margin:0}'), '.hk-t{margin:0}')
|
||||
assert.equal(nur('html,body{margin:0}'), '.hk-t{margin:0}')
|
||||
assert.equal(nur(':root{--a:1}'), '.hk-t{--a:1}')
|
||||
assert.equal(nur('html body{margin:0}'), '.hk-t{margin:0}')
|
||||
assert.equal(nur('html > body{margin:0}'), '.hk-t{margin:0}')
|
||||
assert.equal(nur('body.dunkel{color:#fff}'), '.hk-t.hk-dunkel{color:#fff}')
|
||||
assert.equal(nur('body > .a, html .b{x:1}'), '.hk-t > .hk-a,.hk-t .hk-b{x:1}')
|
||||
assert.equal(nur('body::selection{x:1}'), '.hk-t::selection{x:1}')
|
||||
})
|
||||
|
||||
test('andere Selektoren stehen unter der Wurzel, auch Element-Selektoren', () => {
|
||||
assert.equal(nur('a{color:red}'), '.hk-t a{color:red}')
|
||||
assert.equal(nur('a:hover{color:red}'), '.hk-t a:hover{color:red}')
|
||||
assert.equal(nur('a, button{x:1}'), '.hk-t a,.hk-t button{x:1}')
|
||||
assert.equal(nur('*{box-sizing:border-box}'), '.hk-t *{box-sizing:border-box}')
|
||||
assert.equal(nur('button:focus-visible{outline:0}'), '.hk-t button:focus-visible{outline:0}')
|
||||
assert.equal(nur('.suche::-webkit-search-cancel-button{display:none}'), '.hk-t .hk-suche::-webkit-search-cancel-button{display:none}')
|
||||
assert.equal(nur('li:nth-child(2n+1){x:1}'), '.hk-t li:nth-child(2n+1){x:1}')
|
||||
})
|
||||
|
||||
test('jede Klasse im Selektor bekommt hk- (auch in :not und nach Kombinatoren), Deklarationen bleiben', () => {
|
||||
assert.equal(nur('.panel{width:.5em}'), '.hk-t .hk-panel{width:.5em}')
|
||||
assert.equal(nur('a:not(.knopf):hover{color:#7a2e3a}'), '.hk-t a:not(.hk-knopf):hover{color:#7a2e3a}')
|
||||
assert.equal(nur('.a.b > .c + .d ~ .e .f{x:1}'), '.hk-t .hk-a.hk-b > .hk-c + .hk-d ~ .hk-e .hk-f{x:1}')
|
||||
assert.equal(nur('.stempel:not(.stempel-frage):hover{background:#fff}'), '.hk-t .hk-stempel:not(.hk-stempel-frage):hover{background:#fff}')
|
||||
assert.equal(nur('.a{transition:opacity .12s steps(2); margin:0 .5em; font-size:.9rem}'), '.hk-t .hk-a{transition:opacity .12s steps(2); margin:0 .5em; font-size:.9rem}')
|
||||
assert.equal(nur('.a{content:".b .c"}'), '.hk-t .hk-a{content:".b .c"}')
|
||||
})
|
||||
|
||||
test('Attributselektoren und Zeichenketten im Selektor bleiben unberührt, Klassen danach werden trotzdem ergänzt', () => {
|
||||
assert.equal(nur('a[href^=".x"] .c{x:1}'), '.hk-t a[href^=".x"] .hk-c{x:1}')
|
||||
assert.equal(nur('[data-a="b.c"].d{x:1}'), '.hk-t [data-a="b.c"].hk-d{x:1}')
|
||||
assert.equal(nur('input[type=text]:focus .z{x:1}'), '.hk-t input[type=text]:focus .hk-z{x:1}')
|
||||
})
|
||||
|
||||
test('html/body/:root mitten im Selektor werden nicht geraten, sondern gemeldet', () => {
|
||||
assert.throws(() => nur('.dunkel body{x:1}'), (e) => e instanceof ImportFehler && /mitten im Selektor/.test(e.message))
|
||||
assert.throws(() => wandleSelektor('a:not(body)', 'hk-t'), ImportFehler)
|
||||
assert.throws(() => wandleSelektor(' ', 'hk-t'), /leerer Selektor/)
|
||||
})
|
||||
|
||||
test('@keyframes: Namen bekommen den Vorsatz, Animationen und animation-name ziehen mit (nur ganze, bekannte Namen)', () => {
|
||||
const { css, keyframes } = umformen(`
|
||||
@keyframes funkeln{0%,100%{opacity:.25}50%{opacity:1}}
|
||||
@keyframes k24x4{to{background-position:-96px 0}}
|
||||
.a{animation:funkeln 3.2s steps(4) infinite}
|
||||
.b{animation:k24x4 600ms steps(4) infinite, funkeln 1s linear -0.5s infinite alternate}
|
||||
.c{animation-name:funkeln,k24x4; animation-duration:.5s}
|
||||
.d{animation:none!important}
|
||||
.e{animation:funkeln2 1s linear infinite}
|
||||
.f{-webkit-animation:funkeln 1s}
|
||||
`)
|
||||
assert.deepEqual(keyframes, ['hk-x-funkeln', 'hk-x-k24x4'])
|
||||
const zeilen = css.split('\n')
|
||||
assert.equal(zeilen[0], '@keyframes hk-x-funkeln{0%,100%{opacity:.25}50%{opacity:1}}')
|
||||
assert.equal(zeilen[1], '@keyframes hk-x-k24x4{to{background-position:-96px 0}}')
|
||||
assert.equal(zeilen[2], '.hk-t .hk-a{animation:hk-x-funkeln 3.2s steps(4) infinite}')
|
||||
assert.equal(zeilen[3], '.hk-t .hk-b{animation:hk-x-k24x4 600ms steps(4) infinite, hk-x-funkeln 1s linear -0.5s infinite alternate}')
|
||||
assert.equal(zeilen[4], '.hk-t .hk-c{animation-name:hk-x-funkeln,hk-x-k24x4; animation-duration:.5s}')
|
||||
assert.equal(zeilen[5], '.hk-t .hk-d{animation:none!important}')
|
||||
assert.equal(zeilen[6], '.hk-t .hk-e{animation:funkeln2 1s linear infinite}', 'unbekannte Namen (nur Teilstring eines Keyframes) bleiben')
|
||||
assert.equal(zeilen[7], '.hk-t .hk-f{-webkit-animation:hk-x-funkeln 1s}')
|
||||
})
|
||||
|
||||
test('Animationen: Schlüsselwörter, steps(1,end) und Zahlen wie .5s oder -3s bleiben, Namen nach einer Funktion werden trotzdem umbenannt', () => {
|
||||
const { css } = umformen('@keyframes dampfzeit{0%{opacity:1}} .a{animation:k10 1000ms steps(4) infinite, dampfzeit 3200ms steps(1,end) infinite -.5s}')
|
||||
assert.match(css, /\.hk-t \.hk-a\{animation:k10 1000ms steps\(4\) infinite, hk-x-dampfzeit 3200ms steps\(1,end\) infinite -\.5s\}/)
|
||||
const { css: c2 } = umformen('@keyframes a{to{x:1}} .b{animation:cubic-bezier(.2,.7,.2,1) a 2s ease-in-out infinite}')
|
||||
assert.match(c2, /animation:cubic-bezier\(\.2,\.7,\.2,1\) hk-x-a 2s ease-in-out infinite/)
|
||||
})
|
||||
|
||||
test('verschachtelte @media/@supports: Regeln darin werden wie oben umgeformt, die Bedingung bleibt', () => {
|
||||
const { css } = umformen(`
|
||||
@media (prefers-reduced-motion: reduce){.a,.b{animation:none} body{margin:1px} @supports (display:grid){.c{display:grid}}}
|
||||
@media (min-width:720px){a:hover{color:red}}
|
||||
`)
|
||||
assert.equal(css, [
|
||||
'@media (prefers-reduced-motion: reduce){',
|
||||
'.hk-t .hk-a,.hk-t .hk-b{animation:none}',
|
||||
'.hk-t{margin:1px}',
|
||||
'@supports (display:grid){',
|
||||
'.hk-t .hk-c{display:grid}',
|
||||
'}',
|
||||
'}',
|
||||
'@media (min-width:720px){',
|
||||
'.hk-t a:hover{color:red}',
|
||||
'}',
|
||||
].join('\n'))
|
||||
})
|
||||
|
||||
test('Kommentare und Zeichenketten: Klammern und Semikola in Texten stören nicht', () => {
|
||||
const { css } = umformen(`
|
||||
/* Kopf { mit Klammer } */
|
||||
.a{content:"}{;"; /* mitten { drin */ color:red}
|
||||
/* .b{x:1} */
|
||||
.c::after{content:'a\\'b}'}
|
||||
.d{font-family:'Pixelify Sans',monospace}
|
||||
`)
|
||||
assert.equal(css, [
|
||||
'.hk-t .hk-a{content:"}{;"; color:red}',
|
||||
".hk-t .hk-c::after{content:'a\\'b}'}",
|
||||
".hk-t .hk-d{font-family:'Pixelify Sans',monospace}",
|
||||
].join('\n'))
|
||||
assert.throws(() => umformen('.a{content:"offen}'), /Zeichenkette ohne Ende/)
|
||||
assert.throws(() => umformen('.a{color:red} /* offen'), /Kommentar ohne Ende/)
|
||||
})
|
||||
|
||||
test('@font-face und @import entfallen samt Inhalt (deren Blob-Adressen werden gar nicht erst aufgelöst)', () => {
|
||||
const aufgeloest = []
|
||||
const { css } = umformen('@import url("x.css");\n@font-face{font-family:"X";src:url(/_blob/zzz)}\n.a{color:red}\n@import "y.css";', { blobPfad: (id) => { aufgeloest.push(id); return BLOB(id) } })
|
||||
assert.equal(css, '.hk-t .hk-a{color:red}')
|
||||
assert.deepEqual(aufgeloest, [])
|
||||
})
|
||||
|
||||
test('Blob-Adressen: url(…) in allen Schreibweisen und in border-image/mask werden zum Asset-Pfad', () => {
|
||||
const { css } = umformen(`
|
||||
.a{background:url(/_blob/abc1) 0 0 / 10px repeat, linear-gradient(red, blue)}
|
||||
.b{background-image:url("/_blob/abc2")}
|
||||
.c{background-image:url( '/_blob/abc3' )}
|
||||
.d{border-image:url(/_blob/abc4) 8 fill stretch}
|
||||
.e{-webkit-mask-image:url(/_blob/abc5); mask-image:url(/_blob/abc5)}
|
||||
.f{background:URL(/_blob/abc6)}
|
||||
.g{background:url(data:image/png;base64,AAAA)}
|
||||
`)
|
||||
assert.match(css, /\.hk-t \.hk-a\{background:url\(\.\.\/\.\.\/assets\/hustler\/abc1\.png\) 0 0 \/ 10px repeat, linear-gradient\(red, blue\)\}/)
|
||||
assert.match(css, /\.hk-b\{background-image:url\("\.\.\/\.\.\/assets\/hustler\/abc2\.png"\)\}/)
|
||||
assert.match(css, /\.hk-c\{background-image:url\( '\.\.\/\.\.\/assets\/hustler\/abc3\.png' \)\}|\.hk-c\{background-image:url\('\.\.\/\.\.\/assets\/hustler\/abc3\.png'\)\}/)
|
||||
assert.match(css, /\.hk-d\{border-image:url\(\.\.\/\.\.\/assets\/hustler\/abc4\.png\) 8 fill stretch\}/)
|
||||
assert.match(css, /\.hk-e\{-webkit-mask-image:url\(\.\.\/\.\.\/assets\/hustler\/abc5\.png\); mask-image:url\(\.\.\/\.\.\/assets\/hustler\/abc5\.png\)\}/)
|
||||
assert.match(css, /\.hk-f\{background:url\(\.\.\/\.\.\/assets\/hustler\/abc6\.png\)\}/)
|
||||
assert.match(css, /\.hk-g\{background:url\(data:image\/png;base64,AAAA\)\}/, 'data:-Adressen mit ; bleiben ganz')
|
||||
assert.ok(!css.includes('/_blob/'))
|
||||
})
|
||||
|
||||
test('Blob-Adressen: unbekannte Id und unerwartete Schreibweise brechen mit klarer Meldung ab', () => {
|
||||
const streng = (id) => { throw new ImportFehler(`Im CSS steht /_blob/${id}, aber blobs.json kennt diese Adresse nicht.`) }
|
||||
assert.throws(() => umformen('.a{background:url(/_blob/unbekannt)}', { blobPfad: streng }), /kennt diese Adresse nicht/)
|
||||
assert.throws(() => umformen('.a{background:url(/_blob/ab/cd)}'), /unerwartete Blob-Adresse/)
|
||||
})
|
||||
|
||||
test('nicht unterstützte Konstrukte werden gemeldet statt falsch umgeformt', () => {
|
||||
assert.throws(() => umformen('.a{.b{x:1}}'), /verschachtelte Regel/)
|
||||
assert.throws(() => umformen('@page{margin:0}'), /nicht unterstützte At-Regel/)
|
||||
assert.throws(() => umformen('.a{color:red'), ImportFehler)
|
||||
assert.throws(() => umformen('.a{color}'), /Deklaration ohne/)
|
||||
assert.throws(() => umformen('}'), /unerwartete schließende Klammer/)
|
||||
assert.throws(() => umformen('@namespace svg url(x);'), /nicht unterstützte Anweisung/)
|
||||
})
|
||||
|
||||
test('Keyframe-Namen aus @keyframes in @media und mit Vendor-Präfix werden ebenfalls erfasst', () => {
|
||||
const { css, keyframes } = umformen('@media (min-width:1px){@keyframes w{to{x:1}}} @-webkit-keyframes w{to{x:1}} .a{animation:w 1s}')
|
||||
assert.deepEqual(keyframes, ['hk-x-w'])
|
||||
assert.match(css, /\.hk-t \.hk-a\{animation:hk-x-w 1s\}/)
|
||||
assert.match(css, /@-webkit-keyframes hk-x-w\{to\{x:1\}\}/)
|
||||
})
|
||||
|
||||
// ───────────────────────── Planer gegen die echte Zeichenfläche (übersprungen, wenn sie fehlt) ─────────────────────────
|
||||
|
||||
const opt = { skip: hatV2 ? false : `pixelwelt-v2 nicht gefunden (${V2}); HUSTLER_V2 setzen` }
|
||||
|
||||
test('baue: zwei Läufe liefern byte-gleiche Ausgaben (gleiche Prüfsumme); je Schlüssel der Tabelle eine PNG, dazu 3 CSS + 1 JS', opt, async () => {
|
||||
const quellen = JSON.parse(readFileSync(new URL('../../scripts/hustler-import/quellen.json', import.meta.url), 'utf8'))
|
||||
const a = await baue({ v2: V2 })
|
||||
const b = await baue({ v2: V2 })
|
||||
assert.equal(pruefsumme(a.dateien), pruefsumme(b.dateien))
|
||||
const namen = [...a.dateien.keys()]
|
||||
const verschieden = new Set(Object.values(quellen).flatMap((t) => Object.values(t)))
|
||||
assert.equal(namen.filter((n) => n.endsWith('.png')).length, verschieden.size)
|
||||
assert.deepEqual(namen.filter((n) => !n.endsWith('.png')).sort(), ['src/lib/hustlerWelt.js', 'src/styles/hustler/hochhaus.css', 'src/styles/hustler/keller.css', 'src/styles/hustler/pult.css'])
|
||||
for (const name of ['hochhaus', 'keller', 'pult']) {
|
||||
assert.equal(a.bericht.bildschirme[name].dateien, Object.keys(quellen[name]).length, `${name}: jedes Bild der Tabelle wird im CSS benutzt`)
|
||||
}
|
||||
// gemeinsam benutzte Bilder liegen einmal, die Zielpfade spiegeln die Ordner ohne „entwurf/“
|
||||
assert.ok(a.dateien.has('src/assets/hustler/ui/knopf.png'))
|
||||
assert.ok(a.dateien.has('src/assets/hustler/keller/ui/robo.png'))
|
||||
assert.ok(a.dateien.has('src/assets/hustler/ui/robo.png'))
|
||||
assert.ok(a.dateien.has('src/assets/hustler/pult/eigen/lichterkette-0.png'))
|
||||
assert.ok(![...a.dateien.keys()].some((n) => n.includes('/entwurf/')))
|
||||
})
|
||||
|
||||
test('baue: die Dateien im Repo entsprechen dem, was das Skript aus pixelwelt-v2 erzeugt (außer wenn mit pixelwelt-woms gebaut wurde)', opt, async () => {
|
||||
const plan = await baue({ v2: V2 })
|
||||
const repo = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../..')
|
||||
const cssDatei = readFileSync(path.join(repo, 'src/styles/hustler/keller.css'), 'utf8')
|
||||
assert.equal(cssDatei, plan.dateien.get('src/styles/hustler/keller.css'))
|
||||
const pultDatei = readFileSync(path.join(repo, 'src/styles/hustler/pult.css'), 'utf8')
|
||||
assert.equal(pultDatei, plan.dateien.get('src/styles/hustler/pult.css'))
|
||||
})
|
||||
|
||||
test('baue: fehlende Schlüssel, Dateien und falsche Schlüssel brechen mit deutscher Meldung ab', opt, async () => {
|
||||
const quellen = JSON.parse(readFileSync(new URL('../../scripts/hustler-import/quellen.json', import.meta.url), 'utf8'))
|
||||
const ohneKey = structuredClone(quellen)
|
||||
delete ohneKey.pult.klammer
|
||||
await assert.rejects(baue({ v2: V2, quellen: ohneKey }), (e) => e instanceof ImportFehler && /Schlüssel „klammer“.*fehlt in scripts\/hustler-import\/quellen\.json/.test(e.message))
|
||||
const ohneDatei = structuredClone(quellen)
|
||||
ohneDatei.keller.keller = 'keller/szene/gibt-es-nicht.png'
|
||||
await assert.rejects(baue({ v2: V2, quellen: ohneDatei }), (e) => e instanceof ImportFehler && /Quelldatei fehlt: keller\/szene\/gibt-es-nicht\.png/.test(e.message))
|
||||
const falscherKey = structuredClone(quellen)
|
||||
falscherKey.hochhaus['gibt-es-nicht'] = 'haus/haus.png'
|
||||
await assert.rejects(baue({ v2: V2, quellen: falscherKey }), /Schlüssel „gibt-es-nicht“ steht nicht in/)
|
||||
const unbekannterBildschirm = { ...structuredClone(quellen), handy: {} }
|
||||
await assert.rejects(baue({ v2: V2, quellen: unbekannterBildschirm }), /unbekannter Bildschirm „handy“/)
|
||||
const gefaehrlich = structuredClone(quellen)
|
||||
gefaehrlich.pult.pult = '../../etc/passwd.png'
|
||||
await assert.rejects(baue({ v2: V2, quellen: gefaehrlich }), /ungültiger Quellpfad/)
|
||||
await assert.rejects(baue({ v2: path.join(V2, 'gibt-es-nicht') }), /pixelwelt-v2 existiert nicht/)
|
||||
})
|
||||
|
||||
test('baue: eine veränderte Datei fällt über hochgeladen.sha256 auf; ein veralteter Blob im CSS ebenso', opt, async () => {
|
||||
const tmp = mkdtempSync(path.join(os.tmpdir(), 'hustler-v2-'))
|
||||
try {
|
||||
cpSync(V2, tmp, { recursive: true })
|
||||
const datei = path.join(tmp, 'detail/dampf.png')
|
||||
const bytes = readFileSync(datei)
|
||||
bytes[bytes.length - 1] ^= 0xff
|
||||
writeFileSync(datei, bytes)
|
||||
await assert.rejects(baue({ v2: tmp }), (e) => e instanceof ImportFehler && /Prüfsumme stimmt nicht: detail\/dampf\.png/.test(e.message))
|
||||
// Blob-Adresse im CSS, die blobs.json nicht kennt
|
||||
writeFileSync(datei, readFileSync(path.join(V2, 'detail/dampf.png')))
|
||||
const html = path.join(tmp, 'keller/entwurf/BotLeads.dc.html')
|
||||
writeFileSync(html, readFileSync(html, 'utf8').replace('</style>', '.neu{background:url(/_blob/ffffffffffffffffffffffffffffffff)}</style>'))
|
||||
await assert.rejects(baue({ v2: tmp }), (e) => e instanceof ImportFehler && /BotLeads\.dc\.html steht \/_blob\/ffffffffffffffffffffffffffffffff, aber .*blobs\.json kennt diese Adresse nicht/.test(e.message))
|
||||
} finally {
|
||||
loesche(tmp)
|
||||
}
|
||||
})
|
||||
|
||||
test('baue mit pixelwelt-woms: gleichnamige Haus-Dateien ersetzen die aus v2, graue Räume werden Klassen und Daten, alte Ebenen verschwinden wieder', opt, async () => {
|
||||
const tmp = mkdtempSync(path.join(os.tmpdir(), 'hustler-woms-'))
|
||||
const wurzel = mkdtempSync(path.join(os.tmpdir(), 'hustler-wurzel-'))
|
||||
try {
|
||||
mkdirSync(path.join(tmp, 'haus'))
|
||||
cpSync(path.join(V2, 'haus/schild-a.png'), path.join(tmp, 'haus/haus.png'))
|
||||
cpSync(path.join(V2, 'ui/panel.png'), path.join(tmp, 'haus/raum-grau-netz.png'))
|
||||
cpSync(path.join(V2, 'ui/knopf.png'), path.join(tmp, 'haus/raum-grau-kleinanzeigen.png'))
|
||||
writeFileSync(path.join(tmp, 'haus/manifest.json'), JSON.stringify({ graueRaeume: { netz: { x: 1040, y: 558, w: 24, h: 24 }, kleinanzeigen: { file: 'raum-grau-kleinanzeigen.png', x: 1140, y: 558 } } }))
|
||||
const ohne = await baue({ v2: V2 })
|
||||
const mit = await baue({ v2: V2, woms: tmp })
|
||||
assert.notEqual(pruefsumme(ohne.dateien), pruefsumme(mit.dateien))
|
||||
assert.ok(mit.dateien.get('src/assets/hustler/haus/haus.png').equals(readFileSync(path.join(V2, 'haus/schild-a.png'))), 'haus.png stammt aus pixelwelt-woms')
|
||||
assert.ok(mit.dateien.get('src/assets/hustler/haus/haus-fenster-a.png').equals(ohne.dateien.get('src/assets/hustler/haus/haus-fenster-a.png')), 'andere Dateien bleiben aus v2')
|
||||
assert.deepEqual(mit.bericht.graueRaeume, ['netz', 'kleinanzeigen'])
|
||||
const css = mit.dateien.get('src/styles/hustler/hochhaus.css')
|
||||
assert.match(css, /\.hk-hochhaus \.hk-raum-grau-netz\{position:absolute; left:1040px; top:558px; width:24px; height:24px; background-image:url\(\.\.\/\.\.\/assets\/hustler\/haus\/raum-grau-netz\.png\); image-rendering:pixelated; pointer-events:none\}/)
|
||||
assert.match(css, /\.hk-hochhaus \.hk-raum-grau-kleinanzeigen\{position:absolute; left:1140px; top:558px; width:24px; height:24px;/)
|
||||
const welt = mit.dateien.get('src/lib/hustlerWelt.js')
|
||||
assert.match(welt, /export const GRAUE_RAEUME = Object\.freeze\(\{\s*netz: Object\.freeze\(\{ left: 1040, top: 558, width: 24, height: 24 \}\),\s*kleinanzeigen:/)
|
||||
// schreiben, nochmal schreiben (nichts mehr zu tun), dann ohne woms: die grauen Ebenen werden entfernt
|
||||
const z1 = schreibe(mit, wurzel)
|
||||
assert.equal(z1.geschrieben, mit.dateien.size)
|
||||
assert.equal(schreibe(mit, wurzel).geschrieben, 0)
|
||||
assert.ok(existsSync(path.join(wurzel, 'src/assets/hustler/haus/raum-grau-netz.png')))
|
||||
const z2 = schreibe(ohne, wurzel)
|
||||
assert.deepEqual(z2.entfernt.sort(), ['raum-grau-kleinanzeigen.png', 'raum-grau-netz.png'])
|
||||
assert.ok(!existsSync(path.join(wurzel, 'src/assets/hustler/haus/raum-grau-netz.png')))
|
||||
// Fehler: kein Such-Raum, falsche Größe
|
||||
writeFileSync(path.join(tmp, 'haus/manifest.json'), JSON.stringify({ graueRaeume: { bruecke: { x: 1, y: 1 } } }))
|
||||
await assert.rejects(baue({ v2: V2, woms: tmp }), /„bruecke“ ist kein Such-Raum/)
|
||||
writeFileSync(path.join(tmp, 'haus/manifest.json'), JSON.stringify({ graueRaeume: { netz: { x: 1040, y: 558, w: 99, h: 24 } } }))
|
||||
await assert.rejects(baue({ v2: V2, woms: tmp }), /99×24 im Manifest, aber raum-grau-netz\.png ist 24×24/)
|
||||
// ein Ordner ohne graueRaeume ändert an den Räumen nichts
|
||||
writeFileSync(path.join(tmp, 'haus/manifest.json'), JSON.stringify({}))
|
||||
assert.deepEqual((await baue({ v2: V2, woms: tmp })).bericht.graueRaeume, [])
|
||||
} finally {
|
||||
loesche(tmp)
|
||||
loesche(wurzel)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,167 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { loadModule } from './load.mjs'
|
||||
|
||||
// Geometrie des Hochhauses (src/lib/hustlerWelt.js, vom Import-Skript erzeugt) gegen das Roster
|
||||
|
||||
const W = await loadModule('src/lib/hustlerWelt.js')
|
||||
const R = await loadModule('src/lib/workersRoster.js')
|
||||
const { WELT, HAUS, HAUS_BILD, ETAGEN, ETAGEN_SPALTE, RAEUME, HUSTLER, SCHILDER, AUFZUG, DETAIL, GRAUE_RAEUME } = W
|
||||
const { ROLES, FLOORS, ROOM_TITLES, SEARCH_ROOMS, roleOf } = R
|
||||
|
||||
function tiefEingefroren(wert, pfad = 'wert') {
|
||||
if (wert === null || typeof wert !== 'object') return
|
||||
assert.ok(Object.isFrozen(wert), `${pfad} ist nicht eingefroren`)
|
||||
for (const [k, v] of Object.entries(wert)) tiefEingefroren(v, `${pfad}.${k}`)
|
||||
}
|
||||
|
||||
test('Welt und Haus: Größen wie in der Zeichenfläche', () => {
|
||||
assert.deepEqual({ ...WELT }, { w: 2400, h: 1200, hud: 84 })
|
||||
assert.deepEqual({ ...HAUS }, { x: 930, y: 366, w: 570, h: 706 })
|
||||
assert.deepEqual({ ...HAUS_BILD }, { x: 930, y: 370, w: 570, h: 700 })
|
||||
assert.ok(HAUS.x >= 0 && HAUS.x + HAUS.w <= WELT.w && HAUS.y >= 0 && HAUS.y + HAUS.h <= WELT.h)
|
||||
assert.ok(HAUS_BILD.x >= HAUS.x && HAUS_BILD.y >= HAUS.y && HAUS_BILD.x + HAUS_BILD.w <= HAUS.x + HAUS.w && HAUS_BILD.y + HAUS_BILD.h <= HAUS.y + HAUS.h)
|
||||
})
|
||||
|
||||
test('alle Geometrie-Daten sind tief eingefroren', () => {
|
||||
for (const [name, wert] of Object.entries(W)) tiefEingefroren(wert, name)
|
||||
})
|
||||
|
||||
test('ETAGEN: Ids gleich FLOORS in Reihenfolge, Klickbereiche lückenlos von oben nach unten', () => {
|
||||
assert.deepEqual(ETAGEN.map((e) => e.id), [...FLOORS])
|
||||
for (let i = 0; i < ETAGEN.length; i += 1) {
|
||||
const e = ETAGEN[i]
|
||||
assert.ok(e.oben < e.unten, `${e.id}: Klickbereich leer`)
|
||||
if (i > 0) assert.equal(ETAGEN[i - 1].unten, e.oben, `${ETAGEN[i - 1].id} und ${e.id} schließen nicht lückenlos an`)
|
||||
const [o, u] = e.oeffnung
|
||||
assert.ok(e.oben <= o && o < u && u <= e.unten, `${e.id}: Öffnung ${o}-${u} nicht im Klickbereich ${e.oben}-${e.unten}`)
|
||||
}
|
||||
assert.equal(ETAGEN[0].oben, 476)
|
||||
assert.equal(ETAGEN.at(-1).unten, 1062)
|
||||
assert.deepEqual({ ...ETAGEN_SPALTE }, { x: 960, w: 512 })
|
||||
// Etagen links vom Raum-Bereich, Spalte deckt ihn ab
|
||||
assert.ok(ETAGEN_SPALTE.x <= 1040 && ETAGEN_SPALTE.x + ETAGEN_SPALTE.w >= 1440)
|
||||
})
|
||||
|
||||
test('RAEUME: jeder Raum aus ROOM_TITLES hat ein Rechteck innerhalb des Hauses', () => {
|
||||
assert.deepEqual(Object.keys(RAEUME).sort(), Object.keys(ROOM_TITLES).sort())
|
||||
for (const [schluessel, r] of Object.entries(RAEUME)) {
|
||||
assert.ok(r.x0 < r.x1 && r.y0 < r.y1, `${schluessel}: leeres Rechteck`)
|
||||
assert.ok(r.x0 >= HAUS.x && r.x1 <= HAUS.x + HAUS.w && r.y0 >= HAUS.y && r.y1 <= HAUS.y + HAUS.h, `${schluessel}: außerhalb von HAUS`)
|
||||
const etage = ETAGEN.find((e) => e.id === r.etage)
|
||||
assert.ok(etage, `${schluessel}: unbekannte Etage ${r.etage}`)
|
||||
assert.deepEqual([r.y0, r.y1], [...etage.oeffnung], `${schluessel}: y-Bereich ungleich Öffnung der Etage`)
|
||||
// Etiketten-Punkt: x0 + 20, Öffnung oben + 3
|
||||
assert.deepEqual({ ...r.etikett }, { x: r.x0 + 20, y: r.y0 + 3 }, `${schluessel}: Etikett`)
|
||||
}
|
||||
})
|
||||
|
||||
test('RAEUME: die Räume einer Etage füllen 1040 bis 1440 ohne Lücke und ohne Überlappung', () => {
|
||||
for (const etage of FLOORS) {
|
||||
const raeume = Object.values(RAEUME).filter((r) => r.etage === etage).sort((a, b) => a.x0 - b.x0)
|
||||
assert.ok(raeume.length >= 1, `${etage}: kein Raum`)
|
||||
assert.equal(raeume[0].x0, 1040, `${etage}: Anfang`)
|
||||
assert.equal(raeume.at(-1).x1, 1440, `${etage}: Ende`)
|
||||
for (let i = 1; i < raeume.length; i += 1) assert.equal(raeume[i - 1].x1, raeume[i].x0, `${etage}: Lücke oder Überlappung`)
|
||||
}
|
||||
// Worker-Räume je Etage wie im Roster
|
||||
for (const rolle of ROLES) assert.equal(RAEUME[rolle.room].etage, rolle.floor, `${rolle.workerId}: Raum ${rolle.room} liegt auf der falschen Etage`)
|
||||
assert.equal(RAEUME.pult.etage, 'freigabe')
|
||||
assert.equal(RAEUME.tresor.etage, 'tresor')
|
||||
})
|
||||
|
||||
test('HUSTLER: jede Rolle hat einen Eintrag, nicht mehr und nicht weniger', () => {
|
||||
assert.deepEqual(Object.keys(HUSTLER).sort(), ROLES.map((r) => r.workerId).sort())
|
||||
assert.equal(Object.keys(HUSTLER).length, 19)
|
||||
})
|
||||
|
||||
test('HUSTLER: Anker liegt im Rechteck des Raums (otto auf der Brücke)', () => {
|
||||
for (const [id, h] of Object.entries(HUSTLER)) {
|
||||
const raum = RAEUME[roleOf(id).room]
|
||||
assert.ok(raum, `${id}: Raum fehlt`)
|
||||
assert.ok(h.a >= raum.x0 && h.a <= raum.x1, `${id}: a=${h.a} nicht in ${raum.x0}-${raum.x1}`)
|
||||
assert.ok(h.f >= raum.y0 && h.f <= raum.y1, `${id}: f=${h.f} nicht in ${raum.y0}-${raum.y1}`)
|
||||
// Fußpunkt der Hustler am Schreibtisch: 4 px über der Unterkante der Öffnung (Spec der Zeichenfläche);
|
||||
// der Overseer schwebt über der Holo-Konsole und steht höher
|
||||
if (id === 'otto') assert.ok(h.f < raum.y1 - 4, 'Overseer schwebt über dem Boden')
|
||||
else assert.equal(h.f, raum.y1 - 4, `${id}: Fuß nicht 4 px über dem Boden`)
|
||||
}
|
||||
assert.equal(roleOf('otto').room, 'bruecke')
|
||||
assert.equal(RAEUME[roleOf('otto').room].etage, 'bruecke')
|
||||
})
|
||||
|
||||
test('HUSTLER: Knopf-Rechteck = Anker (left a-12, top f-31, 24x32), Overseer eigene Größe', () => {
|
||||
for (const [id, h] of Object.entries(HUSTLER)) {
|
||||
if (id === 'otto') continue
|
||||
assert.deepEqual({ ...h.knopf }, { left: h.a - 12, top: h.f - 31, w: 24, h: 32 }, id)
|
||||
}
|
||||
const o = HUSTLER.otto
|
||||
assert.deepEqual({ ...o.knopf }, { left: 1236, top: 497, w: 28, h: 40 })
|
||||
// Sprite-Anker (14, 39) sitzt auf dem Anker
|
||||
assert.equal(o.knopf.left + 14, o.a)
|
||||
assert.equal(o.knopf.top + 39, o.f)
|
||||
assert.deepEqual([o.a, o.f], [1250, 536])
|
||||
})
|
||||
|
||||
test('HUSTLER: Bildschirm bei a-40/f-26 für alle außer otto, Radar nur bei Rita', () => {
|
||||
for (const [id, h] of Object.entries(HUSTLER)) {
|
||||
if (id === 'otto') {
|
||||
assert.equal(h.bildschirm, undefined, 'Overseer hat keinen Schreibtisch-Bildschirm')
|
||||
} else {
|
||||
assert.deepEqual({ ...h.bildschirm }, { left: h.a - 40, top: h.f - 26 }, id)
|
||||
}
|
||||
if (id === 'rita') assert.deepEqual({ ...h.radar }, { left: 1118, top: 566 })
|
||||
else assert.equal(h.radar, undefined, `${id}: unerwartetes Radar`)
|
||||
}
|
||||
const rita = RAEUME.netz
|
||||
assert.ok(HUSTLER.rita.radar.left >= rita.x0 && HUSTLER.rita.radar.left + 20 <= rita.x1)
|
||||
})
|
||||
|
||||
test('HUSTLER: Anker verschiedener Hustler im selben Raum liegen nicht aufeinander', () => {
|
||||
const gesehen = new Map()
|
||||
for (const [id, h] of Object.entries(HUSTLER)) {
|
||||
const schluessel = `${h.a}/${h.f}`
|
||||
assert.ok(!gesehen.has(schluessel), `${id} und ${gesehen.get(schluessel)} stehen auf demselben Punkt`)
|
||||
gesehen.set(schluessel, id)
|
||||
}
|
||||
})
|
||||
|
||||
test('SCHILDER, AUFZUG und DETAIL: Positionen vorhanden und im erwarteten Bereich', () => {
|
||||
const zahl = (v, was) => assert.ok(Number.isFinite(v), `${was} ist keine Zahl`)
|
||||
// Briefkasten-Zähler im Pult, Tresor-/Keller-Text in ihren Räumen
|
||||
assert.ok(SCHILDER.briefkasten.left >= RAEUME.pult.x0 && SCHILDER.briefkasten.top >= RAEUME.pult.y0 - 10 && SCHILDER.briefkasten.top < RAEUME.pult.y1)
|
||||
assert.ok(SCHILDER.tresor.left >= RAEUME.tresor.x0 && SCHILDER.tresor.left < RAEUME.tresor.x1 && SCHILDER.tresor.top >= RAEUME.tresor.y0 && SCHILDER.tresor.top < RAEUME.tresor.y1)
|
||||
assert.ok(SCHILDER.keller.left >= RAEUME.archiv.x0 && SCHILDER.keller.left < RAEUME.archiv.x1 && SCHILDER.keller.top >= RAEUME.archiv.y0 && SCHILDER.keller.top < RAEUME.archiv.y1)
|
||||
// Neon-Fläche und Antenne über dem Dach (oberhalb der Brücke)
|
||||
assert.ok(SCHILDER.neon.top + SCHILDER.neon.h <= RAEUME.bruecke.y0 && SCHILDER.antenne.top + SCHILDER.antenne.h <= RAEUME.bruecke.y0)
|
||||
assert.deepEqual({ ...SCHILDER.neon }, { left: 1088, top: 394, w: 224, h: 32, schrift: 22 })
|
||||
assert.deepEqual({ ...SCHILDER.antenne }, { left: 1408, top: 394, w: 8, h: 6 })
|
||||
for (const [name, s] of Object.entries(SCHILDER)) for (const [k, v] of Object.entries(s)) if (typeof v === 'number') zahl(v, `SCHILDER.${name}.${k}`)
|
||||
// „nicht angeschlossen“-Zettel gehören zu Such-Räumen
|
||||
assert.ok(SCHILDER.nichtAngeschlossen.breite > 0)
|
||||
for (const raum of Object.keys(SCHILDER.nichtAngeschlossen.raeume)) assert.ok(SEARCH_ROOMS.includes(raum), `${raum} ist kein Such-Raum`)
|
||||
// Aufzug: ein Halt je Etage, Kabine startet am ersten Halt, Seil hängt über der Kabine
|
||||
assert.equal(AUFZUG.halte.length, FLOORS.length)
|
||||
assert.deepEqual([...AUFZUG.halte], [...AUFZUG.halte].sort((a, b) => a - b))
|
||||
assert.equal(AUFZUG.kabine.top, AUFZUG.halte[0])
|
||||
assert.ok(AUFZUG.seil.top + AUFZUG.seil.h <= AUFZUG.kabine.top + 1 && AUFZUG.seil.top < AUFZUG.kabine.top)
|
||||
ETAGEN.forEach((e, i) => assert.equal(AUFZUG.halte[i] + AUFZUG.kabine.h, e.oeffnung[1], `Halt ${e.id}: Kabinenboden bündig mit dem Boden der Öffnung`))
|
||||
// Detailansicht
|
||||
assert.deepEqual({ ...DETAIL.szene }, { w: 640, h: 400 })
|
||||
assert.deepEqual(Object.keys(DETAIL.figur).sort(), ['bruecke', 'buero', 'nicht'])
|
||||
for (const p of [...Object.values(DETAIL.figur), DETAIL.monitor, DETAIL.dampf, DETAIL.zzz, DETAIL.wirbel, DETAIL.schild]) {
|
||||
assert.ok(p.left >= 0 && p.top >= 0 && p.left < DETAIL.szene.w && p.top < DETAIL.szene.h, `Overlay außerhalb der Szene: ${JSON.stringify(p)}`)
|
||||
}
|
||||
assert.ok(DETAIL.schild.w > 0 && DETAIL.schild.h > 0)
|
||||
})
|
||||
|
||||
test('GRAUE_RAEUME: nur Such-Räume mit sinnvoller Lage (leer, solange das neue Haus fehlt)', () => {
|
||||
assert.equal(typeof GRAUE_RAEUME, 'object')
|
||||
for (const [raum, g] of Object.entries(GRAUE_RAEUME)) {
|
||||
assert.ok(SEARCH_ROOMS.includes(raum), `${raum} ist kein Such-Raum`)
|
||||
for (const k of ['left', 'top', 'width', 'height']) assert.ok(Number.isInteger(g[k]), `${raum}.${k}`)
|
||||
const r = RAEUME[raum]
|
||||
assert.ok(g.left >= HAUS_BILD.x && g.left + g.width <= HAUS_BILD.x + HAUS_BILD.w && g.top >= HAUS_BILD.y && g.top + g.height <= HAUS_BILD.y + HAUS_BILD.h, `${raum}: außerhalb des Hausbilds`)
|
||||
assert.ok(g.left < r.x1 && g.left + g.width > r.x0 && g.top < r.y1 && g.top + g.height > r.y0, `${raum}: Ebene trifft den Raum nicht`)
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user