- 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>
457 lines
25 KiB
JavaScript
457 lines
25 KiB
JavaScript
import test from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import path from 'node:path'
|
||
import { fileURLToPath } from 'node:url'
|
||
import esbuild from 'esbuild'
|
||
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 -> 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.
|
||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
|
||
const importBundle = (out) => import(`data:text/javascript;base64,${Buffer.from(out.outputFiles[0].text).toString('base64')}`)
|
||
const settle = () => new Promise((resolve) => setImmediate(resolve))
|
||
|
||
const nodes = (node, match, out = []) => {
|
||
if (node === null || typeof node === 'string') return out
|
||
if (match(node)) out.push(node)
|
||
for (const child of node.children) nodes(child, match, out)
|
||
return out
|
||
}
|
||
const textOf = (node) => (typeof node === 'string' ? node : node === null ? '' : node.children.map(textOf).join(''))
|
||
const hasClass = (n, name) => String(n.props.className ?? '').split(' ').includes(name)
|
||
const byClass = (tree, name) => nodes(tree, (n) => typeof n.tag === 'string' && hasClass(n, name))
|
||
|
||
// ---------- 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, teilePlugin],
|
||
}).then(importBundle))
|
||
|
||
async function hochhaus(feed, extra = {}) {
|
||
const { __mount, jsx, Hochhaus } = await loadHochhaus()
|
||
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()
|
||
// setFeed: der Feed ändert sich zwischendurch (wie der Hook der Seite), das Hochhaus bleibt am selben Einhängepunkt
|
||
return { props, hh: globalThis.__hh, tree: () => mounted.tree(), setFeed(next) { props.feed = { ...props.feed, ...next }; mounted.rerender() }, 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()
|
||
const view = await hochhaus({ state: null, error: null, loaded: false, refresh: c.refresh })
|
||
try {
|
||
const root = view.tree()
|
||
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(teil(root, 'welt').length, 0); assert.equal(byClass(root, 'hk-leer').length, 0)
|
||
assert.equal(c.n.refresh, 0)
|
||
} finally { view.end() }
|
||
})
|
||
|
||
test('Hochhaus: Dienst nie erreicht -> Pixel-Leerzustand mit Titel, Fehlertext als Text und „Neu laden“, das refresh ruft', async () => {
|
||
const c = counting()
|
||
const view = await hochhaus({ state: null, error: '<b>Der Dienst antwortet nicht.</b>', loaded: false, refresh: c.refresh })
|
||
try {
|
||
const root = view.tree()
|
||
assert.equal(root.props.className, 'hk-hochhaus')
|
||
const leer = byClass(root, 'hk-leer')
|
||
assert.equal(leer.length, 1)
|
||
assert.equal(textOf(byClass(leer[0], 'hk-leer-titel')[0]), 'Der Hustler-Dienst ist noch nicht erreichbar')
|
||
// der Fehlertext ist ein Textknoten (kein HTML)
|
||
const text = byClass(leer[0], 'hk-leer-text')[0]
|
||
assert.deepEqual(text.children, ['<b>Der Dienst antwortet nicht.</b>'])
|
||
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(teil(root, 'welt').length, 0)
|
||
knoepfe[0].props.onClick({ preventDefault() {} })
|
||
assert.equal(c.n.refresh, 1)
|
||
knoepfe[0].props.onClick({ preventDefault() {} })
|
||
assert.equal(c.n.refresh, 2)
|
||
} finally { view.end() }
|
||
})
|
||
|
||
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 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')
|
||
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(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(teil(view.tree(), 'welt').length, 0)
|
||
assert.equal(byClass(view.tree(), 'hk-leer').length, 1)
|
||
} finally { view.end() }
|
||
})
|
||
|
||
// Frühere Prüfung des Boards (Hooks vor der frühen Rückgabe): Laden, Leerzustand und Welt wechseln am selben Einhängepunkt, die Auswahl überlebt Verbindungsfehler.
|
||
test('Hochhaus: Laden -> Dienst nicht erreicht -> bereit -> Verbindungsfehler -> wieder gut, ohne Neueinhängen; die offene Detailansicht überlebt den Fehler', async () => {
|
||
const c = counting()
|
||
const view = await hochhaus({ state: null, error: null, loaded: false, refresh: c.refresh })
|
||
try {
|
||
assert.equal(byClass(view.tree(), 'hk-laden').length, 1)
|
||
view.setFeed({ error: 'weg' })
|
||
assert.equal(byClass(view.tree(), 'hk-leer').length, 1); assert.equal(byClass(view.tree(), 'hk-laden').length, 0)
|
||
view.setFeed({ state: makeState(), error: null, loaded: true })
|
||
assert.equal(teil(view.tree(), 'welt').length, 1); assert.equal(byClass(view.tree(), 'hk-leer').length, 0)
|
||
view.hh.welt.onZeige('lea')
|
||
await settle()
|
||
assert.equal(teil(view.tree(), 'dialog').length, 1)
|
||
view.setFeed({ error: 'Verbindung weg' })
|
||
assert.equal(teil(view.tree(), 'welt').length, 1); assert.equal(teil(view.tree(), 'dialog').length, 1)
|
||
assert.equal(view.hh.dialog.workerId, 'lea'); assert.equal(view.hh.welt.gesperrt, true)
|
||
view.setFeed({ error: null })
|
||
assert.equal(teil(view.tree(), 'welt').length, 1); assert.equal(view.hh.dialog.workerId, 'lea')
|
||
assert.equal(c.n.refresh, 0)
|
||
} finally { view.end() }
|
||
})
|
||
|
||
// ---------- WorkersPage ----------
|
||
|
||
// Attrappen: alle Zustände und Aufrufe laufen über globalThis.__hk, das der Test je Lauf anlegt
|
||
const STUBS = {
|
||
'react-router-dom': "export function useSearchParams() { return [globalThis.__hk.params, globalThis.__hk.setParams] }",
|
||
auth: "export function useAuth() { return globalThis.__hk.auth }",
|
||
// die api-Attrappe entsteht beim Laden des Moduls (WorkersPage liest sie auf oberster Ebene) und wird für den Test unter __hkApi abgelegt
|
||
client: "const api = { name: 'workers-api-attrappe' }\nglobalThis.__hkApi = api\nexport const workersApi = api",
|
||
feed: `
|
||
import { useEffect } from 'react'
|
||
export function useWorkersFeed(api, options) {
|
||
const hk = globalThis.__hk
|
||
hk.feedCalls.push({ api, options })
|
||
hk.onEvents = options.onEvents
|
||
useEffect(() => { hk.feedMounts += 1; return () => { hk.feedUnmounts += 1 } }, [])
|
||
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' }) }",
|
||
}
|
||
const stubPlugin = {
|
||
name: 'seite-attrappen',
|
||
setup(build) {
|
||
const table = [
|
||
[/^react-router-dom$/, 'react-router-dom'],
|
||
[/^\.\.\/context\/AuthContext$/, 'auth'],
|
||
[/^\.\.\/lib\/workersClient$/, 'client'],
|
||
[/^\.\.\/hooks\/useWorkersFeed$/, 'feed'],
|
||
[/^\.\.\/components\/workers\/hochhaus\/Hochhaus$/, 'hochhaus'],
|
||
[/^\.\.\/components\/workers\/PultList$/, 'pult'],
|
||
[/^\.\.\/components\/workers\/BotLeadList$/, 'keller'],
|
||
]
|
||
for (const [filter, key] of table) {
|
||
build.onResolve({ filter }, () => ({ path: key, namespace: 'seite-attrappe' }))
|
||
}
|
||
build.onLoad({ filter: /.*/, namespace: 'seite-attrappe' }, (args) => ({ contents: STUBS[args.path], loader: 'js' }))
|
||
},
|
||
}
|
||
|
||
let seiteBundle
|
||
const loadSeite = () => (seiteBundle ??= esbuild.build({
|
||
stdin: { contents: "export { __mount, jsx } from 'react'\nexport { default as WorkersPage } from './src/pages/WorkersPage.jsx'", resolveDir: ROOT, loader: 'js' },
|
||
bundle: true, format: 'esm', platform: 'neutral', write: false, jsx: 'automatic', loader: { '.jsx': 'jsx', '.css': 'empty' }, plugins: [fakeReact, stubPlugin],
|
||
}).then(importBundle))
|
||
|
||
// Ein Lauf der Seite. query ist der Teil nach dem ?, feed wird vom Test zwischendurch geändert (setFeed) und die Seite neu gerendert.
|
||
async function seite(query, { admin = false, feed } = {}) {
|
||
const { __mount, jsx, WorkersPage } = await loadSeite()
|
||
const setParamsCalls = []
|
||
const hk = {
|
||
params: new URLSearchParams(query),
|
||
setParams: (next) => { setParamsCalls.push(next) },
|
||
auth: { isAdmin: admin, user: { $id: 'u-1', name: 'Kenso' } },
|
||
api: globalThis.__hkApi,
|
||
feed: feed ?? { state: null, error: null, loaded: false, refresh: () => {} },
|
||
feedCalls: [], feedMounts: 0, feedUnmounts: 0, props: {}, onEvents: null,
|
||
}
|
||
const savedHk = globalThis.__hk
|
||
const hadDocument = Object.hasOwn(globalThis, 'document')
|
||
const savedDocument = globalThis.document
|
||
globalThis.__hk = hk
|
||
globalThis.document = { title: 'Vorher' }
|
||
const mounted = __mount(jsx(WorkersPage, {}))
|
||
await settle()
|
||
const view = {
|
||
hk, setParamsCalls,
|
||
tree: () => mounted.tree(),
|
||
root: () => mounted.tree(),
|
||
screen: () => nodes(mounted.tree(), (n) => n.props && n.props['data-screen']).map((n) => n.props['data-screen']),
|
||
// Adresse wechseln, wie es der Router nach setParams täte, und neu rendern
|
||
go(next) { hk.params = new URLSearchParams(next); mounted.rerender() },
|
||
setFeed(next) { hk.feed = { ...hk.feed, ...next }; mounted.rerender() },
|
||
end() {
|
||
mounted.unmount()
|
||
if (savedHk === undefined) delete globalThis.__hk; else globalThis.__hk = savedHk
|
||
if (hadDocument) globalThis.document = savedDocument; else delete globalThis.document
|
||
},
|
||
}
|
||
return view
|
||
}
|
||
|
||
test('Seite: ohne ?tab, mit ?tab=hochhaus und mit unbekanntem Tab erscheint das Hochhaus; Wurzel hk-seite trägt data-tab', async () => {
|
||
for (const query of ['', 'tab=hochhaus', 'tab=gibtsnicht', 'tab=', 'bot=x']) {
|
||
const ui = await seite(query)
|
||
try {
|
||
const root = ui.root()
|
||
assert.equal(root.tag, 'div'); assert.equal(root.props.className, 'hk-seite'); assert.equal(root.props['data-tab'], 'hochhaus', query)
|
||
assert.deepEqual(ui.screen(), ['hochhaus'], query)
|
||
} finally { ui.end() }
|
||
}
|
||
})
|
||
|
||
test('Seite: ?tab=freigabe zeigt das Pult, ?tab=botleads den Keller, beide direkt in .hk-seite (keine alte helle Hülle mehr); data-tab folgt', async () => {
|
||
const pult = await seite('tab=freigabe')
|
||
try {
|
||
assert.equal(pult.root().props['data-tab'], 'freigabe'); assert.deepEqual(pult.screen(), ['pult'])
|
||
assert.equal(byClass(pult.root(), 'page').length, 0); assert.equal(byClass(pult.root(), 'wk-page').length, 0)
|
||
} finally { pult.end() }
|
||
const keller = await seite('tab=botleads')
|
||
try {
|
||
assert.equal(keller.root().props['data-tab'], 'botleads'); assert.deepEqual(keller.screen(), ['keller'])
|
||
assert.equal(byClass(keller.root(), 'page').length, 0); assert.equal(byClass(keller.root(), 'wk-page').length, 0)
|
||
} finally { keller.end() }
|
||
})
|
||
|
||
test('Seite: keine Seitenüberschrift, keine Reiterleiste', async () => {
|
||
for (const query of ['', 'tab=freigabe', 'tab=botleads']) {
|
||
const ui = await seite(query)
|
||
try {
|
||
assert.equal(nodes(ui.root(), (n) => n.tag === 'h1').length, 0)
|
||
assert.equal(nodes(ui.root(), (n) => n.props && (n.props.role === 'tablist' || n.props.role === 'tab')).length, 0)
|
||
assert.ok(!textOf(ui.root()).includes('Nachtschicht'))
|
||
} finally { ui.end() }
|
||
}
|
||
})
|
||
|
||
test('Seite: Hochhaus bekommt api, isAdmin, Feed-Werte, Hustler-Tabelle, regieRef; onOpen wechselt per setParams({ tab })', async () => {
|
||
const state = makeState({ statuses: { lea: 'wartet' } })
|
||
const refresh = () => {}
|
||
const feed = { state, error: null, loaded: true, refresh }
|
||
for (const admin of [false, true]) {
|
||
const ui = await seite('tab=hochhaus&bot=zurueckgeblieben', { admin, feed })
|
||
try {
|
||
const p = ui.hk.props.hochhaus
|
||
assert.equal(p.api, ui.hk.api); assert.equal(p.isAdmin, admin)
|
||
assert.deepEqual(Object.keys(p.feed).sort(), ['error', 'loaded', 'refresh', 'state'])
|
||
assert.equal(p.feed.state, state); assert.equal(p.feed.loaded, true); assert.equal(p.feed.error, null); assert.equal(p.feed.refresh, refresh)
|
||
assert.equal(Object.keys(p.workers).length, 19); assert.equal(p.workers.lea.status, 'wartet')
|
||
assert.deepEqual(Object.keys(p.regieRef), ['current']); assert.equal(p.regieRef.current, null)
|
||
p.onOpen('freigabe'); p.onOpen('botleads')
|
||
// setParams ersetzt alle Parameter: ein Wechsel nimmt bot nicht mit
|
||
assert.deepEqual(ui.setParamsCalls, [{ tab: 'freigabe' }, { tab: 'botleads' }])
|
||
} finally { ui.end() }
|
||
}
|
||
})
|
||
|
||
test('Seite: Pult bekommt api, me (Id und Name), Kennzahlen, Hustler-Tabelle und onBack -> Hochhaus', async () => {
|
||
const state = makeState({ kpis: { pultOpen: 4 } })
|
||
const ui = await seite('tab=freigabe', { feed: { state, error: null, loaded: true, refresh() {} } })
|
||
try {
|
||
const p = ui.hk.props.pult
|
||
assert.equal(p.api, ui.hk.api); assert.deepEqual(p.me, { id: 'u-1', name: 'Kenso' })
|
||
assert.equal(p.kpis, state.kpis); assert.equal(p.kpis.pultOpen, 4)
|
||
assert.equal(Object.keys(p.workers).length, 19)
|
||
p.onBack()
|
||
assert.deepEqual(ui.setParamsCalls, [{ tab: 'hochhaus' }])
|
||
} finally { ui.end() }
|
||
})
|
||
|
||
test('Seite: Keller bekommt api, bot aus der Adresse, Kennzahlen, Hustler-Tabelle; onClearBot behält den Reiter, onBack geht ins Hochhaus', async () => {
|
||
const state = makeState()
|
||
const ui = await seite('tab=botleads&bot=abc123', { feed: { state, error: null, loaded: true, refresh() {} } })
|
||
try {
|
||
const p = ui.hk.props.keller
|
||
assert.equal(p.api, ui.hk.api); assert.equal(p.bot, 'abc123'); assert.equal(p.kpis, state.kpis); assert.equal(Object.keys(p.workers).length, 19)
|
||
p.onClearBot(); p.onBack()
|
||
assert.deepEqual(ui.setParamsCalls, [{ tab: 'botleads' }, { tab: 'hochhaus' }])
|
||
} finally { ui.end() }
|
||
const ohne = await seite('tab=botleads')
|
||
try { assert.equal(ohne.hk.props.keller.bot, '') } finally { ohne.end() }
|
||
})
|
||
|
||
test('Seite: vor dem ersten Schnappschuss gibt es keine Kennzahlen und eine leere Hustler-Tabelle', async () => {
|
||
const ui = await seite('tab=freigabe')
|
||
try {
|
||
assert.equal(ui.hk.props.pult.kpis, undefined); assert.deepEqual(ui.hk.props.pult.workers, {})
|
||
} finally { ui.end() }
|
||
})
|
||
|
||
test('Seite: der Feed läuft genau einmal pro Seite, mit der api der Seite, und startet beim Bildschirmwechsel nicht neu', async () => {
|
||
const state = makeState()
|
||
const ui = await seite('tab=hochhaus', { feed: { state, error: null, loaded: true, refresh() {} } })
|
||
try {
|
||
assert.equal(ui.hk.feedMounts, 1)
|
||
assert.equal(ui.hk.feedCalls[0].api, ui.hk.api); assert.equal(typeof ui.hk.feedCalls[0].options.onEvents, 'function')
|
||
ui.go('tab=freigabe'); assert.deepEqual(ui.screen(), ['pult'])
|
||
ui.go('tab=botleads&bot=x'); assert.deepEqual(ui.screen(), ['keller'])
|
||
ui.go('tab=hochhaus'); assert.deepEqual(ui.screen(), ['hochhaus'])
|
||
assert.equal(ui.hk.feedMounts, 1); assert.equal(ui.hk.feedUnmounts, 0)
|
||
// Die Hustler-Tabelle bleibt über Bildschirmwechsel erhalten
|
||
assert.equal(Object.keys(ui.hk.props.hochhaus.workers).length, 19)
|
||
// onEvents bleibt zwischen den Rendern dieselbe Funktion (der Feed-Hook darf sich nicht an einer neuen Funktion verschlucken)
|
||
assert.ok(ui.hk.feedCalls.every((c) => c.options.onEvents === ui.hk.feedCalls[0].options.onEvents))
|
||
} finally { ui.end() }
|
||
assert.equal(ui.hk.feedUnmounts, 1)
|
||
})
|
||
|
||
test('Seite: Ereignisse schreiben die Hustler-Tabelle fort und gehen als dieselbe Liste an regieRef.current.push; ohne Regie kein Fehler', async () => {
|
||
const state = makeState({ statuses: { lea: 'wartet' } })
|
||
const ui = await seite('tab=hochhaus', { feed: { state, error: null, loaded: true, refresh() {} } })
|
||
try {
|
||
assert.equal(ui.hk.props.hochhaus.workers.lea.status, 'wartet')
|
||
// noch keine Regie eingehängt: nur die Tabelle ändert sich
|
||
const e1 = [ev('auftrag', 'lea', { text: 'Suche Betriebe in Kaiserslautern' })]
|
||
ui.hk.onEvents(e1)
|
||
assert.equal(ui.hk.props.hochhaus.workers.lea.status, 'arbeitet'); assert.equal(ui.hk.props.hochhaus.workers.lea.task, 'Suche Betriebe in Kaiserslautern')
|
||
// Das Hochhaus hängt eine Regie ein
|
||
const pushed = []
|
||
ui.hk.props.hochhaus.regieRef.current = { push: (events) => pushed.push(events) }
|
||
const e2 = [ev('leerlauf', 'lea', { text: 'Nichts zu tun' }), ev('fehler', 'kim', { text: 'Portal gesperrt' })]
|
||
ui.hk.onEvents(e2)
|
||
assert.equal(pushed.length, 1); assert.equal(pushed[0], e2)
|
||
assert.equal(ui.hk.props.hochhaus.workers.lea.status, 'leerlauf'); assert.equal(ui.hk.props.hochhaus.workers.kim.status, 'fehler')
|
||
assert.equal(ui.hk.props.hochhaus.workers.rita.status, 'arbeitet')
|
||
// Ereignisse erreichen die Tabelle auch, wenn ein anderer Bildschirm offen ist; die Regie bekommt sie weiter (nur das Hochhaus hängt eine ein)
|
||
ui.go('tab=freigabe')
|
||
ui.hk.onEvents([ev('fehler', 'lea', { text: 'Stopp' })])
|
||
assert.equal(ui.hk.props.pult.workers.lea.status, 'fehler')
|
||
} finally { ui.end() }
|
||
})
|
||
|
||
test('Seite: ein neuer Schnappschuss setzt die Hustler-Tabelle neu (Ereignisse davor wirken nicht mehr)', async () => {
|
||
const feed = { state: null, error: null, loaded: false, refresh() {} }
|
||
const ui = await seite('tab=hochhaus', { feed })
|
||
try {
|
||
assert.deepEqual(ui.hk.props.hochhaus.workers, {})
|
||
ui.setFeed({ state: makeState(), loaded: true })
|
||
assert.equal(ui.hk.props.hochhaus.workers.lea.status, 'arbeitet')
|
||
ui.hk.onEvents([ev('leerlauf', 'lea', { text: 'Pause' })])
|
||
assert.equal(ui.hk.props.hochhaus.workers.lea.status, 'leerlauf')
|
||
ui.setFeed({ state: makeState({ statuses: { lea: 'wartet', kim: 'leerlauf' }, connected: { pia: false } }) })
|
||
const w = ui.hk.props.hochhaus.workers
|
||
assert.equal(w.lea.status, 'wartet'); assert.equal(w.kim.status, 'leerlauf'); assert.equal(w.pia.status, 'grau'); assert.equal(w.pia.connected, false)
|
||
} finally { ui.end() }
|
||
})
|
||
|
||
test('Seite: Tab-Titel „Hustler“ solange die Seite offen ist, beim Verlassen kommt der vorige zurück', async () => {
|
||
const { __mount, jsx, WorkersPage } = await loadSeite()
|
||
const hk = {
|
||
params: new URLSearchParams(''), setParams() {}, auth: { isAdmin: false, user: null }, api: {}, feed: { state: null, error: null, loaded: false, refresh() {} },
|
||
feedCalls: [], feedMounts: 0, feedUnmounts: 0, props: {}, onEvents: null,
|
||
}
|
||
const savedHk = globalThis.__hk
|
||
globalThis.__hk = hk
|
||
globalThis.document = { title: 'WOMS – Projekte' }
|
||
const mounted = __mount(jsx(WorkersPage, {}))
|
||
try {
|
||
await settle()
|
||
assert.equal(globalThis.document.title, 'Hustler')
|
||
mounted.unmount()
|
||
assert.equal(globalThis.document.title, 'WOMS – Projekte')
|
||
} finally {
|
||
if (savedHk === undefined) delete globalThis.__hk; else globalThis.__hk = savedHk
|
||
delete globalThis.document
|
||
}
|
||
})
|
||
|
||
test('Seite: ohne angemeldeten Nutzer bleibt me ohne Id und Name (kein Absturz)', async () => {
|
||
const ui = await seite('tab=freigabe')
|
||
try {
|
||
ui.hk.auth = { isAdmin: false, user: null }
|
||
ui.setFeed({})
|
||
assert.deepEqual(ui.hk.props.pult.me, { id: undefined, name: undefined })
|
||
} finally { ui.end() }
|
||
})
|