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

+178 -3
View File
@@ -215,9 +215,10 @@ const STUBS = {
return hk.feed
}
`,
hochhaus: "import { jsx } from 'react'\nexport default function Hochhaus(props) { globalThis.__hk.props.hochhaus = props; return jsx('div', { 'data-screen': 'hochhaus' }) }",
pult: "import { jsx } from 'react'\nexport default function PultList(props) { globalThis.__hk.props.pult = props; return jsx('div', { 'data-screen': 'pult' }) }",
keller: "import { jsx } from 'react'\nexport default function BotLeadList(props) { globalThis.__hk.props.keller = props; return jsx('div', { 'data-screen': 'keller' }) }",
// renders: jede Darstellung eines Bildschirms (nicht nur die letzte), damit der Test auch Zwischenstände sieht (R5)
hochhaus: "import { jsx } from 'react'\nexport default function Hochhaus(props) { globalThis.__hk.props.hochhaus = props; globalThis.__hk.renders?.hochhaus.push(props); return jsx('div', { 'data-screen': 'hochhaus' }) }",
pult: "import { jsx } from 'react'\nexport default function PultList(props) { globalThis.__hk.props.pult = props; globalThis.__hk.renders?.pult.push(props); return jsx('div', { 'data-screen': 'pult' }) }",
keller: "import { jsx } from 'react'\nexport default function BotLeadList(props) { globalThis.__hk.props.keller = props; globalThis.__hk.renders?.keller.push(props); return jsx('div', { 'data-screen': 'keller' }) }",
}
const stubPlugin = {
name: 'seite-attrappen',
@@ -255,6 +256,7 @@ async function seite(query, { admin = false, feed } = {}) {
api: globalThis.__hkApi,
feed: feed ?? { state: null, error: null, loaded: false, refresh: () => {} },
feedCalls: [], feedMounts: 0, feedUnmounts: 0, props: {}, onEvents: null,
renders: { hochhaus: [], pult: [], keller: [] },
}
const savedHk = globalThis.__hk
const hadDocument = Object.hasOwn(globalThis, 'document')
@@ -454,3 +456,176 @@ test('Seite: ohne angemeldeten Nutzer bleibt me ohne Id und Name (kein Absturz)'
assert.deepEqual(ui.hk.props.pult.me, { id: undefined, name: undefined })
} finally { ui.end() }
})
// ---------- Korrekturen der Endprüfung (T13) ----------
test('R5 Seite: die Hustler-Tabelle liegt nie einen Render hinter dem Schnappschuss (kein „Leerlauf“-Blitz der Figuren, in keinem Bildschirm)', async () => {
const state = makeState({ statuses: { lea: 'wartet', kim: 'fehler' }, connected: { pia: false } })
const ui = await seite('tab=hochhaus', { feed: { state, error: null, loaded: true, refresh() {} } })
try {
// schon die allererste Darstellung mit Zustand hat die Tabelle daraus
const renders = ui.hk.renders.hochhaus
assert.ok(renders.length >= 1)
for (const p of renders) {
assert.equal(Object.keys(p.workers).length, 19)
assert.equal(p.workers.lea.status, 'wartet'); assert.equal(p.workers.kim.status, 'fehler'); assert.equal(p.workers.pia.status, 'grau')
}
// neuer Schnappschuss mitten im Lauf: jede weitere Darstellung zeigt schon die neue Lage (nie die alte Tabelle zum neuen Zustand)
renders.length = 0
const zweiter = makeState({ statuses: { lea: 'leerlauf' } })
ui.setFeed({ state: zweiter })
assert.ok(renders.length >= 1)
for (const p of renders) { assert.equal(p.feed.state, zweiter); assert.equal(p.workers.lea.status, 'leerlauf'); assert.equal(p.workers.kim.status, 'arbeitet') }
// dasselbe für Pult und Keller (sie bekommen dieselbe Tabelle)
ui.go('tab=freigabe')
ui.hk.renders.pult.length = 0
const dritter = makeState({ statuses: { lea: 'wartet' } })
ui.setFeed({ state: dritter })
for (const p of ui.hk.renders.pult) assert.equal(p.workers.lea.status, 'wartet')
ui.go('tab=botleads')
ui.hk.renders.keller.length = 0
ui.setFeed({ state: makeState({ statuses: { lea: 'fehler' } }) })
for (const p of ui.hk.renders.keller) assert.equal(p.workers.lea.status, 'fehler')
assert.ok(ui.hk.renders.keller.length >= 1)
// Ereignisse wirken weiter auf die Tabelle und überleben das Neu-Rendern desselben Schnappschusses
ui.hk.onEvents([ev('leerlauf', 'lea', { text: 'Pause' })])
assert.equal(ui.hk.props.keller.workers.lea.status, 'leerlauf')
ui.setFeed({})
assert.equal(ui.hk.props.keller.workers.lea.status, 'leerlauf')
} finally { ui.end() }
// Erster Schnappschuss kommt erst später: vorher leer, danach nie eine Darstellung mit Zustand und leerer Tabelle
const spaet = await seite('tab=hochhaus')
try {
assert.deepEqual(spaet.hk.props.hochhaus.workers, {})
spaet.hk.renders.hochhaus.length = 0
spaet.setFeed({ state: makeState({ statuses: { lea: 'wartet' } }), loaded: true })
for (const p of spaet.hk.renders.hochhaus) { assert.ok(p.feed.state); assert.equal(Object.keys(p.workers).length, 19) }
assert.ok(spaet.hk.renders.hochhaus.length >= 1)
} finally { spaet.end() }
})
test('R3 Seite: Pult und Keller bekommen paused aus state.paused (nur ein echtes true), Pause schaltet mit dem Zustand um', async () => {
const ui = await seite('tab=freigabe', { feed: { state: makeState({ paused: false }), error: null, loaded: true, refresh() {} } })
try {
assert.equal(ui.hk.props.pult.paused, false)
ui.setFeed({ state: makeState({ paused: true }) })
assert.equal(ui.hk.props.pult.paused, true)
ui.go('tab=botleads')
assert.equal(ui.hk.props.keller.paused, true)
ui.setFeed({ state: makeState({ paused: false }) })
assert.equal(ui.hk.props.keller.paused, false)
ui.setFeed({ state: { ...makeState(), paused: 'ja' } })
assert.equal(ui.hk.props.keller.paused, false)
// vor dem ersten Schnappschuss: nicht angehalten
ui.setFeed({ state: null, loaded: false })
assert.equal(ui.hk.props.keller.paused, false)
} finally { ui.end() }
})
test('M2 Seite: das Hochhaus bekommt vonTab (der Bildschirm, von dem man zurückkommt); beim ersten Besuch null', async () => {
const ui = await seite('tab=hochhaus', { feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
try {
assert.equal(ui.hk.props.hochhaus.vonTab, null)
ui.go('tab=freigabe'); assert.deepEqual(ui.screen(), ['pult'])
ui.go('tab=hochhaus'); assert.equal(ui.hk.props.hochhaus.vonTab, 'freigabe')
// schon der erste Render nach dem Wechsel kennt vonTab (das Hochhaus fokussiert beim Einhängen)
assert.ok(ui.hk.renders.hochhaus.every((p, i) => i === 0 || p.vonTab === null || p.vonTab === 'freigabe'))
ui.go('tab=botleads'); assert.deepEqual(ui.screen(), ['keller'])
ui.go('tab=hochhaus'); assert.equal(ui.hk.props.hochhaus.vonTab, 'botleads')
// Neu-Rendern im Hochhaus ändert nichts
ui.setFeed({}); assert.equal(ui.hk.props.hochhaus.vonTab, 'botleads')
} finally { ui.end() }
// direkt im Pult gestartet, dann zurück
const direkt = await seite('tab=freigabe', { feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
try {
direkt.go('tab=hochhaus')
assert.equal(direkt.hk.props.hochhaus.vonTab, 'freigabe')
} finally { direkt.end() }
})
// Hochhaus mit Wurzel-Attrappe (querySelector, focus) für den Rückweg und die Überschrift
function wurzelAttrappe() {
const gefragt = []
const etagen = new Map()
const wurzel = {
querySelector(sel) {
gefragt.push(sel)
const m = /^\[data-hk-etage="([\w-]+)"\]$/.exec(sel)
if (!m) return null
if (!etagen.has(m[1])) etagen.set(m[1], { id: m[1], focusCalls: [], focus(o) { this.focusCalls.push(o) } })
return etagen.get(m[1])
},
}
const gehabt = Object.hasOwn(globalThis, '__fakeRef')
const alt = globalThis.__fakeRef
globalThis.__fakeRef = (tag, props) => (tag === 'div' && props.className === 'hk-hochhaus' ? wurzel : null)
return { gefragt, etagen, restore() { if (gehabt) globalThis.__fakeRef = alt; else delete globalThis.__fakeRef } }
}
test('M2 Hochhaus: nach dem Rückweg aus dem Pult fokussiert die Etage „freigabe“, aus dem Keller die Etage „keller“ (ohne Scrollen), erst wenn die Welt steht; sonst nichts', async () => {
const ready = { state: makeState(), error: null, loaded: true, refresh() {} }
for (const [von, etage] of [['freigabe', 'freigabe'], ['botleads', 'keller']]) {
const w = wurzelAttrappe()
const view = await hochhaus(ready, { vonTab: von })
try {
assert.deepEqual(w.gefragt, [`[data-hk-etage="${etage}"]`])
assert.deepEqual(w.etagen.get(etage).focusCalls, [{ preventScroll: true }])
// Neu-Rendern (Feed-Aktualisierung) holt den Fokus nicht noch einmal
view.setFeed({ state: makeState() })
assert.equal(w.etagen.get(etage).focusCalls.length, 1)
} finally { view.end(); w.restore() }
}
// kein vonTab / unbekannter Wert: kein Fokus
for (const von of [null, undefined, 'hochhaus', 'toString', '__proto__']) {
const w = wurzelAttrappe()
const view = await hochhaus(ready, { vonTab: von })
try { assert.deepEqual(w.gefragt, [], String(von)) } finally { view.end(); w.restore() }
}
// noch nicht geladen: erst wenn der Zustand da ist
const w = wurzelAttrappe()
const view = await hochhaus({ state: null, error: null, loaded: false, refresh() {} }, { vonTab: 'freigabe' })
try {
assert.deepEqual(w.gefragt, [])
view.setFeed({ state: makeState(), loaded: true })
assert.deepEqual(w.etagen.get('freigabe').focusCalls, [{ preventScroll: true }])
} finally { view.end(); w.restore() }
})
test('M2/M7 Hochhaus: Überschrift „Hustler – Hochhaus“ (h1, nur für Screenreader) in jedem Zustand; die Kopfzeile steht im DOM vor der Welt', async () => {
const ready = { state: makeState(), error: null, loaded: true, refresh() {} }
const faelle = [
['geladen', ready],
['lädt', { state: null, error: null, loaded: false, refresh() {} }],
['Dienst nicht erreichbar', { state: null, error: 'weg', loaded: false, refresh() {} }],
]
for (const [wo, feed] of faelle) {
const view = await hochhaus(feed)
try {
const h1 = nodes(view.tree(), (n) => n.tag === 'h1')
assert.equal(h1.length, 1, wo)
assert.equal(textOf(h1[0]), 'Hustler – Hochhaus', wo); assert.equal(h1[0].props.className, 'hk-nur-sr', wo)
assert.equal(view.tree().children[0], h1[0], `${wo}: die Überschrift steht zuerst`)
} finally { view.end() }
}
const view = await hochhaus(ready)
try {
const reihenfolge = view.tree().children.filter((k) => k && k.props && k.props['data-teil']).map((k) => k.props['data-teil'])
assert.deepEqual(reihenfolge, ['kopf', 'welt'], 'Tab-Reihenfolge: Kopfzeile vor der Welt')
view.hh.welt.onZeige('lea')
await settle()
assert.deepEqual(view.tree().children.filter((k) => k && k.props && k.props['data-teil']).map((k) => k.props['data-teil']), ['kopf', 'welt', 'dialog'])
} finally { view.end() }
})
test('R3 Hochhaus: paused des Dienstes geht an die Detailansicht (nur ein echtes true)', async () => {
const view = await hochhaus({ state: makeState({ paused: false }), error: null, loaded: true, refresh() {} })
try {
view.hh.welt.onZeige('lea')
await settle()
assert.equal(view.hh.dialog.paused, false)
view.setFeed({ state: makeState({ paused: true }) })
assert.equal(view.hh.dialog.paused, true)
view.setFeed({ state: { ...makeState(), paused: 'ja' } })
assert.equal(view.hh.dialog.paused, false)
} finally { view.end() }
})