diff options
-rw-r--r-- | .drone.yml | 48 | ||||
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | apps/encryption/l10n/ja.js | 1 | ||||
-rw-r--r-- | apps/encryption/l10n/ja.json | 1 | ||||
-rw-r--r-- | core/css/apps.scss | 36 | ||||
-rw-r--r-- | core/css/guest.css | 7 | ||||
-rw-r--r-- | core/css/mobile.scss | 37 | ||||
-rw-r--r-- | core/css/styles.scss | 2 | ||||
-rw-r--r-- | core/templates/layout.base.php | 6 | ||||
-rw-r--r-- | core/templates/layout.public.php | 8 | ||||
-rw-r--r-- | core/templates/layout.user.php | 6 | ||||
-rw-r--r-- | tests/ui-regression/config.js | 58 | ||||
-rw-r--r-- | tests/ui-regression/helper.js | 256 | ||||
-rw-r--r-- | tests/ui-regression/out/index.html | 219 | ||||
-rw-r--r-- | tests/ui-regression/package.json | 21 | ||||
-rw-r--r-- | tests/ui-regression/runTests.js | 129 | ||||
-rw-r--r-- | tests/ui-regression/test/appsSpec.js | 60 | ||||
-rw-r--r-- | tests/ui-regression/test/filesSpec.js | 102 | ||||
-rw-r--r-- | tests/ui-regression/test/installSpec.js | 76 | ||||
-rw-r--r-- | tests/ui-regression/test/loginSpec.js | 81 | ||||
-rw-r--r-- | tests/ui-regression/test/publicSpec.js | 102 | ||||
-rw-r--r-- | tests/ui-regression/test/settingsSpec.js | 76 |
22 files changed, 1272 insertions, 63 deletions
diff --git a/.drone.yml b/.drone.yml index 709079bac01..da877a95d50 100644 --- a/.drone.yml +++ b/.drone.yml @@ -703,6 +703,32 @@ pipeline: when: matrix: 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: include: - TESTS: checkers @@ -848,6 +874,7 @@ matrix: ENABLE_REDIS_CLUSTER: true - TESTS: sqlite-php7.0-webdav-apache ENABLE_REDIS: true + - TESTS: ui-regression services: cache: @@ -855,6 +882,27 @@ services: when: matrix: 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: image: morrisjobke/redis-cluster when: diff --git a/.gitignore b/.gitignore index a11e3a14597..f8721e67fa8 100644 --- a/.gitignore +++ b/.gitignore @@ -139,6 +139,9 @@ Vagrantfile /tests/autotest* /tests/data/lorem-copy.txt /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/autoconfig.php clover.xml diff --git a/apps/encryption/l10n/ja.js b/apps/encryption/l10n/ja.js index ddcc2857738..4168636983d 100644 --- a/apps/encryption/l10n/ja.js +++ b/apps/encryption/l10n/ja.js @@ -32,6 +32,7 @@ OC.L10N.register( "Can not read this file, probably this is a shared file. Please ask the file owner to reshare the file with you." : "このファイルを読み取ることができません、共有ファイルの可能性があります。ファイルの所有者にお願いして、ファイルを共有しなおしてもらってください。", "Default encryption module" : "デフォルトの暗号化モジュール", "Default encryption module for server-side encryption" : "サーバーサイド暗号化のデフォルトの暗号化モジュール", + "In order to use this encryption module you need to enable server-side\n\t\tencryption in the admin settings. Once enabled this module will encrypt\n\t\tall your files transparently. The encryption is based on AES 256 keys.\n\t\tThe module won't touch existing files, only new files will be encrypted\n\t\tafter server-side encryption was enabled. It is also not possible to\n\t\tdisable the encryption again and switch back to a unencrypted system.\n\t\tPlease read the documentation to know all implications before you decide\n\t\tto enable server-side encryption." : "この暗号化モジュールを使うには管理者画面でサーバーサイド暗号化を有効にする\n\t\t必要があります。このモジュールを一旦有効にすると全てのファイルが\n\t\t意識することなく暗号化されます。AES 256bit の鍵で暗号化されます。\n\t\tこのモジュールは既存ファイルはそのままで、サーバーサイド暗号化を\n\t\t有効にした後の新しいファイルのみ暗号化します。 一旦暗号化したシステムを\n\t\t無効化して元の暗号化されていない状態に戻すことはできません。\n\t\tサーバーサイド暗号化するかどうか決める前にドキュメントをよく読んで\n\t\t全ての要件を確認してください。", "Hey there,\n\nthe admin enabled server-side-encryption. Your files were encrypted using the password '%s'.\n\nPlease login to the web interface, go to the section 'basic encryption module' of your personal settings and update your encryption password by entering this password into the 'old log-in password' field and your current login-password.\n\n" : "こんにちは\n\n管理者がサーバーサイド暗号化を有効にしました。'%s'というパスワードであなたのファイルが暗号化されました。\n\nWeb画面からログインして、個人設定画面の'基本暗号化モジュール' セクションにいき、暗号化パスワードの更新をお願いします。 '旧ログインパスワード'部分に上記パスワードを入力し、現在のログインパスワードで更新します。\n", "The share will expire on %s." : "共有は %s で有効期限が切れます。", "Cheers!" : "それでは!", diff --git a/apps/encryption/l10n/ja.json b/apps/encryption/l10n/ja.json index d08d34450cb..0d22fd27739 100644 --- a/apps/encryption/l10n/ja.json +++ b/apps/encryption/l10n/ja.json @@ -30,6 +30,7 @@ "Can not read this file, probably this is a shared file. Please ask the file owner to reshare the file with you." : "このファイルを読み取ることができません、共有ファイルの可能性があります。ファイルの所有者にお願いして、ファイルを共有しなおしてもらってください。", "Default encryption module" : "デフォルトの暗号化モジュール", "Default encryption module for server-side encryption" : "サーバーサイド暗号化のデフォルトの暗号化モジュール", + "In order to use this encryption module you need to enable server-side\n\t\tencryption in the admin settings. Once enabled this module will encrypt\n\t\tall your files transparently. The encryption is based on AES 256 keys.\n\t\tThe module won't touch existing files, only new files will be encrypted\n\t\tafter server-side encryption was enabled. It is also not possible to\n\t\tdisable the encryption again and switch back to a unencrypted system.\n\t\tPlease read the documentation to know all implications before you decide\n\t\tto enable server-side encryption." : "この暗号化モジュールを使うには管理者画面でサーバーサイド暗号化を有効にする\n\t\t必要があります。このモジュールを一旦有効にすると全てのファイルが\n\t\t意識することなく暗号化されます。AES 256bit の鍵で暗号化されます。\n\t\tこのモジュールは既存ファイルはそのままで、サーバーサイド暗号化を\n\t\t有効にした後の新しいファイルのみ暗号化します。 一旦暗号化したシステムを\n\t\t無効化して元の暗号化されていない状態に戻すことはできません。\n\t\tサーバーサイド暗号化するかどうか決める前にドキュメントをよく読んで\n\t\t全ての要件を確認してください。", "Hey there,\n\nthe admin enabled server-side-encryption. Your files were encrypted using the password '%s'.\n\nPlease login to the web interface, go to the section 'basic encryption module' of your personal settings and update your encryption password by entering this password into the 'old log-in password' field and your current login-password.\n\n" : "こんにちは\n\n管理者がサーバーサイド暗号化を有効にしました。'%s'というパスワードであなたのファイルが暗号化されました。\n\nWeb画面からログインして、個人設定画面の'基本暗号化モジュール' セクションにいき、暗号化パスワードの更新をお願いします。 '旧ログインパスワード'部分に上記パスワードを入力し、現在のログインパスワードで更新します。\n", "The share will expire on %s." : "共有は %s で有効期限が切れます。", "Cheers!" : "それでは!", diff --git a/core/css/apps.scss b/core/css/apps.scss index d8b2def9e5f..02603562c6f 100644 --- a/core/css/apps.scss +++ b/core/css/apps.scss @@ -77,7 +77,7 @@ kbd { position: fixed; top: $header-height; left: 0; - z-index: 500; + z-index: 1000; overflow-y: auto; overflow-x: hidden; // Do not use vh because of mobile headers @@ -583,31 +583,25 @@ kbd { /* CONTENT --------------------------------------------------------- */ -#content-wrapper { - // everything not related to content but needs to be on the window - // goes here (popups, tooltips...) - position: relative; - min-height: 100%; - display: unset; -} #content { box-sizing: border-box; position: relative; display: flex; - margin-top: $header-height; + // padding is included in height + padding-top: $header-height; min-height: 100%; } /* APP-CONTENT AND WRAPPER ------------------------------------------ */ /* Part where the content will be loaded into */ #app-content { - z-index: 1000; + z-index: 500; background-color: var(--color-main-background); position: relative; - min-height: 100%; flex-basis: 100vw; + min-height: 100%; /* margin if navigation element is here */ - #app-navigation + & { + #app-navigation + & { margin-left: $navigation-width; } /* no top border for first settings item */ @@ -619,17 +613,10 @@ kbd { #app-content-wrapper { display: flex; position: relative; - align-items: start; - .app-content-list, - .app-content-detail { - min-height: calc(100vh - #{$header-height}); - max-height: calc(100vh - #{$header-height}); - overflow-x: hidden; - overflow-y: auto; - } + align-items: stretch; /* CONTENT DETAILS AFTER LIST*/ - .app-content-detail { + .app-content-details { /* grow full width */ flex-grow: 1; #app-navigation-toggle-back { @@ -1049,10 +1036,17 @@ $popovericon-size: 16px; /* CONTENT LIST ------------------------------------------------------------ */ .app-content-list { width: 300px; + position: sticky; + top: $header-height; border-right: 1px solid var(--color-border); display: flex; flex-direction: column; transition: transform 250ms ease-in-out; + min-height: calc(100vh - #{$header-height}); + max-height: calc(100vh - #{$header-height}); + overflow-y: auto; + overflow-x: hidden; + flex: 0 0 300px; /* Default item */ .app-content-list-item { diff --git a/core/css/guest.css b/core/css/guest.css index 75ad1a787da..422ceace5bc 100644 --- a/core/css/guest.css +++ b/core/css/guest.css @@ -454,13 +454,14 @@ form #selectDbType { text-align:center; white-space: nowrap; margin: 0; + display: flex; } form #selectDbType .info { white-space: normal; } form #selectDbType label { - position: static; - margin: 0 -3px 5px; + flex-grow: 1; + margin: 0 -1px 5px; font-size: 12px; background:#f8f8f8; color:#888; @@ -469,7 +470,7 @@ form #selectDbType label { } form #selectDbType label span { cursor: pointer; - padding: 10px 20px; + padding: 10px 17px; } form #selectDbType label.ui-state-hover, form #selectDbType label.ui-state-active { diff --git a/core/css/mobile.scss b/core/css/mobile.scss index 93e2909a510..6330be9d399 100644 --- a/core/css/mobile.scss +++ b/core/css/mobile.scss @@ -35,19 +35,18 @@ /* full width for message list on mobile */ .app-content-list { - width: 100%; background: var(--color-main-background); - position: relative; - z-index: 100; - } - - /* since list and content are only displayed full window size - * we don't ant inner scrolling - */ - #app-content-wrapper { - .app-content-list, - .app-content-detail { - max-height: unset; + flex: 1 1 100%; + // make full height scroll since app-content-details is hidden + max-height: unset; + + .app-content-details { + display: none; + } + &.showdetails { + display: none; + + .app-content-details { + display: initial; + } } } @@ -59,7 +58,7 @@ #app-navigation-toggle-back { position: fixed; display: inline-block !important; - top: 45px; + top: $header-height; left: 0; width: 44px; height: 44px; @@ -73,18 +72,11 @@ transform: translateX(-100%); } - /* end of media query */ -} - /* allow horizontal scrollbar in settings - otherwise user management is not usable on mobile */ - #body-settings #app-content { - overflow-x: auto !important; } #app-navigation-toggle { position: fixed; display: inline-block !important; - top: $header-height; left: 0; width: 44px; height: 44px; @@ -132,11 +124,6 @@ max-width: 80%; } - /* fix controls bar jumping when navigation is slid out */ - .snapjs-left #app-navigation-toggle, - .snapjs-left #controls { - top: 0; - } .snapjs-left table.multiselect thead { top: 44px; } diff --git a/core/css/styles.scss b/core/css/styles.scss index 1269796b266..8996bee1cc3 100644 --- a/core/css/styles.scss +++ b/core/css/styles.scss @@ -818,7 +818,7 @@ span.ui-icon { } .content { - max-height: calc(100% - $header-height); + max-height: calc(100% - #{$header-height}); height: 100%; overflow-y: auto; diff --git a/core/templates/layout.base.php b/core/templates/layout.base.php index 8bb8e8ba3ca..d38bd114c3e 100644 --- a/core/templates/layout.base.php +++ b/core/templates/layout.base.php @@ -18,10 +18,8 @@ </head> <body id="body-public"> <?php include 'layout.noscript.warning.php'; ?> - <div id="content-wrapper"> - <div id="content" class="app-public" role="main"> - <?php print_unescaped($_['content']); ?> - </div> + <div id="content" class="app-public" role="main"> + <?php print_unescaped($_['content']); ?> </div> </body> </html> diff --git a/core/templates/layout.public.php b/core/templates/layout.public.php index afdd5656be4..07aff03127d 100644 --- a/core/templates/layout.public.php +++ b/core/templates/layout.public.php @@ -70,11 +70,9 @@ </div> <?php } ?> </header> - <div id="content-wrapper"> - <div id="content" class="app-<?php p($_['appid']) ?>" role="main"> - <?php print_unescaped($_['content']); ?> - </div> - </div + <div id="content" class="app-<?php p($_['appid']) ?>" role="main"> + <?php print_unescaped($_['content']); ?> + </div> <?php if($template->getFooterVisible()) { ?> <footer> <p class="info"><?php print_unescaped($theme->getLongFooter()); ?></p> diff --git a/core/templates/layout.user.php b/core/templates/layout.user.php index fbd71ec825b..32385c37aea 100644 --- a/core/templates/layout.user.php +++ b/core/templates/layout.user.php @@ -159,10 +159,8 @@ <input class="confirm" value="<?php p($l->t('Confirm')); ?>" type="submit"> </form> - <div id="content-wrapper"> - <div id="content" class="app-<?php p($_['appid']) ?>" role="main"> - <?php print_unescaped($_['content']); ?> - </div> + <div id="content" class="app-<?php p($_['appid']) ?>" role="main"> + <?php print_unescaped($_['content']); ?> </div> </body> diff --git a/tests/ui-regression/config.js b/tests/ui-regression/config.js new file mode 100644 index 00000000000..c40efd722d7 --- /dev/null +++ b/tests/ui-regression/config.js @@ -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, + +}; diff --git a/tests/ui-regression/helper.js b/tests/ui-regression/helper.js new file mode 100644 index 00000000000..1ec62728dc7 --- /dev/null +++ b/tests/ui-regression/helper.js @@ -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 + '\']/..'); + } +}; diff --git a/tests/ui-regression/out/index.html b/tests/ui-regression/out/index.html new file mode 100644 index 00000000000..a94dae13445 --- /dev/null +++ b/tests/ui-regression/out/index.html @@ -0,0 +1,219 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous" /> + <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" /> + <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script> + <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> + <title>Nextcloud UI regression tests</title> + <style> + + h2 { + margin-top: 40px; + margin-bottom: 20px; + } + .error { + color: #aa0000; + } + .success { + color: #00aa00; + } + .success img { + display: none; + width: 100px; + } + .success pre { + display: none; + } + .test-result h3 span { + width: 40px; + } + .test-result { + padding: 20px; + } + img { + max-width: 33%; + padding: 10px; + background-color: #eee; + margin: 0; + } + .overview ul { + position: fixed; + max-width: inherit; + margin: 0; + padding: 0; + } + ul li { + list-style-type: none; + padding: 3px; + } + ul a:first-child { + width: 100%; + display: inline-block; + } + ul span { + width: 16px; + height: 16px; + margin: 1px; + display: inline-block; + } + span.fa-check { + color: green; + } + span.fa-times { + color: red; + } + .navbar a { + color: #fff; + } + + .fade-enter-active, .fade-leave-active { + transition: opacity .5s; + } + .fade-enter, .fade-leave-to { + opacity: 0; + } + </style> +</head> + +<body> +<div id="app"> +<nav class="navbar navbar-expand-md navbar-dark bg-dark sticky-top"> + <div class="container"> + <a class="navbar-brand" href="#">Nextcloud UI regression test</a> + <a class="nav-link" :href="config.repoUrl">{{config.repoUrl}}</a> + <a class="nav-link" :href="config.repoUrl + '/pull/' + config.pr">#{{ config.pr }}</span></a> + </div> +</nav> + +<main role="main" class="container-fluid"> + <div class="row"> + <div class="col-md-2 overview"> + <ul> + <li v-for="suite in config.tests" v-if="result[suite]"> + <a :href="'#' + suite">{{ suite }}</a> + <a v-for="test in result[suite].tests" :href="test.fullTitle | convertToAnchor" :title="test.fullTitle"> + <span class="fa fa-times" v-if="Object.keys(test.err).length > 0"></span> + <span class="fa fa-check" v-else></span> + </a> + </li> + </ul> + </div> + <div class="col-md-10" id="container"> + <div v-for="suite in config.tests" v-if="result[suite]"> + <h2 :id="suite | convertToId">{{ suite }} <span>{{ result[suite].passes.length }}/{{ result[suite].tests.length }}</span></h2> + <test-result v-for="test in result[suite].tests" :key="test.fullTitle" :suite="suite" :test="test"></test-result> + </div> + </div> + </div> +</main> +</div> + +<script type="text/x-template" id="test-result-template"> + <div class="test-result" :id="test.fullTitle | convertToId"> + <h3 :class="{ error: Object.keys(test.err).length > 0, success: Object.keys(test.err).length == 0}" + v-on:click="hidden === undefined ? hidden = false : hidden = !hidden"> + <span class="fa fa-times" v-if="Object.keys(test.err).length > 0"></span> + <span class="fa fa-check" v-else></span> + {{ test.title }} + <i v-if="test.duration">{{ test.duration }}ms</i> + </h3> + <transition name="fade"> + <div v-if="(hidden === undefined && Object.keys(test.err).length > 0) || hidden === false"> + <div v-if="Object.keys(test.err).length > 0 && !test.err.failedAction"> + <a :href="getImagePath('.base')"><img :src="getImagePath('.base')" /></a> + <a :href="getImagePath('.diff')"><img :src="getImagePath('.diff')" /></a> + <a :href="getImagePath('.change')"><img :src="getImagePath('.change')" /></a> + </div> + <div v-else> + <a :href="getImagePath('')"><img :src="getImagePath('')" /></a> + </div> + <pre>{{ jsonData }}</pre> + </div> + </transition> + </div> +</script> + +<script> + + Vue.filter('convertToId', function (id) { + return id.replace(/\W/g,'_'); + }); + + Vue.filter('convertToAnchor', function (id) { + return '#' + id.replace(/\W/g,'_'); + }); + + Vue.component('test-result', { + template: '#test-result-template', + props: ['test', 'suite'], + data: function () { + return { + hidden: undefined + } + }, + computed: { + jsonData: function() { + return JSON.stringify(this.test, null, 2) + } + }, + methods: { + getImagePath: function(type) { + return this.suite + '/' + this.test.title + type + '.png'; + } + } + }); + + var app = new Vue({ + el: '#app', + data: { + message: 'Hello Vue!', + config: {}, + result: { + login: {} + }, + }, + created: function() { + this.fetchConfig(); + }, + methods: { + fetchConfig: function() { + var request = new XMLHttpRequest(); + request.open('GET', 'config.json', true); + + request.onload = function() { + if (request.status >= 200 && request.status < 400) { + app.config = JSON.parse(request.responseText); + app.config.tests.forEach(function(item, i){ + app.fetchResults(item); + }); + } + }; + + request.onerror = function() { + }; + + request.send(); + }, + fetchResults: function(suite) { + var request = new XMLHttpRequest(); + request.open('GET', suite + '.json', true); + + request.onload = function() { + if (request.status >= 200 && request.status < 400) { + Vue.set(app.result, suite, JSON.parse(request.responseText)); + } + }; + + request.onerror = function() { + }; + + request.send(); + } + } + }); + +</script> +</body> +</html> diff --git a/tests/ui-regression/package.json b/tests/ui-regression/package.json new file mode 100644 index 00000000000..8ab4cf530f2 --- /dev/null +++ b/tests/ui-regression/package.json @@ -0,0 +1,21 @@ +{ + "name": "ui-regression", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "mocha test/" + }, + "author": "", + "dependencies": { + "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" + } +} diff --git a/tests/ui-regression/runTests.js b/tests/ui-regression/runTests.js new file mode 100644 index 00000000000..4eb94f79347 --- /dev/null +++ b/tests/ui-regression/runTests.js @@ -0,0 +1,129 @@ +/** + * @copyright 2018 Julius Härtl <jus@bitgrid.net> + * + * @author 2018 Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +const fs = require('fs') +const Mocha = require('mocha') + +const testFolder = './test/' + + +var tests = [ + 'install', + 'login', + 'files', + 'public', + 'settings', + 'apps', +] + +var args = process.argv.slice(2); +if (args.length > 0) { + tests = args +} + +var config = { + tests: tests, + pr: process.env.DRONE_PULL_REQUEST, + repoUrl: process.env.DRONE_REPO_LINK, +}; + +console.log('=> Write test config'); +console.log(config); +fs.writeFile('out/config.json', JSON.stringify(config), 'utf8', () => {}); + +var mocha = new Mocha({ + timeout: 60000 +}); +let result = {}; + +tests.forEach(async function (test) { + mocha.addFile('./test/' + test + 'Spec.js') + result[test] = { + failures: [], + passes: [], + tests: [], + pending: [], + stats: {} + } + +}); + +// fixme fail if installation failed +// write json to file + +function clean (test) { + return { + title: test.title, + fullTitle: test.fullTitle(), + duration: test.duration, + currentRetry: test.currentRetry(), + failedAction: test.failedAction, + err: errorJSON(test.err || {}) + }; +} + +function errorJSON (err) { + var res = {}; + Object.getOwnPropertyNames(err).forEach(function (key) { + res[key] = err[key]; + }, err); + return res; +} + +mocha.run() + .on('test', function (test) { + }) + .on('suite end', function(suite) { + if (result[suite.title] === undefined) + return; + result[suite.title].stats = suite.stats; + }) + .on('test end', function (test) { + result[test.parent.title].tests.push(test); + }) + .on('pass', function (test) { + result[test.parent.title].passes.push(test); + }) + .on('fail', function (test) { + result[test.parent.title].failures.push(test); + }) + .on('pending', function (test) { + result[test.parent.title].pending.push(test); + }) + .on('end', function () { + tests.forEach(function (test) { + var json = JSON.stringify({ + stats: result[test].stats, + tests: result[test].tests.map(clean), + pending: result[test].pending.map(clean), + failures: result[test].failures.map(clean), + passes: result[test].passes.map(clean) + }, null, 2); + fs.writeFile(`out/${test}.json`, json, 'utf8', function () { + console.log(`Written test result to out/${test}.json`) + }); + }); + + var errorMessage = 'This PR introduces some UI differences, please check at {LINK}, if there are regressions based on the changes.' + fs.writeFile('out/GITHUB_COMMENT', errorMessage, 'utf8', () => {}); + }); + diff --git a/tests/ui-regression/test/appsSpec.js b/tests/ui-regression/test/appsSpec.js new file mode 100644 index 00000000000..3a23c1df773 --- /dev/null +++ b/tests/ui-regression/test/appsSpec.js @@ -0,0 +1,60 @@ +/** + * @copyright 2018 Julius Härtl <jus@bitgrid.net> + * + * @author 2018 Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +const helper = require('../helper.js'); +const config = require('../config.js'); + +describe('apps', function () { + + before(async () => { + await helper.init(this) + await helper.login(this) + }); + after(async () => await helper.exit()); + + config.resolutions.forEach(function (resolution) { + it('apps.' + resolution.title, async function () { + return helper.takeAndCompare(this, 'index.php/settings/apps', async function (page) { + await page.waitForSelector('#apps-list .section', {timeout: 5000}); + await page.waitFor(500); + }, {viewport: resolution, waitUntil: 'networkidle2'}); + }); + + ['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}); + }); + }); + }); + +}); diff --git a/tests/ui-regression/test/filesSpec.js b/tests/ui-regression/test/filesSpec.js new file mode 100644 index 00000000000..7a029b2f311 --- /dev/null +++ b/tests/ui-regression/test/filesSpec.js @@ -0,0 +1,102 @@ +/** + * @copyright 2018 Julius Härtl <jus@bitgrid.net> + * + * @author 2018 Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +const puppeteer = require('puppeteer'); +const helper = require('../helper.js'); +const config = require('../config.js'); + +describe('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}); + }); + + + }); + + + +}); diff --git a/tests/ui-regression/test/installSpec.js b/tests/ui-regression/test/installSpec.js new file mode 100644 index 00000000000..ffb4854f1b6 --- /dev/null +++ b/tests/ui-regression/test/installSpec.js @@ -0,0 +1,76 @@ +/** + * @copyright 2018 Julius Härtl <jus@bitgrid.net> + * + * @author 2018 Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +const helper = require('../helper.js'); +const config = require('../config.js'); + +describe('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}}); + }); + +}); diff --git a/tests/ui-regression/test/loginSpec.js b/tests/ui-regression/test/loginSpec.js new file mode 100644 index 00000000000..8607bbabc84 --- /dev/null +++ b/tests/ui-regression/test/loginSpec.js @@ -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}); + }) + }); + +}); diff --git a/tests/ui-regression/test/publicSpec.js b/tests/ui-regression/test/publicSpec.js new file mode 100644 index 00000000000..0893adf9a42 --- /dev/null +++ b/tests/ui-regression/test/publicSpec.js @@ -0,0 +1,102 @@ +/** + * @copyright 2018 Julius Härtl <jus@bitgrid.net> + * + * @author 2018 Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +const puppeteer = require('puppeteer'); +const helper = require('../helper.js'); +const config = require('../config.js'); + +describe('public', function () { + + before(async () => { + await helper.init(this) + await helper.login(this) + }); + after(async () => await helper.exit()); + + /** + * Test invalid file share rendering + */ + config.resolutions.forEach(function (resolution) { + it('file-share-invalid.' + resolution.title, async function () { + return helper.takeAndCompare(this, 'index.php/s/invalid', async function () { + }, {waitUntil: 'networkidle2', viewport: resolution}); + }); + }); + + /** + * Share a file via public link + */ + + var shareLink = {}; + it('file-share-link', async function () { + return helper.takeAndCompare(this, 'index.php/apps/files', async function (page) { + const element = await page.$('[data-file="welcome.txt"] .action-share'); + await element.click('[data-file="welcome.txt"] .action-share'); + await page.waitForSelector('input.linkCheckbox'); + const linkCheckbox = await page.$('.linkShareView label'); + await Promise.all([ + linkCheckbox.click(), + page.waitForSelector('.linkText') + ]); + await helper.delay(500); + const text = await page.waitForSelector('.linkText'); + const link = await (await text.getProperty('value')).jsonValue(); + shareLink[page.url()] = link; + return await helper.delay(500); + }, { + runOnly: true, + waitUntil: 'networkidle2', + viewport: {w: 1920, h: 1080} + }); + }); + + config.resolutions.forEach(function (resolution) { + it('file-share-valid.' + resolution.title, async function () { + return helper.takeAndCompare(this, 'index.php/apps/files', async function (page) { + await page.goto(shareLink[page.url()]); + await helper.delay(500); + }, {waitUntil: 'networkidle2', viewport: resolution}); + }); + it('file-share-valid-actions.' + resolution.title, async function () { + return helper.takeAndCompare(this, undefined, async function (page) { + const moreButton = await page.waitForSelector('#header-secondary-action'); + await moreButton.click(); + await page.evaluate((data) => { + return document.querySelector('#directLink').value = 'http://nextcloud.example.com/'; + }); + await helper.delay(500); + }, {waitUntil: 'networkidle2', viewport: resolution}); + }); + }); + + it('file-unshare', async function () { + return helper.takeAndCompare(this, 'index.php/apps/files', async function (page) { + const element = await page.$('[data-file="welcome.txt"] .action-share'); + await element.click('[data-file="welcome.txt"] .action-share'); + await page.waitForSelector('input.linkCheckbox'); + const linkCheckbox = await page.$('.linkShareView label'); + await linkCheckbox.click(); + await helper.delay(500); + }, { waitUntil: 'networkidle2', viewport: {w: 1920, h:1080}}); + }); + +}); diff --git a/tests/ui-regression/test/settingsSpec.js b/tests/ui-regression/test/settingsSpec.js new file mode 100644 index 00000000000..8b10b281fb6 --- /dev/null +++ b/tests/ui-regression/test/settingsSpec.js @@ -0,0 +1,76 @@ +/** + * @copyright 2018 Julius Härtl <jus@bitgrid.net> + * + * @author 2018 Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +const helper = require('../helper.js'); +const config = require('../config.js'); + +describe('settings', function () { + + before(async () => { + await helper.init(this) + await helper.login(this) + }); + after(async () => await helper.exit()); + + config.resolutions.forEach(function (resolution) { + it('personal.' + resolution.title, async function () { + return helper.takeAndCompare(this, 'index.php/settings/user', async function (page) { + }, {viewport: resolution}); + }); + + it('admin.' + resolution.title, async function () { + return helper.takeAndCompare(this, 'index.php/settings/admin', async function (page) { + }, {viewport: resolution}); + }); + + ['sharing', 'security', 'theming', 'encryption', 'additional', 'tips-tricks'].forEach(function(endpoint) { + it('admin.' + endpoint + '.' + resolution.title, async function () { + return helper.takeAndCompare(this, 'index.php/settings/admin/' + endpoint, async function (page) { + }, {viewport: resolution, waitUntil: 'networkidle2'}); + }); + }); + + it('usermanagement.' + resolution.title, async function () { + return helper.takeAndCompare(this, 'index.php/settings/users', async function (page) { + }, {viewport: resolution}); + }); + + it('usermanagement.add.' + resolution.title, async function () { + return helper.takeAndCompare(this, undefined, async function (page) { + try { + await page.waitForSelector('#app-navigation-toggle', { + visible: true, + timeout: 1000, + }).then((element) => element.click()) + } catch (err) {} + let newUserButton = await page.waitForSelector('#new-user-button'); + await newUserButton.click(); + await helper.delay(200); + await page.$eval('body', function (e) { + $('#newusername').blur(); + }) + await helper.delay(100); + }, {viewport: resolution}); + }); + + }); +}); |