Hustler-Pixelwelt T13: Korrekturen Hochhaus (Fokus, Tastatur, weniger Bewegung, Handy-Tippflächen, Schnee per transform, Pause überall, Seite ohne Leerlauf-Blitz)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-22 09:47:57 +00:00
1 parent 0924a8d2ae
commit 5e6698f5e0
14 files changed
+994 -52

No files matched your search

+305 -1
View File
@@ -34,7 +34,7 @@ const laden = () => (bundle ??= esbuild.build({
"export { default as HochhausWelt } from './src/components/workers/hochhaus/HochhausWelt.jsx'",
"export { indexWorkers } from './src/lib/workersEvents.js'",
"export { AUFZUG, ETAGEN, GRAUE_RAEUME, HUSTLER, RAEUME, SCHILDER } from './src/lib/hustlerWelt.js'",
"export { figurZustand, kameraEtage, kameraGrenzen, kameraMinS, kameraPassend, kameraZoomAuf } from './src/lib/hustlerSicht.js'",
"export { figurZustand, kameraEtage, kameraGrenzen, kameraMinS, kameraPassend, kameraZeige, kameraZoomAuf, tippRand } from './src/lib/hustlerSicht.js'",
"export { FLOOR_TITLES, ROLES, ROOM_TITLES, SEARCH_ROOMS } from './src/lib/workersRoster.js'",
].join('\n'),
resolveDir: ROOT, loader: 'js',
@@ -865,6 +865,306 @@ test('Kamera: beim Abbau fallen Rad-Zuhörer, Beobachter und Gleit-Timer weg; ei
} finally { wechsel.end(); b2.restore(); r2() }
})
// ---------- Korrekturen der Endprüfung (T13): Fokus, Tastatur, Handy, Schnee, weniger Bewegung ----------
const lies = (datei) => readFileSync(path.join(ROOT, 'src/styles/hustler', datei), 'utf8')
const ohneKommentare = (text) => text.replace(/\/\*[\s\S]*?\*\//g, '')
// Block { ... } ab der Klammer an Position `ab` (geschachtelt)
function block(text, ab) {
let tiefe = 0
for (let i = ab; i < text.length; i += 1) {
if (text[i] === '{') tiefe += 1
else if (text[i] === '}') { tiefe -= 1; if (tiefe === 0) return text.slice(ab + 1, i) }
}
throw new Error('Klammer nicht geschlossen')
}
const mediaBlock = (text, kopf) => {
const i = text.indexOf(kopf)
assert.ok(i >= 0, kopf)
return block(text, text.indexOf('{', i))
}
const leucht = (hex) => {
const c = [1, 3, 5].map((i) => Number.parseInt(hex.slice(i, i + 2), 16) / 255).map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4))
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
}
const kontrast = (a, b) => { const [x, y] = [leucht(a), leucht(b)].sort((p, q) => q - p); return (x + 0.05) / (y + 0.05) }
// Element-Attrappe für den Tastaturfokus: Lage in der Bühne (offset*), :focus-visible und Kind der Gruppe `.hk-etage, .hk-hustler`
function fokusZiel({ left, top, width, height }, { figur = true, tastatur = true } = {}) {
const el = {
offsetLeft: left, offsetTop: top, offsetWidth: width, offsetHeight: height,
closest: () => el,
matches: (sel) => (sel === ':focus-visible' ? tastatur : sel === '.hk-hustler' ? figur : false),
}
return el
}
const fokus = (ziel, sicht) => ({ target: ziel, currentTarget: sicht })
test('M1 Fokus per Tastatur: eine Figur außerhalb des Bildes holt die Kamera heran (Kopfzeile abgezogen), mit Gleiten; sichtbar, Maus und gesperrt bewegen nichts', async () => {
const { HUSTLER, kameraEtage, kameraZeige } = await laden()
const restore = fensterAttrappe()
const ui = await start()
try {
const sicht = ui.anhaengen(fakeSicht(390, 844))
ui.etage('Werkstatt').props.onClick(ereignis()) // Handy hochkant: Etage heranzoomen
await wait(650)
const vorher = ui.kamera()
nearKamera(vorher, kameraEtage(776, 832, { w: 390, h: 844 }), 'Werkstatt nah')
// Ben (Werkstatt, rechts) liegt am rechten Rand, außerhalb der 24 px Luft
const k = HUSTLER.ben.knopf
const rechts = vorher.x + (k.left + k.w) * vorher.s
assert.ok(rechts > 390 - 24, `Ausgangslage: Ben ragt in den Rand (${rechts})`)
ui.ruf('onFocus', fokus(fokusZiel({ left: k.left, top: k.top, width: k.w, height: k.h }), sicht))
const nachher = ui.kamera()
const erwartet = kameraZeige(vorher, { left: k.left, top: k.top - 34 / vorher.s, width: k.w, height: k.h + 34 / vorher.s }, { w: 390, h: 844 }, { oben: 84 })
nearKamera(nachher, erwartet, 'Ben sichtbar')
assert.equal(nachher.s, vorher.s, 'Zoom bleibt')
assert.ok(nachher.x + (k.left + k.w) * nachher.s <= 390 - 24 + 1e-6)
assert.ok(hasClass(ui.buehne(), 'hk-gleitet'), 'gleitet')
await wait(650)
// schon sichtbar: keine Bewegung
const still = ui.kamera()
ui.ruf('onFocus', fokus(fokusZiel({ left: k.left, top: k.top, width: k.w, height: k.h }), sicht))
assert.deepEqual(ui.kamera(), still); assert.ok(!hasClass(ui.buehne(), 'hk-gleitet'))
// Mausfokus (kein :focus-visible): kein Schwenk
const bob = HUSTLER.bob.knopf
ui.ruf('onFocus', fokus(fokusZiel({ left: bob.left - 600, top: bob.top, width: bob.w, height: bob.h }, { tastatur: false }), sicht))
assert.deepEqual(ui.kamera(), still)
// ohne passendes Ziel (Fokus auf das Sichtfenster selbst): nichts
ui.ruf('onFocus', { target: { closest: () => null, matches: () => true }, currentTarget: sicht })
ui.ruf('onFocus', { target: null, currentTarget: sicht })
assert.deepEqual(ui.kamera(), still)
// Detailansicht offen: nichts
ui.set({ gesperrt: true })
ui.ruf('onFocus', fokus(fokusZiel({ left: bob.left - 600, top: bob.top, width: bob.w, height: bob.h }), sicht))
assert.deepEqual(ui.kamera(), still)
} finally { ui.end(); restore() }
})
test('M1 Fokus per Tastatur: die Kopfzeile wird gemessen (Unterkante von .hk-kopf, sonst 84 px); bei „weniger Bewegung“ springt die Kamera statt zu gleiten', async () => {
const { kameraZeige } = await laden()
const g = { w: 1280, h: 720 }
const ruhig = fensterAttrappe({ reduziert: true })
const ui = await start()
try {
const sicht = ui.anhaengen(fakeSicht(g.w, g.h))
for (let i = 0; i < 3; i += 1) ui.ruf('onKeyDown', ereignis({ key: '+' }))
const k0 = ui.kamera()
assert.ok(k0.s > 1.5)
// Etage, deren Oberkante auf dem Bildschirm bei 130 px liegt: unter 84 + 24 = 108 (Standard), aber über 150 + 24 = 174 (gemessene Kopfzeile)
const rechteck = { left: 1000, top: (130 - k0.y) / k0.s, width: 512, height: 56 }
assert.equal(kameraZeige(k0, rechteck, g, { oben: 84 }), null, 'mit Standardhöhe nichts zu tun')
const erwartet = kameraZeige(k0, rechteck, g, { oben: 150 })
assert.notEqual(erwartet, null, 'mit gemessener Kopfzeile muss die Kamera schieben')
const ziel = fokusZiel(rechteck, { figur: false })
// ohne messbare Kopfzeile (kein Geschwister .hk-kopf): Standardhöhe, keine Bewegung
ui.ruf('onFocus', fokus(ziel, sicht))
nearKamera(ui.kamera(), k0, 'ohne Messung')
// mit Messung: Kopfzeile reicht bis 150 px -> die Kamera schiebt die Etage darunter
sicht.parentElement = { querySelector: (sel) => (sel === '.hk-kopf' ? { getBoundingClientRect: () => ({ bottom: 150 }) } : null) }
ui.ruf('onFocus', fokus(ziel, sicht))
nearKamera(ui.kamera(), erwartet, 'mit Messung')
assert.ok(ui.kamera().y + rechteck.top * ui.kamera().s >= 150 + 24 - 1e-6, 'Etage liegt unter Kopfzeile + Luft')
assert.equal(ui.kamera().s, k0.s)
assert.ok(!hasClass(ui.buehne(), 'hk-gleitet'), 'bei weniger Bewegung springt die Kamera')
// eine unsinnig hohe Kopfzeile wird auf die Hälfte des Fensters begrenzt (viele Meldungen)
sicht.parentElement = { querySelector: () => ({ getBoundingClientRect: () => ({ bottom: 9999 }) }) }
const k1 = ui.kamera()
ui.ruf('onFocus', fokus(fokusZiel({ left: 1000, top: (400 - k1.y) / k1.s, width: 24, height: 32 }), sicht))
assert.ok(ui.kamera().y + ((400 - k1.y) / k1.s) * ui.kamera().s >= 360 + 24 - 1e-6, 'Obergrenze 50 %')
} finally { ui.end(); ruhig() }
})
test('K1 Zeigerdruck in die Welt gibt dem Sichtfenster den Fokus (tabindex -1, preventScroll); auf Figur/Etage und bei gesperrter Kamera nicht', async () => {
const restore = fensterAttrappe()
const ui = await start()
try {
assert.equal(ui.tree().props.tabIndex, -1)
const sicht = ui.anhaengen()
sicht.focusCalls = []
sicht.focus = (optionen) => { sicht.focusCalls.push(optionen) }
ui.ruf('onPointerDown', zeiger({ clientX: 600, clientY: 400 }))
assert.deepEqual(sicht.focusCalls, [{ preventScroll: true }])
ui.ruf('onPointerUp', zeiger({ clientX: 600, clientY: 400 }))
// auf einem Knopf/Link der Welt fokussiert der Browser selbst: kein zweiter Fokus
ui.ruf('onPointerDown', zeiger({ clientX: 600, clientY: 400, target: aufBedienbarem }))
assert.equal(sicht.focusCalls.length, 1)
ui.ruf('onPointerUp', zeiger({ clientX: 600, clientY: 400 }))
// Finger zählt wie die Maus
ui.ruf('onPointerDown', finger({ clientX: 600, clientY: 400 }))
assert.equal(sicht.focusCalls.length, 2)
ui.ruf('onPointerUp', finger({ clientX: 600, clientY: 400 }))
// Detailansicht offen: die Welt nimmt dem Dialog den Fokus nicht
ui.set({ gesperrt: true })
ui.ruf('onPointerDown', zeiger({ clientX: 600, clientY: 400 }))
assert.equal(sicht.focusCalls.length, 2)
} finally { ui.end(); restore() }
// Sichtfenster ohne focus() (Attrappe, alter Browser): kein Absturz
const r2 = fensterAttrappe()
const ohne = await start()
try { ohne.anhaengen(); ohne.ruf('onPointerDown', zeiger({ clientX: 1, clientY: 1 })) } finally { ohne.end(); r2() }
})
test('K3 Figuren melden einen Dialog (aria-haspopup), das Neon „HUSTLER“ ist für Screenreader versteckt; Etagen tragen ihre Kennung für den Rückweg', async () => {
const { ETAGEN } = await laden()
const ui = await start()
try {
const knoepfe = nodes(ui.tree(), (n) => n.tag === 'button' && n.props['data-hk-hustler'] !== undefined)
assert.equal(knoepfe.length, 19)
for (const b of knoepfe) assert.equal(b.props['aria-haspopup'], 'dialog', b.props['data-hk-hustler'])
// Etagen (die zoomen) melden keinen Dialog
for (const e of byClass(ui.tree(), 'hk-etage')) assert.equal(e.props['aria-haspopup'], undefined)
assert.equal(byClass(ui.tree(), 'hk-neon')[0].props['aria-hidden'], 'true')
assert.deepEqual(byClass(ui.tree(), 'hk-etage').map((n) => n.props['data-hk-etage']), ETAGEN.map((e) => e.id))
} finally { ui.end() }
})
test('M8 Tippflächen: die Bühne setzt die Überstände aus tippRand(Zoom) (--hk-tipp-x/-oben/-unten); sie wachsen beim Herauszoomen, hustler-welt.css wertet sie am ::after aus', async () => {
const { tippRand } = await laden()
const restore = fensterAttrappe()
const ui = await start()
try {
ui.anhaengen(fakeSicht(390, 844))
const gleich = () => {
const t = tippRand(ui.kamera().s)
const stil = ui.buehne().props.style
assert.equal(stil['--hk-tipp-x'], `${t.x.toFixed(2)}px`); assert.equal(stil['--hk-tipp-oben'], `${t.oben.toFixed(2)}px`); assert.equal(stil['--hk-tipp-unten'], `${t.unten.toFixed(2)}px`)
return t
}
const ganz = gleich()
assert.ok(ganz.x > 12, 'ganzes Haus am Handy: breiter als die alten 12 px')
ui.etage('Werkstatt').props.onClick(ereignis())
gleich()
await wait(650)
for (let i = 0; i < 40; i += 1) ui.rad(ereignis({ clientX: 195, clientY: 400, deltaY: -400, deltaMode: 0, ctrlKey: false }))
assert.equal(ui.kamera().s, 5)
assert.deepEqual({ x: gleich().x, oben: gleich().oben, unten: gleich().unten }, { x: 12, oben: 8, unten: 6 })
} finally { ui.end(); restore() }
const css = ohneKommentare(lies('hustler-welt.css'))
const regel = css.match(/\.hk-hochhaus \.hk-buehne \.hk-hustler::after\s*\{([^}]*)\}/)
assert.ok(regel, '::after-Regel der Figuren')
assert.match(regel[1], /left:\s*calc\(-1 \* var\(--hk-tipp-x, 12px\)\)/); assert.match(regel[1], /right:\s*calc\(-1 \* var\(--hk-tipp-x, 12px\)\)/)
assert.match(regel[1], /top:\s*calc\(-1 \* var\(--hk-tipp-oben, 8px\)\)/); assert.match(regel[1], /bottom:\s*calc\(-1 \* var\(--hk-tipp-unten, 6px\)\)/)
// und die Vorgabe bleibt die alte Regel aus hochhaus.css (-12/-12/-8/-6), wenn die Bühne nichts setzt
assert.match(ohneKommentare(lies('hochhaus.css')), /\.hk-hustler::after\{content:""; position:absolute; left:-12px; right:-12px; top:-8px; bottom:-6px\}/)
})
test('Schnee (R1): Bewegung per transform statt background-position, gleiche Richtung, Strecke und Dauer wie die Vorlage; Ebene um genau diese Strecke größer; Container schneidet ab', () => {
const welt = ohneKommentare(lies('hustler-welt.css'))
const vorlage = ohneKommentare(lies('hochhaus.css'))
const dauer = (text, klasse, name) => {
const m = text.match(new RegExp(`${klasse}[^{]*\\{[^}]*animation:\\s*${name}\\s+(\\d+)s linear infinite`))
assert.ok(m, `${klasse} ${name}`)
return Number(m[1])
}
for (const [i, kachel] of [[1, 512], [2, 512], [3, 768]]) {
// Vorlage: background-position: dx dy (Bild wandert um genau diese Strecke)
const v = vorlage.match(new RegExp(`@keyframes hk-h-schnee${i}\\{to\\{background-position:(-?\\d+)px (-?\\d+)px\\}\\}`))
assert.ok(v, `Vorlage hk-h-schnee${i}`)
const [dx, dy] = [Number(v[1]), Number(v[2])]
// neu: dieselbe Strecke als transform
const k = welt.match(new RegExp(`@keyframes hk-welt-schnee${i}\\s*\\{\\s*to\\s*\\{\\s*transform:\\s*translate\\((-?\\d+)px,\\s*(-?\\d+)px\\);\\s*\\}\\s*\\}`))
assert.ok(k, `Keyframes hk-welt-schnee${i} mit transform`)
assert.deepEqual([Number(k[1]), Number(k[2])], [dx, dy], `Strecke ${i}`)
assert.ok(!/background-position/.test(welt.slice(welt.indexOf(`@keyframes hk-welt-schnee${i}`), welt.indexOf(`@keyframes hk-welt-schnee${i}`) + 120)), 'kein background-position')
// gleiche Dauer wie die Vorlage
assert.equal(dauer(welt, `\\.hk-schnee-${i}::before`, `hk-welt-schnee${i}`), dauer(vorlage, `\\.hk-schnee-${i}`, `hk-h-schnee${i}`), `Dauer ${i}`)
// Kachelgröße wie die Vorlage, die Strecke ist ein Vielfaches (nahtlos)
assert.match(vorlage, new RegExp(`\\.hk-schnee-${i}\\{background:url\\([^)]*\\) 0 0 / ${kachel}px ${kachel}px repeat`))
assert.equal(Math.abs(dx) % kachel, 0); assert.equal(Math.abs(dy) % kachel, 0)
// Ebene: um |dx| breiter, um |dy| höher als das Fenster; Anfangslage so, dass die Bewegung sie nie vom Fenster wegzieht
const regel = welt.match(new RegExp(`\\.hk-schnee-${i}::before\\s*\\{([^}]*)\\}`))[1]
assert.match(regel, new RegExp(`width:\\s*calc\\(100% \\+ ${Math.abs(dx)}px\\)`)); assert.match(regel, new RegExp(`height:\\s*calc\\(100% \\+ ${Math.abs(dy)}px\\)`))
assert.match(regel, new RegExp(`left:\\s*${dx > 0 ? `-${dx}px` : '0'}[;\\s]`)); assert.match(regel, new RegExp(`top:\\s*-${dy}px`))
assert.match(regel, new RegExp(`background-size:\\s*${kachel}px ${kachel}px`))
assert.match(regel, new RegExp(`url\\(\\.\\./\\.\\./assets/hustler/welt/schnee-${i}\\.png\\)`))
}
// der Container hat kein eigenes Bild und keine eigene Animation mehr und schneidet ab
const container = welt.match(/\.hk-hochhaus \.hk-sicht > \.hk-schnee\s*\{([^}]*)\}/)[1]
assert.match(container, /background:\s*none/); assert.match(container, /animation:\s*none/); assert.match(container, /overflow:\s*hidden/); assert.match(container, /overflow:\s*clip/)
// Detailansicht offen: Schnee weg; weniger Bewegung: keine Animation; Pause: auch Pseudo-Elemente stehen
assert.match(welt, /\.hk-sicht\.hk-verdeckt > \.hk-schnee\s*\{\s*display:\s*none;?\s*\}/)
assert.match(mediaBlock(welt, '@media (prefers-reduced-motion: reduce)'), /\.hk-schnee::before\s*\{\s*animation:\s*none/)
assert.match(mediaBlock(vorlage, '@media (prefers-reduced-motion: reduce)'), /\.hk-hochhaus \.hk-schnee\{display:none\}/)
const ui = ohneKommentare(lies('hustler-ui.css'))
assert.match(ui, /\.hk-seite \.hk-steht \*::before/); assert.match(ui, /\.hk-seite \.hk-steht \*::after/)
assert.match(ui.match(/\.hk-seite \.hk-steht,[^{]*\{([^}]*)\}/)[1], /animation-play-state:\s*paused !important/)
})
test('Schnee (R1): in der Welt hängen die drei Schnee-Ebenen im Sichtfenster; die Pause (hk-steht) und die offene Detailansicht (hk-verdeckt) stehen auf demselben Element', async () => {
const ui = await start({ state: makeState({ paused: true }), gesperrt: true })
try {
const wurzel = ui.tree()
assert.ok(hasClass(wurzel, 'hk-steht')); assert.ok(hasClass(wurzel, 'hk-verdeckt'))
assert.deepEqual(wurzel.children.filter((k) => hasClass(k, 'hk-schnee')).map((k) => classes(k)[1]), ['hk-schnee-1', 'hk-schnee-2', 'hk-schnee-3'])
ui.set({ gesperrt: false })
assert.ok(hasClass(ui.tree(), 'hk-steht')); assert.ok(!hasClass(ui.tree(), 'hk-verdeckt'))
} finally { ui.end() }
})
test('M4 weniger Bewegung: alle Sprite-Schleifen im Hochhaus (und in der Detailansicht) stehen auf dem ersten Bild; jedes animierte Element der Welt ist von einer Reduziert-Regel erfasst', async () => {
const ui = css => ohneKommentare(lies(css))
const vorlage = ui('hochhaus.css')
const regel = mediaBlock(ui('hustler-ui.css'), '@media (prefers-reduced-motion: reduce)')
assert.match(regel, /\.hk-seite \.hk-hochhaus \.hk-spr\s*\{\s*animation:\s*none;?\s*\}/)
// mit höherer Spezifität als die Sprite-Regeln der Vorlage (.hk-hochhaus .hk-fig-… = 0,2,0): .hk-seite .hk-hochhaus .hk-spr = 0,3,0
assert.match(vorlage, /\.hk-hochhaus \.hk-fig-muetze-arbeiten\{[^}]*animation:/)
// Klassen mit Animation in der Vorlage (außerhalb der Reduziert-Regeln)
const reduziert = mediaBlock(vorlage, '@media (prefers-reduced-motion: reduce)')
const ohneReduziert = vorlage.replace(reduziert, '')
const animiert = new Set([...ohneReduziert.matchAll(/\.hk-hochhaus (\.hk-[\w-]+)\{[^}]*animation:/g)].map((m) => m[1].slice(1)))
assert.ok(animiert.has('hk-fig-muetze-arbeiten') && animiert.has('hk-gr-muetze-arbeiten') && animiert.has('hk-dampf') && animiert.has('hk-bildschirm'))
// erfasst: Elemente mit hk-spr, oder eine Klasse aus den Reduziert-Regeln (Vorlage, hustler-ui, hustler-welt)
const reduzierteKlassen = new Set([...[reduziert, regel, mediaBlock(ui('hustler-welt.css'), '@media (prefers-reduced-motion: reduce)')].join('\n').matchAll(/\.hk-[\w-]+/g)].map((m) => m[0].slice(1)))
const zustaende = [
makeState({ statuses: { rita: 'arbeitet', pia: 'fehler', ali: 'wartet', hanna: 'leerlauf', otto: 'arbeitet' } }),
makeState({ statuses: { otto: 'wartet' }, connected: { kim: false, lea: false } }),
]
for (const state of zustaende) {
const lauf = await start({ state })
try {
const knoten = nodes(lauf.tree(), (n) => typeof n.tag === 'string' && classes(n).some((c) => animiert.has(c)))
assert.ok(knoten.length > 20)
for (const n of knoten) {
const erfasst = hasClass(n, 'hk-spr') || classes(n).some((c) => reduzierteKlassen.has(c))
assert.ok(erfasst, `nicht erfasst: ${classes(n).join(' ')}`)
}
} finally { lauf.end() }
}
})
test('K6 Weiß auf Rot: Briefkasten-Zähler und Fehler-Marker nutzen #c8372d (Kontrast zu Weiß mindestens 4,5:1; #e2483d reichte nicht)', () => {
assert.ok(kontrast('#e2483d', '#ffffff') < 4.5, 'Ausgangsfarbe war zu hell')
assert.ok(kontrast('#c8372d', '#ffffff') >= 4.5)
assert.ok(Math.abs(kontrast('#c8372d', '#ffffff') - 5.2) < 0.1)
const briefkasten = ohneKommentare(lies('hustler-welt.css')).match(/\.hk-hochhaus \.hk-briefkasten\s*\{([^}]*)\}/)[1]
const alarm = ohneKommentare(lies('hustler-ui.css')).match(/\.hk-seite \.hk-alarm\s*\{([^}]*)\}/)[1]
for (const [wo, regel] of [['Briefkasten', briefkasten], ['Fehler-Marker', alarm]]) {
const hg = regel.match(/background:\s*(#[0-9a-f]{6})/i)[1]; const vg = regel.match(/[^-]color:\s*(#[0-9a-f]{6})/i)[1]
assert.equal(hg.toLowerCase(), '#c8372d', wo)
assert.ok(kontrast(hg, vg) >= 4.5, `${wo}: ${kontrast(hg, vg).toFixed(2)}`)
}
})
test('R4 Überlagerungen: z-index der Detailansicht liegt unter Seitenleiste (20) und Abdunkler (15) der Handy-Navigation, aber über der Kopfzeile (5)', () => {
const global = readFileSync(path.join(ROOT, 'src/styles/global.css'), 'utf8')
const sidebar = Number(global.match(/\.sidebar \{[^}]*z-index:\s*(\d+)/)?.[1] ?? 20)
const scrim = Number(global.match(/\.shell\.menu-open \.scrim \{[^}]*z-index:\s*(\d+)/)[1])
assert.equal(scrim, 15); assert.equal(sidebar, 20)
const detail = ohneKommentare(lies('hustler-detail.css')).match(/\.hk-hochhaus \.hk-dialog-ueber\s*\{([^}]*)\}/)[1]
const z = Number(detail.match(/z-index:\s*(\d+)/)[1])
assert.ok(z <= 9 && z > 5, `z-index ${z}`)
const kopf = Number(ohneKommentare(lies('hustler-kopf.css')).match(/\.hk-hochhaus \.hk-kopf\s*\{[^}]*z-index:\s*(\d+)/)[1])
assert.ok(z > kopf)
})
test('R7 Hover in WOMS-Bausteinen: Links im WOMS-Dialog bleiben beim Überfahren in der WOMS-Akzentfarbe (nicht gelb wie .hk-hochhaus a:hover)', () => {
const ui = ohneKommentare(lies('hustler-ui.css'))
assert.match(ui, /\.hk-seite \.hk-woms a:hover\s*\{\s*color:\s*var\(--accent\);?\s*\}/)
assert.match(ohneKommentare(lies('hochhaus.css')), /\.hk-hochhaus a:hover\{color:#ffe9a0\}/)
// höhere Spezifität als die Vorlage: .hk-seite .hk-woms a:hover (0,3,1) gegen .hk-hochhaus a:hover (0,2,1)
})
// ---------- Aufbau ----------
test('Aufbau: kein CSS-Import, keine PNG-Importe, kein innerHTML; jede gerenderte Klasse steht in einer der Hustler-Stilregeln', async () => {
@@ -888,6 +1188,10 @@ test('Aufbau: kein CSS-Import, keine PNG-Importe, kein innerHTML; jede gerendert
for (const c of allClasses(ui.tree())) gesehen.add(c)
} finally { ui.end() }
}
// Detailansicht offen (gesperrt): die Welt trägt hk-verdeckt
const gesperrt = await start({ gesperrt: true })
try { for (const c of allClasses(gesperrt.tree())) gesehen.add(c) } finally { gesperrt.end() }
assert.ok(gesehen.has('hk-verdeckt'))
// Klassen, die nur im Lauf entstehen: Ziehen und Gleiten
gesehen.add('hk-zieht'); gesehen.add('hk-gleitet')
assert.ok(SEARCH_ROOMS.every((r) => gesehen.has(`hk-raum-grau-${r}`)))