Compare commits

..

14 Commits
1.0.0 ... 1.1.0

Author SHA1 Message Date
Rowan Manning
2ebb54f545 Version 1.1.0 2013-11-22 14:01:53 +00:00
Rowan Manning
d57ae45b4f Write tests for result pages (and related actions) 2013-11-22 11:44:54 +00:00
Rowan Manning
ea330548b1 Write tests for task pages (and related actions) 2013-11-22 11:10:57 +00:00
Rowan Manning
ae5b214834 Write tests for the new URL form 2013-11-22 09:17:44 +00:00
Rowan Manning
b5735b7f05 Reload fixtures for tests and finish testing home 2013-11-21 13:54:52 +00:00
Rowan Manning
a47fb38d7f Add test running instructions to the readme 2013-11-21 12:04:42 +00:00
Rowan Manning
da0e98eab1 Update the Travis config to run tests 2013-11-21 11:52:40 +00:00
Rowan Manning
623a52e112 Build a basic framework for functional testing 2013-11-21 09:58:44 +00:00
Rowan Manning
9d72e50b4e Update docs to include webservice dependencies 2013-11-21 09:25:28 +00:00
Rowan Manning
3535b9b11c Document new configuration options 2013-11-21 09:21:15 +00:00
Rowan Manning
8f636173a0 Allow the webservice to run automatically 2013-11-21 09:12:54 +00:00
Rowan Manning
ec9f82aa6f Add build status to readme 2013-11-20 13:47:47 +00:00
Rowan Manning
34a7c351c9 Add Travis config 2013-11-20 13:46:47 +00:00
Rowan Manning
c13af05422 Fix lint errors 2013-11-20 13:46:41 +00:00
29 changed files with 758 additions and 43 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
# Config files # Config files
config/development.json config/development.json
config/production.json config/production.json
config/test.json
# Generated npm files # Generated npm files
node_modules node_modules

24
.travis.yml Normal file
View File

@@ -0,0 +1,24 @@
# Language/versions
language: node_js
node_js:
- "0.10"
# Services setup
services:
- mongodb
# Build script
before_script:
- npm install -g grunt-cli
- cp config/test.sample.json config/test.json
- grunt start-test &
- sleep 5 # give server time to start
script: 'grunt ci'
# Notifications
notifications:
email:
- j.robinson@nature.com
- perry.harlock@nature.com
- rowan.manning@nature.com

View File

@@ -34,6 +34,15 @@ module.exports = function (grunt) {
} }
}, },
mochaTest: {
functional: {
src: ['test/functional/**/*.js'],
options: {
reporter: 'spec'
}
}
},
nodemon: { nodemon: {
development: { development: {
options: { options: {
@@ -43,6 +52,15 @@ module.exports = function (grunt) {
NODE_ENV: 'development' NODE_ENV: 'development'
} }
} }
},
test: {
options: {
cwd: __dirname,
file: 'index.js',
env: {
NODE_ENV: 'test'
}
}
} }
}, },
@@ -84,12 +102,15 @@ module.exports = function (grunt) {
grunt.loadNpmTasks('grunt-contrib-less'); grunt.loadNpmTasks('grunt-contrib-less');
grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-mocha-test');
grunt.loadNpmTasks('grunt-nodemon'); grunt.loadNpmTasks('grunt-nodemon');
grunt.registerTask('lint', ['jshint']); grunt.registerTask('lint', ['jshint']);
grunt.registerTask('test', ['mochaTest']);
grunt.registerTask('compile', ['less', 'uglify']); grunt.registerTask('compile', ['less', 'uglify']);
grunt.registerTask('start', ['nodemon:development']); grunt.registerTask('start', ['nodemon:development']);
grunt.registerTask('default', ['compile', 'lint']); grunt.registerTask('start-test', ['nodemon:test']);
grunt.registerTask('ci', ['compile', 'lint']); grunt.registerTask('default', ['compile', 'lint', 'test']);
grunt.registerTask('ci', ['lint', 'test']);
}; };

View File

