Enable Protractor end-to-end testing.

This enables end-to-end testing with Protractor, along with some
basic route and page load testing for the home and project pages.
PhantomJS is used to execute tests, and API requests are mocked
using `protractor-http-mock` (see `test/e2e/mocks/`).

A new gulp `e2e` task is also added to prepare and execute the
tests, along with a shortcut, `npm run protractor`. The default
`npm run test` is also amended to include E2E tests, with unit
tests moved to the new shortcut `npm run unit`.

Change-Id: Idb2eef2d851035c715e23553db56fc80deeab8e7
This commit is contained in:
Tim Buckley 2015-11-11 17:02:27 -07:00
parent a90ac92bed
commit 6e3f74a847
10 changed files with 533 additions and 36 deletions

View File

@ -7,11 +7,9 @@ var browserSync = require('browser-sync');
gulp.task('dev-resources', function() {
if (!global.isProd) {
return gulp.src(config.devResources.src)
.pipe(changed(config.devResources.dest))
.pipe(gulp.dest(config.devResources.dest))
.pipe(browserSync.reload({ stream: true, once: true }));
}
return gulp.src(config.devResources.src)
.pipe(changed(config.devResources.dest))
.pipe(gulp.dest(config.devResources.dest))
.pipe(browserSync.reload({ stream: true, once: true }));
});

16
gulp/tasks/e2e.js Normal file
View File

@ -0,0 +1,16 @@
'use strict';
var gulp = require('gulp');
var runSequence = require('run-sequence');
gulp.task('e2e', function(callback) {
callback = callback || function() {};
runSequence(
'prod',
'dev-resources',
'protractor',
callback);
});

View File

@ -18,6 +18,10 @@ gulp.task('protractor', ['webdriver-update', 'webdriver', 'server'], function()
.on('error', function(err) {
// Make sure failed tests cause gulp to exit non-zero
throw err;
})
.on('end', function() {
// server task will wait for user to quit, so force it to end here
process.exit();
});
});

View File

@ -60,6 +60,7 @@
"phantomjs": "1.9.17",
"pretty-hrtime": "^1.0.0",
"protractor": "^2.2.0",
"protractor-http-mock": "^0.1.18",
"run-sequence": "^1.1.2",
"tiny-lr": "^0.1.6",
"uglifyify": "^3.0.1",
@ -70,9 +71,9 @@
},
"scripts": {
"pretest": "npm install",
"test": "gulp unit",
"preprotractor": "npm run update-webdriver",
"protractor": "npm run protractor test/protractor.conf.js",
"unit": "gulp unit",
"protractor": "gulp e2e",
"test": "gulp unit && gulp e2e",
"lint": "eslint ./"
},
"dependencies": {},

View File

@ -1,21 +0,0 @@
/*global browser, by */
'use strict';
describe('E2E: Example', function() {
beforeEach(function() {
browser.get('/');
browser.waitForAngular();
});
it('should route correctly', function() {
expect(browser.getLocationAbsUrl()).toMatch('/');
});
it('should show the number defined in the controller', function() {
var element = browser.findElement(by.css('.number-example'));
expect(element.getText()).toEqual('1234');
});
});

11
test/e2e/mocks/config.js Normal file
View File

@ -0,0 +1,11 @@
module.exports = {
request: {
method: 'GET',
path: 'config.json'
},
response: {
data: {
"apiRoot": "http://localhost:5000"
}
}
};

View File

