Skip to content

Commit dea792e

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 0f47174 commit dea792e

File tree

15 files changed

+1081
-27
lines changed

15 files changed

+1081
-27
lines changed

CHANGELOG.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to
99
### Added
1010

1111
- ✨(frontend) add pdf block to the editor #1293
12+
- ✨ Add comments feature to the editor #1330
1213

1314
### Changed
1415

@@ -25,6 +26,7 @@ and this project adheres to
2526
- ♿ remove redundant aria-label on hidden icons and update tests #1432
2627
- ♿ improve semantic structure and aria roles of leftpanel #1431
2728
- ♿ add default background to left panel for better accessibility #1423
29+
- ♿improve NVDA navigation in DocShareModal #1396
2830

2931
### Fixed
3032

@@ -34,11 +36,6 @@ and this project adheres to
3436
- 🐛(frontend) fix legacy role computation #1376
3537
- 🐛(frontend) scroll back to top when navigate to a document #1406
3638

37-
### Changed
38-
39-
- ♿(frontend) improve accessibility:
40-
- ♿improve NVDA navigation in DocShareModal #1396
41-
4239
## [3.7.0] - 2025-09-12
4340

4441
### Added
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
import { createDoc, getOtherBrowserName } from './utils-common';
4+
import { writeInEditor } from './utils-editor';
5+
import { addNewMember, connectOtherUserToDoc } from './utils-share';
6+
7+
test.beforeEach(async ({ page }) => {
8+
await page.goto('/');
9+
});
10+
11+
test.describe('Doc Comments', () => {
12+
test('it checks comments with 2 users in real time', async ({
13+
page,
14+
browserName,
15+
}) => {
16+
const [docTitle] = await createDoc(page, 'comment-doc', browserName, 1);
17+
18+
// We share the doc with another user
19+
const otherBrowserName = getOtherBrowserName(browserName);
20+
await page.getByRole('button', { name: 'Share' }).click();
21+
await addNewMember(page, 0, 'Administrator', otherBrowserName);
22+
23+
await expect(
24+
page
25+
.getByRole('listbox', { name: 'Suggestions' })
26+
.getByText(new RegExp(otherBrowserName)),
27+
).toBeVisible();
28+
29+
await page.getByRole('button', { name: 'close' }).click();
30+
31+
// We add a comment with the first user
32+
const editor = await writeInEditor({ page, text: 'Hello World' });
33+
await editor.getByText('Hello').selectText();
34+
await page.getByRole('button', { name: 'Add comment' }).click();
35+
36+
const thread = page.locator('.bn-thread');
37+
await thread.getByRole('paragraph').first().fill('This is a comment');
38+
await thread.locator('[data-test="save"]').click();
39+
await editor.getByText('Hello').click();
40+
41+
await thread.getByText('This is a comment').first().hover();
42+
43+
// We add a reaction with the first user
44+
await thread.locator('[data-test="addreaction"]').first().click();
45+
await thread.getByRole('button', { name: '👍' }).click();
46+
47+
await expect(thread.getByText('This is a comment').first()).toBeVisible();
48+
await expect(thread.getByText(`E2E ${browserName}`).first()).toBeVisible();
49+
await expect(thread.locator('.bn-comment-reaction')).toHaveText('👍1');
50+
51+
const urlCommentDoc = page.url();
52+
53+
const { otherPage, cleanup } = await connectOtherUserToDoc({
54+
otherBrowserName,
55+
docUrl: urlCommentDoc,
56+
docTitle,
57+
});
58+
59+
const otherEditor = otherPage.locator('.ProseMirror');
60+
await otherEditor.getByText('Hello').click();
61+
const otherThread = otherPage.locator('.bn-thread');
62+
63+
await otherThread.getByText('This is a comment').first().hover();
64+
await otherThread.locator('[data-test="addreaction"]').first().click();
65+
await otherThread.getByRole('button', { name: '👍' }).click();
66+
67+
// We check that the comment made by the first user is visible for the second user
68+
await expect(
69+
otherThread.getByText('This is a comment').first(),
70+
).toBeVisible();
71+
await expect(
72+
otherThread.getByText(`E2E ${browserName}`).first(),
73+
).toBeVisible();
74+
await expect(otherThread.locator('.bn-comment-reaction')).toHaveText('👍2');
75+
76+
// We add a comment with the second user
77+
await otherThread
78+
.getByRole('paragraph')
79+
.last()
80+
.fill('This is a comment from the other user');
81+
await otherThread.locator('[data-test="save"]').click();
82+
83+
// We check that the second user can see the comment he just made
84+
await expect(
85+
otherThread.getByText('This is a comment from the other user').first(),
86+
).toBeVisible();
87+
await expect(
88+
otherThread.getByText(`E2E ${otherBrowserName}`).first(),
89+
).toBeVisible();
90+
91+
// We check that the first user can see the comment made by the second user in real time
92+
await expect(
93+
thread.getByText('This is a comment from the other user').first(),
94+
).toBeVisible();
95+
await expect(
96+
thread.getByText(`E2E ${otherBrowserName}`).first(),
97+
).toBeVisible();
98+
99+
await cleanup();
100+
});
101+
102+
test('it checks the comments interactions', async ({ page, browserName }) => {
103+
await createDoc(page, 'comment-interaction', browserName, 1);
104+
105+
// Checks add react reaction
106+
const editor = page.locator('.ProseMirror');
107+
await editor.locator('.bn-block-outer').last().fill('Hello World');
108+
await editor.getByText('Hello').selectText();
109+
await page.getByRole('button', { name: 'Add comment' }).click();
110+
111+
const thread = page.locator('.bn-thread');
112+
await thread.getByRole('paragraph').first().fill('This is a comment');
113+
await thread.locator('[data-test="save"]').click();
114+
// Check background color changed
115+
await expect(editor.getByText('Hello')).toHaveCSS(
116+
'background-color',
117+
'rgb(244, 210, 97)',
118+
);
119+
await editor.getByText('Hello').click();
120+
121+
await thread.getByText('This is a comment').first().hover();
122+
123+
// We add a reaction with the first user
124+
await thread.locator('[data-test="addreaction"]').first().click();
125+
await thread.getByRole('button', { name: '👍' }).click();
126+
127+
await expect(thread.locator('.bn-comment-reaction')).toHaveText('👍1');
128+
129+
// Edit Comment
130+
await thread.getByText('This is a comment').first().hover();
131+
await thread.locator('[data-test="moreactions"]').first().click();
132+
await thread.getByRole('menuitem', { name: 'Edit comment' }).click();
133+
const commentEditor = thread.getByText('This is a comment').first();
134+
await commentEditor.fill('This is an edited comment');
135+
const saveBtn = thread.getByRole('button', { name: 'Save' });
136+
await saveBtn.click();
137+
await expect(saveBtn).toBeHidden();
138+
await expect(
139+
thread.getByText('This is an edited comment').first(),
140+
).toBeVisible();
141+
await expect(thread.getByText('This is a comment').first()).toBeHidden();
142+
143+
// Add second comment
144+
await thread.getByRole('paragraph').last().fill('This is a second comment');
145+
await thread.getByRole('button', { name: 'Save' }).click();
146+
await expect(
147+
thread.getByText('This is an edited comment').first(),
148+
).toBeVisible();
149+
await expect(
150+
thread.getByText('This is a second comment').first(),
151+
).toBeVisible();
152+
153+
// Delete second comment
154+
await thread.getByText('This is a second comment').first().hover();
155+
await thread.locator('[data-test="moreactions"]').first().click();
156+
await thread.getByRole('menuitem', { name: 'Delete comment' }).click();
157+
await expect(
158+
thread.getByText('This is a second comment').first(),
159+
).toBeHidden();
160+
161+
// Resolve thread
162+
await thread.getByText('This is an edited comment').first().hover();
163+
await thread.locator('[data-test="resolve"]').click();
164+
await expect(thread).toBeHidden();
165+
await expect(editor.getByText('Hello')).toHaveCSS(
166+
'background-color',
167+
'rgba(0, 0, 0, 0)',
168+
);
169+
});
170+
});

