From b5f94cc8201c072e0ceff913f6923da23f96d82d Mon Sep 17 00:00:00 2001 From: Nathan Panchout Date: Wed, 1 Jul 2026 14:16:20 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=85(frontend)=20cover=20presenter=20share?= =?UTF-8?q?=20links?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exercise direct present links, copied slide URLs, and mobile behavior. Keep the e2e slice scoped to the share-link workflow. --- .../app-impress/presenter-mode.spec.ts | 131 +++++++++++++++++- 1 file changed, 129 insertions(+), 2 deletions(-) diff --git a/src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts index 169f69c64..c9e0b93ff 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts @@ -1,8 +1,13 @@ import path from 'path'; -import { Page, expect, test } from '@playwright/test'; +import { Locator, Page, expect, test } from '@playwright/test'; -import { createDoc, goToGridDoc, mockedDocument } from './utils-common'; +import { + createDoc, + goToGridDoc, + mockedDocument, + saveContent, +} from './utils-common'; import { openSuggestionMenu, tryFocusEditorContent, @@ -46,6 +51,14 @@ const insertImageInCurrentBlock = async (page: Page) => { return image; }; +const getDocIdFromUrl = (page: Page) => { + const id = /\/docs\/([^/?]+)/.exec(page.url())?.[1]; + if (!id) { + throw new Error(`Could not extract doc id from URL: ${page.url()}`); + } + return id; +}; + const writeMultiSlideDoc = async (page: Page) => { const editor = await writeInEditor({ page, text: 'Slide one' }); await editor.press('Enter'); @@ -58,6 +71,18 @@ const writeMultiSlideDoc = async (page: Page) => { await writeInEditor({ page, text: 'Slide three' }); }; +const openPresenterActions = async (page: Page, overlay: Locator) => { + await overlay.getByRole('button', { name: 'More options' }).click(); + await expect( + page.getByRole('menuitem', { name: 'Copy link to slide' }), + ).toBeVisible(); +}; + +const copyCurrentPresenterSlideLink = async (page: Page, overlay: Locator) => { + await openPresenterActions(page, overlay); + await page.getByRole('menuitem', { name: 'Copy link to slide' }).click(); +}; + test.beforeEach(async ({ page }) => { await page.goto('/'); }); @@ -362,6 +387,90 @@ test.describe('Presenter Mode', () => { topVisible.clientHeight ?? 0, ); }); + + test('opens presenter deep-links and normalizes slide params', async ({ + page, + browserName, + }) => { + const [docTitle] = await createDoc( + page, + 'presenter-deeplink', + browserName, + 1, + ); + await writeMultiSlideDoc(page); + const docId = getDocIdFromUrl(page); + + // Ensure the typed content is persisted (awaits the PATCH /content/) before + // reloading the page through the deep-link, instead of using a fixed sleep. + await saveContent(page, docTitle); + await page.goto(`/docs/${docId}/?view=present&slide=3`); + + const overlay = page.getByRole('dialog', { name: 'Presenter mode' }); + await expect(overlay).toBeVisible({ timeout: 15000 }); + await expect(overlay.getByText('3 / 4')).toBeVisible(); + await expect(overlay.getByText('Slide two')).toBeVisible(); + + await page.goto(`/docs/${docId}/?view=present&slide=99`); + await expect(overlay).toBeVisible({ timeout: 15000 }); + await expect(overlay.getByText('4 / 4')).toBeVisible(); + await expect.poll(() => page.url()).toContain('slide=4'); + + await page.goto(`/docs/${docId}/?view=present&slide=abc`); + await expect(overlay).toBeVisible({ timeout: 15000 }); + await expect(overlay.getByText('1 / 4')).toBeVisible(); + }); + + test('syncs slide URLs, copies the current-slide link, and strips params on close', async ({ + page, + browserName, + }) => { + await createDoc(page, 'presenter-url-sync', browserName, 1); + await writeMultiSlideDoc(page); + + const overlay = await openPresenter(page); + await expect.poll(() => page.url()).toContain('view=present'); + await expect.poll(() => page.url()).toContain('slide=1'); + + await overlay.getByRole('button', { name: 'Next slide' }).click(); + await expect(overlay.getByText('2 / 4')).toBeVisible(); + await expect.poll(() => page.url()).toContain('slide=2'); + + await copyCurrentPresenterSlideLink(page, overlay); + await expect(page.getByText('Link Copied !')).toBeVisible(); + + await overlay.getByRole('button', { name: 'Close presenter' }).click(); + await expect(overlay).toBeHidden(); + await expect.poll(() => page.url()).not.toContain('view=present'); + await expect.poll(() => page.url()).not.toContain('slide='); + await expect(page.locator('.--docs--editor-container')).toBeVisible(); + }); + + test('copies a deep-link pointing to the current slide', async ({ + page, + browserName, + context, + }) => { + test.skip( + browserName !== 'chromium', + 'Clipboard read-back is Chromium-only', + ); + await context.grantPermissions(['clipboard-read', 'clipboard-write']); + + await createDoc(page, 'presenter-copy-link-content', browserName, 1); + await writeMultiSlideDoc(page); + const docId = getDocIdFromUrl(page); + + const overlay = await openPresenter(page); + await overlay.getByRole('button', { name: 'Next slide' }).click(); + await expect(overlay.getByText('2 / 4')).toBeVisible(); + + await copyCurrentPresenterSlideLink(page, overlay); + await expect(page.getByText('Link Copied !')).toBeVisible(); + + const clipboard = await page.evaluate(() => navigator.clipboard.readText()); + expect(clipboard).toContain(`/docs/${docId}/?view=present&slide=2`); + }); }); test.describe('Presenter Mode mobile', () => { @@ -395,4 +504,22 @@ test.describe('Presenter Mode mobile', () => { await page.getByLabel('Open the document options').click(); await expect(page.getByRole('menuitem', { name: 'Present' })).toBeHidden(); }); + + test('ignores a ?view=present deep-link on small mobile viewports', async ({ + page, + browserName, + }) => { + await createDoc(page, 'presenter-mobile-deeplink', browserName, 1, true); + await writeInEditor({ page, text: 'Slide A' }); + const docId = getDocIdFromUrl(page); + + await page.goto(`/docs/${docId}/?view=present&slide=1`); + + await expect(page.locator('.--docs--editor-container')).toBeVisible({ + timeout: 15000, + }); + await expect( + page.getByRole('dialog', { name: 'Presenter mode' }), + ).toBeHidden(); + }); });