diff --git a/cypress/e2e/circle-with-group.spec.js b/cypress/e2e/circle-with-group.spec.js index 3e82d2467..9fc448a37 100644 --- a/cypress/e2e/circle-with-group.spec.js +++ b/cypress/e2e/circle-with-group.spec.js @@ -34,7 +34,9 @@ describe('Pages are accessible via group membership to circle', function() { cy.loginAs('jane') cy.visit('apps/collectives') cy.deleteAndSeedCollective('Group Collective') - cy.seedCircleMember('Group Collective', 'Bobs Group', 2, 8) + cy.circleFind('Group Collective') + .circleAddMember('Bobs Group', 2) + .circleSetMemberLevel(8) }) it('Lists the collective', function() { diff --git a/cypress/e2e/collective-members.spec.js b/cypress/e2e/collective-members.spec.js index b9fad1e87..b764eae5d 100644 --- a/cypress/e2e/collective-members.spec.js +++ b/cypress/e2e/collective-members.spec.js @@ -28,7 +28,6 @@ describe('Collective members', function() { before(function() { cy.loginAs('bob') cy.visit('apps/collectives') - cy.deleteCollective('Members Collective') cy.deleteAndSeedCollective('Members Collective') }) diff --git a/cypress/e2e/collective-readonly.spec.js b/cypress/e2e/collective-readonly.spec.js index a9de3700b..afc92f4ea 100644 --- a/cypress/e2e/collective-readonly.spec.js +++ b/cypress/e2e/collective-readonly.spec.js @@ -28,7 +28,8 @@ describe('Read-only collective', function() { cy.deleteAndSeedCollective('PermissionCollective') cy.seedPage('SecondPage', '', 'Readme.md') cy.seedCollectivePermissions('PermissionCollective', 'edit', 4) - cy.seedCircleMember('PermissionCollective', 'bob') + cy.circleFind('PermissionCollective') + .circleAddMember('bob') }) describe('in read-only collective', function() { diff --git a/cypress/e2e/collective.spec.js b/cypress/e2e/collective.spec.js index f00da6a12..f7a739e37 100644 --- a/cypress/e2e/collective.spec.js +++ b/cypress/e2e/collective.spec.js @@ -34,7 +34,8 @@ describe('Collective', function() { cy.deleteCollective('History Club') cy.deleteCollective(specialCollective) cy.deleteAndSeedCollective('Preexisting Collective') - cy.seedCircleMember('Preexisting Collective', 'jane') + cy.circleFind('Preexisting Collective') + .circleAddMember('jane') cy.seedCircle('Preexisting Circle') cy.seedCircle('History Club', { visible: true, open: true }) cy.loginAs('jane') diff --git a/cypress/e2e/page-details.spec.js b/cypress/e2e/page-details.spec.js index ffddbabe5..34249b501 100644 --- a/cypress/e2e/page-details.spec.js +++ b/cypress/e2e/page-details.spec.js @@ -55,8 +55,7 @@ describe('Page details', function() { .find('.editor--toc .editor--toc__item') .should('contain', 'Second-Level Heading') - // Switch to edit mode - cy.switchPageMode(1) + cy.switchToEditMode() cy.getEditor() .find('.editor--toc .editor--toc__item') @@ -68,7 +67,7 @@ describe('Page details', function() { .click() // Switch back to view mode - cy.switchPageMode(0) + cy.switchToViewMode() cy.get('.editor--toc') .should('not.exist') diff --git a/cypress/e2e/page-landingpage.spec.js b/cypress/e2e/page-landingpage.spec.js index 7ac5df6f0..4d92fa2db 100644 --- a/cypress/e2e/page-landingpage.spec.js +++ b/cypress/e2e/page-landingpage.spec.js @@ -31,9 +31,9 @@ describe('Page landing page', function() { cy.loginAs('bob') cy.visit('/apps/collectives') cy.deleteAndSeedCollective(collective) - cy.seedCircleMember(collective, 'alice') - cy.seedCircleMember(collective, 'jane') - cy.seedCircleMember(collective, 'john') + cy.circleFind(collective).circleAddMember('alice') + cy.circleFind(collective).circleAddMember('jane') + cy.circleFind(collective).circleAddMember('john') cy.seedPage('Page 1', '', 'Readme.md') cy.seedPage('Page 2', '', 'Readme.md') cy.seedPage('Page 3', '', 'Readme.md') diff --git a/cypress/e2e/page-list.spec.js b/cypress/e2e/page-list.spec.js index 7a641d0e7..24fa8df5c 100644 --- a/cypress/e2e/page-list.spec.js +++ b/cypress/e2e/page-list.spec.js @@ -257,7 +257,7 @@ describe('Page list', function() { cy.getEditor() .should('be.visible') .type('text') - cy.switchPageMode(0) + cy.switchToViewMode() // Trash page cy.openPageMenu('Day 1') diff --git a/cypress/e2e/pages-links.spec.js b/cypress/e2e/pages-links.spec.js index 11ee56033..f00b22a4c 100644 --- a/cypress/e2e/pages-links.spec.js +++ b/cypress/e2e/pages-links.spec.js @@ -103,8 +103,7 @@ describe('Page Link Handling', function() { const clickLink = function(href, edit) { if (edit) { - // Change to edit mode - cy.switchPageMode(1) + cy.switchToEditMode() cy.getEditor() .should('be.visible') .find(`a[href="${href}"]`) diff --git a/cypress/e2e/pages.spec.js b/cypress/e2e/pages.spec.js index 60be285a9..982501bd1 100644 --- a/cypress/e2e/pages.spec.js +++ b/cypress/e2e/pages.spec.js @@ -188,7 +188,7 @@ describe('Page', function() { cy.wait(1000) // eslint-disable-line cypress/no-unnecessary-waiting // Switch back to view mode - cy.switchPageMode(0) + cy.switchToViewMode() cy.getEditor() .should('not.be.visible') @@ -205,8 +205,7 @@ describe('Page', function() { it('Lists attachments for the page and allows restore', function() { cy.openPage('Day 1') - // Switch to edit mode - cy.switchPageMode(1) + cy.switchToEditMode() // Open attachment list cy.get('button.action-item .icon-menu-sidebar').click() diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 6f72da7d5..128e8a118 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -18,6 +18,7 @@ import axios from '@nextcloud/axios' const url = Cypress.config('baseUrl').replace(/\/index.php\/?$/g, '') Cypress.env('baseUrl', url) +const silent = { log: false } /** * Ignore ResizeObserver loop limit exceeded' exceptions from browser @@ -62,89 +63,122 @@ Cypress.Commands.add('getReadOnlyEditor', () => { /** * Switch page mode to view or edit */ -Cypress.Commands.add('switchPageMode', (pageMode) => { - if (pageMode === 0) { - cy.log('Switch to view mode') - cy.get('button.titleform-button') - .should('contain', 'Done') - .click() - cy.getReadOnlyEditor() - .should('be.visible') - } else if (pageMode === 1) { - cy.log('Switch to edit mode') - cy.get('button.titleform-button') - .should('contain', 'Edit') - .click() - cy.getEditor() - .should('be.visible') - } else { - throw new Error(`Unknown page mode: ${pageMode}`) - } +Cypress.Commands.add('switchToViewMode', () => { + Cypress.log() + cy.get('button.titleform-button') + .should('contain', 'Done') + .click() + cy.getReadOnlyEditor() + .should('be.visible') +}) + +Cypress.Commands.add('switchToEditMode', () => { + Cypress.log() + cy.get('button.titleform-button') + .should('contain', 'Edit') + .click() + cy.getEditor() + .should('be.visible') }) /** * Enable/disable a Nextcloud app */ -Cypress.Commands.add('enableApp', appName => cy.setAppEnabled(appName)) -Cypress.Commands.add('disableApp', appName => cy.setAppEnabled(appName, false)) +Cypress.Commands.add('enableApp', appName => { + Cypress.log() + cy.setAppEnabled(appName) +}) + +Cypress.Commands.add('disableApp', appName => { + Cypress.log() + cy.setAppEnabled(appName, false) +}) + Cypress.Commands.add('setAppEnabled', (appName, value = true) => { - cy.request('/csrftoken').then(({ body }) => { - const requesttoken = body.token - const verb = value ? 'enable' : 'disable' - const api = `${Cypress.env('baseUrl')}/index.php/settings/apps/${verb}` - return axios.post(api, - { appIds: [appName] }, - { headers: { requesttoken } }, - ) - }) + const verb = value ? 'enable' : 'disable' + const api = `${Cypress.env('baseUrl')}/index.php/settings/apps/${verb}` + return axios.post(api, + { appIds: [appName] }, + ) }) /** * Enable dashboard widget */ Cypress.Commands.add('enableDashboardWidget', (widgetName) => { - cy.request('/csrftoken').then(({ body }) => { - const requesttoken = body.token - const api = `${Cypress.env('baseUrl')}/index.php/apps/dashboard/layout` - return axios.post(api, - { layout: widgetName }, - { headers: { requesttoken } }, - ) - }) + Cypress.log() + const api = `${Cypress.env('baseUrl')}/index.php/apps/dashboard/layout` + return axios.post(api, + { layout: widgetName }, + ) }) -/** - * First delete, then seed a collective (to start fresh) - */ -Cypress.Commands.add('deleteAndSeedCollective', (name) => { - cy.deleteCollective(name) - cy.seedCollective(name) +Cypress.Commands.add('store', (selector, options = {}) => { + Cypress.log() + if (selector) { + cy.window(silent) + .its(`app.$store.${selector}`, silent) + } else { + cy.window(silent) + .its('app.$store', silent) + } }) -/** - * Create a collective if it doesn't exist +Cypress.Commands.add('routeTo', (path) => { + Cypress.log() + cy.window(silent) + .its('app.$router', silent) + .invoke(silent, 'push', path) +}) + +/* + * Dispatch action + * + * When used as a child command expects an object to be yielded + * and merges it into the payload: + * `cy.wrap({ id: 123 }).dispatch(SOME_ACTION, { value: 'Hello' })` + * will dispatch `SOME_ACTION` with a payload of `{ id: 123, value: 'Hello'}`. + * + * If null is yielded the action won't be dispatched. + * This is useful for cleanup commands like `deleteCollective`. */ -Cypress.Commands.add('seedCollective', (name) => { - cy.log(`Seeding collective ${name}`) - cy.window() - .its('app') - .then(async app => { - await app.$store.dispatch(NEW_COLLECTIVE, { name }) - .catch(e => { - if (e.request && e.request.status === 422) { - // The collective already existed... carry on. - } else { - throw e - } - }) - const updatedCollectivePath = app.$store.getters.updatedCollectivePath - if (updatedCollectivePath) { - app.$router.push(updatedCollectivePath) - } else { - // Fallback - if collective exists, updatedCollectivePath is undefined - app.$router.push(`/${name}`) +Cypress.Commands.add('dispatch', + { prevSubject: 'optional' }, + (subject, action, payload) => { + // used as a child command but null was yielded + if (subject === null) { + return + } + Cypress.log() + cy.store() + .invoke(silent, 'dispatch', action, { ...payload, ...subject }) + }) + +Cypress.Commands.add('findBy', + { prevSubject: true }, + (subject, properties) => { + Cypress.log() + return subject.find(item => { + for (const key in properties) { + if (item[key] !== properties[key]) { + return false + } } - }) + return true + }) || null + }) + +/** + * Create a fresh collective for use in the test + * + * If the collective already existed it will be deleted first. + */ +Cypress.Commands.add('deleteAndSeedCollective', (name) => { + Cypress.log() + cy.deleteCollective(name) + cy.dispatch(NEW_COLLECTIVE, { name }) + cy.store('getters.updatedCollectivePath') + .then(path => cy.routeTo(path)) // Make sure new collective is loaded cy.get('#titleform input').should('have.value', name) }) @@ -153,7 +187,7 @@ Cypress.Commands.add('seedCollective', (name) => { * Create a collective via UI */ Cypress.Commands.add('createCollective', (name, members = []) => { - cy.log(`Creating collective ${name}`) + Cypress.log() cy.get('button').contains('New collective').click() cy.get('.collective-name input[type="text"]').type(`${name}{enter}`) if (members.length > 0) { @@ -168,103 +202,76 @@ Cypress.Commands.add('createCollective', (name, members = []) => { }) /** - * Delete a collective if it exists + * Delete a collective if exists and clean it from the trash. */ Cypress.Commands.add('deleteCollective', (name) => { - cy.window() - .its('app') - .then(async app => { - await app.$store.dispatch(GET_COLLECTIVES) - const id = app.$store.state.collectives.collectives.find(c => c.name === name)?.id - if (id) { - cy.log(`Deleting collective ${name} (id ${id})`) - await app.$store.dispatch(TRASH_COLLECTIVE, { id }) - return await app.$store.dispatch(DELETE_COLLECTIVE, { id, circle: true }) - } - - // Try to find and delete collective from trash - await app.$store.dispatch(GET_TRASH_COLLECTIVES) - const trashId = app.$store.state.collectives.trashCollectives.find(c => c.name === name)?.id - if (trashId) { - cy.log(`Deleting trashed collective ${name} (id ${trashId})`) - return await app.$store.dispatch(DELETE_COLLECTIVE, { id: trashId, circle: true }) - } - }) + cy.dispatch(GET_COLLECTIVES) + cy.store('state.collectives.collectives') + .findBy({ name }) + .dispatch(TRASH_COLLECTIVE) + // Try to find and delete collective from trash + cy.dispatch(GET_TRASH_COLLECTIVES) + cy.store('state.collectives.trashCollectives') + .findBy({ name }) + .dispatch(DELETE_COLLECTIVE, { circle: true }) }) /** * Change permission settings for a collective */ Cypress.Commands.add('seedCollectivePermissions', (name, type, level) => { - cy.log(`Seeding collective permissions for ${name}`) - cy.window() - .its('app') - .then(async app => { - const id = app.$store.state.collectives.collectives.find(c => c.name === name).id - if (type === 'edit') { - await app.$store.dispatch(UPDATE_COLLECTIVE_EDIT_PERMISSIONS, { id, level }) - } else if (type === 'share') { - await app.$store.dispatch(UPDATE_COLLECTIVE_SHARE_PERMISSIONS, { id, level }) - } - }) + Cypress.log() + const action = (type === 'edit') + ? UPDATE_COLLECTIVE_EDIT_PERMISSIONS + : UPDATE_COLLECTIVE_SHARE_PERMISSIONS + cy.store('state.collectives.collectives') + .findBy({ name }) + .dispatch(action, { level }) }) /** * Change default page mode for a collective */ Cypress.Commands.add('seedCollectivePageMode', (name, mode) => { - cy.log(`Seeding collective page mode for ${name}`) - cy.window() - .its('app') - .then(async app => { - const id = app.$store.state.collectives.collectives.find(c => c.name === name).id - await app.$store.dispatch(UPDATE_COLLECTIVE_PAGE_MODE, { id, mode }) - }) + Cypress.log() + cy.store('state.collectives.collectives') + .findBy({ name }) + .dispatch(UPDATE_COLLECTIVE_PAGE_MODE, { mode }) }) /** * Add a page to a collective */ Cypress.Commands.add('seedPage', (name, parentFilePath, parentFileName) => { - cy.log(`Seeding collective page ${name}`) - cy.window() - .its('app') - .then(async app => { - await app.$store.dispatch(GET_PAGES) - const parentPage = app.$store.state.pages.pages.find(function(p) { - return p.filePath === parentFilePath - && p.fileName === parentFileName - }) - const parentId = parentPage.id - await app.$store.dispatch(NEW_PAGE, { title: name, pagePath: name, parentId }) - // Return pageId of created page - return app.$store.state.pages.pages.find(function(p) { - return p.parentId === parentId - && p.title === name - }).id - }) + Cypress.log() + cy.dispatch(GET_PAGES) + cy.store('state.pages.pages') + .findBy({ filePath: parentFilePath, fileName: parentFileName }) + .its('id') + .as('parentId') + .then(id => ({ parentId: id })) + .dispatch(NEW_PAGE, { title: name, pagePath: name }) + // Return pageId of created page + cy.get('@parentId').then(parentId => { + return cy.store('state.pages.pages') + .findBy({ parentId, title: name }) + .its('id') + }) }) /** * Upload a file */ Cypress.Commands.add('uploadFile', (path, mimeType, remotePath = '') => { + Cypress.log() // Get fixture return cy.fixture(path, 'base64').then(file => { // convert the base64 string to a blob const blob = Cypress.Blob.base64StringToBlob(file, mimeType) try { const file = new File([blob], path, { type: mimeType }) - return cy.window() - .its('app') - .then(async app => { - const response = await axios.put(`${Cypress.env('baseUrl')}/remote.php/webdav/${remotePath}${path}`, file, { - headers: { - requesttoken: app.OC.requestToken, - 'Content-Type': mimeType, - }, - }) - cy.log(`Uploaded file to ${remotePath}${path}`) + return cy.uploadContent(remotePath + path, file, mimeType) + .then(response => { const ocFileId = response.headers['oc-fileid'] const fileId = parseInt(ocFileId.substring(0, ocFileId.indexOf('oc'))) return fileId @@ -280,14 +287,26 @@ Cypress.Commands.add('uploadFile', (path, mimeType, remotePath = '') => { * Upload content of a page */ Cypress.Commands.add('seedPageContent', (pagePath, content) => { - cy.log(`Seeding collective page content for ${pagePath}`) + const contentForLog = content.length > 200 + ? content.substring(0, 200) + '...' + : content + Cypress.log({ message: `${pagePath}, ${contentForLog}` }) + cy.uploadContent(`Collectives/${pagePath}`, content) +}) + +/** + * Generic upload of content - used by seedPageContent and uploadPage + */ +Cypress.Commands.add('uploadContent', (path, content, mimetype = 'text/markdown') => { + // @nextcloud/axios automatic handling for request tokens does not work for webdav cy.window() - .its('app') - .then(async app => { - await axios.put(`${Cypress.env('baseUrl')}/remote.php/webdav/Collectives/${pagePath}`, content, { + .its('app.OC.requestToken') + .then(requesttoken => { + const url = `${Cypress.env('baseUrl')}/remote.php/webdav/${path}` + return axios.put(url, content, { headers: { - requesttoken: app.OC.requestToken, - 'Content-Type': 'text/markdown', + requesttoken, + 'Content-Type': mimetype, }, }) }) @@ -297,18 +316,16 @@ Cypress.Commands.add('seedPageContent', (pagePath, content) => { * Create a circle (optionally with given config) */ Cypress.Commands.add('seedCircle', (name, config = null) => { - cy.log(`Seeding circle ${name}`) - cy.window() - .its('app') - .then(async app => { - await app.$store.dispatch(GET_CIRCLES) - const circle = app.$store.state.circles.circles.find(c => c.sanitizedName === name) + Cypress.log() + cy.dispatch(GET_CIRCLES) + cy.store('state.circles.circles') + .findBy({ sanitizedName: name }) + .then(async circle => { const api = `${Cypress.env('baseUrl')}/ocs/v2.php/apps/circles/circles` let circleId if (!circle) { const response = await axios.post(api, { name, personal: false, local: true }, - { headers: { requesttoken: app.OC.requestToken } }, ) circleId = response.data.ocs.data.id } else { @@ -325,7 +342,6 @@ Cypress.Commands.add('seedCircle', (name, config = null) => { .reduce((sum, [k, v]) => sum + v, 0) await axios.put(`${api}/${circleId}/config`, { value }, - { headers: { requesttoken: app.OC.requestToken } }, ) } }) @@ -334,37 +350,37 @@ Cypress.Commands.add('seedCircle', (name, config = null) => { /** * Add someone to a circle */ -Cypress.Commands.add('seedCircleMember', (name, userId, type = 1, level) => { - cy.log(`Seeding circle member ${name} of type ${type}`) - cy.window() - .its('app') - .then(async app => { - await app.$store.dispatch(GET_CIRCLES) - const circleId = app.$store.state.circles.circles.find(c => c.sanitizedName === name).id - cy.log(`circleId: ${circleId}`) - const api = `${Cypress.env('baseUrl')}/ocs/v2.php/apps/circles/circles/${circleId}/members` - const response = await axios.post(api, - { userId, type }, - { headers: { requesttoken: app.OC.requestToken } }, - ).catch(e => { - if (e.request && e.request.status === 400) { - // The member already got added... carry on. - } else { - throw e - } - }) - if (level) { - const memberId = response.data.ocs.data.id - cy.log(memberId) - cy.log(`Setting circle ${name} member ${userId} level to ${level}`) - await axios.put(`${api}/${memberId}/level`, - { level }, - { headers: { requesttoken: app.OC.requestToken } }, - ) - } - }) +Cypress.Commands.add('circleFind', (name) => { + Cypress.log() + cy.dispatch(GET_CIRCLES) + return cy.store('state.circles.circles') + .findBy({ sanitizedName: name }) }) +Cypress.Commands.add('circleAddMember', + { prevSubject: true }, + async ({ id }, userId, type = 1) => { + Cypress.log() + const url = `${Cypress.env('baseUrl')}/ocs/v2.php/apps/circles/circles/${id}/members` + const response = await axios.post(url, + { userId, type }, + ) + const memberId = response.data.ocs.data.id + return { circleId: id, userId, memberId } + }, +) + +Cypress.Commands.add('circleSetMemberLevel', + { prevSubject: true }, + ({ circleId, memberId }, level) => { + Cypress.log() + const url = `${Cypress.env('baseUrl')}/ocs/v2.php/apps/circles/circles/${circleId}/members` + return axios.put(`${url}/${memberId}/level`, + { level }, + ) + }, +) + /** * Fail the test on the initial run to check if retries work */ diff --git a/cypress/support/navigation.js b/cypress/support/navigation.js index 6719f8ffd..a02469cba 100644 --- a/cypress/support/navigation.js +++ b/cypress/support/navigation.js @@ -1,23 +1,28 @@ Cypress.Commands.add('openApp', (appName) => { + Cypress.log() cy.get(`nav.app-menu li[data-app-id="${appName}"] a`).click() }) Cypress.Commands.add('openPage', (pageName) => { + Cypress.log() cy.contains('.app-content-list-item a', pageName).click() }) Cypress.Commands.add('openPageMenu', (pageName) => { + Cypress.log() cy.contains('.app-content-list-item', pageName) .find('.action-item__menutoggle') .click({ force: true }) }) Cypress.Commands.add('openCollective', (collectiveName) => { + Cypress.log() cy.get(`.collectives_list_item a[title="${collectiveName}"]`) .click() }) Cypress.Commands.add('openCollectiveMenu', (collectiveName) => { + Cypress.log() cy.get('.collectives_list_item') .contains('li', collectiveName) .find('.action-item__menutoggle') @@ -25,6 +30,7 @@ Cypress.Commands.add('openCollectiveMenu', (collectiveName) => { }) Cypress.Commands.add('openTrashedCollectiveMenu', (collectiveName) => { + Cypress.log() cy.get('.collectives_trash_list_item') .contains('li', collectiveName) .find('.action-item__menutoggle') @@ -32,6 +38,7 @@ Cypress.Commands.add('openTrashedCollectiveMenu', (collectiveName) => { }) Cypress.Commands.add('clickMenuButton', (title) => { + Cypress.log() cy.get('button.action-button') .contains(title) .click()