Hustler-Pixelwelt: Hochhaus zusammengesteckt (Welt mit Regie, Kopfzeile, Detailansicht), CSS-Importe, forwardRef im Mini-Läufer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-22 08:23:49 +00:00
1 parent 4c5074f4f6
commit b4e67a43a2
7 files changed
+135 -27

No files matched your search

+81 -11
View File
@@ -7,7 +7,7 @@ import { fakeReact } from './helpers/fakeReact.mjs'
import { ev, makeState } from './helpers/workersFixtures.mjs'
// Das Gerüst der Hustler-Pixelwelt (T3) läuft wirklich: im Mini-Läufer anstelle von React (Muster botleadList.test.mjs, Baustein helpers/fakeReact.mjs).
// 1. Hochhaus.jsx: Laden, Dienst nicht erreichbar („Neu laden“), geladen -> Rahmen der Welt.
// 1. Hochhaus.jsx: Laden, Dienst nicht erreichbar („Neu laden“), geladen -> Welt, Kopfzeile, Detailansicht auf Klick.
// 2. WorkersPage.jsx: mit esbuild-Attrappen für Router, Anmeldung, Dienst-Client, Feed-Hook, CSS und die drei Bildschirme. Geprüft wird,
// welcher Bildschirm zu ?tab gehört, welche Props er bekommt, dass der Feed einmal pro Seite läuft (kein Neustart beim Bildschirmwechsel),
// dass Ereignisse die Hustler-Tabelle fortschreiben und an die Regie gehen, und dass ein Schnappschuss die Tabelle neu setzt.
@@ -27,20 +27,49 @@ const byClass = (tree, name) => nodes(tree, (n) => typeof n.tag === 'string' &&
// ---------- Hochhaus ----------
// Die vier Teile des Hochhauses (Welt T5, Kopfzeile T6, Detailansicht T7, Regie T8) haben eigene Tests; hier sind sie Attrappen,
// geprüft wird das Zusammenstecken (Props, Auswahl, Sperre der Kamera, Regie-Handle, „Ganzes Haus zeigen“).
const TEILE = {
welt: `import { jsx, forwardRef, useImperativeHandle } from 'react'
export default forwardRef(function HochhausWelt(props, ref) {
useImperativeHandle(ref, () => ({ ganz: () => { globalThis.__hh.ganz += 1 } }))
globalThis.__hh.welt = props
return jsx('div', { 'data-teil': 'welt', children: props.children })
})`,
kopf: "import { jsx } from 'react'\nexport default function HochhausKopf(props) { globalThis.__hh.kopf = props; return jsx('div', { 'data-teil': 'kopf' }) }",
dialog: "import { jsx } from 'react'\nexport default function HustlerDialog(props) { globalThis.__hh.dialog = props; return jsx('div', { 'data-teil': 'dialog' }) }",
regie: `import { jsx, forwardRef, useImperativeHandle } from 'react'
export default forwardRef(function RegieEbene(props, ref) {
useImperativeHandle(ref, () => ({ push: (events) => { globalThis.__hh.pushed.push(events) }, flush() {} }))
return jsx('div', { 'data-teil': 'regie' })
})`,
}
const teilePlugin = {
name: 'hochhaus-teile',
setup(build) {
for (const [filter, key] of [[/^\.\/HochhausWelt$/, 'welt'], [/^\.\/HochhausKopf$/, 'kopf'], [/^\.\/HustlerDialog$/, 'dialog'], [/^\.\/RegieEbene$/, 'regie']]) {
build.onResolve({ filter }, () => ({ path: key, namespace: 'hochhaus-teil' }))
}
build.onLoad({ filter: /.*/, namespace: 'hochhaus-teil' }, (args) => ({ contents: TEILE[args.path], loader: 'js' }))
},
}
let hochhausBundle
const loadHochhaus = () => (hochhausBundle ??= esbuild.build({
stdin: { contents: "export { __mount, jsx } from 'react'\nexport { default as Hochhaus } from './src/components/workers/hochhaus/Hochhaus.jsx'", resolveDir: ROOT, loader: 'js' },
bundle: true, format: 'esm', platform: 'neutral', write: false, jsx: 'automatic', loader: { '.jsx': 'jsx' }, plugins: [fakeReact],
bundle: true, format: 'esm', platform: 'neutral', write: false, jsx: 'automatic', loader: { '.jsx': 'jsx' }, plugins: [fakeReact, teilePlugin],
}).then(importBundle))
async function hochhaus(feed, extra = {}) {
const { __mount, jsx, Hochhaus } = await loadHochhaus()
const props = { api: {}, isAdmin: false, workers: {}, regieRef: { current: null }, onOpen() {}, ...extra, feed }
globalThis.__hh = { ganz: 0, pushed: [], welt: null, kopf: null, dialog: null }
const props = { api: { name: 'api' }, isAdmin: false, workers: {}, regieRef: { current: null }, onOpen() {}, ...extra, feed }
const mounted = __mount(jsx(() => jsx(Hochhaus, props), {}))
await settle()
return { tree: () => mounted.tree(), end: () => mounted.unmount() }
return { props, hh: globalThis.__hh, tree: () => mounted.tree(), end: () => mounted.unmount() }
}
const counting = (n = { refresh: 0 }) => ({ n, refresh: () => { n.refresh += 1 } })
const teil = (tree, name) => nodes(tree, (n) => n.props && n.props['data-teil'] === name)
test('Hochhaus: solange nichts geladen ist, steht „Wird geladen …“ mit role=status in der Wurzel hk-hochhaus', async () => {
const c = counting()
@@ -50,7 +79,7 @@ test('Hochhaus: solange nichts geladen ist, steht „Wird geladen …“ mit rol
assert.equal(root.tag, 'div'); assert.equal(root.props.className, 'hk-hochhaus')
const laden = byClass(root, 'hk-laden')
assert.equal(laden.length, 1); assert.equal(laden[0].props.role, 'status'); assert.equal(textOf(laden[0]), 'Wird geladen …')
assert.equal(byClass(root, 'hk-welt-rahmen').length, 0); assert.equal(byClass(root, 'hk-leer').length, 0)
assert.equal(teil(root, 'welt').length, 0); assert.equal(byClass(root, 'hk-leer').length, 0)
assert.equal(c.n.refresh, 0)
} finally { view.end() }
})
@@ -70,7 +99,7 @@ test('Hochhaus: Dienst nie erreicht -> Pixel-Leerzustand mit Titel, Fehlertext a
const knoepfe = nodes(root, (n) => n.tag === 'button')
assert.equal(knoepfe.length, 1)
assert.equal(textOf(knoepfe[0]), 'Neu laden'); assert.equal(knoepfe[0].props.type, 'button'); assert.ok(hasClass(knoepfe[0], 'hk-knopf'))
assert.equal(byClass(root, 'hk-laden').length, 0); assert.equal(byClass(root, 'hk-welt-rahmen').length, 0)
assert.equal(byClass(root, 'hk-laden').length, 0); assert.equal(teil(root, 'welt').length, 0)
knoepfe[0].props.onClick({ preventDefault() {} })
assert.equal(c.n.refresh, 1)
knoepfe[0].props.onClick({ preventDefault() {} })
@@ -78,28 +107,69 @@ test('Hochhaus: Dienst nie erreicht -> Pixel-Leerzustand mit Titel, Fehlertext a
} finally { view.end() }
})
test('Hochhaus: geladen -> leerer Rahmen hk-welt-rahmen; ein späterer Verbindungsfehler ersetzt die Welt nicht', async () => {
test('Hochhaus: geladen -> Welt mit Regie-Ebene darin, Kopfzeile, noch keine Detailansicht; ein späterer Verbindungsfehler ersetzt die Welt nicht', async () => {
const c = counting()
const state = makeState()
const view = await hochhaus({ state, error: null, loaded: true, refresh: c.refresh })
const workers = { lea: { status: 'arbeitet', task: 'x', lastTs: null, connected: true, counters: {} } }
const onOpen = () => {}
const view = await hochhaus({ state, error: null, loaded: true, refresh: c.refresh }, { isAdmin: true, workers, onOpen })
try {
const root = view.tree()
assert.equal(root.props.className, 'hk-hochhaus')
assert.equal(byClass(root, 'hk-welt-rahmen').length, 1)
const welt = teil(root, 'welt')
assert.equal(welt.length, 1); assert.equal(teil(welt[0], 'regie').length, 1) // Regie liegt in der Bühne der Welt
assert.equal(teil(root, 'kopf').length, 1); assert.equal(teil(root, 'dialog').length, 0)
assert.equal(byClass(root, 'hk-laden').length, 0); assert.equal(byClass(root, 'hk-leer').length, 0)
const w = view.hh.welt
assert.equal(w.state, state); assert.equal(w.workers, workers); assert.equal(w.onOpen, onOpen); assert.equal(w.gesperrt, false)
const k = view.hh.kopf
assert.equal(k.api, view.props.api); assert.equal(k.isAdmin, true); assert.equal(k.state, state); assert.equal(k.error, null); assert.equal(k.refresh, c.refresh)
} finally { view.end() }
const gestoert = await hochhaus({ state, error: 'Verbindung weg', loaded: true, refresh: c.refresh })
try {
assert.equal(byClass(gestoert.tree(), 'hk-welt-rahmen').length, 1)
assert.equal(teil(gestoert.tree(), 'welt').length, 1)
assert.equal(byClass(gestoert.tree(), 'hk-leer').length, 0)
assert.equal(gestoert.hh.kopf.error, 'Verbindung weg')
} finally { gestoert.end() }
})
test('Hochhaus: das Regie-Handle landet in regieRef der Seite, Ereignisse kommen bei der Ebene an; nach dem Abbau ist es weg', async () => {
const regieRef = { current: null }
const view = await hochhaus({ state: makeState(), error: null, loaded: true, refresh() {} }, { regieRef })
assert.equal(typeof regieRef.current?.push, 'function')
const events = [ev('fund', 'lea')]
regieRef.current.push(events)
assert.deepEqual(view.hh.pushed, [events])
view.end()
await settle()
assert.equal(regieRef.current, null)
})
test('Hochhaus: Figur-Klick öffnet die Detailansicht und sperrt die Kamera, Blättern wechselt, Schließen gibt frei; „Ganzes Haus zeigen“ ruft die Welt', async () => {
const workers = { lea: { status: 'arbeitet', task: 'x', lastTs: null, connected: true, counters: {} } }
const view = await hochhaus({ state: makeState(), error: null, loaded: true, refresh() {} }, { workers })
try {
view.hh.welt.onZeige('lea')
await settle()
assert.equal(teil(view.tree(), 'dialog').length, 1)
assert.equal(view.hh.dialog.workerId, 'lea'); assert.equal(view.hh.dialog.workers, workers); assert.equal(view.hh.dialog.api, view.props.api)
assert.equal(view.hh.welt.gesperrt, true)
view.hh.dialog.onWechsel('bea')
await settle()
assert.equal(view.hh.dialog.workerId, 'bea')
view.hh.dialog.onClose()
await settle()
assert.equal(teil(view.tree(), 'dialog').length, 0); assert.equal(view.hh.welt.gesperrt, false)
view.hh.kopf.onGanz()
assert.equal(view.hh.ganz, 1)
} finally { view.end() }
})
test('Hochhaus: ohne Zustand gilt auch ein geladen-Flag nicht als bereit (wie bisher im Board)', async () => {
const c = counting()
const view = await hochhaus({ state: null, error: 'weg', loaded: true, refresh: c.refresh })
try {
assert.equal(byClass(view.tree(), 'hk-welt-rahmen').length, 0)
assert.equal(teil(view.tree(), 'welt').length, 0)
assert.equal(byClass(view.tree(), 'hk-leer').length, 1)
} finally { view.end() }
})