Merge pull request #146 from ascribe/AD-1519-visual-regression-cli

AD-1519 Visual regression tests
This commit is contained in:
Brett Sun 2016-02-04 12:03:50 +01:00
commit 3c6dea3585
28 changed files with 1320 additions and 80 deletions

10
.gitignore vendored
View File

@ -16,10 +16,14 @@ webapp-dependencies.txt
pids
logs
results
node_modules/*
build
build/*
gemini-coverage/*
gemini-report/*
test/gemini/screenshots/*
node_modules/*
.DS_Store
.env

View File

@ -1,18 +1,19 @@
Introduction
============
Onion is the web client for Ascribe. The idea is to have a well documented,
easy to test, easy to hack, JavaScript application.
Onion is the web client for Ascribe. The idea is to have a well documented, modern, easy to test, easy to hack, JavaScript application.
The code is JavaScript ECMA 6.
The code is JavaScript 2015 / ECMAScript 6.
Getting started
===============
Install some nice extension for Chrom(e|ium):
- [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi)
- [Alt Developer Tools](https://github.com/goatslacker/alt-devtool)
```bash
git clone git@github.com:ascribe/onion.git
cd onion
@ -37,17 +38,34 @@ Additionally, to work on the white labeling functionality, you need to edit your
JavaScript Code Conventions
===========================
For this project, we're using:
* 4 Spaces
* We use ES6
* ES6
* We don't use ES6's class declaration for React components because it does not support Mixins as well as Autobinding ([Blog post about it](http://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding))
* We don't use camel case for file naming but in everything Javascript related
* We use `let` instead of `var`: [SA Post](http://stackoverflow.com/questions/762011/javascript-let-keyword-vs-var-keyword)
* We don't use Javascript's `Date` object, as its interface introduced bugs previously and we're including `momentjs` for other dependencies anyways
* We use `momentjs` instead of Javascript's `Date` object, as the native `Date` interface previously introduced bugs and we're including `momentjs` for other dependencies anyway
Make sure to check out the [style guide](https://github.com/ascribe/javascript).
Linting
-------
We use [ESLint](https://github.com/eslint/eslint) with our own [custom ruleset](.eslintrc).
SCSS Code Conventions
=====================
Install [lint-scss](https://github.com/brigade/scss-lint), check the [editor integration docs](https://github.com/brigade/scss-lint#editor-integration) to integrate the lint in your editor.
Some interesting links:
* [Improving Sass code quality on theguardian.com](https://www.theguardian.com/info/developer-blog/2014/may/13/improving-sass-code-quality-on-theguardiancom)
Branch names
=====================
============
To allow Github and JIRA to track branches while still allowing us to switch branches quickly using a ticket's number (and keep our peace of mind), we have the following rules for naming branches:
@ -61,22 +79,21 @@ AD-<JIRA-ticket-id>-brief-and-sane-description-of-the-ticket
where `brief-and-sane-description-of-the-ticket` does not need to equal to the issue or ticket's title.
Example
-------------
-------
**JIRA ticket name:** `AD-1242 - Frontend caching for simple endpoints to measure perceived page load <more useless information>`
**Github branch name:** `AD-1242-caching-solution-for-stores`
SCSS Code Conventions
=====================
Install [lint-scss](https://github.com/brigade/scss-lint), check the [editor integration docs](https://github.com/brigade/scss-lint#editor-integration) to integrate the lint in your editor.
Some interesting links:
* [Improving Sass code quality on theguardian.com](https://www.theguardian.com/info/developer-blog/2014/may/13/improving-sass-code-quality-on-theguardiancom)
Testing
===============
=======
Unit Testing
------------
We're using Facebook's jest to do testing as it integrates nicely with react.js as well.
Tests are always created per directory by creating a `__tests__` folder. To test a specific file, a `<file_name>_tests.js` file needs to be created.
@ -86,7 +103,24 @@ This is due to the fact that jest's function mocking and ES6 module syntax are [
Therefore, to require a module in your test file, you need to use CommonJS's `require` syntax. Except for this, all tests can be written in ES6 syntax.
## Workflow
Visual Regression Testing
-------------------------
We're using [Gemini](https://github.com/gemini-testing/gemini) for visual regression tests because it supports both PhantomJS2 and SauceLabs.
See the [helper docs](test/gemini/README.md) for information on installing Gemini, its dependencies, and running and writing tests.
Integration Testing
-------------------
We're using [Sauce Labs](https://saucelabs.com/home) with [WD.js](https://github.com/admc/wd) for integration testing across browser grids with Selenium.
See the [helper docs](test/integration/README.md) for information on each part of the test stack and how to run and write tests.
Workflow
========
Generally, when you're runing `gulp serve`, all tests are being run.
If you want to test exclusively (without having the obnoxious ES6Linter warnings), you can just run `gulp jest:watch`.
@ -137,9 +171,16 @@ A: Easily by starting the your gulp process with the following command:
ONION_BASE_URL='/' ONION_SERVER_URL='http://localhost.com:8000/' gulp serve
```
Or, by adding these two your environment variables:
```
ONION_BASE_URL='/'
ONION_SERVER_URL='http://localhost.com:8000/'
```
Q: I want to know all dependencies that get bundled into the live build.
A: ```browserify -e js/app.js --list > webapp-dependencies.txt```
Reading list
============
@ -152,7 +193,6 @@ Start here
- [alt.js](http://alt.js.org/)
- [alt.js readme](https://github.com/goatslacker/alt)
Moar stuff
----------

View File

@ -97,7 +97,8 @@ gulp.task('browser-sync', function() {
proxy: 'http://localhost:4000',
port: 3000,
open: false, // does not open the browser-window anymore (handled manually)
ghostMode: false
ghostMode: false,
notify: false // stop showing the browsersync pop up
});
});

View File

@ -84,6 +84,7 @@ let PieceListToolbarFilterWidget = React.createClass({
if (this.props.filterParams && this.props.filterParams.length) {
return (
<DropdownButton
id="ascribe-piece-list-toolbar-filter-widget-dropdown"
pullRight={true}
title={filterIcon}
className="ascribe-piece-list-toolbar-filter-widget">

View File

@ -45,7 +45,7 @@ let PieceListToolbarOrderWidget = React.createClass({
},
render() {
let filterIcon = (
let orderIcon = (
<span>
<span className="ascribe-icon icon-ascribe-sort" aria-hidden="true"></span>
<span style={this.isOrderActive()}>&middot;</span>
@ -55,9 +55,10 @@ let PieceListToolbarOrderWidget = React.createClass({
if (this.props.orderParams && this.props.orderParams.length) {
return (
<DropdownButton
id="ascribe-piece-list-toolbar-order-widget-dropdown"
pullRight={true}
title={filterIcon}
className="ascribe-piece-list-toolbar-filter-widget">
className="ascribe-piece-list-toolbar-filter-widget"
title={orderIcon}>
<li style={{'textAlign': 'center'}}>
<em>{getLangText('Sort by')}:</em>
</li>

View File

@ -27,7 +27,7 @@ let CoaVerifyContainer = React.createClass({
return (
<div className="ascribe-login-wrapper">
<br/>
<br />
<div className="ascribe-login-text ascribe-login-header">
{getLangText('Verify your Certificate of Authenticity')}
</div>
@ -37,7 +37,7 @@ let CoaVerifyContainer = React.createClass({
signature={signature}/>
<br />
<br />
{getLangText('ascribe is using the following public key for verification')}:
{getLangText('ascribe is using the following public key for verification')}:
<br />
<pre>
-----BEGIN PUBLIC KEY-----
@ -60,9 +60,8 @@ let CoaVerifyForm = React.createClass({
},
handleSuccess(response){
let notification = null;
if (response.verdict) {
notification = new GlobalNotificationModel(getLangText('Certificate of Authenticity successfully verified'), 'success');
const notification = new GlobalNotificationModel(getLangText('Certificate of Authenticity successfully verified'), 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
}
},
@ -71,46 +70,44 @@ let CoaVerifyForm = React.createClass({
const { message, signature } = this.props;
return (
<div>
<Form
url={ApiUrls.coa_verify}
handleSuccess={this.handleSuccess}
buttons={
<button
type="submit"
className="btn btn-default btn-wide">
{getLangText('Verify your Certificate of Authenticity')}
</button>}
spinner={
<span className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" />
</span>
}>
<Property
name='message'
label={getLangText('Message')}>
<input
type="text"
placeholder={getLangText('Copy paste the message on the bottom of your Certificate of Authenticity')}
autoComplete="on"
defaultValue={message}
name="username"
required/>
</Property>
<Property
name='signature'
label="Signature"
editable={true}
overrideForm={true}>
<InputTextAreaToggable
rows={3}
placeholder={getLangText('Copy paste the signature on the bottom of your Certificate of Authenticity')}
defaultValue={signature}
required/>
</Property>
<hr />
</Form>
</div>
<Form
url={ApiUrls.coa_verify}
handleSuccess={this.handleSuccess}
buttons={
<button
type="submit"
className="btn btn-default btn-wide">
{getLangText('Verify your Certificate of Authenticity')}
</button>
}
spinner={
<span className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" />
</span>
}>
<Property
name='message'
label={getLangText('Message')}>
<input
type="text"
placeholder={getLangText('Copy paste the message on the bottom of your Certificate of Authenticity')}
autoComplete="on"
defaultValue={message}
required />
</Property>
<Property
name='signature'
label="Signature"
editable={true}
overrideForm={true}>
<InputTextAreaToggable
rows={3}
placeholder={getLangText('Copy paste the signature on the bottom of your Certificate of Authenticity')}
defaultValue={signature}
required />
</Property>
<hr />
</Form>
);
}
});

View File

@ -173,6 +173,7 @@ let Header = React.createClass({
account = (
<DropdownButton
ref='dropdownbutton'
id="nav-route-user-dropdown"
eventKey="1"
title={currentUser.username}>
<LinkContainer

View File

@ -126,6 +126,7 @@ let HeaderNotifications = React.createClass({
<Nav navbar right>
<DropdownButton
ref='dropdownbutton'
id="header-notification-dropdown"
eventKey="1"
title={
<span>

View File

@ -30,6 +30,7 @@ let NavRoutesLinksLink = React.createClass({
return (
<DropdownButton
disabled={disabled}
id={`nav-route-${headerTitle.toLowerCase()}-dropdown`}
title={headerTitle}>
{children}
</DropdownButton>

View File

@ -57,7 +57,7 @@ const ROUTES = {
headerTitle='COLLECTION'/>
<Route path='pieces/:pieceId' component={SluicePieceContainer} />
<Route path='editions/:editionId' component={EditionContainer} />
<Route path='verify' component={CoaVerifyContainer} />
<Route path='coa_verify' component={CoaVerifyContainer} />
<Route path='*' component={ErrorNotFoundPage} />
</Route>
),
@ -97,7 +97,7 @@ const ROUTES = {
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SPSettingsContainer)}/>
<Route path='pieces/:pieceId' component={SPPieceContainer} />
<Route path='editions/:editionId' component={EditionContainer} />
<Route path='verify' component={CoaVerifyContainer} />
<Route path='coa_verify' component={CoaVerifyContainer} />
<Route path='*' component={ErrorNotFoundPage} />
</Route>
)

View File

@ -78,7 +78,7 @@ let ROUTES = {
headerTitle='COLLECTION'
disableOn='noPieces' />
<Route path='editions/:editionId' component={EditionContainer} />
<Route path='verify' component={CoaVerifyContainer} />
<Route path='coa_verify' component={CoaVerifyContainer} />
<Route path='pieces/:pieceId' component={CylandPieceContainer} />
<Route path='*' component={ErrorNotFoundPage} />
</Route>
@ -114,7 +114,7 @@ let ROUTES = {
disableOn='noPieces' />
<Route path='pieces/:pieceId' component={PieceContainer} />
<Route path='editions/:editionId' component={EditionContainer} />
<Route path='verify' component={CoaVerifyContainer} />
<Route path='coa_verify' component={CoaVerifyContainer} />
<Route path='*' component={ErrorNotFoundPage} />
</Route>
),
@ -159,7 +159,7 @@ let ROUTES = {
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(IkonotvContractNotifications)} />
<Route path='pieces/:pieceId' component={IkonotvPieceContainer} />
<Route path='editions/:editionId' component={EditionContainer} />
<Route path='verify' component={CoaVerifyContainer} />
<Route path='coa_verify' component={CoaVerifyContainer} />
<Route path='*' component={ErrorNotFoundPage} />
</Route>
),
@ -196,7 +196,7 @@ let ROUTES = {
disableOn='noPieces' />
<Route path='pieces/:pieceId' component={MarketPieceContainer} />
<Route path='editions/:editionId' component={MarketEditionContainer} />
<Route path='verify' component={CoaVerifyContainer} />
<Route path='coa_verify' component={CoaVerifyContainer} />
<Route path='*' component={ErrorNotFoundPage} />
</Route>
),
@ -233,7 +233,7 @@ let ROUTES = {
disableOn='noPieces' />
<Route path='pieces/:pieceId' component={MarketPieceContainer} />
<Route path='editions/:editionId' component={MarketEditionContainer} />
<Route path='verify' component={CoaVerifyContainer} />
<Route path='coa_verify' component={CoaVerifyContainer} />
<Route path='*' component={ErrorNotFoundPage} />
</Route>
)

View File

@ -12,8 +12,22 @@
"postinstall": "npm run build",
"build": "gulp build --production",
"start": "node server.js",
"test": "mocha",
"tunnel": "node test/tunnel.js"
"test": "npm run sauce-test",
"sauce-test": "mocha ./test/integration/tests/",
"tunnel": "node ./test/integration/tunnel.js",
"vi-clean": "rm -rf ./gemini-report",
"vi-phantom": "phantomjs --webdriver=4444",
"vi-update": "gemini update -c ./test/gemini/.gemini.yml",
"vi-test": "npm run vi-test:base || true",
"vi-test:base": "npm run vi-clean && gemini test -c ./test/gemini/.gemini.yml --reporter html --reporter vflat",
"vi-test:all": "npm run vi-test",
"vi-test:main": "npm run vi-test:base -- --browser MainDesktop --browser MainMobile || true",
"vi-test:whitelabel": "GEMINI_BROWSERS='CcDesktop, CcMobile, CylandDesktop, CylandMobile, IkonotvDesktop, IkonotvMobile, LumenusDesktop, LumenusMobile, 23viviDesktop, 23viviMobile' npm run vi-test:base || true",
"vi-test:cc": "npm run vi-test:base -- --browser CcDesktop --browser CcMobile",
"vi-test:cyland": "npm run vi-test:base -- --browser CylandDesktop --browser CylandMobile || true",
"vi-test:ikonotv": "npm run vi-test:base -- --browser IkonotvDesktop --browser IkonotvMobile || true",
"vi-test:lumenus": "npm run vi-test:base -- --browser LumenusDesktop --browser LumenusMobile || true",
"vi-test:23vivi": "npm run vi-test:base -- --browser 23viviDesktop --browser 23viviMobile || true"
},
"browser": {
"fineUploader": "./js/components/ascribe_uploader/vendor/s3.fine-uploader.js"
@ -42,8 +56,10 @@
"chai-as-promised": "^5.1.0",
"colors": "^1.1.2",
"dotenv": "^1.2.0",
"gemini": "^2.1.0",
"jest-cli": "^0.4.0",
"mocha": "^2.3.4",
"phantomjs2": "^2.0.2",
"sauce-connect-launcher": "^0.13.0",
"wd": "^0.4.0"
},

133
test/gemini/.gemini.yml Normal file
View File

@ -0,0 +1,133 @@
rootUrl: http://localhost.com:3000/
sessionsPerBrowser: 1
browsers:
MainDesktop:
rootUrl: http://localhost.com:3000/
screenshotsDir: './screenshots/main-desktop'
windowSize: 1900x1080
desiredCapabilities:
browserName: phantomjs
MainMobile:
rootUrl: http://localhost.com:3000/
screenshotsDir: './screenshots/main-mobile'
windowSize: 600x1056
desiredCapabilities:
browserName: phantomjs
CcDesktop:
rootUrl: http://cc.localhost.com:3000/
screenshotsDir: './screenshots/cc-desktop'
windowSize: 1900x1080
desiredCapabilities:
browserName: phantomjs
CcMobile:
rootUrl: http://cc.localhost.com:3000/
screenshotsDir: './screenshots/cc-mobile'
windowSize: 600x1056
desiredCapabilities:
browserName: phantomjs
CylandDesktop:
rootUrl: http://cyland.localhost.com:3000/
screenshotsDir: './screenshots/cyland-desktop'
windowSize: 1900x1080
desiredCapabilities:
browserName: phantomjs
CylandMobile:
rootUrl: http://cyland.localhost.com:3000/
screenshotsDir: './screenshots/cyland-mobile'
windowSize: 600x1056
desiredCapabilities:
browserName: phantomjs
IkonotvDesktop:
rootUrl: http://ikonotv.localhost.com:3000/
screenshotsDir: './screenshots/ikonotv-desktop'
windowSize: 1900x1080
desiredCapabilities:
browserName: phantomjs
IkonotvMobile:
rootUrl: http://ikonotv.localhost.com:3000/
screenshotsDir: './screenshots/ikonotv-mobile'
windowSize: 600x1056
desiredCapabilities:
browserName: phantomjs
LumenusDesktop:
rootUrl: http://lumenus.localhost.com:3000/
screenshotsDir: './screenshots/lumenus-desktop'
windowSize: 1900x1080
desiredCapabilities:
browserName: phantomjs
LumenusMobile:
rootUrl: http://lumenus.localhost.com:3000/
screenshotsDir: './screenshots/lumenus-mobile'
windowSize: 600x1056
desiredCapabilities:
browserName: phantomjs
23viviDesktop:
rootUrl: http://23vivi.localhost.com:3000/
screenshotsDir: './screenshots/23vivi-desktop'
windowSize: 1900x1080
desiredCapabilities:
browserName: phantomjs
23viviMobile:
rootUrl: http://23vivi.localhost.com:3000/
screenshotsDir: './screenshots/23vivi-mobile'
windowSize: 600x1056
desiredCapabilities:
browserName: phantomjs
sets:
main:
files:
- tests/main
browsers:
- MainDesktop
- MainMobile
cc:
files:
- tests/whitelabel/shared
browsers:
- CcDesktop
- CcMobile
cyland:
files:
- tests/whitelabel/shared
- tests/whitelabel/cyland
browsers:
- CylandDesktop
- CylandMobile
ikonotv:
files:
- tests/whitelabel/shared
- tests/whitelabel/ikonotv
browsers:
- IkonotvDesktop
- IkonotvMobile
lumenus:
files:
- tests/whitelabel/shared
- tests/whitelabel/lumenus
browsers:
- LumenusDesktop
- LumenusMobile
23vivi:
files:
- tests/whitelabel/shared
- tests/whitelabel/23vivi
browsers:
- 23viviDesktop
- 23viviMobile

208
test/gemini/README.md Normal file
View File

@ -0,0 +1,208 @@
Introduction
============
When in doubt, see [Gemini](https://github.com/gemini-testing/gemini) and [their
docs](https://github.com/gemini-testing/gemini/tree/master/doc) for more information as well as configuration options.
Contents
========
1. [Installation](#installation)
1. [Running Tests](#running-tests)
1. [Gemini Usage and Writing Tests](#gemini-usage-and-writing-tests)
1. [PhantomJS](#phantomjs)
1. [TODO](#todo)
Installation
============
First make sure that you're using NodeJS 5.0+ as the tests are written using ES6 syntax.
Then, install [PhantomJS2](https://www.npmjs.com/package/phantomjs2):
```bash
# Until phantomjs2 is updated for the new 2.1 version of PhantomJS, use the following (go to https://bitbucket.org/ariya/phantomjs/downloads to find a build for your OS)
npm install -g phantomjs2 --phantomjs_downloadurl=https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-macosx.zip
npm install --save-dev phantomjs2 --phantomjs_downloadurl=https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-macosx.zip
# If using OSX, you may have to install upx and decompress the binary downloaded by npm manually:
brew install upx
# Navigate to the binary, ie. /Users/Brett/.nvm/versions/node/v5.4.0/lib/node_modules/phantomjs2/lib/phantom/bin/phantomjs
upx -d phantomjs
```
Finally, [install Gemini globally and locally with npm](https://github.com/gemini-testing/gemini/blob/master/README.md#installation).
Running Tests
=============
Run PhantomJS:
```bash
npm run vi-phantom
```
And then run Gemini tests:
```bash
npm run vi-test
# Run only main tests
npm run vi-test:main
# Run only whitelabel tests
npm run vi-test:whitelabel
# Run only specific whitelabel tests
npm run vi-test:cyland
```
If you've made changes and want them to be the new baseline (ie. it's a correct change--**make sure** to test there are
no regressions first!), use
```bash
npm run vi-update
# Update just the main app for desktop and mobile
npm run vi-update -- --browser MainDesktop --browser MainMobile
```
Gemini Usage and Writing Tests
==============================
While Gemini itself is easy to use on simple, static pages, there are some nice to knows when dealing with a single page
app like ours (where much of it is behind an authentication barrier as well).
Command Line Interface
----------------------
See [the docs](https://github.com/gemini-testing/gemini/blob/master/doc/commands.md) on the commands that are available.
`npm run vi-*` is set up with some of these commands, but you may want to build your own or learn about some of the
other functions.
Authentication
--------------
Authentication presents a tricky problem with Gemini, since we can't inject any cookies or even run a start up script
through the browser before letting Gemini hook in. The solution is to script the log in process through Gemini, and put
waits for the log in to succeed, before testing parts of the app that require the authentication.
Browser Session States
----------------------
Gemini will start a new instance of the browser for each browser configuration defined in the .gemini.yml file when
Gemini's launched (ie. `gemini update`, `gemini test`, etc).
Although each new suite will cause the testing browser to be refreshed, the above means that cookies and other
persistent state will be kept across suites for a browser across all runs, even if the suites are from different files.
**What this comes down to is**: once you've logged in, you'll stay logged in until you decide to log out or the running
instance of Gemini ends. In general practice, it's a good idea to clear the state of the app at the end of each suite of
tests by logging out.
(**Note**: Persistent storage, such as local storage, has not been explicitly tested as to whether they are kept, but as
the cookies are cleared each time, this seems unlikely)
Test Reporting
--------------
Using the `--reporter html` flag with Gemini will produce a webpage with the test's results in `onion/gemini-report`
that will show the old, new, and diff images. Using this is highly recommended (and fun!) and is used by default in `npm
run vi-test`.
Writing Tests
-------------
See [the docs](https://github.com/gemini-testing/gemini/blob/master/doc/tests.md), and the [section on the available
actions](https://github.com/gemini-testing/gemini/blob/master/doc/tests.md#available-actions) for what scripted actions
are available.
Our tests are located in `onion/test/gemini/tests/`. For now, the tests use the environment defined in
`onion/test/gemini/tests/environment.js` for which user, piece, and edition to run tests against. In the future, it'd be
nice if we had some db scripts that we could use to populate a test db for these regression tests.
**It would also be nice if we kept the whitelabels up to date, so if you add one, please also test (at least) its landing
page.**
Some useful tips:
* The `find()` method in the callbacks is equivalent to `document.querySelector`; it will only return the first
element found that matches the selector. Use pseudo classes like `nth-of-type()`, `nth-child()`, and etc. to select
later elements.
* Nested suites inherit from their parent suites' configurations, but will **override** their inherited configuration
if another is specified. For example, if `parentSuite` had a `.before()` method, all children of `parentSuite` would
run its `.before()`, but if any of the children specified their own `.before()`, those children would **not** run
`parentSuite`'s `.before()`.
* Gemini takes a screenshot of the minimum bounding rect for all specified selectors, so this means you can't take a
screenshot of two items far away from each other without the rest being considered (ie. trying to get the header and
footer)
* Unfortunately, `setCaptureElements` and `ignoreElements` will only apply for the first element found matching those
selectors.
PhantomJS
=========
[PhantomJS](http://phantomjs.org/) is a headless browser that allows us to run tests and take screenshots without
needing a browser.
Its second version (PhantomJS2) uses a much more recent version of Webkit, and is a big reason why Gemini (as opposed to
other utilities, ie. PhantomCSS) was chosen. Due to the large number of breaking changes introduced between PhantomJS
1.9 to 2.0, a large number of tools (ie. CasperJS) are, at the time of writing, lacking support for 2.0.
While you don't need to know too much about PhantomJS to use and write Gemini tests, there are still a number of useful
things to know about.
Useful features
---------------
You can find the full list of CLI commands in the [documentation](http://phantomjs.org/api/command-line.html).
Flags that are of particular interest to us:
* `--webdriver=4444`: sets the webdriver port to be 4444, the default webdriver port that Gemini expects.
* `--ignore-ssl-errors=true`: ignores any SSL errors that may occur. Particular useful when hooking up the tests to
staging, as the certificate we use is self-signed.
* `--ssl-protocol=any`: allows any ssl protocol to be used. May be useful when `--ignore-ssl-errors=true` doesn't work.
* '--remote-debugger-port`: allows for remote debugging the running PhantomJS instance. More on this later.
Troubleshooting and Debugging
-----------------------------
Remote debugging is possible with PhantomJS using the `--remote-debugger-port` option. See the [troubleshooting
docs](http://phantomjs.org/troubleshooting.html).
To begin using it, add `debugger;` statements to the file being run by `phantomjs`, and access the port number specified
after `--remote-debugger-port` on localhost:
```bash
phantomjs --remote-debugger-port=9000 debug.js
```
PhantomJS will start and then immediately breakpoint. Go to http://localhost:9000/webkit/inspector/inspector.html?page=1
and then to its console tab. Go to your first breakpoint (the first `debugger;` statement executed) by running `__run()`
in the console tab. Subsequent breakpoints can be reached by successively running `__run()` in that same console tab.
At each breakpoint, you can to http://localhost:9000 on a new browser tab and click on one of the links to go to the
current execution state of that breakpoint on the page you're on.
---
To simplify triaging simple issues and test if everything is working, The repo had a short test script that can be run
with PhantomJS to check if it can access the web app and log in. Find `onion/test/phantomjs/launch_app_and_login.js` in
the repo's history, restore it, and then run:
```bash
# In root /onion folder
phantomjs test/phantomjs/launch_app_and_login.js
```
TODO
====
* Write scripts to automate creation of test users (and modify tests to accomodate)
* Set scripts with rootUrls pointing to staging / live using environment variables
* Set up with Sauce Labs

View File

@ -0,0 +1,35 @@
'use strict';
const MAIN_USER = {
email: 'dimi@mailinator.com',
password: '0000000000'
};
const MAIN_PIECE_ID = '12374';
const MAIN_EDITION_ID = '14gw9x3VA9oJaxp4cHaAuK2bvJzvEj4Xvc';
const TIMEOUTS = {
SHORT: 3000,
NORMAL: 5000,
LONG: 10000,
SUPER_DUPER_EXTRA_LONG: 30000
};
console.log('================== Test environment ==================\n');
console.log('Main user:');
console.log(` Email: ${MAIN_USER.email}`);
console.log(` Password: ${MAIN_USER.password}\n`);
console.log(`Main piece: ${MAIN_PIECE_ID}`);
console.log(`Main edition: ${MAIN_EDITION_ID}\n`);
console.log('Timeouts:');
console.log(` Short: ${TIMEOUTS.SHORT}`);
console.log(` Normal: ${TIMEOUTS.NORMAL}\n`);
console.log(` Long: ${TIMEOUTS.LONG}\n`);
console.log(` Super super extra long: ${TIMEOUTS.SUPER_DUPER_EXTRA_LONG}\n`);
console.log('========================================================\n');
module.exports = {
MAIN_USER,
MAIN_PIECE_ID,
MAIN_EDITION_ID,
TIMEOUTS
};

View File

@ -0,0 +1,218 @@
'use strict';
const gemini = require('gemini');
const environment = require('../environment');
const MAIN_USER = environment.MAIN_USER;
const TIMEOUTS = environment.TIMEOUTS;
/**
* Suite of tests against routes that require the user to be authenticated.
*/
gemini.suite('Authenticated', (suite) => {
suite
.setUrl('/collection')
.setCaptureElements('.ascribe-body')
.before((actions, find) => {
// This will be called before every nested suite begins unless that suite
// also defines a `.before()`
// FIXME: use a more generic class for this, like just '.app',
// when we can use this file with the whitelabels
actions.waitForElementToShow('.ascribe-default-app', TIMEOUTS.NORMAL);
});
// Suite just to log us in before any other suites run
gemini.suite('Login', (loginSuite) => {
loginSuite
.setUrl('/login')
.ignoreElements('.ascribe-body')
.capture('logged in', (actions, find) => {
actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL);
actions.sendKeys(find('.ascribe-login-wrapper input[name=email]'), MAIN_USER.email);
actions.sendKeys(find('.ascribe-login-wrapper input[name=password]'), MAIN_USER.password);
actions.click(find('.ascribe-login-wrapper button[type=submit]'));
actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', TIMEOUTS.NORMAL);
});
});
gemini.suite('Header-desktop', (headerSuite) => {
headerSuite
.setCaptureElements('nav.navbar .container')
// Ignore Cyland's logo as it's a gif
.ignoreElements('.client--cyland img.img-brand')
.skip(/Mobile/)
.before((actions, find) => {
actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', TIMEOUTS.NORMAL);
})
.capture('desktop header');
gemini.suite('User dropdown', (headerUserSuite) => {
headerUserSuite
.setCaptureElements('#nav-route-user-dropdown ~ .dropdown-menu')
.capture('expanded user dropdown', (actions, find) => {
actions.click(find('#nav-route-user-dropdown'));
});
});
gemini.suite('Notification dropdown', (headerNotificationSuite) => {
headerNotificationSuite
.setCaptureElements('#header-notification-dropdown ~ .dropdown-menu')
.capture('expanded notifications dropdown', (actions, find) => {
actions.click(find('#header-notification-dropdown'));
});
});
});
// Test for the collapsed header in mobile
gemini.suite('Header-mobile', (headerMobileSuite) => {
headerMobileSuite
.setCaptureElements('nav.navbar .container')
// Ignore Cyland's logo as it's a gif
.ignoreElements('.client--cyland img.img-brand')
.skip(/Desktop/)
.before((actions, find) => {
actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', TIMEOUTS.NORMAL);
})
.capture('mobile header')
.capture('expanded mobile header', (actions, find) => {
actions.click(find('nav.navbar .navbar-toggle'));
// Wait for the header to expand
actions.wait(500);
})
.capture('expanded user dropdown', (actions, find) => {
actions.click(find('#nav-route-user-dropdown'));
})
.capture('expanded notifications dropdown', (actions, find) => {
actions.click(find('#header-notification-dropdown'));
});
});
gemini.suite('Collection', (collectionSuite) => {
collectionSuite
.setCaptureElements('.ascribe-accordion-list')
.before((actions, find) => {
actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', TIMEOUTS.NORMAL);
// Wait for the images to load
// FIXME: unfortuntately gemini doesn't support ignoring multiple elements from a single selector
// so we're forced to wait and hope that the images will all finish loading after 5s.
// We could also change the thumbnails with JS, but setting up a test user is probably easier.
actions.wait(TIMEOUTS.NORMAL);
})
.capture('collection')
.capture('expanded edition in collection', (actions, find) => {
actions.click(find('.ascribe-accordion-list-item .ascribe-accordion-list-item-edition-widget'));
// Wait for editions to load
actions.waitForElementToShow('.ascribe-accordion-list-item-table', TIMEOUTS.LONG);
})
gemini.suite('Collection placeholder', (collectionPlaceholderSuite) => {
collectionPlaceholderSuite
.setCaptureElements('.ascribe-accordion-list-placeholder')
.capture('collection empty search', (actions, find) => {
actions.sendKeys(find('.ascribe-piece-list-toolbar .search-bar input[type="text"]'), 'no search result');
actions.waitForElementToShow('.ascribe-accordion-list-placeholder', TIMEOUTS.NORMAL);
});
});
gemini.suite('PieceListBulkModal', (pieceListBulkModalSuite) => {
pieceListBulkModalSuite
.setCaptureElements('.piece-list-bulk-modal')
.capture('items selected', (actions, find) => {
actions.click(find('.ascribe-accordion-list-item .ascribe-accordion-list-item-edition-widget'));
// Wait for editions to load
actions.waitForElementToShow('.ascribe-accordion-list-item-table', TIMEOUTS.NORMAL);
actions.click('.ascribe-table thead tr input[type="checkbox"]');
actions.waitForElementToShow('.piece-list-bulk-modal');
});
});
});
gemini.suite('PieceListToolbar', (pieceListToolbarSuite) => {
pieceListToolbarSuite
.setCaptureElements('.ascribe-piece-list-toolbar')
.capture('piece list toolbar')
.capture('piece list toolbar search filled', (actions, find) => {
actions.sendKeys(find('.ascribe-piece-list-toolbar .search-bar input[type="text"]'), 'search text');
actions.waitForElementToShow('.ascribe-piece-list-toolbar .search-bar .icon-ascribe-search', TIMEOUTS.NORMAL);
})
gemini.suite('Order widget dropdown', (pieceListToolbarOrderWidgetSuite) => {
pieceListToolbarOrderWidgetSuite
.setCaptureElements('#ascribe-piece-list-toolbar-order-widget-dropdown',
'#ascribe-piece-list-toolbar-order-widget-dropdown ~ .dropdown-menu')
.capture('expanded order dropdown', (actions, find) => {
actions.click(find('#ascribe-piece-list-toolbar-order-widget-dropdown'));
// Wait as the dropdown screenshot still includes the collection in the background
actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', TIMEOUTS.NORMAL);
});
});
gemini.suite('Filter widget dropdown', (pieceListToolbarFilterWidgetSuite) => {
pieceListToolbarFilterWidgetSuite
.setCaptureElements('#ascribe-piece-list-toolbar-filter-widget-dropdown',
'#ascribe-piece-list-toolbar-filter-widget-dropdown ~ .dropdown-menu')
.capture('expanded filter dropdown', (actions, find) => {
actions.click(find('#ascribe-piece-list-toolbar-filter-widget-dropdown'));
// Wait as the dropdown screenshot still includes the collection in the background
actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', TIMEOUTS.NORMAL);
});
});
});
gemini.suite('Register work', (registerSuite) => {
registerSuite
.setUrl('/register_piece')
.capture('register work', (actions, find) => {
// The uploader options are only rendered after the user is fetched, so
// we have to wait for it here
actions.waitForElementToShow('.file-drag-and-drop-dialog .present-options', TIMEOUTS.NORMAL);
})
.capture('register work filled', (actions, find) => {
actions.sendKeys(find('.ascribe-form input[name="artist_name"]'), 'artist name');
actions.sendKeys(find('.ascribe-form input[name="title"]'), 'title');
actions.sendKeys(find('.ascribe-form input[name="date_created"]'), 'date created');
})
.capture('register work filled with editions', (actions, find) => {
actions.click(find('.ascribe-form input[type="checkbox"] ~ .checkbox'));
actions.wait(500);
actions.sendKeys(find('.ascribe-form input[name="num_editions"]'), '50');
});
gemini.suite('Register work hash', (registerHashSuite) => {
registerHashSuite
.setUrl('/register_piece?method=hash')
.capture('register work hash method');
});
gemini.suite('Register work upload', (registerUploadSuite) => {
registerUploadSuite
.setUrl('/register_piece?method=upload')
.capture('register work upload method');
});
});
gemini.suite('User settings', (userSettingsSuite) => {
userSettingsSuite
.setUrl('/settings')
.before((actions, find) => {
// This will be called before every nested suite begins unless that suite
// also defines a `.before()`
actions.waitForElementToShow('.settings-container', TIMEOUTS.NORMAL);
})
.capture('user settings');
});
// Suite just to log out after suites have run
gemini.suite('Log out', (logoutSuite) => {
logoutSuite
.setUrl('/logout')
.ignoreElements('.ascribe-body')
.capture('logout', (actions, find) => {
actions.waitForElementToShow('.ascribe-login-wrapper', TIMEOUTS.LONG);
});
});
});

