// @vitest-environment node /** * PresenceEmitter (TASK-0093) — pure unit tests with an injected clock + * timer (deterministic; no socket, no DOM). The wire boundary is the * `send` spy; the throttle (120 ms default) is driven by hand. */ import { describe, expect, it } from "vitest"; import { PresenceEmitter } from "../presence-emitter"; import type { OwnPresence } from "../types"; /** A fake clock + timer pair: advance time, then flush due timers. */ function makeClock(t0 = 0) { let t = t0; const queue: { at: number; fn: () => void }[] = []; return { now: () => t, advance: (ms: number) => { t += ms; const due = queue.filter((e) => e.at <= t); // Run in due-time order; a timer may schedule the next one. due.sort((a, b) => a.at - b.at); for (const e of due) e.fn(); }, setTimeout: (fn: () => void, ms: number) => { const id = { at: t + ms, fn }; queue.push(id); return id as unknown as ReturnType; }, clearTimeout: (id: unknown) => { const idx = queue.findIndex((e) => e === id); if (idx >= 0) queue.splice(idx, 1); }, pending: () => queue.length, }; } function mk(opts?: { minIntervalMs?: number }) { const sent: OwnPresence[] = []; const clock = makeClock(); const emitter = new PresenceEmitter({ send: (s) => sent.push(s), now: clock.now, minIntervalMs: opts?.minIntervalMs ?? 120, timer: { setTimeout: clock.setTimeout, clearTimeout: clock.clearTimeout }, }); return { emitter, sent, clock }; } const caret = (blockId: string, offset: number) => ({ blockId, offset }); describe("PresenceEmitter — the cursor throttle (doc §5.2, < 500 ms budget)", () => { it("the first caret is sent immediately (no waiting for the throttle window)", () => { const { emitter, sent } = mk(); emitter.setCaret(caret("blk_1", 3)); expect(sent).toEqual([{ cursor: caret("blk_1", 3), viewing: true }]); }); it("caret moves within 120 ms are coalesced; the FINAL position is sent (trailing flush)", () => { const { emitter, sent, clock } = mk(); emitter.setCaret(caret("blk_1", 3)); // t=0: immediate emitter.setCaret(caret("blk_1", 5)); // t=0: queued (within the window) emitter.setCaret(caret("blk_1", 7)); // t=0: still queued expect(sent).toHaveLength(1); expect(clock.pending()).toBe(1); clock.advance(119); // not due yet expect(sent).toHaveLength(1); clock.advance(1); // t=120: the trailing flush fires expect(sent).toHaveLength(2); expect(sent[1]).toEqual({ cursor: caret("blk_1", 7), viewing: true }); // the final position }); it("moves spaced ≥ 120 ms apart each send immediately", () => { const { emitter, sent, clock } = mk(); emitter.setCaret(caret("blk_1", 3)); // t=0 clock.advance(120); emitter.setCaret(caret("blk_1", 6)); // t=120: elapsed → immediate expect(sent).toHaveLength(2); expect(sent[1]?.cursor).toEqual(caret("blk_1", 6)); expect(clock.pending()).toBe(0); }); it("an unchanged caret sends nothing", () => { const { emitter, sent } = mk(); emitter.setCaret(caret("blk_1", 3)); emitter.setCaret(caret("blk_1", 3)); expect(sent).toHaveLength(1); }); it("an immediate (viewing) send cancels the pending cursor flush — the position is delivered once, via the viewing frame", () => { const { emitter, sent, clock } = mk(); emitter.setCaret(caret("blk_1", 3)); // t=0: immediate emitter.setCaret(caret("blk_1", 5)); // t=0: queued (flush due at t=120) emitter.setViewing(false); // t=0: immediate — sends the current state (caret 5) and cancels the flush expect(sent).toHaveLength(2); expect(sent.at(-1)).toEqual({ cursor: caret("blk_1", 5), viewing: false }); expect(clock.pending()).toBe(0); clock.advance(10_000); // the cancelled flush must NOT fire a duplicate later expect(sent).toHaveLength(2); }); it("dispose cancels a pending trailing flush", () => { const { emitter, sent, clock } = mk(); emitter.setCaret(caret("blk_1", 3)); emitter.setCaret(caret("blk_1", 5)); // queued emitter.dispose(); clock.advance(10_000); expect(sent).toHaveLength(1); }); }); describe("PresenceEmitter — cursor clear + viewing (never throttled)", () => { it("cursor: null (focus left the page) is sent immediately — clears are discrete, not streamed", () => { const { emitter, sent, clock } = mk(); emitter.setCaret(caret("blk_1", 3)); // t=0: immediate emitter.setCaret(null); // t=0: a clear must not wait for the trailing flush expect(sent).toHaveLength(2); expect(sent.at(-1)).toEqual({ cursor: null, viewing: true }); expect(clock.pending()).toBe(0); }); it("viewing=false (tab hidden) is sent immediately, cursor hidden state kept", () => { const { emitter, sent } = mk(); emitter.setCaret(caret("blk_1", 3)); emitter.setViewing(false); expect(sent.at(-1)).toEqual({ cursor: caret("blk_1", 3), viewing: false }); }); it("viewing=true (tab shown again) is sent immediately", () => { const { emitter, sent } = mk(); emitter.setCaret(caret("blk_1", 3)); emitter.setViewing(false); emitter.setViewing(true); expect(sent.at(-1)).toEqual({ cursor: caret("blk_1", 3), viewing: true }); }); it("viewing changes during a pending cursor flush do not delay the flush (and vice versa)", () => { const { emitter, sent, clock } = mk(); emitter.setCaret(caret("blk_1", 3)); // t=0 emitter.setViewing(false); // t=0: immediate emitter.setCaret(caret("blk_1", 5)); // t=0: queued (window from t=0) expect(sent).toHaveLength(2); clock.advance(120); expect(sent).toHaveLength(3); expect(sent.at(-1)).toEqual({ cursor: caret("blk_1", 5), viewing: false }); }); }); describe("PresenceEmitter — the wire payload (identity is server-stamped)", () => { it("frames carry ONLY cursor + viewing (the wire schema forbids identity)", () => { const { emitter, sent } = mk(); emitter.setCaret({ blockId: "blk_1", offset: 2, selection: { fromBlockId: "blk_1", fromOffset: 0, toBlockId: "blk_1", toOffset: 2 }, }); const last = sent.at(-1); expect(last).toBeDefined(); expect([...Object.keys(last ?? {}).sort()]).toEqual(["cursor", "viewing"]); }); it("a multi-block selection (selection inside this block's doc) is carried verbatim", () => { const { emitter, sent } = mk(); const sel = { fromBlockId: "blk_9", fromOffset: 1, toBlockId: "blk_9", toOffset: 9 }; emitter.setCaret({ blockId: "blk_9", offset: 9, selection: sel }); expect(sent.at(-1)?.cursor?.selection).toEqual(sel); }); }); describe("PresenceEmitter — assert() (the reconnect re-send)", () => { it("assert() re-sends the current state immediately (a (re)connect re-asserts)", () => { const { emitter, sent, clock } = mk(); emitter.setCaret(caret("blk_1", 3)); // t=0: immediate clock.advance(120); emitter.setCaret(caret("blk_1", 8)); // t=120: immediate expect(sent).toHaveLength(2); emitter.assert(); // a reconnect: the FULL state is re-sent (not a no-op) expect(sent).toHaveLength(3); expect(sent.at(-1)).toEqual({ cursor: caret("blk_1", 8), viewing: true }); }); it("assert() on a never-moved caret still sends (the initial avatar frame)", () => { const { emitter, sent } = mk(); emitter.assert(); expect(sent).toEqual([{ cursor: null, viewing: true }]); }); it("assert() cancels a pending trailing flush (the re-sent state supersedes it)", () => { const { emitter, sent, clock } = mk(); emitter.setCaret(caret("blk_1", 3)); // t=0: immediate emitter.setCaret(caret("blk_1", 5)); // t=0: queued (flush due t=120) emitter.assert(); // t=0: re-sends the current state (caret 5), cancels the flush expect(sent).toHaveLength(2); expect(sent.at(-1)).toEqual({ cursor: caret("blk_1", 5), viewing: true }); expect(clock.pending()).toBe(0); clock.advance(10_000); // no duplicate later expect(sent).toHaveLength(2); }); });