diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index d4b327ba4..18c90a712 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -15,6 +15,7 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest strategy: + fail-fast: false matrix: server-branch: ['stable32', 'master'] shardIndex: [1, 2, 3, 4] diff --git a/playwright/e2e/page-links-handler.spec.ts b/playwright/e2e/page-links-handler.spec.ts new file mode 100644 index 000000000..519b7e460 --- /dev/null +++ b/playwright/e2e/page-links-handler.spec.ts @@ -0,0 +1,124 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { + GetCollectiveUrlParameters, + SameTabLinkTestCaseData, +} from '../support/helpers/links.ts' + +import { mergeTests } from '@playwright/test' +import { test as createCollectiveTest } from '../support/fixtures/create-collectives.ts' +import { test as editorTest } from '../support/fixtures/editor.ts' +import { testLinkOpensInSameTab } from '../support/helpers/links.ts' +import { randomString } from '../support/helpers/randomString.ts' + +const triggers = ['preview', 'openLinkButton', 'ctrlClick'] as const + +const collectiveTest = createCollectiveTest.extend({ + // eslint-disable-next-line no-empty-pattern + collectiveConfigs: async ({}, use) => use([ + { + name: randomString(), + pages: [ + { title: 'Link Target', content: 'Some content' }, + { title: 'Link Source' }, + ], + }, + ]), +}) + +const test = mergeTests(collectiveTest, editorTest) + +test.describe('Link handler: authenticated → public share URL', () => { + for (const editMode of [false, true]) { + const modeLabel = editMode ? 'edit' : 'preview' + for (const trigger of triggers) { + test(`Opens public share URL link in same tab via ${trigger} (${modeLabel} mode)`, async ({ baseURL, collective, editor, page, user }) => { + test.skip( + trigger === 'ctrlClick' && process.env.PLAYWRIGHT_NC_SERVER_BRANCH === 'stable32', + 'ctrlClick handler not implemented on stable32', + ) + + const sourcePage = collective.getPageByTitle('Link Source') + const targetPage = collective.getPageByTitle('Link Target') + + if (!baseURL) { + throw new Error('baseURL is not defined') + } + + const share = await collective.createShare({ page }) + + const linkData: SameTabLinkTestCaseData = { + description: 'public share URL', + getLinkUrl: ({ targetPage }: GetCollectiveUrlParameters) => targetPage.getPageUrl(share.data.token), + getExpectedUrl: ({ baseURL, targetPage }: GetCollectiveUrlParameters) => (new URL(targetPage.getPageUrl(), baseURL)).href, + } + + await testLinkOpensInSameTab({ + baseURL, + page, + user, + editor, + sourcePage, + targetPage, + targetCollective: collective, + linkData, + editMode, + trigger, + }) + + await share.delete() + }) + } + } +}) + +test.describe('Link handler: public share → internal URL', () => { + for (const editMode of [false, true]) { + const modeLabel = editMode ? 'edit' : 'preview' + for (const trigger of triggers) { + test(`Opens internal URL link in same tab via ${trigger} in share context (${modeLabel} mode)`, async ({ baseURL, collective, editor, page, user }) => { + test.skip( + trigger === 'ctrlClick' && process.env.PLAYWRIGHT_NC_SERVER_BRANCH === 'stable32', + 'ctrlClick handler not implemented on stable32', + ) + + const sourcePage = collective.getPageByTitle('Link Source') + const targetPage = collective.getPageByTitle('Link Target') + + if (!baseURL) { + throw new Error('baseURL is not defined') + } + + const share = await collective.createShare({ page }) + if (editMode) { + await share.setEditable(true) + } + + const linkData: SameTabLinkTestCaseData = { + description: 'internal URL without share token', + getLinkUrl: ({ targetPage }: GetCollectiveUrlParameters) => targetPage.getPageUrl(), + getExpectedUrl: ({ baseURL, targetPage, shareToken }: GetCollectiveUrlParameters) => (new URL(targetPage.getPageUrl(shareToken), baseURL)).href, + } + + await testLinkOpensInSameTab({ + baseURL, + page, + user, + editor, + sourcePage, + targetPage, + targetCollective: collective, + linkData, + editMode, + shareToken: share.data.token, + trigger, + }) + + await share.delete() + }) + } + } +}) diff --git a/playwright/support/helpers/links.ts b/playwright/support/helpers/links.ts index 5a341f1c2..59fd31428 100644 --- a/playwright/support/helpers/links.ts +++ b/playwright/support/helpers/links.ts @@ -42,6 +42,8 @@ export type NewTabLinkTestCaseData = { getExpectedUrl: (params: GetUrlParameters) => string } +type LinkTrigger = 'preview' | 'openLinkButton' | 'ctrlClick' + export type ViewerLinkTestCase = { page: Page user: User @@ -63,6 +65,7 @@ export type SameTabLinkTestCase = { linkData: SameTabLinkTestCaseData editMode: boolean shareToken?: string + trigger?: LinkTrigger } export type NewTabLinkTestCase = { @@ -111,7 +114,7 @@ export async function testLinkOpensInViewer({ await sourcePage.open(false) await sourcePage.switchMode(editMode) editor.setMode(editMode) - await editor.openLink({ linkText }) + await editor.openLinkViaBubblePreview({ linkText }) await expect(sourcePage.getViewerContent() .locator('.modal-header')) .toContainText(linkData.fixtureName) @@ -143,6 +146,7 @@ export async function testLinkOpensInViewer({ * @param options.linkData test case data * @param options.editMode whether to test in edit mode or preview mode * @param options.shareToken share token if the page is a share + * @param options.trigger trigger to open the link */ export async function testLinkOpensInSameTab({ baseURL, @@ -155,6 +159,7 @@ export async function testLinkOpensInSameTab({ linkData, editMode, shareToken, + trigger, }: SameTabLinkTestCase) { const linkText = 'Link Text' if (!targetPage || !targetCollective) { @@ -171,10 +176,17 @@ export async function testLinkOpensInSameTab({ await sourcePage.open(false, shareToken) await sourcePage.switchMode(editMode) editor.setMode(editMode) - await editor.openCollectiveLink({ - linkText, - pageTitle, - }) + + if (trigger === 'openLinkButton') { + await editor.openLinkViaOpenLinkButton({ linkText }) + } else if (trigger === 'ctrlClick') { + await editor.ctrlClickLink({ linkText }) + } else { + await editor.openCollectiveLinkViaBubblePreview({ + linkText, + pageTitle, + }) + } await expect(page).toHaveURL(linkData.getExpectedUrl({ baseURL, collective: targetCollective, targetPage, shareToken })) } @@ -215,7 +227,7 @@ export async function testLinkOpensInNewTab({ await sourcePage.switchMode(editMode) editor.setMode(editMode) const newTabPromise = page.waitForEvent('popup') - await editor.openLink({ linkText }) + await editor.openLinkViaBubblePreview({ linkText }) const newTab = await newTabPromise await newTab.waitForLoadState() diff --git a/playwright/support/sections/EditorSection.ts b/playwright/support/sections/EditorSection.ts index 0e0ef913d..3c0bf434d 100644 --- a/playwright/support/sections/EditorSection.ts +++ b/playwright/support/sections/EditorSection.ts @@ -75,34 +75,52 @@ export class EditorSection { await this.getContent() .getByRole('link', { name: linkText, exact: true }) .click() - await this.page.locator('.widgets--list') + await this.page.locator('.link-view-bubble') .waitFor({ state: 'visible' }) - return this.page.locator('.widgets--list') + return this.page.locator('.link-view-bubble') } public async hasCollectiveLink(linkText: string): Promise { await expect((await this.getLinkBubble(linkText)) - .locator('.collective-page .title')) + .locator('.widgets--list .collective-page .title')) .toHaveText(linkText) // Click somewhere else to close the link bubble await this.getContent() .click() } - public async openLink({ linkText }: { + public async openLinkViaBubblePreview({ linkText }: { linkText: string }): Promise { - const link = await this.getLinkBubble(linkText) - await link + const linkBubble = await this.getLinkBubble(linkText) + await linkBubble + .locator('.widgets--list') .getByRole('link') .click() } + public async openLinkViaOpenLinkButton({ linkText }: { + linkText: string + }): Promise { + const linkBubble = await this.getLinkBubble(linkText) + await linkBubble + .getByRole('button', { name: 'Open link' }) + .click() + } + + public async ctrlClickLink({ linkText }: { + linkText: string + }): Promise { + await this.getContent() + .getByRole('link', { name: linkText, exact: true }) + .click({ modifiers: ['Control'] }) + } + public async save(): Promise { await this.editor.getByRole('button', { name: 'Save document' }).click() } - public async openCollectiveLink({ linkText, pageTitle }: { + public async openCollectiveLinkViaBubblePreview({ linkText, pageTitle }: { linkText: string pageTitle?: string }): Promise { diff --git a/src/components/PagePreview.vue b/src/components/PagePreview.vue index 46dcc885a..22e107c2d 100644 --- a/src/components/PagePreview.vue +++ b/src/components/PagePreview.vue @@ -53,7 +53,6 @@