View File

@ -0,0 +1,148 @@
'use strict';
const gemini = require('gemini');
const environment = require('../environment');
const MAIN_USER = environment.MAIN_USER;
const TIMEOUTS = environment.TIMEOUTS;
/**
* Basic suite of tests against routes that do not require the user to be authenticated.
*/
gemini.suite('Basic', (suite) => {
suite
.setUrl('/login')
.setCaptureElements('.ascribe-body')
.before((actions, find) => {
// This will be called before every nested suite begins unless that suite
// also defines a `.before()`
// FIXME: use a more generic class for this, like just '.ascribe-app'
actions.waitForElementToShow('.ascribe-default-app', TIMEOUTS.NORMAL);
});
gemini.suite('Header-desktop', (headerSuite) => {
headerSuite
.setCaptureElements('nav.navbar .container')
.skip(/Mobile/)
.capture('desktop header', (actions, find) => {
actions.waitForElementToShow('nav.navbar .container', TIMEOUTS.NORMAL);
})
.capture('hover on active item', (actions, find) => {
const activeItem = find('nav.navbar li.active');
actions.mouseMove(activeItem);
})
.capture('hover on inactive item', (actions, find) => {
const inactiveItem = find('nav.navbar li:not(.active)');
actions.mouseMove(inactiveItem);
});
});
// Test for the collapsed header in mobile
gemini.suite('Header-mobile', (headerMobileSuite) => {
headerMobileSuite
.setCaptureElements('nav.navbar .container')
.skip(/Desktop/)
.capture('mobile header', (actions, find) => {
actions.waitForElementToShow('nav.navbar .container', TIMEOUTS.NORMAL);
})
.capture('expanded mobile header', (actions, find) => {
actions.click(find('nav.navbar .navbar-toggle'));
// Wait for the header to expand
actions.wait(500);
})
.capture('hover on expanded mobile header item', (actions, find) => {
actions.mouseMove(find('nav.navbar li'));
});
});
gemini.suite('Footer', (footerSuite) => {
footerSuite
.setCaptureElements('.ascribe-footer')
.capture('footer', (actions, find) => {
actions.waitForElementToShow('.ascribe-footer', TIMEOUTS.NORMAL);
})
.capture('hover on footer item', (actions, find) => {
const footerItem = find('.ascribe-footer a:not(.social)');
actions.mouseMove(footerItem);
})
.capture('hover on footer social item', (actions, find) => {
const footerSocialItem = find('.ascribe-footer a.social')
actions.mouseMove(footerSocialItem);
});
});
gemini.suite('Login', (loginSuite) => {
loginSuite
.capture('login', (actions, find) => {
actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL);
})
.capture('hover on login submit', (actions, find) => {
actions.mouseMove(find('.ascribe-form button[type=submit]'));
})
.capture('hover on sign up link', (actions, find) => {
actions.mouseMove(find('.ascribe-login-text a[href="/signup"]'));
})
.capture('login form filled with focus', (actions, find) => {
const emailInput = find('.ascribe-form input[name=email]');
// Remove hover from sign up link
actions.click(emailInput);
actions.sendKeys(emailInput, MAIN_USER.email);
actions.sendKeys(find('.ascribe-form input[name=password]'), MAIN_USER.password);
})
.capture('login form filled', (actions, find) => {
actions.click(find('.ascribe-form-header'));
});
});
gemini.suite('Sign up', (signUpSuite) => {
signUpSuite
.setUrl('/signup')
.capture('sign up', (actions, find) => {
actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL);
})
.capture('sign up form filled with focus', (actions, find) => {
actions.sendKeys(find('.ascribe-form input[name=email]'), MAIN_USER.email);
actions.sendKeys(find('.ascribe-form input[name=password]'), MAIN_USER.password);
actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), MAIN_USER.password);
})
.capture('sign up form filled with check', (actions, find) => {
actions.click(find('.ascribe-form input[type="checkbox"] ~ .checkbox'));
});
});
gemini.suite('Password reset', (passwordResetSuite) => {
passwordResetSuite
.setUrl('/password_reset')
.capture('password reset', (actions, find) => {
actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL);
})
.capture('password reset form filled with focus', (actions, find) => {
actions.sendKeys(find('.ascribe-form input[name="email"]'), MAIN_USER.email);
})
.capture('password reset form filled', (actions, find) => {
actions.click(find('.ascribe-form-header'));
});
});
gemini.suite('Coa verify', (coaVerifySuite) => {
coaVerifySuite
.setUrl('/coa_verify')
.capture('coa verify', (actions, find) => {
actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL);
})
.capture('coa verify form filled with focus', (actions, find) => {
actions.sendKeys(find('.ascribe-form input[name="message"]'), 'sample text');
actions.sendKeys(find('.ascribe-form .ascribe-property-wrapper:nth-of-type(2) textarea'), 'sample signature');
})
.capture('coa verify form filled', (actions, find) => {
actions.click(find('.ascribe-login-header'));
});
});
gemini.suite('Not found', (notFoundSuite) => {
notFoundSuite
.setUrl('/not_found_page')
.capture('not found page');
});
});

