1
2
7import { describe, expect, it } from "vitest";
8import { resolvePresenceCursors, selectionRanges } from "../presence-cursor";
9import type { Presence } from "../types";
10
11function peer(userId: string, cursor: Presence["cursor"], viewing?: boolean): Presence {
12 return {
13 userId,
14 name: `peer-${userId}`,
15 color: "blue",
16 cursor,
17 ...(viewing !== undefined ? { viewing } : {}),
18 lastSeen: 1,
19 };
20}
21
22
23function fakeLookup(missing: string[] = []) {
24 const known = new Set(["b1", "b2", "b3"]);
25 const seen: string[] = [];
26 const lookup = (blockId: string, offset: number | "end") => {
27 if (!known.has(blockId) || missing.includes(blockId)) return null;
28 seen.push(`${blockId}:${offset}`);
29 const y = blockId === "b1" ? 10 : blockId === "b2" ? 20 : 30;
30 return { x: 100, y: y + (offset === "end" ? 0 : offset) };
31 };
32 return { lookup, seen };
33}
34
35describe("resolvePresenceCursors", () => {
36 it("a peer with a resolvable caret gets a caret at the resolved point", () => {
37 const { lookup } = fakeLookup();
38 const r = resolvePresenceCursors(
39 [peer("u1", { blockId: "b1", offset: 2 })],
40 ["b1", "b2", "b3"],
41 lookup,
42 );
43 expect(r.dropped).toBe(0);
44 expect(r.cursors).toHaveLength(1);
45 expect(r.cursors[0]?.caret).toEqual({ x: 100, y: 12 });
46 });
47
48 it("null cursor (cleared / 50-peer avatars-only cap) gets NO caret", () => {
49 const { lookup } = fakeLookup();
50 const r = resolvePresenceCursors([peer("u1", null)], ["b1"], lookup);
51 expect(r.cursors).toHaveLength(0);
52 expect(r.dropped).toBe(0);
53 });
54
55 it("viewing:false (a background tab) gets NO caret (the avatar stays elsewhere)", () => {
56 const { lookup } = fakeLookup();
57 const r = resolvePresenceCursors(
58 [peer("u1", { blockId: "b1", offset: 1 }, false)],
59 ["b1"],
60 lookup,
61 );
62 expect(r.cursors).toHaveLength(0);
63 expect(r.dropped).toBe(0);
64 });
65
66 it("a stale cursor (block gone) is DROPPED silently and counted", () => {
67 const { lookup } = fakeLookup();
68 const r = resolvePresenceCursors(
69 [peer("u1", { blockId: "gone", offset: 1 }), peer("u2", { blockId: "b1", offset: 1 })],
70 ["b1"],
71 lookup,
72 );
73 expect(r.dropped).toBe(1);
74 expect(r.cursors.map((c) => c.peer.userId)).toEqual(["u2"]);
75 });
76
77 it("a single-block selection renders one rect spanning both endpoints", () => {
78 const { lookup } = fakeLookup();
79 const r = resolvePresenceCursors(
80 [
81 peer("u1", {
82 blockId: "b1",
83 offset: 4,
84 selection: { fromBlockId: "b1", fromOffset: 1, toBlockId: "b1", toOffset: 4 },
85 }),
86 ],
87 ["b1", "b2"],
88 lookup,
89 );
90 expect(r.cursors[0]?.rects).toEqual([{ x1: 100, y1: 11, x2: 100, y2: 14 }]);
91 });
92
93 it("a multi-block selection = head partial + full middles + tail partial", () => {
94 const { lookup } = fakeLookup();
95 const r = resolvePresenceCursors(
96 [
97 peer("u1", {
98 blockId: "b1",
99 offset: 1,
100 selection: { fromBlockId: "b1", fromOffset: 1, toBlockId: "b3", toOffset: 2 },
101 }),
102 ],
103 ["b1", "b2", "b3"],
104 lookup,
105 );
106
107 expect(r.cursors[0]?.rects).toHaveLength(3);
108 });
109
110 it("a selection whose endpoint block is gone drops only that range (the caret stays)", () => {
111 const { lookup } = fakeLookup(["b3"]);
112 const r = resolvePresenceCursors(
113 [
114 peer("u1", {
115 blockId: "b1",
116 offset: 1,
117 selection: { fromBlockId: "b1", fromOffset: 1, toBlockId: "b3", toOffset: 2 },
118 }),
119 ],
120 ["b1", "b2", "b3"],
121 lookup,
122 );
123 expect(r.cursors).toHaveLength(1);
124
125 expect(r.cursors[0]?.rects).toHaveLength(2);
126 });
127
128 it("a reversed selection (to before from) still resolves (order is normalized)", () => {
129
130
131
132 const ranges = selectionRanges(["b1", "b2", "b3"], {
133 fromBlockId: "b3",
134 fromOffset: 2,
135 toBlockId: "b1",
136 toOffset: 1,
137 });
138 expect(ranges).toEqual([
139 { blockId: "b1", from: 1, to: "end" },
140 { blockId: "b2", from: 0, to: "end" },
141 { blockId: "b3", from: 0, to: 2 },
142 ]);
143 });
144
145 it("a selection referencing a missing block resolves to NO ranges (stale)", () => {
146 expect(
147 selectionRanges(["b1"], {
148 fromBlockId: "b1",
149 fromOffset: 0,
150 toBlockId: "ghost",
151 toOffset: 0,
152 }),
153 ).toEqual([]);
154 });
155});
156