Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
124 changes: 124 additions & 0 deletions playwright/e2e/page-links-handler.spec.ts
Original file line number Diff line number Diff line change
@@ -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()
})
}
}
})
24 changes: 18 additions & 6 deletions playwright/support/helpers/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export type NewTabLinkTestCaseData = {
getExpectedUrl: (params: GetUrlParameters) => string
}

type LinkTrigger = 'preview' | 'openLinkButton' | 'ctrlClick'

export type ViewerLinkTestCase = {
page: Page
user: User
Expand All @@ -63,6 +65,7 @@ export type SameTabLinkTestCase = {
linkData: SameTabLinkTestCaseData
editMode: boolean
shareToken?: string
trigger?: LinkTrigger
}

export type NewTabLinkTestCase = {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -155,6 +159,7 @@ export async function testLinkOpensInSameTab({
linkData,
editMode,
shareToken,
trigger,
}: SameTabLinkTestCase) {
const linkText = 'Link Text'
if (!targetPage || !targetCollective) {
Expand All @@ -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 }))
}
Expand Down Expand Up @@ -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()

Expand Down
32 changes: 25 additions & 7 deletions playwright/support/sections/EditorSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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<void> {
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<void> {
const linkBubble = await this.getLinkBubble(linkText)
await linkBubble
.getByRole('button', { name: 'Open link' })
.click()
}

public async ctrlClickLink({ linkText }: {
linkText: string
}): Promise<void> {
await this.getContent()
.getByRole('link', { name: linkText, exact: true })
.click({ modifiers: ['Control'] })
}

public async save(): Promise<void> {
await this.editor.getByRole('button', { name: 'Save document' }).click()
}

public async openCollectiveLink({ linkText, pageTitle }: {
public async openCollectiveLinkViaBubblePreview({ linkText, pageTitle }: {
linkText: string
pageTitle?: string
}): Promise<void> {
Expand Down
12 changes: 3 additions & 9 deletions src/components/PagePreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@

<script lang="ts">
import { t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { defineComponent } from 'vue'
import NcUserBubble from '@nextcloud/vue/components/NcUserBubble'
import PageIcon from './Icon/PageIcon.vue'
Expand Down Expand Up @@ -143,18 +142,13 @@ export default defineComponent({
t,

clickLink(event: Event) {
if (this.notFound || !this.link) {
if (!this.link) {
return false
}

const appUrl = '/apps/collectives'
const linkUrl = new URL(this.link, window.location)
// Only consider rerouting if we're inside the collectives app and for links to collectives app
if (window.OCA.Collectives?.vueRouter
&& linkUrl.pathname.toString().startsWith(generateUrl(appUrl))) {
if (window.OCA.Collectives?.openLink) {
event.preventDefault()
const collectivesUrl = linkUrl.href.substring(linkUrl.href.indexOf(appUrl) + appUrl.length)
window.OCA.Collectives.vueRouter.push(collectivesUrl)
window.OCA.Collectives.openLink(this.link)
}
},
},
Expand Down
Loading
Loading