From e24cca1c1232c36bbe8bafcb37d3d2babdbff87f Mon Sep 17 00:00:00 2001 From: vrde Date: Thu, 17 Dec 2015 19:23:56 +0100 Subject: [PATCH 01/14] WIP on documentation and sample test --- .eslintrc | 3 ++- package.json | 6 +++++- tests/README.md | 52 +++++++++++++++++++++++++++++++++++++++++++++ tests/test-login.js | 34 +++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 tests/README.md create mode 100644 tests/test-login.js diff --git a/.eslintrc b/.eslintrc index d41c0a2a..0ce24daa 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,7 +2,8 @@ "parser": "babel-eslint", "env": { "browser": true, - "es6": true + "es6": true, + "mocha": true }, "rules": { "new-cap": [2, {newIsCap: true, capIsNew: false}], diff --git a/package.json b/package.json index be5c1202..efe9648e 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,11 @@ "devDependencies": { "babel-eslint": "^3.1.11", "babel-jest": "^5.2.0", - "jest-cli": "^0.4.0" + "chai": "^3.4.1", + "chai-as-promised": "^5.1.0", + "jest-cli": "^0.4.0", + "mocha": "^2.3.4", + "wd": "^0.4.0" }, "dependencies": { "alt": "^0.16.5", diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..dfa6f8cf --- /dev/null +++ b/tests/README.md @@ -0,0 +1,52 @@ +# Welcome to our test suite, let me be your guide + +Dear reader, first of all thanks for taking your time reading this document. +The purpose of this document is to give you an overview on what we want to test +and how we are doing it. + + +# How it works (bird's-eye view) +You will notice that the setup is a bit convoluted. This section will explain +you why. Testing single functions in JavaScript is not that hard (if you don't +need to interact with the DOM), and can be easily achieved using frameworks +like [Mocha](https://mochajs.org/). Integration and cross browser testing is, +on the other side, a huge [PITA](https://saucelabs.com/selenium/selenium-grid). +Moreover, "browser testing" includes also "mobile browser testing". Moreover, +the same browser (type and version) can behave in a different way on different +operating systems. + +To achieve that you can have your own cluster of machines with different +operating systems and browsers or, if you don't want to spend the rest of your +life configuring an average of 100 browsers for each different operating +system, you can pay someone else to do that. + +We decided to use [saucelabs](https://saucelabs.com/) cloud (they support [over +700 combinations](https://saucelabs.com/platforms/) of operating systems and +browsers) to run our tests. + + +## Components and tools + +The components involved are: + - **[Selenium WebDriver](https://www.npmjs.com/package/wd)**: it's a library + that can control a browser. You can use the **WebDriver** to load new URLs, + click around, fill out forms, submit forms etc. It's basically a way to + control remotely a browser. There are other implementations in Python, PHP, + Java, etc. + + - **[Selenium Grid](https://github.com/SeleniumHQ/selenium/wiki/Grid2)**: it's + the controller for the cluster of machines/devices that can run browsers. + Selenium Grid is able to scale by distributing tests on several machines, + manage multiple environments from a central point, making it easy to run the + tests against a vast combination of browsers / OS, minimize the maintenance + time for the grid by allowing you to implement custom hooks to leverage + virtual infrastructure for instance. + + - **[Saucelabs](https://saucelabs.com/)**: a private company providing a + cluster to run your tests on over 700 combinations of browsers/operating + systems. (They do other things, check out their websites). + + +## Anatomy of a test +A test is a `.js` file. We use [Mocha](https://mochajs.org/) and [Should](https://shouldjs.github.io/). + diff --git a/tests/test-login.js b/tests/test-login.js new file mode 100644 index 00000000..07c3d1d7 --- /dev/null +++ b/tests/test-login.js @@ -0,0 +1,34 @@ +'use strict'; + +const wd = require('wd'); +const chai = require("chai"); +const chaiAsPromised = require("chai-as-promised"); +chai.use(chaiAsPromised); +chai.should(); + + +describe('Login logs users in', function() { + let browser; + + before(function() { + browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80, + process.env.ONION_SAUCELABS_USER || 'ascribe', + process.env.ONION_SAUCELABS_APIKEY || 'b072b4f2-6302-42f6-a25d-47162666ca66') + + return browser.init({ browserName: 'chrome' }); + }); + + beforeEach(function() { + return browser.get('http://www.ascribe.ninja/app/login'); + }); + + after(function() { + return browser.quit(); + }); + + it('should contain "Log in" in the title', function() { + return browser.title().should.become('Log in'); + }); + +}); + From 9a7dbb55ead7995611a29828aa600b2eac7d2bfb Mon Sep 17 00:00:00 2001 From: vrde Date: Fri, 18 Dec 2015 12:03:54 +0100 Subject: [PATCH 02/14] Add config --- .env-template | 2 ++ .eslintrc | 1 - .gitignore | 1 + package.json | 1 + tests/.eslintrc | 36 +++++++++++++++++++++++ tests/README.md | 69 +++++++++++++++++++++++++++++++++++++++++++-- tests/test-login.js | 12 ++++---- 7 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 .env-template create mode 100644 tests/.eslintrc diff --git a/.env-template b/.env-template new file mode 100644 index 00000000..593923ec --- /dev/null +++ b/.env-template @@ -0,0 +1,2 @@ +ONION_SAUCELABS_USER=ascribe +ONION_SAUCELABS_APIKEY= diff --git a/.eslintrc b/.eslintrc index 0ce24daa..5751f3ad 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,7 +3,6 @@ "env": { "browser": true, "es6": true, - "mocha": true }, "rules": { "new-cap": [2, {newIsCap: true, capIsNew: false}], diff --git a/.gitignore b/.gitignore index 30c9eae9..f5bf11e8 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ node_modules/* build .DS_Store +.env diff --git a/package.json b/package.json index efe9648e..b2bbd68c 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "babel-jest": "^5.2.0", "chai": "^3.4.1", "chai-as-promised": "^5.1.0", + "dotenv": "^1.2.0", "jest-cli": "^0.4.0", "mocha": "^2.3.4", "wd": "^0.4.0" diff --git a/tests/.eslintrc b/tests/.eslintrc new file mode 100644 index 00000000..64f8d90a --- /dev/null +++ b/tests/.eslintrc @@ -0,0 +1,36 @@ +{ + "parser": "babel-eslint", + "env": { + "mocha": true, + "node": true + }, + "rules": { + "new-cap": [2, {newIsCap: true, capIsNew: false}], + "quotes": [2, "single"], + "eol-last": [0], + "no-mixed-requires": [0], + "no-underscore-dangle": [0], + "global-strict": [2, "always"], + "no-trailing-spaces": [2, { skipBlankLines: true }], + "no-console": 0, + "camelcase": [2, {"properties": "never"}], + }, + "globals": {}, + "plugins": [], + "ecmaFeatures": { + "modules": 1, + "arrowFunctions", + "classes": 1, + "blockBindings": 1, + "defaultParams": 1, + "destructuring": 1, + "objectLiteralComputedProperties": 1, + "objectLiteralDuplicateProperties": 0, + "objectLiteralShorthandMethods": 1, + "objectLiteralShorthandProperties": 1, + "restParams": 1, + "spread": 1, + "superInFunctions": 1, + "templateStrings": 1 + } +} diff --git a/tests/README.md b/tests/README.md index dfa6f8cf..458ba4cc 100644 --- a/tests/README.md +++ b/tests/README.md @@ -6,6 +6,7 @@ and how we are doing it. # How it works (bird's-eye view) + You will notice that the setup is a bit convoluted. This section will explain you why. Testing single functions in JavaScript is not that hard (if you don't need to interact with the DOM), and can be easily achieved using frameworks @@ -27,12 +28,17 @@ browsers) to run our tests. ## Components and tools +Right now we are just running the test locally, so no Continuous Integration™. + The components involved are: - **[Selenium WebDriver](https://www.npmjs.com/package/wd)**: it's a library that can control a browser. You can use the **WebDriver** to load new URLs, click around, fill out forms, submit forms etc. It's basically a way to control remotely a browser. There are other implementations in Python, PHP, - Java, etc. + Java, etc. Also, a **WebDriver** can be initialized with a list of [desired + capabilities](https://code.google.com/p/selenium/wiki/DesiredCapabilities) + describing which features (like the platform, browser name and version) you + want to use to run your tests. - **[Selenium Grid](https://github.com/SeleniumHQ/selenium/wiki/Grid2)**: it's the controller for the cluster of machines/devices that can run browsers. @@ -46,7 +52,66 @@ The components involved are: cluster to run your tests on over 700 combinations of browsers/operating systems. (They do other things, check out their websites). + - **[SauceConnect](https://wiki.saucelabs.com/display/DOCS/Setting+Up+Sauce+Connect)**: + it allows Saucelabs to connect to your `localhost` to test the app. (There + is also a [Node.js wrapper](https://www.npmjs.com/package/sauce-connect), so + you can use it programmatically within your code for tests). + + +On the JavaScript side, we use: + - [Mocha](https://mochajs.org/): a test framework running on Node.js. + + - [chai](http://chaijs.com/): a BDD/TDD assertion library for node that can be + paired with any javascript testing framework. + + - [chaiAsPromised](https://github.com/domenic/chai-as-promised/): an extension + for Chai with a fluent language for asserting facts about promises. The + extension is actually quite cool, we can do assertions on promises without + writing callbacks but just chaining operators. Check out their `README` on + GitHub to see an example. + + - [dotenv](https://github.com/motdotla/dotenv): a super nice package to loads + environment variables from `.env` into `process.env`. + ## Anatomy of a test -A test is a `.js` file. We use [Mocha](https://mochajs.org/) and [Should](https://shouldjs.github.io/). +```javascript +'use strict'; + +require('dotenv').load(); + +const wd = require('wd'); +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +chai.use(chaiAsPromised); +chai.should(); +``` + + +```javascript +describe('Login logs users in', function() { + let browser; + + before(function() { + browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80, + process.env.ONION_SAUCELABS_USER || 'ascribe', + process.env.ONION_SAUCELABS_APIKEY || 'b072b4f2-6302-42f6-a25d-47162666ca66'); + + return browser.init({ browserName: 'chrome' }); + }); + + beforeEach(function() { + return browser.get('http://www.ascribe.ninja/app/login'); + }); + + after(function() { + return browser.quit(); + }); + + it('should contain "Log in" in the title', function() { + return browser.title().should.become('Log in'); + }); + +}); +``` diff --git a/tests/test-login.js b/tests/test-login.js index 07c3d1d7..249a26c2 100644 --- a/tests/test-login.js +++ b/tests/test-login.js @@ -1,8 +1,10 @@ 'use strict'; +require('dotenv').load(); + const wd = require('wd'); -const chai = require("chai"); -const chaiAsPromised = require("chai-as-promised"); +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); chai.use(chaiAsPromised); chai.should(); @@ -12,10 +14,10 @@ describe('Login logs users in', function() { before(function() { browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80, - process.env.ONION_SAUCELABS_USER || 'ascribe', - process.env.ONION_SAUCELABS_APIKEY || 'b072b4f2-6302-42f6-a25d-47162666ca66') + process.env.ONION_SAUCELABS_USER, + process.env.ONION_SAUCELABS_APIKEY); - return browser.init({ browserName: 'chrome' }); + return browser.init({ browserName: 'chrome' }); }); beforeEach(function() { From e631f3374f0d33387faf6cd15af2059524293292 Mon Sep 17 00:00:00 2001 From: vrde Date: Fri, 18 Dec 2015 12:05:49 +0100 Subject: [PATCH 03/14] update code in readme --- tests/README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/README.md b/tests/README.md index 458ba4cc..b95df74c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -95,10 +95,9 @@ describe('Login logs users in', function() { before(function() { browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80, - process.env.ONION_SAUCELABS_USER || 'ascribe', - process.env.ONION_SAUCELABS_APIKEY || 'b072b4f2-6302-42f6-a25d-47162666ca66'); - - return browser.init({ browserName: 'chrome' }); + process.env.ONION_SAUCELABS_USER, + process.env.ONION_SAUCELABS_APIKEY, + return browser.init({ browserName: 'chrome' }); }); beforeEach(function() { From e8bdeffad868fc2aae6e99bbe1243695d7ec7e32 Mon Sep 17 00:00:00 2001 From: vrde Date: Sat, 19 Dec 2015 17:40:53 +0100 Subject: [PATCH 04/14] Refactor, add setup.py --- .env-template | 4 +- package.json | 2 + {tests => test}/.eslintrc | 0 {tests => test}/README.md | 90 ++++++++++++++++++++++++++++++++--- test/setup.js | 33 +++++++++++++ {tests => test}/test-login.js | 8 +--- 6 files changed, 122 insertions(+), 15 deletions(-) rename {tests => test}/.eslintrc (100%) rename {tests => test}/README.md (55%) create mode 100644 test/setup.js rename {tests => test}/test-login.js (78%) diff --git a/.env-template b/.env-template index 593923ec..a836e649 100644 --- a/.env-template +++ b/.env-template @@ -1,2 +1,2 @@ -ONION_SAUCELABS_USER=ascribe -ONION_SAUCELABS_APIKEY= +SAUCE_USERNAME=ascribe +SAUCE_ACCESS_KEY= diff --git a/package.json b/package.json index b2bbd68c..ba8bfc45 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "scripts": { "lint": "eslint ./js", + "preinstall": "export SAUCE_CONNECT_DOWNLOAD_ON_INSTALL=true", "postinstall": "npm run build", "build": "gulp build --production", "start": "node server.js" @@ -40,6 +41,7 @@ "dotenv": "^1.2.0", "jest-cli": "^0.4.0", "mocha": "^2.3.4", + "sauce-connect-launcher": "^0.13.0", "wd": "^0.4.0" }, "dependencies": { diff --git a/tests/.eslintrc b/test/.eslintrc similarity index 100% rename from tests/.eslintrc rename to test/.eslintrc diff --git a/tests/README.md b/test/README.md similarity index 55% rename from tests/README.md rename to test/README.md index b95df74c..2c8d6d46 100644 --- a/tests/README.md +++ b/test/README.md @@ -34,7 +34,11 @@ The components involved are: - **[Selenium WebDriver](https://www.npmjs.com/package/wd)**: it's a library that can control a browser. You can use the **WebDriver** to load new URLs, click around, fill out forms, submit forms etc. It's basically a way to - control remotely a browser. There are other implementations in Python, PHP, + control remotely a browser. The protocol (language agnostic) is called + [JsonWire](https://code.google.com/p/selenium/wiki/JsonWireProtocol), `wd` + wraps it and gives you a nice + [API](https://github.com/admc/wd/blob/master/doc/jsonwire-full-mapping.md) + you can use in JavaScript. There are other implementations in Python, PHP, Java, etc. Also, a **WebDriver** can be initialized with a list of [desired capabilities](https://code.google.com/p/selenium/wiki/DesiredCapabilities) describing which features (like the platform, browser name and version) you @@ -53,9 +57,12 @@ The components involved are: systems. (They do other things, check out their websites). - **[SauceConnect](https://wiki.saucelabs.com/display/DOCS/Setting+Up+Sauce+Connect)**: - it allows Saucelabs to connect to your `localhost` to test the app. (There - is also a [Node.js wrapper](https://www.npmjs.com/package/sauce-connect), so - you can use it programmatically within your code for tests). + is a Java software by Saucelabs to connect to your `localhost` to test the + application. There is also a Node.js wrapper + [sauce-connect-launcher](https://www.npmjs.com/package/sauce-connect-launcher), + so you can use it programmatically within your code for tests. Please note + that this module is just a wrapper around the actual software. Running `npm + install` should install the additional Java software as well. On the JavaScript side, we use: @@ -74,8 +81,49 @@ On the JavaScript side, we use: environment variables from `.env` into `process.env`. +## How to set up your `.env` config file +In the root of this repository there is a file called `.env-template`. Create a +copy and call it `.env`. This file will store some values we need to connect to +Saucelabs. + +There are two values to be set: + - `SAUCE_ACCESS_KEY` + - `SAUCE_USERNAME` + +The two keys are the [default +ones](https://github.com/admc/wd#environment-variables-for-saucelabs) used by +many products related to Saucelabs. This allow us to keep the configuration +fairly straightforward and simple. + +After logging in to https://saucelabs.com/, you can find your **api key** under +the **My Account**. Copy paste the value in your `.env` file. + + ## Anatomy of a test +First, you need to learn how [Mocha](https://mochajs.org/) works. Brew a coffee +(or tea, if coffee is not your cup of tea), sit down and read the docs. + +Done? Great, let's move on and analyze how a test is written. + +From a very high level, the flow of a test is the following: + 1. load a page with a specific URL + 2. do something on the page (click a button, submit a form, etc.) + 3. maybe wait some seconds, or wait if something has changed + 4. check if the new page contains some text you expect to be there + +This is not set in stone, so go crazy if you want. But keep in mind that we +have a one page application, there might be some gotchas on how to wait for +stuff to happen. I suggest you to read the section [Wait for +something](https://github.com/admc/wd#waiting-for-something) to understand +better which tools you have to solve this problem. +Again, take a look to the [`wd` implementation of the JsonWire +protocol](https://github.com/admc/wd/blob/master/doc/jsonwire-full-mapping.md) +to know all the methods you can use to control the browser. + + +Import the libraries we need. + ```javascript 'use strict'; @@ -84,33 +132,61 @@ require('dotenv').load(); const wd = require('wd'); const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); +``` + + +Set up `chai` to use `chaiAsPromised`. + +```javascript chai.use(chaiAsPromised); chai.should(); ``` +`browser` is the main object to interact with Saucelab "real" browsers. We will +use this object a lot. It allow us to load pages, click around, check if a +specific text is present etc. ```javascript describe('Login logs users in', function() { let browser; +``` +Create the driver to control the browser. +```javascript before(function() { - browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80, - process.env.ONION_SAUCELABS_USER, - process.env.ONION_SAUCELABS_APIKEY, + browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80); return browser.init({ browserName: 'chrome' }); }); +``` +This function will be executed before each `it` function. Here we point the browser to a specific URL. + +```javascript beforeEach(function() { return browser.get('http://www.ascribe.ninja/app/login'); }); +``` +While this function will be executed after each `it` function. `quit` will destroy the browser session. + +```javascript after(function() { return browser.quit(); }); +``` +The actual test. We query the `browser` object to get the title of the page. +Note that `.title()` returns a `promise` **but**, since we are using +`chaiAsPromised`, we have some syntactic sugar to handle the promise in line, +without writing new functions. + +```javascript it('should contain "Log in" in the title', function() { return browser.title().should.become('Log in'); }); }); ``` + +## How to run the test suite + diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 00000000..0313a4ae --- /dev/null +++ b/test/setup.js @@ -0,0 +1,33 @@ +'use strict'; + +require('dotenv').load(); + +const sauceConnectLauncher = require('sauce-connect-launcher'); +let globalSauceProcess; + + +if (process.env.SAUCE_AUTO_CONNECT) { + before(function(done) { + // Creating the tunnel takes a bit of time. For this case we can safely disable it. + this.timeout(0); + + sauceConnectLauncher(function (err, sauceConnectProcess) { + if (err) { + console.error(err.message); + return; + } + globalSauceProcess = sauceConnectProcess; + done(); + }); + }); + + + after(function (done) { + // Creating the tunnel takes a bit of time. For this case we can safely disable it. + this.timeout(0); + + if (globalSauceProcess) { + globalSauceProcess.close(done); + } + }); +} diff --git a/tests/test-login.js b/test/test-login.js similarity index 78% rename from tests/test-login.js rename to test/test-login.js index 249a26c2..82bdad9d 100644 --- a/tests/test-login.js +++ b/test/test-login.js @@ -1,7 +1,5 @@ 'use strict'; -require('dotenv').load(); - const wd = require('wd'); const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); @@ -10,13 +8,11 @@ chai.should(); describe('Login logs users in', function() { + this.timeout(0); let browser; before(function() { - browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80, - process.env.ONION_SAUCELABS_USER, - process.env.ONION_SAUCELABS_APIKEY); - + browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80); return browser.init({ browserName: 'chrome' }); }); From 2e3e5d9a60b1e09718574a9e5adf9349337f7cb7 Mon Sep 17 00:00:00 2001 From: vrde Date: Sat, 19 Dec 2015 17:54:28 +0100 Subject: [PATCH 05/14] Small updates to the readme --- test/README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/README.md b/test/README.md index 2c8d6d46..0aab26bd 100644 --- a/test/README.md +++ b/test/README.md @@ -11,15 +11,16 @@ You will notice that the setup is a bit convoluted. This section will explain you why. Testing single functions in JavaScript is not that hard (if you don't need to interact with the DOM), and can be easily achieved using frameworks like [Mocha](https://mochajs.org/). Integration and cross browser testing is, -on the other side, a huge [PITA](https://saucelabs.com/selenium/selenium-grid). -Moreover, "browser testing" includes also "mobile browser testing". Moreover, -the same browser (type and version) can behave in a different way on different -operating systems. +on the other side, a huge PITA. Moreover, "browser testing" includes also +"mobile browser testing". On the top of that the same browser (type and +version) can behave in a different way on different operating systems. To achieve that you can have your own cluster of machines with different operating systems and browsers or, if you don't want to spend the rest of your life configuring an average of 100 browsers for each different operating -system, you can pay someone else to do that. +system, you can pay someone else to do that. Check out [this +article](https://saucelabs.com/selenium/selenium-grid) if you want to know why +using Selenium Grid is better than a DIY approach. We decided to use [saucelabs](https://saucelabs.com/) cloud (they support [over 700 combinations](https://saucelabs.com/platforms/) of operating systems and @@ -77,7 +78,7 @@ On the JavaScript side, we use: writing callbacks but just chaining operators. Check out their `README` on GitHub to see an example. - - [dotenv](https://github.com/motdotla/dotenv): a super nice package to loads + - [dotenv](https://github.com/motdotla/dotenv): a super nice package to load environment variables from `.env` into `process.env`. From a95a0fd2ea624ce5c96cdca20e2067fd2b4163e5 Mon Sep 17 00:00:00 2001 From: vrde Date: Fri, 22 Jan 2016 17:52:03 +0100 Subject: [PATCH 06/14] Add multi browser test --- test/README.md | 12 ++++++++++-- test/config.js | 16 ++++++++++++++++ test/test-login.js | 46 ++++++++++++++++++++++++++-------------------- 3 files changed, 52 insertions(+), 22 deletions(-) create mode 100644 test/config.js diff --git a/test/README.md b/test/README.md index 0aab26bd..10b5784c 100644 --- a/test/README.md +++ b/test/README.md @@ -160,7 +160,8 @@ Create the driver to control the browser. }); ``` -This function will be executed before each `it` function. Here we point the browser to a specific URL. +This function will be executed before each `it` function. Here we point the +browser to a specific URL. ```javascript beforeEach(function() { @@ -168,7 +169,8 @@ This function will be executed before each `it` function. Here we point the brow }); ``` -While this function will be executed after each `it` function. `quit` will destroy the browser session. +While this function will be executed after each `it` function. `quit` will +destroy the browser session. ```javascript after(function() { @@ -190,4 +192,10 @@ without writing new functions. ``` ## How to run the test suite +To run the tests, type: +```bash +$ mocha +``` +By default the test suite runs on `http://www.localhost.com:3000/`, if you +want to change the URL, change the `APP_URL` env variable. diff --git a/test/config.js b/test/config.js new file mode 100644 index 00000000..a1ac78db --- /dev/null +++ b/test/config.js @@ -0,0 +1,16 @@ +'use strict'; + + +// https://code.google.com/p/selenium/wiki/DesiredCapabilities +const BROWSERS = [ + 'chrome,47,WINDOWS', + 'chrome,46,WINDOWS', + 'firefox,43,MAC', + 'internet explorer,10,VISTA' +]; + +const APP_URL = process.env.APP_URL || 'http://www.localhost.com:3000'; + + +module.exports.BROWSERS = BROWSERS.map(x => x.split(',')); +module.exports.APP_URL = APP_URL; diff --git a/test/test-login.js b/test/test-login.js index 82bdad9d..0df913ca 100644 --- a/test/test-login.js +++ b/test/test-login.js @@ -3,30 +3,36 @@ const wd = require('wd'); const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); +const config = require('./config.js'); chai.use(chaiAsPromised); chai.should(); -describe('Login logs users in', function() { - this.timeout(0); - let browser; - before(function() { - browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80); - return browser.init({ browserName: 'chrome' }); +function testSuite(browserName, version, platform) { + describe(`[${browserName} ${version} ${platform}] Login logs users in`, function() { + // Set timeout to zero so Mocha won't time out. + this.timeout(0); + let browser; + + before(function() { + browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80); + return browser.init({ browserName, version, platform }); + }); + + beforeEach(function() { + return browser.get(config.APP_URL + '/login'); + }); + + after(function() { + return browser.quit(); + }); + + it('should contain "Log in" in the title', function() { + return browser.title().should.become('Log in'); + }); + }); +} - beforeEach(function() { - return browser.get('http://www.ascribe.ninja/app/login'); - }); - - after(function() { - return browser.quit(); - }); - - it('should contain "Log in" in the title', function() { - return browser.title().should.become('Log in'); - }); - -}); - +config.BROWSERS.map(x => testSuite(...x)); From 778c61bfdc1de5a929cb9aafb96418a70ce6bb94 Mon Sep 17 00:00:00 2001 From: vrde Date: Mon, 25 Jan 2016 11:05:01 +0100 Subject: [PATCH 07/14] Add utils to manage tunnel and stuff --- .env-template | 1 + package.json | 5 ++++- test/README.md | 17 +++++++++++++++++ test/config.js | 11 +++++++---- test/setup.js | 25 +++++++++++++++++++++---- test/test-login.js | 2 ++ test/tunnel.js | 23 +++++++++++++++++++++++ 7 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 test/tunnel.js diff --git a/.env-template b/.env-template index a836e649..8c4fe11c 100644 --- a/.env-template +++ b/.env-template @@ -1,2 +1,3 @@ SAUCE_USERNAME=ascribe SAUCE_ACCESS_KEY= +SAUCE_DEFAULT_URL= diff --git a/package.json b/package.json index deb70cca..cd1eae93 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "preinstall": "export SAUCE_CONNECT_DOWNLOAD_ON_INSTALL=true", "postinstall": "npm run build", "build": "gulp build --production", - "start": "node server.js" + "start": "node server.js", + "test": "mocha", + "tunnel": "node test/tunnel.js" }, "browser": { "fineUploader": "./js/components/ascribe_uploader/vendor/s3.fine-uploader.js" @@ -38,6 +40,7 @@ "babel-jest": "^5.2.0", "chai": "^3.4.1", "chai-as-promised": "^5.1.0", + "colors": "^1.1.2", "dotenv": "^1.2.0", "gulp-sass": "^2.1.1", "jest-cli": "^0.4.0", diff --git a/test/README.md b/test/README.md index 10b5784c..49d7718c 100644 --- a/test/README.md +++ b/test/README.md @@ -1,3 +1,13 @@ +# TL;DR +Copy the file `.env-template` in `.env` and fill up the missing keys. + +```bash +$ npm install +$ npm run tunnel +$ npm test && git commit +``` + + # Welcome to our test suite, let me be your guide Dear reader, first of all thanks for taking your time reading this document. @@ -199,3 +209,10 @@ $ mocha By default the test suite runs on `http://www.localhost.com:3000/`, if you want to change the URL, change the `APP_URL` env variable. + + +# How to have fun +Try this! +```bash +$ mocha -R nyan +``` diff --git a/test/config.js b/test/config.js index a1ac78db..aad28856 100644 --- a/test/config.js +++ b/test/config.js @@ -1,5 +1,7 @@ 'use strict'; +require('dotenv').load(); + // https://code.google.com/p/selenium/wiki/DesiredCapabilities const BROWSERS = [ @@ -9,8 +11,9 @@ const BROWSERS = [ 'internet explorer,10,VISTA' ]; -const APP_URL = process.env.APP_URL || 'http://www.localhost.com:3000'; - -module.exports.BROWSERS = BROWSERS.map(x => x.split(',')); -module.exports.APP_URL = APP_URL; +module.exports = { + BROWSERS: BROWSERS.map(x => x.split(',')), + APP_URL: process.env.SAUCE_DEFAULT_URL || 'http://www.localhost.com:3000', + TUNNEL_AUTO_CONNECT: process.env.SAUCE_AUTO_CONNECT +}; diff --git a/test/setup.js b/test/setup.js index 0313a4ae..0202ef29 100644 --- a/test/setup.js +++ b/test/setup.js @@ -1,14 +1,27 @@ 'use strict'; -require('dotenv').load(); - +const config = require('./config'); +const colors = require('colors'); const sauceConnectLauncher = require('sauce-connect-launcher'); + + let globalSauceProcess; +if (!process.env.SAUCE_USERNAME) { + console.log(colors.red('SAUCE_USERNAME is missing. Please check the README.md file.')); + process.exit(1); //eslint-disable-line no-process-exit +} -if (process.env.SAUCE_AUTO_CONNECT) { +if (!process.env.SAUCE_ACCESS_KEY2) { + console.log(colors.red('SAUCE_ACCESS_KEY is missing. Please check the README.md file.')); + process.exit(1); //eslint-disable-line no-process-exit +} + + +if (config.TUNNEL_AUTO_CONNECT) { before(function(done) { - // Creating the tunnel takes a bit of time. For this case we can safely disable it. + console.log(colors.yellow('Setting up tunnel from Saucelabs to your lovely computer, will take a while.')); + // Creating the tunnel takes a bit of time. For this case we can safely disable Mocha timeouts. this.timeout(0); sauceConnectLauncher(function (err, sauceConnectProcess) { @@ -30,4 +43,8 @@ if (process.env.SAUCE_AUTO_CONNECT) { globalSauceProcess.close(done); } }); +} else if (config.APP_URL.match(/localhost/)) { + console.log(colors.yellow(`You are running tests on ${config.APP_URL}, make sure you already have a tunnel running.`)); + console.log(colors.yellow('To create the tunnel, run:')); + console.log(colors.yellow(' $ node test/tunnel.js')); } diff --git a/test/test-login.js b/test/test-login.js index 0df913ca..38a1e377 100644 --- a/test/test-login.js +++ b/test/test-login.js @@ -16,6 +16,8 @@ function testSuite(browserName, version, platform) { let browser; before(function() { + // No need to inject `username` or `access_key`, by default the constructor + // looks up the values in `process.env.SAUCE_USERNAME` and `process.env.SAUCE_ACCESS_KEY` browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80); return browser.init({ browserName, version, platform }); }); diff --git a/test/tunnel.js b/test/tunnel.js new file mode 100644 index 00000000..2a7fa371 --- /dev/null +++ b/test/tunnel.js @@ -0,0 +1,23 @@ +'use strict'; + +const config = require('./config'); //eslint-disable-line no-unused-vars +const colors = require('colors'); +const sauceConnectLauncher = require('sauce-connect-launcher'); + + +function connect() { + console.log(colors.yellow('Setting up tunnel from Saucelabs to your lovely computer, will take a while.')); + // Creating the tunnel takes a bit of time. For this case we can safely disable Mocha timeouts. + + sauceConnectLauncher(function (err) { + if (err) { + console.error(err.message); + return; + } + console.log(colors.green('Connected! Keep this process running and execute your tests.')); + }); +} + +if (require.main === module) { + connect(); +} From 51d88a2ed23cae3ced68a6cb872486142219e286 Mon Sep 17 00:00:00 2001 From: vrde Date: Mon, 25 Jan 2016 11:24:35 +0100 Subject: [PATCH 08/14] Remove typo on env var name --- test/setup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/setup.js b/test/setup.js index 0202ef29..adb5937f 100644 --- a/test/setup.js +++ b/test/setup.js @@ -12,7 +12,7 @@ if (!process.env.SAUCE_USERNAME) { process.exit(1); //eslint-disable-line no-process-exit } -if (!process.env.SAUCE_ACCESS_KEY2) { +if (!process.env.SAUCE_ACCESS_KEY) { console.log(colors.red('SAUCE_ACCESS_KEY is missing. Please check the README.md file.')); process.exit(1); //eslint-disable-line no-process-exit } From c3c9c884211e61514d905eadf98175dc16e32f03 Mon Sep 17 00:00:00 2001 From: vrde Date: Mon, 25 Jan 2016 11:32:43 +0100 Subject: [PATCH 09/14] Remove env variable from config --- test/config.js | 3 +-- test/setup.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/test/config.js b/test/config.js index aad28856..400c6d90 100644 --- a/test/config.js +++ b/test/config.js @@ -14,6 +14,5 @@ const BROWSERS = [ module.exports = { BROWSERS: BROWSERS.map(x => x.split(',')), - APP_URL: process.env.SAUCE_DEFAULT_URL || 'http://www.localhost.com:3000', - TUNNEL_AUTO_CONNECT: process.env.SAUCE_AUTO_CONNECT + APP_URL: process.env.SAUCE_DEFAULT_URL || 'http://www.localhost.com:3000' }; diff --git a/test/setup.js b/test/setup.js index adb5937f..334423e7 100644 --- a/test/setup.js +++ b/test/setup.js @@ -18,7 +18,7 @@ if (!process.env.SAUCE_ACCESS_KEY) { } -if (config.TUNNEL_AUTO_CONNECT) { +if (process.env.SAUCE_AUTO_CONNECT) { before(function(done) { console.log(colors.yellow('Setting up tunnel from Saucelabs to your lovely computer, will take a while.')); // Creating the tunnel takes a bit of time. For this case we can safely disable Mocha timeouts. From a2ea9b4aa280bf7e5f3f5e890a1cc7395ad2358e Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 25 Jan 2016 17:31:09 +0100 Subject: [PATCH 10/14] Add todo to the test README and make slight changes --- test/README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/test/README.md b/test/README.md index 49d7718c..60793129 100644 --- a/test/README.md +++ b/test/README.md @@ -1,5 +1,6 @@ # TL;DR -Copy the file `.env-template` in `.env` and fill up the missing keys. +Copy the contents of `.env-template` to `.env` and [fill up the missing keys with +information from your SauceLabs account](#how-to-set-up-your-env-config-file). ```bash $ npm install @@ -8,6 +9,11 @@ $ npm test && git commit ``` +# TODO +* Use gulp to parallelize mocha invocations with different browsers +* Figure out good system for changing subdomain through test scripts + + # Welcome to our test suite, let me be your guide Dear reader, first of all thanks for taking your time reading this document. @@ -154,8 +160,8 @@ chai.should(); ``` `browser` is the main object to interact with Saucelab "real" browsers. We will -use this object a lot. It allow us to load pages, click around, check if a -specific text is present etc. +use this object a lot. It allows us to load pages, click around, check if a +specific text is present, etc. ```javascript describe('Login logs users in', function() { @@ -179,11 +185,13 @@ browser to a specific URL. }); ``` -While this function will be executed after each `it` function. `quit` will -destroy the browser session. +Close the browser after finishing all tests. `after` will be executed at the end +of all `it` functions. Use `afterEach` instead if you'd like to run some code +after each `it` function. ```javascript after(function() { + // Destroys the browser session return browser.quit(); }); ``` @@ -197,7 +205,6 @@ without writing new functions. it('should contain "Log in" in the title', function() { return browser.title().should.become('Log in'); }); - }); ``` From f246005d9cc6989e585f795ba2cb5d2f7d84adff Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 25 Jan 2016 17:31:18 +0100 Subject: [PATCH 11/14] Remove duplicate instance of gulp-sass --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index cd1eae93..091203b5 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "chai-as-promised": "^5.1.0", "colors": "^1.1.2", "dotenv": "^1.2.0", - "gulp-sass": "^2.1.1", "jest-cli": "^0.4.0", "mocha": "^2.3.4", "sauce-connect-launcher": "^0.13.0", @@ -72,7 +71,7 @@ "gulp-if": "^1.2.5", "gulp-minify-css": "^1.1.6", "gulp-notify": "^2.2.0", - "gulp-sass": "^2.0.1", + "gulp-sass": "^2.1.1", "gulp-sourcemaps": "^1.5.2", "gulp-template": "~3.0.0", "gulp-uglify": "^1.2.0", From bdc13473a1449e08b38ad43d0cacf6918e1a1db4 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 25 Jan 2016 17:32:04 +0100 Subject: [PATCH 12/14] Fix initial test script to wait for app to load --- test/README.md | 51 +++++++++++++++++++++++++++++++++++++--------- test/test-login.js | 19 +++++++++++------ 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/test/README.md b/test/README.md index 60793129..8fca1cac 100644 --- a/test/README.md +++ b/test/README.md @@ -168,20 +168,19 @@ describe('Login logs users in', function() { let browser; ``` -Create the driver to control the browser. +Create the driver to control the browser. `before` will be executed once at the +start of the test before any `it` functions. Use `beforeEach` instead if you'd +like to run some code before each `it` function. + ```javascript before(function() { browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80); - return browser.init({ browserName: 'chrome' }); - }); -``` -This function will be executed before each `it` function. Here we point the -browser to a specific URL. - -```javascript - beforeEach(function() { - return browser.get('http://www.ascribe.ninja/app/login'); + // Start the browser, go to /login, and wait for the react app to render + return browser + .init({ browserName, version, platform }) + .get(config.APP_URL + '/login') + .waitForElementByCss('.ascribe-default-app', asserters.isDisplayed, 10000); }); ``` @@ -208,6 +207,38 @@ without writing new functions. }); ``` +All together: + +```javascript +function testSuite(browserName, version, platform) { + describe(`[${browserName} ${version} ${platform}] Login logs users in`, function() { + // Set timeout to zero so Mocha won't time out. + this.timeout(0); + let browser; + + before(function() { + // No need to inject `username` or `access_key`, by default the constructor + // looks up the values in `process.env.SAUCE_USERNAME` and `process.env.SAUCE_ACCESS_KEY` + browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80); + + // Start the browser, go to /login, and wait for the react app to render + return browser + .init({ browserName, version, platform }) + .get(config.APP_URL + '/login') + .waitForElementByCss('.ascribe-default-app', asserters.isDisplayed, 10000); + }); + + after(function() { + return browser.quit(); + }); + + it('should contain "Log in" in the title', function() { + return browser.title().should.become('Log in'); + }); + }); +} +``` + ## How to run the test suite To run the tests, type: ```bash diff --git a/test/test-login.js b/test/test-login.js index 38a1e377..fcd6f301 100644 --- a/test/test-login.js +++ b/test/test-login.js @@ -1,14 +1,16 @@ 'use strict'; +const Q = require('q'); 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'); + chai.use(chaiAsPromised); chai.should(); - function testSuite(browserName, version, platform) { describe(`[${browserName} ${version} ${platform}] Login logs users in`, function() { // Set timeout to zero so Mocha won't time out. @@ -19,11 +21,17 @@ function testSuite(browserName, version, platform) { // No need to inject `username` or `access_key`, by default the constructor // looks up the values in `process.env.SAUCE_USERNAME` and `process.env.SAUCE_ACCESS_KEY` browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80); - return browser.init({ browserName, version, platform }); - }); - beforeEach(function() { - return browser.get(config.APP_URL + '/login'); + // Start the browser, go to /login, and wait for the react app to render + return browser + .init({ browserName, version, platform }) + .get(config.APP_URL + '/login') + .waitForElementByCss('.ascribe-default-app', asserters.isDisplayed, 1000) + .catch(function (err) { + console.log('Failure -- unable to load app.'); + console.log('Skipping tests for this browser...'); + return Q.reject(err); + }); }); after(function() { @@ -33,7 +41,6 @@ function testSuite(browserName, version, platform) { it('should contain "Log in" in the title', function() { return browser.title().should.become('Log in'); }); - }); } From ebb8248364922f63b43fbc2901d08d4acb30ac00 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 25 Jan 2016 17:37:04 +0100 Subject: [PATCH 13/14] Make sure to wait for LoginContainer --- test/test-login.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test-login.js b/test/test-login.js index fcd6f301..e66fa3a5 100644 --- a/test/test-login.js +++ b/test/test-login.js @@ -26,7 +26,7 @@ function testSuite(browserName, version, platform) { return browser .init({ browserName, version, platform }) .get(config.APP_URL + '/login') - .waitForElementByCss('.ascribe-default-app', asserters.isDisplayed, 1000) + .waitForElementByCss('.ascribe-default-app', asserters.isDisplayed, 10000) .catch(function (err) { console.log('Failure -- unable to load app.'); console.log('Skipping tests for this browser...'); @@ -39,7 +39,9 @@ function testSuite(browserName, version, platform) { }); it('should contain "Log in" in the title', function() { - return browser.title().should.become('Log in'); + return browser. + waitForElementByCss('.ascribe-login-wrapper', asserters.isDisplayed, 2000) + title().should.become('Log in'); }); }); } From 0fefba549c9cea0618339a66834c70a893b30322 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 25 Jan 2016 17:55:45 +0100 Subject: [PATCH 14/14] Use `configureHttp` to set baseUrl for tests --- test/test-login.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test-login.js b/test/test-login.js index e66fa3a5..e2736fe1 100644 --- a/test/test-login.js +++ b/test/test-login.js @@ -24,8 +24,9 @@ function testSuite(browserName, version, platform) { // Start the browser, go to /login, and wait for the react app to render return browser + .configureHttp({ baseUrl: config.APP_URL }) .init({ browserName, version, platform }) - .get(config.APP_URL + '/login') + .get('/login') .waitForElementByCss('.ascribe-default-app', asserters.isDisplayed, 10000) .catch(function (err) { console.log('Failure -- unable to load app.');