Skip to content
72 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import * as Y from "yjs";
5
6import { applyThreadAction, bodyMentions, bodyText, listThreads } from "./threads.ts";
7
8const body = (text: string, mention?: { kind: string; id: string; name: string }) => [
9 {
10 type: "paragraph",
11 content: [{ type: "text", text, styles: {} }, ...(mention ? [{ type: "mention", props: mention }] : [])],
12 children: [],
13 },
14];
15
16test("threads are kept in the shape the editor reads", () => {
17 const doc = new Y.Doc();
18 const made = applyThreadAction(doc, "user:u1", "comment", { op: "create", body: body("Is this right? ", { kind: "user", id: "u2", name: "bo" }) }, 1000);
19 assert.equal(made.ok, true);
20 if (!made.ok) return;
21 assert.deepEqual(made.mentions, ["user:u2"]);
22 const thread = doc.getMap<Y.Map<unknown>>("threads").get(made.thread_id!)!;
23 assert.equal(thread.get("resolved"), false);
24 const comment = (thread.get("comments") as Y.Array<Y.Map<unknown>>).get(0);
25 assert.equal(comment.get("userId"), "user:u1");
26 assert.equal(comment.get("createdAt"), 1000);
27 assert.ok(comment.get("reactionsByUser") instanceof Y.Map);
28});
29
30test("viewers can't comment; only authors edit; editors delete others' comments and threads", () => {
31 const doc = new Y.Doc();
32 assert.equal(applyThreadAction(doc, "user:u1", "view", { op: "create", body: body("x") }).ok, false);
33 const made = applyThreadAction(doc, "user:u1", "comment", { op: "create", body: body("x") });
34 assert.ok(made.ok);
35 const threadId = made.ok ? made.thread_id! : "";
36 const commentId = String(((doc.getMap<Y.Map<unknown>>("threads").get(threadId)!.get("comments") as Y.Array<Y.Map<unknown>>).get(0)).get("id"));
37 assert.equal(applyThreadAction(doc, "user:u2", "edit", { op: "edit_comment", thread_id: threadId, comment_id: commentId, body: body("y") }).ok, false);
38 assert.equal(applyThreadAction(doc, "user:u1", "comment", { op: "edit_comment", thread_id: threadId, comment_id: commentId, body: body("y") }).ok, true);
39 assert.equal(applyThreadAction(doc, "user:u2", "comment", { op: "delete_comment", thread_id: threadId, comment_id: commentId }).ok, false);
40 assert.equal(applyThreadAction(doc, "user:u2", "comment", { op: "delete_thread", thread_id: threadId }).ok, false);
41 assert.equal(applyThreadAction(doc, "user:u2", "edit", { op: "delete_thread", thread_id: threadId }).ok, true);
42 assert.equal(doc.getMap("threads").size, 0);
43});
44
45test("replies, resolving and reactions", () => {
46 const doc = new Y.Doc();
47 const made = applyThreadAction(doc, "user:u1", "comment", { op: "create", body: body("first"), page_level: true });
48 const threadId = made.ok ? made.thread_id! : "";
49 const reply = applyThreadAction(doc, "agent:a1", "comment", { op: "comment", thread_id: threadId, body: body("an answer") });
50 assert.ok(reply.ok);
51 const replyId = reply.ok ? String((reply.value as { id: string }).id) : "";
52 applyThreadAction(doc, "user:u2", "comment", { op: "react", thread_id: threadId, comment_id: replyId, emoji: "👍" });
53 applyThreadAction(doc, "user:u2", "comment", { op: "react", thread_id: threadId, comment_id: replyId, emoji: "👍" });
54 applyThreadAction(doc, "user:u1", "comment", { op: "resolve", thread_id: threadId });
55 const [listed] = listThreads(doc);
56 assert.equal(listed!.resolved, true);
57 assert.equal(listed!.quote, null);
58 assert.deepEqual(
59 listed!.comments.map((c) => [c.author, c.text]),
60 [
61 ["user:u1", "first"],
62 ["agent:a1", "an answer"],
63 ],
64 );
65 const reactions = ((doc.getMap<Y.Map<unknown>>("threads").get(threadId)!.get("comments") as Y.Array<Y.Map<unknown>>).get(1).get("reactionsByUser") as Y.Map<unknown>);
66 assert.equal(reactions.size, 1);
67});
68
69test("a comment's text and mentions come from its blocks", () => {
70 assert.equal(bodyText(body("hi ", { kind: "agent", id: "a1", name: "inky" })), "hi @inky");
71 assert.deepEqual(bodyMentions(body("x", { kind: "page", id: "p", name: "Plan" })), []);
72});