View File

@ -0,0 +1,134 @@
'use strict';
const gemini = require('gemini');
const environment = require('../environment');
const MAIN_USER = environment.MAIN_USER;
const TIMEOUTS = environment.TIMEOUTS;
const pieceUrl = `/pieces/${environment.MAIN_PIECE_ID}`;
const editionUrl = `/editions/${environment.MAIN_EDITION_ID}`;
/**
* Suite of tests against the piece and edition routes.
* Tests include accessing the piece / edition as the owner or as another user
* (we can just use an anonymous user in this case).
*/
gemini.suite('Work detail', (suite) => {
suite
.setCaptureElements('.ascribe-body')
.before((actions, find) => {
// This will be called before every nested suite begins unless that suite
// also defines a `.before()`
// FIXME: use a more generic class for this, like just '.app',
// when we can use this file with the whitelabels
actions.waitForElementToShow('.ascribe-default-app', TIMEOUTS.NORMAL);
// Wait for the social media buttons to appear
actions.waitForElementToShow('.ascribe-social-button-list .fb-share-button iframe', TIMEOUTS.SUPER_DUPER_EXTRA_LONG);
actions.waitForElementToShow('.ascribe-social-button-list .twitter-share-button', TIMEOUTS.SUPER_DUPER_EXTRA_LONG);
actions.waitForElementToShow('.ascribe-media-player', TIMEOUTS.LONG);
});
gemini.suite('Basic piece', (basicPieceSuite) => {
basicPieceSuite
.setUrl(pieceUrl)
.capture('basic piece')
gemini.suite('Shmui', (shmuiSuite) => {
shmuiSuite.
setCaptureElements('.shmui-wrap')
.capture('shmui', (actions, find) => {
actions.click(find('.ascribe-media-player'));
actions.waitForElementToShow('.shmui-wrap:not(.loading)', TIMEOUTS.SUPER_DUPER_EXTRA_LONG);
// Wait for the transition to end
actions.wait(1000);
});
});
});
gemini.suite('Basic edition', (basicEditionSuite) => {
basicEditionSuite
.setUrl(editionUrl)
.capture('basic edition');
});
// Suite just to log us in before any other suites run
gemini.suite('Login', (loginSuite) => {
loginSuite
.setUrl('/login')
.ignoreElements('.ascribe-body')
.before((actions, find) => {
actions.waitForElementToShow('.ascribe-default-app', TIMEOUTS.NORMAL);
})
.capture('logged in', (actions, find) => {
actions.sendKeys(find('.ascribe-login-wrapper input[name=email]'), MAIN_USER.email);
actions.sendKeys(find('.ascribe-login-wrapper input[name=password]'), MAIN_USER.password);
actions.click(find('.ascribe-login-wrapper button[type=submit]'));
actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', TIMEOUTS.NORMAL);
});
});
gemini.suite('Authorized piece', (authorizedPieceSuite) => {
authorizedPieceSuite
.setUrl(pieceUrl)
.capture('authorized piece');
});
gemini.suite('Authorized edition', (authorizedEditionSuite) => {
authorizedEditionSuite
.setUrl(editionUrl)
.capture('authorized edition')
});
gemini.suite('Detail action buttons', (detailActionButtonSuite) => {
detailActionButtonSuite
.setUrl(editionUrl)
.capture('hover on action button', (actions, find) => {
actions.mouseMove(find('.ascribe-detail-property .ascribe-button-list button.btn-default'));
})
.capture('hover on delete button', (actions, find) => {
actions.mouseMove(find('.ascribe-detail-property .ascribe-button-list button.btn-tertiary'));
})
.capture('hover on info button', (actions, find) => {
actions.mouseMove(find('.ascribe-detail-property .ascribe-button-list button.glyphicon-question-sign'));
})
.capture('expand info text', (actions, find) => {
actions.click(find('.ascribe-detail-property .ascribe-button-list button.glyphicon-question-sign'));
});
});
gemini.suite('Action form modal', (actionFormModalSuite) => {
actionFormModalSuite
.setUrl(editionUrl)
.setCaptureElements('.modal-dialog')
.capture('open email form', (actions, find) => {
// Add class names to make the action buttons easier to select
actions.executeJS(function (window) {
var actionButtons = window.document.querySelectorAll('.ascribe-detail-property .ascribe-button-list button.btn-default');
for (var ii = 0; ii < actionButtons.length; ++ii) {
if (actionButtons[ii].textContent) {
actionButtons[ii].className += ' ascribe-action-button-' + actionButtons[ii].textContent.toLowerCase();
}
}
});
actions.click(find('.ascribe-detail-property .ascribe-button-list button.ascribe-action-button-email'));
// Wait for transition
actions.wait(1000);
});
});
// Suite just to log out after suites have run
gemini.suite('Log out', (logoutSuite) => {
logoutSuite
.setUrl('/logout')
.ignoreElements('.ascribe-body')
.before((actions, find) => {
actions.waitForElementToShow('.ascribe-default-app', TIMEOUTS.NORMAL);
})
.capture('logout', (actions, find) => {
actions.waitForElementToShow('.ascribe-login-wrapper', TIMEOUTS.LONG);
});
});
});

