Signed-off-by: Julius Härtl <jus@bitgrid.net>pull/8357/head
parent
0c7a17795b
commit
33f26c8da2
@ -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, |
||||
|
||||
}; |
@ -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 + '\']/..'); |
||||
} |
||||
}; |
@ -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": { |
||||
"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" |
||||
} |
||||
} |
@ -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'}); |
||||
}); |
||||
|
||||
['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}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
}); |
@ -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'}); |
||||
}); |
||||
|
||||
|
||||
}); |
||||
|
||||
|
||||
|
||||
}); |
@ -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}}); |
||||
}); |
||||
|
||||
}); |
@ -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}); |
||||
}) |
||||
}); |
||||
|
||||
}); |
@ -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