| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import * as Y from "yjs"; |
| 5 | |
| 6 | import { applyThreadAction, bodyMentions, bodyText, listThreads } from "./threads.ts"; |
| 7 | |
| 8 | const 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 | |
| 16 | test("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 | |
| 30 | test("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 | |
| 45 | test("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 | |
| 69 | test("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 | }); |