Skip to content

Commit f0f9395

Browse files
committed
✨(frontend) add comments feature
Implemented the comments feature for the document editor. We are now able to add, view, and manage comments within the document editor interface.
1 parent c5cb886 commit f0f9395

File tree

17 files changed

+1360
-40
lines changed

17 files changed

+1360
-40
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to
1111
- ✨(frontend) create skeleton component for DocEditor #1491
1212
- ✨(frontend) add an EmojiPicker in the document tree and title #1381
1313
- ✨(frontend) ajustable left panel #1456
14+
- ✨ Add comments feature to the editor #1330
1415

1516
### Changed
1617

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
import { createDoc, getOtherBrowserName, verifyDocName } from './utils-common';
4+
import { writeInEditor } from './utils-editor';
5+
import {
6+
addNewMember,
7+
connectOtherUserToDoc,
8+
updateRoleUser,
9+
updateShareLink,
10+
} from './utils-share';
11+
12+
test.beforeEach(async ({ page }) => {
13+
await page.goto('/');
14+
});
15+
16+
test.describe('Doc Comments', () => {
17+
test('it checks comments with 2 users in real time', async ({
18+
page,
19+
browserName,
20+
}) => {
21+
const [docTitle] = await createDoc(page, 'comment-doc', browserName, 1);
22+
23+
// We share the doc with another user
24+
const otherBrowserName = getOtherBrowserName(browserName);
25+
await page.getByRole('button', { name: 'Share' }).click();
26+
await addNewMember(page, 0, 'Administrator', otherBrowserName);
27+
28+
await expect(
29+
page
30+
.getByRole('listbox', { name: 'Suggestions' })
31+
.getByText(new RegExp(otherBrowserName)),
32+
).toBeVisible();
33+
34+
await page.getByRole('button', { name: 'close' }).click();
35+
36+
// We add a comment with the first user
37+
const editor = await writeInEditor({ page, text: 'Hello World' });
38+
await editor.getByText('Hello').selectText();
39+
await page.getByRole('button', { name: 'Add comment' }).click();
40+
41+
const thread = page.locator('.bn-thread');
42+
await thread.getByRole('paragraph').first().fill('This is a comment');
43+
await thread.locator('[data-test="save"]').click();
44+
await expect(thread.getByText('This is a comment').first()).toBeHidden();
45+
46+
await editor.getByText('Hello').click();
47+
48+
await thread.getByText('This is a comment').first().hover();
49+
50+
// We add a reaction with the first user
51+
await thread.locator('[data-test="addreaction"]').first().click();
52+
await thread.getByRole('button', { name: '👍' }).click();
53+
54+
await expect(thread.getByText('This is a comment').first()).toBeVisible();
55+
await expect(thread.getByText(`E2E ${browserName}`).first()).toBeVisible();
56+
await expect(thread.locator('.bn-comment-reaction')).toHaveText('👍1');
57+
58+
const urlCommentDoc = page.url();
59+
60+
const { otherPage, cleanup } = await connectOtherUserToDoc({
61+
otherBrowserName,
62+
docUrl: urlCommentDoc,
63+
docTitle,
64+
});
65+
66+
const otherEditor = otherPage.locator('.ProseMirror');
67+
await otherEditor.getByText('Hello').click();
68+
const otherThread = otherPage.locator('.bn-thread');
69+
70+
await otherThread.getByText('This is a comment').first().hover();
71+
await otherThread.locator('[data-test="addreaction"]').first().click();
72+
await otherThread.getByRole('button', { name: '👍' }).click();
73+
74+
// We check that the comment made by the first user is visible for the second user
75+
await expect(
76+
otherThread.getByText('This is a comment').first(),
77+
).toBeVisible();
78+
await expect(
79+
otherThread.getByText(`E2E ${browserName}`).first(),
80+
).toBeVisible();
81+
await expect(otherThread.locator('.bn-comment-reaction')).toHaveText('👍2');
82+
83+
// We add a comment with the second user
84+
await otherThread
85+
.getByRole('paragraph')
86+
.last()
87+
.fill('This is a comment from the other user');
88+
await otherThread.locator('[data-test="save"]').click();
89+
90+
// We check that the second user can see the comment he just made
91+
await expect(
92+
otherThread.getByText('This is a comment from the other user').first(),
93+
).toBeVisible();
94+
await expect(
95+
otherThread.getByText(`E2E ${otherBrowserName}`).first(),
96+
).toBeVisible();
97+
98+
// We check that the first user can see the comment made by the second user in real time
99+
await expect(
100+
thread.getByText('This is a comment from the other user').first(),
101+
).toBeVisible();
102+
await expect(
103+
thread.getByText(`E2E ${otherBrowserName}`).first(),
104+
).toBeVisible();
105+
106+
await cleanup();
107+
});
108+
109+
test('it checks the comments interactions', async ({ page, browserName }) => {
110+
await createDoc(page, 'comment-interaction', browserName, 1);
111+
112+
// Checks add react reaction
113+
const editor = page.locator('.ProseMirror');
114+
await editor.locator('.bn-block-outer').last().fill('Hello World');
115+
await editor.getByText('Hello').selectText();
116+
await page.getByRole('button', { name: 'Add comment' }).click();
117+
118+
const thread = page.locator('.bn-thread');
119+
await thread.getByRole('paragraph').first().fill('This is a comment');
120+
await thread.locator('[data-test="save"]').click();
121+
await expect(thread.getByText('This is a comment').first()).toBeHidden();
122+
123+
// Check background color changed
124+
await expect(editor.getByText('Hello')).toHaveCSS(
125+
'background-color',
126+
'rgba(237, 180, 0, 0.4)',
127+
);
128+
await editor.getByText('Hello').click();
129+
130+
await thread.getByText('This is a comment').first().hover();
131+
132+
// We add a reaction with the first user
133+
await thread.locator('[data-test="addreaction"]').first().click();
134+
await thread.getByRole('button', { name: '👍' }).click();
135+
136+
await expect(thread.locator('.bn-comment-reaction')).toHaveText('👍1');
137+
138+
// Edit Comment
139+
await thread.getByText('This is a comment').first().hover();
140+
await thread.locator('[data-test="moreactions"]').first().click();
141+
await thread.getByRole('menuitem', { name: 'Edit comment' }).click();
142+
const commentEditor = thread.getByText('This is a comment').first();
143+
await commentEditor.fill('This is an edited comment');
144+
const saveBtn = thread.getByRole('button', { name: 'Save' });
145+
await saveBtn.click();
146+
await expect(saveBtn).toBeHidden();
147+
await expect(
148+
thread.getByText('This is an edited comment').first(),
149+
).toBeVisible();
150+
await expect(thread.getByText('This is a comment').first()).toBeHidden();
151+
152+
// Add second comment
153+
await thread.getByRole('paragraph').last().fill('This is a second comment');
154+
await thread.getByRole('button', { name: 'Save' }).click();
155+
await expect(
156+
thread.getByText('This is an edited comment').first(),
157+
).toBeVisible();
158+
await expect(
159+
thread.getByText('This is a second comment').first(),
160+
).toBeVisible();
161+
162+
// Delete second comment
163+
await thread.getByText('This is a second comment').first().hover();
164+
await thread.locator('[data-test="moreactions"]').first().click();
165+
await thread.getByRole('menuitem', { name: 'Delete comment' }).click();
166+
await expect(
167+
thread.getByText('This is a second comment').first(),
168+
).toBeHidden();
169+
170+
// Resolve thread
171+
await thread.getByText('This is an edited comment').first().hover();
172+
await thread.locator('[data-test="resolve"]').click();
173+
await expect(thread).toBeHidden();
174+
await expect(editor.getByText('Hello')).toHaveCSS(
175+
'background-color',
176+
'rgba(0, 0, 0, 0)',
177+
);
178+
});
179+
180+
test('it checks the comments abilities', async ({ page, browserName }) => {
181+
test.slow();
182+
183+
const [docTitle] = await createDoc(page, 'comment-doc', browserName, 1);
184+
185+
// We share the doc with another user
186+
const otherBrowserName = getOtherBrowserName(browserName);
187+
188+
// Add a new member with editor role
189+
await page.getByRole('button', { name: 'Share' }).click();
190+
await addNewMember(page, 0, 'Editor', otherBrowserName);
191+
192+
await expect(
193+
page
194+
.getByRole('listbox', { name: 'Suggestions' })
195+
.getByText(new RegExp(otherBrowserName)),
196+
).toBeVisible();
197+
198+
const urlCommentDoc = page.url();
199+
200+
const { otherPage, cleanup } = await connectOtherUserToDoc({
201+
otherBrowserName,
202+
docUrl: urlCommentDoc,
203+
docTitle,
204+
});
205+
206+
const otherEditor = await writeInEditor({
207+
page: otherPage,
208+
text: 'Hello, I can edit the document',
209+
});
210+
await expect(
211+
otherEditor.getByText('Hello, I can edit the document'),
212+
).toBeVisible();
213+
await otherEditor.getByText('Hello').selectText();
214+
await otherPage.getByRole('button', { name: 'Comment' }).click();
215+
const otherThread = otherPage.locator('.bn-thread');
216+
await otherThread
217+
.getByRole('paragraph')
218+
.first()
219+
.fill('I can add a comment');
220+
await otherThread.locator('[data-test="save"]').click();
221+
await expect(
222+
otherThread.getByText('I can add a comment').first(),
223+
).toBeHidden();
224+
225+
await expect(otherEditor.getByText('Hello')).toHaveCSS(
226+
'background-color',
227+
'rgba(237, 180, 0, 0.4)',
228+
);
229+
230+
// We change the role of the second user to reader
231+
await updateRoleUser(page, 'Reader', `user.test@${otherBrowserName}.test`);
232+
233+
// With the reader role, the second user cannot see comments
234+
await otherPage.reload();
235+
await verifyDocName(otherPage, docTitle);
236+
237+
await expect(otherEditor.getByText('Hello')).toHaveCSS(
238+
'background-color',
239+
'rgba(0, 0, 0, 0)',
240+
);
241+
await otherEditor.getByText('Hello').click();
242+
await expect(otherThread).toBeHidden();
243+
await otherEditor.getByText('Hello').selectText();
244+
await expect(
245+
otherPage.getByRole('button', { name: 'Comment' }),
246+
).toBeHidden();
247+
248+
await otherPage.reload();
249+
250+
// Change the link role of the doc to set it in commenting mode
251+
await updateShareLink(page, 'Public', 'Editing');
252+
253+
// Anonymous user can see and add comments
254+
await otherPage.getByRole('button', { name: 'Logout' }).click();
255+
256+
await otherPage.goto(urlCommentDoc);
257+
258+
await verifyDocName(otherPage, docTitle);
259+
260+
await expect(otherEditor.getByText('Hello')).toHaveCSS(
261+
'background-color',
262+
'rgba(237, 180, 0, 0.4)',
263+
);
264+
await otherEditor.getByText('Hello').click();
265+
await expect(
266+
otherThread.getByText('I can add a comment').first(),
267+
).toBeVisible();
268+
269+
await otherThread
270+
.locator('.ProseMirror.bn-editor[contenteditable="true"]')
271+
.getByRole('paragraph')
272+
.first()
273+
.fill('Comment by anonymous user');
274+
await otherThread.locator('[data-test="save"]').click();
275+
276+
await expect(
277+
otherThread.getByText('Comment by anonymous user').first(),
278+
).toBeVisible();
279+
280+
await expect(
281+
otherThread.getByRole('img', { name: `Anonymous` }).first(),
282+
).toBeVisible();
283+
284+
await otherThread.getByText('Comment by anonymous user').first().hover();
285+
await expect(otherThread.locator('[data-test="moreactions"]')).toBeHidden();
286+
287+
await cleanup();
288+
});
289+
});

