diff --git a/cypress/e2e/directediting.spec.js b/cypress/e2e/directediting.spec.js index 6bef63d7a5d..0b3d633dd8e 100644 --- a/cypress/e2e/directediting.spec.js +++ b/cypress/e2e/directediting.spec.js @@ -14,10 +14,8 @@ function enterContentAndClose() { cy.intercept({ method: 'POST', url: '**/session/*/close' }).as('closeRequest') cy.intercept({ method: 'POST', url: '**/session/*/push' }).as('push') cy.intercept({ method: 'POST', url: '**/session/*/sync' }).as('sync') - cy.getContent().type('# This is a headline') - cy.getContent().type('{enter}') - cy.getContent().type('Some text') - cy.getContent().type('{enter}') + cy.insertLine('# This is a headline') + cy.insertLine('Some text') cy.getContent().type('{ctrl+s}') cy.wait('@push') cy.wait('@sync') diff --git a/cypress/e2e/nodes/CodeBlock.spec.js b/cypress/e2e/nodes/CodeBlock.spec.js index 996651b9486..bc1cfe7c6c9 100644 --- a/cypress/e2e/nodes/CodeBlock.spec.js +++ b/cypress/e2e/nodes/CodeBlock.spec.js @@ -89,7 +89,7 @@ describe('Front matter support', function() { cy.isolateTest({ sourceFile: 'codeblock.md' }) cy.openFile('codeblock.md').then(() => { cy.clearContent() - cy.getContent().type('{enter}```javascript{enter}') + cy.insertLine('```javascript') cy.getContent().type('const foo = "bar"{enter}{enter}{enter}') cy.getContent().find('.hljs-keyword').first().contains('const') }) @@ -108,7 +108,7 @@ describe('Front matter support', function() { it('Add an invalid mermaid block', function() { cy.isolateTest() cy.openFile('empty.md').then(() => { - cy.getContent().type('```mermaid{enter}') + cy.insertLine('```mermaid') cy.getContent().find('code').should('exist') cy.getContent().get('.split-view__preview').should('be.visible') // eslint-disable-next-line cypress/no-unnecessary-waiting @@ -123,7 +123,7 @@ describe('Front matter support', function() { it('Add a valid mermaid block', function() { cy.isolateTest() cy.openFile('empty.md').then(() => { - cy.getContent().type('```mermaid{enter}') + cy.insertLine('```mermaid') cy.getContent().find('code').should('exist') cy.getContent().get('.split-view__preview').should('be.visible') // eslint-disable-next-line cypress/no-unnecessary-waiting diff --git a/cypress/e2e/nodes/Links.spec.js b/cypress/e2e/nodes/Links.spec.js index fe77952f894..40518b7b937 100644 --- a/cypress/e2e/nodes/Links.spec.js +++ b/cypress/e2e/nodes/Links.spec.js @@ -3,14 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { initUserAndFiles, randUser } from '../../utils/index.js' +import { randUser } from '../../utils/index.js' const user = randUser() const fileName = 'empty.md' describe('test link marks', function() { before(function() { - initUserAndFiles(user) + cy.createUser(user) }) beforeEach(function() { @@ -26,14 +26,17 @@ describe('test link marks', function() { }) describe('link bubble', function() { - it('shows a link preview in the bubble after clicking link', () => { - const link = 'https://nextcloud.com/' - cy.getContent() - .type(`${link}{enter}`) + function clickLink(link, options = {}) { cy.getContent() .find(`a[href*="${link}"]`) - .click() + .click(options) + } + + it('shows a link preview in the bubble after clicking link', () => { + const link = 'https://nextcloud.com/' + cy.insertLine(link) + clickLink(link) cy.get('.link-view-bubble .widget-default', { timeout: 10000 }) .find('.widget-default--name') @@ -43,8 +46,7 @@ describe('test link marks', function() { it('shows a link preview in the bubble after browsing to link', () => { const link = 'https://nextcloud.com/' - cy.getContent() - .type(`${link}{enter}`) + cy.insertLine(link) cy.getContent() .find(`a[href*="${link}"]`) @@ -58,12 +60,9 @@ describe('test link marks', function() { it('closes the link bubble when clicking elsewhere', () => { const link = 'https://nextcloud.com/' - cy.getContent() - .type(`${link}{enter}`) - cy.getContent() - .find(`a[href*="${link}"]`) - cy.getContent() - .type('{upArrow}') + cy.insertLine(link) + clickLink(link) + cy.get('.link-view-bubble .widget-default', { timeout: 10000 }) .find('.widget-default--name') .contains('Nextcloud') @@ -75,10 +74,8 @@ describe('test link marks', function() { }) it('allows to edit a link in the bubble', () => { - cy.getContent() - .type('https://example.org{enter}') - cy.getContent() - .type('{upArrow}{rightArrow}') + cy.insertLine('https://example.org') + clickLink('https://example.org') cy.get('.link-view-bubble button[title="Edit link"]') .click() @@ -96,10 +93,8 @@ describe('test link marks', function() { it('allows to remove a link in the bubble', () => { const link = 'https://nextcloud.com' - cy.getContent() - .type(`${link}{enter}`) - cy.getContent() - .type('{upArrow}{rightArrow}') + cy.insertLine(link) + clickLink(link) cy.get('.link-view-bubble button[title="Remove link"]') .click() @@ -112,17 +107,39 @@ describe('test link marks', function() { it('Ctrl-click on a link opens a new tab', () => { const link = 'https://nextcloud.com/' - cy.getContent() - .type(`${link}{enter}`) + cy.insertLine(link) - cy.getContent() - .find(`a[href*="${link}"]`) - .click({ ctrlKey: true }) + 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://nextcloud.com/' + cy.insertLine(`[text](${link})`) + clickLink(link) + cy.get('.link-view-bubble .widget-default', { timeout: 10000 }) + .find('.widget-default--name') + .contains('Nextcloud') + 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') + clickLink('#') + cy.get('.link-view-bubble__title', { timeout: 10000 }) + .contains('other://protocol') + cy.get('.link-view-bubble a') + .should('not.exist') + }) + }) describe('autolink', function() { @@ -133,8 +150,7 @@ describe('test link marks', function() { const link = `${Cypress.env('baseUrl')}/apps/files/?dir=/&openfile=${id}#relPath=/${fileName}` cy.clearContent() - cy.getContent() - .type(`${link}{enter}`) + cy.insertLine(link) cy.getContent() .find(`a[href*="${Cypress.env('baseUrl')}"]`) @@ -143,16 +159,14 @@ describe('test link marks', function() { it('without protocol', () => { cy.clearContent() - cy.getContent() - .type('google.com{enter}') + cy.insertLine('google.com') cy.getContent() .find('a[href*="google.com"]') .should('not.exist') }) it('with protocol but without space', () => { - cy.getContent() - .type('https://nextcloud.com') + cy.getContent().type('https://nextcloud.com') cy.getContent() .find('a[href*="nextcloud.com"]') diff --git a/cypress/e2e/sections.spec.js b/cypress/e2e/sections.spec.js index cc486bb73fa..ddc8e31b7c5 100644 --- a/cypress/e2e/sections.spec.js +++ b/cypress/e2e/sections.spec.js @@ -50,7 +50,7 @@ describe('Content Sections', () => { it('Anchor ID is updated', () => { cy.visitTestFolder() cy.openFile(fileName, { force: true }) - cy.getContent().type('# Heading 1{enter}') + cy.insertLine('# Heading 1') cy.getContent() .find('h1 > a') .should('have.attr', 'id') @@ -89,8 +89,7 @@ describe('Content Sections', () => { cy.visitTestFolder() cy.openFile(fileName, { force: true }) // Issue #2868 - cy.getContent() - .type('# Heading 1{enter}') + cy.insertLine('# Heading 1') cy.getContent() .find('h1 > a') .should('have.attr', 'id') diff --git a/cypress/e2e/sync.spec.js b/cypress/e2e/sync.spec.js index 0cbe5359a0c..6fcd092b5ce 100644 --- a/cypress/e2e/sync.spec.js +++ b/cypress/e2e/sync.spec.js @@ -21,7 +21,7 @@ describe('Sync', () => { cy.openTestFile() cy.wait('@sync', { timeout: 10000 }) cy.getContent().find('h2').should('contain', 'Hello world') - cy.getContent().type('{moveToEnd}* Saving the doc saves the doc state{enter}') + cy.insertLine('{moveToEnd}* Saving the doc saves the doc state') cy.wait('@sync', { timeout: 10000 }) }) @@ -60,7 +60,7 @@ describe('Sync', () => { .should('not.contain', 'Document could not be loaded.') // FIXME: There seems to be a bug where typed words maybe lost if not waiting for the new session cy.wait('@syncAfterRecovery', { timeout: 10000 }) - cy.getContent().type('* more content added after the lost connection{enter}') + cy.insertLine('* more content added after the lost connection') cy.wait('@syncAfterRecovery', { timeout: 10000 }) cy.closeFile() cy.testName() @@ -120,7 +120,7 @@ describe('Sync', () => { .should('not.contain', 'Document could not be loaded.') // FIXME: There seems to be a bug where typed words maybe lost if not waiting for the new session cy.wait('@syncAfterRecovery', { timeout: 10000 }) - cy.getContent().type('* more content added after the lost connection{enter}') + cy.insertLine('* more content added after the lost connection') cy.wait('@syncAfterRecovery', { timeout: 10000 }) cy.closeFile() cy.testName() diff --git a/cypress/e2e/viewer.spec.js b/cypress/e2e/viewer.spec.js index 671c8fe9abd..b8d89c3478e 100644 --- a/cypress/e2e/viewer.spec.js +++ b/cypress/e2e/viewer.spec.js @@ -78,8 +78,7 @@ describe('Open test.md in viewer', function() { // This used to break with the focus trap that the viewer modal has cy.openFile('empty.md') - cy.getContent() - .type('- test{enter}') + cy.insertLine('- test') // Cypress does not have native tab key support, though this seems to work // for triggering the key handler of tiptap diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 1e50668566e..8cf877f7f8f 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -373,6 +373,11 @@ Cypress.Commands.add('clearContent', () => { cy.getContent() }) +Cypress.Commands.add('insertLine', (line = '') => { + cy.getContent() + .type(`${line}{enter}`) +}) + Cypress.Commands.add('openWorkspace', () => { cy.createDescription() cy.get('#rich-workspace .editor__content').click({ force: true }) diff --git a/src/components/Link/LinkBubbleView.vue b/src/components/Link/LinkBubbleView.vue index 3a991ae4184..ed5bd188b39 100644 --- a/src/components/Link/LinkBubbleView.vue +++ b/src/components/Link/LinkBubbleView.vue @@ -74,7 +74,7 @@ - { return { href: match.groups.href } } @@ -58,9 +60,13 @@ const Link = TipTapLink.extend({ renderHTML(options) { const { mark } = options + const url = new URL(mark.attrs.href, window.location) + const href = PROTOCOLS_TO_LINK_TO.includes(url.protocol) + ? domHref(mark, this.options.relativePath) + : '#' return ['a', { ...mark.attrs, - href: domHref(mark, this.options.relativePath), + href, 'data-md-href': mark.attrs.href, rel: 'noopener noreferrer nofollow', }, 0] diff --git a/src/tests/helpers/links.spec.js b/src/tests/helpers/links.spec.js index 22452fac6ab..e31c4740e6d 100644 --- a/src/tests/helpers/links.spec.js +++ b/src/tests/helpers/links.spec.js @@ -21,10 +21,12 @@ global._oc_webroot = '' jest.mock('@nextcloud/initial-state') loadState.mockImplementation((app, key) => 'files') +const linkTo = href => domHref({ attrs: { href } }) + describe('Preparing href attributes for the DOM', () => { test('leave empty hrefs alone', () => { - expect(domHref({attrs: {href: ''}})).toBe('') + expect(linkTo('')).toBe('') }) test('leave undefined hrefs alone', () => { @@ -32,32 +34,30 @@ describe('Preparing href attributes for the DOM', () => { }) test('full url', () => { - expect(domHref({attrs: {href: 'https://otherdomain.tld'}})) - .toBe('https://otherdomain.tld') + expect(linkTo('https://otherdomain.tld')).toBe('https://otherdomain.tld') }) - test('other protocol', () => { - expect(domHref({attrs: {href: 'mailTo:test@mail.example'}})) - .toBe('mailTo:test@mail.example') + test('other protocols', () => { + expect(linkTo('mailto:name@otherdomain.tld')).toBe('mailto:name@otherdomain.tld') }) test('relative link with fileid (old format from file picker)', () => { - expect(domHref({attrs: {href: 'otherfile?fileId=123'}})) + expect(linkTo('otherfile?fileId=123')) .toBe('http://localhost/f/123') }) test('relative path with ../ (old format from file picker)', () => { - expect(domHref({attrs: {href: '../other/otherfile?fileId=123'}})) + expect(linkTo('../other/otherfile?fileId=123')) .toBe('http://localhost/f/123') }) test('absolute path (old format from file picker)', () => { - expect(domHref({attrs: {href: '/other/otherfile?fileId=123'}})) + expect(linkTo('/other/otherfile?fileId=123')) .toBe('http://localhost/f/123') }) test('absolute path (old format from file picker)', () => { - expect(domHref({attrs: {href: '/otherfile?fileId=123'}})) + expect(linkTo('/otherfile?fileId=123')) .toBe('http://localhost/f/123') }) @@ -139,7 +139,7 @@ describe('Preparing href attributes for the DOM in Collectives app', () => { }) test('relative link with fileid in Collectives', () => { - expect(domHref({attrs: {href: 'otherfile?fileId=123'}})) + expect(linkTo('otherfile?fileId=123')) .toBe('otherfile?fileId=123') }) })