import { expect, test } from '@playwright/test'; import { createDoc, getGridRow, getOtherBrowserName, mockedListDocs, toggleHeaderMenu, verifyDocName, } from './utils-common'; import { writeInEditor } from './utils-editor'; import { addNewMember, connectOtherUserToDoc, updateShareLink, } from './utils-share'; import { createRootSubPage } from './utils-sub-pages'; test.describe('Doc grid move', () => { test('it checks drag and drop functionality', async ({ page, browserName, }) => { await page.goto('/'); await createDoc(page, 'Draggable doc', browserName, 1); await page.getByRole('button', { name: 'Back to homepage' }).click(); await createDoc(page, 'Droppable doc', browserName, 1); await page.getByRole('button', { name: 'Back to homepage' }).click(); const response = await page.waitForResponse( (response) => response.url().endsWith('documents/?page=1') && response.status() === 200, ); const responseJson = await response.json(); const items = responseJson.results; const docsGrid = page.getByTestId('docs-grid'); await expect(docsGrid).toBeVisible(); await expect(page.getByTestId('grid-loader')).toBeHidden(); const draggableElement = page.getByTestId(`draggable-doc-${items[1].id}`); const dropZone = page.getByTestId(`droppable-doc-${items[0].id}`); await expect(draggableElement).toBeVisible(); await expect(dropZone).toBeVisible(); // Get the position of the elements const draggableBoundingBox = await draggableElement.boundingBox(); const dropZoneBoundingBox = await dropZone.boundingBox(); expect(draggableBoundingBox).toBeDefined(); expect(dropZoneBoundingBox).toBeDefined(); if (!draggableBoundingBox || !dropZoneBoundingBox) { throw new Error('Unable to determine the position of the elements'); } await page.mouse.move( draggableBoundingBox.x + draggableBoundingBox.width / 2, draggableBoundingBox.y + draggableBoundingBox.height / 2, ); await page.mouse.down(); // Move to the target zone await page.mouse.move( dropZoneBoundingBox.x + dropZoneBoundingBox.width / 2, dropZoneBoundingBox.y + dropZoneBoundingBox.height / 2, { steps: 10 }, // Make the movement smoother ); const dragOverlay = page.getByTestId('drag-doc-overlay'); await expect(dragOverlay).toBeVisible(); await expect(dragOverlay).toHaveText(items[1].title as string); await page.mouse.up(); await expect(dragOverlay).toBeHidden(); }); test("it checks can't drop when we have not the minimum role", async ({ page, }) => { await mockedListDocs(page, data); await page.goto('/'); const docsGrid = page.getByTestId('docs-grid'); await expect(docsGrid).toBeVisible(); await expect(page.getByTestId('grid-loader')).toBeHidden(); const canDropAndDrag = page.getByTestId('droppable-doc-can-drop-and-drag'); const noDropAndNoDrag = page.getByTestId( 'droppable-doc-no-drop-and-no-drag', ); await expect(canDropAndDrag).toBeVisible(); await expect(noDropAndNoDrag).toBeVisible(); const canDropAndDragBoundigBox = await canDropAndDrag.boundingBox(); const noDropAndNoDragBoundigBox = await noDropAndNoDrag.boundingBox(); if (!canDropAndDragBoundigBox || !noDropAndNoDragBoundigBox) { throw new Error('Unable to determine the position of the elements'); } await page.mouse.move( canDropAndDragBoundigBox.x + canDropAndDragBoundigBox.width / 2, canDropAndDragBoundigBox.y + canDropAndDragBoundigBox.height / 2, ); await page.mouse.down(); await page.mouse.move( noDropAndNoDragBoundigBox.x + noDropAndNoDragBoundigBox.width / 2, noDropAndNoDragBoundigBox.y + noDropAndNoDragBoundigBox.height / 2, { steps: 10 }, ); const dragOverlay = page.getByTestId('drag-doc-overlay'); await expect(dragOverlay).toBeVisible(); await expect(dragOverlay).toHaveText( 'You must be at least the administrator of the target document', ); await page.mouse.up(); }); test("it checks can't drag when we have not the minimum role", async ({ page, }) => { await mockedListDocs(page, data); await page.goto('/'); const docsGrid = page.getByTestId('docs-grid'); await expect(docsGrid).toBeVisible(); await expect(page.getByTestId('grid-loader')).toBeHidden(); const canDropAndDrag = page.getByTestId('droppable-doc-can-drop-and-drag'); const noDropAndNoDrag = page.getByTestId( 'droppable-doc-no-drop-and-no-drag', ); await expect(canDropAndDrag).toBeVisible(); await expect(noDropAndNoDrag).toBeVisible(); const canDropAndDragBoundigBox = await canDropAndDrag.boundingBox(); const noDropAndNoDragBoundigBox = await noDropAndNoDrag.boundingBox(); if (!canDropAndDragBoundigBox || !noDropAndNoDragBoundigBox) { throw new Error('Unable to determine the position of the elements'); } await page.mouse.move( noDropAndNoDragBoundigBox.x + noDropAndNoDragBoundigBox.width / 2, noDropAndNoDragBoundigBox.y + noDropAndNoDragBoundigBox.height / 2, ); await page.mouse.down(); await page.mouse.move( canDropAndDragBoundigBox.x + canDropAndDragBoundigBox.width / 2, canDropAndDragBoundigBox.y + canDropAndDragBoundigBox.height / 2, { steps: 10 }, ); const dragOverlay = page.getByTestId('drag-doc-overlay'); await expect(dragOverlay).toBeVisible(); await expect(dragOverlay).toHaveText( 'You must be the owner to move the document', ); await page.mouse.up(); }); test('it moves a doc from the doc search modal', async ({ page, browserName, }) => { await page.goto('/'); const [titleDoc1] = await createDoc(page, 'Draggable doc', browserName, 1); const otherBrowserName = getOtherBrowserName(browserName); await page.getByRole('button', { name: 'Share' }).click(); await addNewMember(page, 0, 'Administrator', otherBrowserName); await page .getByRole('dialog') .getByRole('button', { name: 'close' }) .click(); await page.getByRole('button', { name: 'Back to homepage' }).click(); const [titleDoc2] = await createDoc(page, 'Droppable doc', browserName, 1); await page.getByRole('button', { name: 'Back to homepage' }).click(); const docsGrid = page.getByTestId('docs-grid'); await expect(docsGrid.getByText(titleDoc1)).toBeVisible(); await expect(docsGrid.getByText(titleDoc2)).toBeVisible(); const row = await getGridRow(page, titleDoc1); await row.getByRole('button', { name: /Open the menu of actions/ }).click(); await page.getByRole('menuitem', { name: 'Move into a doc' }).click(); await expect( page.getByRole('dialog').getByRole('heading', { name: 'Move' }), ).toBeVisible(); const input = page.getByRole('combobox', { name: 'Quick search input' }); await input.click(); await input.fill(titleDoc2); await expect( page.getByRole('option').first().getByText(titleDoc2), ).toBeVisible(); // Select the first result await page.keyboard.press('Enter'); // The CTA should get the focus await page.keyboard.press('Tab'); // Validate the move action await page.keyboard.press('Enter'); await expect( page .getByRole('dialog') .getByText('it will lose its current access rights'), ).toBeVisible(); await page .getByRole('dialog') .getByRole('button', { name: 'Move', exact: true }) .first() .click(); await expect(docsGrid.getByText(titleDoc1)).toBeHidden(); await docsGrid .getByRole('link', { name: `Open document ${titleDoc2}` }) .click(); await verifyDocName(page, titleDoc2); const docTree = page.getByTestId('doc-tree'); await expect(docTree.getByText(titleDoc1)).toBeVisible(); }); test('it proposes an access request when moving a doc without sufficient permissions', async ({ page, browserName, }) => { test.slow(); await page.goto('/'); const [titleDoc1] = await createDoc(page, 'Move doc', browserName, 1); const { otherPage, cleanup } = await connectOtherUserToDoc({ docUrl: '/', browserName, }); // Another user creates a doc const [titleDoc2] = await createDoc(otherPage, 'Drop doc', browserName, 1); await writeInEditor({ page: otherPage, text: 'Hello world', }); // Make it public await otherPage.getByRole('button', { name: 'Share' }).click(); await updateShareLink(otherPage, 'Public'); await otherPage .getByRole('dialog') .getByRole('button', { name: 'close' }) .click(); const otherPageUrl = otherPage.url(); // The first user visit the doc to have it in his grid list await page.goto(otherPageUrl); await expect(page.getByText('Hello world')).toBeVisible(); await page.waitForTimeout(1000); await page.getByRole('button', { name: 'Back to homepage' }).click(); const docsGrid = page.getByTestId('docs-grid'); await expect(docsGrid.getByText(titleDoc1)).toBeVisible(); await expect(docsGrid.getByText(titleDoc2)).toBeVisible(); const row = await getGridRow(page, titleDoc1); await row.getByRole('button', { name: /Open the menu of actions/ }).click(); await page.getByRole('menuitem', { name: 'Move into a doc' }).click(); await expect( page.getByRole('dialog').getByRole('heading', { name: 'Move' }), ).toBeVisible(); const input = page.getByRole('combobox', { name: 'Quick search input' }); await input.click(); await input.fill(titleDoc2); await expect( page.getByRole('option').first().getByText(titleDoc2), ).toBeVisible(); // Select the first result await page.keyboard.press('Enter'); // The CTA should get the focus await page.keyboard.press('Tab'); // Validate the move action await page.keyboard.press('Enter'); // Request access modal should be visible await expect( page .getByRole('dialog') .getByText( 'You need edit access to the destination. Request access, then try again.', ), ).toBeVisible(); await page .getByRole('dialog') .getByRole('button', { name: 'Request access', exact: true }) .first() .click(); // The other user should receive the access request and be able to approve it await otherPage.getByRole('button', { name: 'Share' }).click(); await expect(otherPage.getByText('Access Requests')).toBeVisible(); await expect( otherPage.getByText( process.env[`USERNAME_${browserName.toUpperCase()}`] || '', ), ).toBeVisible(); const emailRequest = process.env[`SIGN_IN_USERNAME_${browserName.toUpperCase()}`] || ''; await expect(otherPage.getByText(emailRequest)).toBeVisible(); const container = otherPage.getByTestId( `doc-share-access-request-row-${emailRequest}`, ); await container.getByTestId('doc-role-dropdown').click(); await otherPage .getByRole('menuitemradio', { name: 'Administrator' }) .click(); await container.getByRole('button', { name: 'Approve' }).click(); await expect(otherPage.getByText('Access Requests')).toBeHidden(); await expect(otherPage.getByText('Share with 2 users')).toBeVisible(); await expect( otherPage.getByText( process.env[`USERNAME_${browserName.toUpperCase()}`] || '', ), ).toBeVisible(); // The first user should now be able to move the doc await page.reload(); await row.getByRole('button', { name: /Open the menu of actions/ }).click(); await page.getByRole('menuitem', { name: 'Move into a doc' }).click(); await expect( page.getByRole('dialog').getByRole('heading', { name: 'Move' }), ).toBeVisible(); await input.click(); await input.fill(titleDoc2); await expect( page.getByRole('option').first().getByText(titleDoc2), ).toBeVisible(); // Select the first result await page.keyboard.press('Enter'); // The CTA should get the focus await page.keyboard.press('Tab'); // Validate the move action await page.keyboard.press('Enter'); await expect(docsGrid.getByText(titleDoc1)).toBeHidden(); await docsGrid .getByRole('link', { name: `Open document ${titleDoc2}` }) .click(); await verifyDocName(page, titleDoc2); const docTree = page.getByTestId('doc-tree'); await expect(docTree.getByText(titleDoc1)).toBeVisible({ timeout: 15000, }); await cleanup(); }); }); test.describe('Doc grid dnd mobile', () => { test.use({ viewport: { width: 500, height: 1200 } }); test('DND is deactivated on mobile', async ({ page, browserName }) => { await page.goto('/'); const docsGrid = page.getByTestId('docs-grid'); await expect(page.getByTestId('docs-grid')).toBeVisible(); await expect(page.getByTestId('grid-loader')).toBeHidden(); await expect(docsGrid.getByRole('listitem').first()).toBeVisible(); await expect(docsGrid.locator('.--docs--grid-droppable')).toHaveCount(0); await createDoc(page, 'Draggable doc mobile', browserName, 1, true); await createRootSubPage( page, browserName, 'Draggable doc mobile child', true, ); await toggleHeaderMenu(page); await expect(page.locator('.--docs-sub-page-item').first()).toHaveAttribute( 'draggable', 'false', ); }); }); const data = [ { id: 'can-drop-and-drag', abilities: { accesses_manage: true, accesses_view: true, ai_transform: true, ai_translate: true, attachment_upload: true, children_list: true, children_create: true, collaboration_auth: true, descendants: true, destroy: true, favorite: true, link_configuration: true, invite_owner: true, move: true, partial_update: true, restore: true, retrieve: true, media_auth: true, link_select_options: { restricted: ['reader', 'editor'], authenticated: ['reader', 'editor'], public: ['reader', 'editor'], }, tree: true, update: true, versions_destroy: true, versions_list: true, versions_retrieve: true, }, created_at: '2025-03-14T14:45:22.527221Z', creator: 'bc6895e0-8f6d-4b00-827d-c143aa6b2ecb', depth: 1, excerpt: null, is_favorite: false, link_role: 'reader', link_reach: 'restricted', nb_accesses_ancestors: 1, nb_accesses_direct: 1, numchild: 5, path: '000000o', title: 'Can drop and drag', updated_at: '2025-03-14T14:45:27.699542Z', user_roles: ['owner'], user_role: 'owner', }, { id: 'can-only-drop', title: 'Can only drop', abilities: { accesses_manage: true, accesses_view: true, ai_transform: true, ai_translate: true, attachment_upload: true, children_list: true, children_create: true, collaboration_auth: true, descendants: true, destroy: true, favorite: true, link_configuration: true, invite_owner: true, move: true, partial_update: true, restore: true, retrieve: true, media_auth: true, link_select_options: { restricted: ['reader', 'editor'], authenticated: ['reader', 'editor'], public: ['reader', 'editor'], }, tree: true, update: true, versions_destroy: true, versions_list: true, versions_retrieve: true, }, created_at: '2025-03-14T14:45:22.527221Z', creator: 'bc6895e0-8f6d-4b00-827d-c143aa6b2ecb', depth: 1, excerpt: null, is_favorite: false, link_role: 'reader', link_reach: 'restricted', nb_accesses_ancestors: 1, nb_accesses_direct: 1, numchild: 5, path: '000000o', updated_at: '2025-03-14T14:45:27.699542Z', user_roles: ['editor'], user_role: 'editor', }, { id: 'no-drop-and-no-drag', abilities: { accesses_manage: false, accesses_view: true, ai_transform: false, ai_translate: false, attachment_upload: false, children_list: true, children_create: false, collaboration_auth: true, descendants: true, destroy: false, favorite: true, link_configuration: false, invite_owner: false, move: false, partial_update: false, restore: false, retrieve: true, media_auth: true, link_select_options: { restricted: ['reader', 'editor'], authenticated: ['reader', 'editor'], public: ['reader', 'editor'], }, tree: true, update: false, versions_destroy: false, versions_list: true, versions_retrieve: true, }, created_at: '2025-03-14T14:44:16.032773Z', creator: '9264f420-f018-4bd6-96ae-4788f41af56d', depth: 1, excerpt: null, is_favorite: false, link_role: 'reader', link_reach: 'restricted', nb_accesses_ancestors: 14, nb_accesses_direct: 14, numchild: 0, path: '000000l', title: 'No drop and no drag', updated_at: '2025-03-14T14:44:16.032774Z', user_roles: ['reader'], user_role: 'reader', }, ];