View File

@ -0,0 +1,29 @@
'use strict';
const gemini = require('gemini');
const environment = require('../../environment');
const TIMEOUTS = environment.TIMEOUTS;
/**
* Suite of tests against 23vivi specific routes
*/
gemini.suite('23vivi', (suite) => {
suite
//TODO: maybe this should be changed to .ascribe-body once the PR that does this is merged
.setCaptureElements('.ascribe-wallet-app')
.before((actions, find) => {
// This will be called before every nested suite begins
actions.waitForElementToShow('.ascribe-wallet-app', TIMEOUTS.NORMAL);
});
gemini.suite('Landing', (landingSuite) => {
landingSuite
.setUrl('/')
.capture('landing', (actions, find) => {
// Wait for the logo to appear
actions.waitForElementToShow('.vivi23-landing--header-logo', TIMEOUTS.LONG);
});
});
// TODO: add more tests for market specific pages after authentication
});

View File

@ -0,0 +1,30 @@
'use strict';
const gemini = require('gemini');
const environment = require('../../environment');
const TIMEOUTS = environment.TIMEOUTS;
/**
* Suite of tests against Cyland specific routes
*/
gemini.suite('Cyland', (suite) => {
suite
//TODO: maybe this should be changed to .ascribe-body once the PR that does this is merged
.setCaptureElements('.ascribe-wallet-app')
.before((actions, find) => {
// This will be called before every nested suite begins
actions.waitForElementToShow('.ascribe-wallet-app', TIMEOUTS.NORMAL);
});
gemini.suite('Landing', (landingSuite) => {
landingSuite
.setUrl('/')
// Ignore Cyland's logo as it's a gif
.ignoreElements('.cyland-landing img')
.capture('landing', (actions, find) => {
actions.waitForElementToShow('.cyland-landing img', TIMEOUTS.LONG);
});
});
// TODO: add more tests for cyland specific pages after authentication
});

