import { describe, it, expect } from "vitest"; import { h, createComponent } from "../src/core/h.js"; import { createRoot as createHostRoot } from "../src/host/config.js"; import type { HostConfig } from "../src/host/config.js"; function makeHost(): { host: HostConfig>; instances: { tag: string; props: Record }[]; texts: string[]; appends: { parent: string; child: string }[]; prepareUpdateCalls: { instance: string; tag: string; prevProps: Record; nextProps: Record }[]; commitUpdateCalls: { instance: string; payload: unknown; tag: string; prevProps: Record; nextProps: Record }[]; } { const instances: { tag: string; props: Record }[] = []; const texts: string[] = []; const appends: { parent: string; child: string }[] = []; const prepareUpdateCalls: { instance: string; tag: string; prevProps: Record; nextProps: Record }[] = []; const commitUpdateCalls: { instance: string; payload: unknown; tag: string; prevProps: Record; nextProps: Record }[] = []; const host: HostConfig> = { name: "test", createRootContext: () => ({}), createInstance: (tag, props) => { const id = `${tag}_${instances.length}`; instances.push({ tag, props }); return id; }, createTextInstance: (text) => { texts.push(text); return text; }, appendChild: (parent, child) => { appends.push({ parent, child }); }, prepareUpdate: (instance, tag, prevProps, nextProps) => { prepareUpdateCalls.push({ instance, tag, prevProps, nextProps }); const changed = Object.keys(nextProps).some( (k) => prevProps[k] !== nextProps[k], ) || Object.keys(prevProps).some( (k) => !(k in nextProps), ); return changed ? { prevProps, nextProps } : null; }, commitUpdate: (instance, payload, tag, prevProps, nextProps) => { commitUpdateCalls.push({ instance, payload, tag, prevProps, nextProps }); }, }; return { host, instances, texts, appends, prepareUpdateCalls, commitUpdateCalls }; } describe("Root.render() re-renderable", () => { it("first render mounts and builds fiber tree", () => { const { host, instances } = makeHost(); const root = createHostRoot(host, {}); root.render(h("div", { class: "a" }, "hello")); expect(root.rootFiber).not.toBeNull(); expect(instances.length).toBe(1); expect(instances[0]!.tag).toBe("div"); }); it("second render does not create duplicate instances", () => { const { host, instances } = makeHost(); const root = createHostRoot(host, {}); root.render(h("div", { class: "a" }, "hello")); const countAfterFirst = instances.length; root.render(h("div", { class: "b" }, "hello")); expect(instances.length).toBe(countAfterFirst); }); it("second render produces one instance tree with updated props", () => { const { host } = makeHost(); const root = createHostRoot(host, {}); root.render(h("div", { class: "a" }, "hello")); const divFiber = root.rootFiber!.children[0]!; expect(divFiber.props.class).toBe("a"); root.render(h("div", { class: "b" }, "hello")); expect(divFiber.props.class).toBe("b"); }); it("commitUpdate is called for changed props on re-render", () => { const { host, commitUpdateCalls } = makeHost(); const root = createHostRoot(host, {}); root.render(h("div", { class: "old" }, "hello")); commitUpdateCalls.length = 0; root.render(h("div", { class: "new" }, "hello")); expect(commitUpdateCalls.length).toBeGreaterThanOrEqual(1); const divCall = commitUpdateCalls.find((c) => c.tag === "div"); expect(divCall).toBeDefined(); expect(divCall!.prevProps.class).toBe("old"); expect(divCall!.nextProps.class).toBe("new"); }); it("commitUpdate is called for changed props on re-render", () => { const { host, commitUpdateCalls } = makeHost(); const root = createHostRoot(host, {}); root.render(h("div", { class: "old" }, "hello")); commitUpdateCalls.length = 0; root.render(h("div", { class: "new" }, "hello")); expect(commitUpdateCalls.length).toBeGreaterThanOrEqual(1); const divCall = commitUpdateCalls.find((c) => c.tag === "div"); expect(divCall).toBeDefined(); }); it("no commitUpdate when props are unchanged", () => { const { host, commitUpdateCalls } = makeHost(); const root = createHostRoot(host, {}); root.render(h("div", { class: "same" }, "hello")); commitUpdateCalls.length = 0; root.render(h("div", { class: "same" }, "hello")); const divCall = commitUpdateCalls.find((c) => c.tag === "div"); expect(divCall).toBeUndefined(); }); it("positional children matching: Nth old child matches Nth new child", () => { const { host, commitUpdateCalls } = makeHost(); const root = createHostRoot(host, {}); root.render(h("div", null, h("span", { color: "red" }), h("em", { size: "big" }))); commitUpdateCalls.length = 0; root.render(h("div", null, h("span", { color: "blue" }), h("em", { size: "small" }))); const spanCall = commitUpdateCalls.find((c) => c.tag === "span"); const emCall = commitUpdateCalls.find((c) => c.tag === "em"); expect(spanCall).toBeDefined(); expect(emCall).toBeDefined(); expect(spanCall!.nextProps.color).toBe("blue"); expect(emCall!.nextProps.size).toBe("small"); }); it("excess old children are removed when new children are fewer", () => { const { host, instances } = makeHost(); const root = createHostRoot(host, {}); root.render(h("div", null, h("span", null), h("em", null))); root.render(h("div", null, h("span", null))); expect(root.rootFiber!.children[0]!.children.length).toBe(1); expect(root.rootFiber!.children[0]!.children[0]!.tag).toBe("span"); }); it("text node props are reconciled on re-render", () => { const { host, prepareUpdateCalls } = makeHost(); const root = createHostRoot(host, {}); root.render(h("div", null, "hello")); prepareUpdateCalls.length = 0; root.render(h("div", null, "world")); const textCall = prepareUpdateCalls.find((c) => c.tag === "#text"); expect(textCall).toBeDefined(); expect(textCall!.prevProps.text).toBe("hello"); expect(textCall!.nextProps.text).toBe("world"); }); it("function components are transparent during reconciliation", () => { const { host, commitUpdateCalls } = makeHost(); const MyComp = createComponent("MyComp", (props) => h("section", { id: props.id as string })); const root = createHostRoot(host, {}); root.render(h(MyComp, { id: "1" })); commitUpdateCalls.length = 0; root.render(h(MyComp, { id: "2" })); const sectionCall = commitUpdateCalls.find((c) => c.tag === "section"); expect(sectionCall).toBeDefined(); expect(sectionCall!.nextProps.id).toBe("2"); }); it("existing tests still pass — rootFiber null before render", () => { const { host } = makeHost(); const root = createHostRoot(host, {}); expect(root.rootFiber).toBeNull(); }); it("existing tests still pass — rootFiber set after render", () => { const { host } = makeHost(); const root = createHostRoot(host, {}); root.render(h("div", null, "hello")); expect(root.rootFiber).not.toBeNull(); }); });