(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.
This commit is contained in:
Anthony LC
2025-09-12 15:44:27 +02:00
parent b13571c6df
commit 48e1370ba3
16 changed files with 1330 additions and 18 deletions

View File

@@ -0,0 +1,289 @@
import { expect, test } from '@playwright/test';
import { createDoc, getOtherBrowserName, verifyDocName } from './utils-common';
import { writeInEditor } from './utils-editor';
import {
addNewMember,
connectOtherUserToDoc,
updateRoleUser,
updateShareLink,
} from './utils-share';
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test.describe('Doc Comments', () => {
test('it checks comments with 2 users in real time', async ({
page,
browserName,
}) => {
const [docTitle] = await createDoc(page, 'comment-doc', browserName, 1);
// We share the doc with another user
const otherBrowserName = getOtherBrowserName(browserName);
await page.getByRole('button', { name: 'Share' }).click();
await addNewMember(page, 0, 'Administrator', otherBrowserName);
await expect(
page
.getByRole('listbox', { name: 'Suggestions' })
.getByText(new RegExp(otherBrowserName)),
).toBeVisible();
await page.getByRole('button', { name: 'close' }).click();
// We add a comment with the first user
const editor = await writeInEditor({ page, text: 'Hello World' });
await editor.getByText('Hello').selectText();
await page.getByRole('button', { name: 'Add comment' }).click();
const thread = page.locator('.bn-thread');
await thread.getByRole('paragraph').first().fill('This is a comment');
await thread.locator('[data-test="save"]').click();
await expect(thread.getByText('This is a comment').first()).toBeHidden();
await editor.getByText('Hello').click();
await thread.getByText('This is a comment').first().hover();
// We add a reaction with the first user
await thread.locator('[data-test="addreaction"]').first().click();
await page.getByRole('button', { name: '👍' }).click();
await expect(thread.getByText('This is a comment').first()).toBeVisible();
await expect(thread.getByText(`E2E ${browserName}`).first()).toBeVisible();
await expect(thread.locator('.bn-comment-reaction')).toHaveText('👍1');
const urlCommentDoc = page.url();
const { otherPage, cleanup } = await connectOtherUserToDoc({
otherBrowserName,
docUrl: urlCommentDoc,
docTitle,
});
const otherEditor = otherPage.locator('.ProseMirror');
await otherEditor.getByText('Hello').click();
const otherThread = otherPage.locator('.bn-thread');
await otherThread.getByText('This is a comment').first().hover();
await otherThread.locator('[data-test="addreaction"]').first().click();
await otherPage.getByRole('button', { name: '👍' }).click();
// We check that the comment made by the first user is visible for the second user
await expect(
otherThread.getByText('This is a comment').first(),
).toBeVisible();
await expect(
otherThread.getByText(`E2E ${browserName}`).first(),
).toBeVisible();
await expect(otherThread.locator('.bn-comment-reaction')).toHaveText('👍2');
// We add a comment with the second user
await otherThread
.getByRole('paragraph')
.last()
.fill('This is a comment from the other user');
await otherThread.locator('[data-test="save"]').click();
// We check that the second user can see the comment he just made
await expect(
otherThread.getByText('This is a comment from the other user').first(),
).toBeVisible();
await expect(
otherThread.getByText(`E2E ${otherBrowserName}`).first(),
).toBeVisible();
// We check that the first user can see the comment made by the second user in real time
await expect(
thread.getByText('This is a comment from the other user').first(),
).toBeVisible();
await expect(
thread.getByText(`E2E ${otherBrowserName}`).first(),
).toBeVisible();
await cleanup();
});
test('it checks the comments interactions', async ({ page, browserName }) => {
await createDoc(page, 'comment-interaction', browserName, 1);
// Checks add react reaction
const editor = page.locator('.ProseMirror');
await editor.locator('.bn-block-outer').last().fill('Hello World');
await editor.getByText('Hello').selectText();
await page.getByRole('button', { name: 'Add comment' }).click();
const thread = page.locator('.bn-thread');
await thread.getByRole('paragraph').first().fill('This is a comment');
await thread.locator('[data-test="save"]').click();
await expect(thread.getByText('This is a comment').first()).toBeHidden();
// Check background color changed
await expect(editor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(237, 180, 0, 0.4)',
);
await editor.getByText('Hello').click();
await thread.getByText('This is a comment').first().hover();
// We add a reaction with the first user
await thread.locator('[data-test="addreaction"]').first().click();
await page.getByRole('button', { name: '👍' }).click();
await expect(thread.locator('.bn-comment-reaction')).toHaveText('👍1');
// Edit Comment
await thread.getByText('This is a comment').first().hover();
await thread.locator('[data-test="moreactions"]').first().click();
await thread.getByRole('menuitem', { name: 'Edit comment' }).click();
const commentEditor = thread.getByText('This is a comment').first();
await commentEditor.fill('This is an edited comment');
const saveBtn = thread.getByRole('button', { name: 'Save' });
await saveBtn.click();
await expect(saveBtn).toBeHidden();
await expect(
thread.getByText('This is an edited comment').first(),
).toBeVisible();
await expect(thread.getByText('This is a comment').first()).toBeHidden();
// Add second comment
await thread.getByRole('paragraph').last().fill('This is a second comment');
await thread.getByRole('button', { name: 'Save' }).click();
await expect(
thread.getByText('This is an edited comment').first(),
).toBeVisible();
await expect(
thread.getByText('This is a second comment').first(),
).toBeVisible();
// Delete second comment
await thread.getByText('This is a second comment').first().hover();
await thread.locator('[data-test="moreactions"]').first().click();
await thread.getByRole('menuitem', { name: 'Delete comment' }).click();
await expect(
thread.getByText('This is a second comment').first(),
).toBeHidden();
// Resolve thread
await thread.getByText('This is an edited comment').first().hover();
await thread.locator('[data-test="resolve"]').click();
await expect(thread).toBeHidden();
await expect(editor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)',
);
});
test('it checks the comments abilities', async ({ page, browserName }) => {
test.slow();
const [docTitle] = await createDoc(page, 'comment-doc', browserName, 1);
// We share the doc with another user
const otherBrowserName = getOtherBrowserName(browserName);
// Add a new member with editor role
await page.getByRole('button', { name: 'Share' }).click();
await addNewMember(page, 0, 'Editor', otherBrowserName);
await expect(
page
.getByRole('listbox', { name: 'Suggestions' })
.getByText(new RegExp(otherBrowserName)),
).toBeVisible();
const urlCommentDoc = page.url();
const { otherPage, cleanup } = await connectOtherUserToDoc({
otherBrowserName,
docUrl: urlCommentDoc,
docTitle,
});
const otherEditor = await writeInEditor({
page: otherPage,
text: 'Hello, I can edit the document',
});
await expect(
otherEditor.getByText('Hello, I can edit the document'),
).toBeVisible();
await otherEditor.getByText('Hello').selectText();
await otherPage.getByRole('button', { name: 'Comment' }).click();
const otherThread = otherPage.locator('.bn-thread');
await otherThread
.getByRole('paragraph')
.first()
.fill('I can add a comment');
await otherThread.locator('[data-test="save"]').click();
await expect(
otherThread.getByText('I can add a comment').first(),
).toBeHidden();
await expect(otherEditor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(237, 180, 0, 0.4)',
);
// We change the role of the second user to reader
await updateRoleUser(page, 'Reader', `user.test@${otherBrowserName}.test`);
// With the reader role, the second user cannot see comments
await otherPage.reload();
await verifyDocName(otherPage, docTitle);
await expect(otherEditor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)',
);
await otherEditor.getByText('Hello').click();
await expect(otherThread).toBeHidden();
await otherEditor.getByText('Hello').selectText();
await expect(
otherPage.getByRole('button', { name: 'Comment' }),
).toBeHidden();
await otherPage.reload();
// Change the link role of the doc to set it in commenting mode
await updateShareLink(page, 'Public', 'Editing');
// Anonymous user can see and add comments
await otherPage.getByRole('button', { name: 'Logout' }).click();
await otherPage.goto(urlCommentDoc);
await verifyDocName(otherPage, docTitle);
await expect(otherEditor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(237, 180, 0, 0.4)',
);
await otherEditor.getByText('Hello').click();
await expect(
otherThread.getByText('I can add a comment').first(),
).toBeVisible();
await otherThread
.locator('.ProseMirror.bn-editor[contenteditable="true"]')
.getByRole('paragraph')
.first()
.fill('Comment by anonymous user');
await otherThread.locator('[data-test="save"]').click();
await expect(
otherThread.getByText('Comment by anonymous user').first(),
).toBeVisible();
await expect(
otherThread.getByRole('img', { name: `Anonymous` }).first(),
).toBeVisible();
await otherThread.getByText('Comment by anonymous user').first().hover();
await expect(otherThread.locator('[data-test="moreactions"]')).toBeHidden();
await cleanup();
});
});