View File

@ -0,0 +1,98 @@
'use strict';
const gemini = require('gemini');
const environment = require('../../environment');
const MAIN_USER = environment.MAIN_USER;
const TIMEOUTS = environment.TIMEOUTS;
/**
* Suite of tests against Cyland specific routes
*/
gemini.suite('Ikonotv', (suite) => {
suite
//TODO: maybe this should be changed to .ascribe-body once the PR that does this is merged
.setCaptureElements('.ascribe-wallet-app')
.before((actions, find) => {
// This will be called before every nested suite begins
actions.waitForElementToShow('.ascribe-wallet-app', TIMEOUTS.NORMAL);
});
gemini.suite('Landing', (landingSuite) => {
landingSuite
.setUrl('/')
// Gemini complains if we try to capture the entire app for Ikonotv's landing page for some reason
.setCaptureElements('.ikonotv-landing')
.setTolerance(5)
.capture('landing', (actions, find) => {
// Stop background animation
actions.executeJS(function (window) {
var landingBackground = window.document.querySelector('.client--ikonotv .route--landing');
landingBackground.style.animation = 'none';
landingBackground.style.webkitAnimation = 'none';
});
// Wait for logo to appear
actions.waitForElementToShow('.ikonotv-landing header img', TIMEOUTS.LONG);
});
});
// Ikono needs its own set of tests for some pre-authorization pages to wait for
// its logo to appear
gemini.suite('Ikonotv basic', (suite) => {
suite
.setCaptureElements('.ascribe-wallet-app')
.before((actions, find) => {
// This will be called before every nested suite begins unless that suite
// also defines a `.before()`
// FIXME: use a more generic class for this, like just '.app',
// when we can use this file with the whitelabels
actions.waitForElementToShow('.ascribe-wallet-app', TIMEOUTS.NORMAL);
// Wait for the forms to appear
actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL);
// Just use a dumb wait because the logo is set as a background image
actions.wait(TIMEOUTS.SHORT);
});
gemini.suite('Login', (loginSuite) => {
loginSuite
.setUrl('/login')
.capture('login')
.capture('hover on login submit', (actions, find) => {
actions.mouseMove(find('.ascribe-form button[type=submit]'));
})
.capture('hover on sign up link', (actions, find) => {
actions.mouseMove(find('.ascribe-login-text a[href="/signup"]'));
})
.capture('login form filled with focus', (actions, find) => {
const emailInput = find('.ascribe-form input[name=email]');
// Remove hover from sign up link
actions.click(emailInput);
actions.sendKeys(emailInput, MAIN_USER.email);
actions.sendKeys(find('.ascribe-form input[name=password]'), MAIN_USER.password);
})
.capture('login form filled', (actions, find) => {
actions.click(find('.ascribe-form-header'));
});
});
gemini.suite('Sign up', (signUpSuite) => {
signUpSuite
.setUrl('/signup')
.capture('sign up')
.capture('sign up form filled with focus', (actions, find) => {
actions.sendKeys(find('.ascribe-form input[name=email]'), MAIN_USER.email);
actions.sendKeys(find('.ascribe-form input[name=password]'), MAIN_USER.password);
actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), MAIN_USER.password);
})
.capture('sign up form filled with check', (actions, find) => {
actions.click(find('.ascribe-form input[type="checkbox"] ~ .checkbox'));
});
});
});
// TODO: add more tests for ikonotv specific pages after authentication
});

