Merge pull request #8357 from nextcloud/ui-regression
Frontend testing using puppeteer
This commit is contained in:
commit
75589badf3
48
.drone.yml
48
.drone.yml
|
@ -703,6 +703,32 @@ pipeline:
|
||||||
when:
|
when:
|
||||||
matrix:
|
matrix:
|
||||||
TEST: memcache-redis-cluster
|
TEST: memcache-redis-cluster
|
||||||
|
ui-regression:
|
||||||
|
image: nextcloudci/ui-regression:ui-regression-1
|
||||||
|
commands:
|
||||||
|
- cd tests/ui-regression
|
||||||
|
- npm install
|
||||||
|
- chown -R pptruser out node_modules
|
||||||
|
- bash -c "until curl -s http://ui-regression-php-master > /dev/null && curl -s http://ui-regression-php > /dev/null; do sleep 2; done"
|
||||||
|
- sudo -u pptruser node runTests.js || true
|
||||||
|
- echo "The result can be found at https://s3.bitgrid.net/nextcloud-ui-regression/nextcloud/server/${DRONE_PULL_REQUEST}/index.html"
|
||||||
|
shm_size: '1gb'
|
||||||
|
when:
|
||||||
|
matrix:
|
||||||
|
TESTS: ui-regression
|
||||||
|
publish-s3:
|
||||||
|
image: plugins/s3
|
||||||
|
endpoint: https://ci-assets.nextcloud.com
|
||||||
|
bucket: nextcloud-ui-regression
|
||||||
|
path_style: true
|
||||||
|
source: tests/ui-regression/out/**/*
|
||||||
|
strip_prefix: tests/ui-regression/out/
|
||||||
|
acl: public-read
|
||||||
|
target: ${DRONE_REPO_OWNER}/${DRONE_REPO_NAME}/${DRONE_PULL_REQUEST}
|
||||||
|
secrets: [ aws_access_key_id, aws_secret_access_key ]
|
||||||
|
when:
|
||||||
|
matrix:
|
||||||
|
TESTS: ui-regression
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- TESTS: checkers
|
- TESTS: checkers
|
||||||
|
@ -848,6 +874,7 @@ matrix:
|
||||||
ENABLE_REDIS_CLUSTER: true
|
ENABLE_REDIS_CLUSTER: true
|
||||||
- TESTS: sqlite-php7.0-webdav-apache
|
- TESTS: sqlite-php7.0-webdav-apache
|
||||||
ENABLE_REDIS: true
|
ENABLE_REDIS: true
|
||||||
|
- TESTS: ui-regression
|
||||||
|
|
||||||
services:
|
services:
|
||||||
cache:
|
cache:
|
||||||
|
@ -855,6 +882,27 @@ services:
|
||||||
when:
|
when:
|
||||||
matrix:
|
matrix:
|
||||||
ENABLE_REDIS: true
|
ENABLE_REDIS: true
|
||||||
|
ui-regression-php:
|
||||||
|
image: nextcloudci/server:server-1
|
||||||
|
commands:
|
||||||
|
- . /etc/apache2/envvars
|
||||||
|
- rm -fr /var/www/html
|
||||||
|
- mkdir /var/www/html && cp -rT . /var/www/html && chown -R www-data:www-data /var/www/html
|
||||||
|
- rm -fr /var/www/html/config/config.php /var/www/html/data/*
|
||||||
|
- apache2 -DFOREGROUND
|
||||||
|
when:
|
||||||
|
matrix:
|
||||||
|
TESTS: ui-regression
|
||||||
|
ui-regression-php-master:
|
||||||
|
image: nextcloudci/server:server-1
|
||||||
|
commands:
|
||||||
|
- . /etc/apache2/envvars
|
||||||
|
- rm -fr /var/www/html/config/config.php /var/www/html/data/*
|
||||||
|
- su www-data -c "cd /var/www/html/ && git checkout $DRONE_REPO_BRANCH && git pull && git submodule update"
|
||||||
|
- apache2 -DFOREGROUND
|
||||||
|
when:
|
||||||
|
matrix:
|
||||||
|
TESTS: ui-regression
|
||||||
cache-cluster:
|
cache-cluster:
|
||||||
image: morrisjobke/redis-cluster
|
image: morrisjobke/redis-cluster
|
||||||
when:
|
when:
|
||||||
|
|
|
@ -139,6 +139,9 @@ Vagrantfile
|
||||||
/tests/autotest*
|
/tests/autotest*
|
||||||
/tests/data/lorem-copy.txt
|
/tests/data/lorem-copy.txt
|
||||||
/tests/data/testimage-copy.png
|
/tests/data/testimage-copy.png
|
||||||
|
/tests/ui-regression/out/
|
||||||
|
/tests/ui-regression/node_modules/
|
||||||
|
/tests/ui-regression/package-lock.json
|
||||||
/config/config-autotest-backup.php
|
/config/config-autotest-backup.php
|
||||||
/config/autoconfig.php
|
/config/autoconfig.php
|
||||||
clover.xml
|
clover.xml
|
||||||
|
|
|
@ -454,13 +454,14 @@ form #selectDbType {
|
||||||
text-align:center;
|
text-align:center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
form #selectDbType .info {
|
form #selectDbType .info {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
form #selectDbType label {
|
form #selectDbType label {
|
||||||
position: static;
|
flex-grow: 1;
|
||||||
margin: 0 -3px 5px;
|
margin: 0 -1px 5px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
background:#f8f8f8;
|
background:#f8f8f8;
|
||||||
color:#888;
|
color:#888;
|
||||||
|
@ -469,7 +470,7 @@ form #selectDbType label {
|
||||||
}
|
}
|
||||||
form #selectDbType label span {
|
form #selectDbType label span {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 10px 20px;
|
padding: 10px 17px;
|
||||||
}
|
}
|
||||||
form #selectDbType label.ui-state-hover,
|
form #selectDbType label.ui-state-hover,
|
||||||
form #selectDbType label.ui-state-active {
|
form #selectDbType label.ui-state-active {
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
/**
|
||||||
|
* @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},
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
|
||||||
|
slowMo: 0,
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,256 @@
|
||||||
|
/**
|
||||||
|
* @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,
|
||||||
|
lastBase: 0,
|
||||||
|
lastCompare: 0,
|
||||||
|
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,
|
||||||
|
slowMo: config.slowMo,
|
||||||
|
});
|
||||||
|
this.pageBase = await this.browser.newPage();
|
||||||
|
this.pageCompare = await this.browser.newPage();
|
||||||
|
this.pageBase.setDefaultNavigationTimeout(60000);
|
||||||
|
this.pageCompare.setDefaultNavigationTimeout(60000);
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
this.pageCompare.on('requestfinished', function() {
|
||||||
|
self.lastCompare = Date.now();
|
||||||
|
});
|
||||||
|
this.pageBase.on('requestfinished', function() {
|
||||||
|
self.lastBase = Date.now();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
awaitNetworkIdle: async function (seconds) {
|
||||||
|
var self = this;
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
const timeout = setTimeout(function() {
|
||||||
|
reject();
|
||||||
|
}, 10000)
|
||||||
|
const waitForFoo = function() {
|
||||||
|
const currentTime = Date.now() - seconds*1000;
|
||||||
|
if (self.lastBase < currentTime && self.lastCompare < currentTime) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
setTimeout(waitForFoo, 100);
|
||||||
|
};
|
||||||
|
waitForFoo();
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
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.bringToFront();
|
||||||
|
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]');
|
||||||
|
await inputElement.click();
|
||||||
|
await page.waitForNavigation({waitUntil: 'networkidle2'});
|
||||||
|
return await page.waitForSelector('#header');
|
||||||
|
},
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
await this.delay(100);
|
||||||
|
}
|
||||||
|
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})
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
await this.pageBase.$eval('body', function (e) {
|
||||||
|
$('.live-relative-timestamp').removeClass('live-relative-timestamp').text('5 minutes ago');
|
||||||
|
$(':focus').blur();
|
||||||
|
});
|
||||||
|
await this.pageCompare.$eval('body', function (e) {
|
||||||
|
$('.live-relative-timestamp').removeClass('live-relative-timestamp').text('5 minutes ago');
|
||||||
|
$(':focus').blur();
|
||||||
|
});
|
||||||
|
var failed = null;
|
||||||
|
try {
|
||||||
|
await this.pageBase.bringToFront();
|
||||||
|
await action(this.pageBase);
|
||||||
|
await this.pageCompare.bringToFront();
|
||||||
|
await action(this.pageCompare);
|
||||||
|
} catch (err) {
|
||||||
|
failed = err;
|
||||||
|
}
|
||||||
|
await this.awaitNetworkIdle(3);
|
||||||
|
await this.pageBase.$eval('body', function (e) {
|
||||||
|
$('.live-relative-timestamp').removeClass('live-relative-timestamp').text('5 minutes ago');
|
||||||
|
$(':focus').blur();
|
||||||
|
});
|
||||||
|
await this.pageCompare.$eval('body', function (e) {
|
||||||
|
$('.live-relative-timestamp').removeClass('live-relative-timestamp').text('5 minutes ago');
|
||||||
|
$(':focus').blur();
|
||||||
|
});
|
||||||
|
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 + '\']/..');
|
||||||
|
}
|
||||||
|
};
|
|
@ -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>
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"name": "ui-regression",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "mocha test/"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"dependencies": {
|
||||||
|
"chai": "^4.1.2",
|
||||||
|
"fs": "0.0.1-security",
|
||||||
|
"mocha": "^5.2.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.6.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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', () => {});
|
||||||
|
});
|
||||||
|
|
|
@ -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'});
|
||||||
|
});
|
||||||
|
|
||||||
|
['your-apps', '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});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -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('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});
|
||||||
|
});
|
||||||
|
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});
|
||||||
|
});
|
||||||
|
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('[data-tabid=\'commentsTabView\']');
|
||||||
|
await page.$eval('body', e => { $('.shareWithField').blur() });
|
||||||
|
await helper.delay(500); // wait for animation
|
||||||
|
}, {viewport: resolution});
|
||||||
|
});
|
||||||
|
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 page.$eval('body', e => { $('.shareWithField').blur() });
|
||||||
|
await helper.delay(500); // wait for animation
|
||||||
|
}, {viewport: resolution});
|
||||||
|
});
|
||||||
|
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
|
||||||
|
}, {viewport: resolution});
|
||||||
|
});
|
||||||
|
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});
|
||||||
|
});
|
||||||
|
|
||||||
|
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});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
});
|
|
@ -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('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(300);
|
||||||
|
}, { waitUntil: 'networkidle0', viewport: resolution});
|
||||||
|
});
|
||||||
|
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(300);
|
||||||
|
}, { waitUntil: 'networkidle0', viewport: resolution});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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: 'networkidle2'});
|
||||||
|
await page.waitForSelector('#header');
|
||||||
|
helper.pageBase.setDefaultNavigationTimeout(60000);
|
||||||
|
helper.pageCompare.setDefaultNavigationTimeout(60000);
|
||||||
|
}, { waitUntil: 'networkidle0', viewport: {w: 1920, h: 1080}});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,81 @@
|
||||||
|
/**
|
||||||
|
* @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.waitForSelector('input#user');
|
||||||
|
await page.type('#user', 'admin');
|
||||||
|
await page.type('#password', 'admin');
|
||||||
|
const inputElement = await page.$('input[type=submit]');
|
||||||
|
await inputElement.click();
|
||||||
|
await page.waitForNavigation({waitUntil: 'networkidle2'});
|
||||||
|
await page.waitForSelector('#header');
|
||||||
|
await page.$eval('body', function (e) {
|
||||||
|
// force relative timestamp to fixed value, since it breaks screenshot diffing
|
||||||
|
$('.live-relative-timestamp').removeClass('live-relative-timestamp').text('5 minutes ago');
|
||||||
|
});
|
||||||
|
return await helper.delay(100);
|
||||||
|
}, {viewport: resolution});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -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}});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -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});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue