Separate shell subscriptions from Home session data and cover effect convergence, transition cleanup, current callbacks and signed-out fallback with real React lifecycle tests. Record baseline-equivalent local failures and outstanding browser/build verification. Co-Authored-By: Claude Code <noreply@anthropic.com>
97 lines
3.6 KiB
TypeScript
97 lines
3.6 KiB
TypeScript
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<string, string>();
|
|
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<number, ReturnType<typeof setTimeout>>();
|
|
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);
|
|
}
|
|
}
|
|
},
|
|
};
|
|
}
|