import { describe, it, expect, beforeEach } from "vitest"; import { h } from "../src/core/h.js"; import { createRoot as createHostRoot } from "../src/host/config.js"; import type { HostConfig } from "../src/host/config.js"; import type { Fiber } from "../src/host/fiber.js"; import { reconcileProps, resetUpdateQueue } from "../src/host/reconcile.js"; function makeTrackingHost() { const prepareUpdateCalls: { instance: string; tag: string; prevProps: Record; nextProps: Record }[] = []; const 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 host: HostConfig> = { name: "tracking", createRootContext: () => ({}), createInstance: (tag, props) => { const id = `${tag}_${instances.length}`; instances.push({ tag, props }); return id; }, createTextInstance: (text) => { texts.push(text); return `text_${texts.length - 1}`; }, appendChild: (parent, child) => { appends.push({ parent, child }); }, prepareUpdate: (instance, tag, prevProps, nextProps) => { prepareUpdateCalls.push({ instance, tag, prevProps: { ...prevProps }, nextProps: { ...nextProps } }); const changed: Record = {}; let hasChanges = false; for (const key of Object.keys(nextProps)) { if (prevProps[key] !== nextProps[key]) { changed[key] = nextProps[key]; hasChanges = true; } } for (const key of Object.keys(prevProps)) { if (!(key in nextProps)) { changed[key] = undefined; hasChanges = true; } } return hasChanges ? changed : null; }, commitUpdate: (instance, payload, tag, prevProps, nextProps) => { commitUpdateCalls.push({ instance, payload, tag, prevProps: { ...prevProps }, nextProps: { ...nextProps } }); }, }; return { host, prepareUpdateCalls, commitUpdateCalls, instances, texts, appends }; } describe("Value.Equal bail-out", () => { beforeEach(() => { resetUpdateQueue(); }); describe("unchanged subtree skips prepareUpdate/commitUpdate", () => { it("identical re-render skips prepareUpdate for unchanged element", () => { const { host, prepareUpdateCalls, commitUpdateCalls } = makeTrackingHost(); const root = createHostRoot(host, {}); root.render(h("div", { color: "red" })); prepareUpdateCalls.length = 0; commitUpdateCalls.length = 0; root.render(h("div", { color: "red" })); expect(prepareUpdateCalls.length).toBe(0); expect(commitUpdateCalls.length).toBe(0); }); it("identical re-render with children skips prepareUpdate for parent and children", () => { const { host, prepareUpdateCalls, commitUpdateCalls } = makeTrackingHost(); const root = createHostRoot(host, {}); root.render(h("div", { color: "red" }, h("span", { label: "a" }))); prepareUpdateCalls.length = 0; commitUpdateCalls.length = 0; root.render(h("div", { color: "red" }, h("span", { label: "a" }))); expect(prepareUpdateCalls.length).toBe(0); expect(commitUpdateCalls.length).toBe(0); }); }); describe("changed prop still triggers prepareUpdate", () => { it("changed prop on parent triggers prepareUpdate", () => { const { host, prepareUpdateCalls } = makeTrackingHost(); const root = createHostRoot(host, {}); root.render(h("div", { color: "red" })); prepareUpdateCalls.length = 0; root.render(h("div", { color: "blue" })); expect(prepareUpdateCalls.length).toBeGreaterThanOrEqual(1); const divCall = prepareUpdateCalls.find((c) => c.tag === "div"); expect(divCall).toBeDefined(); expect(divCall!.prevProps.color).toBe("red"); expect(divCall!.nextProps.color).toBe("blue"); }); it("changed child prop triggers prepareUpdate only for child", () => { const { host, prepareUpdateCalls } = makeTrackingHost(); const root = createHostRoot(host, {}); root.render(h("div", { color: "red" }, h("span", { label: "a" }))); prepareUpdateCalls.length = 0; root.render(h("div", { color: "red" }, h("span", { label: "b" }))); const spanCalls = prepareUpdateCalls.filter((c) => c.tag === "span"); expect(spanCalls.length).toBe(1); expect(spanCalls[0]!.prevProps.label).toBe("a"); expect(spanCalls[0]!.nextProps.label).toBe("b"); }); }); describe("deeply nested unchanged subtree is fully skipped", () => { it("three levels of unchanged elements are all skipped", () => { const { host, prepareUpdateCalls } = makeTrackingHost(); const root = createHostRoot(host, {}); root.render( h("div", { id: "1" }, h("section", { class: "outer" }, h("p", { align: "center" }, "text"), ), ), ); prepareUpdateCalls.length = 0; root.render( h("div", { id: "1" }, h("section", { class: "outer" }, h("p", { align: "center" }, "text"), ), ), ); expect(prepareUpdateCalls.length).toBe(0); }); it("deep subtree with one change only commitUpdates the changed node", () => { const { host, commitUpdateCalls } = makeTrackingHost(); const root = createHostRoot(host, {}); root.render( h("div", { id: "1" }, h("section", { class: "outer" }, h("p", { align: "center" }), ), ), ); commitUpdateCalls.length = 0; root.render( h("div", { id: "1" }, h("section", { class: "outer" }, h("p", { align: "left" }), ), ), ); const pCommit = commitUpdateCalls.filter((c) => c.tag === "p"); expect(pCommit.length).toBe(1); expect(pCommit[0]!.prevProps.align).toBe("center"); expect(pCommit[0]!.nextProps.align).toBe("left"); }); }); describe("cachedNode is set on mount", () => { it("fibers created during mount have cachedNode set", () => { const { host } = makeTrackingHost(); const root = createHostRoot(host, {}); root.render(h("div", { color: "red" }, h("span", { label: "a" }))); const divFiber = root.rootFiber!.children[0]!; expect(divFiber.cachedNode).not.toBeNull(); const spanFiber = divFiber.children[0]!; expect(spanFiber.cachedNode).not.toBeNull(); }); it("cachedNode matches the node used to create the fiber", () => { const { host } = makeTrackingHost(); const root = createHostRoot(host, {}); const divNode = h("div", { color: "red" }); root.render(divNode); const divFiber = root.rootFiber!.children[0]!; expect(divFiber.cachedNode).not.toBeNull(); }); }); describe("reconcileProps direct call with Value.Equal bail-out", () => { it("reconcileProps with identical node does not call prepareUpdate", () => { const { host, prepareUpdateCalls } = makeTrackingHost(); const root = createHostRoot(host, {}); root.render(h("div", { color: "red" })); const divFiber = root.rootFiber!.children[0]!; expect(divFiber.cachedNode).not.toBeNull(); prepareUpdateCalls.length = 0; reconcileProps(divFiber, h("div", { color: "red" }), host as HostConfig, {}); expect(prepareUpdateCalls.length).toBe(0); expect(divFiber.effect).toBeNull(); }); it("reconcileProps with changed node calls prepareUpdate", () => { const { host, prepareUpdateCalls } = makeTrackingHost(); const root = createHostRoot(host, {}); root.render(h("div", { color: "red" })); const divFiber = root.rootFiber!.children[0]!; prepareUpdateCalls.length = 0; reconcileProps(divFiber, h("div", { color: "blue" }), host as HostConfig, {}); expect(prepareUpdateCalls.length).toBe(1); }); }); describe("Value.Equal deep equality", () => { it("deep-equal props with different object references still bail out", () => { const { host, prepareUpdateCalls } = makeTrackingHost(); const root = createHostRoot(host, {}); root.render(h("div", { style: { color: "red", size: 10 } })); prepareUpdateCalls.length = 0; root.render(h("div", { style: { color: "red", size: 10 } })); expect(prepareUpdateCalls.length).toBe(0); }); }); });