src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ export const keyCloakSignIn = async (
7070
await page.click('button[type="submit"]', { force: true });
7171
};
7272

73+
export const getOtherBrowserName = (browserName: BrowserName) => {
74+
const otherBrowserName = BROWSERS.find((b) => b !== browserName);
75+
if (!otherBrowserName) {
76+
throw new Error('No alternative browser found');
77+
}
78+
return otherBrowserName;
79+
};
80+
7381
export const randomName = (name: string, browserName: string, length: number) =>
7482
Array.from({ length }, (_el, index) => {
7583
return `${browserName}-${Math.floor(Math.random() * 10000)}-${index}-${name}`;
@@ -125,7 +133,9 @@ export const verifyDocName = async (page: Page, docName: string) => {
125133
try {
126134
await expect(
127135
page.getByRole('textbox', { name: 'Document title' }),
128-
).toContainText(docName);
136+
).toContainText(docName, {
137+
timeout: 1000,
138+
});
129139
} catch {
130140
await expect(page.getByRole('heading', { name: docName })).toBeVisible();
131141
}

src/frontend/apps/e2e/__tests__/app-impress/utils-share.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Page, chromium, expect } from '@playwright/test';
22

33
import {
4-
BROWSERS,
54
BrowserName,
5+
getOtherBrowserName,
66
keyCloakSignIn,
77
verifyDocName,
88
} from './utils-common';
@@ -14,7 +14,7 @@ export type LinkRole = 'Reading' | 'Editing';
1414
export const addNewMember = async (
1515
page: Page,
1616
index: number,
17-
role: 'Administrator' | 'Owner' | 'Editor' | 'Reader',
17+
role: Role,
1818
fillText = 'user.test',
1919
) => {
2020
const responsePromiseSearchUser = page.waitForResponse(
@@ -88,21 +88,30 @@ export const updateRoleUser = async (
8888
* @param docTitle The title of the document (optional).
8989
* @returns An object containing the other browser, context, and page.
9090
*/
91+
type ConnectOtherUserToDocParams = {
92+
docUrl: string;
93+
docTitle?: string;
94+
withoutSignIn?: boolean;
95+
} & (
96+
| {
97+
otherBrowserName: BrowserName;
98+
browserName?: never;
99+
}
100+
| {
101+
browserName: BrowserName;
102+
otherBrowserName?: never;
103+
}
104+
);
105+
91106
export const connectOtherUserToDoc = async ({
92107
browserName,
93108
docUrl,
94109
docTitle,
110+
otherBrowserName: _otherBrowserName,
95111
withoutSignIn,
96-
}: {
97-
browserName: BrowserName;
98-
docUrl: string;
99-
docTitle?: string;
100-
withoutSignIn?: boolean;
101-
}) => {
102-
const otherBrowserName = BROWSERS.find((b) => b !== browserName);
103-
if (!otherBrowserName) {
104-
throw new Error('No alternative browser found');
105-
}
112+
}: ConnectOtherUserToDocParams) => {
113+
const otherBrowserName =
114+
_otherBrowserName || getOtherBrowserName(browserName);
106115

107116
const otherBrowser = await chromium.launch({ headless: true });
108117
const otherContext = await otherBrowser.newContext({

src/frontend/apps/impress/src/features/auth/api/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ export interface User {
1313
short_name: string;
1414
language?: string;
1515
}
16+
17+
export type UserLight = Pick<User, 'full_name' | 'short_name'>;

0 commit comments

Comments
 (0)