import { describe, it, expect, vi } from "vitest"; import type { Fiber } from "../src/host/fiber.js"; import type { UNode } from "../src/core/schema.js"; import { reconcileChildren } from "../src/host/reconcile.js"; function makeFiber(key: string | undefined, tag: string): Fiber { return { instance: `inst-${tag}-${key ?? "nokey"}`, tag, props: {}, key, children: [], parent: null, effect: null, signalDisposers: [], prevProps: null, disposed: false, }; } function makeElement(type: string, key?: string): UNode { return { type, props: {}, children: [], ...(key !== undefined ? { key } : {}) }; } describe("reconcileChildren", () => { it("keyed children reordered → matched correctly by key", () => { const oldFibers = [ makeFiber("a", "div"), makeFiber("b", "span"), makeFiber("c", "p"), ]; const newChildren = [ makeElement("p", "c"), makeElement("div", "a"), makeElement("span", "b"), ]; const result = reconcileChildren(oldFibers, newChildren); expect(result.matched).toHaveLength(3); expect(result.added).toHaveLength(0); expect(result.removed).toHaveLength(0); expect(result.matched[0]!.oldFiber.key).toBe("c"); expect(result.matched[1]!.oldFiber.key).toBe("a"); expect(result.matched[2]!.oldFiber.key).toBe("b"); }); it("keyed child added at start → old children matched, new child added", () => { const oldFibers = [ makeFiber("b", "span"), makeFiber("c", "p"), ]; const newChildren = [ makeElement("div", "a"), makeElement("span", "b"), makeElement("p", "c"), ]; const result = reconcileChildren(oldFibers, newChildren); expect(result.matched).toHaveLength(2); expect(result.added).toHaveLength(1); expect(result.removed).toHaveLength(0); expect(result.added[0]!.newChild.key).toBe("a"); expect(result.matched[0]!.oldFiber.key).toBe("b"); expect(result.matched[1]!.oldFiber.key).toBe("c"); }); it("keyed child removed → classified as removed", () => { const oldFibers = [ makeFiber("a", "div"), makeFiber("b", "span"), makeFiber("c", "p"), ]; const newChildren = [ makeElement("div", "a"), makeElement("p", "c"), ]; const result = reconcileChildren(oldFibers, newChildren); expect(result.matched).toHaveLength(2); expect(result.added).toHaveLength(0); expect(result.removed).toHaveLength(1); expect(result.removed[0]!.key).toBe("b"); }); it("mixed keyed and unkeyed children → key-based for keyed, positional for unkeyed", () => { const oldFibers = [ makeFiber("a", "div"), makeFiber(undefined, "span"), makeFiber(undefined, "p"), makeFiber("b", "section"), ]; const newChildren = [ makeElement("span"), makeElement("section", "b"), makeElement("p"), makeElement("div", "a"), ]; const result = reconcileChildren(oldFibers, newChildren); expect(result.matched).toHaveLength(4); expect(result.added).toHaveLength(0); expect(result.removed).toHaveLength(0); const matchedKeys = result.matched.map((m) => m.oldFiber.key); expect(matchedKeys).toContain("a"); expect(matchedKeys).toContain("b"); expect(matchedKeys).toContain(undefined); expect(result.matched.filter((m) => m.oldFiber.key === undefined)).toHaveLength(2); }); it("duplicate key → last-wins, no crash", () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const oldFibers = [ makeFiber("a", "div"), makeFiber("a", "span"), ]; const newChildren = [ makeElement("span", "a"), ]; const result = reconcileChildren(oldFibers, newChildren); expect(result.matched).toHaveLength(1); expect(result.added).toHaveLength(0); expect(result.removed).toHaveLength(1); expect(result.matched[0]!.oldFiber.tag).toBe("span"); expect(result.removed[0]!.tag).toBe("div"); expect(warnSpy).toHaveBeenCalled(); warnSpy.mockRestore(); }); it("duplicate key among new children → last-wins", () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const oldFibers = [ makeFiber("a", "div"), ]; const newChildren = [ makeElement("div", "a"), makeElement("div", "a"), ]; const result = reconcileChildren(oldFibers, newChildren); expect(result.matched).toHaveLength(1); expect(result.matched[0]!.newChild.key).toBe("a"); expect(result.added).toHaveLength(0); expect(result.removed).toHaveLength(1); expect(warnSpy).toHaveBeenCalled(); warnSpy.mockRestore(); }); it("matched with different type → old removed, new added", () => { const oldFibers = [ makeFiber("a", "div"), ]; const newChildren = [ makeElement("span", "a"), ]; const result = reconcileChildren(oldFibers, newChildren); expect(result.matched).toHaveLength(0); expect(result.added).toHaveLength(1); expect(result.removed).toHaveLength(1); expect(result.added[0]!.newChild.type).toBe("span"); expect(result.removed[0]!.tag).toBe("div"); }); it("unkeyed children with type change → old removed, new added", () => { const oldFibers = [ makeFiber(undefined, "div"), ]; const newChildren = [ makeElement("span"), ]; const result = reconcileChildren(oldFibers, newChildren); expect(result.matched).toHaveLength(0); expect(result.added).toHaveLength(1); expect(result.removed).toHaveLength(1); }); it("returns empty classification for empty inputs", () => { const result = reconcileChildren([], []); expect(result.matched).toHaveLength(0); expect(result.added).toHaveLength(0); expect(result.removed).toHaveLength(0); }); it("all old children removed when new children is empty", () => { const oldFibers = [makeFiber("a", "div"), makeFiber("b", "span")]; const result = reconcileChildren(oldFibers, []); expect(result.matched).toHaveLength(0); expect(result.added).toHaveLength(0); expect(result.removed).toHaveLength(2); }); it("all new children added when old children is empty", () => { const newChildren = [makeElement("div", "a"), makeElement("span", "b")]; const result = reconcileChildren([], newChildren); expect(result.matched).toHaveLength(0); expect(result.added).toHaveLength(2); expect(result.removed).toHaveLength(0); }); it("skips non-UElement children (primitives/roots) in matching", () => { const oldFibers = [makeFiber("a", "div")]; const newChildren: UNode[] = ["text", makeElement("div", "a")]; const result = reconcileChildren(oldFibers, newChildren); expect(result.matched).toHaveLength(1); expect(result.matched[0]!.oldFiber.key).toBe("a"); }); it("pure function — does not mutate old fibers", () => { const oldFibers = [makeFiber("a", "div"), makeFiber("b", "span")]; const snapshots = oldFibers.map((f) => ({ ...f })); const newChildren = [makeElement("span", "b"), makeElement("div", "a")]; reconcileChildren(oldFibers, newChildren); for (let i = 0; i < oldFibers.length; i++) { expect(oldFibers[i]!.instance).toBe(snapshots[i]!.instance); expect(oldFibers[i]!.tag).toBe(snapshots[i]!.tag); expect(oldFibers[i]!.key).toBe(snapshots[i]!.key); } }); });