evidence-binder/src/visual-guide/rect-registry.test.ts
tegwick a10f080f12
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Extract binder package from citation-evidence (EBIND-WP-0001)
Extracts citation-evidence/src/binder/ into this repo as the standalone
@citation-evidence/evidence-binder package: headless binding service +
in-memory link repo, active-state machine, the SharedContracts §7
rect-registry contract (registry, change pumps, hooks, SVG overlay), and
the target-neutral reference FormRenderer.

- toolchain mirrors sibling extracted repos (pnpm/tsc/vitest/eslint);
  imports rewritten from @shared/@engine aliases to the engine's
  @citation-evidence/engine package specifiers
- dependency boundary (engine + anchor only; no source/work/umbrella)
  enforced via eslint no-restricted-imports
- docs: extraction inventory + contract deltas, ADR-0001 (reference UI
  kept as supported exports), refreshed README/SCOPE/INTENT, populated
  capabilities index
- typecheck + lint green, 37 tests passing

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 01:36:33 +02:00

151 lines
4.7 KiB
TypeScript

/**
* 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" },
]),
);
});
});