/** * Rect registry unit tests โ€” exercise every public surface plus the * ยง7-contract guarantees: * - register/unregister fire subscriber events * - getRect returns the live result of the registered callback * - invalidate fires a global `rect-changed` event * - version bumps on every emit * - re-registering the same (kind,id) supersedes the prior callback; * the stale unregister cleanup does not delete the new entry. */ import { describe, expect, it } from "vitest"; import { createRectRegistry, type RectRegistryEvent, } from "./rect-registry"; function fakeRect(x: number, y: number, w: number, h: number): DOMRect { // happy-dom/jsdom isn't loaded for this test โ€” synth a DOMRect-shaped // object. The registry contract only reads these properties. return { x, y, width: w, height: h, top: y, left: x, right: x + w, bottom: y + h, toJSON() { return { x, y, width: w, height: h }; }, } as DOMRect; } describe("createRectRegistry", () => { it("returns null for unknown rects", () => { const r = createRectRegistry(); expect(r.getRect("field", "missing")).toBeNull(); }); it("register/getRect roundtrip", () => { const r = createRectRegistry(); r.register("field", "f1", () => fakeRect(1, 2, 3, 4)); const rect = r.getRect("field", "f1"); expect(rect).not.toBeNull(); expect(rect!.x).toBe(1); expect(rect!.width).toBe(3); }); it("getRect reflects live callback results", () => { const r = createRectRegistry(); let xPos = 10; r.register("highlight", "h1", () => fakeRect(xPos, 0, 5, 5)); expect(r.getRect("highlight", "h1")!.x).toBe(10); xPos = 200; expect(r.getRect("highlight", "h1")!.x).toBe(200); }); it("returns null when the callback throws", () => { const r = createRectRegistry(); r.register("field", "boom", () => { throw new Error("nope"); }); expect(r.getRect("field", "boom")).toBeNull(); }); it("emits registered + unregistered events", () => { const r = createRectRegistry(); const events: RectRegistryEvent[] = []; r.subscribe((e) => events.push(e)); const unregister = r.register("evidence-card", "ev1", () => fakeRect(0, 0, 1, 1)); unregister(); expect(events).toEqual([ { type: "registered", kind: "evidence-card", id: "ev1" }, { type: "unregistered", kind: "evidence-card", id: "ev1" }, ]); }); it("invalidate emits a global rect-changed event and bumps version", () => { const r = createRectRegistry(); const events: RectRegistryEvent[] = []; r.subscribe((e) => events.push(e)); const before = r.getVersion(); r.invalidate(); expect(events).toEqual([{ type: "rect-changed" }]); expect(r.getVersion()).toBe(before + 1); }); it("re-registering the same (kind,id) supersedes; stale cleanup is a no-op", () => { const r = createRectRegistry(); const events: RectRegistryEvent[] = []; r.subscribe((e) => events.push(e)); const firstGetRect = () => fakeRect(1, 1, 1, 1); const secondGetRect = () => fakeRect(9, 9, 9, 9); const cleanup1 = r.register("highlight", "x", firstGetRect); r.register("highlight", "x", secondGetRect); // supersede // The stale cleanup must not remove the new registration. cleanup1(); expect(r.getRect("highlight", "x")!.x).toBe(9); // Two `registered` events, no `unregistered` event โ€” the second // register overwrote without an explicit unregister, and the stale // cleanup detected the (kind,id) holds a different callback. expect(events.filter((e) => e.type === "unregistered")).toHaveLength(0); expect(events.filter((e) => e.type === "registered")).toHaveLength(2); }); it("subscribe returns an unsubscribe that detaches the listener", () => { const r = createRectRegistry(); let count = 0; const off = r.subscribe(() => count++); r.invalidate(); off(); r.invalidate(); expect(count).toBe(1); }); it("listener errors do not break sibling listeners", () => { const r = createRectRegistry(); let okCount = 0; r.subscribe(() => { throw new Error("boom"); }); r.subscribe(() => { okCount++; }); r.invalidate(); expect(okCount).toBe(1); }); it("list enumerates current registrations", () => { const r = createRectRegistry(); r.register("field", "f1", () => null); r.register("evidence-card", "ev1", () => null); r.register("highlight", "h1", () => null); const list = r.list(); expect(list).toHaveLength(3); expect(list).toEqual( expect.arrayContaining([ { kind: "field", id: "f1" }, { kind: "evidence-card", id: "ev1" }, { kind: "highlight", id: "h1" }, ]), ); }); });