diff options
author | Julius Härtl <jus@bitgrid.net> | 2018-02-14 15:04:08 +0100 |
---|---|---|
committer | Julius Härtl <jus@bitgrid.net> | 2018-07-29 22:51:58 +0200 |
commit | 33f26c8da236761a41b874421ba07979ab1d5bbf (patch) | |
tree | 227aa105bbad4db9637301c72c77ca57bab28daa /tests/ui-regression | |
parent | 0c7a17795bc553cbee8e0169e2db4c41c1d6e765 (diff) | |
download | nextcloud-server-33f26c8da236761a41b874421ba07979ab1d5bbf.tar.gz nextcloud-server-33f26c8da236761a41b874421ba07979ab1d5bbf.zip |
Frontend regression testing with puppeteer
Signed-off-by: Julius Härtl <jus@bitgrid.net>
Diffstat (limited to 'tests/ui-regression')
-rw-r--r-- | tests/ui-regression/config.js | 57 | ||||
-rw-r--r-- | tests/ui-regression/helper.js | 207 | ||||
-rw-r--r-- | tests/ui-regression/out/index.html | 219 | ||||
-rw-r--r-- | tests/ui-regression/package.json | 21 | ||||
-rw-r--r-- | tests/ui-regression/runTests.js | 129 | ||||
-rw-r--r-- | tests/ui-regression/test/appsSpec.js | 60 | ||||
-rw-r--r-- | tests/ui-regression/test/filesSpec.js | 101 | ||||
-rw-r--r-- | tests/ui-regression/test/installSpec.js | 75 | ||||
-rw-r--r-- | tests/ui-regression/test/loginSpec.js | 75 | ||||
-rw-r--r-- | tests/ui-regression/test/publicSpec.js | 102 | ||||
-rw-r--r-- | tests/ui-regression/test/settingsSpec.js | 76 |
11 files changed, 1122 insertions, 0 deletions
diff --git a/tests/ui-regression/config.js b/tests/ui-regression/config.js new file mode 100644 index 00000000000..c6519ba289d --- /dev/null +++ b/tests/ui-regression/config.js @@ -0,0 +1,57 @@ +/** + * @copyright 2018 Julius Härtl <jus@bitgrid.net> + * + * @author 2018 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/>. + * + */ + +module.exports = { + + /** + * Define resolutions to be tested when diffing screenshots + */ + resolutions: [ + {title: 'mobile', w: 360, h: 480}, + {title: 'narrow', w: 800, h: 600}, + {title: 'normal', w: 1024, h: 768}, + {title: 'wide', w: 1920, h: 1080}, + {title: 'qhd', w: 2560, h: 1440}, + {title: 'uhd', w: 3840, h: 2160}, + ], + + /** + * URL that holds the base branch + */ + urlBase: 'http://ui-regression-php-master/', + + /** + * URL that holds the branch to be diffed + */ + urlChange: 'http://ui-regression-php/', + + /** + * Path to output directory for screenshot files + */ + outputDirectory: 'out', + + /** + * Run in headless mode (useful for debugging) + */ + headless: true, + +}; diff --git a/tests/ui-regression/helper.js b/tests/ui-regression/helper.js new file mode 100644 index 00000000000..7168c80585b --- /dev/null +++ b/tests/ui-regression/helper.js @@ -0,0 +1,207 @@ +/** + * @copyright 2018 Julius Härtl <jus@bitgrid.net> + * + * @author 2018 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/>. + * + */ + +const puppeteer = require('puppeteer'); +const pixelmatch = require('pixelmatch'); +const expect = require('chai').expect; +const PNG = require('pngjs2').PNG; +const fs = require('fs'); +const config = require('./config.js'); + + +module.exports = { + browser: null, + pageBase: null, + pageCompare: null, + init: async function (test) { + this._outputDirectory = `${config.outputDirectory}/${test.title}`; + if (!fs.existsSync(config.outputDirectory)) fs.mkdirSync(config.outputDirectory); + if (!fs.existsSync(this._outputDirectory)) fs.mkdirSync(this._outputDirectory); + await this.resetBrowser(); + }, + exit: async function () { + await this.browser.close(); + }, + resetBrowser: async function () { + if (this.browser) { + await this.browser.close(); + } + this.browser = await puppeteer.launch({ + args: ['--no-sandbox', '--disable-setuid-sandbox'], + headless: config.headless + }); + this.pageBase = await this.browser.newPage(); + this.pageCompare = await this.browser.newPage(); + this.pageBase.setDefaultNavigationTimeout(60000); + this.pageCompare.setDefaultNavigationTimeout(60000); + }, + + login: async function (test) { + test.timeout(20000); + await this.resetBrowser(); + await Promise.all([ + this.performLogin(this.pageBase, config.urlBase), + this.performLogin(this.pageCompare, config.urlChange) + ]); + }, + + performLogin: async function (page, baseUrl) { + await page.goto(baseUrl + '/index.php/login', {waitUntil: 'networkidle0'}); + await page.type('#user', 'admin'); + await page.type('#password', 'admin'); + const inputElement = await page.$('input[type=submit]'); + inputElement.click(); + return await page.waitForNavigation({waitUntil: 'networkidle0'}); + }, + + takeAndCompare: async function (test, route, action, options) { + // use Promise.all + if (options === undefined) + options = {}; + if (options.waitUntil === undefined) { + options.waitUntil = 'networkidle0'; + } + if (options.viewport) { + if (options.viewport.scale === undefined) { + options.viewport.scale = 1; + } + await Promise.all([ + this.pageBase.setViewport({ + width: options.viewport.w, + height: options.viewport.h, + deviceScaleFactor: options.viewport.scale + }), + this.pageCompare.setViewport({ + width: options.viewport.w, + height: options.viewport.h, + deviceScaleFactor: options.viewport.scale + }) + ]); + } + let fileName = test.test.title + if (route !== undefined) { + await Promise.all([ + this.pageBase.goto(`${config.urlBase}${route}`, {waitUntil: options.waitUntil}), + this.pageCompare.goto(`${config.urlChange}${route}`, {waitUntil: options.waitUntil}) + ]); + } + var failed = null; + try { + await Promise.all([ + action(this.pageBase), + action(this.pageCompare) + ]); + } catch (err) { + failed = err; + } + await this.delay(500); + await Promise.all([ + this.pageBase.screenshot({ + path: `${this._outputDirectory}/${fileName}.base.png`, + fullPage: false, + }), + this.pageCompare.screenshot({ + path: `${this._outputDirectory}/${fileName}.change.png`, + fullPage: false + }) + ]); + + if (options.runOnly === true) { + fs.unlinkSync(`${this._outputDirectory}/${fileName}.base.png`); + fs.renameSync(`${this._outputDirectory}/${fileName}.change.png`, `${this._outputDirectory}/${fileName}.png`); + } + + return new Promise(async (resolve, reject) => { + try { + if (options.runOnly !== true) { + await this.compareScreenshots(fileName); + } + } catch (err) { + if (failed) { + console.log('Failure during takeAndCompare action callback'); + console.log(failed); + } + console.log('Failure when comparing images'); + return reject(err); + } + if (options.runOnly !== true && failed) { + console.log('Failure during takeAndCompare action callback'); + console.log(failed); + failed.failedAction = true; + return reject(failed); + } + return resolve(); + }); + }, + + compareScreenshots: function (fileName) { + let self = this; + return new Promise((resolve, reject) => { + const img1 = fs.createReadStream(`${self._outputDirectory}/${fileName}.base.png`).pipe(new PNG()).on('parsed', doneReading); + const img2 = fs.createReadStream(`${self._outputDirectory}/${fileName}.change.png`).pipe(new PNG()).on('parsed', doneReading); + + let filesRead = 0; + + function doneReading () { + // Wait until both files are read. + if (++filesRead < 2) return; + + // The files should be the same size. + expect(img1.width, 'image widths are the same').equal(img2.width); + expect(img1.height, 'image heights are the same').equal(img2.height); + + // Do the visual diff. + const diff = new PNG({width: img1.width, height: img2.height}); + const numDiffPixels = pixelmatch( + img1.data, img2.data, diff.data, img1.width, img1.height, + {threshold: 0.3}); + if (numDiffPixels > 0) { + diff.pack().pipe(fs.createWriteStream(`${self._outputDirectory}/${fileName}.diff.png`)); + } else { + fs.unlinkSync(`${self._outputDirectory}/${fileName}.base.png`); + fs.renameSync(`${self._outputDirectory}/${fileName}.change.png`, `${self._outputDirectory}/${fileName}.png`); + } + + // The files should look the same. + expect(numDiffPixels, 'number of different pixels').equal(0); + resolve(); + } + }); + }, + /** + * Helper function to wait + * to make sure that initial animations are done + */ + delay: async function (timeout) { + return new Promise((resolve) => { + setTimeout(resolve, timeout); + }); + }, + + childOfClassByText: async function (page, classname, text) { + return page.$x('//*[contains(concat(" ", normalize-space(@class), " "), " ' + classname + ' ")]//text()[normalize-space() = \'' + text + '\']/..'); + }, + + childOfIdByText: async function (page, classname, text) { + return page.$x('//*[contains(concat(" ", normalize-space(@id), " "), " ' + classname + ' ")]//text()[normalize-space() = \'' + text + '\']/..'); + } +}; diff --git a/tests/ui-regression/out/index.html b/tests/ui-regression/out/index.html new file mode 100644 index 00000000000..a94dae13445 --- /dev/null +++ b/tests/ui-regression/out/index.html @@ -0,0 +1,219 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous" /> + <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" /> + <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script> + <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> + <title>Nextcloud UI regression tests</title> + <style> + + h2 { + margin-top: 40px; + margin-bottom: 20px; + } + .error { + color: #aa0000; + } + .success { + color: #00aa00; + } + .success img { + display: none; + width: 100px; + } + .success pre { + display: none; + } + .test-result h3 span { + width: 40px; + } + .test-result { + padding: 20px; + } + img { + max-width: 33%; + padding: 10px; + background-color: #eee; + margin: 0; + } + .overview ul { + position: fixed; + max-width: inherit; + margin: 0; + padding: 0; + } + ul li { + list-style-type: none; + padding: 3px; + } + ul a:first-child { + width: 100%; + display: inline-block; + } + ul span { + width: 16px; + height: 16px; + margin: 1px; + display: inline-block; + } + span.fa-check { + color: green; + } + span.fa-times { + color: red; + } + .navbar a { + color: #fff; + } + + .fade-enter-active, .fade-leave-active { + transition: opacity .5s; + } + .fade-enter, .fade-leave-to { + opacity: 0; + } + </style> +</head> + +<body> +<div id="app"> +<nav class="navbar navbar-expand-md navbar-dark bg-dark sticky-top"> + <div class="container"> + <a class="navbar-brand" href="#">Nextcloud UI regression test</a> + <a class="nav-link" :href="config.repoUrl">{{config.repoUrl}}</a> + <a class="nav-link" :href="config.repoUrl + '/pull/' + config.pr">#{{ config.pr }}</span></a> + </div> +</nav> + +<main role="main" class="container-fluid"> + <div class="row"> + <div class="col-md-2 overview"> + <ul> + <li v-for="suite in config.tests" v-if="result[suite]"> + <a :href="'#' + suite">{{ suite }}</a> + <a v-for="test in result[suite].tests" :href="test.fullTitle | convertToAnchor" :title="test.fullTitle"> + <span class="fa fa-times" v-if="Object.keys(test.err).length > 0"></span> + <span class="fa fa-check" v-else></span> + </a> + </li> + </ul> + </div> + <div class="col-md-10" id="container"> + <div v-for="suite in config.tests" v-if="result[suite]"> + <h2 :id="suite | convertToId">{{ suite }} <span>{{ result[suite].passes.length }}/{{ result[suite].tests.length }}</span></h2> + <test-result v-for="test in result[suite].tests" :key="test.fullTitle" :suite="suite" :test="test"></test-result> + </div> + </div> + </div> +</main> +</div> + +<script type="text/x-template" id="test-result-template"> + <div class="test-result" :id="test.fullTitle | convertToId"> + <h3 :class="{ error: Object.keys(test.err).length > 0, success: Object.keys(test.err).length == 0}" + v-on:click="hidden === undefined ? hidden = false : hidden = !hidden"> + <span class="fa fa-times" v-if="Object.keys(test.err).length > 0"></span> + <span class="fa fa-check" v-else></span> + {{ test.title }} + <i v-if="test.duration">{{ test.duration }}ms</i> + </h3> + <transition name="fade"> + <div v-if="(hidden === undefined && Object.keys(test.err).length > 0) || hidden === false"> + <div v-if="Object.keys(test.err).length > 0 && !test.err.failedAction"> + <a :href="getImagePath('.base')"><img :src="getImagePath('.base')" /></a> + <a :href="getImagePath('.diff')"><img :src="getImagePath('.diff')" /></a> + <a :href="getImagePath('.change')"><img :src="getImagePath('.change')" /></a> + </div> + <div v-else> + <a :href="getImagePath('')"><img :src="getImagePath('')" /></a> + </div> + <pre>{{ jsonData }}</pre> + </div> + </transition> + </div> +</script> + +<script> + + Vue.filter('convertToId', function (id) { + return id.replace(/\W/g,'_'); + }); + + Vue.filter('convertToAnchor', function (id) { + return '#' + id.replace(/\W/g,'_'); + }); + + Vue.component('test-result', { + template: '#test-result-template', + props: ['test', 'suite'], + data: function () { + return { + hidden: undefined + } + }, + computed: { + jsonData: function() { + return JSON.stringify(this.test, null, 2) + } + }, + methods: { + getImagePath: function(type) { + return this.suite + '/' + this.test.title + type + '.png'; + } + } + }); + + var app = new Vue({ + el: '#app', + data: { + message: 'Hello Vue!', + config: {}, + result: { + login: {} + }, + }, + created: function() { + this.fetchConfig(); + }, + methods: { + fetchConfig: function() { + var request = new XMLHttpRequest(); + request.open('GET', 'config.json', true); + + request.onload = function() { + if (request.status >= 200 && request.status < 400) { + app.config = JSON.parse(request.responseText); + app.config.tests.forEach(function(item, i){ + app.fetchResults(item); + }); + } + }; + + request.onerror = function() { + }; + + request.send(); + }, + fetchResults: function(suite) { + var request = new XMLHttpRequest(); + request.open('GET', suite + '.json', true); + + request.onload = function() { + if (request.status >= 200 && request.status < 400) { + Vue.set(app.result, suite, JSON.parse(request.responseText)); + } + }; + + request.onerror = function() { + }; + + request.send(); + } + } + }); + +</script> +</body> +</html> diff --git a/tests/ui-regression/package.json b/tests/ui-regression/package.json new file mode 100644 index 00000000000..979dfed3dec --- /dev/null +++ b/tests/ui-regression/package.json @@ -0,0 +1,21 @@ +{ + "name": "ui-regression", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "mocha test/" + }, + "author": "", + "dependencies": { + "fs": "0.0.1-security", + "chai": "^4.1.2", + "mocha": "^5.0.0", + "mocha-json-report": "0.0.2", + "pixelmatch": "^4.0.2", + "png-js": "^0.1.1", + "pngjs2": "^2.0.0", + "polyserve": "^0.23.0", + "puppeteer": "^1.0.0" + } +} diff --git a/tests/ui-regression/runTests.js b/tests/ui-regression/runTests.js new file mode 100644 index 00000000000..4eb94f79347 --- /dev/null +++ b/tests/ui-regression/runTests.js @@ -0,0 +1,129 @@ +/** + * @copyright 2018 Julius Härtl <jus@bitgrid.net> + * + * @author 2018 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/>. + * + */ + +const fs = require('fs') +const Mocha = require('mocha') + +const testFolder = './test/' + + +var tests = [ + 'install', + 'login', + 'files', + 'public', + 'settings', + 'apps', +] + +var args = process.argv.slice(2); +if (args.length > 0) { + tests = args +} + +var config = { + tests: tests, + pr: process.env.DRONE_PULL_REQUEST, + repoUrl: process.env.DRONE_REPO_LINK, +}; + +console.log('=> Write test config'); +console.log(config); +fs.writeFile('out/config.json', JSON.stringify(config), 'utf8', () => {}); + +var mocha = new Mocha({ + timeout: 60000 +}); +let result = {}; + +tests.forEach(async function (test) { + mocha.addFile('./test/' + test + 'Spec.js') + result[test] = { + failures: [], + passes: [], + tests: [], + pending: [], + stats: {} + } + +}); + +// fixme fail if installation failed +// write json to file + +function clean (test) { + return { + title: test.title, + fullTitle: test.fullTitle(), + duration: test.duration, + currentRetry: test.currentRetry(), + failedAction: test.failedAction, + err: errorJSON(test.err || {}) + }; +} + +function errorJSON (err) { + var res = {}; + Object.getOwnPropertyNames(err).forEach(function (key) { + res[key] = err[key]; + }, err); + return res; +} + +mocha.run() + .on('test', function (test) { + }) + .on('suite end', function(suite) { + if (result[suite.title] === undefined) + return; + result[suite.title].stats = suite.stats; + }) + .on('test end', function (test) { + result[test.parent.title].tests.push(test); + }) + .on('pass', function (test) { + result[test.parent.title].passes.push(test); + }) + .on('fail', function (test) { + result[test.parent.title].failures.push(test); + }) + .on('pending', function (test) { + result[test.parent.title].pending.push(test); + }) + .on('end', function () { + tests.forEach(function (test) { + var json = JSON.stringify({ + stats: result[test].stats, + tests: result[test].tests.map(clean), + pending: result[test].pending.map(clean), + failures: result[test].failures.map(clean), + passes: result[test].passes.map(clean) + }, null, 2); + fs.writeFile(`out/${test}.json`, json, 'utf8', function () { + console.log(`Written test result to out/${test}.json`) + }); + }); + + var errorMessage = 'This PR introduces some UI differences, please check at {LINK}, if there are regressions based on the changes.' + fs.writeFile('out/GITHUB_COMMENT', errorMessage, 'utf8', () => {}); + }); + diff --git a/tests/ui-regression/test/appsSpec.js b/tests/ui-regression/test/appsSpec.js new file mode 100644 index 00000000000..b5e5a889e41 --- /dev/null +++ b/tests/ui-regression/test/appsSpec.js @@ -0,0 +1,60 @@ +/** + * @copyright 2018 Julius Härtl <jus@bitgrid.net> + * + * @author 2018 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/>. + * + */ + +const helper = require('../helper.js'); +const config = require('../config.js'); + +describe('apps', function () { + + before(async () => { + await helper.init(this) + await helper.login(this) + }); + after(async () => await helper.exit()); + + config.resolutions.forEach(function (resolution) { + it('apps.' + resolution.title, async function () { + return helper.takeAndCompare(this, 'index.php/settings/apps', async function (page) { + await page.waitForSelector('#apps-list .section', {timeout: 5000}); + await page.waitFor(500); + }, {viewport: resolution, waitUntil: 'networkidle2'}); + }); + + ['installed', 'enabled', 'disabled', 'app-bundles'].forEach(function(endpoint) { + it('apps.' + endpoint + '.' + resolution.title, async function () { + return helper.takeAndCompare(this, undefined, async function (page) { + try { + await page.waitForSelector('#app-navigation-toggle', { + visible: true, + timeout: 1000, + }).then((element) => element.click()) + } catch (err) {} + await helper.delay(500); + await page.click('li#app-category-' + endpoint + ' a'); + await helper.delay(500); + await page.waitForSelector('#app-content:not(.icon-loading)'); + }, {viewport: resolution}); + }); + }); + }); + +}); diff --git a/tests/ui-regression/test/filesSpec.js b/tests/ui-regression/test/filesSpec.js new file mode 100644 index 00000000000..be507390f4e --- /dev/null +++ b/tests/ui-regression/test/filesSpec.js @@ -0,0 +1,101 @@ +/** + * @copyright 2018 Julius Härtl <jus@bitgrid.net> + * + * @author 2018 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/>. + * + */ + +const puppeteer = require('puppeteer'); +const helper = require('../helper.js'); +const config = require('../config.js'); + +describe('files', function () { + + before(async () => { + await helper.init(this) + await helper.login(this) + }); + after(async () => await helper.exit()); + + config.resolutions.forEach(function (resolution) { + + it('file-sidebar-share.' + resolution.title, async function () { + return helper.takeAndCompare(this, 'index.php/apps/files', async function (page) { + let element = await page.$('[data-file="welcome.txt"] .action-share'); + await element.click('[data-file="welcome.txt"] .action-share'); + await page.waitForSelector('.shareWithField'); + await helper.delay(500); + await page.$eval('body', e => { $('.shareWithField').blur() }); + }, {viewport: resolution, waitUntil: 'networkidle2'}); + }); + it('file-popover.' + resolution.title, async function () { + return helper.takeAndCompare(this, 'index.php/apps/files', async function (page) { + await page.click('[data-file=\'welcome.txt\'] .action-menu'); + await page.waitForSelector('.fileActionsMenu'); + }, {viewport: resolution, waitUntil: 'networkidle2'}); + }); + it('file-sidebar-details.' + resolution.title, async function() { + return helper.takeAndCompare(this, undefined, async function (page) { + await page.click('[data-file=\'welcome.txt\'] .fileActionsMenu [data-action=\'Details\']'); + await page.waitForSelector('#commentsTabView'); + await helper.delay(500); // wait for animation + }); + }); + it('file-sidebar-details-sharing.' + resolution.title, async function() { + return helper.takeAndCompare(this, undefined, async function (page) { + let tab = await helper.childOfClassByText(page, 'tabHeaders', 'Sharing'); + tab[0].click(); + await page.waitForSelector('input.shareWithField'); + await helper.delay(500); // wait for animation + await page.$eval('body', e => { $('.shareWithField').blur() }); + }); + }); + it('file-sidebar-details-versions.' + resolution.title, async function() { + return helper.takeAndCompare(this, undefined, async function (page) { + let tab = await helper.childOfClassByText(page, 'tabHeaders', 'Versions'); + tab[0].click(); + await helper.delay(100); // wait for animation + }); + }); + it('file-popover.favorite.' + resolution.title, async function () { + return helper.takeAndCompare(this, 'index.php/apps/files', async function (page) { + await page.click('[data-file=\'welcome.txt\'] .action-menu'); + await page.waitForSelector('.fileActionsMenu') + await page.click('[data-file=\'welcome.txt\'] .fileActionsMenu [data-action=\'Favorite\']');; + }, {viewport: resolution, waitUntil: 'networkidle2'}); + }); + + it('file-favorites.' + resolution.title, async function () { + return helper.takeAndCompare(this, 'index.php/apps/files', async function (page) { + try { + await page.waitForSelector('#app-navigation-toggle', { + visible: true, + timeout: 1000, + }).then((element) => element.click()) + } catch (err) {} + await page.click('#app-navigation [data-id=\'favorites\'] a'); + await helper.delay(500); // wait for animation + }, {viewport: resolution, waitUntil: 'networkidle2'}); + }); + + + }); + + + +}); diff --git a/tests/ui-regression/test/installSpec.js b/tests/ui-regression/test/installSpec.js new file mode 100644 index 00000000000..02577883aae --- /dev/null +++ b/tests/ui-regression/test/installSpec.js @@ -0,0 +1,75 @@ +/** + * @copyright 2018 Julius Härtl <jus@bitgrid.net> + * + * @author 2018 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/>. + * + */ + +const helper = require('../helper.js'); +const config = require('../config.js'); + +describe('install', function () { + + before(async () => await helper.init(this)); + after(async () => await helper.exit()); + + config.resolutions.forEach(function (resolution) { + it('show-page.' + resolution.title, async function () { + // (test, route, prepare, action, options + return helper.takeAndCompare(this, '/index.php', async (page) => { + await helper.delay(100); + await page.$eval('body', function (e) { + $('#adminlogin').blur(); + }); + await helper.delay(100); + }, { waitUntil: 'networkidle0', viewport: resolution}); + }); + + it('show-advanced.' + resolution.title, async function () { + // (test, route, prepare, action, options + return helper.takeAndCompare(this, undefined, async (page) => { + await page.click('#showAdvanced'); + await helper.delay(500); + }); + }); + it('show-advanced-mysql.' + resolution.title, async function () { + // (test, route, prepare, action, options + return helper.takeAndCompare(this, undefined, async (page) => { + await page.click('label.mysql'); + await helper.delay(500); + }); + }); + }); + + it('runs', async function () { + this.timeout(5*60*1000); + helper.pageBase.setDefaultNavigationTimeout(5*60*1000); + helper.pageCompare.setDefaultNavigationTimeout(5*60*1000); + // just run for one resolution since we can only install once + return helper.takeAndCompare(this, '/index.php', async function (page) { + const login = await page.type('#adminlogin', 'admin'); + const password = await page.type('#adminpass', 'admin'); + const inputElement = await page.$('input[type=submit]'); + await inputElement.click(); + await page.waitForNavigation({waitUntil: 'networkidle0'}); + helper.pageBase.setDefaultNavigationTimeout(60000); + helper.pageCompare.setDefaultNavigationTimeout(60000); + }, { waitUntil: 'networkidle0', viewport: {w: 1920, h: 1080}}); + }); + +}); diff --git a/tests/ui-regression/test/loginSpec.js b/tests/ui-regression/test/loginSpec.js new file mode 100644 index 00000000000..23f86737a76 --- /dev/null +++ b/tests/ui-regression/test/loginSpec.js @@ -0,0 +1,75 @@ +/** + * @copyright 2018 Julius Härtl <jus@bitgrid.net> + * + * @author 2018 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/>. + * + */ + +const helper = require('../helper.js'); +const config = require('../config.js'); + +describe('login', function () { + + before(async () => await helper.init(this)); + after(async () => await helper.exit()); + + /** + * Test login page rendering + */ + config.resolutions.forEach(function (resolution) { + it('login-page.' + resolution.title, async function () { + return helper.takeAndCompare(this, '/', async (page) => { + // make sure the cursor is not blinking in the login field + await page.$eval('body', function (e) { + $('#user').blur(); + }); + return await helper.delay(100); + }, {viewport: resolution}); + }); + + it('login-page.forgot.' + resolution.title, async function () { + return helper.takeAndCompare(this, undefined, async (page) => { + const lostPassword = await page.$('#lost-password'); + await lostPassword.click(); + await helper.delay(500); + await page.$eval('body', function (e) { + $('#user').blur(); + }); + }, {viewport: resolution}); + }); + }); + + /** + * Perform login + */ + config.resolutions.forEach(function (resolution) { + it('login-success.' + resolution.title, async function () { + this.timeout(30000); + await helper.resetBrowser(); + return helper.takeAndCompare(this, '/', async function (page) { + await page.type('#user', 'admin'); + await page.type('#password', 'admin'); + const inputElement = await page.$('input[type=submit]'); + await inputElement.click(); + await page.waitForNavigation({waitUntil: 'networkidle0'}); + return await helper.delay(100); + }, {viewport: resolution}); + }) + }); + +}); diff --git a/tests/ui-regression/test/publicSpec.js b/tests/ui-regression/test/publicSpec.js new file mode 100644 index 00000000000..843f8f50cef --- /dev/null +++ b/tests/ui-regression/test/publicSpec.js @@ -0,0 +1,102 @@ +/** + * @copyright 2018 Julius Härtl <jus@bitgrid.net> + * + * @author 2018 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/>. + * + */ + +const puppeteer = require('puppeteer'); +const helper = require('../helper.js'); +const config = require('../config.js'); + +describe('public', function () { + + before(async () => { + await helper.init(this) + await helper.login(this) + }); + after(async () => await helper.exit()); + + /** + * Test invalid file share rendering + */ + config.resolutions.forEach(function (resolution) { + it('file-share-invalid.' + resolution.title, async function () { + return helper.takeAndCompare(this, '/index.php/s/invalid', async function () { + }, {waitUntil: 'networkidle2', viewport: resolution}); + }); + }); + + /** + * Share a file via public link + */ + + var shareLink = {}; + it('file-share-link', async function () { + return helper.takeAndCompare(this, '/index.php/apps/files', async function (page) { + const element = await page.$('[data-file="welcome.txt"] .action-share'); + await element.click('[data-file="welcome.txt"] .action-share'); + await page.waitForSelector('input.linkCheckbox'); + const linkCheckbox = await page.$('.linkShareView label'); + await Promise.all([ + linkCheckbox.click(), + page.waitForSelector('.linkText') + ]); + await helper.delay(500); + const text = await page.waitForSelector('.linkText'); + const link = await (await text.getProperty('value')).jsonValue(); + shareLink[page.url()] = link; + return await helper.delay(500); + }, { + runOnly: true, + waitUntil: 'networkidle2', + viewport: {w: 1920, h: 1080} + }); + }); + + config.resolutions.forEach(function (resolution) { + it('file-share-valid.' + resolution.title, async function () { + return helper.takeAndCompare(this, '/index.php/apps/files', async function (page) { + await page.goto(shareLink[page.url()]); + await helper.delay(500); + }, {waitUntil: 'networkidle2', viewport: resolution}); + }); + it('file-share-valid-actions.' + resolution.title, async function () { + return helper.takeAndCompare(this, undefined, async function (page) { + const moreButton = await page.waitForSelector('#header-secondary-action'); + await moreButton.click(); + await page.evaluate((data) => { + return document.querySelector('#directLink').value = 'http://nextcloud.example.com/'; + }); + await helper.delay(500); + }, {waitUntil: 'networkidle2', viewport: resolution}); + }); + }); + + it('file-unshare', async function () { + return helper.takeAndCompare(this, '/index.php/apps/files', async function (page) { + const element = await page.$('[data-file="welcome.txt"] .action-share'); + await element.click('[data-file="welcome.txt"] .action-share'); + await page.waitForSelector('input.linkCheckbox'); + const linkCheckbox = await page.$('.linkShareView label'); + await linkCheckbox.click(); + await helper.delay(500); + }, { waitUntil: 'networkidle2', viewport: {w: 1920, h:1080}}); + }); + +}); diff --git a/tests/ui-regression/test/settingsSpec.js b/tests/ui-regression/test/settingsSpec.js new file mode 100644 index 00000000000..560218c80f8 --- /dev/null +++ b/tests/ui-regression/test/settingsSpec.js @@ -0,0 +1,76 @@ +/** + * @copyright 2018 Julius Härtl <jus@bitgrid.net> + * + * @author 2018 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/>. + * + */ + +const helper = require('../helper.js'); +const config = require('../config.js'); + +describe('settings', function () { + + before(async () => { + await helper.init(this) + await helper.login(this) + }); + after(async () => await helper.exit()); + + config.resolutions.forEach(function (resolution) { + it('personal.' + resolution.title, async function () { + return helper.takeAndCompare(this, '/index.php/settings/user', async function (page) { + }, {viewport: resolution}); + }); + + it('admin.' + resolution.title, async function () { + return helper.takeAndCompare(this, '/index.php/settings/admin', async function (page) { + }, {viewport: resolution}); + }); + + ['sharing', 'security', 'theming', 'encryption', 'additional', 'tips-tricks'].forEach(function(endpoint) { + it('admin.' + endpoint + '.' + resolution.title, async function () { + return helper.takeAndCompare(this, '/index.php/settings/admin/' + endpoint, async function (page) { + }, {viewport: resolution, waitUntil: 'networkidle2'}); + }); + }); + + it('usermanagement.' + resolution.title, async function () { + return helper.takeAndCompare(this, '/index.php/settings/users', async function (page) { + }, {viewport: resolution}); + }); + + it('usermanagement.add.' + resolution.title, async function () { + return helper.takeAndCompare(this, undefined, async function (page) { + try { + await page.waitForSelector('#app-navigation-toggle', { + visible: true, + timeout: 1000, + }).then((element) => element.click()) + } catch (err) {} + let newUserButton = await page.waitForSelector('#new-user-button'); + await newUserButton.click(); + await helper.delay(200); + await page.$eval('body', function (e) { + $('#newusername').blur(); + }) + await helper.delay(100); + }, {viewport: resolution}); + }); + + }); +}); |