@ -0,0 +1,401 @@
module.exports = {
request: {
method: 'JSONP',
path: '/runs/group_by/project'
},
response: {
data: {
"runs": {
"2015-10-20T20:00:00": {
"openstack/keystone": [
{
"fail": 0,
"pass": 1154,
"skip": 120
}
],
"openstack/requirements": [
{
"fail": 0,
"pass": 1154,
"skip": 120
}
]
},
"2015-10-20T21:00:00": {
"openstack/ceilometer": [
{
"fail": 0,
"pass": 58,
"skip": 34
},
{
"fail": 0,
"pass": 55,
"skip": 38
},
{
"fail": 0,
"pass": 1164,
"skip": 116
},
{
"fail": 0,
"pass": 1164,
"skip": 116
},
{
"fail": 0,
"pass": 58,
"skip": 34
},
{
"fail": 0,
"pass": 1164,
"skip": 116
},
{
"fail": 0,
"pass": 1444,
"skip": 82
},
{
"fail": 0,
"pass": 55,
"skip": 38
},
{
"fail": 0,
"pass": 1164,
"skip": 116
}
],
"openstack/keystone": [
{
"fail": 0,
"pass": 3,
"skip": 0
},
{
"fail": 0,
"pass": 1154,
"skip": 120
},
{
"fail": 0,
"pass": 58,
"skip": 34
},
{
"fail": 0,
"pass": 1154,
"skip": 120
},
{
"fail": 0,
"pass": 58,
"skip": 34
},
{
"fail": 0,
"pass": 1434,
"skip": 86
},
{
"fail": 0,
"pass": 3,
"skip": 0
}
],
"openstack/neutron": [
{
"fail": 0,
"pass": 1434,
"skip": 86
},
{
"fail": 0,
"pass": 3,
"skip": 0
},
{
"fail": 0,
"pass": 3,
"skip": 0
},
{
"fail": 0,
"pass": 108,
"skip": 23
},
{
"fail": 0,
"pass": 112,
"skip": 18
},
{
"fail": 0,
"pass": 112,
"skip": 18
},
{
"fail": 0,
"pass": 108,
"skip": 23
}
],
"openstack/python-novaclient": [
{
"fail": 0,
"pass": 1434,
"skip": 86
}
],
"openstack/requirements": [
{
"fail": 0,
"pass": 1154,
"skip": 120
},
{
"fail": 0,
"pass": 58,
"skip": 34
},
{
"fail": 0,
"pass": 1154,
"skip": 120
},
{
"fail": 0,
"pass": 3,
"skip": 0
},
{
"fail": 0,
"pass": 58,
"skip": 34
},
{
"fail": 0,
"pass": 58,
"skip": 34
},
{
"fail": 0,
"pass": 58,
"skip": 34
}
],
"openstack/taskflow": [
{
"fail": 0,
"pass": 1434,
"skip": 86
},
{
"fail": 0,
"pass": 1434,
"skip": 86
},
{
"fail": 0,
"pass": 1434,
"skip": 86
},
{
"fail": 1,
"pass": 1433,
"skip": 86
},
{
"fail": 0,
"pass": 1434,
"skip": 86
},
{
"fail": 0,
"pass": 1434,
"skip": 86
},
{
"fail": 0,
"pass": 1434,
"skip": 86
}
]
},
"2015-10-20T22:00:00": {
"openstack-infra/devstack-gate": [
{
"fail": 0,
"pass": 3,
"skip": 0
}
],
"openstack/ceilometer": [
{
"fail": 0,
"pass": 1444,
"skip": 82
}
],
"openstack/diskimage-builder": [
{
"fail": 0,
"pass": 71,
"skip": 0
}
],
"openstack/keystone": [
{
"fail": 0,
"pass": 1434,
"skip": 86
}
],
"openstack/neutron": [
{
"fail": 0,
"pass": 3,
"skip": 0
}
],
"openstack/requirements": [
{
"fail": 0,
"pass": 1154,
"skip": 120
},
{
"fail": 0,
"pass": 1434,
"skip": 86
}
],
"openstack/taskflow": [
{
"fail": 0,
"pass": 1434,
"skip": 86
},
{
"fail": 0,
"pass": 1434,
"skip": 86
},
{
"fail": 0,
"pass": 1434,
"skip": 86
},
{
"fail": 0,
"pass": 1434,
"skip": 86
}
],
"openstack/trove": [
{
"fail": 0,
"pass": 6,
"skip": 0
}
]
},
"2015-10-20T23:00:00": {
"openstack-dev/pbr": [
{
"fail": 0,
"pass": 0,
"skip": 0
}
],
"openstack-infra/devstack-gate": [
{
"fail": 0,
"pass": 58,
"skip": 34
},
{
"fail": 0,
"pass": 1154,
"skip": 120
},
{
"fail": 0,
"pass": 1154,
"skip": 120
},
{
"fail": 0,
"pass": 3,
"skip": 0
},
{
"fail": 0,
"pass": 58,
"skip": 34
}
],
"openstack/keystone": [
{
"fail": 0,
"pass": 1154,
"skip": 120
},
{
"fail": 0,
"pass": 3,
"skip": 0
},
{
"fail": 0,
"pass": 1154,
"skip": 120
},
{
"fail": 0,
"pass": 1434,
"skip": 86
}
],
"openstack/neutron": [
{
"fail": 0,
"pass": 112,
"skip": 18
},
{
"fail": 0,
"pass": 112,
"skip": 18
}
],
"openstack/python-neutronclient": [
{
"fail": 0,
"pass": 1434,
"skip": 86
}
],
"openstack/python-novaclient": [
{
"fail": 0,
"pass": 1434,
"skip": 86
}
],
"openstack/trove": [
{
"fail": 0,
"pass": 6,
"skip": 0
}
]
}
}
}
}
};