View File

@@ -70,6 +70,14 @@ export const keyCloakSignIn = async (
await page.click('button[type="submit"]', { force: true });
};
export const getOtherBrowserName = (browserName: BrowserName) => {
const otherBrowserName = BROWSERS.find((b) => b !== browserName);
if (!otherBrowserName) {
throw new Error('No alternative browser found');
}
return otherBrowserName;
};
export const randomName = (name: string, browserName: string, length: number) =>
Array.from({ length }, (_el, index) => {
return `${browserName}-${Math.floor(Math.random() * 10000)}-${index}-${name}`;
@@ -125,7 +133,9 @@ export const verifyDocName = async (page: Page, docName: string) => {
try {
await expect(
page.getByRole('textbox', { name: 'Document title' }),
).toContainText(docName);
).toContainText(docName, {
timeout: 1000,
});
} catch {
await expect(page.getByRole('heading', { name: docName })).toBeVisible();
}

View File

@@ -1,8 +1,8 @@
import { Page, chromium, expect } from '@playwright/test';
import {
BROWSERS,
BrowserName,
getOtherBrowserName,
keyCloakSignIn,
verifyDocName,
} from './utils-common';
@@ -88,21 +88,30 @@ export const updateRoleUser = async (
* @param docTitle The title of the document (optional).
* @returns An object containing the other browser, context, and page.
*/
type ConnectOtherUserToDocParams = {
docUrl: string;
docTitle?: string;
withoutSignIn?: boolean;
} & (
| {
otherBrowserName: BrowserName;
browserName?: never;
}
| {
browserName: BrowserName;
otherBrowserName?: never;
}
);
export const connectOtherUserToDoc = async ({
browserName,
docUrl,
docTitle,
otherBrowserName: _otherBrowserName,
withoutSignIn,
}: {
browserName: BrowserName;
docUrl: string;
docTitle?: string;
withoutSignIn?: boolean;
}) => {
const otherBrowserName = BROWSERS.find((b) => b !== browserName);
if (!otherBrowserName) {
throw new Error('No alternative browser found');
}
}: ConnectOtherUserToDocParams) => {
const otherBrowserName =
_otherBrowserName || getOtherBrowserName(browserName);
const otherBrowser = await chromium.launch({ headless: true });
const otherContext = await otherBrowser.newContext({