1
20import { test, expect } from "../fixtures";
21import { createComment, replyToComment, resolveComment } from "../utils/api";
22
23test.describe("Comments — Phase 8", () => {
24 test("select text → comment popup → create comment", async ({
25 authPage,
26 api,
27 freshWorkspace,
28 }) => {
29 const ws = await freshWorkspace("Comment Space");
30 const pgRes = await api.post(`/api/v1/workspaces/${ws.id}/pages`, {
31 data: { title: "Commentable" },
32 });
33 const pageId = ((await pgRes.json()) as { page: { id: string } }).page.id;
34
35 await api.post(`/api/v1/pages/${pageId}/blocks`, {
36 data: { type: "PARAGRAPH", content: { text: "Select me for a comment" } },
37 });
38
39 await authPage.goto(`/w/${ws.id}/page/${pageId}`);
40 await expect(authPage.getByTestId("editor-root")).toBeVisible({ timeout: 20_000 });
41
42
43 const block = authPage.locator('[data-testid^="block-"]').first();
44 await block.dblclick();
45
46
47 await authPage.keyboard.press("Shift+End");
48 await authPage.getByTestId("comment-popup").waitFor({ timeout: 10_000 });
49 await authPage.getByTestId("comment-input").fill("Does this need a source?");
50 await authPage.getByTestId("comment-submit").click();
51
52
53 const thread = authPage.getByTestId("comment-thread").first();
54 await expect(thread).toBeVisible({ timeout: 10_000 });
55 await expect(thread).toContainText("Does this need a source?");
56 });
57
58 test("reply to a comment", async ({ authPage, api, freshWorkspace }) => {
59 const ws = await freshWorkspace("Reply Space");
60 const pgRes = await api.post(`/api/v1/workspaces/${ws.id}/pages`, {
61 data: { title: "Reply Page" },
62 });
63 const pageId = ((await pgRes.json()) as { page: { id: string } }).page.id;
64
65
66 const comment = await createComment(authPage, pageId, "Top-level thought");
67 await authPage.goto(`/w/${ws.id}/page/${pageId}`);
68 await expect(authPage.getByTestId("editor-root")).toBeVisible({ timeout: 20_000 });
69
70
71 const thread = authPage.getByTestId(`comment-thread-${comment.id}`);
72 await thread.click();
73 await authPage.getByTestId("comment-reply-input").fill("Agreed — adding it.");
74 await authPage.getByTestId("comment-reply-submit").click();
75
76 await expect(
77 authPage.getByTestId(`comment-replies-${comment.id}`),
78 ).toContainText("Agreed — adding it.", { timeout: 10_000 });
79 });
80
81 test("resolve a comment thread", async ({ authPage, api, freshWorkspace }) => {
82 const ws = await freshWorkspace("Resolve Space");
83 const pgRes = await api.post(`/api/v1/workspaces/${ws.id}/pages`, {
84 data: { title: "Resolve Page" },
85 });
86 const pageId = ((await pgRes.json()) as { page: { id: string } }).page.id;
87 const comment = await createComment(authPage, pageId, "Resolved soon");
88
89 await authPage.goto(`/w/${ws.id}/page/${pageId}`);
90 await expect(authPage.getByTestId("editor-root")).toBeVisible({ timeout: 20_000 });
91 const thread = authPage.getByTestId(`comment-thread-${comment.id}`);
92 await thread.click();
93 await authPage.getByTestId(`comment-resolve-${comment.id}`).click();
94
95
96 await expect
97 .poll(async () => {
98 const resolved = await authPage
99 .getByTestId("comments-resolved-section")
100 .getByText("Resolved soon")
101 .count()
102 .catch(() => 0);
103 const open = await authPage
104 .getByTestId("comment-thread")
105 .filter({ hasText: "Resolved soon" })
106 .count()
107 .catch(() => 0);
108 return resolved > 0 ? "resolved" : open === 0 ? "hidden" : "still-open";
109 }, { timeout: 10_000 })
110 .not.toBe("still-open");
111
112
113 const after = await resolveComment(authPage, comment.id).catch(() => null);
114 if (after) expect(after.resolvedAt).toBeTruthy();
115 });
116
117 test("@mention shows a badge and notifies the user", async ({
118 authPage,
119 api,
120 secondUser,
121 freshWorkspace,
122 }) => {
123 const ws = await freshWorkspace("Mention Space");
124 const { user: mentioned } = await secondUser();
125 const pgRes = await api.post(`/api/v1/workspaces/${ws.id}/pages`, {
126 data: { title: "Mention Page" },
127 });
128 const pageId = ((await pgRes.json()) as { page: { id: string } }).page.id;
129
130
131
132 const inviteRes = await api.post(`/api/v1/workspaces/${ws.id}/invitations`, {
133 data: { email: mentioned.email, role: "MEMBER" },
134 });
135 const invite = (await inviteRes.json()) as { invitation: { token: string } };
136 const { api: secondApi } = await secondUser();
137 await secondApi.post(`/api/v1/invitations/${invite.invitation.token}/accept`, {
138 data: {},
139 });
140
141
142
143 await authPage.goto(`/w/${ws.id}/page/${pageId}`);
144 await expect(authPage.getByTestId("editor-root")).toBeVisible({ timeout: 20_000 });
145 const block = authPage.locator('[data-testid^="block-"]').first();
146 await block.dblclick();
147 await authPage.keyboard.press("Shift+End");
148 await authPage.getByTestId("comment-popup").waitFor({ timeout: 10_000 });
149 const input = authPage.getByTestId("comment-input");
150 await input.type("@");
151
152 const mentionPicker = authPage.getByTestId("mention-picker");
153 await expect(mentionPicker).toBeVisible({ timeout: 5_000 });
154 await mentionPicker.getByText(mentioned.email).first().click();
155 await input.type(" — ping!");
156 await authPage.getByTestId("comment-submit").click();
157
158
159 const thread = authPage.getByTestId("comment-thread").first();
160 await expect(thread.getByTestId("mention-badge").first()).toBeVisible({
161 timeout: 10_000,
162 });
163 });
164
165 test("second user sees the comment in real time (skip: realtime lands in Phase 7)", async ({
166 authPage,
167 api,
168 secondUser,
169 freshWorkspace,
170 }) => {
171
172
173
174 test.skip(true, "realtime comment sync lands in Phase 7");
175
176
177 });
178
179 test("reactions on a comment", async ({ authPage, api, freshWorkspace }) => {
180 const ws = await freshWorkspace("React Space");
181 const pgRes = await api.post(`/api/v1/workspaces/${ws.id}/pages`, {
182 data: { title: "React Page" },
183 });
184 const pageId = ((await pgRes.json()) as { page: { id: string } }).page.id;
185 const comment = await createComment(authPage, pageId, "React to me");
186
187 await authPage.goto(`/w/${ws.id}/page/${pageId}`);
188 await expect(authPage.getByTestId("editor-root")).toBeVisible({ timeout: 20_000 });
189 const thread = authPage.getByTestId(`comment-thread-${comment.id}`);
190 await thread.click();
191
192
193 const commentEl = authPage.getByTestId(`comment-${comment.id}`);
194 await commentEl.hover();
195 await authPage.getByTestId("reaction-bar").getByTestId("reaction-add").click();
196 await authPage.getByTestId("reaction-picker").getByTestId("reaction-emoji-like").click();
197
198
199 await expect(commentEl.getByText("👍").first()).toBeVisible({ timeout: 5_000 });
200
201 await commentEl.hover();
202 await authPage.getByTestId("reaction-bar").getByTestId("reaction-add").click();
203 await authPage.getByTestId("reaction-picker").getByTestId("reaction-emoji-like").click();
204 await expect(commentEl.getByText("👍")).toHaveCount(0);
205 });
206});
207