View File

@ -0,0 +1,34 @@
module.exports = {
request: {
method: 'JSONP',
path: '/projects/openstack/taskflow/runs'
},
response: {
data: {
"timedelta": [
{
"datetime": "2015-10-23T20:00:00",
"job_data": [
{
"fail": 0,
"job_name": "gate-tempest-dsvm-neutron-src-taskflow",
"mean_run_time": 4859.3,
"pass": 1
}
]
},
{
"datetime": "2015-11-10T23:00:00",
"job_data": [
{
"fail": 0,
"job_name": "gate-tempest-dsvm-neutron-src-taskflow",
"mean_run_time": 6231.47,
"pass": 1
}
]
}
]
}
}
};

View File

@ -2,11 +2,55 @@
'use strict';
var mock = require('protractor-http-mock');
describe('E2E: Routes', function() {
it('should have a working home route', function() {
mock(['config', 'home_project']);
browser.get('#/');
// route should be defined (will redirect to / if not)
expect(browser.getLocationAbsUrl()).toMatch('/');
// data should actually be requested (no request if error)
expect(mock.requestsMade()).toContain(jasmine.objectContaining({
url: 'http://localhost:5000/runs/group_by/project',
method: 'JSONP'
}));
// should have a link to the next page
var selector = 'a[href="#/project/openstack%252Ftaskflow"]';
expect(element(by.css(selector)).isPresent()).toBe(true);
});
it('should have a working project route', function() {
mock(['config', 'project_taskflow']);
browser.get('#/project/openstack%252Ftaskflow');
// route should be defined (will redirect to / if not)
browser.getLocationAbsUrl().then(function(url) {
// note: phantomjs converts the octal escape to '/' for getLocationAbsUrl
// for browsers that don't do this (chrome, firefox, etc), escape it
// manually to make sure the expectation works correctly
expect(url.replace('%252F', '/')).toMatch('/project/openstack/taskflow');
});
// data should actually be requested (no request if error)
expect(mock.requestsMade()).toContain(jasmine.objectContaining({
url: 'http://localhost:5000/projects/openstack/taskflow/runs',
method: 'JSONP'
}));
// should have a link to the next page
var selector = 'a[href="#/job/gate-tempest-dsvm-neutron-src-taskflow"]';
expect(element(by.css(selector)).isPresent()).toBe(true);
});
afterEach(function() {
mock.teardown();
});
});

View File

@ -1,5 +1,7 @@
'use strict';
var phantomjs = require('phantomjs');
var gulpConfig = require('../gulp/config');
exports.config = {
@ -8,12 +10,9 @@ exports.config = {
baseUrl: 'http://localhost:' + gulpConfig.serverPort + '/',
directConnect: true,
capabilities: {
browserName: 'chrome',
version: '',
platform: 'ANY'
browserName: 'phantomjs',
'phantomjs.binary.path': phantomjs.path
},
framework: 'jasmine',
@ -27,6 +26,16 @@ exports.config = {
specs: [
'e2e/**/*.js'
]
],
mocks: {
dir: 'e2e/mocks'
},
onPrepare: function() {
require('protractor-http-mock').config = {
rootDirectory: __dirname
};
}
};