@@ -3,7 +3,8 @@ pa11y-dashboard
pa11y-dashboard is a web interface to the [pa11y][pa11y] accessibility reporter; allowing you to focus on *fixing* issues rather than hunting them down. pa11y-dashboard is a web interface to the [pa11y][pa11y] accessibility reporter; allowing you to focus on *fixing* issues rather than hunting them down.
**Current Version:** *1.0.0* **Current Version:** *1.1.0*
**Build Status:** [![Build Status][travis-img]][travis]
**Node Version Support:** *0.10* **Node Version Support:** *0.10*
@@ -14,7 +15,7 @@ pa11y-dashboard is a web interface to the [pa11y][pa11y] accessibility reporter;
Setup Setup
----- -----
pa11y-dashboard requires [Node.js][node] 0.10+ and [pa11y-webservice][pa11y-webservice] to be installed and running. You'll need to follow the setup guide for pa11y-webservice before setting up pa11y-dashboard. pa11y-dashboard requires [Node.js][node] 0.10+, [PhantomJS][phantom], and [MongoDB][mongo] to be installed. See the [pa11y][pa11y] and [pa11y-webservice][pa11y-webservice] documentation for more information on these dependencies.
You'll then need to clone this repo locally and install dependencies with `npm install`. You'll then need to clone this repo locally and install dependencies with `npm install`.
@@ -23,6 +24,7 @@ Once you have a local clone, you'll need to copy some sample configuration files
```sh ```sh
$ cp config/development.sample.json config/development.json $ cp config/development.sample.json config/development.json
$ cp config/production.sample.json config/production.json $ cp config/production.sample.json config/production.json
$ cp config/test.sample.json config/test.json
``` ```
Each of these files defines configurations for a different environment. If you're just running the application locally, then you should be OK with just development configurations. The [available configurations are documented here](#configurations). Each of these files defines configurations for a different environment. If you're just running the application locally, then you should be OK with just development configurations. The [available configurations are documented here](#configurations).
@@ -32,6 +34,7 @@ Now that you've got your application configured, you can run in each mode with t
```sh ```sh
$ NODE_ENV=production node . # Run in production $ NODE_ENV=production node . # Run in production
$ NODE_ENV=development node . # Run in development $ NODE_ENV=development node . # Run in development
$ NODE_ENV=test node . # Run in test
``` ```
Check the [development instructions](#development) for more information about running locally (and restarting automatically when files change). Check the [development instructions](#development) for more information about running locally (and restarting automatically when files change).
@@ -42,9 +45,6 @@ Configurations
The boot configurations for pa11y-dashboard are as follows. Look at the sample JSON files in the repo for example usage. The boot configurations for pa11y-dashboard are as follows. Look at the sample JSON files in the repo for example usage.
### webservice
*(string)* The base URL of the [pa11y-webservice][pa11y-webservice] instance you intend on using.
### port ### port
*(number)* The port to run the application on. *(number)* The port to run the application on.
@@ -57,20 +57,31 @@ The boot configurations for pa11y-dashboard are as follows. Look at the sample J
### siteMessage ### siteMessage
*(string)* A message to display prominently on the site home page. Defaults to `null`. *(string)* A message to display prominently on the site home page. Defaults to `null`.
### webservice
This can either be an object containing [pa11y-webservice configurations][pa11y-webservice-config], or a string which is the base URL of a [pa11y-webservice][pa11y-webservice] instance you are running separately.
Development Development
----------- -----------
To develop pa11y-dashboard, you'll need to clone the repo and get set up as outlined in the [setup guide](#setup). You'll also need [Grunt][grunt] to be installed globally in order to run tests, you can do this with `npm install -g grunt-cli`. To develop pa11y-dashboard, you'll need to clone the repo and get set up as outlined in the [setup guide](#setup). You'll also need [Grunt][grunt] to be installed globally in order to run tests, you can do this with `npm install -g grunt-cli`.
Once you've done this, you'll need to start the application in test mode with:
```sh
$ grunt start-test
```
Now you'll be able to run the following commands: Now you'll be able to run the following commands:
```sh ```sh
$ grunt # Run the lint and test tasks together $ grunt # Run the lint and test tasks together
$ grunt lint # Run JSHint with the correct config $ grunt lint # Run JSHint with the correct config
$ grunt compile # Compile front-end assets $ grunt compile # Compile front-end assets
$ grunt start # Run app in development mode, restarting if files change $ grunt start # Run app in development mode, restarting if files change
$ grunt watch # Watch for file changes and compile assets $ grunt start-test # Run app in test mode, restarting if files change
$ grunt test # Run functional tests
$ grunt watch # Watch for file changes and compile assets
``` ```
Code with lint errors or failing tests will not be accepted, please use the build tools outlined above. Code with lint errors or failing tests will not be accepted, please use the build tools outlined above.
@@ -88,7 +99,11 @@ pa11y-dashboard is licensed under the [GNU General Public License 3.0][gpl].
[gpl]: http://www.gnu.org/licenses/gpl-3.0.html [gpl]: http://www.gnu.org/licenses/gpl-3.0.html
[grunt]: http://gruntjs.com/ [grunt]: http://gruntjs.com/
[mongo]: http://www.mongodb.org/
[node]: http://nodejs.org/ [node]: http://nodejs.org/
[pa11y]: https://github.com/nature/pa11y [pa11y]: https://github.com/nature/pa11y
[pa11y-webservice]: https://github.com/nature/pa11y-webservice [pa11y-webservice]: https://github.com/nature/pa11y-webservice
[supervisor]: https://github.com/isaacs/node-supervisor [pa11y-webservice-config]: https://github.com/nature/pa11y-webservice#configurations
[phantom]: http://phantomjs.org/
[travis]: https://travis-ci.org/nature/pa11y-dashboard
[travis-img]: https://travis-ci.org/nature/pa11y-dashboard.png?branch=master

7
app.js
View File

@@ -13,11 +13,16 @@ module.exports = initApp;
function initApp (config, callback) { function initApp (config, callback) {
config = defaultConfig(config); config = defaultConfig(config);
var webserviceUrl = config.webservice;
if (typeof webserviceUrl == 'object') {
webserviceUrl = 'http://' + webserviceUrl.host + ':' + webserviceUrl.port + '/';
}
var app = new EventEmitter(); var app = new EventEmitter();
app.address = null; app.address = null;
app.express = express(); app.express = express();
app.server = http.createServer(app.express); app.server = http.createServer(app.express);
app.webservice = createClient(config.webservice); app.webservice = createClient(webserviceUrl);
// Compression // Compression
app.express.use(express.compress()); app.express.use(express.compress());

View File

@@ -1,6 +1,12 @@
{ {
"webservice": "http://localhost:3000/",
"port": 4000, "port": 4000,
"noindex": true, "noindex": true,
"readonly": false "readonly": false,
"webservice": {
"database": "mongodb://localhost/pa11y-webservice-dev",
"host": "0.0.0.0",
"port": 3000,
"cron": "0 30 0 * * *"
}
} }

View File

@@ -1,6 +1,12 @@
{ {
"webservice": "http://localhost:3000/",
"port": 4000, "port": 4000,
"noindex": true, "noindex": true,
"readonly": false "readonly": false,
"webservice": {
"database": "mongodb://localhost/pa11y-webservice",
"host": "0.0.0.0",
"port": 3000,
"cron": "0 30 0 * * *"
}
} }

12
config/test.sample.json Normal file
View File

@@ -0,0 +1,12 @@
{
"port": 4000,
"noindex": true,
"readonly": false,
"webservice": {
"database": "mongodb://localhost/pa11y-webservice-test",
"host": "0.0.0.0",
"port": 3000,
"cron": "0 30 0 * * *"
}
}

View File

@@ -18,4 +18,14 @@ require('./app')(config, function (err, app) {
console.error(chalk.grey(stack.join('\n'))); console.error(chalk.grey(stack.join('\n')));
}); });
// Start the webservice if required
if (typeof config.webservice === 'object') {
require('pa11y-webservice')(config.webservice, function (err, webservice) {
console.log('');
console.log(chalk.underline.cyan('pa11y-webservice started'));
console.log(chalk.grey('mode: %s'), process.env.NODE_ENV);
console.log(chalk.grey('uri: %s'), webservice.server.info.uri);
});
}
}); });

