Skip to content
Open
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
113 changes: 0 additions & 113 deletions cypress/e2e/Links.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,119 +24,6 @@ describe('test link marks', function() {
cy.openFile(fileName, { force: true })
})

describe('link bubble', function() {
/**
* Find link and click on it
*
* @param {string} link The link URL
* @param {object|null} options the click options
*/
const clickLink = (link, options = {}) => {
cy.getContent().find(`a[href*="${link}"]`).click(options)
}

it('shows a link preview in the bubble after clicking link', () => {
const link = 'https://example.org/'
cy.insertLine(link)
clickLink(link)

cy.get('.link-view-bubble .widget-default', { timeout: 10000 })
.find('.widget-default--name')
.contains('Example Domain')
.click()
})

it('shows a link preview in the bubble after browsing to link', () => {
const link = 'https://example.org/'
cy.insertLine(link)
cy.getContent().find(`a[href*="${link}"]`)

cy.getContent().type('{upArrow}')

cy.get('.link-view-bubble .widget-default', { timeout: 10000 })
.find('.widget-default--name')
.contains('Example Domain')
})

it('open button opens a new tab', () => {
const link = 'https://example.org/'
cy.insertLine(link)
clickLink(link)

cy.get('.link-view-bubble button[title="Open link"]').click()

cy.get('@winOpen').should('have.been.calledOnce')
})

it('closes the link bubble when clicking elsewhere', () => {
const link = 'https://example.org/'
cy.insertLine(link)
clickLink(link)

cy.get('.link-view-bubble .widget-default', { timeout: 10000 })
.find('.widget-default--name')
.contains('Example Domain')

cy.get('[role="dialog"] h2.modal-header__name')
.contains(fileName)
.click()

cy.get('.link-view-bubble .widget-default').should('not.exist')
})

it('allows to edit a link in the bubble', () => {
cy.insertLine('https://example.com')
clickLink('https://example.com')

cy.get('.link-view-bubble button[title="Edit link"]').click()

cy.get('.link-view-bubble input').type('{selectAll}https://example.org')

cy.get('.link-view-bubble button[title="Save changes"]').click()

cy.getContent().find('a[href*="https://example.org"]')
})

it('allows to remove a link in the bubble', () => {
const link = 'https://example.org'
cy.insertLine(link)
clickLink(link)

cy.get('.link-view-bubble .link-options').click()
cy.get('button').contains('Remove').click()

cy.getContent().find(`a[href*="${link}"]`).should('not.exist')
})

it('Ctrl-click on a link opens a new tab', () => {
const link = 'https://example.org/'
cy.insertLine(link)

clickLink(link, { ctrlKey: true })

cy.get('@winOpen')
.should('have.been.calledOnce')
.should('have.been.calledWith', link)
})

it('Handles typed in markdown links with text', () => {
const link = 'https://example.org/'
cy.insertLine(`[text](${link})`)
clickLink(link)
cy.get('.link-view-bubble .widget-default', { timeout: 10000 })
.find('.widget-default--name')
.contains('Example Domain')
cy.get('.link-view-bubble a').should('have.attr', 'href', link)
})

it('Leaves out link to other protocols', () => {
const link = 'other://protocol'
cy.insertLine(`[text](${link})`)
cy.getContent().find(`a[href*="${link}"]`).should('not.exist')
cy.getContent().find('a[href="#]').should('not.exist')
})
})