src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,11 +229,11 @@ test.describe('Document create member', () => {
229229
.last()
230230
.fill('Hello World');
231231

232-
const urlDoc = page.url();
232+
const docUrl = page.url();
233233

234234
// Other user will request access
235235
const { otherPage, otherBrowserName, cleanup } =
236-
await connectOtherUserToDoc(browserName, urlDoc);
236+
await connectOtherUserToDoc({ browserName, docUrl });
237237

238238
await expect(
239239
otherPage.getByText('Insufficient access rights to view the document.'),

src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,12 @@ test.describe('Doc Visibility: Restricted', () => {
157157
.last()
158158
.fill('Hello World');
159159

160-
const urlDoc = page.url();
160+
const docUrl = page.url();
161161

162-
const { otherBrowserName, otherPage } = await connectOtherUserToDoc(
162+
const { otherBrowserName, otherPage } = await connectOtherUserToDoc({
163163
browserName,
164-
urlDoc,
165-
);
164+
docUrl,
165+
});
166166

167167
await expect(
168168
otherPage.getByText('Insufficient access rights to view the document.'),

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

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

72+
export const getOtherBrowserName = (browserName: BrowserName) => {
73+
const otherBrowserName = BROWSERS.find((b) => b !== browserName);
74+
if (!otherBrowserName) {
75+
throw new Error('No alternative browser found');
76+
}
77+
return otherBrowserName;
78+
};
79+
7280
export const randomName = (name: string, browserName: string, length: number) =>
7381
Array.from({ length }, (_el, index) => {
7482
return `${browserName}-${Math.floor(Math.random() * 10000)}-${index}-${name}`;

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

Lines changed: 23 additions & 10 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';
@@ -88,15 +88,28 @@ 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-
export const connectOtherUserToDoc = async (
92-
browserName: BrowserName,
93-
docUrl: string,
94-
docTitle?: string,
95-
) => {
96-
const otherBrowserName = BROWSERS.find((b) => b !== browserName);
97-
if (!otherBrowserName) {
98-
throw new Error('No alternative browser found');
99-
}
91+
type ConnectOtherUserToDocParams = {
92+
docUrl: string;
93+
docTitle?: string;
94+
} & (
95+
| {
96+
otherBrowserName: BrowserName;
97+
browserName?: never;
98+
}
99+
| {
100+
browserName: BrowserName;
101+
otherBrowserName?: never;
102+
}
103+
);
104+
105+
export const connectOtherUserToDoc = async ({
106+
browserName,
107+
docUrl,
108+
docTitle,
109+
otherBrowserName: _otherBrowserName,
110+
}: ConnectOtherUserToDocParams) => {
111+
const otherBrowserName =
112+
_otherBrowserName || getOtherBrowserName(browserName);
100113

101114
const otherBrowser = await chromium.launch({ headless: true });
102115
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)