import { Window } from 'happy-dom'; import { act, type ReactNode } from 'react'; import { createRoot, type Root } from 'react-dom/client'; const clientDomGlobalNames = [ 'window', 'document', 'navigator', 'location', 'history', 'Element', 'HTMLElement', 'HTMLButtonElement', 'HTMLInputElement', 'HTMLTextAreaElement', 'Node', 'NodeFilter', 'DocumentFragment', 'Event', 'MouseEvent', 'KeyboardEvent', 'FocusEvent', 'PointerEvent', 'CustomEvent', 'MutationObserver', 'ResizeObserver', 'getComputedStyle', 'requestAnimationFrame', 'cancelAnimationFrame', 'IS_REACT_ACT_ENVIRONMENT' ] as const; type ClientDomGlobalName = (typeof clientDomGlobalNames)[number]; type ClientDomRenderer = { container: HTMLDivElement; render: (node: ReactNode) => Promise; click: (element: HTMLElement) => Promise; cleanup: () => Promise; }; function setClientDomGlobals(window: Window): Map { const descriptors = new Map(); const values: Record = { window, document: window.document, navigator: window.navigator, location: window.location, history: window.history, Element: window.Element, HTMLElement: window.HTMLElement, HTMLButtonElement: window.HTMLButtonElement, HTMLInputElement: window.HTMLInputElement, HTMLTextAreaElement: window.HTMLTextAreaElement, Node: window.Node, NodeFilter: window.NodeFilter, DocumentFragment: window.DocumentFragment, Event: window.Event, MouseEvent: window.MouseEvent, KeyboardEvent: window.KeyboardEvent, FocusEvent: window.FocusEvent, PointerEvent: window.PointerEvent, CustomEvent: window.CustomEvent, MutationObserver: window.MutationObserver, ResizeObserver: window.ResizeObserver, getComputedStyle: window.getComputedStyle.bind(window), requestAnimationFrame: window.requestAnimationFrame.bind(window), cancelAnimationFrame: window.cancelAnimationFrame.bind(window), IS_REACT_ACT_ENVIRONMENT: true }; for (const name of clientDomGlobalNames) { descriptors.set(name, Object.getOwnPropertyDescriptor(globalThis, name)); Object.defineProperty(globalThis, name, { configurable: true, writable: true, value: values[name] }); } return descriptors; } function restoreClientDomGlobals(descriptors: Map) { for (const name of clientDomGlobalNames) { const descriptor = descriptors.get(name); if (descriptor) { Object.defineProperty(globalThis, name, descriptor); } else { Reflect.deleteProperty(globalThis, name); } } } function dispatchClientDomEvent(element: HTMLElement, event: unknown) { element.dispatchEvent(event as Event); } export async function renderInClientDom(node: ReactNode): Promise { const window = new Window({ url: 'http://localhost' }); const descriptors = setClientDomGlobals(window); const happyDomContainer = window.document.createElement('div'); window.document.body.append(happyDomContainer); const container = happyDomContainer as unknown as HTMLDivElement; const root: Root = createRoot(container); let cleanedUp = false; const render = async (nextNode: ReactNode) => { await act(async () => { root.render(nextNode); }); }; const cleanup = async () => { if (cleanedUp) { return; } cleanedUp = true; try { await act(async () => { root.unmount(); }); } finally { container.remove(); try { await window.happyDOM.close(); } finally { restoreClientDomGlobals(descriptors); } } }; try { await render(node); } catch (error) { await cleanup(); throw error; } return { container, render, click: async (element) => { await act(async () => { dispatchClientDomEvent( element, new window.PointerEvent('pointerdown', { bubbles: true, button: 0, buttons: 1 }) ); dispatchClientDomEvent( element, new window.MouseEvent('mousedown', { bubbles: true, button: 0, buttons: 1 }) ); dispatchClientDomEvent(element, new window.MouseEvent('mouseup', { bubbles: true, button: 0 })); dispatchClientDomEvent(element, new window.PointerEvent('pointerup', { bubbles: true, button: 0 })); element.click(); }); }, cleanup }; }