describe('autolink', function() {
it('with protocol to files app and fileId', () => {
cy.getFile(fileName).then(($el) => {
Expand Down
148 changes: 148 additions & 0 deletions playwright/e2e/links.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { expect, mergeTests } from '@playwright/test'
import { test as editorTest } from '../support/fixtures/editor.ts'
import { test as uploadFileTest } from '../support/fixtures/upload-file.ts'

const test = mergeTests(editorTest, uploadFileTest)

const href = 'https://example.org/'

test.describe('links', () => {
test.use({ fileContent: `[Example](${href})\n\nsecond paragraph\n` })

test.beforeEach(async ({ open }) => {
await open()
})

test('click opens the link', async ({ editor, page }) => {
const popupPromise = page.waitForEvent('popup')
await editor.content.getByRole('link', { name: 'Example' }).click()
const popup = await popupPromise
expect(popup.url()).toBe(href)
await popup.close()
})

test('ctrl-click opens the link', async ({ editor, page }) => {
const popupPromise = page.waitForEvent('popup')
await editor.content.getByRole('link', { name: 'Example' })
.click({ modifiers: ['Control'] })
const popup = await popupPromise
expect(popup.url()).toBe(href)
await popup.close()
})
})

test.describe('link bubble', () => {
test.use({ fileContent: `[Example](${href})\n\nsecond paragraph\n` })

test.beforeEach(async ({ open }) => {
await open()
})

test('hover opens the link bubble', async ({ editor, page }) => {
await editor.content.getByRole('link', { name: 'Example' }).hover()
const bubble = page.locator('.link-view-bubble')
await expect(bubble).toBeVisible()
await expect(bubble.locator('.link-view-bubble__title')).toContainText(/example/i)
})

test('moving the cursor into the link opens the link bubble', async ({ editor, page }) => {
await editor.content.getByText('second paragraph').click()
await editor.press('Home')
await editor.press('ArrowUp')
await editor.press('ArrowRight')
await expect(page.locator('.link-view-bubble')).toBeVisible()
})

test('pill button opens the link bubble without opening the link', async ({ editor, page }) => {
let popups = 0
page.on('popup', () => popups++)
await editor.content.locator('.link-pill').click()
await expect(page.locator('.link-view-bubble')).toBeVisible()
expect(popups).toBe(0)
})

test('open button in the link bubble opens the link', async ({ editor, page }) => {
await editor.content.locator('.link-pill').click()
const bubble = page.locator('.link-view-bubble')
const popupPromise = page.waitForEvent('popup')
await bubble.getByRole('button', { name: 'Open link' }).click()
const popup = await popupPromise
expect(popup.url()).toBe(href)
await popup.close()
})

test('edits the hovered link while the cursor is elsewhere', async ({ editor, page }) => {
await editor.content.getByText('second paragraph').click()
await editor.content.getByRole('link', { name: 'Example' }).hover()
const bubble = page.locator('.link-view-bubble')
await bubble.getByRole('button', { name: 'Edit link' }).click()
await bubble.getByLabel('URL').fill('https://example.com/')

Check failure on line 84 in playwright/e2e/links.spec.ts

View workflow job for this annotation

GitHub Actions / playwright (2, 3)

[chromium] › playwright/e2e/links.spec.ts:79:2 › link bubble › edits the hovered link while the cursor is elsewhere

1) [chromium] › playwright/e2e/links.spec.ts:79:2 › link bubble › edits the hovered link while the cursor is elsewhere Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: locator.fill: Target page, context or browser has been closed Call log: - waiting for locator('.link-view-bubble').getByLabel('URL') 82 | const bubble = page.locator('.link-view-bubble') 83 | await bubble.getByRole('button', { name: 'Edit link' }).click() > 84 | await bubble.getByLabel('URL').fill('https://example.com/') | ^ 85 | await bubble.getByLabel('URL').press('Enter') 86 | await expect(editor.content.getByRole('link', { name: 'Example' })) 87 | .toHaveAttribute('href', 'https://example.com/') at /home/runner/actions-runner/_work/text/text/playwright/e2e/links.spec.ts:84:34

Check failure on line 84 in playwright/e2e/links.spec.ts

View workflow job for this annotation

GitHub Actions / playwright (2, 3)

[chromium] › playwright/e2e/links.spec.ts:79:2 › link bubble › edits the hovered link while the cursor is elsewhere

1) [chromium] › playwright/e2e/links.spec.ts:79:2 › link bubble › edits the hovered link while the cursor is elsewhere Error: locator.fill: Target page, context or browser has been closed Call log: - waiting for locator('.link-view-bubble').getByLabel('URL') - locator resolved to <input type="text" name="newHref" id="nc-vue-25" placeholder="" data-v-feb04bef="" aria-live="polite" class="input-field__input" value="https://example.org/"/> - fill("https://example.com/") - attempting fill action - waiting for element to be visible, enabled and editable - element was detached from the DOM, retrying 82 | const bubble = page.locator('.link-view-bubble') 83 | await bubble.getByRole('button', { name: 'Edit link' }).click() > 84 | await bubble.getByLabel('URL').fill('https://example.com/') | ^ 85 | await bubble.getByLabel('URL').press('Enter') 86 | await expect(editor.content.getByRole('link', { name: 'Example' })) 87 | .toHaveAttribute('href', 'https://example.com/') at /home/runner/actions-runner/_work/text/text/playwright/e2e/links.spec.ts:84:34
await bubble.getByLabel('URL').press('Enter')
await expect(editor.content.getByRole('link', { name: 'Example' }))
.toHaveAttribute('href', 'https://example.com/')
await expect(editor.content.getByRole('link')).toHaveCount(1)
})

test('edits the link text from the bubble', async ({ editor, page }) => {
await editor.content.locator('.link-pill').click()
const bubble = page.locator('.link-view-bubble')
await bubble.getByRole('button', { name: 'Edit link' }).click()
await expect(bubble.getByLabel('Link text')).toHaveValue('Example')
await bubble.getByLabel('Link text').fill('Renamed')
await bubble.getByLabel('Link text').press('Enter')

Check failure on line 97 in playwright/e2e/links.spec.ts

View workflow job for this annotation

GitHub Actions / playwright (2, 3)

[chromium] › playwright/e2e/links.spec.ts:91:2 › link bubble › edits the link text from the bubble

2) [chromium] › playwright/e2e/links.spec.ts:91:2 › link bubble › edits the link text from the bubble Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: locator.press: Target page, context or browser has been closed Call log: - waiting for locator('.link-view-bubble').getByLabel('Link text') 95 | await expect(bubble.getByLabel('Link text')).toHaveValue('Example') 96 | await bubble.getByLabel('Link text').fill('Renamed') > 97 | await bubble.getByLabel('Link text').press('Enter') | ^ 98 | await expect(editor.content.getByRole('link', { name: 'Renamed' })) 99 | .toHaveAttribute('href', href) 100 | await expect(editor.content.getByRole('link')).toHaveCount(1) at /home/runner/actions-runner/_work/text/text/playwright/e2e/links.spec.ts:97:40

Check failure on line 97 in playwright/e2e/links.spec.ts

View workflow job for this annotation

GitHub Actions / playwright (2, 3)

[chromium] › playwright/e2e/links.spec.ts:91:2 › link bubble › edits the link text from the bubble

2) [chromium] › playwright/e2e/links.spec.ts:91:2 › link bubble › edits the link text from the bubble Error: locator.press: Target page, context or browser has been closed Call log: - waiting for locator('.link-view-bubble').getByLabel('Link text') 95 | await expect(bubble.getByLabel('Link text')).toHaveValue('Example') 96 | await bubble.getByLabel('Link text').fill('Renamed') > 97 | await bubble.getByLabel('Link text').press('Enter') | ^ 98 | await expect(editor.content.getByRole('link', { name: 'Renamed' })) 99 | .toHaveAttribute('href', href) 100 | await expect(editor.content.getByRole('link')).toHaveCount(1) at /home/runner/actions-runner/_work/text/text/playwright/e2e/links.spec.ts:97:40
await expect(editor.content.getByRole('link', { name: 'Renamed' }))
.toHaveAttribute('href', href)
await expect(editor.content.getByRole('link')).toHaveCount(1)
await expect(editor.content).not.toContainText('Example')
})

test('removes the link from the bubble', async ({ editor, page }) => {
await editor.content.locator('.link-pill').click()
await page.locator('.link-view-bubble .link-options button').click()
await page.getByRole('menuitem', { name: 'Remove link' }).click()
await expect(editor.content.getByRole('link')).toHaveCount(0)
await expect(editor.content).toContainText('Example')
})

test('link typed in markdown syntax gets the link bubble', async ({ editor, page }) => {
await editor.content.getByText('second paragraph').click()
await editor.press('End')
await editor.press('Enter')
await editor.type('[typed](https://example.com/)')
const link = editor.content.getByRole('link', { name: 'typed' })
await expect(link).toHaveAttribute('href', 'https://example.com/')
await link.hover()
const bubble = page.locator('.link-view-bubble')
await expect(bubble).toBeVisible()
await expect(bubble.locator('.link-view-bubble__title')).toContainText(/example\.com/)

Check failure on line 122 in playwright/e2e/links.spec.ts

View workflow job for this annotation

GitHub Actions / playwright (2, 3)

[chromium] › playwright/e2e/links.spec.ts:112:2 › link bubble › link typed in markdown syntax gets the link bubble

3) [chromium] › playwright/e2e/links.spec.ts:112:2 › link bubble › link typed in markdown syntax gets the link bubble Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(locator).toContainText(expected) failed Locator: locator('.link-view-bubble').locator('.link-view-bubble__title') Expected pattern: /example\.com/ Received string: "Example Domain" Timeout: 5000ms Call log: - Expect "toContainText" with timeout 5000ms - waiting for locator('.link-view-bubble').locator('.link-view-bubble__title') 14 × locator resolved to <div data-v-14986300="" class="link-view-bubble__title">Example Domain</div> - unexpected value "Example Domain" 120 | const bubble = page.locator('.link-view-bubble') 121 | await expect(bubble).toBeVisible() > 122 | await expect(bubble.locator('.link-view-bubble__title')).toContainText(/example\.com/) | ^ 123 | }) 124 | 125 | test('mod-k turns the selection into a link and focuses the URL field', async ({ editor, page }) => { at /home/runner/actions-runner/_work/text/text/playwright/e2e/links.spec.ts:122:60

Check failure on line 122 in playwright/e2e/links.spec.ts

View workflow job for this annotation

GitHub Actions / playwright (2, 3)

[chromium] › playwright/e2e/links.spec.ts:112:2 › link bubble › link typed in markdown syntax gets the link bubble

3) [chromium] › playwright/e2e/links.spec.ts:112:2 › link bubble › link typed in markdown syntax gets the link bubble Error: expect(locator).toContainText(expected) failed Locator: locator('.link-view-bubble').locator('.link-view-bubble__title') Expected pattern: /example\.com/ Received string: "Example Domain" Timeout: 5000ms Call log: - Expect "toContainText" with timeout 5000ms - waiting for locator('.link-view-bubble').locator('.link-view-bubble__title') 14 × locator resolved to <div data-v-14986300="" class="link-view-bubble__title">Example Domain</div> - unexpected value "Example Domain" 120 | const bubble = page.locator('.link-view-bubble') 121 | await expect(bubble).toBeVisible() > 122 | await expect(bubble.locator('.link-view-bubble__title')).toContainText(/example\.com/) | ^ 123 | }) 124 | 125 | test('mod-k turns the selection into a link and focuses the URL field', async ({ editor, page }) => { at /home/runner/actions-runner/_work/text/text/playwright/e2e/links.spec.ts:122:60
})

test('mod-k turns the selection into a link and focuses the URL field', async ({ editor, page }) => {
await editor.content.getByText('second paragraph').click()
await editor.press('End')
await editor.press('Shift+Home')
await editor.press('Control+k')
const bubble = page.locator('.link-view-bubble')
await expect(bubble).toBeVisible()
await expect(bubble.getByLabel('URL')).toBeFocused()
await bubble.getByLabel('URL').fill('https://example.com/')
await bubble.getByLabel('URL').press('Enter')
await expect(editor.content.getByRole('link', { name: 'second paragraph' }))
.toHaveAttribute('href', 'https://example.com/')
})
})

test.describe('links with unsafe protocols', () => {
test.use({ fileContent: '[text](other://protocol)\n' })

test('are rendered without href', async ({ editor, open }) => {
await open()
await expect(editor.content.getByText('text')).toBeVisible()
await expect(editor.content.locator('a[href*="other://"]')).toHaveCount(0)
})
})
Loading
Loading