152 lines
5.0 KiB
TypeScript
152 lines
5.0 KiB
TypeScript
import { describe, it, expect, beforeEach } from "vitest";
|
|
import { Value } from "@alkdev/typebox/value";
|
|
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 { reconcileProps, commitEffects, resetUpdateQueue } from "../src/host/reconcile.js";
|
|
import type { Fiber } from "../src/host/fiber.js";
|
|
|
|
function makeTrackingHost() {
|
|
const commitUpdateCalls: {
|
|
instance: string;
|
|
payload: unknown;
|
|
tag: string;
|
|
prevProps: Record<string, unknown>;
|
|
nextProps: Record<string, unknown>;
|
|
}[] = [];
|
|
|
|
const host: HostConfig<string, string, Record<string, unknown>> = {
|
|
name: "tracking",
|
|
createRootContext: () => ({}),
|
|
createInstance: (tag, _props) => `${tag}_inst`,
|
|
createTextInstance: (text) => `text:${text}`,
|
|
appendChild: () => {},
|
|
prepareUpdate: (_instance, _tag, prevProps, nextProps) => {
|
|
const changed: Record<string, unknown> = {};
|
|
let hasChanges = false;
|
|
for (const key of Object.keys(nextProps)) {
|
|
if (prevProps[key] !== nextProps[key]) {
|
|
changed[key] = nextProps[key];
|
|
hasChanges = true;
|
|
}
|
|
}
|
|
return hasChanges ? changed : null;
|
|
},
|
|
commitUpdate: (instance, payload, tag, prevProps, nextProps) => {
|
|
commitUpdateCalls.push({
|
|
instance: instance as string,
|
|
payload,
|
|
tag: tag as string,
|
|
prevProps,
|
|
nextProps,
|
|
});
|
|
},
|
|
};
|
|
|
|
return { host, commitUpdateCalls };
|
|
}
|
|
|
|
describe("Value.Clone for prevProps snapshots", () => {
|
|
beforeEach(() => {
|
|
resetUpdateQueue();
|
|
});
|
|
|
|
it("mutating fiber.props after setting prevProps does not affect prevProps", () => {
|
|
const { host } = makeTrackingHost();
|
|
const root = createHostRoot(host, {});
|
|
root.render(h("div", { style: { color: "red", size: 10 } }));
|
|
|
|
const divFiber = root.rootFiber!.children[0]!;
|
|
|
|
reconcileProps(
|
|
divFiber,
|
|
h("div", { style: { color: "blue", size: 20 } }),
|
|
host as HostConfig<string, string, unknown>,
|
|
{},
|
|
);
|
|
commitEffects(divFiber, host as HostConfig<string, string, unknown>, {});
|
|
|
|
expect(divFiber.prevProps).not.toBeNull();
|
|
const prevStyle = (divFiber.prevProps!.style as Record<string, unknown>);
|
|
|
|
(divFiber.props.style as Record<string, unknown>).color = "green";
|
|
|
|
expect(prevStyle.color).toBe("red");
|
|
expect((divFiber.prevProps!.style as Record<string, unknown>).color).toBe("red");
|
|
});
|
|
|
|
it("commitUpdate receives independent prevProps and nextProps with no shared references", () => {
|
|
const { host, commitUpdateCalls } = makeTrackingHost();
|
|
const root = createHostRoot(host, {});
|
|
root.render(h("div", { style: { color: "red" } }));
|
|
|
|
commitUpdateCalls.length = 0;
|
|
|
|
root.render(h("div", { style: { color: "blue" } }));
|
|
|
|
const divCall = commitUpdateCalls.find((c) => c.tag === "div");
|
|
expect(divCall).toBeDefined();
|
|
|
|
const prevStyle = divCall!.prevProps.style as Record<string, unknown>;
|
|
const nextStyle = divCall!.nextProps.style as Record<string, unknown>;
|
|
|
|
expect(prevStyle.color).toBe("red");
|
|
expect(nextStyle.color).toBe("blue");
|
|
});
|
|
|
|
it("prevProps is a deep clone — nested objects are independent", () => {
|
|
const { host } = makeTrackingHost();
|
|
const root = createHostRoot(host, {});
|
|
root.render(h("div", { data: { nested: { deep: "original" } } }));
|
|
|
|
const divFiber = root.rootFiber!.children[0]!;
|
|
reconcileProps(
|
|
divFiber,
|
|
h("div", { data: { nested: { deep: "changed" } } }),
|
|
host as HostConfig<string, string, unknown>,
|
|
{},
|
|
);
|
|
|
|
expect(divFiber.prevProps).not.toBeNull();
|
|
const prevNested = (divFiber.prevProps!.data as Record<string, unknown>).nested as Record<string, unknown>;
|
|
expect(prevNested.deep).toBe("original");
|
|
});
|
|
|
|
it("commitUpdate receives correct before/after prop values", () => {
|
|
const { host, commitUpdateCalls } = makeTrackingHost();
|
|
const root = createHostRoot(host, {});
|
|
root.render(h("div", { color: "red", count: 1 }));
|
|
|
|
commitUpdateCalls.length = 0;
|
|
|
|
root.render(h("div", { color: "blue", count: 2 }));
|
|
|
|
const divCall = commitUpdateCalls.find((c) => c.tag === "div");
|
|
expect(divCall).toBeDefined();
|
|
expect(divCall!.prevProps.color).toBe("red");
|
|
expect(divCall!.nextProps.color).toBe("blue");
|
|
expect(divCall!.prevProps.count).toBe(1);
|
|
expect(divCall!.nextProps.count).toBe(2);
|
|
});
|
|
|
|
it("text node prevProps is deep cloned", () => {
|
|
const { host } = makeTrackingHost();
|
|
const root = createHostRoot(host, {});
|
|
root.render(h("div", null, "hello"));
|
|
|
|
const divFiber = root.rootFiber!.children[0]!;
|
|
const textFiber = divFiber.children[0]!;
|
|
|
|
reconcileProps(
|
|
textFiber,
|
|
"world",
|
|
host as HostConfig<string, string, unknown>,
|
|
{},
|
|
);
|
|
commitEffects(textFiber, host as HostConfig<string, string, unknown>, {});
|
|
|
|
expect(textFiber.prevProps).not.toBeNull();
|
|
expect(textFiber.prevProps!.text).toBe("hello");
|
|
expect(textFiber.props.text).toBe("world");
|
|
});
|
|
}); |