View File

@@ -1,6 +1,6 @@
{ {
"name": "pa11y-dashboard", "name": "pa11y-dashboard",
"version": "1.0.0", "version": "1.1.0",
"private": true, "private": true,
"description": "pa11y-dashboard is a visual web interface to the pa11y accessibility reporter", "description": "pa11y-dashboard is a visual web interface to the pa11y accessibility reporter",
@@ -25,6 +25,7 @@
"express": "~3.4", "express": "~3.4",
"express-hbs": "~0.2", "express-hbs": "~0.2",
"moment": "~2.2", "moment": "~2.2",
"pa11y-webservice": "~1.1",
"pa11y-webservice-client-node": "~1.0", "pa11y-webservice-client-node": "~1.0",
"underscore": "~1.5" "underscore": "~1.5"
}, },
@@ -35,7 +36,11 @@
"grunt-contrib-less": "~0.8", "grunt-contrib-less": "~0.8",
"grunt-contrib-uglify": "~0.2", "grunt-contrib-uglify": "~0.2",
"grunt-contrib-watch": "~0.5", "grunt-contrib-watch": "~0.5",
"grunt-nodemon": "~0.1" "grunt-mocha-test": "~0.7",
"grunt-nodemon": "~0.1",
"jsdom": "~0.8",
"proclaim": "~2.0",
"request": "~2.27"
}, },
"scripts": { "scripts": {

View File

@@ -167,12 +167,20 @@ $(document).ready(function(){
$.each(datasets, function(key, val) { $.each(datasets, function(key, val) {
var lowerCaseValue = (val.label.substring(0, val.label.length - 1)).toLowerCase(); var lowerCaseValue = (val.label.substring(0, val.label.length - 1)).toLowerCase();
choiceContainer.append('<li class="text-center '+ choiceContainer.append(
lowerCaseValue +'"><div class="series-checkbox-container"><input type="checkbox" name="' + key + '<li class="text-center ' + lowerCaseValue + '">' +
'" checked="checked" id="id' + key + '<div class="series-checkbox-container">' +
'"/><label for="id' + key + '<input type="checkbox"' +
'"><span class="stat-type">' + val.label + 'name="' + key + '" ' +
'</span></label></div></li>'); 'checked="checked" ' +
'id="id' + key + '"' +
'/>' +
'<label for="id' + key + '">' +
'<span class="stat-type">' + val.label + '</span>' +
'</label>' +
'</div>' +
'</li>'
);
}); });
choiceContainer.find('input').click(plotAccordingToChoices); choiceContainer.find('input').click(plotAccordingToChoices);

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,51 @@
'use strict';
var jsdom = require('jsdom');
var request = require('request');
module.exports = createNavigator;
// Create a navigate function
function createNavigator (baseUrl, store) {
return function (opts, callback) {
store.body = null;
store.dom = null;
store.request = null;
store.response = null;
store.status = null;
store.window = null;
request({
url: baseUrl + opts.endpoint,
method: opts.method || 'GET',
body: opts.body,
json: true,
qs: opts.query,
followAllRedirects: true
}, function (err, res, body) {
store.body = body;
store.request = res.request;
store.response = res;
store.status = res.statusCode;
if (opts.nonDom) {
store.window = null;
store.dom = null;
callback();
} else {
jsdom.env(
store.body,
function (err, window) {
store.window = window;
store.dom = window.document;
callback();
}
);
}
});
};
}

View File

@@ -0,0 +1,14 @@
'use strict';
var createClient = require('pa11y-webservice-client-node');
module.exports = createWebserviceClient;
// Create a webservice client
function createWebserviceClient (config) {
var webserviceUrl = config.webservice;
if (typeof webserviceUrl == 'object') {
webserviceUrl = 'http://' + webserviceUrl.host + ':' + webserviceUrl.port + '/';
}
return createClient(webserviceUrl);
}

View File

@@ -0,0 +1,65 @@
/* global beforeEach, describe, it */
/* jshint maxlen: false, maxstatements: false */
'use strict';
var assert = require('proclaim');
describe('GET /', function () {
beforeEach(function (done) {
var req = {
method: 'GET',
endpoint: '/'
};
this.navigate(req, done);
});
it('should send a 200 status', function () {
assert.strictEqual(this.last.status, 200);
});
it('should display an "Add new URL" button', function () {
var elem = this.last.dom.querySelectorAll('[data-test=add-task]');
assert.strictEqual(elem.length, 1);
assert.strictEqual(elem[0].getAttribute('href'), '/new');
});
it('should display all of the expected tasks', function () {
var tasks = this.last.dom.querySelectorAll('[data-test=task]');
assert.strictEqual(tasks.length, 3);
assert.match(tasks[0].textContent, /npg home\s+\(wcag2aa\)/i);
assert.match(tasks[1].textContent, /npg home\s+\(wcag2aaa\)/i);
assert.match(tasks[2].textContent, /nature news\s+\(section508\)/i);
});
it('should have links to each task', function () {
var tasks = this.last.dom.querySelectorAll('[data-test=task]');
assert.strictEqual(tasks[0].querySelectorAll('[href="/abc000000000000000000001"]').length, 1);
assert.strictEqual(tasks[1].querySelectorAll('[href="/abc000000000000000000002"]').length, 1);
assert.strictEqual(tasks[2].querySelectorAll('[href="/abc000000000000000000003"]').length, 1);
});
it('should display a delete button for each task', function () {
var tasks = this.last.dom.querySelectorAll('[data-test=task]');
assert.strictEqual(tasks[0].querySelectorAll('[href="/abc000000000000000000001/delete"]').length, 1);
assert.strictEqual(tasks[1].querySelectorAll('[href="/abc000000000000000000002/delete"]').length, 1);
assert.strictEqual(tasks[2].querySelectorAll('[href="/abc000000000000000000003/delete"]').length, 1);
});
it('should display the task result counts if the task has been run', function () {
var tasks = this.last.dom.querySelectorAll('[data-test=task]');
assert.match(tasks[0].textContent, /1\s*errors/i);
assert.match(tasks[0].textContent, /2\s*warnings/i);
assert.match(tasks[0].textContent, /3\s*notices/i);
});
it('should display a message indicating that there are no results if the task has not been run', function () {
var tasks = this.last.dom.querySelectorAll('[data-test=task]');
assert.match(tasks[2].textContent, /no results/i);
});
it('should not display an alert message', function () {
assert.strictEqual(this.last.dom.querySelectorAll('[data-test=alert]').length, 0);
});
});

View File

@@ -0,0 +1,136 @@
/* global beforeEach, describe, it */
/* jshint maxlen: false, maxstatements: false */
'use strict';
var assert = require('proclaim');
describe('GET /new', function () {
beforeEach(function (done) {
var req = {
method: 'GET',
endpoint: '/new'
};
this.navigate(req, done);
});
it('should send a 200 status', function () {
assert.strictEqual(this.last.status, 200);
});
it('should not display an error message', function () {
assert.strictEqual(this.last.dom.querySelectorAll('[data-test=error]').length, 0);
});
it('should have an "Add new URL" form', function () {
var form = this.last.dom.querySelectorAll('[data-test=new-url-form]')[0];
assert.isDefined(form);
assert.strictEqual(form.getAttribute('action'), '/new');
assert.strictEqual(form.getAttribute('method'), 'post');
});
describe('"Add New URL" form', function () {
beforeEach(function () {
this.form = this.last.dom.querySelectorAll('[data-test=new-url-form]')[0];
});
it('should have a "name" field', function () {
var field = this.form.querySelectorAll('input[name=name]')[0];
assert.isDefined(field);
assert.strictEqual(field.getAttribute('type'), 'text');
assert.strictEqual(field.getAttribute('value'), '');
});
it('should have a "url" field', function () {
var field = this.form.querySelectorAll('input[name=url]')[0];
assert.isDefined(field);
assert.strictEqual(field.getAttribute('type'), 'url');
assert.strictEqual(field.getAttribute('value'), '');
});
it('should have a "standard" field', function () {
var field = this.form.querySelectorAll('select[name=standard]')[0];
assert.isDefined(field);
assert.strictEqual(field.querySelectorAll('option').length, 4);
});
it('should have "ignore" fields', function () {
var fields = this.form.querySelectorAll('input[name="ignore[]"]');
assert.isDefined(fields);
assert.notStrictEqual(fields.length, 0);
});
});
});
describe('POST /new', function () {
describe('with invalid query', function () {
beforeEach(function (done) {
var req = {
method: 'POST',
endpoint: '/new',
body: {
name: '',
url: ''
}
};
this.navigate(req, done);
});
it('should send a 200 status', function () {
assert.strictEqual(this.last.status, 200);
});
it('should display an error message', function () {
assert.strictEqual(this.last.dom.querySelectorAll('[data-test=error]').length, 1);
});
});
describe('with valid query', function () {
beforeEach(function (done) {
var req = {
method: 'POST',
endpoint: '/new',
body: {
name: 'Example',
url: 'http://example.com/',
standard: 'WCAG2AA'
}
};
this.navigate(req, done);
});
it('should send a 200 status', function () {
assert.strictEqual(this.last.status, 200);
});
it('should create the task', function (done) {
this.webservice.tasks.get({}, function (err, tasks) {
assert.strictEqual(tasks.length, 4);
done();
});
});
it('should redirect me to the new URL page', function () {
assert.match(this.last.request.uri.pathname, /^\/[a-z0-9]{24}$/i);
});
it('should not display an error message', function () {
assert.strictEqual(this.last.dom.querySelectorAll('[data-test=error]').length, 0);
});
it('should display a success message', function () {
var alert = this.last.dom.querySelectorAll('[data-test=alert]')[0];
assert.isDefined(alert);
assert.match(alert.textContent, /url has been added/i);
});
});
});

View File

@@ -0,0 +1,53 @@
/* global beforeEach, describe, it */
/* jshint maxlen: false, maxstatements: false */
'use strict';
var assert = require('proclaim');
describe('GET /<task-id>/<result-id>.csv', function () {
beforeEach(function (done) {
var req = {
method: 'GET',
endpoint: '/abc000000000000000000001/def000000000000000000001.csv',
nonDom: true
};
this.navigate(req, done);
});
it('should send a 200 status', function () {
assert.strictEqual(this.last.status, 200);
});
it('should output CSV results', function () {
assert.match(this.last.body, /^"code", "message", "type"/);
});
});
describe('GET /<task-id>/<result-id>.json', function () {
beforeEach(function (done) {
var req = {
method: 'GET',
endpoint: '/abc000000000000000000001/def000000000000000000001.json',
nonDom: true
};
this.navigate(req, done);
});
it('should send a 200 status', function () {
assert.strictEqual(this.last.status, 200);
});
it('should output JSON results', function () {
var json = this.last.body;
assert.strictEqual(json.task.name, 'NPG Home');
assert.strictEqual(json.task.url, 'nature.com');
assert.strictEqual(json.count.error, 1);
assert.strictEqual(json.count.warning, 2);
assert.strictEqual(json.count.notice, 3);
assert.isArray(json.results);
});
});

View File

@@ -0,0 +1,55 @@
/* global beforeEach, describe, it */
/* jshint maxlen: false, maxstatements: false */
'use strict';
var assert = require('proclaim');
describe('GET /<task-id>/<result-id>', function () {
beforeEach(function (done) {
var req = {
method: 'GET',
endpoint: '/abc000000000000000000001/def000000000000000000001'
};
this.navigate(req, done);
});
it('should send a 200 status', function () {
assert.strictEqual(this.last.status, 200);
});
it('should display a "Download CSV" button', function () {
var elem = this.last.dom.querySelectorAll('[data-test=download-csv]');
assert.strictEqual(elem.length, 1);
assert.strictEqual(elem[0].getAttribute('href'), '/abc000000000000000000001/def000000000000000000001.csv');
});
it('should display a "Download JSON" button', function () {
var elem = this.last.dom.querySelectorAll('[data-test=download-json]');
assert.strictEqual(elem.length, 1);
assert.strictEqual(elem[0].getAttribute('href'), '/abc000000000000000000001/def000000000000000000001.json');
});
it('should display a link back to the task', function () {
assert.isDefined(this.last.dom.querySelectorAll('[href="/abc000000000000000000001"]')[0]);
});
it('should display errors', function () {
var elem = this.last.dom.querySelectorAll('[data-test=task-errors]')[0];
assert.isDefined(elem);
assert.match(elem.textContent, /errors \( 1 \)/i);
});
it('should display warnings', function () {
var elem = this.last.dom.querySelectorAll('[data-test=task-warnings]')[0];
assert.isDefined(elem);
assert.match(elem.textContent, /warnings \( 2 \)/i);
});
it('should display notices', function () {
var elem = this.last.dom.querySelectorAll('[data-test=task-notices]')[0];
assert.isDefined(elem);
assert.match(elem.textContent, /notices \( 3 \)/i);
});
});

View File

@@ -0,0 +1,61 @@
/* global beforeEach, describe, it */
/* jshint maxlen: false, maxstatements: false */
'use strict';
var assert = require('proclaim');
describe('GET /<task-id>/delete', function () {
beforeEach(function (done) {
var req = {
method: 'GET',
endpoint: '/abc000000000000000000001/delete'
};
this.navigate(req, done);
});
it('should send a 200 status', function () {
assert.strictEqual(this.last.status, 200);
});
it('should have a "Delete URL" form', function () {
var form = this.last.dom.querySelectorAll('[data-test=delete-url-form]')[0];
assert.isDefined(form);
assert.strictEqual(form.getAttribute('action'), '/abc000000000000000000001/delete');
assert.strictEqual(form.getAttribute('method'), 'post');
});
});
describe('POST /<task-id>/delete', function () {
beforeEach(function (done) {
var req = {
method: 'POST',
endpoint: '/abc000000000000000000001/delete'
};
this.navigate(req, done);
});
it('should send a 200 status', function () {
assert.strictEqual(this.last.status, 200);
});
it('should delete the task', function (done) {
this.webservice.task('abc000000000000000000001').get({}, function (err) {
assert.strictEqual(err.message, 'Error 404');
done();
});
});
it('should redirect me to the home page', function () {
assert.strictEqual(this.last.request.uri.pathname, '/');
});
it('should display a success message', function () {
var alert = this.last.dom.querySelectorAll('[data-test=alert]')[0];
assert.isDefined(alert);
assert.match(alert.textContent, /been deleted/i);
});
});

View File

@@ -0,0 +1,94 @@
/* global beforeEach, describe, it */
/* jshint maxlen: false, maxstatements: false */
'use strict';
var assert = require('proclaim');
describe('GET /<task-id>', function () {
describe('when task has results', function () {
beforeEach(function (done) {
var req = {
method: 'GET',
endpoint: '/abc000000000000000000001'
};
this.navigate(req, done);
});
it('should send a 200 status', function () {
assert.strictEqual(this.last.status, 200);
});
it('should display a "Run" button', function () {
var elem = this.last.dom.querySelectorAll('[data-test=run-task]');
assert.strictEqual(elem.length, 1);
assert.strictEqual(elem[0].getAttribute('href'), '/abc000000000000000000001/run');
});
it('should display a "Download CSV" button for the latest result', function () {
var elem = this.last.dom.querySelectorAll('[data-test=download-csv]');
assert.strictEqual(elem.length, 1);
assert.strictEqual(elem[0].getAttribute('href'), '/abc000000000000000000001/def000000000000000000001.csv');
});
it('should display a "Download JSON" button for the latest result', function () {
var elem = this.last.dom.querySelectorAll('[data-test=download-json]');
assert.strictEqual(elem.length, 1);
assert.strictEqual(elem[0].getAttribute('href'), '/abc000000000000000000001/def000000000000000000001.json');
});
it('should display links to all results', function () {
assert.isDefined(this.last.dom.querySelectorAll('[href="/abc000000000000000000001/def000000000000000000001"]')[0]);
assert.isDefined(this.last.dom.querySelectorAll('[href="/abc000000000000000000001/def000000000000000000003"]')[0]);
});
it('should display errors', function () {
var elem = this.last.dom.querySelectorAll('[data-test=task-errors]')[0];
assert.isDefined(elem);
assert.match(elem.textContent, /errors \( 1 \)/i);
});
it('should display warnings', function () {
var elem = this.last.dom.querySelectorAll('[data-test=task-warnings]')[0];
assert.isDefined(elem);
assert.match(elem.textContent, /warnings \( 2 \)/i);
});
it('should display notices', function () {
var elem = this.last.dom.querySelectorAll('[data-test=task-notices]')[0];
assert.isDefined(elem);
assert.match(elem.textContent, /notices \( 3 \)/i);
});
});
describe('when task has no results', function () {
beforeEach(function (done) {
var req = {
method: 'GET',
endpoint: '/abc000000000000000000003'
};
this.navigate(req, done);
});
it('should send a 200 status', function () {
assert.strictEqual(this.last.status, 200);
});
it('should display a "Run" button', function () {
var elem = this.last.dom.querySelectorAll('[data-test=run-task]');
assert.strictEqual(elem.length, 1);
assert.strictEqual(elem[0].getAttribute('href'), '/abc000000000000000000003/run');
});
it('should display a message indicating that there are no results', function () {
var alert = this.last.dom.querySelectorAll('[data-test=alert]')[0];
assert.isDefined(alert);
assert.match(alert.textContent, /there are no results to show/i);
});
});
});

View File

@@ -0,0 +1,31 @@
/* global beforeEach, describe, it */
/* jshint maxlen: false, maxstatements: false */
'use strict';
var assert = require('proclaim');
describe('GET /<task-id>/run', function () {
beforeEach(function (done) {
var req = {
method: 'GET',
endpoint: '/abc000000000000000000001/run'
};
this.navigate(req, done);
});
it('should send a 200 status', function () {
assert.strictEqual(this.last.status, 200);
});
it('should redirect me to the task page', function () {
assert.strictEqual(this.last.request.uri.pathname, '/abc000000000000000000001');
});
it('should display a success message', function () {
var alert = this.last.dom.querySelectorAll('[data-test=alert]')[0];
assert.isDefined(alert);
assert.match(alert.textContent, /new results are being generated/i);
});
});

36
test/functional/setup.js Normal file
View File

@@ -0,0 +1,36 @@
/* global afterEach, before */
/* jshint maxlen: false, maxstatements: false */
'use strict';
var config = require('../../config/test.json');
var createNavigator = require('./helper/navigate');
var createWebserviceClient = require('./helper/webservice');
var loadFixtures = require('pa11y-webservice/data/fixture/load');
var request = require('request');
// Run before all tests
before(function (done) {
this.baseUrl = 'http://localhost:' + config.port;
this.last = {};
this.navigate = createNavigator(this.baseUrl, this.last);
this.webservice = createWebserviceClient(config);
assertTestAppIsRunning(this.baseUrl, function () {
loadFixtures('test', config.webservice, done);
});
});
// Run after each test
afterEach(function (done) {
loadFixtures('test', config.webservice, done);
});
// Check that the test application is running, and exit if not
function assertTestAppIsRunning (url, done) {
request(url, function (err) {
if (err) {
console.error('Error: Test app not started; run with `grunt start-test`');
process.exit(1);
}
done();
});
}

View File

@@ -1,7 +1,7 @@
{{#content "title"}}pa11y-dashboard{{/content}} {{#content "title"}}pa11y-dashboard{{/content}}
{{#if siteMessage}} {{#if siteMessage}}
<div class="col-md-12 clearfix"> <div class="col-md-12 clearfix" data-test="alert">
<div class="alert alert-info site-message"> <div class="alert alert-info site-message">
<h3 class="crunch-top"><span class="pull-left glyphicon glyphicon-exclamation-sign"></span> Important</h3> <h3 class="crunch-top"><span class="pull-left glyphicon glyphicon-exclamation-sign"></span> Important</h3>
<p class="h5">{{siteMessage}}</p> <p class="h5">{{siteMessage}}</p>
@@ -10,7 +10,7 @@
{{/if}} {{/if}}
{{#deleted}} {{#deleted}}
<div class="col-md-12 clearfix"> <div class="col-md-12 clearfix" data-test="alert">
<div class="alert alert-info"> <div class="alert alert-info">
<button aria-hidden="true" data-dismiss="alert" class="close" type="button">×</button> <button aria-hidden="true" data-dismiss="alert" class="close" type="button">×</button>
<strong>Bye Bye URL</strong> <strong>Bye Bye URL</strong>

View File

@@ -3,14 +3,14 @@
Add a new URL Add a new URL
{{/content}} {{/content}}
<form role="form" class="col-md-12" action="/new" method="post"> <form role="form" class="col-md-12" action="/new" method="post" data-test="new-url-form">
<div class="legend"> <div class="legend">
<h1 class="h2 crunch-top">Add a new URL</h1> <h1 class="h2 crunch-top">Add a new URL</h1>
</div> </div>
{{#error}} {{#error}}
<div class="col-md-12 clearfix"> <div class="col-md-12 clearfix" data-test="error">
<div class="alert alert-danger"> <div class="alert alert-danger">
<strong>Oh my gosh!</strong> <strong>Oh my gosh!</strong>
<p>{{.}}</p> <p>{{.}}</p>

View File

@@ -12,10 +12,14 @@
<div class="action-buttons col-md-12 col-sm-6 clearfix"> <div class="action-buttons col-md-12 col-sm-6 clearfix">
<div class="row"> <div class="row">
<div class="col-md-12 col-sm-6 col-xs-12"> <div class="col-md-12 col-sm-6 col-xs-12">
<a href="{{mainResult.hrefCsv}}" class="btn-full-width btn btn-default">Download CSV <span class="glyphicon glyphicon-download"></span></a> <a href="{{mainResult.hrefCsv}}" class="btn-full-width btn btn-default" data-test="download-csv">
Download CSV <span class="glyphicon glyphicon-download"></span>
</a>
</div> </div>
<div class="col-md-12 col-sm-6 col-xs-12"> <div class="col-md-12 col-sm-6 col-xs-12">
<a href="{{mainResult.hrefJson}}" class="btn-full-width btn btn-default">Download JSON <span class="glyphicon glyphicon-download"></span></a> <a href="{{mainResult.hrefJson}}" class="btn-full-width btn btn-default" data-test="download-json">
Download JSON <span class="glyphicon glyphicon-download"></span>
</a>
</div> </div>
</div> </div>
</div> </div>
@@ -37,7 +41,7 @@
<div class="col-md-9"> <div class="col-md-9">
{{#if mainResult.count.error}} {{#if mainResult.count.error}}
<div class="heading label-danger showing first" id="errors"> <div class="heading label-danger showing first" id="errors" data-test="task-errors">
<span data-role="expander" class="pull-right expander"> - </span> <span data-role="expander" class="pull-right expander"> - </span>
Errors ( {{mainResult.count.error}} ) Errors ( {{mainResult.count.error}} )
</div> </div>
@@ -58,7 +62,7 @@
{{/if}} {{/if}}
{{#if mainResult.count.warning}} {{#if mainResult.count.warning}}
<div class="heading label-warning" id="warnings"> <div class="heading label-warning" id="warnings" data-test="task-warnings">
<span data-role="expander" class="pull-right expander"> + </span> <span data-role="expander" class="pull-right expander"> + </span>
Warnings ( {{mainResult.count.warning}} ) Warnings ( {{mainResult.count.warning}} )
</div> </div>
@@ -79,7 +83,7 @@
{{/if}} {{/if}}
{{#if mainResult.count.notice}} {{#if mainResult.count.notice}}
<div class="heading label-info" id="notices"> <div class="heading label-info" id="notices" data-test="task-notices">
<span data-role="expander" class="pull-right expander"> + </span> <span data-role="expander" class="pull-right expander"> + </span>
Notices ( {{mainResult.count.notice}} ) Notices ( {{mainResult.count.notice}} )
</div> </div>

View File

@@ -7,7 +7,9 @@
</div> </div>
<div class="col-md-3 col-sm-3 text-right run-details"> <div class="col-md-3 col-sm-3 text-right run-details">
{{#unless readonly}} {{#unless readonly}}
<a href="{{task.hrefRun}}" class="btn btn-success">Run <span class="glyphicon glyphicon-play"></span></a> <a href="{{task.hrefRun}}" class="btn btn-success" data-test="run-task">
Run <span class="glyphicon glyphicon-play"></span>
</a>
{{/unless}} {{/unless}}
{{#if mainResult}} {{#if mainResult}}
<div class="date">Last run : {{date-format mainResult.date format="DD MMM YYYY"}}</div> <div class="date">Last run : {{date-format mainResult.date format="DD MMM YYYY"}}</div>

View File

@@ -8,14 +8,14 @@
<p class="supersize-me crunch">+</p> <p class="supersize-me crunch">+</p>
</span> </span>
{{else}} {{else}}
<a class="well task-card-link crunch-bottom" data-role="add-task" href="/new"> <a class="well task-card-link crunch-bottom" data-role="add-task" href="/new" data-test="add-task">
<p class="h3 crunch">Add new URL</p> <p class="h3 crunch">Add new URL</p>
<p class="supersize-me crunch">+</p> <p class="supersize-me crunch">+</p>
</a> </a>
{{/if}} {{/if}}
</li> </li>
{{#each tasks}} {{#each tasks}}
<li class="col-md-4 col-sm-6 task-card"> <li class="col-md-4 col-sm-6 task-card" data-test="task">
<a class="well task-card-link crunch-bottom" title="Details for URL {{simplify-url url}}" href="{{href}}"> <a class="well task-card-link crunch-bottom" title="Details for URL {{simplify-url url}}" href="{{href}}">
<p class="h3">{{name}}</p> <p class="h3">{{name}}</p>
<p class="h5">({{standard}})</p> <p class="h5">({{standard}})</p>

View File

@@ -3,7 +3,7 @@
Delete {{task.url}} ({{task.standard}}) Delete {{task.url}} ({{task.standard}})
{{/content}} {{/content}}
<form class="col-md-12" action="{{task.hrefDelete}}" method="post"> <form class="col-md-12" action="{{task.hrefDelete}}" method="post" data-test="delete-url-form">
<div class="legend"> <div class="legend">
<legend>Delete URL ({{simplify-url task.url}})</legend> <legend>Delete URL ({{simplify-url task.url}})</legend>
</div> </div>

View File

@@ -4,7 +4,7 @@
{{/content}} {{/content}}
{{#added}} {{#added}}
<div class="col-md-12 clearfix"> <div class="col-md-12 clearfix" data-test="alert">
<div class="alert alert-success"> <div class="alert alert-success">
<button aria-hidden="true" data-dismiss="alert" class="close" type="button">×</button> <button aria-hidden="true" data-dismiss="alert" class="close" type="button">×</button>
<strong>Whoop whoop!</strong> <strong>Whoop whoop!</strong>
@@ -14,7 +14,7 @@
{{/added}} {{/added}}
{{#running}} {{#running}}
<div class="col-md-12 clearfix"> <div class="col-md-12 clearfix" data-test="alert">
<div class="alert alert-success"> <div class="alert alert-success">
<button aria-hidden="true" data-dismiss="alert" class="close" type="button">×</button> <button aria-hidden="true" data-dismiss="alert" class="close" type="button">×</button>
<strong>New results incoming!</strong> <strong>New results incoming!</strong>
@@ -41,7 +41,7 @@
{{#if mainResult}} {{#if mainResult}}
{{> result}} {{> result}}
{{else}} {{else}}
<div class="col-md-12"> <div class="col-md-12" data-test="alert">
<div class="alert alert-info"> <div class="alert alert-info">
<h4>There are no results to show</h4> <h4>There are no results to show</h4>
<p>pa11y has not been run against this URL yet so there are no results to show.</p> <p>pa11y has not been run against this URL yet so there are no results to show.</p>