import { describe, it, expect, expectTypeOf } from "vitest"; import type { Fiber, Effect, HostLike } from "../src/host/fiber.js"; import { disposeFiber } from "../src/host/fiber.js"; describe("Fiber interface", () => { it("has all required fields", () => { const fiber: Fiber = { instance: "inst-1", tag: "div", props: { class: "test" }, key: "a", children: [], parent: null, effect: null, signalDisposers: [], prevProps: null, disposed: false, }; expect(fiber.instance).toBe("inst-1"); expect(fiber.tag).toBe("div"); expect(fiber.props.class).toBe("test"); expect(fiber.key).toBe("a"); expect(fiber.children).toEqual([]); expect(fiber.parent).toBeNull(); expect(fiber.effect).toBeNull(); expect(fiber.signalDisposers).toEqual([]); expect(fiber.prevProps).toBeNull(); }); it("supports key as undefined", () => { const fiber: Fiber = { instance: "inst-2", tag: "span", props: {}, key: undefined, children: [], parent: null, effect: null, signalDisposers: [], prevProps: null, disposed: false, }; expect(fiber.key).toBeUndefined(); }); it("supports parent reference", () => { const parent: Fiber = { instance: "parent-inst", tag: "div", props: {}, key: undefined, children: [], parent: null, effect: null, signalDisposers: [], prevProps: null, disposed: false, }; const child: Fiber = { instance: "child-inst", tag: "span", props: {}, key: "child-1", children: [], parent, effect: null, signalDisposers: [], prevProps: null, disposed: false, }; parent.children.push(child); expect(child.parent).toBe(parent); expect(parent.children[0]).toBe(child); }); it("supports signalDisposers array", () => { const disposed: string[] = []; const fiber: Fiber = { instance: "inst", tag: "div", props: {}, key: undefined, children: [], parent: null, effect: null, signalDisposers: [() => disposed.push("a"), () => disposed.push("b")], prevProps: null, disposed: false, }; fiber.signalDisposers.forEach((d) => d()); expect(disposed).toEqual(["a", "b"]); }); }); describe("Effect union type", () => { it("update effect", () => { const effect: Effect = { type: "update", payload: { class: "new" } }; expect(effect.type).toBe("update"); expect((effect as { type: "update"; payload: unknown }).payload).toEqual({ class: "new" }); }); it("insert effect with before=null (append)", () => { const effect: Effect = { type: "insert", before: null }; expect(effect.type).toBe("insert"); }); it("insert effect with before=fiber", () => { const before: Fiber = { instance: "existing", tag: "span", props: {}, key: undefined, children: [], parent: null, effect: null, signalDisposers: [], prevProps: null, disposed: false, }; const effect: Effect = { type: "insert", before }; expect(effect.type).toBe("insert"); expect(effect.before).toBe(before); }); it("move effect with before=fiber", () => { const before: Fiber = { instance: "target", tag: "span", props: {}, key: undefined, children: [], parent: null, effect: null, signalDisposers: [], prevProps: null, disposed: false, }; const effect: Effect = { type: "move", before }; expect(effect.type).toBe("move"); expect(effect.before).toBe(before); }); it("move effect with before=null (append at end)", () => { const effect: Effect = { type: "move", before: null }; expect(effect.type).toBe("move"); }); it("remove effect", () => { const effect: Effect = { type: "remove" }; expect(effect.type).toBe("remove"); }); it("effect field on Fiber accepts all variants", () => { const fiber: Fiber = { instance: 1, tag: "div", props: {}, key: undefined, children: [], parent: null, effect: { type: "update", payload: "x" }, signalDisposers: [], prevProps: {}, disposed: false, }; expect(fiber.effect!.type).toBe("update"); fiber.effect = { type: "remove" }; expect(fiber.effect.type).toBe("remove"); fiber.effect = { type: "insert", before: null }; expect(fiber.effect.type).toBe("insert"); fiber.effect = { type: "move", before: null }; expect(fiber.effect.type).toBe("move"); fiber.effect = null; expect(fiber.effect).toBeNull(); }); }); describe("Fiber re-export from barrel", () => { it("Fiber and Effect types are importable from src/mod.ts", async () => { type _FiberCheck = import("../src/mod.js").Fiber; type _EffectCheck = import("../src/mod.js").Effect; const _fiber: _FiberCheck = { instance: "inst", tag: "div", props: {}, key: undefined, children: [], parent: null, effect: null, signalDisposers: [], prevProps: null, disposed: false, }; const _effect: _EffectCheck = { type: "remove" }; expect(_fiber.instance).toBe("inst"); expect(_effect.type).toBe("remove"); }); }); describe("disposeFiber", () => { it("disposes a 3-level tree bottom-up: children before parent", () => { const finalized: string[] = []; const disposed: string[] = []; const ctx = {}; const host: HostLike = { finalizeInstance(instance, _ctx) { finalized.push(instance); }, }; const grandchild: Fiber = { instance: "gc", tag: "span", props: {}, key: undefined, children: [], parent: null as unknown as Fiber, effect: null, signalDisposers: [() => disposed.push("gc-signal")], prevProps: null, disposed: false, }; const child: Fiber = { instance: "child", tag: "div", props: {}, key: undefined, children: [grandchild], parent: null as unknown as Fiber, effect: { type: "update", payload: null }, signalDisposers: [() => disposed.push("child-signal")], prevProps: null, disposed: false, }; const root: Fiber = { instance: "root", tag: "root", props: {}, key: undefined, children: [child], parent: null, effect: null, signalDisposers: [() => disposed.push("root-signal")], prevProps: null, disposed: false, }; child.parent = root; grandchild.parent = child; disposeFiber(root, host, ctx); expect(finalized).toEqual(["gc", "child", "root"]); expect(disposed).toEqual(["gc-signal", "child-signal", "root-signal"]); }); it("idempotent: double-dispose does not error or double-call", () => { const finalized: string[] = []; const disposed: string[] = []; const ctx = {}; const host: HostLike = { finalizeInstance(instance) { finalized.push(instance); }, }; const fiber: Fiber = { instance: "inst", tag: "div", props: {}, key: undefined, children: [], parent: null, effect: { type: "update", payload: null }, signalDisposers: [() => disposed.push("sig")], prevProps: null, disposed: false, }; disposeFiber(fiber, host, ctx); disposeFiber(fiber, host, ctx); expect(finalized).toEqual(["inst"]); expect(disposed).toEqual(["sig"]); expect(fiber.signalDisposers).toEqual([]); expect(fiber.parent).toBeNull(); expect(fiber.effect).toBeNull(); }); it("signal subscriptions are cleaned up (effect no longer fires after dispose)", () => { let callCount = 0; const ctx = {}; const host: HostLike = {}; const disposer = () => { callCount = -1; }; const fiber: Fiber = { instance: "inst", tag: "div", props: {}, key: undefined, children: [], parent: null, effect: null, signalDisposers: [disposer], prevProps: null, disposed: false, }; disposeFiber(fiber, host, ctx); expect(fiber.signalDisposers).toEqual([]); expect(callCount).toBe(-1); }); it("does not call removeChild on host", () => { const ctx = {}; const operations: string[] = []; const host: HostLike = { finalizeInstance() { operations.push("finalizeInstance"); }, }; const fiber: Fiber = { instance: "inst", tag: "div", props: {}, key: undefined, children: [], parent: null, effect: null, signalDisposers: [], prevProps: null, disposed: false, }; disposeFiber(fiber, host, ctx); expect(operations).toEqual(["finalizeInstance"]); }); it("works when finalizeInstance is undefined (no host cleanup)", () => { const disposed: string[] = []; const ctx = {}; const host: HostLike = {}; const fiber: Fiber = { instance: "inst", tag: "div", props: {}, key: undefined, children: [], parent: null, effect: null, signalDisposers: [() => disposed.push("sig")], prevProps: null, disposed: false, }; expect(() => disposeFiber(fiber, host, ctx)).not.toThrow(); expect(disposed).toEqual(["sig"]); }); it("clears fiber.parent after disposal", () => { const ctx = {}; const host: HostLike = {}; const parent: Fiber = { instance: "parent", tag: "div", props: {}, key: undefined, children: [], parent: null, effect: null, signalDisposers: [], prevProps: null, disposed: false, }; const child: Fiber = { instance: "child", tag: "span", props: {}, key: undefined, children: [], parent, effect: null, signalDisposers: [], prevProps: null, disposed: false, }; parent.children.push(child); disposeFiber(child, host, ctx); expect(child.parent).toBeNull(); }); it("clears fiber.effect after disposal", () => { const ctx = {}; const host: HostLike = {}; const fiber: Fiber = { instance: "inst", tag: "div", props: {}, key: undefined, children: [], parent: null, effect: { type: "update", payload: "x" }, signalDisposers: [], prevProps: null, disposed: false, }; disposeFiber(fiber, host, ctx); expect(fiber.effect).toBeNull(); }); });