🛂(frontend) block editing title when not allowed

We had a case where the title input was editable
even when the user did not have the right to
edit it because of websocket problem during
collaboration. We fixed this issue by checking
the collaboration status before allowing the
edition of the title.
This commit is contained in:
Anthony LC
2025-09-19 16:45:40 +02:00
parent 18f4ab880f
commit 7ed46ab225
7 changed files with 66 additions and 54 deletions

View File

@@ -1,7 +1,7 @@
/* eslint-disable playwright/no-conditional-expect */
import path from 'path';
import { chromium, expect, test } from '@playwright/test';
import { expect, test } from '@playwright/test';
import cs from 'convert-stream';
import {
@@ -12,6 +12,7 @@ import {
verifyDocName,
} from './utils-common';
import { getEditor, openSuggestionMenu, writeInEditor } from './utils-editor';
import { connectOtherUserToDoc, updateShareLink } from './utils-share';
import { createRootSubPage, navigateToPageFromTree } from './utils-sub-pages';
test.beforeEach(async ({ page }) => {
@@ -560,20 +561,7 @@ test.describe('Doc Editor', () => {
await page.getByRole('button', { name: 'Share' }).click();
await page.getByTestId('doc-visibility').click();
await page
.getByRole('menuitem', {
name: 'Public',
})
.click();
await expect(
page.getByText('The document visibility has been updated.'),
).toBeVisible();
await page.getByTestId('doc-access-mode').click();
await page.getByRole('menuitem', { name: 'Editing' }).click();
await updateShareLink(page, 'Public', 'Editing');
// Close the modal
await page.getByRole('button', { name: 'close' }).first().click();
@@ -597,17 +585,12 @@ test.describe('Doc Editor', () => {
* We open another browser that will connect to the collaborative server
* and will block the current browser to edit the doc.
*/
const otherBrowser = await chromium.launch({ headless: true });
const otherContext = await otherBrowser.newContext({
locale: 'en-US',
timezoneId: 'Europe/Paris',
permissions: [],
storageState: {
cookies: [],
origins: [],
},
const { otherPage } = await connectOtherUserToDoc({
browserName,
docUrl: urlChildDoc,
docTitle: childTitle,
withoutSignIn: true,
});
const otherPage = await otherContext.newPage();
const webSocketPromise = otherPage.waitForEvent(
'websocket',
@@ -648,6 +631,11 @@ test.describe('Doc Editor', () => {
await expect(editor).toHaveAttribute('contenteditable', 'false');
await expect(
page.getByRole('textbox', { name: 'Document title' }),
).toBeHidden();
await expect(page.getByRole('heading', { name: childTitle })).toBeVisible();
await page.goto(urlParentDoc);
await verifyDocName(page, parentTitle);
@@ -664,6 +652,11 @@ test.describe('Doc Editor', () => {
await expect(editor).toHaveAttribute('contenteditable', 'true');
await expect(
page.getByRole('textbox', { name: 'Document title' }),
).toContainText(childTitle);
await expect(page.getByRole('heading', { name: childTitle })).toBeHidden();
await expect(
card.getByText('Others are editing. Your network prevent changes.'),
).toBeHidden();

View File

@@ -142,6 +142,10 @@ test.describe('Doc Header', () => {
await goToGridDoc(page);
await expect(
page.getByRole('textbox', { name: 'Document title' }),
).toContainText('Mocked document');
await expect(
page.getByRole('button', { name: 'Export the document' }),
).toBeVisible();
@@ -218,6 +222,10 @@ test.describe('Doc Header', () => {
await goToGridDoc(page);
await expect(
page.getByRole('textbox', { name: 'Document title' }),
).toContainText('Mocked document');
await expect(
page.getByRole('button', { name: 'Export the document' }),
).toBeVisible();
@@ -286,6 +294,10 @@ test.describe('Doc Header', () => {
await goToGridDoc(page);
await expect(
page.getByRole('heading', { name: 'Mocked document' }),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Export the document' }),
).toBeVisible();

View File

@@ -229,11 +229,11 @@ test.describe('Document create member', () => {
.last()
.fill('Hello World');
const urlDoc = page.url();
const docUrl = page.url();
// Other user will request access
const { otherPage, otherBrowserName, cleanup } =
await connectOtherUserToDoc(browserName, urlDoc);
await connectOtherUserToDoc({ browserName, docUrl });
await expect(
otherPage.getByText('Insufficient access rights to view the document.'),

View File

@@ -157,12 +157,12 @@ test.describe('Doc Visibility: Restricted', () => {
.last()
.fill('Hello World');
const urlDoc = page.url();
const docUrl = page.url();
const { otherBrowserName, otherPage } = await connectOtherUserToDoc(
const { otherBrowserName, otherPage } = await connectOtherUserToDoc({
browserName,
urlDoc,
);
docUrl,
});
await expect(
otherPage.getByText('Insufficient access rights to view the document.'),

View File

@@ -9,7 +9,7 @@ import {
export type Role = 'Administrator' | 'Owner' | 'Member' | 'Editor' | 'Reader';
export type LinkReach = 'Private' | 'Connected' | 'Public';
export type LinkRole = 'Reading' | 'Edition';
export type LinkRole = 'Reading' | 'Editing';
export const addNewMember = async (
page: Page,
@@ -88,11 +88,17 @@ export const updateRoleUser = async (
* @param docTitle The title of the document (optional).
* @returns An object containing the other browser, context, and page.
*/
export const connectOtherUserToDoc = async (
browserName: BrowserName,
docUrl: string,
docTitle?: string,
) => {
export const connectOtherUserToDoc = async ({
browserName,
docUrl,
docTitle,
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');
@@ -111,15 +117,16 @@ export const connectOtherUserToDoc = async (
const otherPage = await otherContext.newPage();
await otherPage.goto(docUrl);
await otherPage
.getByRole('main', { name: 'Main content' })
.getByLabel('Login')
.click({
timeout: 15000,
});
await keyCloakSignIn(otherPage, otherBrowserName, false);
if (!withoutSignIn) {
await otherPage
.getByRole('main', { name: 'Main content' })
.getByLabel('Login')
.click({
timeout: 15000,
});
await keyCloakSignIn(otherPage, otherBrowserName, false);
}
if (docTitle) {
await verifyDocName(otherPage, docTitle);
}

View File

@@ -11,6 +11,7 @@ import {
KEY_DOC,
KEY_LIST_DOC,
useDocStore,
useIsCollaborativeEditable,
useTrans,
useUpdateDoc,
} from '@/docs/doc-management';
@@ -21,7 +22,10 @@ interface DocTitleProps {
}
export const DocTitle = ({ doc }: DocTitleProps) => {
if (!doc.abilities.partial_update) {
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
const readOnly = !doc.abilities.partial_update || !isEditable || isLoading;
if (readOnly) {
return <DocTitleText />;
}