| |
| |
| |
| |
| |
| |
| |
| |
| |
| import { describe, it, expect } from "vitest"; |
| import * as Y from "yjs"; |
| import { createCommentStore, type CommentData, type ReplyData } from "../src/editor/comments"; |
|
|
| function makeStore() { |
| const ydoc = new Y.Doc(); |
| return { ydoc, store: createCommentStore(ydoc) }; |
| } |
|
|
| function baseComment(overrides: Partial<Omit<CommentData, "replies">> = {}) { |
| return { |
| id: "c1", |
| author: "alice", |
| authorColor: "#f00", |
| text: "hello", |
| createdAt: 1_000, |
| resolved: false, |
| ...overrides, |
| } as Omit<CommentData, "replies">; |
| } |
|
|
| describe("comment-store — CRUD", () => { |
| it("add() stores a comment with an empty replies array", () => { |
| const { store } = makeStore(); |
| store.add(baseComment()); |
| const got = store.get("c1"); |
| expect(got).toBeDefined(); |
| expect(got!.text).toBe("hello"); |
| expect(got!.replies).toEqual([]); |
| }); |
|
|
| it("getAll() returns comments sorted by createdAt", () => { |
| const { store } = makeStore(); |
| store.add(baseComment({ id: "late", createdAt: 3000 })); |
| store.add(baseComment({ id: "early", createdAt: 1000 })); |
| store.add(baseComment({ id: "mid", createdAt: 2000 })); |
| const all = store.getAll(); |
| expect(all.map((c) => c.id)).toEqual(["early", "mid", "late"]); |
| }); |
|
|
| it("remove() deletes a comment", () => { |
| const { store } = makeStore(); |
| store.add(baseComment()); |
| store.remove("c1"); |
| expect(store.get("c1")).toBeUndefined(); |
| }); |
|
|
| it("get() on a missing id returns undefined", () => { |
| const { store } = makeStore(); |
| expect(store.get("missing")).toBeUndefined(); |
| }); |
| }); |
|
|
| describe("comment-store — replies", () => { |
| const reply: ReplyData = { |
| id: "r1", |
| author: "bob", |
| authorColor: "#00f", |
| text: "ack", |
| createdAt: 1500, |
| }; |
|
|
| it("addReply() appends to the replies array", () => { |
| const { store } = makeStore(); |
| store.add(baseComment()); |
| store.addReply("c1", reply); |
| store.addReply("c1", { ...reply, id: "r2", text: "ack2" }); |
| const got = store.get("c1")!; |
| expect(got.replies.map((r) => r.id)).toEqual(["r1", "r2"]); |
| expect(got.replies[1].text).toBe("ack2"); |
| }); |
|
|
| it("addReply() on a missing comment is a no-op", () => { |
| const { store } = makeStore(); |
| store.addReply("missing", reply); |
| expect(store.get("missing")).toBeUndefined(); |
| }); |
| }); |
|
|
| describe("comment-store — resolve/unresolve", () => { |
| it("resolve() marks the comment resolved and records the resolver", () => { |
| const { store } = makeStore(); |
| store.add(baseComment()); |
| store.resolve("c1", "carol"); |
| const got = store.get("c1")!; |
| expect(got.resolved).toBe(true); |
| expect(got.resolvedBy).toBe("carol"); |
| expect(typeof got.resolvedAt).toBe("number"); |
| }); |
|
|
| it("unresolve() clears the resolved metadata", () => { |
| const { store } = makeStore(); |
| store.add(baseComment()); |
| store.resolve("c1", "carol"); |
| store.unresolve("c1"); |
| const got = store.get("c1")!; |
| expect(got.resolved).toBe(false); |
| expect(got.resolvedBy).toBeUndefined(); |
| expect(got.resolvedAt).toBeUndefined(); |
| }); |
|
|
| it("resolve() on a missing comment is a no-op", () => { |
| const { store } = makeStore(); |
| store.resolve("missing", "carol"); |
| expect(store.get("missing")).toBeUndefined(); |
| }); |
| }); |
|
|
| describe("comment-store — observe", () => { |
| it("fires on add and remove, stops after unsubscribe", () => { |
| const { store } = makeStore(); |
| let count = 0; |
| const unsub = store.observe(() => { count++; }); |
| store.add(baseComment({ id: "a" })); |
| store.add(baseComment({ id: "b" })); |
| store.remove("a"); |
| expect(count).toBe(3); |
| unsub(); |
| store.remove("b"); |
| expect(count).toBe(3); |
| }); |
| }); |
|
|
| describe("comment-store — collaboration", () => { |
| it("concurrent adds in different docs both survive after sync", () => { |
| const docA = new Y.Doc(); |
| const docB = new Y.Doc(); |
| const a = createCommentStore(docA); |
| const b = createCommentStore(docB); |
|
|
| a.add(baseComment({ id: "from-a" })); |
| b.add(baseComment({ id: "from-b", author: "bob", authorColor: "#0f0" })); |
|
|
| Y.applyUpdate(docA, Y.encodeStateAsUpdate(docB)); |
| Y.applyUpdate(docB, Y.encodeStateAsUpdate(docA)); |
|
|
| expect(a.getAll().map((c) => c.id).sort()).toEqual(["from-a", "from-b"]); |
| expect(b.getAll().map((c) => c.id).sort()).toEqual(["from-a", "from-b"]); |
| }); |
| }); |
|
|