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 = { setProperty() {}, getPropertyValue() { return ""; }, removeProperty() { return ""; }, }; open = false; get disabled() { return this.hasAttribute("disabled"); } isConnected = true; getAttribute(name: string) { return this.attributes.get(name) ?? null; } hasAttribute(name: string) { return this.attributes.has(name); } focus() { this.ownerDocument.activeElement = this; } showModal() { this.open = true; } close() { this.open = false; } scrollIntoView() {} scrollTop = 0; scrollHeight = 0; clientHeight = 0; clientWidth = 1440; offsetWidth = 1440; readonly scrollCalls: { top?: number }[] = []; scrollTo(options: { top?: number }) { this.scrollCalls.push(options); } /** No layout here: every box is the origin. Components that position from rects still run. */ getBoundingClientRect() { return { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0, x: 0, y: 0 }; } /** Class selectors only (`.a` / `.a.b`); enough for components that find their scroller. */ closest(selector: string): HostElement | null { const wanted = selector.split(".").filter(Boolean); const classes = (this.getAttribute("class") ?? "").split(/\s+/); if (wanted.every(name => classes.includes(name))) return this; return this.parentNode?.closest(selector) ?? null; } get parentElement() { return this.parentNode; } get children() { return this.childNodes.filter((child) => child.nodeType === 1); } matches() { return false; } querySelector() { return null; } querySelectorAll() { return []; } getRootNode() { return this.ownerDocument; } contains(node: HostElement | null): boolean { return node === this || this.childNodes.some(child => child.contains(node)); } get text(): string { return this.textContent + this.childNodes.map(child => child.text).join(""); } get props(): Record { return (this as unknown as Record>)[Object.keys(this).find(key => key.startsWith("__reactProps$")) ?? ""] ?? {}; } readonly attributes = new Map(); readonly dataset: Record = {}; parentNode: HostElement | null = null; textContent = ""; get nodeValue() { return this.textContent; } set nodeValue(value: string) { this.textContent = value; } 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 body: HostElement; readonly head: HostElement; readonly documentElement: HostElement; activeElement: HostElement | null = null; constructor() { this.documentElement = new HostElement("HTML", this); this.head = new HostElement("HEAD", this); this.body = new HostElement("BODY", this); this.documentElement.appendChild(this.head); this.documentElement.appendChild(this.body); } createTextNode(text: string) { const node = new HostElement("#text", this); node.textContent = text; return node; } getElementById() { return null; } // React 19 hoists