diff options
author | Robin Appelman <robin@icewind.nl> | 2020-10-01 17:29:37 +0200 |
---|---|---|
committer | Robin Appelman <robin@icewind.nl> | 2020-10-01 19:53:33 +0200 |
commit | d20d14cb42b53ea5322447da28618ec2d796ebc5 (patch) | |
tree | 3d43f257016378aeb92e24c10932d3d93f5de825 /cypress | |
parent | f77cf749c0ba5094c88e207b7b660ece8dd44cbe (diff) |
add cypress testing
Signed-off-by: Robin Appelman <robin@icewind.nl>
Diffstat (limited to 'cypress')
-rw-r--r-- | cypress/docker-compose.yml | 16 | ||||
-rwxr-xr-x | cypress/initserver.sh | 10 | ||||
-rw-r--r-- | cypress/integration/0.setup.spec.js | 26 | ||||
-rw-r--r-- | cypress/integration/post.spec.js | 93 | ||||
-rw-r--r-- | cypress/plugins/index.js | 20 | ||||
-rwxr-xr-x | cypress/start.sh | 20 | ||||
-rwxr-xr-x | cypress/stop.sh | 12 | ||||
-rw-r--r-- | cypress/support/commands.js | 161 | ||||
-rw-r--r-- | cypress/support/index.js | 20 | ||||
-rw-r--r-- | cypress/utils/index.js | 35 |
10 files changed, 413 insertions, 0 deletions
diff --git a/cypress/docker-compose.yml b/cypress/docker-compose.yml new file mode 100644 index 00000000..59712508 --- /dev/null +++ b/cypress/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3' + +services: + nextcloud: + image: nextcloudci/server + + ports: + - 8082:80 + + environment: + CYPRESS_baseUrl: "http://127.0.0.1:8082/index.php" + BRANCH: master + + volumes: + - ../:/var/www/html/apps/social + - ./initserver.sh:/initserver.sh diff --git a/cypress/initserver.sh b/cypress/initserver.sh new file mode 100755 index 00000000..4d997b58 --- /dev/null +++ b/cypress/initserver.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +echo "APP_NAME: $APP_NAME" +chown -R www-data:www-data /var/www/html/data + +su www-data -c " + php occ config:system:set force_language --value en + php occ app:enable $APP_NAME + php occ app:list +" diff --git a/cypress/integration/0.setup.spec.js b/cypress/integration/0.setup.spec.js new file mode 100644 index 00000000..4c60f8bb --- /dev/null +++ b/cypress/integration/0.setup.spec.js @@ -0,0 +1,26 @@ +describe('Social app setup', function() { + + before(function() { + cy.login('admin', 'admin') + }) + + it('See the welcome message', function() { + cy.visit('/apps/social/') + cy.get('.social__welcome').should('contain', 'Nextcloud becomes part of the federated social networks!') + cy.get('.social__welcome').find('.icon-close').click() + cy.get('.social__welcome').should('not.exist') + }) + + it('See the home section in the sidebar', function() { + cy.get('.app-navigation').contains('Home').click() + cy.get('.emptycontent').should('be.visible') + }) + + it('See the empty content illustration', function() { + cy.get('.app-navigation').contains('Direct messages').click() + cy.get('.emptycontent').should('be.visible').contains('No direct messages found') + cy.get('.app-navigation').contains('Profile').click() + cy.get('.emptycontent').should('be.visible').contains('You haven\'t tooted yet') + }) + +}) diff --git a/cypress/integration/post.spec.js b/cypress/integration/post.spec.js new file mode 100644 index 00000000..1de7688f --- /dev/null +++ b/cypress/integration/post.spec.js @@ -0,0 +1,93 @@ +/* + * @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * 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 <http://www.gnu.org/licenses/>. + * + */ + +describe('Create posts', function() { + + before(function() { + cy.visit('/apps/social/') + cy.logout() + cy.login('admin', 'admin') + cy.nextcloudCreateUser('janedoe', 'p4ssw0rd') + cy.logout() + cy.login('janedoe', 'p4ssw0rd', '/apps/social/') + cy.get('.app-content').should('be.visible') + }) + + afterEach(function() { + cy.screenshot() + }) + + it('Write a post to followers', function() { + cy.server() + cy.route('POST', '/index.php/apps/social/api/v1/post').as('postMessage') + cy.get('.new-post input[type=submit]') + .should('be.disabled') + cy.get('.new-post').find('[contenteditable]').type('Hello world') + cy.get('.new-post input[type=submit]') + .should('not.be.disabled') + cy.get('.new-post input[type=submit]') + .click() + cy.wait('@postMessage') + cy.get('.social__timeline div.timeline-entry:first-child').should('contain', 'Hello world') + }) + + it('Write a post to followers with shift enter', function() { + cy.server() + cy.route('POST', '/index.php/apps/social/api/v1/post').as('postMessage') + cy.get('.new-post').find('[contenteditable]').type('Hello world 2{shift}{enter}') + cy.wait('@postMessage') + cy.get('.social__timeline div.timeline-entry:first-child').should('contain', 'Hello world 2') + }) + + it('Write a post to @admin', function() { + cy.server() + cy.route('POST', '/index.php/apps/social/api/v1/post').as('postMessage') + cy.get('.new-post').find('[contenteditable]').type('@adm', {delay: 500}) + cy.get('.tribute-container').should('be.visible') + cy.get('.tribute-container ul li:first').contains('admin') + cy.get('.new-post').find('[contenteditable]').type('{enter} Hello there', {delay: 100, force: true}) + cy.get('.new-post input[type=submit]') + .click() + cy.wait('@postMessage') + cy.get('.social__timeline div.timeline-entry:first-child').should('contain', 'admin@localhost') + }) + + it('Opens the menu and shows that followers is selected by default', function() { + cy.get('.new-post').find('[contenteditable]').click({force: true}).type('@adm{enter} Hello world', {delay: 500, force: true}) + cy.wait(500) + cy.get('.new-post input[type=submit]').should('not.be.disabled') + const visibilityButton = cy.get('.new-post .options > div > button') + visibilityButton.should('have.class', 'icon-contacts-dark') + + visibilityButton.click() + cy.get('.new-post-form .popovermenu').should('be.visible') + cy.get('.new-post-form .popovermenu .active').contains('Followers') + visibilityButton.click() + cy.get('.new-post-form .popovermenu').should('not.be.visible') + + cy.get('.new-post input[type=submit]') + .click() + cy.get('.social__timeline div.timeline-entry:first-child').should('contain', 'Hello there').should('contain', 'admin@localhost') + + }) + +}) diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 00000000..e1d51dd1 --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +const { + addMatchImageSnapshotPlugin +} = require('cypress-image-snapshot/plugin') + +module.exports = (on, config) => { + addMatchImageSnapshotPlugin(on, config) +} diff --git a/cypress/start.sh b/cypress/start.sh new file mode 100755 index 00000000..b55fef2b --- /dev/null +++ b/cypress/start.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# RUN THIS SCRIPT FROM THE ROOT FOLDER OF YOUR APP +APP_NAME=${PWD##*/} + +if [[ $APP_NAME == "cypress" ]] +then + echo "Please run this app from your app root folder." +else + echo "Launching docker server for the $APP_NAME app" + cd cypress + docker-compose up -d + echo -n "Waiting for server start " + until [[ $(docker-compose exec -u www-data -T nextcloud php ./occ status --output=json) == *"\"installed\":true"* ]] + do + echo -n "." + done + echo "" + docker-compose exec --env APP_NAME=$APP_NAME -T nextcloud bash /initserver.sh + docker-compose exec -u www-data -T nextcloud php ./occ social:reset -n +fi diff --git a/cypress/stop.sh b/cypress/stop.sh new file mode 100755 index 00000000..aea0338a --- /dev/null +++ b/cypress/stop.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# RUN THIS SCRIPT FROM THE ROOT FOLDER OF YOUR APP +appname=${PWD##*/} + +if [[ $appname == "cypress" ]] +then + echo "Please run this app from your app root folder." +else + echo "Killing server for the $appname app" + cd cypress + docker-compose down +fi diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 00000000..283f3a19 --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,161 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @license GNU AGPL version 3 or any later version + * + * 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 <http://www.gnu.org/licenses/>. + * + */ + +import { addMatchImageSnapshotCommand } from 'cypress-image-snapshot/command' +import axios from '@nextcloud/axios' + +addMatchImageSnapshotCommand() + +const url = Cypress.config('baseUrl').replace(/\/index.php\/?$/g, '') +Cypress.env('baseUrl', url) + +Cypress.Commands.add('login', (user, password, route = '/apps/files') => { + cy.clearCookies() + Cypress.Cookies.defaults({ + preserve: /^(oc|nc)/, + }) + cy.visit(route) + cy.get('input[name=user]').type(user) + cy.get('input[name=password]').type(password) + cy.get('#submit-wrapper input[type=submit]').click() + cy.url().should('include', route) +}) + +Cypress.Commands.add('logout', () => { + if (Cypress.$("input[name=user]").length > 0) { + // already logged out + } else { + cy.get('#expanddiv li[data-id="logout"] a').then(logout => { + if (logout) { + cy.visit(logout[0].href) + } + }) + } +}) + +Cypress.Commands.add('nextcloudCreateUser', (user, password) => { + cy.clearCookies() + cy.request({ + method: 'POST', + url: `${Cypress.env('baseUrl')}/ocs/v1.php/cloud/users?format=json`, + form: true, + body: { + userid: user, + password, + }, + auth: { user: 'admin', pass: 'admin' }, + headers: { + 'OCS-ApiRequest': 'true', + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${btoa('admin:admin')}`, + }, + }).then(response => { + cy.log(`Created user ${user}`, response.status) + }) +}) + +Cypress.Commands.add('uploadFile', (fileName, mimeType, path = '') => { + // get fixture + return cy.fixture(fileName, 'base64').then(file => { + // convert the logo base64 string to a blob + const blob = Cypress.Blob.base64StringToBlob(file, mimeType) + try { + const file = new File([blob], fileName, { type: mimeType }) + return cy.window().then(async window => { + await axios.put(`${Cypress.env('baseUrl')}/remote.php/webdav${path}/${fileName}`, file, { + headers: { + requesttoken: window.OC.requestToken, + 'Content-Type': mimeType, + } + }).then(response => { + cy.log(`Uploaded ${fileName}`, response) + }) + }) + } catch (error) { + cy.log(error) + throw new Error(`Unable to process file ${fileName}`) + } + }) + +}) + +Cypress.Commands.add('createFolder', dirName => { + cy.get('#controls .actions > .button.new').click() + cy.get('#controls .actions .newFileMenu a[data-action="folder"]').click() + cy.get('#controls .actions .newFileMenu a[data-action="folder"] input[type="text"]').type(dirName) + cy.get('#controls .actions .newFileMenu a[data-action="folder"] input.icon-confirm').click() + cy.log('Created folder', dirName) +}) + +Cypress.Commands.add('openFile', fileName => { + cy.get(`#fileList tr[data-file="${fileName}"] a.name`).click() + cy.wait(250) +}) + +Cypress.Commands.add('getFileId', fileName => { + return cy.get(`#fileList tr[data-file="${fileName}"]`) + .should('have.attr', 'data-id') +}) + +Cypress.Commands.add('deleteFile', fileName => { + cy.get(`#fileList tr[data-file="${fileName}"] a.name .action-menu`).click() + cy.get(`#fileList tr[data-file="${fileName}"] a.name + .popovermenu .action-delete`).click() +}) + +/** + * Create a share link and return the share url + * + * @param {string} path the file/folder path + * @returns {string} the share link url + */ +Cypress.Commands.add('createLinkShare', path => { + return cy.window().then(async window => { + try { + const request = await axios.post(`${Cypress.env('baseUrl')}/ocs/v2.php/apps/files_sharing/api/v1/shares`, { + path, + shareType: window.OC.Share.SHARE_TYPE_LINK, + }, { + headers: { + requesttoken: window.OC.requestToken, + } + }) + if (!('ocs' in request.data) || !('token' in request.data.ocs.data && request.data.ocs.data.token.length > 0)) { + throw request + } + cy.log('Share link created', request.data.ocs.data.token) + return cy.wrap(request.data.ocs.data.token) + } catch(error) { + console.error(error) + } + }).should('have.length', 15) +}) + +Cypress.Commands.overwrite('matchImageSnapshot', (originalFn, subject, name, options) => { + // hide avatar because random colour break the visual regression tests + cy.window().then(window => { + const avatarDiv = window.document.querySelector('.avatardiv') + if (avatarDiv) { + avatarDiv.remove() + } + }) + return originalFn(subject, name, options) +}) diff --git a/cypress/support/index.js b/cypress/support/index.js new file mode 100644 index 00000000..d68db96d --- /dev/null +++ b/cypress/support/index.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/cypress/utils/index.js b/cypress/utils/index.js new file mode 100644 index 00000000..cee81c0f --- /dev/null +++ b/cypress/utils/index.js @@ -0,0 +1,35 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @license GNU AGPL version 3 or any later version + * + * 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 <http://www.gnu.org/licenses/>. + * + */ + +const getSearchParams = url => { + return url + .split(/[?&]/) + .reduce((acc, cur) => { + const parts = cur.split('=') + parts[1] && (acc[parts[0]] = parts[1]) + return acc + }, {}) +} + +const randHash = () => Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10) + +export default { getSearchParams, randHash } |