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>
This commit is contained in:
parent
87d4eb3006
commit
a10f080f12
36 changed files with 6643 additions and 170 deletions
151
src/visual-guide/rect-registry.test.ts
Normal file
151
src/visual-guide/rect-registry.test.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
/**
|
||||
* 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" },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue