diff --git a/tests/unit/helpers/fakeReact.mjs b/tests/unit/helpers/fakeReact.mjs index 40b133d..84f36b5 100644 --- a/tests/unit/helpers/fakeReact.mjs +++ b/tests/unit/helpers/fakeReact.mjs @@ -1,5 +1,5 @@ // 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 }). -// Dieselbe Fassung wie in workersPultUi.test.mjs (FAKE_REACT), hier als Baustein für weitere Komponententests. Die Bauteile laufen wirklich, Klicks und +// 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') diff --git a/tests/unit/workersPultUi.test.mjs b/tests/unit/workersPultUi.test.mjs index 8e19f48..57c82d2 100644 --- a/tests/unit/workersPultUi.test.mjs +++ b/tests/unit/workersPultUi.test.mjs @@ -4,9 +4,10 @@ import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import esbuild from 'esbuild' +import { fakeReact } from './helpers/fakeReact.mjs' // Die React-Teile sind unter Node nicht ladbar (JSX, Router): geprüft wird der Quelltext (Muster adminRoute.test.mjs); die Logik -// steckt in P3-T18. usePultFeed und PultList laufen wirklich: im Mini-Läufer anstelle von React (siehe unten), PultList zusätzlich als Text gerendert. +// steckt in P3-T18. usePultFeed und PultList laufen wirklich: im Mini-Läufer anstelle von React (helpers/fakeReact.mjs), PultList zusätzlich als Text gerendert. const read = (p) => fs.readFileSync(new URL('../../' + p, import.meta.url), 'utf8') test('WorkersPage hängt den Reiter freigabe an hochhaus an und zeigt dort die PultList', () => { @@ -45,132 +46,12 @@ test('PultList: Aktionen über pultAction, "Als gesendet" sendet den bearbeitete assert.doesNotMatch(s, /innerHTML/) }) -// ---- gemeinsame Bausteine der Läufer: EIN Mini-Läufer anstelle von React (Hooks, Zustand, Effekte, Baum aus { tag, props, children }) ---- +// ---- gemeinsame Bausteine der Läufer: EIN Mini-Läufer anstelle von React (Hooks, Zustand, Effekte, Baum aus { tag, props, children }), er liegt in helpers/fakeReact.mjs ---- // ---- und EINE Tabelle mit Attrappen für alles, was PultList von außen holt. usePultFeed und PultList laufen im selben Läufer. ---- 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 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() - }) -} -` -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' })) - }, -} - // Was PultList von außen holt: der Feed (je Filter eine Antwort aus globalThis.__pultFeedFor), die Mitarbeiterliste, die Zwischenablage, das Panel (Dialog) const STUBS = { usePultFeed: 'export function usePultFeed({ statuses } = {}) { return globalThis.__pultFeedFor(statuses) }', @@ -614,7 +495,7 @@ test('PultList (gerendert, P4-T32): Aktionen je Art – Antworten, Freigeben, Ba } finally { delete globalThis.__pultFeedFor; delete globalThis.__pultEmployees } }) -// ---- PultList wird bedient (P4-T32): derselbe Mini-Läufer (FAKE_REACT) statt React und DOM. Die Komponenten laufen wirklich (Zustand, Effekte, Ereignisse), ---- +// ---- PultList wird bedient (P4-T32): derselbe Mini-Läufer (helpers/fakeReact.mjs) statt React und DOM. Die Komponenten laufen wirklich (Zustand, Effekte, Ereignisse), ---- // ---- gerendert wird in einen Baum aus { tag, props, children }; Klicks und Eingaben ruft der Test von Hand auf. Der Dienst ist eine Attrappe. ---- async function loadUi() {