View File

@ -0,0 +1,29 @@
'use strict';
const gemini = require('gemini');
const environment = require('../../environment');
const TIMEOUTS = environment.TIMEOUTS;
/**
* Suite of tests against lumenus specific routes
*/
gemini.suite('Lumenus', (suite) => {
suite
//TODO: maybe this should be changed to .ascribe-body once the PR that does this is merged
.setCaptureElements('.ascribe-wallet-app')
.before((actions, find) => {
// This will be called before every nested suite begins
actions.waitForElementToShow('.ascribe-wallet-app', TIMEOUTS.NORMAL);
});
gemini.suite('Landing', (landingSuite) => {
landingSuite
.setUrl('/')
.capture('landing', (actions, find) => {
// Wait for the logo to appear
actions.waitForElementToShow('.wp-landing-wrapper img', TIMEOUTS.LONG);
});
});
// TODO: add more tests for market specific pages after authentication
});

View File

@ -0,0 +1,115 @@
'use strict';
const gemini = require('gemini');
const environment = require('../../environment');
const MAIN_USER = environment.MAIN_USER;
const TIMEOUTS = environment.TIMEOUTS;
/**
* Basic suite of tests against whitelabel routes that do not require authentication.
*/
gemini.suite('Whitelabel basic', (suite) => {
suite
.setCaptureElements('.ascribe-wallet-app > .container')
.before((actions, find) => {
// This will be called before every nested suite begins unless that suite
// also defines a `.before()`
// FIXME: use a more generic class for this, like just '.ascribe-app'
actions.waitForElementToShow('.ascribe-wallet-app', TIMEOUTS.NORMAL);
// Use a dumb wait in case we're still waiting for other assets, like fonts, to load
actions.wait(1000);
});
gemini.suite('Login', (loginSuite) => {
loginSuite
.setUrl('/login')
// See Ikono
.skip(/Ikono/)
.capture('login', (actions, find) => {
actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL);
// For some reason, the screenshots seem to keep catching the whitelabel login form
// on a refresh and without fonts loaded (maybe because they're the first tests run
// and the cache isn't hot yet?).
// Let's wait a bit and hope they load.
actions.wait(TIMEOUTS.SHORT);
})
.capture('hover on login submit', (actions, find) => {
actions.mouseMove(find('.ascribe-form button[type=submit]'));
})
.capture('hover on sign up link', (actions, find) => {
actions.mouseMove(find('.ascribe-login-text a[href="/signup"]'));
})
.capture('login form filled with focus', (actions, find) => {
const emailInput = find('.ascribe-form input[name=email]');
// Remove hover from sign up link
actions.click(emailInput);
actions.sendKeys(emailInput, MAIN_USER.email);
actions.sendKeys(find('.ascribe-form input[name=password]'), MAIN_USER.password);
})
.capture('login form filled', (actions, find) => {
actions.click(find('.ascribe-form-header'));
});
});
gemini.suite('Sign up', (signUpSuite) => {
signUpSuite
.setUrl('/signup')
// See Ikono
.skip(/Ikono/)
.capture('sign up', (actions, find) => {
actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL);
// Wait in case the form reloads due to other assets loading
actions.wait(500);
})
.capture('sign up form filled with focus', (actions, find) => {
actions.sendKeys(find('.ascribe-form input[name=email]'), MAIN_USER.email);
actions.sendKeys(find('.ascribe-form input[name=password]'), MAIN_USER.password);
actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), MAIN_USER.password);
})
.capture('sign up form filled with check', (actions, find) => {
actions.click(find('.ascribe-form input[type="checkbox"] ~ .checkbox'));
});
});
gemini.suite('Password reset', (passwordResetSuite) => {
passwordResetSuite
.setUrl('/password_reset')
.capture('password reset', (actions, find) => {
actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL);
// Wait in case the form reloads due to other assets loading
actions.wait(500);
})
.capture('password reset form filled with focus', (actions, find) => {
actions.sendKeys(find('.ascribe-form input[name="email"]'), MAIN_USER.email);
})
.capture('password reset form filled', (actions, find) => {
actions.click(find('.ascribe-form-header'));
});
});
gemini.suite('Coa verify', (coaVerifySuite) => {
coaVerifySuite
.setUrl('/coa_verify')
.capture('coa verify', (actions, find) => {
actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL);
// Wait in case the form reloads due to other assets loading
actions.wait(500);
})
.capture('coa verify form filled with focus', (actions, find) => {
actions.sendKeys(find('.ascribe-form input[name="message"]'), 'sample text');
actions.sendKeys(find('.ascribe-form .ascribe-property-wrapper:nth-of-type(2) textarea'), 'sample signature');
})
.capture('coa verify form filled', (actions, find) => {
actions.click(find('.ascribe-login-header'));
});
});
gemini.suite('Not found', (notFoundSuite) => {
notFoundSuite
.setUrl('/not_found_page')
.capture('not found page');
});
});

View File

@ -5,7 +5,7 @@ const wd = require('wd');
const asserters = wd.asserters; // Commonly used asserters for async waits in the browser
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const config = require('./config.js');
const config = require('../config.js');
chai.use(chaiAsPromised);
chai.should();