import { act, type ReactNode } from "react"; import { createRoot } from "react-dom/client"; /** Minimal host DOM for React lifecycle tests, not a browser/layout simulator. * Components may render div/section wrappers; no event dispatch, geometry or * accessibility behavior is emulated. React's reconciler/effects are real. */ class HostElement { readonly nodeType = 1; readonly namespaceURI = "http://www.w3.org/1999/xhtml"; readonly childNodes: HostElement[] = []; readonly style = {}; readonly attributes = new Map(); parentNode: HostElement | null = null; textContent = ""; constructor(readonly tagName: string, readonly ownerDocument: HostDocument) {} get nodeName() { return this.tagName; } get firstChild() { return this.childNodes[0] ?? null; } addEventListener() {} removeEventListener() {} setAttribute(name: string, value: string) { this.attributes.set(name, String(value)); } removeAttribute(name: string) { this.attributes.delete(name); } appendChild(node: HostElement) { this.childNodes.push(node); node.parentNode = this; return node; } insertBefore(node: HostElement, before: HostElement) { this.childNodes.splice(this.childNodes.indexOf(before), 0, node); node.parentNode = this; return node; } removeChild(node: HostElement) { this.childNodes.splice(this.childNodes.indexOf(node), 1); node.parentNode = null; return node; } } class HostDocument { readonly nodeType = 9; readonly activeElement = null; defaultView: unknown; addEventListener() {} removeEventListener() {} createElement(tag: string) { return new HostElement(tag.toUpperCase(), this); } createElementNS(_namespace: string, tag: string) { return this.createElement(tag); } } export function createClientLifecycleHarness() { const document = new HostDocument(); const frames = new Map>(); let frameId = 0; const window = { document, innerWidth: 1440, HTMLElement: HostElement, HTMLIFrameElement: class {}, addEventListener() {}, removeEventListener() {}, requestAnimationFrame(callback: () => void) { const id = ++frameId; frames.set(id, setTimeout(() => { frames.delete(id); callback(); }, 0)); return id; }, cancelAnimationFrame(id: number) { clearTimeout(frames.get(id)); frames.delete(id); }, }; document.defaultView = window; const globals = { window, document, IS_REACT_ACT_ENVIRONMENT: true }; const originals = Object.fromEntries(Object.keys(globals).map((key) => [ key, Object.getOwnPropertyDescriptor(globalThis, key), ])); for (const [key, value] of Object.entries(globals)) { Object.defineProperty(globalThis, key, { value, writable: true, configurable: true }); } const errors: unknown[] = []; const root = createRoot(document.createElement("div") as unknown as HTMLElement, { onUncaughtError: (error) => { errors.push(error); }, }); return { errors, async render(node: ReactNode) { await act(async () => { root.render(node); }); }, async update(action: () => void) { await act(async () => { action(); }); }, async idle() { await act(async () => { await new Promise((resolve) => setTimeout(resolve, 10)); }); }, async close() { try { await act(async () => { root.unmount(); }); } finally { for (const timer of frames.values()) clearTimeout(timer); for (const [key, descriptor] of Object.entries(originals)) { if (descriptor) Object.defineProperty(globalThis, key, descriptor); else Reflect.deleteProperty(globalThis, key); } } }, }; }