import { describe, it, expect } from "vitest"; import { signal, computed, ReactiveRoot, reactiveComponent, reactiveElement } from "../src/core/reactive.js"; import { h, createComponent } from "../src/core/h.js"; import { isUElement } from "../src/core/schema.js"; import type { UNode } from "../src/core/schema.js"; describe("ReactiveRoot.dispose()", () => { it("prevents future effect fires from subscribe()", () => { const root = new ReactiveRoot(h("div", null, "initial")); const received: UNode[] = []; root.subscribe((n) => received.push(n)); expect(received.length).toBe(1); root.update(() => h("div", null, "second")); expect(received.length).toBe(2); root.dispose(); root.update(() => h("div", null, "third")); expect(received.length).toBe(2); }); it("prevents future effect fires from render()", () => { const events: { type: string; id: string; payload: unknown }[] = []; const root = new ReactiveRoot(h("div", null, "hello")); root.render((e) => events.push(e)); expect(events.length).toBe(1); root.dispose(); root.update(() => h("div", null, "world")); expect(events.length).toBe(1); }); it("is idempotent — safe to call twice", () => { const root = new ReactiveRoot(h("div", null, "initial")); const received: UNode[] = []; root.subscribe((n) => received.push(n)); root.dispose(); root.dispose(); root.update(() => h("div", null, "after")); expect(received.length).toBe(1); }); it("clears renderDisposer", () => { const root = new ReactiveRoot(h("div", null, "x")); const stop = root.render(() => {}); root.dispose(); stop(); }); it("iterates all subscriber disposers and calls each", () => { const root = new ReactiveRoot(h("div", null, "initial")); const received1: UNode[] = []; const received2: UNode[] = []; root.subscribe((n) => received1.push(n)); root.subscribe((n) => received2.push(n)); root.dispose(); root.update(() => h("div", null, "after")); expect(received1.length).toBe(1); expect(received2.length).toBe(1); }); }); describe("subscribe() unsubscribe removes from tracking", () => { it("unsubscribe removes disposer from tracking", () => { const root = new ReactiveRoot(h("div", null, "initial")); const received: UNode[] = []; const unsub = root.subscribe((n) => received.push(n)); expect(received.length).toBe(1); unsub(); root.update(() => h("div", null, "after-disconnect")); expect(received.length).toBe(1); }); it("unsubscribe is idempotent — safe to call twice", () => { const root = new ReactiveRoot(h("div", null, "initial")); const received: UNode[] = []; const unsub = root.subscribe((n) => received.push(n)); unsub(); unsub(); root.update(() => h("div", null, "after")); expect(received.length).toBe(1); }); it("other subscribers still fire after one unsubscribes", () => { const root = new ReactiveRoot(h("div", null, "initial")); const received1: UNode[] = []; const received2: UNode[] = []; const unsub1 = root.subscribe((n) => received1.push(n)); root.subscribe((n) => received2.push(n)); unsub1(); root.update(() => h("div", null, "updated")); expect(received1.length).toBe(1); expect(received2.length).toBe(2); }); }); describe("reactiveComponent.dispose()", () => { it("prevents future computed evaluations", () => { const MyComp = createComponent("MyComp", (props) => h("div", null, props.text as string)); const propsSignal = signal({ text: "hello" }); let evalCount = 0; const nodeSignal = computed(() => { evalCount++; return MyComp(propsSignal.value); }); const node = nodeSignal.value; expect(evalCount).toBe(1); expect(isUElement(node)).toBe(true); const reactive = reactiveComponent(MyComp, propsSignal); reactive.dispose(); propsSignal.value = { text: "world" }; expect(reactive.signal.value).toBeUndefined(); }); it("is idempotent — safe to call twice", () => { const MyComp = createComponent("MyComp", (props) => h("div", null, props.text as string)); const propsSignal = signal({ text: "hello" }); const reactive = reactiveComponent(MyComp, propsSignal); reactive.dispose(); reactive.dispose(); }); }); describe("reactiveElement.dispose()", () => { it("cleans up the underlying computed subscription", () => { const propsSignal = signal({ class: "initial" }); const childSignal = signal("child-text"); const reactive = reactiveElement("div", propsSignal, [childSignal]); expect(reactive.signal.value).toBeDefined(); reactive.dispose(); expect(reactive.signal.value).toBeUndefined(); }); it("is idempotent — safe to call twice", () => { const propsSignal = signal({}); const reactive = reactiveElement("div", propsSignal, []); reactive.dispose(); reactive.dispose(); }); }); describe("render() disposes previous render effect on re-call", () => { it("calling render() twice disposes the first effect", () => { const root = new ReactiveRoot(h("div", null, "initial")); const events1: unknown[] = []; const events2: unknown[] = []; root.render(() => events1.push(1)); expect(events1.length).toBe(1); root.render(() => events2.push(2)); expect(events2.length).toBe(1); root.update(() => h("div", null, "after")); expect(events1.length).toBe(1); expect(events2.length).toBe(2); }); });