diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index e6f282da88f..15aece91dda 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -83,7 +83,7 @@ jobs: fail-fast: false matrix: node-version: [16] - containers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + containers: [1, 2 ] php-versions: [ '8.1' ] server-versions: [ 'stable28' ] run-in-parallel: diff --git a/cypress.config.js b/cypress.config.js index 8076b4830d0..2979e203843 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -44,7 +44,7 @@ module.exports = defineConfig({ specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}', }, retries: { - runMode: 2, + runMode: 0, // do not retry in `cypress open` openMode: 0, }, diff --git a/cypress/e2e/MenuBar.spec.js b/cypress/e2e/MenuBar.spec.js deleted file mode 100644 index c97a88717ee..00000000000 --- a/cypress/e2e/MenuBar.spec.js +++ /dev/null @@ -1,66 +0,0 @@ -import { initUserAndFiles, randUser } from '../utils/index.js' - -const user = randUser() -const fileName = 'empty.md' - -describe('Test the rich text editor menu bar', function() { - before(() => initUserAndFiles(user, fileName)) - - beforeEach(function() { - cy.login(user) - cy.visit('/apps/files', { - onBeforeLoad(win) { - cy.stub(win, 'open') - .as('winOpen') - }, - }) - - cy.openFile(fileName) - }) - - describe('word count', function() { - /** - * - */ - function getWordCount() { - return cy.get('.v-popper__popper .open').get('[data-text-action-entry="character-count"]') - } - - beforeEach(cy.clearContent) - it('empty file', () => { - cy.getFile(fileName) - .then($el => { - cy.getActionEntry('remain') - .click() - getWordCount() - .should('include.text', '0 words, 0 chars') - }) - }) - - it('single word', () => { - cy.getFile(fileName) - .then($el => { - cy.clearContent() - cy.getContent() - .type(' Hello ') - cy.getActionEntry('remain') - .click() - getWordCount() - .should('include.text', '1 word, 9 chars') - }) - }) - - it('multiple words', () => { - cy.getFile(fileName) - .then($el => { - cy.clearContent() - cy.getContent() - .type('Hello \nworld') - cy.getActionEntry('remain') - .click() - getWordCount() - .should('include.text', '2 words, 11 chars') - }) - }) - }) -}) diff --git a/cypress/e2e/SmartPicker.spec.js b/cypress/e2e/SmartPicker.spec.js deleted file mode 100644 index 8a9fbab8355..00000000000 --- a/cypress/e2e/SmartPicker.spec.js +++ /dev/null @@ -1,83 +0,0 @@ -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/api/SessionApi.spec.js b/cypress/e2e/api/SessionApi.spec.js deleted file mode 100644 index 82b7879f398..00000000000 --- a/cypress/e2e/api/SessionApi.spec.js +++ /dev/null @@ -1,381 +0,0 @@ -/* - * @copyright Copyright (c) 2022 Max - * - * @author Max - * - * @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 { randUser } from '../../utils/index.js' - -const user = randUser() -const messages = { - awareness: 'AQoBw9LM0gwAAnt9', - update: 'AAIKAAHYidydCwEEAQ==', - query: 'AAABAA==', - response: 'AAECAAA=', -} - -describe('The session Api', function() { - - before(function() { - cy.createUser(user) - window.OC = { - config: { modRewriteWorking: false }, - webroot: '', - } - }) - - beforeEach(function() { - cy.login(user) - cy.prepareSessionApi() - }) - - describe('open the session', function() { - let fileId - let filePath - - beforeEach(function() { - cy.uploadTestFile('test.md') - .then(id => { - fileId = id - }) - cy.testName().then(name => { - filePath = `/${name}.md` - }) - }) - - it('returns connection', function() { - cy.createTextSession(fileId).then(connection => { - cy.wrap(connection) - .its('document.id') - .should('equal', fileId) - connection.close() - }) - }) - - it('provides initial content', function() { - cy.createTextSession(fileId, { filePath }).then(connection => { - cy.wrap(connection) - .its('state.documentSource') - .should('eql', '## Hello world\n') - connection.close() - }) - }) - - it('handles invalid file id', function() { - cy.failToCreateTextSession(123456789) - .its('status') - .should('equal', 404) - }) - - it('handles missing file id', function() { - cy.failToCreateTextSession() - .its('status') - .should('equal', 412) - }) - - }) - - describe('step types', function() { - let connection - - beforeEach(function() { - cy.uploadTestFile() - .then(cy.createTextSession) - .then(con => { - connection = con - }) - }) - - afterEach(function() { - connection.close() - }) - - // Echoes all message types but queries - Object.entries(messages) - .filter(([key, _value]) => key !== 'query') - .forEach(([type, sample]) => { - it(`echos ${type} messages`, function() { - const steps = [sample] - const version = 0 - cy.pushSteps({ connection, steps, version }) - .its('version') - .should('be.at.least', 1) - cy.syncSteps(connection) - .its('steps[0].data') - .should('eql', steps) - }) - }) - - it('responds to queries', function() { - const version = 0 - Object.entries(messages) - .forEach(([type, sample]) => { - cy.pushSteps({ connection, steps: [sample], version }) - }) - cy.pushSteps({ connection, steps: [messages.query], version }) - .then(response => { - cy.wrap(response) - .its('version') - .should('eql', 0) - cy.wrap(response) - .its('steps.length') - .should('eql', 1) - cy.wrap(response) - .its('steps[0].data') - .should('eql', [messages.update]) - }) - }) - }) - - describe('sync', function() { - const version = 0 - let connection - let fileId - let filePath - let joining - - beforeEach(function() { - cy.testName().then(name => { - filePath = `/${name}.md` - }) - cy.uploadTestFile() - .then(id => { - fileId = id - return cy.createTextSession(fileId, { filePath }) - }) - .then(con => { - connection = con - }) - }) - - it('starts empty', function() { - cy.syncSteps(connection) - .its('steps') - .should('eql', []) - }) - - it('saves', function() { - cy.pushSteps({ connection, steps: [messages.update], version }) - .its('version') - .should('be.at.least', 1) - cy.save(connection, { version: 1, autosaveContent: '# Heading 1', manualSave: true }) - cy.downloadFile(filePath) - .its('data') - .should('eql', '# Heading 1') - }) - - it('saves yjs document state', function() { - const documentState = 'Base64 encoded string' - cy.pushSteps({ connection, steps: [messages.update], version }) - .its('version') - .should('be.at.least', 1) - cy.save(connection, { - version: 1, - autosaveContent: '# Heading 1', - documentState, - manualSave: true, - }) - cy.createTextSession(fileId, { filePath }) - .then(con => { - joining = con - return joining - }) - .its('state.documentState') - .should('eql', documentState) - .then(() => joining.close()) - }) - - afterEach(function() { - connection.close() - }) - }) - - describe('public sync', function() { - const version = 0 - let connection - let filePath - let shareToken - let joining - - beforeEach(function() { - cy.testName().then(name => { - filePath = `/${name}.md` - }) - cy.uploadTestFile() - .then(_id => { - return cy.shareFile(filePath, { edit: true }) - }) - .then(token => { - shareToken = token - }) - .then(() => cy.clearCookies()) - .then(() => { - cy.prepareSessionApi() - return cy.createTextSession(undefined, { filePath: '', shareToken }) - .then(con => { - connection = con - }) - }) - }) - - afterEach(function() { - connection.close() - }) - - it('starts empty public', function() { - cy.syncSteps(connection) - .its('steps') - .should('eql', []) - }) - - it('saves public', function() { - cy.pushSteps({ connection, steps: [messages.update], version }) - .its('version') - .should('be.at.least', 1) - cy.save(connection, { version: 1, autosaveContent: '# Heading 1', manualSave: true }) - cy.login(user) - cy.prepareSessionApi() - cy.downloadFile('saves.md') - .its('data') - .should('eql', '# Heading 1') - }) - - it('saves yjs document state public', function() { - const documentState = 'Base64 encoded string' - cy.pushSteps({ connection, steps: [messages.update], version }) - .its('version') - .should('be.at.least', 1) - cy.save(connection, { - version: 1, - autosaveContent: '# Heading 1', - documentState, - manualSave: true, - }) - cy.createTextSession(undefined, { filePath: '', shareToken }) - .then(con => { - joining = con - return con - }) - .its('state.documentState') - .should('eql', documentState) - .then(() => joining.close()) - }) - - }) - - describe('race conditions', function() { - const version = 0 - let connection - let shareToken - - beforeEach(function() { - cy.testName().then(name => { - const filePath = `/${name}.md` - cy.uploadTestFile('test.md') - return cy.shareFile(filePath, { edit: true }) - }).then(token => { - cy.log(token) - shareToken = token - cy.clearCookies() - cy.prepareSessionApi() - cy.createTextSession(undefined, { filePath: '', shareToken }) - .then(con => { - connection = con - }) - }) - }) - - it('signals closing connection', function() { - cy.then(() => { - return new Promise((resolve, reject) => { - connection.close() - connection.push({ steps: [messages.update], version, awareness: '' }) - .then( - () => reject(new Error('Push should have thrown ConnectionClosed()')), - resolve, - ) - }) - }) - }) - - it('sends initial content if other session is alive but did not push any steps', function() { - let joining - cy.createTextSession(undefined, { filePath: '', shareToken }) - .then(con => { - joining = con - return con - }) - .its('state.documentSource') - .should('not.eql', '') - .then(() => joining.close()) - .then(() => connection.close()) - }) - - it('does not send initial content if session is alive even without saved state', function() { - let joining - cy.pushSteps({ connection, steps: [messages.update], version }) - .its('version') - .should('be.at.least', 1) - cy.createTextSession(undefined, { filePath: '', shareToken }) - .then(con => { - joining = con - return con - }) - .its('state.documentSource') - .should('eql', '') - .then(() => joining.close()) - .then(() => connection.close()) - }) - - it('recovers session even if last person leaves right after create', function() { - let joining - cy.log('Initial user pushes steps') - cy.pushSteps({ connection, steps: [messages.update], version }) - .its('version') - .should('be.at.least', 1) - cy.log('Other user creates session') - cy.createTextSession(undefined, { filePath: '', shareToken }) - .then(con => { - joining = con - }) - cy.log('Initial user closes session') - .then(() => connection.close()) - cy.log('Other user still finds the steps') - .then(() => { - cy.syncSteps(joining, { - version: 0, - }).its('steps[0].data') - .should('eql', [messages.update]) - }) - }) - - // Failed with a probability of ~ 50% initially - // Skipped for now since the behaviour chanced by not cleaning up the state on close/create - it.skip('ignores steps stored after close cleaned up', function() { - cy.pushAndClose({ connection, steps: [messages.update], version }) - cy.createTextSession(undefined, { filePath: '', shareToken }) - .then(con => { - connection = con - }) - .its('state.documentSource') - .should('eql', '## Hello world\n') - }) - - }) -}) diff --git a/cypress/e2e/api/SyncServiceProvider.spec.js b/cypress/e2e/api/SyncServiceProvider.spec.js deleted file mode 100644 index aae87849b4e..00000000000 --- a/cypress/e2e/api/SyncServiceProvider.spec.js +++ /dev/null @@ -1,165 +0,0 @@ -/* - * @copyright Copyright (c) 2023 Max - * - * @author Max - * - * @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 { randUser } from '../../utils/index.js' -import { SyncService } from '../../../src/services/SyncService.js' -import createSyncServiceProvider from '../../../src/services/SyncServiceProvider.js' -import { Doc } from 'yjs' - -const user = randUser() - -describe('Sync service provider', function() { - let fileId - - before(function() { - cy.createUser(user) - window.OC = { - config: { modRewriteWorking: false }, - webroot: '', - } - }) - - beforeEach(function() { - cy.login(user) - cy.prepareSessionApi() - cy.uploadTestFile('test.md') - .then(id => { - fileId = id - }) - cy.wrap(new Doc()).as('source') - cy.wrap(new Doc()).as('target') - cy.get('@source').then(source => createProvider(source)).as('sourceProvider') - cy.get('@target').then(target => createProvider(target)).as('targetProvider') - }) - - afterEach(function() { - this.sourceProvider?.destroy() - this.targetProvider?.destroy() - }) - - /** - * @param {object} ydoc Yjs document - */ - function createProvider(ydoc) { - const syncService = new SyncService({ - serialize: () => 'Serialized', - getDocumentState: () => null, - }) - syncService.on('opened', () => syncService.startSync()) - return createSyncServiceProvider({ - ydoc, - syncService, - fileId, - initialSession: null, - disableBc: true, - }) - } - - it('recovers from a dropped message', function() { - const sourceMap = this.source.getMap() - const targetMap = this.target.getMap() - cy.intercept({ method: 'POST', url: '**/apps/text/session/*/push' }) - .as('push') - cy.intercept({ method: 'POST', url: '**/apps/text/session/*/sync' }) - .as('sync') - cy.wait('@push') - cy.then(() => { - sourceMap.set('keyA', 'valueA') - expect(targetMap.get('keyB')).to.be.eq(undefined) - }) - cy.wait('@sync') - cy.wait('@sync') - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(1000) - cy.then(() => { - expect(targetMap.get('keyA')).to.be.eq('valueA') - }) - cy.intercept({ - method: 'POST', - url: '**/apps/text/session/*/push', - }, req => { - if (req.body.steps) { - req.reply({ forceNetworkError: true }) - req.alias = 'dead' - } else { - req.continue() - } - }) - cy.then(() => { - sourceMap.set('keyB', 'valueB') - expect(targetMap.get('keyB')).to.be.eq(undefined) - }) - cy.wait('@dead') - cy.then(() => { - expect(targetMap.get('keyB')).to.be.eq(undefined) - }) - cy.intercept({ - method: 'POST', - url: '**/apps/text/session/*/push', - }, req => { - if (req.body.steps) { - req.alias = 'alive' - req.continue() - } else { - req.continue() - } - }) - cy.then(() => { - sourceMap.set('keyC', 'valueC') - expect(targetMap.get('keyB')).to.be.eq(undefined) - }) - cy.wait('@alive') - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(1000) - cy.then(() => { - expect(targetMap.get('keyC')).to.be.eq('valueC') - expect(targetMap.get('keyB')).to.be.eq('valueB') - }) - }) - - /* - * Counts the amount of push and sync requests in one minute. - * Skipped per default, useful for comparison before/after changes to SyncProvider or PollingBackend. - */ - it.skip('is not too chatty', function() { - const sourceMap = this.source.getMap() - const targetMap = this.target.getMap() - cy.intercept({ method: 'POST', url: '**/apps/text/session/*/push' }) - .as('push') - cy.intercept({ method: 'POST', url: '**/apps/text/session/*/sync' }) - .as('sync') - cy.wait('@push') - cy.then(() => { - sourceMap.set('keyA', 'valueA') - expect(targetMap.get('keyB')).to.be.eq(undefined) - }) - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(60000) - cy.then(() => { - expect(targetMap.get('keyA')).to.be.eq('valueA') - }) - // 2 clients push awareness updates every 15 seconds -> 2*5 = 10. Actual 15. - cy.get('@push.all').its('length').should('be.lessThan', 30) - // 2 clients sync fast first and then every 5 seconds -> 2*12 = 24. Actual 32. - cy.get('@sync.all').its('length').should('be.lessThan', 60) - }) -}) diff --git a/cypress/e2e/api/UsersApi.spec.js b/cypress/e2e/api/UsersApi.spec.js deleted file mode 100644 index 2e3d72287ea..00000000000 --- a/cypress/e2e/api/UsersApi.spec.js +++ /dev/null @@ -1,109 +0,0 @@ -/* - * @copyright Copyright (c) 2022 Max - * @copyright Copyright (c) 2023 Julius Härtl - * - * @author Max - * @author Julius Härtl - * - * @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 { randUser } from '../../utils/index.js' - -const user = randUser() - -describe('The user mention API', function() { - - before(function() { - cy.createUser(user) - window.OC = { - config: { modRewriteWorking: false }, - webroot: '', - } - }) - - let fileId - let requesttoken - - beforeEach(function() { - cy.login(user) - cy.prepareSessionApi().then((token) => { - requesttoken = token - cy.uploadTestFile('test.md') - .then(id => { - fileId = id - }) - }) - }) - - it('fetches users with valid session', function() { - cy.createTextSession(fileId).then(connection => { - cy.wrap(connection) - .its('document.id') - .should('equal', fileId) - - const requestData = { - method: 'POST', - url: '/apps/text/api/v1/users', - body: { - documentId: connection.document.id, - sessionId: connection.session.id, - sessionToken: connection.session.token, - requesttoken, - }, - failOnStatusCode: false, - } - const invalidRequestData = { ...requestData } - - cy.request(requestData).then(({ status }) => { - expect(status).to.eq(200) - - invalidRequestData.body = { - ...requestData.body, - sessionToken: 'invalid', - } - }) - - cy.request(invalidRequestData).then(({ status }) => { - expect(status).to.eq(403) - invalidRequestData.body = { - ...requestData.body, - sessionId: 0, - } - }) - - cy.request(invalidRequestData).then(({ status }) => { - expect(status).to.eq(403) - - invalidRequestData.body = { - ...requestData.body, - documentId: 0, - } - }) - - cy.request(invalidRequestData).then(({ status }) => { - expect(status).to.eq(403) - }) - - cy.wrap(null).then(() => connection.close()) - - cy.request(requestData).then(({ status, body }) => { - expect(status).to.eq(403) - }) - }) - }) -}) diff --git a/cypress/e2e/api/Yjs.spec.js b/cypress/e2e/api/Yjs.spec.js deleted file mode 100644 index b8dc84732b0..00000000000 --- a/cypress/e2e/api/Yjs.spec.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * @copyright Copyright (c) 2023 Jonas - * - * @author Jonas - * - * @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 { applyUpdate, Doc, encodeStateAsUpdate, encodeStateVector } from 'yjs' - -describe('Yjs', function() { - // Only tests that Yjs allows to apply steps in wrong order - it('applies step in wrong order', function() { - const source = new Doc() - const target = new Doc() - const sourceMap = source.getMap() - const targetMap = target.getMap() - - target.on('afterTransaction', (tr, doc) => { - // console.log('afterTransaction', tr) - }) - - // Add keyA to source and apply to target - sourceMap.set('keyA', 'valueA') - const update0A = encodeStateAsUpdate(source) - const sourceVectorA = encodeStateVector(source) - applyUpdate(target, update0A) - expect(targetMap.get('keyA')).to.be.eq('valueA') - - // Add keyB to source, don't apply to target yet - sourceMap.set('keyB', 'valueB') - const updateAB = encodeStateAsUpdate(source, sourceVectorA) - const sourceVectorB = encodeStateVector(source) - - // Add keyC to source, apply to target - sourceMap.set('keyC', 'valueC') - const updateBC = encodeStateAsUpdate(source, sourceVectorB) - applyUpdate(target, updateBC) - expect(targetMap.get('keyB')).to.be.eq(undefined) - expect(targetMap.get('keyC')).to.be.eq(undefined) - - // Apply keyB to target - applyUpdate(target, updateAB) - expect(targetMap.get('keyB')).to.be.eq('valueB') - expect(targetMap.get('keyC')).to.be.eq('valueC') - }) -}) diff --git a/cypress/e2e/attachments.spec.js b/cypress/e2e/attachments.spec.js index 0222f949768..dce8e0052cc 100644 --- a/cypress/e2e/attachments.spec.js +++ b/cypress/e2e/attachments.spec.js @@ -390,63 +390,4 @@ describe('Test all attachment insertion methods', () => { .should('not.exist') }) }) - - it('[share] check everything behaves correctly on the share target user side', () => { - const fileName = 'testShared.md' - cy.createMarkdown(fileName, '![git](.attachments.123/github.png)', false).then((fileId) => { - const attachmentsFolder = `.attachments.${fileId}` - cy.createFolder(attachmentsFolder) - cy.uploadFile('github.png', 'image/png', `${attachmentsFolder}/github.png`) - cy.shareFileToUser(fileName, recipient) - }) - - cy.login(recipient) - - cy.visit('/apps/files') - // check the file list - cy.getFile('testShared.md') - .should('exist') - cy.getFile('github.png') - .should('not.exist') - cy.showHiddenFiles() - - // check the attachment folder is not there - cy.getFile('testShared.md') - .should('exist') - .should('have.attr', 'data-cy-files-list-row-fileid') - .then((documentId) => { - cy.getFile('.attachments.' + documentId) - .should('not.exist') - }) - - // move the file and check the attachment folder is still not there - cy.moveFile('testShared.md', 'testMoved.md') - cy.reloadFileList() - cy.getFile('testMoved.md') - .should('exist') - .should('have.attr', 'data-cy-files-list-row-fileid') - .then((documentId) => { - cy.getFile('.attachments.' + documentId) - .should('not.exist') - }) - - // copy the file and check the attachment folder was copied - cy.copyFile('testMoved.md', 'testCopied.md') - cy.reloadFileList() - cy.getFile('testCopied.md') - .should('exist') - .should('have.attr', 'data-cy-files-list-row-fileid') - .then((documentId) => { - const files = attachmentFileNameToId[documentId] - cy.openFolder('.attachments.' + documentId) - for (const name in files) { - cy.getFile(name) - .should('exist') - .should('have.attr', 'data-cy-files-list-row-fileid') - // these are new copied attachment files - // so they should not have the same IDs than the ones created when uploading the files - .should('not.eq', String(files[name])) - } - }) - }) }) diff --git a/cypress/e2e/conflict.spec.js b/cypress/e2e/conflict.spec.js deleted file mode 100644 index 9e5021dc6c1..00000000000 --- a/cypress/e2e/conflict.spec.js +++ /dev/null @@ -1,123 +0,0 @@ -/** - * @copyright Copyright (c) 2019 John Molakvoæ - * - * @author John Molakvoæ - * - * @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() - -const variants = [ - { fixture: 'lines.txt', mime: 'text/plain' }, - { fixture: 'test.md', mime: 'text/markdown' }, -] - -variants.forEach(function({ fixture, mime }) { - const fileName = fixture - describe(`${mime} (${fileName})`, function() { - const getWrapper = () => cy.get('.viewer__content .text-editor__wrapper.has-conflicts') - - before(() => { - initUserAndFiles(user, fileName) - }) - - beforeEach(function() { - cy.login(user) - cy.visit('/apps/files') - }) - - it('displays conflicts', function() { - cy.openFile(fileName) - - cy.log('Inspect editor') - cy.getContent() - .type('Hello you cruel conflicting world') - cy.uploadFile(fileName, mime) - - cy.get('#viewer .modal-header button.header-close').click() - cy.get('#viewer').should('not.exist') - cy.openFile(fileName) - cy.get('.text-editor .document-status .icon-error') - getWrapper() - .find('#read-only-editor') - .should('contain', 'Hello world') - getWrapper() - .find('.text-editor__main') - .should('contain', 'Hello world') - getWrapper() - .find('.text-editor__main') - .should('contain', 'cruel conflicting') - }) - - it('resolves conflict using current editing session', function() { - cy.openFile(fileName) - - cy.log('Inspect editor') - cy.getContent() - .type('Hello you cruel conflicting world') - cy.uploadFile(fileName, mime) - - cy.get('#viewer .modal-header button.header-close').click() - cy.get('#viewer').should('not.exist') - cy.openFile(fileName) - - cy.get('[data-cy="resolveThisVersion"]').click() - - getWrapper() - .should('not.exist') - - cy.get('[data-cy="resolveThisVersion"]') - .should('not.exist') - - cy.get('.text-editor__main') - .should('contain', 'Hello world') - cy.get('.text-editor__main') - .should('contain', 'cruel conflicting') - }) - - it('resolves conflict using server version', function() { - cy.openFile(fileName) - - cy.log('Inspect editor') - cy.getContent() - .type('Hello you cruel conflicting world') - cy.uploadFile(fileName, mime) - - cy.get('#viewer .modal-header button.header-close').click() - cy.get('#viewer').should('not.exist') - cy.openFile(fileName) - - cy.get('[data-cy="resolveServerVersion"]') - .click() - - getWrapper() - .should('not.exist') - cy.get('[data-cy="resolveThisVersion"]') - .should('not.exist') - cy.get('[data-cy="resolveServerVersion"]') - .should('not.exist') - - cy.get('.text-editor__main') - .should('contain', 'Hello world') - cy.get('.text-editor__main') - .should('not.contain', 'cruel conflicting') - }) - }) -}) diff --git a/cypress/e2e/debug.spec.js b/cypress/e2e/debug.spec.js new file mode 100644 index 00000000000..89bfff86d57 --- /dev/null +++ b/cypress/e2e/debug.spec.js @@ -0,0 +1,74 @@ +/** + * @copyright Copyright (c) 2021 Julien Veyssier + * + * @author Julien Veyssier + * + * @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 { randUser } from '../utils/index.js' + +const user = randUser() + +describe('Test all attachment insertion methods', () => { + before(() => { + cy.createUser(user) + }) + + it('See test files in the list and display hidden files', () => { + cy.login(user) + cy.createFolder('.hidden') + cy.showHiddenFiles() + cy.visit('/apps/files') + cy.getFile('.hidden') + }) + + it('deletes file just fine', () => { + cy.login(user) + cy.showHiddenFiles() + const fileName = 'deleteMe.md' + cy.createFile(fileName, '# Hello world!', 'text/markdown') + cy.visit('/apps/files') + cy.getFile(fileName) + .should('exist') + .should('have.attr', 'data-cy-files-list-row-fileid') + .then((documentId) => { + cy.deleteFile(fileName) + cy.reloadFileList() + }) + }) + + it('deletes attachment folder when deleting a markdown file', () => { + const fileName = 'deleteSource.md' + cy.createMarkdown(fileName, '![git](.attachments.123/github.png)', false).then((fileId) => { + const attachmentsFolder = `.attachments.${fileId}` + cy.createFolder(attachmentsFolder) + cy.uploadFile('github.png', 'image/png', `${attachmentsFolder}/github.png`) + }) + + cy.visit('/apps/files') + cy.getFile(fileName) + .should('exist') + .should('have.attr', 'data-cy-files-list-row-fileid') + .then((documentId) => { + cy.deleteFile(fileName) + cy.reloadFileList() + cy.getFile('.attachments.' + documentId) + .should('not.exist') + }) + }) +}) diff --git a/cypress/e2e/directediting.spec.js b/cypress/e2e/directediting.spec.js deleted file mode 100644 index e798bf3f999..00000000000 --- a/cypress/e2e/directediting.spec.js +++ /dev/null @@ -1,149 +0,0 @@ -import { initUserAndFiles, randUser } from '../utils/index.js' - -const user = randUser() - -const createDirectEditingLink = (user, file) => { - cy.login(user) - return cy.request({ - method: 'POST', - url: `${Cypress.env('baseUrl')}/ocs/v2.php/apps/files/api/v1/directEditing/open?format=json`, - form: true, - body: { - path: file, - }, - auth: { user: user.userId, pass: user.password }, - headers: { - 'OCS-ApiRequest': 'true', - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }).then(response => { - cy.log(response) - const token = response.body?.ocs?.data?.url - cy.log(`Created direct editing token for ${user.userId}`, token) - return cy.wrap(token) - }) -} - -const createDirectEditingLinkForNewFile = (user, file) => { - cy.login(user) - return cy.request({ - method: 'POST', - url: `${Cypress.env('baseUrl')}/ocs/v2.php/apps/files/api/v1/directEditing/create?format=json`, - form: true, - body: { - path: file, - editorId: 'text', - creatorId: 'textdocument', - }, - auth: { user: user.userId, pass: user.password }, - headers: { - 'OCS-ApiRequest': 'true', - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }).then(response => { - cy.log(response) - const token = response.body?.ocs?.data?.url - cy.log(`Created direct editing token for ${user.userId}`, token) - return cy.wrap(token) - }) -} - -describe('direct editing', function() { - - const visitHooks = { - onBeforeLoad(win) { - win.DirectEditingMobileInterface = { - close() {}, - } - }, - } - before(function() { - initUserAndFiles(user, 'test.md', 'empty.md', 'empty.txt') - }) - - it('Open an existing file, edit it', () => { - 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') - - createDirectEditingLink(user, 'empty.md') - .then((token) => { - cy.logout() - cy.visit(token, visitHooks) - }) - cy.getContent().type('# This is a headline') - cy.getContent().type('{enter}') - cy.getContent().type('Some text') - cy.getContent().type('{enter}') - cy.getContent().type('{ctrl+s}') - - cy.wait('@push') - cy.wait('@sync') - - cy.get('button.icon-close').click() - cy.wait('@closeRequest') - .then(() => { - cy.getFileContent('empty.md').then((content) => { - expect(content).to.equal('# This is a headline\n\nSome text') - }) - }) - }) - - it('Create a file, edit it', () => { - 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') - - createDirectEditingLinkForNewFile(user, 'newfile.md') - .then((token) => { - cy.logout() - cy.visit(token, visitHooks) - }) - - cy.getContent().type('# This is a headline') - cy.getContent().type('{enter}') - cy.getContent().type('Some text') - cy.getContent().type('{enter}') - cy.getContent().type('{ctrl+s}') - - cy.wait('@push') - cy.wait('@sync') - - cy.get('button.icon-close').click() - cy.wait('@closeRequest') - .then(() => { - cy.getFileContent('newfile.md').then((content) => { - expect(content).to.equal('# This is a headline\n\nSome text') - }) - }) - }) - - it('Open an existing plain text file, edit it', () => { - 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') - - createDirectEditingLink(user, 'empty.txt') - .then((token) => { - cy.logout() - cy.visit(token, visitHooks) - }) - - cy.getContent().type('# This is a headline') - cy.getContent().type('{enter}') - cy.getContent().type('Some text') - cy.getContent().type('{enter}') - cy.getContent().type('{ctrl+s}') - - cy.wait('@push') - cy.wait('@sync') - - cy.get('button.icon-close').click() - cy.wait('@closeRequest') - .then(() => { - cy.getFileContent('empty.txt').then((content) => { - expect(content).to.equal('# This is a headline\nSome text\n') - }) - }) - }) -}) diff --git a/cypress/e2e/files.spec.js b/cypress/e2e/files.spec.js deleted file mode 100644 index a65227ea465..00000000000 --- a/cypress/e2e/files.spec.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @copyright Copyright (c) 2019 John Molakvoæ - * - * @author John Molakvoæ - * - * @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 { randUser } from '../utils/index.js' - -const user = randUser() - -describe('Text and server mimetypes', () => { - before(() => { - cy.createUser(user) - }) - - beforeEach(() => { - cy.login(user) - }) - - it('handle plaintext in a pre tag', () => { - cy.uploadFile('empty.md', 'text/plain', 'textfile.txt') - cy.visit('/apps/files') - cy.getFile('textfile.txt') - cy.openFile('textfile.txt') - cy.getContent().find('pre').should('exist') - }) - - it('handle asciidoc as plaintext for now', () => { - cy.uploadFile('test.adoc', 'text/asciidoc', 'hello.adoc') - cy.visit('/apps/files') - cy.getFile('hello.adoc') - cy.openFile('hello.adoc') - cy.getContent().find('pre').should('contain', 'Hello world') - }) - - it('handle markdown with richtext editor', () => { - cy.uploadFile('test.md', 'text/markdown', 'markdown.md') - cy.visit('/apps/files') - cy.getFile('markdown.md') - cy.openFile('markdown.md') - cy.getContent().find('h2').should('contain', 'Hello world') - }) - -}) diff --git a/cypress/e2e/mobile.spec.js b/cypress/e2e/mobile.spec.js deleted file mode 100644 index bf53dcad302..00000000000 --- a/cypress/e2e/mobile.spec.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @copyright Copyright (c) 2019 Vinicius Reis - * - * @author Vinicius Reis - * - * @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 { randUser } from '../utils/index.js' - -const user = randUser() - -const getRemainItem = (name) => { - cy.getActionEntry('remain').click() - return cy.get('.v-popper__wrapper .open').getActionEntry(name) -} - -describe('Mobile actions', { - // moto g4 - viewportWidth: 360, - viewportHeight: 640, -}, () => { - before(() => { - cy.createUser(user) - }) - - beforeEach(function() { - cy.login(user) - cy.visit('/apps/files').then(() => { - // isolate tests - each happens in its own folder - const retry = cy.state('test').currentRetry() - const folderName = retry - ? `${Cypress.currentTest.title} (${retry})` - : Cypress.currentTest.title - cy.createFolder(folderName) - cy.uploadFile('test.md', 'text/markdown', `${encodeURIComponent(folderName)}/text.md`) - - cy.visit(`apps/files?dir=/${encodeURIComponent(folderName)}`) - cy.openFile('text.md', { force: true }) - }) - }) - - it('formatting modal help', () => { - getRemainItem('formatting-help').click() - - cy.get('[data-text-el="formatting-help"]').should('be.visible') - cy.get('[data-text-el="formatting-help"]').find('button.modal-container__close').click() - cy.get('[data-text-el="formatting-help"]').should('not.exist') - }) -}) diff --git a/cypress/e2e/nodes/CodeBlock.spec.js b/cypress/e2e/nodes/CodeBlock.spec.js deleted file mode 100644 index fb49f3b4750..00000000000 --- a/cypress/e2e/nodes/CodeBlock.spec.js +++ /dev/null @@ -1,155 +0,0 @@ -/** - * @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('Show a code block in a public read only link', function() { - cy.shareFile('/codeblock.md') - .then((token) => { - cy.logout() - cy.visit(`/s/${token}`) - }) - .then(() => { - cy.getEditor().should('be.visible') - // 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') - - // Mermaid diagram - cy.get('#app-content').scrollTo('bottom') - cy.getContent().find('.split-view__preview').eq(2).should('be.visible') - cy.get('.code-block').eq(2).find('code').should('not.be.visible') - cy.get('.split-view__preview').find('svg .entityTitleText') - .contains('Order example') - }) - }) - - 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 .entityTitleText') - .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/nodes/FrontMatter.spec.js b/cypress/e2e/nodes/FrontMatter.spec.js deleted file mode 100644 index c681385c95d..00000000000 --- a/cypress/e2e/nodes/FrontMatter.spec.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * @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, 'frontmatter.md', 'empty.md') - }) - - beforeEach(function() { - cy.login(user) - cy.visit('/apps/files') - }) - - it('Open file with front matter', function() { - cy.openFile('frontmatter.md').then(() => { - cy.getContent().find('pre.frontmatter').should(pre => { - expect(pre.length === 1) - expect(pre[0].text === 'some: value\nother: 1.2') - }) - }) - }) - - it('Add front matter', function() { - cy.openFile('empty.md').clearContent() - cy.getContent().type('---') - cy.getContent().type('test') - cy.getContent().find('pre.frontmatter').should(pre => { - expect(pre.length === 1) - expect(pre[0].text === 'test') - }) - }) - - it('Do not add multiple front matter', function() { - cy.openFile('empty.md').clearContent() - cy.getContent().type('---test') - cy.getContent().type('{downArrow}') - cy.getContent().type('---test') - cy.getContent().find('pre.frontmatter').should(pre => expect(pre.length === 1)) - cy.getContent().find('hr').should(hr => expect(hr.length === 1)) - }) - - it('Reopen front matter', function() { - cy.openFile('frontmatter.md') - cy.getContent() - .type('{moveToEnd}New line{enter}') - cy.getContent() - .find('pre.frontmatter').should(pre => { - expect(pre.length === 1) - }) - cy.closeFile() - cy.intercept({ method: 'POST', url: '**/apps/text/session/*/sync' }).as('sync') - cy.openFile('frontmatter.md') - cy.wait('@sync') - cy.wait('@sync') - cy.getContent().find('pre.frontmatter').should('have.length', 1) - }) -}) diff --git a/cypress/e2e/nodes/HardBreak.spec.js b/cypress/e2e/nodes/HardBreak.spec.js deleted file mode 100644 index c938727cf37..00000000000 --- a/cypress/e2e/nodes/HardBreak.spec.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * @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 { randUser } from '../../utils/index.js' - -const fileName = 'empty.md' -const user = randUser() - -describe('Hard break support', function() { - before(function() { - cy.createUser(user) - }) - - beforeEach(function() { - cy.login(user) - cy.isolateTest({ - sourceFile: fileName, - }) - - return cy.openFile(fileName, { force: true }) - }) - - it('Can create hard breaks with shift+enter', () => { - cy.getContent().type('Hello') - cy.getContent().type('{shift+enter}world') - cy.getContent() - .find('p br') - .should('exist') - }) - - it('Convert paragraph break into hard break', () => { - cy.getContent().type('Hello') - cy.getContent().type('{enter}') - cy.getContent().type('world') - cy.getContent() - .find('p') - .should('have.length', 2) - .and($p => { - expect($p[0]).to.contain('Hello') - expect($p[1]).to.contain('world') - }) - cy.getContent() - .type('{home}{backspace}') - cy.getContent() - .find('p br') - .should('exist') - }) - - it('Do not create hard breaks within headings', () => { - cy.getContent().type('# Hello') - cy.getContent().type('{shift+enter}world') - cy.getContent() - .find('h1 br') - .should('not.exist') - }) -}) diff --git a/cypress/e2e/nodes/ImageView.spec.js b/cypress/e2e/nodes/ImageView.spec.js deleted file mode 100644 index bb1fe9cf858..00000000000 --- a/cypress/e2e/nodes/ImageView.spec.js +++ /dev/null @@ -1,146 +0,0 @@ -import { randUser } from '../../utils/index.js' - -const user = randUser() - -describe('Image View', () => { - before(() => { - cy.createUser(user) - cy.login(user) - - // Upload test files to user's storage - cy.createFolder('child-folder') - cy.uploadFile('github.png', 'image/png') - cy.uploadFile('github.png', 'image/png', 'child-folder/github.png') - }) - - beforeEach(() => { - cy.login(user) - cy.visit('/apps/files') - }) - - describe('direct access', () => { - it('from root', () => { - const fileName = `${Cypress.currentTest.title}.md` - - cy.createMarkdown(fileName, '# from root\n\n ![git](/github.png)') - - cy.openFile(fileName) - - cy.getContent() - .find('[data-component="image-view"]') - .should('have.attr', 'data-src') - .should('eq', '/github.png') - - cy.getContent() - .find('[data-component="image-view"] img') - .should('have.attr', 'src') - .should('contains', `/dav/files/${user.userId}/github.png`) - }) - - it('from child folder', () => { - const fileName = `${Cypress.currentTest.title}.md` - - cy.createMarkdown(fileName, '# from child\n\n ![git](child-folder/github.png)') - - cy.openFile(fileName) - - cy.getContent() - .find('[data-component="image-view"]') - .should('have.attr', 'data-src') - .should('eq', 'child-folder/github.png') - - cy.getContent() - .find('[data-component="image-view"] img') - .should('have.attr', 'src') - .should('contains', `/dav/files/${user.userId}/child-folder/github.png`) - }) - - it('from parent folder', () => { - cy.visit('apps/files?dir=/child-folder') - - const fileName = `${Cypress.currentTest.title}.md` - - cy.createMarkdown(`/child-folder/${fileName}`, '# from parent\n\n ![git](../github.png)') - - cy.openFile(fileName, { force: true }) - - cy.getContent() - .find('[data-component="image-view"]') - .should('have.attr', 'data-src') - .should('eq', '../github.png') - - cy.getContent() - .find('[data-component="image-view"] img') - .should('have.attr', 'src') - .should('contain', `/dav/files/${user.userId}/github.png`) - }) - }) - - describe('fail to load', () => { - it('direct access', () => { - const fileName = `${Cypress.currentTest.title}.md` - - cy.createMarkdown(fileName, '# from root\n\n ![yaha](/yaha.png)') - - cy.openFile(fileName) - - cy.getContent() - .find('[data-component="image-view"]') - .should('have.class', 'image-view--failed') - - cy.getContent() - .find('[data-component="image-view"] svg') - .should('be.visible') - - cy.getContent() - .find('[data-component="image-view"] .image__error-message') - .should('be.visible') - - cy.getContent() - .find('[data-component="image-view"] .image__caption input') - .should('have.value', 'yaha') - }) - }) - - describe('native attachments', () => { - before(() => { - cy.login(user) - cy.visit('/apps/files') - const fileName = 'native attachments.md' - cy.createMarkdown(fileName, '# open image in modal\n\n ![git](.attachments.123/github.png)\n\n ![file.txt.gz](.attachments.123/file.txt.gz)') - - cy.getFileId(fileName).then((fileId) => { - const attachmentsFolder = `.attachments.${fileId}` - cy.createFolder(attachmentsFolder) - cy.uploadFile('github.png', 'image/png', `${attachmentsFolder}/github.png`) - cy.uploadFile('file.txt.gz', 'application/gzip', `${attachmentsFolder}/file.txt.gz`) - }) - }) - - it('open image in modal', () => { - const fileName = 'native attachments.md' - cy.openFile(fileName) - - cy.getContent() - .find('[data-component="image-view"][data-src=".attachments.123/github.png"] img') - .click() - - cy.get('.modal__content img') - .should('have.attr', 'src') - .should('contain', 'imageFileName=github.png') - }) - - it('download non-image gzip attachment', () => { - const fileName = 'native attachments.md' - cy.openFile(fileName) - - cy.getContent() - .find('[data-component="image-view"][data-src=".attachments.123/file.txt.gz"] img') - .click() - - const downloadsFolder = Cypress.config('downloadsFolder') - cy.log(`downloadsFolder: ${downloadsFolder}`) - cy.readFile(`${downloadsFolder}/file.txt.gz`) - }) - }) -}) diff --git a/cypress/e2e/nodes/Links.spec.js b/cypress/e2e/nodes/Links.spec.js deleted file mode 100644 index 43636f75a16..00000000000 --- a/cypress/e2e/nodes/Links.spec.js +++ /dev/null @@ -1,185 +0,0 @@ -import { initUserAndFiles, randUser } from '../../utils/index.js' - -const user = randUser() -const fileName = 'empty.md' - -describe('test link marks', function() { - before(function() { - initUserAndFiles(user) - }) - - beforeEach(function() { - cy.login(user) - cy.isolateTest({ - sourceFile: fileName, - onBeforeLoad(win) { - cy.stub(win, 'open') - .as('winOpen') - }, - }) - cy.openFile(fileName, { force: true }) - }) - - describe('link preview', function() { - it('shows a link preview', () => { - cy.getContent().type('https://nextcloud.com') - cy.getContent().type('{enter}') - - cy.getContent() - .find('.widgets--list', { timeout: 10000 }) - .find('.widget-default--name') - .contains('Nextcloud') - }) - - it('does not show a link preview for links within a paragraph', () => { - cy.getContent().type('Please visit https://nextcloud.com') - cy.getContent().type('{enter}') - - cy.getContent() - .find('.widgets--list', { timeout: 10000 }) - .should('not.exist') - }) - }) - - describe('autolink', function() { - it('with protocol to files app and fileId', () => { - cy.getFile(fileName) - .then($el => { - const id = $el.data('id') - - const link = `${Cypress.env('baseUrl')}/apps/files/file-name?fileId=${id}` - cy.clearContent() - cy.getContent() - .type(`${link}{enter}`) - - cy.getContent() - .find(`a[href*="${Cypress.env('baseUrl')}"]`) - .click({ force: true }) - - cy.get('@winOpen') - .should('have.been.calledOnce') - .should('have.been.calledWithMatch', new RegExp(`/f/${id}$`)) - }) - }) - - it('with protocol and fileId', () => { - cy.getFile(fileName) - .then($el => { - const id = $el.data('id') - - const link = `${Cypress.env('baseUrl')}/file-name?fileId=${id}` - cy.clearContent() - cy.getContent() - .type(`${link}{enter}`) - - cy.getContent() - .find(`a[href*="${Cypress.env('baseUrl')}"]`) - .click({ force: true }) - - cy.get('@winOpen') - .should('have.been.calledOnce') - .should('have.been.calledWithMatch', new RegExp(`${Cypress.env('baseUrl')}/file-name\\?fileId=${id}$`)) - }) - }) - - it('without protocol', () => { - cy.clearContent() - cy.getContent() - .type('google.com{enter}') - cy.getContent() - .find('a[href*="google.com"]') - .should('not.exist') - }) - - it('with protocol but without space', () => { - cy.getContent() - .type('https://nextcloud.com') - - cy.getContent() - .find('a[href*="nextcloud.com"]') - .should('not.exist') - }) - }) - - describe('link menu', function() { - beforeEach(() => cy.clearContent()) - const text = 'some text' - - describe('link to website', function() { - const url = 'https://nextcloud.com/' - // Helper to reduce duplicated code, checking inserting with and without selected text - const checkLinkWebsite = (url, text) => { - cy.getSubmenuEntry('insert-link', 'insert-link-website').click() - cy.getActionSubEntry('insert-link-input').find('input[type="text"]').type(`${url}{enter}`) - cy.getContent() - .get(`a[href*="${url}"]`) - .should('have.text', text) // ensure correct text used - .click({ force: true }) - - cy.get('@winOpen') - .should('have.been.calledOnce') - .should('have.been.calledWith', url) - } - - beforeEach(cy.clearContent) - it('Link website without selection', () => { - cy.getFile(fileName) - .then($el => { - checkLinkWebsite(url, url) - }) - }) - - it('Link website with selection', () => { - cy.getFile(fileName) - .then($el => { - cy.getContent().type(`${text}{selectAll}`) - checkLinkWebsite(url, text) - }) - }) - }) - - describe('link to local file', function() { - // Helper to reduce duplicated code, checking inserting with and without selected text - const checkLinkFile = (filename, text, isFolder = false) => { - cy.getSubmenuEntry('insert-link', 'insert-link-file').click() - cy.get('.file-picker').within(() => { - cy.get(`[data-testid="file-list-row"][data-filename="${filename}"]`).click() - cy.get(isFolder ? '.empty-content__name' : '.file-picker__files') - cy.contains('button', isFolder ? 'Choose' : `Choose ${filename}`).click() - }) - - return cy.getContent() - .find(`a[href*="${encodeURIComponent(filename)}"]`) - .should('have.text', text === undefined ? filename : text) - .click({ force: true }) - } - - beforeEach(() => cy.clearContent()) - - it('without text', () => { - cy.getFile(fileName) - .then($el => { - checkLinkFile(fileName) - cy.get('.modal-name').should('include.text', fileName) - }) - }) - it('with selected text', () => { - cy.getFile(fileName) - .then($el => { - cy.getContent().type(`${text}{selectAll}`) - checkLinkFile(fileName, text) - cy.get('.modal-name').should('include.text', fileName) - }) - }) - it('link to directory', () => { - cy.createFolder(`${window.__currentDirectory}/dummy folder`) - cy.getFile(fileName).then($el => { - cy.getContent().type(`${text}{selectAll}`) - checkLinkFile('dummy folder', text, true) - cy.get('@winOpen') - .should('have.been.calledOnce') - }) - }) - }) - }) -}) diff --git a/cypress/e2e/nodes/ListItem.spec.js b/cypress/e2e/nodes/ListItem.spec.js deleted file mode 100644 index 3af9a3760a8..00000000000 --- a/cypress/e2e/nodes/ListItem.spec.js +++ /dev/null @@ -1,78 +0,0 @@ -/* eslint-disable import/no-named-as-default */ -import OrderedList from '@tiptap/extension-ordered-list' -import ListItem from '@tiptap/extension-list-item' -/* eslint-enable import/no-named-as-default */ -import TaskList from './../../../src/nodes/TaskList.js' -import TaskItem from './../../../src/nodes/TaskItem.js' -import BulletList from './../../../src/nodes/BulletList.js' -import Markdown, { createMarkdownSerializer } from './../../../src/extensions/Markdown.js' -import { findChildren } from './../../../src/helpers/prosemirrorUtils.js' -import { createCustomEditor } from './../../support/components.js' -import testData from '../../fixtures/ListItem.md' -import markdownit from './../../../src/markdownit/index.js' - -describe('ListItem extension integrated in the editor', () => { - - const editor = createCustomEditor({ - content: '', - extensions: [ - Markdown, - BulletList, - OrderedList, - ListItem, - TaskList, - TaskItem, - ], - }) - - for (const spec of testData.split(/#+\s+/)) { - const [description, ...rest] = spec.split(/\n/) - const [input, output] = rest.join('\n').split(/\n\n---\n\n/) - if (!description) { - continue - } - it(description, () => { - expect(spec).to.include('\n') - /* eslint-disable no-unused-expressions */ - expect(input).to.be.ok - expect(output).to.be.ok - /* eslint-enable no-unused-expressions */ - loadMarkdown(input) - runCommands() - expectMarkdown(output.replace(/\n*$/, '')) - }) - } - - const loadMarkdown = (markdown) => { - const stripped = markdown.replace(/\t*/g, '') - editor.commands.setContent(markdownit.render(stripped)) - } - - const runCommands = () => { - let found - while ((found = findCommand())) { - const { node, pos } = found - const name = node.text - editor.commands.setTextSelection(pos) - editor.commands[name]() - editor.commands.insertContent('did ') - } - } - - const findCommand = () => { - const doc = editor.state.doc - return findChildren(doc, child => { - return child.isText && Object.prototype.hasOwnProperty.call(editor.commands, child.text) - })[0] - } - - const expectMarkdown = (markdown) => { - const stripped = markdown.replace(/\t*/g, '') - expect(getMarkdown()).to.equal(stripped) - } - - const getMarkdown = () => { - const serializer = createMarkdownSerializer(editor.schema) - return serializer.serialize(editor.state.doc) - } -}) diff --git a/cypress/e2e/nodes/Mentions.spec.js b/cypress/e2e/nodes/Mentions.spec.js deleted file mode 100644 index f62d4a6866b..00000000000 --- a/cypress/e2e/nodes/Mentions.spec.js +++ /dev/null @@ -1,88 +0,0 @@ -import { initUserAndFiles, randUser } from '../../utils/index.js' - -const user = randUser() -const mentionMe = randUser() -const mention = mentionMe.userId -const currentUser = user - -const fileName = 'empty.md' - -const createFileWithMention = (target, userToMention) => { - const content = `Hello @[${userToMention}](mention://user/${userToMention})` - cy.createFile(target, content) - .then(() => cy.reloadFileList()) -} - -describe('Test mentioning users', () => { - before(() => { - initUserAndFiles(user, 'test.md') - cy.createUser(mentionMe) - }) - - beforeEach(() => { - cy.login(currentUser) - cy.visit('/apps/files') - }) - - it('Type @ and see the user list', () => { - const requestAlias = 'fetchUsersList' - cy.intercept({ method: 'POST', url: '**/users' }).as(requestAlias) - - cy.isolateTest({ - sourceFile: fileName, - onBeforeLoad(win) { - cy.stub(win, 'open') - .as('winOpen') - }, - }) - - cy.openFile(fileName, { force: true }) - - cy.getContent() - .type(`@${mention.substring(0, 3)}`) - - return cy.wait(`@${requestAlias}`) - .then(() => { - cy.get('.tippy-box .suggestion-list').children().should(($children) => { - const users = $children.map((i, el) => el.innerText).get() - expect(users.length).to.be.greaterThan(0) - expect(mention).to.be.oneOf(users) - }) - }) - }) - - it('Select a user will insert the mention', () => { - const autocompleteReauestAlias = 'fetchUsersList' - cy.intercept({ method: 'POST', url: '**/users' }).as(autocompleteReauestAlias) - - cy.isolateTest({ - sourceFile: fileName, - onBeforeLoad(win) { - cy.stub(win, 'open') - .as('winOpen') - }, - }) - - cy.openFile(fileName, { force: true }) - - cy.get('.text-editor__content div[contenteditable="true"]') - .clear() - cy.get('.text-editor__content div[contenteditable="true"]') - .type(`@${mention.substring(0, 3)}`) - - return cy.wait(`@${autocompleteReauestAlias}`) - .then(() => { - cy.get('.tippy-box .suggestion-list').contains(mention).click() - cy.get('span.mention').contains(mention).should('be.visible') - }) - }) - - it('Open a document with an existing mention and properly see the user bubble rendered', () => { - const mentionFilename = 'mention.md' - createFileWithMention(mentionFilename, mention) - cy.openFile(mentionFilename, { force: true }) - cy.get('.text-editor__content div[contenteditable="true"] span.mention') - .contains(mention) - .should('be.visible') - }) -}) diff --git a/cypress/e2e/nodes/Table.spec.js b/cypress/e2e/nodes/Table.spec.js deleted file mode 100644 index 620eb8541d7..00000000000 --- a/cypress/e2e/nodes/Table.spec.js +++ /dev/null @@ -1,203 +0,0 @@ -import { findChildren } from './../../../src/helpers/prosemirrorUtils.js' -import { initUserAndFiles, randUser } from '../../utils/index.js' -import { createCustomEditor } from './../../support/components.js' - -import markdownit from './../../../src/markdownit/index.js' -import EditableTable from './../../../src/nodes/EditableTable.js' -import Markdown, { createMarkdownSerializer } from './../../../src/extensions/Markdown.js' - -import testData from '../../fixtures/Table.md' - -const user = randUser() -const fileName = 'empty.md' - -describe('table plugin', () => { - before(() => { - initUserAndFiles(user) - }) - - beforeEach(() => { - cy.login(user) - - cy.isolateTest({ - sourceFile: fileName, - }) - - return cy.openFile(fileName, { force: true }) - }) - - it('inserts and removes a table', () => { - cy.getContent() - .type('Let\'s insert a Table') - - cy.getActionEntry('table').click() - - cy.getContent() - .type('content') - - cy.getContent() - .find('table tr:first-child th:first-child') - .should('contain', 'content') - - cy.getContent() - .find('[data-text-table-actions="settings"]').click() - - cy.get('[data-text-table-action="delete"]').click() - cy.getContent() - .should('not.contain', 'content') - }) - - it('Enter creates newline and navigates', () => { - cy.getActionEntry('table').click() - - cy.getContent() - .find('table tr') - .should('have.length', 3) - - cy.getContent().type('first{Enter}row') - cy.getContent().type('{Enter}{Enter}second row') - cy.getContent().type('{Enter}{Enter}third row') - cy.getContent().type('{Enter}{Enter}forth row') - - // Added a row - cy.getContent() - .find('table tr') - .should('have.length', 4) - - // First cell now contains a hard break - cy.getContent() - .find('table tr:first-child th:first-child br') - .should('exist') - }) - - it('Table column alignment', () => { - cy.getActionEntry('table').click() - - cy.getContent() - .type('center'); - - ['center', 'left', 'right'].forEach(align => { - cy.getContent() - .find('table tr:first-child th:first-child button') - .click() - cy.get(`[data-text-table-action="align-column-${align}"]`) - .click() - // Check header has correct text align and text is preserved - cy.getContent() - .find('table tr:first-child th:first-child') - .should('contain', 'center') - .should('have.css', 'text-align', align) - // Check every normal cell in this column to have the text align set - cy.getContent() - .find('table tr td:first-child') - .should('have.length', 2) - .each(td => cy.wrap(td).should('have.css', 'text-align', align)) - }) - }) - - it('Keep alignment on new row', () => { - cy.getActionEntry('table').click() - - cy.getContent() - .find('table tr:first-child th:first-child button') - .click() - cy.get('[data-text-table-action="align-column-center"]') - .click() - - // Test before adding a row - cy.getContent() - .find('table tr td:first-child') - .should('have.length', 2) - .each(td => cy.wrap(td).should('have.css', 'text-align', 'center')) - - cy.getContent() - .type('1{enter}{enter}2{enter}{enter}3{enter}{enter}new 4') - - // Test after the row was added - cy.getContent() - .find('table tr td:first-child') - .should('have.length', 3) - .each(td => cy.wrap(td).should('have.css', 'text-align', 'center')) - }) - - it('Creates table and add multilines', function() { - const multilinesContent = 'Line 1\nLine 2\nLine 3' - - cy.getActionEntry('table').click() - cy.getContent() - .find('table:nth-of-type(1) tr:nth-child(2) td:nth-child(1)') - .click() - - cy.getContent() - .type(multilinesContent) - - cy.getContent() - .find('table:nth-of-type(1) tr:nth-child(2) td:nth-child(1) .content') - .then(($el) => { - expect($el.get(0).innerHTML).to.equal(multilinesContent.replace(/\n/g, '
')) - }) - }) -}) - -describe('Table extension integrated in the editor', () => { - - const editor = createCustomEditor({ - content: '', - extensions: [ - Markdown, - EditableTable, - ], - }) - - for (const spec of testData.split(/#+\s+/)) { - const [description, ...rest] = spec.split(/\n/) - const [input, output] = rest.join('\n').split(/\n\n---\n\n/) - if (!description) { - continue - } - it(description, () => { - expect(spec).to.include('\n') - /* eslint-disable no-unused-expressions */ - expect(input).to.be.ok - expect(output).to.be.ok - /* eslint-enable no-unused-expressions */ - loadMarkdown(input) - runCommands() - expectMarkdown(output.replace(/\n*$/, '')) - }) - } - - const loadMarkdown = (markdown) => { - editor.commands.setContent(markdownit.render(markdown)) - } - - const runCommands = () => { - let found - while ((found = findCommand())) { - const name = found.node.text - editor.commands.setTextSelection(found.pos) - editor.commands[name]() - const updated = findCommand() - if (updated) { - editor.commands.setTextSelection(updated.pos) - editor.commands.insertContent('did ') - } - } - } - - const findCommand = () => { - const doc = editor.state.doc - return findChildren(doc, child => { - return child.isText && Object.prototype.hasOwnProperty.call(editor.commands, child.text) - })[0] - } - - const expectMarkdown = (markdown) => { - expect(getMarkdown().replace(/\n$/, '')).to.equal(markdown) - } - - const getMarkdown = () => { - const serializer = createMarkdownSerializer(editor.schema) - return serializer.serialize(editor.state.doc) - } -}) diff --git a/cypress/e2e/print.spec.js b/cypress/e2e/print.spec.js deleted file mode 100644 index fbee361c0cd..00000000000 --- a/cypress/e2e/print.spec.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * - * @copyright Copyright (c) 2023 Jonas - * - * @author Jonas - * - * @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('Open print.md and compare print view', function() { - before(function() { - initUserAndFiles(user, 'print.md') - }) - beforeEach(function() { - cy.login(user) - cy.visit('/apps/files') - }) - - it('Renders print view in viewer', function() { - cy.openFile('print.md') - cy.setCssMedia('print') - - cy.getEditor().should('be.visible') - cy.getContent() - // Ensure cursor is not displayed to prevent flaky tests (flashing input cursor) - .invoke('css', 'caret-color', 'transparent') - .get('h1:not(.hidden-visually)').should('contain', 'Print test') - .should('be.visible') - - cy.compareSnapshot('print view in viewer', { capture: 'fullPage' }) - cy.setCssMedia('screen') - }) - - it('Renders print view in single-file share', function() { - cy.shareFile('/print.md', { edit: true }) - .then((token) => { - cy.logout() - cy.visit(`/s/${token}`) - cy.setCssMedia('print') - }) - .then(() => { - cy.getEditor().should('be.visible') - cy.getContent() - // Ensure cursor is not displayed to prevent flaky tests (flashing input cursor) - .invoke('css', 'caret-color', 'transparent') - .get('h1:not(.hidden-visually)').should('contain', 'Print test') - .should('be.visible') - - cy.compareSnapshot('print view in single-file share', { capture: 'fullPage' }) - cy.setCssMedia('screen') - }) - }) -}) diff --git a/cypress/e2e/propfind.spec.js b/cypress/e2e/propfind.spec.js deleted file mode 100644 index a40966f7c09..00000000000 --- a/cypress/e2e/propfind.spec.js +++ /dev/null @@ -1,94 +0,0 @@ -/** - * @copyright Copyright (c) 2022 Max - * - * @author Max - * - * @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 { randUser } from '../utils/index.js' - -const user = randUser() - -describe('Text PROPFIND extension ', function() { - const richWorkspace = '{http://nextcloud.org/ns}rich-workspace' - - before(function() { - cy.createUser(user) - }) - - beforeEach(function() { - cy.login(user) - cy.visit('/apps/files') - }) - - describe('with workspaces enabled', function() { - - beforeEach(function() { - cy.configureText('workspace_enabled', 1) - }) - - // Android app relies on this to detect rich workspace availability - it('always adds rich workspace property', function() { - cy.uploadFile('empty.md', 'text/markdown', '/Readme.md') - cy.visit('/apps/files') - cy.propfindFolder('/') - .should('have.property', richWorkspace, '') - cy.uploadFile('test.md', 'text/markdown', '/Readme.md') - cy.propfindFolder('/') - .should('have.property', richWorkspace, '## Hello world\n') - cy.removeFile('/Readme.md') - cy.propfindFolder('/') - .should('have.property', richWorkspace, '') - }) - - // Android app relies on this when navigating nested folders - it('adds rich workspace property to nested folders', function() { - cy.createFolder('/workspace') - cy.visit('/apps/files') - cy.propfindFolder('/', 1) - .then(results => results.pop().propStat[0].properties) - .should('have.property', richWorkspace, '') - cy.uploadFile('test.md', 'text/markdown', '/workspace/Readme.md') - cy.propfindFolder('/', 1) - .then(results => results.pop().propStat[0].properties) - .should('have.property', richWorkspace, '## Hello world\n') - }) - - }) - - describe('with workspaces disabled', function() { - - beforeEach(function() { - cy.configureText('workspace_enabled', 0) - }) - - it('does not return a rich workspace property', function() { - cy.visit('/apps/files') - cy.propfindFolder('/') - .should('not.have.property', richWorkspace) - cy.uploadFile('test.md', 'text/markdown', '/Readme.md') - cy.propfindFolder('/') - .should('not.have.property', richWorkspace) - cy.createFolder('/without-workspace') - cy.propfindFolder('/', 1) - .then(results => results.pop().propStat[0].properties) - .should('not.have.property', richWorkspace) - }) - - }) -}) diff --git a/cypress/e2e/sections.spec.js b/cypress/e2e/sections.spec.js deleted file mode 100644 index e4c4f48321a..00000000000 --- a/cypress/e2e/sections.spec.js +++ /dev/null @@ -1,144 +0,0 @@ -import { randUser } from '../utils/index.js' - -const user = randUser() -const fileName = 'empty.md' - -const clickOutline = () => { - cy.getActionEntry('headings') - .click() - - cy.get('.v-popper__wrapper .open').getActionEntry('outline') - .click() -} - -let currentFolder - -describe('Content Sections', () => { - before(function() { - cy.createUser(user) - }) - - beforeEach(function() { - cy.login(user) - cy.createTestFolder().then(folderName => { - currentFolder = folderName - cy.uploadFile(fileName, 'text/markdown', `${currentFolder}/${fileName}`) - }) - }) - - describe('Heading anchors', () => { - it('Anchor exists', () => { - cy.visitTestFolder() - cy.openFile(fileName, { force: true }) - cy.getContent() - .type('# Heading\nText\n## Heading 2\nText\n## Heading 2') - cy.getContent().find('a.heading-anchor') - .should(($anchor) => { - expect($anchor).to.have.length(3) - expect($anchor.eq(0)).to.have.attr('href').and.equal('#h-heading') - expect($anchor.eq(1)).to.have.attr('href').and.equal('#h-heading-2') - expect($anchor.eq(2)).to.have.attr('href').and.equal('#h-heading-2--1') - }) - }) - - it('Anchor ID is updated', () => { - cy.visitTestFolder() - cy.openFile(fileName, { force: true }) - cy.getContent().type('# Heading 1{enter}') - cy.getContent() - .find('h1') - .should('have.attr', 'id') - .and('equal', 'h-heading-1') - cy.getContent() - .find('a.heading-anchor') - .should('have.attr', 'href') - .and('equal', '#h-heading-1') - cy.getContent().type('{backspace}{backspace}2{enter}') - cy.getContent() - .find('h1') - .should('have.attr', 'id') - .and('equal', 'h-heading-2') - cy.getContent() - .find('a.heading-anchor') - .should('have.attr', 'href') - .and('equal', '#h-heading-2') - }) - - it('scrolls anchor into view', () => { - cy.uploadFile('anchors.md', 'text/markdown', `${currentFolder}/anchors.md`) - cy.visitTestFolder() - cy.openFile('anchors.md') - cy.getContent() - .get('h2[id="h-bottom"]') - .should('not.be.inViewport') - cy.getContent() - .find('a[href="#h-bottom"]:not(.heading-anchor)') - .click() - cy.getContent() - .get('h2[id="h-bottom"]') - .should('be.inViewport') - }) - - it('Can change heading level', () => { - cy.visitTestFolder() - cy.openFile(fileName, { force: true }) - // Issue #2868 - cy.getContent() - .type('# Heading 1{enter}') - cy.getContent() - .find('h1') - .should('have.attr', 'id') - .and('equal', 'h-heading-1') - cy.getContent() - .find('h1 [data-node-view-content]') - .click({ force: true, position: 'center' }) - cy.getActionEntry('headings').click() - cy.get('.v-popper__wrapper .open').getActionEntry('headings-h3').click() - cy.getContent().find('h3') - .should('have.attr', 'id') - .and('equal', 'h-heading-1') - }) - }) - - describe('Table of Contents', () => { - it('sidebar toc', () => { - cy.visitTestFolder() - cy.openFile(fileName, { force: true }) - cy.getContent() - .type('# T1 \n## T2 \n### T3 \n#### T4 \n##### T5 \n###### T6\n') - cy.closeFile() - .then(() => cy.openFile(fileName, { force: true })) - .then(clickOutline) - - cy.getOutline() - .find('header') - .should('exist') - - cy.getTOC() - .find('ul li') - .should('have.length', 6) - cy.getTOC() - .find('ul li') - .each((el, index) => { - cy.wrap(el) - .should('have.attr', 'data-toc-level') - .and('equal', String(index + 1)) - - cy.wrap(el) - .find('a') - .should('have.attr', 'href') - .and('equal', `#h-t${index + 1}`) - }) - }) - - it('empty toc', () => { - cy.visitTestFolder() - cy.openFile(fileName, { force: true }) - .then(clickOutline) - - cy.getOutline() - .find('ul') - .should('be.empty') - }) - }) -}) diff --git a/cypress/e2e/share.spec.js b/cypress/e2e/share.spec.js deleted file mode 100644 index 98f42b53ce1..00000000000 --- a/cypress/e2e/share.spec.js +++ /dev/null @@ -1,150 +0,0 @@ -/** - * @copyright Copyright (c) 2020 Julius Härtl - * - * @author Julius Härtl - * - * @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 { randUser } from '../utils/index.js' - -const user = randUser() -const recipient = randUser() - -describe('Open test.md in viewer', function() { - before(function() { - // Init user - cy.createUser(user) - cy.login(user) - - // Upload test files - cy.createFolder('folder') - cy.uploadFile('test.md', 'text/markdown', 'folder/test.md') - cy.uploadFile('test.md', 'text/markdown', 'folder/Readme.md') - cy.uploadFile('test.md', 'text/markdown', 'test2.md') - cy.uploadFile('test.md', 'text/markdown') - - cy.createUser(recipient) - }) - beforeEach(function() { - cy.login(user) - cy.visit('/apps/files') - }) - - it('Shares the file as a public read only link', function() { - cy.shareFile('/test.md') - .then((token) => { - cy.logout() - cy.visit(`/s/${token}`) - }) - .then(() => { - cy.getEditor().should('be.visible') - cy.getContent() - .should('contain', 'Hello world') - .find('h2').should('contain', 'Hello world') - - cy.get('.text-editor--readonly-bar') - .getActionEntry('outline') - .click() - - cy.getOutline() - .find('header') - .should('exist') - }) - }) - - it('Shares the file as a public link with write permissions', function() { - cy.shareFile('/test2.md', { edit: true }) - .then((token) => { - cy.visit(`/s/${token}`) - }) - .then(() => { - cy.getEditor().should('be.visible') - cy.getContent() - .should('contain', 'Hello world') - .find('h2').should('contain', 'Hello world') - - cy.getMenu().should('be.visible') - cy.getActionEntry('undo').should('be.visible') - cy.getActionEntry('redo').should('be.visible') - cy.getActionEntry('bold').should('be.visible') - }) - }) - - it('Opens the editor as guest', function() { - cy.shareFile('/test2.md', { edit: true }) - .then((token) => { - cy.logout() - cy.visit(`/s/${token}`) - }) - .then(() => { - cy.getEditor().should('be.visible') - cy.getContent() - .should('contain', 'Hello world') - .find('h2').should('contain', 'Hello world') - - cy.getMenu().should('be.visible') - cy.getActionEntry('undo').should('be.visible') - cy.getActionEntry('redo').should('be.visible') - cy.getActionEntry('bold').should('be.visible') - }) - }) - - it('Shares a folder as a public read only link', function() { - cy.shareFile('/folder') - .then((token) => { - cy.logout() - - return cy.visit(`/s/${token}`) - }) - .then(() => { - cy.openFileInShare('test.md') - cy.getModal().getContent().should('be.visible') - cy.getModal().getContent().should('contain', 'Hello world') - cy.getModal().getContent().find('h2').should('contain', 'Hello world') - cy.getModal().find('.modal-header button.header-close').click() - cy.get('.modal-mask').should('not.exist') - // cy.get('#rich-workspace').getContent().should('contain', 'Hello world') - }) - }) - - it('Share a file with download disabled shows an error', function() { - cy.shareFileToUser('test.md', recipient, { - attributes: '[{"scope":"permissions","key":"download","enabled":false}]', - }).then(() => { - cy.login(recipient) - cy.visit('/apps/files') - cy.openFile('test.md') - cy.getModal().find('.empty-content__name').should('contain', 'Failed to load file') - cy.getModal().getContent().should('not.exist') - }) - }) - - it('makes use of the file id', function() { - cy.intercept({ method: 'PUT', url: '**/apps/text/public/session/*/create' }) - .as('create') - cy.shareFile('/test2.md', { edit: true }) - .then((token) => { - cy.logout() - cy.visit(`/s/${token}`) - }) - cy.wait('@create', { timeout: 10000 }) - .its('request.body.fileId') - .should('be.a', 'Number') - }) - -}) diff --git a/cypress/e2e/shortcuts.spec.js b/cypress/e2e/shortcuts.spec.js deleted file mode 100644 index 24bd3dd2e32..00000000000 --- a/cypress/e2e/shortcuts.spec.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * @copyright Copyright (c) 2022 Vinicius Reis - * - * @author Vinicius Reis - * - * @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 { randUser } from '../utils/index.js' -const user = randUser() - -const modKey = Cypress.platform === 'darwin' ? '{cmd}' : '{ctrl}' -const testShortcut = (shortcut, tag) => { - cy.getContent() - .type(shortcut) - - cy.getContent() - .find(tag) - .should('contain', Cypress.currentTest.title) - - return cy.closeFile() -} - -const testHeading = (num) => { - testShortcut(`${modKey}{shift}${num}`, `h${num}`) -} - -describe('keyboard shortcuts', () => { - - before(() => { - cy.createUser(user) - }) - - beforeEach(() => { - cy.login(user) - cy.uploadTestFile() - cy.visit('/apps/files') - cy.openTestFile() - cy.getContent().type(Cypress.currentTest.title) - cy.getContent().type('{selectall}') - }) - - it('bold', () => testShortcut(`${modKey}b`, 'strong')) - it('italic', () => testShortcut(`${modKey}i`, 'em')) - it('underline', () => testShortcut(`${modKey}u`, 'u')) - it('strikethrough', () => testShortcut(`${modKey}{shift}s`, 's')) - it('blockquote', () => testShortcut(`${modKey}{shift}b`, 'blockquote')) - it('codeblock', () => testShortcut(`${modKey}{alt}c`, 'pre')) - it('ordered-list', () => testShortcut(`${modKey}{shift}7`, 'ol')) - it('unordered-list', () => testShortcut(`${modKey}{shift}8`, 'ul')) - it('task-list', () => testShortcut(`${modKey}{shift}9`, 'ul[data-type="taskList"]')) - - // Headings - const levels = [1, 2, 3, 4, 5, 6] - levels.forEach((level) => { - it(`heading-${level}`, () => testHeading(level)) - }) - -}) diff --git a/cypress/e2e/sync.spec.js b/cypress/e2e/sync.spec.js deleted file mode 100644 index 01a68ab7035..00000000000 --- a/cypress/e2e/sync.spec.js +++ /dev/null @@ -1,154 +0,0 @@ -/** - * @copyright Copyright (c) 2023 Max - * - * @author Max - * - * @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 { randUser } from '../utils/index.js' - -const user = randUser() - -describe('Sync', () => { - before(() => { - cy.createUser(user) - }) - - beforeEach(() => { - cy.login(user) - cy.uploadTestFile('test.md') - cy.visit('/apps/files') - cy.intercept({ method: 'POST', url: '**/apps/text/session/*/sync' }).as('sync') - cy.intercept({ method: 'POST', url: '**/apps/text/session/*/save' }).as('save') - cy.openTestFile() - cy.getContent().find('h2').should('contain', 'Hello world') - cy.getContent().type('{moveToEnd}* Saving the doc saves the doc state{enter}') - }) - - it('saves the actual file and document state', () => { - cy.getContent().type('{ctrl+s}') - cy.wait('@save').its('request.body') - .should('have.property', 'documentState') - .should('not.be.empty') - cy.testName() - .then(name => cy.downloadFile(`/${name}.md`)) - .its('data') - .should('include', 'saves the doc state') - cy.closeFile() - }) - - it('saves on close', () => { - cy.closeFile() - cy.wait('@save') - cy.testName() - .then(name => cy.downloadFile(`/${name}.md`)) - .its('data') - .should('include', 'saves the doc state') - }) - - it('recovers from a short lost connection', () => { - let reconnect = false - cy.intercept('**/apps/text/session/*/*', (req) => { - if (reconnect) { - req.continue() - req.alias = 'alive' - } else { - req.destroy() - req.alias = 'dead' - } - }).as('sessionRequests') - cy.wait('@dead', { timeout: 30000 }) - cy.get('#editor-container .document-status', { timeout: 30000 }) - .should('contain', 'File could not be loaded') - .then(() => { - reconnect = true - }) - cy.wait('@alive', { timeout: 30000 }) - cy.intercept({ method: 'POST', url: '**/apps/text/session/*/sync' }) - .as('syncAfterRecovery') - cy.wait('@syncAfterRecovery', { timeout: 30000 }) - cy.get('#editor-container .document-status', { timeout: 30000 }) - .should('not.contain', 'File 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.wait('@syncAfterRecovery', { timeout: 10000 }) - cy.closeFile() - cy.testName() - .then(name => cy.downloadFile(`/${name}.md`)) - .its('data') - .should('include', 'after the lost connection') - }) - - it('recovers from a lost and closed connection', () => { - let reconnect = false - cy.intercept('**/apps/text/session/*/*', (req) => { - if (req.url.includes('close') || req.url.includes('create') || reconnect) { - req.continue() - req.alias = 'syncAfterRecovery' - reconnect = true - } else { - req.destroy() - } - }).as('sessionRequests') - - cy.wait('@sessionRequests', { timeout: 30000 }) - cy.get('#editor-container .document-status', { timeout: 30000 }) - .should('contain', 'File could not be loaded') - - cy.wait('@syncAfterRecovery', { timeout: 60000 }) - - cy.get('#editor-container .document-status', { timeout: 30000 }) - .should('not.contain', 'File 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.wait('@syncAfterRecovery', { timeout: 10000 }) - cy.closeFile() - cy.testName() - .then(name => cy.downloadFile(`/${name}.md`)) - .its('data') - .should('include', 'after the lost connection') - }) - - it('passes the doc content from one session to the next', () => { - cy.closeFile() - cy.intercept({ method: 'PUT', url: '**/apps/text/session/*/create' }) - .as('create') - cy.openTestFile() - cy.wait('@create', { timeout: 10000 }) - .its('response.body') - .should('have.property', 'documentState') - .should('not.be.empty') - cy.getContent().find('h2').should('contain', 'Hello world') - cy.getContent().find('li').should('contain', 'Saving the doc saves the doc state') - cy.getContent().type('recovered') - cy.closeFile() - }) - - // regression test #3834 - it('close triggers one close request', () => { - cy.closeFile() - // Wait to make sure there is enough time for all close requests to run - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(100) - // reuse @close intercepted in closeFile() - cy.get('@close.all').should('have.length', 1) - }) - -}) diff --git a/cypress/e2e/versions.spec.js b/cypress/e2e/versions.spec.js deleted file mode 100644 index cf345b2b9cb..00000000000 --- a/cypress/e2e/versions.spec.js +++ /dev/null @@ -1,120 +0,0 @@ -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:contains("Versions")').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:contains("Versions")').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:contains("Versions")').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/e2e/viewer.spec.js b/cypress/e2e/viewer.spec.js deleted file mode 100644 index 640f61b1c34..00000000000 --- a/cypress/e2e/viewer.spec.js +++ /dev/null @@ -1,122 +0,0 @@ -/** - * @copyright Copyright (c) 2019 John Molakvoæ - * - * @author John Molakvoæ - * - * @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('Open test.md in viewer', function() { - const getViewer = () => cy.get('#viewer') - - before(function() { - initUserAndFiles(user, 'test.md', 'empty.md') - }) - - beforeEach(function() { - cy.login(user) - cy.visit('/apps/files') - }) - - it('See test.md in the list', function() { - cy.getFile('test.md') - .should('be.visible') - }) - - it('Open the viewer on file click', function() { - cy.openFile('test.md') - - cy.log('Inspect viewer') - getViewer().should('be.visible') - .and('have.class', 'modal-mask') - .and('not.have.class', 'icon-loading') - getViewer() - .find('.modal-name').should('contain', 'test.md') - getViewer() - .find('.modal-header button.action-item__menutoggle') - .should('be.visible') - - cy.log('Inspect editor') - cy.getContent() - .should('contain', 'Hello world') - cy.getContent() - .get('h2').should('contain', 'Hello world') - - cy.log('Inspect menubar') - cy.getActionEntry('undo').should('be.visible') - cy.getActionEntry('bold').should('be.visible') - }) - - it('Open an empty file', function() { - cy.openFile('empty.md') - - cy.log('Inspect viewer') - getViewer().should('be.visible') - .and('have.class', 'modal-mask') - .and('not.have.class', 'icon-loading') - getViewer() - .find('.modal-name').should('contain', 'empty.md') - getViewer() - .find('.modal-header button.action-item__menutoggle') - .should('be.visible') - - cy.log('Inspect editor') - cy.getContent().should('contain', '') - - cy.log('Inspect menubar') - cy.getActionEntry('undo').should('be.visible') - cy.getActionEntry('bold').should('be.visible') - }) - - it('Closes the editor', function() { - cy.openFile('test.md') - cy.get('#viewer .modal-header button.header-close').click() - cy.get('#viewer').should('not.exist') - }) - - it('Can use tab keys for list in the viewer', function() { - // This used to break with the focus trap that the viewer modal has - cy.openFile('empty.md') - - cy.getContent() - .type('- test{enter}') - - // Cypress does not have native tab key support, though this seems to work - // for triggering the key handler of tiptap - // https://github.com/cypress-io/cypress/issues/311 - cy.getContent() - .trigger('keydown', { keyCode: 9 }) - cy.getContent() - .trigger('keyup', { keyCode: 9 }) - cy.getContent() - .type('Nested list') - - cy.getContent().find('ul li').should('contain', 'test') - cy.getContent().find('ul li ul li').should('contain', 'Nested list') - - cy.getContent() - .trigger('keydown', { keyCode: 9, shiftKey: true }) - cy.getContent() - .trigger('keyup', { keyCode: 9, shiftKey: true }) - - cy.getContent().find('ul li').eq(0).should('contain', 'test') - cy.getContent().find('ul li').eq(1).should('contain', 'Nested list') - }) -}) diff --git a/cypress/e2e/workspace.spec.js b/cypress/e2e/workspace.spec.js deleted file mode 100644 index 12343e83637..00000000000 --- a/cypress/e2e/workspace.spec.js +++ /dev/null @@ -1,332 +0,0 @@ -/** - * @copyright Copyright (c) 2021 Azul - * - * @author Azul - * - * @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 { randUser } from '../utils/index.js' -const user = randUser() - -describe('Workspace', function() { - let currentFolder - - before(function() { - cy.createUser(user, 'password') - }) - - beforeEach(function() { - cy.login(user) - // isolate tests - each happens in its own folder - const retry = cy.state('test').currentRetry() - currentFolder = retry - ? `${Cypress.currentTest.title} (${retry})` - : Cypress.currentTest.title - cy.createFolder(currentFolder) - }) - - it('Hides the workspace when switching to another folder', function() { - cy.uploadFile('test.md', 'text/markdown', `${currentFolder}/README.md`) - cy.createFolder(`${currentFolder}/subdirectory`) - cy.visit(`apps/files?dir=/${encodeURIComponent(currentFolder)}`) - cy.getFile('README.md') - cy.get('#rich-workspace .ProseMirror') - .should('contain', 'Hello world') - cy.openFolder('subdirectory') - cy.get('#rich-workspace') - .should('not.exist') - }) - - it('Hides the workspace when switching to another view', function() { - cy.uploadFile('test.md', 'text/markdown', `${currentFolder}/README.md`) - cy.visit(`apps/files?dir=/${encodeURIComponent(currentFolder)}`) - cy.getFile('README.md') - cy.get('#rich-workspace .ProseMirror') - .should('contain', 'Hello world') - cy.get('a[href*="/apps/files/recent"]') - .click() - cy.get('#rich-workspace') - .should('not.exist') - }) - - it('adds a Readme.md', function() { - cy.createDescription(currentFolder) - openSidebar('Readme.md') - cy.get('#rich-workspace .text-editor .text-editor__wrapper') - .should('be.visible') - }) - - it('formats text', function() { - cy.visit(`apps/files?dir=/${encodeURIComponent(currentFolder)}`) - cy.openWorkspace(currentFolder) - const buttons = [ - ['bold', 'strong'], - ['italic', 'em'], - ['underline', 'u'], - ['strikethrough', 's'], - ] - cy.getContent().click() - buttons.forEach(([button, tag]) => testButtonUnselected(button, tag)) - cy.getContent().type('Format me{selectall}') - buttons.forEach(([button, tag]) => testButton(button, tag, 'Format me')) - }) - - it('creates headings via submenu', function() { - cy.visit(`apps/files?dir=/${encodeURIComponent(currentFolder)}`) - cy.openWorkspace(currentFolder).type('Heading') - cy.getContent().type('{selectall}') - ;['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].forEach((heading) => { - const actionName = `headings-${heading}` - - cy.getSubmenuEntry('headings', actionName).click() - - cy.getContent() - .find(`${heading}`) - .should('contain', 'Heading') - - cy.getSubmenuEntry('headings', actionName) - .should('have.class', 'is-active') - .click() - - cy.getMenuEntry('headings').should('not.have.class', 'is-active') - }) - }) - - it('creates lists', function() { - cy.visit(`apps/files?dir=/${encodeURIComponent(currentFolder)}`) - cy.openWorkspace(currentFolder).type('List me') - cy.getContent().type('{selectall}') - ;[ - ['unordered-list', 'ul'], - ['ordered-list', 'ol'], - ['task-list', 'ul[data-type="taskList"]'], - ].forEach(([button, tag]) => testButton(button, tag, 'List me')) - }) - - it('takes README.md into account', function() { - cy.uploadFile('test.md', 'text/markdown', `${Cypress.currentTest.title}/README.md`) - cy.visit(`apps/files?dir=/${encodeURIComponent(currentFolder)}`) - cy.getFile('README.md') - cy.get('#rich-workspace .ProseMirror') - .should('contain', 'Hello world') - }) - - it('emoji picker', () => { - cy.visit(`apps/files?dir=/${encodeURIComponent(currentFolder)}`) - cy.openWorkspace(currentFolder) - .type('# Let\'s smile together{enter}## ') - - cy.getMenuEntry('emoji-picker') - .click() - - cy.get('#emoji-mart-list button[aria-label="😀, grinning"]') - .first() - .click() - - cy.getEditor() - .find('h2') - .contains('😀') - }) - - it('relative folder links', () => { - cy.createFolder(`${currentFolder}/sub-folder`) - cy.createFolder(`${currentFolder}/sub-folder/alpha`) - - cy.uploadFile('test.md', 'text/markdown', `${currentFolder}/sub-folder/alpha/test.md`) - cy.visit(`apps/files?dir=/${encodeURIComponent(currentFolder)}`) - - cy.openWorkspace(currentFolder) - .type('link me') - cy.getContent() - .type('{selectall}') - - cy.getSubmenuEntry('insert-link', 'insert-link-file') - .click() - - cy.get('.file-picker__main .file-picker__file-name[title="sub-folder"]').click() - cy.get('.file-picker__main .file-picker__file-name[title="alpha"]').click() - cy.get('.file-picker__main .file-picker__file-name[title="test"]').click() - cy.get('.dialog__actions button.button-vue--vue-primary').click() - - cy.getEditor() - .find('a') - .should('have.attr', 'href') - .and('contains', `dir=/${currentFolder}/sub-folder/alpha`) - .and('contains', '#relPath=sub-folder/alpha/test.md') - - cy.getEditor() - .find('a').click() - - cy.getModal() - .find('.modal-header') - .contains('test.md') - - cy.getModal() - .getEditor() - .contains('Hello world') - - cy.getModal().find('button.header-close').click() - }) - - describe('callouts', () => { - const types = ['info', 'warn', 'error', 'success'] - - beforeEach(function() { - cy.visit(`apps/files?dir=/${encodeURIComponent(currentFolder)}`) - cy.openWorkspace(currentFolder).type('Callout') - }) - // eslint-disable-next-line cypress/no-async-tests - it('create callout', () => { - cy.wrap(types).each((type) => { - cy.log(`creating ${type} callout`) - - const actionName = `callout-${type}` - - // enable callout - cy.getSubmenuEntry('callouts', actionName).click() - - // check content - cy.getContent() - .find(`.callout.callout--${type}`) - .should('contain', 'Callout') - - // disable - cy.getSubmenuEntry('callouts', actionName) - .should('have.class', 'is-active') - .click() - }) - }) - - it('toggle callouts', () => { - const [first, ...rest] = types - - // enable callout - cy.getSubmenuEntry('callouts', `callout-${first}`) - .click() - - cy.wrap(rest).each(type => { - const actionName = `callout-${type}` - cy.getSubmenuEntry('callouts', actionName).click() - cy.getContent().find(`.callout.callout--${type}`) - .should('contain', 'Callout') - }) - - cy.getSubmenuEntry('callouts', `callout-${rest.at(-1)}`) - .click() - cy.getMenuEntry('callouts') - .should('not.have.class', 'is-active') - }) - }) - - describe('localize', () => { - it('takes localized file name into account', function() { - cy.modifyUser(user, 'language', 'de_DE') - cy.uploadFile('test.md', 'text/markdown', `${Cypress.currentTest.title}/Anleitung.md`) - cy.visit(`apps/files?dir=/${encodeURIComponent(currentFolder)}`) - cy.getFile('Anleitung.md') - cy.get('#rich-workspace .ProseMirror') - .should('contain', 'Hello world') - }) - - it('ignores localized file name in other language', function() { - cy.modifyUser(user, 'language', 'fr') - cy.uploadFile('test.md', 'text/markdown', `${Cypress.currentTest.title}/Anleitung.md`) - cy.visit(`apps/files?dir=/${encodeURIComponent(currentFolder)}`) - cy.getFile('Anleitung.md') - }) - }) - - describe('create Readme.md', () => { - const checkContent = () => { - const txt = Cypress.currentTest.title - - cy.getEditor().find('[data-text-el="editor-content-wrapper"]').click() - - cy.getContent().type(txt) - cy.getContent().should('contain', txt) - } - - beforeEach(() => { - cy.visit(`apps/files?dir=/${encodeURIComponent(currentFolder)}`) - }) - - it('click', () => { - cy.openWorkspace(currentFolder).click() - checkContent() - }) - - it('enter', () => { - cy.openWorkspace(currentFolder).type('{enter}') - checkContent() - }) - - it('spacebar', () => { - cy.openWorkspace(currentFolder) - .trigger('keyup', { - keyCode: 32, - which: 32, - shiftKey: false, - ctrlKey: false, - force: true, - }) - checkContent() - }) - }) - -}) - -const openSidebar = filename => { - cy.getFile(filename).find('.files-list__row-mtime').click() - cy.get('.app-sidebar-header').should('contain', filename) -} - -/** - * - * @param {string} button Name of the button to click. - * @param {string} tag Html tag expected to be toggled. - * @param {string} content Content expected in the element. - */ -function testButton(button, tag, content) { - cy.getMenuEntry(button).click() - cy.getMenuEntry(button).should('have.class', 'is-active') - cy.getContent() - .find(`${tag}`) - .should('contain', content) - cy.getMenuEntry(button).click() - cy.getMenuEntry(button).should('not.have.class', 'is-active') -} - -/** - * - * @param {string} button Name of the button to click. - * @param {string} tag Html tag expected to be toggled. - */ -function testButtonUnselected(button, tag) { - cy.getMenuEntry(button).click() - cy.getMenuEntry(button).should('have.class', 'is-active') - cy.getContent().type('Format me{selectall}') - cy.getContent().find(`${tag}`) - .should('contain', 'Format me').type('{del}') - cy.getMenuEntry(button).click() - cy.getMenuEntry(button).should('have.class', 'is-active').click() - cy.getMenuEntry(button).should('not.have.class', 'is-active') - cy.getContent().type('Format me{selectall}') - cy.getMenuEntry(button).find(`${tag}`) - .should('not.exist') - cy.getContent().type('{del}') -}