// Mini-Läufer anstelle von React für Tests der Komponenten unter Node (Hooks, Zustand, Effekte; gerendert wird in einen Baum aus { tag, props, children }). // Die einzige Fassung: workersPultUi.test.mjs und botleadList.test.mjs laden sie von hier (workersProspect.test.mjs hat eine eigene, abweichende Fassung mit Panel-Attrappen). Die Bauteile laufen wirklich, Klicks und // Eingaben ruft der Test von Hand auf. `esbuild` bündelt die Komponente mit dem Plug-in `fakeReact` (react und react/jsx-runtime kommen von hier). export const FAKE_REACT = ` export const Fragment = Symbol('fragment') export function jsx(type, props, key) { return { type, props: props ?? {}, key } } export const jsxs = jsx let current = null // Instanz des Bauteils, dessen Render gerade läuft let hookIndex = 0 const flat = (children, out = []) => { if (Array.isArray(children)) children.forEach((c) => flat(c, out)) else if (children !== null && children !== undefined && children !== false && children !== true) out.push(children) return out } // Hängt ein Element (Bauteile mit Hooks, darunter Tags) ein und rendert es; jedes Bauteil behält seinen Zustand unter seinem Pfad im Baum. export function __mount(element) { const owner = { render, effects: [], late: 0 } // late: Zustandsänderungen, die nach dem Abbau noch ankamen const instances = new Map() let tree = null let rendering = false let dirty = false const dispose = (inst) => { inst.alive = false for (const h of inst.hooks) if (h && typeof h.cleanup === 'function') h.cleanup() } const expand = (node, path, visited) => { if (typeof node === 'string' || typeof node === 'number') return String(node) const { type, props } = node if (typeof type === 'function') { visited.add(path) let inst = instances.get(path) if (!inst) { inst = { hooks: [], alive: true, owner }; instances.set(path, inst) } current = inst hookIndex = 0 const out = type(props) current = null return out === null || out === undefined || out === false ? null : expand(out, path + '/c', visited) } const children = flat(props.children).map((c, i) => expand(c, path + '/' + (c && c.key != null ? 'k:' + c.key : i), visited)) // ref-Objekt an einem Tag: wie React vor den Effekten gesetzt; das Element liefert der Test (globalThis.__fakeRef), sonst null if (typeof type === 'string' && props.ref && typeof props.ref === 'object') props.ref.current = globalThis.__fakeRef ? globalThis.__fakeRef(type, props) : null return { tag: type === Fragment ? 'fragment' : type, props, children } } function render() { if (rendering) { dirty = true; return } rendering = true do { dirty = false const visited = new Set() tree = expand(element, 'r', visited) for (const [path, inst] of [...instances]) { if (visited.has(path)) continue dispose(inst) instances.delete(path) } for (const run of owner.effects.splice(0)) run() } while (dirty) rendering = false } render() return { tree: () => tree, rerender: render, lateUpdates: () => owner.late, unmount: () => { for (const inst of instances.values()) dispose(inst); instances.clear() }, } } export function useState(initial) { const inst = current const i = hookIndex++ if (!inst.hooks[i]) inst.hooks[i] = { value: typeof initial === 'function' ? initial() : initial } const slot = inst.hooks[i] return [slot.value, (next) => { if (!inst.alive) { inst.owner.late += 1; return } const value = typeof next === 'function' ? next(slot.value) : next if (Object.is(value, slot.value)) return slot.value = value inst.owner.render() }] } export function useRef(initial) { const inst = current const i = hookIndex++ if (!inst.hooks[i]) inst.hooks[i] = { current: initial } return inst.hooks[i] } export function useCallback(fn, deps) { const inst = current const i = hookIndex++ const slot = inst.hooks[i] if (slot && deps.every((d, k) => Object.is(d, slot.deps[k]))) return slot.fn inst.hooks[i] = { fn, deps } return fn } export function useEffect(fn, deps) { const inst = current const i = hookIndex++ if (!inst.hooks[i]) inst.hooks[i] = { deps: undefined, cleanup: undefined, ran: false } const slot = inst.hooks[i] if (slot.ran && deps && slot.deps && deps.every((d, k) => Object.is(d, slot.deps[k]))) return inst.owner.effects.push(() => { if (typeof slot.cleanup === 'function') slot.cleanup() slot.deps = deps slot.ran = true slot.cleanup = fn() }) } // forwardRef: der ref kommt wie in React 18 als zweites Argument (der Mini-Läufer lässt ref in den props stehen) export function forwardRef(render) { return (props) => render(props, props.ref ?? null) } // useImperativeHandle: Objekt-Ref sofort beim Rendern setzen (wie React vor den Effekten), beim Abbau wieder null export function useImperativeHandle(ref, create) { if (ref && typeof ref === 'object') ref.current = create() useEffect(() => () => { if (ref && typeof ref === 'object') ref.current = null }, [ref]) } ` export const fakeReact = { name: 'fake-react', setup(build) { build.onResolve({ filter: /^react(\/jsx-runtime)?$/ }, () => ({ path: 'react', namespace: 'fake-react' })) build.onLoad({ filter: /.*/, namespace: 'fake-react' }, () => ({ contents: FAKE_REACT, loader: 'js' })) }, }