diff --git a/cypress/e2e/SmartPicker.spec.js b/cypress/e2e/SmartPicker.spec.js new file mode 100644 index 00000000000..8a9fbab8355 --- /dev/null +++ b/cypress/e2e/SmartPicker.spec.js @@ -0,0 +1,83 @@ +import { initUserAndFiles, randUser } from '../utils/index.js' + +const currentUser = randUser() + +const fileName = 'empty.md' + +describe('Smart picker', () => { + before(() => { + initUserAndFiles(currentUser, 'empty.md') + }) + + beforeEach(() => { + cy.login(currentUser) + cy.visit('/apps/files') + }) + + it('Type and see the smart picker', () => { + cy.isolateTest({ + sourceFile: fileName, + }) + cy.openFile(fileName, { force: true }) + + cy.getContent() + .type('/') + + cy.get('.tippy-box .suggestion-list').children().should(($children) => { + const entries = $children.find('.suggestion-list__item').map((i, el) => el.innerText).get() + expect(entries.length).to.be.greaterThan(0) + expect('To-Do list').to.be.oneOf(entries) + expect('Table').to.be.oneOf(entries) + }) + + cy.getContent() + .click({ force: true }) + + cy.getContent() + .type('Heading{enter}Hello World{enter}') + + cy.getContent() + .find('h1 [data-node-view-content]') + .should('have.text', 'Hello World') + }) + + it('Insert a link with the smart picker', () => { + cy.isolateTest({ + sourceFile: fileName, + }) + cy.openFile(fileName, { force: true }) + + cy.getContent() + .type('/Any') + + cy.get('.tippy-box .suggestion-list').children().should(($children) => { + const entries = $children.find('.suggestion-list__item').map((i, el) => el.innerText).get() + expect(entries.length).to.be.eq(1) + expect('Any link').to.be.oneOf(entries) + }) + + cy.getContent() + .click({ force: true }) + + cy.getContent() + .type('{enter}') + + cy.get('.reference-picker-modal--content') + .should('be.visible') + + cy.get('.reference-picker input') + .type('https://github.com') + + cy.get('.reference-widget') + .should('be.visible') + .should('contain.text', 'GitHub') + + cy.get('.reference-picker input') + .type('{enter}') + + cy.getContent() + .find('a') + .should('have.text', 'https://github.com') + + }) +}) diff --git a/cypress/e2e/nodes/CodeBlock.spec.js b/cypress/e2e/nodes/CodeBlock.spec.js new file mode 100644 index 00000000000..22bf3ad7d71 --- /dev/null +++ b/cypress/e2e/nodes/CodeBlock.spec.js @@ -0,0 +1,130 @@ +/** + * @copyright Copyright (c) 2022 + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { initUserAndFiles, randUser } from '../../utils/index.js' + +const user = randUser() + +describe('Front matter support', function() { + before(function() { + initUserAndFiles(user, 'codeblock.md', 'empty.md') + }) + + beforeEach(function() { + cy.login(user) + }) + + it('See existing code block', function() { + cy.isolateTest({ sourceFile: 'codeblock.md' }) + cy.openFile('codeblock.md').then(() => { + // Plain text block + cy.getContent().find('code').eq(0).find('.hljs-keyword').should('not.exist') + + // Javascript block + cy.getContent().find('code').eq(1).find('.hljs-keyword').eq(0).contains('const') + cy.getContent().find('code').eq(1).find('.hljs-string').eq(0).contains('"bar"') + cy.getContent().find('code').eq(1).find('.hljs-keyword').eq(1).contains('function') + + // Remove language + cy.getContent().find('.code-block').eq(1).find('.view-switch button').click() + // FIXME: Label behaviour changed, should be back once https://github.com/nextcloud-libraries/nextcloud-vue/pull/4484 is merged + // cy.get('.action-input__text-label').contains('Code block language') + cy.get('.input-field__input:visible').clear() + + cy.getContent().find('code').eq(1).click() + + cy.getContent().find('code').eq(1).find('.hljs-keyword').should('not.exist') + + // Re-add language + cy.getContent().find('.code-block').eq(1).find('.view-switch button').click() + cy.get('.input-field__input:visible').type('javascript') + + cy.getContent().find('code').eq(1).find('.hljs-keyword').eq(0).contains('const') + cy.getContent().find('code').eq(1).find('.hljs-string').eq(0).contains('"bar"') + cy.getContent().find('code').eq(1).find('.hljs-keyword').eq(1).contains('function') + }) + }) + + it('Add a code block', function() { + cy.isolateTest({ sourceFile: 'codeblock.md' }) + cy.openFile('codeblock.md').then(() => { + cy.clearContent() + cy.getContent().type('{enter}```javascript{enter}') + cy.getContent().type('const foo = "bar"{enter}{enter}{enter}') + cy.getContent().find('.hljs-keyword').first().contains('const') + }) + }) + + it('See a mermaid diagram', function() { + cy.isolateTest({ sourceFile: 'codeblock.md' }) + cy.openFile('codeblock.md').then(() => { + cy.getContent().find('.split-view__preview').find('svg').should('be.visible') + cy.get('.code-block').eq(2).find('code').should('not.be.visible') + cy.get('.split-view__preview').find('svg title') + .contains('Order example') + }) + }) + + it('Add an invalid mermaid block', function() { + cy.isolateTest() + cy.openFile('empty.md').then(() => { + cy.getContent().type('```mermaid{enter}') + cy.getContent().find('code').should('exist') + cy.getContent().get('.split-view__preview').should('be.visible') + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(250) + cy.getContent().type('invalid{enter}{enter}') + + cy.get('.split-view__code').find('code').should('be.visible') + cy.get('.split-view__preview').find('svg').should('not.exist') + }) + }) + + it('Add a valid mermaid block', function() { + cy.isolateTest() + cy.openFile('empty.md').then(() => { + cy.getContent().type('```mermaid{enter}') + cy.getContent().find('code').should('exist') + cy.getContent().get('.split-view__preview').should('be.visible') + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(250) + // Tab key does not work in cypress, using spaces instead + cy.getContent().type('flowchart LR{enter} entry{enter}') + + cy.get('.split-view__code').find('code').should('be.visible') + cy.get('.split-view__preview').find('svg').should('be.visible') + + cy.getContent().find('.code-block').eq(0).find('.view-switch button').click() + cy.get('.action-button').eq(0).contains('Source code').click() + cy.get('.split-view__code').find('code').should('be.visible') + cy.get('.split-view__preview').find('svg').should('not.be.visible') + + cy.getContent().find('.code-block').eq(0).find('.view-switch button').click() + cy.get('.action-button').eq(1).contains('Diagram').click() + cy.get('.split-view__code').find('code').should('not.be.visible') + cy.get('.split-view__preview').find('svg').should('be.visible') + + cy.getContent().find('.code-block').eq(0).find('.view-switch button').click() + cy.get('.action-button').eq(2).contains('Both').click() + cy.get('.split-view__code').find('code').should('be.visible') + cy.get('.split-view__preview').find('svg').should('be.visible') + }) + }) +}) diff --git a/cypress/e2e/versions.spec.js b/cypress/e2e/versions.spec.js new file mode 100644 index 00000000000..5d42cb2f56b --- /dev/null +++ b/cypress/e2e/versions.spec.js @@ -0,0 +1,120 @@ +import { initUserAndFiles, randUser } from '../utils/index.js' + +const currentUser = randUser() + +const versionFileName = 'versioned.md' + +describe('Versions', () => { + before(() => { + initUserAndFiles(currentUser, 'empty.md') + }) + + beforeEach(() => { + cy.login(currentUser) + cy.visit('/apps/files') + }) + + it('View versions with close timestamps', () => { + cy.isolateTest().then(({ folderName, fileName }) => { + const fullPath = folderName + '/' + versionFileName + cy.createFile(fullPath, '# V1', 'text/markdown', { 'x-oc-mtime': 1691420501 }) + cy.createFile(fullPath, '# V2', 'text/markdown', { 'x-oc-mtime': 1691420521 }) + cy.createFile(fullPath, '# V3', 'text/markdown') + + cy.reloadFileList() + + cy.get('[data-cy-files-list-row-name="' + versionFileName + '"] [data-cy-files-list-row-mtime]').click() + cy.get('.app-sidebar-header').should('be.visible').should('contain', versionFileName) + cy.get('.app-sidebar-tabs__tab[data-id="version_vue"]').click() + cy.get('[data-files-versions-versions-list] li > a').should('have.length', 3) + + cy.get('[data-files-versions-versions-list] li > a').eq(1).click() + cy.get('.viewer__content #read-only-editor') + .find('h1 [data-node-view-content]') + .should('have.text', 'V2') + + cy.get('[data-files-versions-versions-list] li > a').eq(2).click() + cy.get('.viewer__content #read-only-editor') + .find('h1 [data-node-view-content]') + .should('have.text', 'V1') + + cy.get('[data-files-versions-versions-list] li > a').eq(0).click() + cy.getContent() + .find('h1 [data-node-view-content]') + .should('have.text', 'V3') + }) + }) + + it('View versions', () => { + cy.isolateTest().then(({ folderName, fileName }) => { + const fullPath = folderName + '/' + versionFileName + cy.createFile(fullPath, '# V1', 'text/markdown', { 'x-oc-mtime': 1691420521 }) + cy.createFile(fullPath, '# V2', 'text/markdown', { 'x-oc-mtime': 1691521521 }) + cy.createFile(fullPath, '# V3') + + cy.reloadFileList() + + cy.get('[data-cy-files-list-row-name="' + versionFileName + '"] [data-cy-files-list-row-mtime]').click() + + cy.get('.app-sidebar-header').should('be.visible').should('contain', versionFileName) + + cy.get('.app-sidebar-tabs__tab[data-id="version_vue"]').click() + + cy.get('[data-files-versions-versions-list] li > a').should('have.length', 3) + + cy.get('[data-files-versions-versions-list] li > a').eq(1).click() + cy.get('.viewer__content #read-only-editor') + .find('h1 [data-node-view-content]') + .should('have.text', 'V2') + + cy.get('[data-files-versions-versions-list] li > a').eq(2).click() + cy.get('.viewer__content #read-only-editor') + .find('h1 [data-node-view-content]') + .should('have.text', 'V1') + + cy.get('[data-files-versions-versions-list] li > a').eq(0).click() + cy.getContent() + .find('h1 [data-node-view-content]') + .should('have.text', 'V3') + + cy.getContent() + .type('Hello') + }) + }) + + it('Compare versions', () => { + cy.isolateTest().then(({ folderName, fileName }) => { + const fullPath = folderName + '/' + versionFileName + cy.createFile(fullPath, '# V1', 'text/markdown', { 'x-oc-mtime': 1691420521 }) + cy.createFile(fullPath, '# V2', 'text/markdown', { 'x-oc-mtime': 1691521521 }) + cy.createFile(fullPath, '# V3') + + cy.reloadFileList() + + cy.get('[data-cy-files-list-row-name="' + versionFileName + '"] [data-cy-files-list-row-mtime]').click() + + cy.get('.app-sidebar-header').should('be.visible').should('contain', versionFileName) + + cy.get('.app-sidebar-tabs__tab[data-id="version_vue"]').click() + + cy.get('[data-files-versions-versions-list] li > a').should('have.length', 3) + + cy.get('[data-files-versions-versions-list] li').eq(2) + .find('button.action-item__menutoggle').first().click({ force: true }) + + cy.get('.v-popper__inner') + .find('button') + .eq(1) + .should('contain', 'Compare to current version') + .click() + + cy.get('.viewer__content #read-only-editor') + .find('h1 [data-node-view-content]') + .should('have.text', 'V1') + + cy.get('.viewer__content .viewer__file--active .ProseMirror') + .find('h1 [data-node-view-content]') + .should('contain.text', 'V3') + }) + }) +}) diff --git a/cypress/fixtures/codeblock.md b/cypress/fixtures/codeblock.md new file mode 100644 index 00000000000..88804e092d2 --- /dev/null +++ b/cypress/fixtures/codeblock.md @@ -0,0 +1,34 @@ +# Some code blocks + +## Plain + +``` +this is a plain text code block +``` + +## Javascript + +```javascript +const foo = "bar"; + +function add(a, b) { + return a + b +} + +console.log(add(1,2)) +``` + +## Mermaid + +```mermaid +--- +title: Order example +--- +erDiagram + CUSTOMER ||--o{ ORDER : places + ORDER ||--|{ LINE-ITEM : contains + CUSTOMER }|..|{ DELIVERY-ADDRESS : uses +``` + +## Some more + diff --git a/cypress/support/commands.js b/cypress/support/commands.js index e25582f7e90..a98e2141372 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -107,7 +107,7 @@ Cypress.Commands.add('downloadFile', (fileName) => { }) }) -Cypress.Commands.add('createFile', (target, content, mimeType = 'text/markdown') => { +Cypress.Commands.add('createFile', (target, content, mimeType = 'text/markdown', headers = {}) => { const blob = new Blob([content], { type: mimeType }) return cy.request('/csrftoken') @@ -121,6 +121,7 @@ Cypress.Commands.add('createFile', (target, content, mimeType = 'text/markdown') headers: { 'Content-Type': mimeType, requesttoken, + ...headers, }, }).then((response) => { return cy.log(`Uploaded ${target}`, response.status) diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index 4251d371232..aa6564bba1e 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -6,6 +6,14 @@ import chaiExtension from './chai.js' before(() => { chai.use(chaiExtension) + + Cypress.on('uncaught:exception', (err) => { + if (err.message.includes('ResizeObserver')) { + return false + } + + return true + }) }) Cypress.on('uncaught:exception', (err, runnable) => { diff --git a/src/components/Suggestion/LinkPicker/suggestions.js b/src/components/Suggestion/LinkPicker/suggestions.js index 402e8050766..8cf589a889a 100644 --- a/src/components/Suggestion/LinkPicker/suggestions.js +++ b/src/components/Suggestion/LinkPicker/suggestions.js @@ -87,7 +87,7 @@ export default () => createSuggestions({ icon: p.icon_url, providerId: p.id, } - }), + }).filter(e => e?.label?.toLowerCase?.()?.includes(query.toLowerCase())), ...formattingSuggestions(query) .filter(({ action, isActive }) => { const canRunState = action(editor?.can())