Compare commits

...

47 Commits
1.0.0 ... 1.2.0

Author SHA1 Message Date
Rowan Manning
b1de71f59c Version 1.2.0 2013-12-12 14:39:19 +00:00
Rowan Manning
4e19b125e0 Add a count for ignored rules 2013-12-12 14:36:14 +00:00
perryharlock
d3148d4194 Add HTML Codesniffer links 2013-12-12 14:17:54 +00:00
Rowan Manning
1ad95627ee Don't trim trailing spaces in markdown 2013-12-12 09:38:21 +00:00
Rowan Manning
4296effbc1 Refactor to reduce repetition 2013-12-11 14:12:25 +00:00
Rowan Manning
917057a109 Display the ignore rules for results, not tasks 2013-12-11 14:06:51 +00:00
Rowan Manning
c8b97df4ee Merge branch 'edit-page' of github.com:nature/pa11y-dashboard into develop 2013-12-02 15:32:40 +00:00
perryharlock
724175ef66 Add number of ignore rules to result page 2013-12-02 11:58:01 +00:00
perryharlock
b2273234ce Merge branch 'edit-page' of github.com:nature/pa11y-dashboard into edit-page 2013-11-29 15:14:31 +00:00
perryharlock
0546175809 Change link decriptions for editing and delting a task 2013-11-29 15:14:22 +00:00
Rowan Manning
52a923ace3 Write basic tests for task editing 2013-11-28 13:59:27 +00:00
Rowan Manning
786922e592 Add an .editorconfig file 2013-11-28 13:28:28 +00:00
Rowan Manning
4ff53093f4 Link breadcrumb on task sub-pages 2013-11-28 08:59:05 +00:00
Rowan Manning
ce77e57659 Fix an issue with saving empty ignore rules 2013-11-28 08:54:40 +00:00
Rowan Manning
4a7800e288 Wire up task editing 2013-11-27 16:47:14 +00:00
Rowan Manning
5b1963599b Update pa11y-webservice to include edit endpoint 2013-11-27 16:38:19 +00:00
perryharlock
1b36dc12ea make dropdowns appear on hover for non js users 2013-11-27 16:37:52 +00:00
perryharlock
963ab33cce Make edit route work and add options dropdown 2013-11-27 16:21:18 +00:00
perryharlock
b95238645a Merge branch 'develop' of github.com:nature/pa11y-dashboard into edit-page 2013-11-27 14:11:45 +00:00
perryharlock
8657138e13 Fix linting erros 2013-11-27 13:58:55 +00:00
perryharlock
f580f00d4c Reduce font size for tooltips in graph 2013-11-27 13:40:27 +00:00
perryharlock
fdc631e3ca Merge branch 'develop' of github.com:nature/pa11y-dashboard into develop 2013-11-27 13:38:06 +00:00
perryharlock
98dafba137 Small tweak to tooltip appearance on graph 2013-11-27 13:37:42 +00:00
Rowan Manning
fd32a5d894 Cache-bust the CSS and JS 2013-11-27 13:29:19 +00:00
perryharlock
68a3e26472 Disable fields that are not editable 2013-11-27 11:40:22 +00:00
Rowan Manning
bb197744fd Add dates to graph tooltips 2013-11-27 11:31:42 +00:00
perryharlock
ae6b551f54 Merge branch 'develop' of github.com:nature/pa11y-dashboard into edit-page 2013-11-27 11:30:59 +00:00
perryharlock
16231a3609 Amend x-axis label width 2013-11-27 11:27:31 +00:00
perryharlock
74dd5b18c9 Add route and view for edit page 2013-11-27 11:13:19 +00:00
perryharlock
5c10261c56 Fix x-axis - stop multiple appearances of dates 2013-11-26 11:36:25 +00:00
perryharlock
e13de45e4a Fix lint error 2013-11-25 16:37:50 +00:00
perryharlock
9383de3410 Amend xaxis mode to time from category to try to reduce x axis clutter 2013-11-25 16:19:01 +00:00
perryharlock
7c2647653d issue 57 - make y axis display ticks as integers 2013-11-25 11:27:36 +00:00
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
46 changed files with 1181 additions and 118 deletions

13
.editorconfig Normal file
View File

@@ -0,0 +1,13 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
# Config files
config/development.json
config/production.json
config/test.json
# Generated npm files
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: {
development: {
options: {
@@ -43,6 +52,15 @@ module.exports = function (grunt) {
NODE_ENV: 'development'
}
}
},
test: {
options: {
cwd: __dirname,
file: 'index.js',
env: {
NODE_ENV: 'test'
}
}
}
},
@@ -58,7 +76,7 @@ module.exports = function (grunt) {
'public/js/vendor/bootstrap/js/dropdown.js',
'public/js/vendor/bootstrap/js/tooltip.js',
'public/js/vendor/flot/jquery.flot.js',
'public/js/vendor/flot/jquery.flot.categories.js',
'public/js/vendor/flot/jquery.flot.time.js',
'public/js/vendor/flot/jquery.flot.selection.js',
'public/js/vendor/flot/jquery.flot.resize.js',
'public/js/site.js'
@@ -84,12 +102,15 @@ module.exports = function (grunt) {
grunt.loadNpmTasks('grunt-contrib-less');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-mocha-test');
grunt.loadNpmTasks('grunt-nodemon');
grunt.registerTask('lint', ['jshint']);
grunt.registerTask('test', ['mochaTest']);
grunt.registerTask('compile', ['less', 'uglify']);
grunt.registerTask('start', ['nodemon:development']);
grunt.registerTask('default', ['compile', 'lint']);
grunt.registerTask('ci', ['compile', 'lint']);
grunt.registerTask('start-test', ['nodemon:test']);
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.
**Current Version:** *1.0.0*
**Current Version:** *1.2.0*
**Build Status:** [![Build Status][travis-img]][travis]
**Node Version Support:** *0.10*
@@ -14,7 +15,7 @@ pa11y-dashboard is a web interface to the [pa11y][pa11y] accessibility reporter;
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`.
@@ -23,6 +24,7 @@ Once you have a local clone, you'll need to copy some sample configuration files
```sh
$ cp config/development.sample.json config/development.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).
@@ -32,6 +34,7 @@ Now that you've got your application configured, you can run in each mode with t
```sh
$ NODE_ENV=production node . # Run in production
$ 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).
@@ -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.
### webservice
*(string)* The base URL of the [pa11y-webservice][pa11y-webservice] instance you intend on using.
### port
*(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
*(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
-----------
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:
```sh
$ grunt # Run the lint and test tasks together
$ grunt lint # Run JSHint with the correct config
$ grunt compile # Compile front-end assets
$ grunt start # Run app in development mode, restarting if files change
$ grunt watch # Watch for file changes and compile assets
$ grunt # Run the lint and test tasks together
$ grunt lint # Run JSHint with the correct config
$ grunt compile # Compile front-end assets
$ grunt start # Run app in development mode, restarting if files change
$ 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.
@@ -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
[grunt]: http://gruntjs.com/
[mongo]: http://www.mongodb.org/
[node]: http://nodejs.org/
[pa11y]: https://github.com/nature/pa11y
[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

8
app.js
View File

@@ -13,11 +13,16 @@ module.exports = initApp;
function initApp (config, callback) {
config = defaultConfig(config);
var webserviceUrl = config.webservice;
if (typeof webserviceUrl == 'object') {
webserviceUrl = 'http://' + webserviceUrl.host + ':' + webserviceUrl.port + '/';
}
var app = new EventEmitter();
app.address = null;
app.express = express();
app.server = http.createServer(app.express);
app.webservice = createClient(config.webservice);
app.webservice = createClient(webserviceUrl);
// Compression
app.express.use(express.compress());
@@ -73,6 +78,7 @@ function initApp (config, callback) {
require('./route/new')(app);
require('./route/task/delete')(app);
require('./route/task/run')(app);
require('./route/task/edit')(app);
}
// Error handling

View File

@@ -1,6 +1,12 @@
{
"webservice": "http://localhost:3000/",
"port": 4000,
"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,
"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')));
});
// 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",
"version": "1.0.0",
"version": "1.2.0",
"private": true,
"description": "pa11y-dashboard is a visual web interface to the pa11y accessibility reporter",
@@ -25,7 +25,8 @@
"express": "~3.4",
"express-hbs": "~0.2",
"moment": "~2.2",
"pa11y-webservice-client-node": "~1.0",
"pa11y-webservice": "~1.3",
"pa11y-webservice-client-node": "~1.1",
"underscore": "~1.5"
},
"devDependencies": {
@@ -35,10 +36,14 @@
"grunt-contrib-less": "~0.8",
"grunt-contrib-uglify": "~0.2",
"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": {
"start": "node ."
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -17,8 +17,13 @@ $(document).ready(function(){
hoverable: true
},
xaxis: {
mode: 'categories',
tickLength: 0
mode: 'time',
tickLength: 0,
minTickSize: [1, 'day'],
timeformat: '%d %b'
},
yaxis: {
tickDecimals: 0
},
lines: {
lineWidth: 3
@@ -112,7 +117,7 @@ $(document).ready(function(){
}
function getXAxisLabel (el) {
return el.find('[data-role="category"]').html();
return el.find('[data-role="date"]').attr('data-value');
}
function storeDatum (el, label) {
@@ -167,12 +172,20 @@ $(document).ready(function(){
$.each(datasets, function(key, val) {
var lowerCaseValue = (val.label.substring(0, val.label.length - 1)).toLowerCase();
choiceContainer.append('<li class="text-center '+
lowerCaseValue +'"><div class="series-checkbox-container"><input type="checkbox" name="' + key +
'" checked="checked" id="id' + key +
'"/><label for="id' + key +
'"><span class="stat-type">' + val.label +
'</span></label></div></li>');
choiceContainer.append(
'<li class="text-center ' + lowerCaseValue + '">' +
'<div class="series-checkbox-container">' +
'<input type="checkbox"' +
'name="' + key + '" ' +
'checked="checked" ' +
'id="id' + key + '"' +
'/>' +
'<label for="id' + key + '">' +
'<span class="stat-type">' + val.label + '</span>' +
'</label>' +
'</div>' +
'</li>'
);
});
choiceContainer.find('input').click(plotAccordingToChoices);
@@ -192,7 +205,7 @@ $(document).ready(function(){
}
function showTooltip(x, y, contents) {
$('<div data-role="tooltip" class="tooltip in"><div class="tooltip-inner">' +
$('<div data-role="tooltip" class="tooltip tooltip-graph in"><div class="tooltip-inner">' +
contents +
'</div></div>').css({top: y + 5,left: x + 5}).appendTo('body').fadeIn(200);
}
@@ -203,8 +216,13 @@ $(document).ready(function(){
if (previousPoint != item.dataIndex) {
previousPoint = item.dataIndex;
$('[data-role="tooltip"]').remove();
var y = item.datapoint[1].toFixed(0);
var contents = '<h6 class="crunch">' + y + ' ' + item.series.label + '</h6>';
var count = item.datapoint[1].toFixed(0);
var date = $.plot.formatDate(new Date(item.datapoint[0]), '%d %b' +
'<small> (%H:%M)</small>');
var contents = '<p class="crunch">' +
date + '<br/>' +
count + ' ' + item.series.label +
'</[h6]>';
showTooltip(item.pageX, item.pageY, contents);
}
} else {

File diff suppressed because one or more lines are too long

View File

@@ -10,7 +10,7 @@
.date-links.hidden {
display:none !important;
}
.date-selector .btn-group:hover ul.date-links {
.btn-group:hover ul {
display:block !important;
}
table.hidden {

View File

@@ -36,11 +36,13 @@
.other-tasks {
.sr-only();
}
.action-buttons,
.task-stats,
.btn-full-width {
margin-bottom:0;
}
.action-buttons {
margin-bottom:20px;
}
.aside {
margin-bottom:15px;
}
@@ -54,7 +56,7 @@
float:none;
li {
width:33%;
width:25%;
}
}
}
@@ -75,6 +77,7 @@
}
.action-buttons {
margin-top:10px;
margin-bottom:0;
.btn-full-width {
margin-bottom:10px;
@@ -126,6 +129,11 @@
margin-bottom:20px;
}
}
.footer .nav {
li {
width:50%;
}
}
}
@media (max-width:479px) {
.graph {
@@ -170,4 +178,4 @@
}
}
}
}
}

View File

@@ -86,15 +86,11 @@
border-bottom-right-radius:3px;
margin-bottom:0;
}
}
.delete-button {
}
.options-button {
position:absolute;
top:5px;
right:20px;
color:@brand-danger;
}
.delete-button:hover {
color:lighten(@brand-danger, 8%);
}
.footer a, .breadcrumb a {
text-decoration:underline;
@@ -188,10 +184,10 @@
transition: background 0.5s;
-webkit-transition: background 0.5s;
}
.delete-button {
.options-button {
display:none;
}
&:hover .delete-button {
&:hover .options-button {
display:block;
}
.task-card-link:hover,
@@ -211,6 +207,9 @@
text-align:center;
color:@badge-color;
}
.dropdown-menu {
top:25px;
}
}
@@ -347,7 +346,7 @@ ul.date-links {
}
.series-checkboxes {
margin-top:15px;
li {
width:32%;
margin-right:2%;
@@ -383,6 +382,14 @@ ul.date-links {
margin-top:-24px;
margin-right:35px
}
.flot-x-axis {
.flot-tick-label {
max-width:45px !important;
}
}
.tooltip-graph {
font-size:12px;
}
/* New task page */
.standards-lists {
@@ -406,8 +413,3 @@ ul.date-links {
}
}
}
/* Sidebar */
.action-buttons {
margin-bottom:30px;
}

View File

@@ -15,7 +15,7 @@ function route (app) {
}
res.render('task/delete', {
task: presentTask(task),
isTaskPage: true
isTaskSubPage: true
});
});
});

72
route/task/edit.js Normal file
View File

@@ -0,0 +1,72 @@
'use strict';
var _ = require('underscore');
var presentTask = require('../../view/presenter/task');
var getStandards = require('../../data/standards');
module.exports = route;
// Route definition
function route (app) {
app.express.get('/:id/edit', function (req, res, next) {
app.webservice.task(req.params.id).get({}, function (err, task) {
if (err) {
return next();
}
var standards = getStandards().map(function (standard) {
if (standard.title === task.standard) {
standard.selected = true;
}
standard.rules = standard.rules.map(function (rule) {
if (task.ignore.indexOf(rule.name) !== -1) {
rule.ignored = true;
}
return rule;
});
return standard;
});
res.render('task/edit', {
edited: (typeof req.query.edited !== 'undefined'),
standards: standards,
task: presentTask(task),
isTaskSubPage: true
});
});
});
app.express.post('/:id/edit', function (req, res, next) {
app.webservice.task(req.params.id).get({}, function (err, task) {
if (err) {
return next();
}
req.body.ignore = req.body.ignore || [];
app.webservice.task(req.params.id).edit(req.body, function (err) {
if (err) {
task.name = req.body.name;
task.ignore = req.body.ignore;
var standards = getStandards().map(function (standard) {
if (standard.title === task.standard) {
standard.selected = true;
}
standard.rules = standard.rules.map(function (rule) {
if (task.ignore.indexOf(rule.name) !== -1) {
rule.ignored = true;
}
return rule;
});
return standard;
});
return res.render('task/edit', {
error: err,
standards: standards,
task: task,
isTaskSubPage: true
});
}
res.redirect('/' + req.params.id + '/edit?edited');
});
});
});
}

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,79 @@
/* 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 an "Edit" button for each task', function () {
var tasks = this.last.dom.querySelectorAll('[data-test=task]');
assert.strictEqual(tasks[0].querySelectorAll('[href="/abc000000000000000000001/edit"]').length, 1);
assert.strictEqual(tasks[1].querySelectorAll('[href="/abc000000000000000000002/edit"]').length, 1);
assert.strictEqual(tasks[2].querySelectorAll('[href="/abc000000000000000000003/edit"]').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 a "Run" button for each task', function () {
var tasks = this.last.dom.querySelectorAll('[data-test=task]');
assert.strictEqual(tasks[0].querySelectorAll('[href="/abc000000000000000000001/run"]').length, 1);
assert.strictEqual(tasks[1].querySelectorAll('[href="/abc000000000000000000002/run"]').length, 1);
assert.strictEqual(tasks[2].querySelectorAll('[href="/abc000000000000000000003/run"]').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,65 @@
/* 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');
});
it('should display a link back to the task page', function () {
assert.greaterThan(this.last.dom.querySelectorAll('[href="/abc000000000000000000001"]').length, 0);
});
});
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,102 @@
/* global beforeEach, describe, it */
/* jshint maxlen: false, maxstatements: false */
'use strict';
var assert = require('proclaim');
describe('GET /<task-id>/edit', function () {
beforeEach(function (done) {
var req = {
method: 'GET',
endpoint: '/abc000000000000000000001/edit'
};
this.navigate(req, done);
});
it('should send a 200 status', function () {
assert.strictEqual(this.last.status, 200);
});
it('should have an "Edit URL" form', function () {
var form = this.last.dom.querySelectorAll('[data-test=edit-url-form]')[0];
assert.isDefined(form);
assert.strictEqual(form.getAttribute('action'), '/abc000000000000000000001/edit');
assert.strictEqual(form.getAttribute('method'), 'post');
});
it('should display a link back to the task page', function () {
assert.greaterThan(this.last.dom.querySelectorAll('[href="/abc000000000000000000001"]').length, 0);
});
describe('"Edit URL" form', function () {
beforeEach(function () {
this.form = this.last.dom.querySelectorAll('[data-test=edit-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'), 'NPG Home');
});
it('should have a disabled "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'), 'nature.com');
assert.isDefined(field.getAttribute('disabled'));
});
it('should have a disabled "standard" field', function () {
var field = this.form.querySelectorAll('select[name=standard]')[0];
assert.isDefined(field);
assert.isDefined(field.getAttribute('disabled'));
});
it('should have "ignore" fields', function () {
var fields = this.form.querySelectorAll('input[name="ignore[]"]');
assert.isDefined(fields);
assert.notStrictEqual(fields.length, 0);
});
});
});
describe('POST /<task-id>/edit', function () {
beforeEach(function (done) {
var req = {
method: 'POST',
endpoint: '/abc000000000000000000001/edit',
body: {
name: 'foo',
ignore: ['bar', 'baz']
}
};
this.navigate(req, done);
});
it('should send a 200 status', function () {
assert.strictEqual(this.last.status, 200);
});
it('should edit the task', function (done) {
this.webservice.task('abc000000000000000000001').get({}, function (err, task) {
assert.strictEqual(task.name, 'foo');
assert.deepEqual(task.ignore, ['bar', 'baz']);
done();
});
});
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 saved/i);
});
});

View File

@@ -0,0 +1,96 @@
/* 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 an "Edit" button', function () {
assert.strictEqual(this.last.dom.querySelectorAll('[href="/abc000000000000000000001/edit"]').length, 1);
});
it('should display a "Delete" button', function () {
assert.strictEqual(this.last.dom.querySelectorAll('[href="/abc000000000000000000001/delete"]').length, 1);
});
it('should display a "Run" button', function () {
assert.strictEqual(this.last.dom.querySelectorAll('[href="/abc000000000000000000001/run"]').length, 1);
});
it('should display a "Download CSV" button for the latest result', function () {
assert.strictEqual(this.last.dom.querySelectorAll('[href="/abc000000000000000000001/def000000000000000000001.csv"]').length, 1);
});
it('should display a "Download JSON" button for the latest result', function () {
assert.strictEqual(this.last.dom.querySelectorAll('[href="/abc000000000000000000001/def000000000000000000001.json"]').length, 1);
});
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

@@ -17,4 +17,9 @@ function helper (register) {
return moment(context).fromNow();
});
register('date-timestamp', function (context) {
return moment(context).valueOf();
});
}

View File

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

View File

@@ -17,7 +17,7 @@
<meta name="viewport" content="width=device-width"/>
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Lato:400,700,900,400italic"/>
<link rel="stylesheet" href="/css/site.min.css"/>
<link rel="stylesheet" href="/css/site.min.css?v={{version}}"/>
</head>
<!--[if IE 7]><body class="ie7"><![endif]-->
@@ -47,7 +47,7 @@
{{> page-footer}}
<script type="text/javascript" src="/js/site.min.js"></script>
<script type="text/javascript" src="/js/site.min.js?v={{version}}"></script>
<!--[if lte IE 8]><script language="javascript" type="text/javascript" src="/js/vendor/flot/excanvas.min.js"></script><![endif]-->
</body>

View File

@@ -3,14 +3,14 @@
Add a new URL
{{/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">
<h1 class="h2 crunch-top">Add a new URL</h1>
</div>
{{#error}}
<div class="col-md-12 clearfix">
<div class="col-md-12 clearfix" data-test="error">
<div class="alert alert-danger">
<strong>Oh my gosh!</strong>
<p>{{.}}</p>

View File

@@ -7,13 +7,16 @@
{{#if isNewTaskPage}}
<li class="active">Add URL</li>
{{/if}}
{{#if isTaskPage}}
{{#if isTaskPage}}
<li class="active">{{task.name}}</li>
{{/if}}
{{#if isTaskSubPage}}
<li><a href="{{task.href}}">{{task.name}}</a></li>
{{/if}}
{{#if isResultPage}}
<li><a href="{{task.href}}">{{task.name}}</a></li>
<li class="active">Results for {{date-format mainResult.date format="DD MMM YYYY"}}</li>
{{/if}}
<li class="active">Results for {{date-format mainResult.date format="DD MMM YYYY"}}</li>
{{/if}}
</ol>
</div>
</div>

View File

@@ -23,7 +23,7 @@
<tbody>
{{#results}}
<tr data-role="url-stats">
<td data-role="category">{{date-format date format="DD MMM YYYY"}}</td>
<td data-value="{{date-timestamp date}}" data-role="date">{{date-format date format="DD MMM YYYY"}}</td>
<td class="text-center" data-label="error">{{count.error}}</td>
<td class="text-center" data-label="warning">{{count.warning}}</td>
<td class="text-center" data-label="notice">{{count.notice}}</td>

View File

@@ -1,10 +1,10 @@
<footer>
<div class="footer" role="contentinfo">
<div class="container">
<div class="col-md-7">
<div class="col-md-5">
<small>&copy; {{year}} Nature Publishing Group.<br/>pa11y dashboard is licensed under the GNU General Public License 3.0.<br/>Version {{version}}</small>
</div>
<div class="col-md-5 clearfix">
<div class="col-md-7 clearfix">
<ul class="crunch-bottom floated-list nav">
<li>
<a href="{{repo}}">GitHub Repo</a>
@@ -15,8 +15,11 @@
<li>
<a href="http://www.w3.org/TR/WCAG20/">WCAG 2.0 spec</a>
</li>
<li>
<a href="http://squizlabs.github.io/HTML_CodeSniffer/">HTML_CodeSniffer</a>
</li>
</ul>
</div>
</div>
</div>
</footer>
</footer>

View File

@@ -12,37 +12,36 @@
<div class="action-buttons col-md-12 col-sm-6 clearfix">
<div class="row">
<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 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 class="row">
<div class="col-md-12 col-sm-12 clearfix">
<div class="well">
<h4 class="crunch-top">View results in browser</h4>
<p class="crunch-bottom">pa11y uses HTML_CodeSniffer to find accessibility issues. <a href="http://squizlabs.github.io/HTML_CodeSniffer/">Use their bookmarklet</a> to view results on the page you are testing.</p>
</div>
</div>
</div>
<!-- ########### Functionality to be done ############# -->
<!-- List of other URLs -->
<!-- <div class="other-tasks well">
<h4 class="crunch-top ruled-sm">Your other tracked URLs</h4>
<p>No other URLs</p>
<ul class="list-unstyled crunch-bottom">
<li><a href="empty-task">rowanmanning.com</a></li>
<li><a href="task">nature.com</a></li>
</ul>
</div> -->
<!-- ##################### End ######################## -->
</div>
<div class="col-md-9">
{{#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>
Errors ( {{mainResult.count.error}} )
</div>
<div class="task-danger tasks-list collapse clearfix in">
<ul class="list-unstyled">
{{#mainResult.errors}}
<li>
@@ -58,7 +57,7 @@
{{/if}}
{{#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>
Warnings ( {{mainResult.count.warning}} )
</div>
@@ -79,7 +78,7 @@
{{/if}}
{{#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>
Notices ( {{mainResult.count.notice}} )
</div>
@@ -101,7 +100,7 @@
{{#if task.ignore.length}}
<div class="heading label-default">
<span data-role="expander" class="pull-right expander"> + </span>
Ignored Rules
Ignored Rules ( {{task.ignore.length}} )
</div>
<div class="task-default tasks-list collapse clearfix">
<ul class="list-unstyled">
@@ -119,4 +118,4 @@
{{else}}
<p class="heading label-default">You have no ignored rules.</p>
{{/if}}
</div>
</div>

View File

@@ -7,7 +7,15 @@
</div>
<div class="col-md-3 col-sm-3 text-right run-details">
{{#unless readonly}}
<a href="{{task.hrefRun}}" class="btn btn-success">Run <span class="glyphicon glyphicon-play"></span></a>
<div class="btn-group">
<button type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown">Options <span class="caret"></span></button>
<ul class="dropdown-menu pull-right" role="menu">
<li><a href="/{{task.id}}/edit">Edit this task</a></li>
<li><a href="/{{task.id}}/delete">Delete this task</a></li>
<li class="divider"></li>
<li><a href="{{task.hrefRun}}" data-test="run-task">Run pa11y</a></li>
</ul>
</div>
{{/unless}}
{{#if mainResult}}
<div class="date">Last run : {{date-format mainResult.date format="DD MMM YYYY"}}</div>

View File

@@ -2,20 +2,19 @@
<ul class="list-unstyled clearfix crunch-bottom">
<li class="col-md-4 col-sm-6 task-card add-task">
{{#if readonly}}
{{! TODO PERRY: make this look disabled }}
<span class="well task-card-link crunch-bottom">
<p class="h3 crunch">Add new URL</p>
<p class="supersize-me crunch">+</p>
</span>
{{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="supersize-me crunch">+</p>
</a>
{{/if}}
</li>
{{#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}}">
<p class="h3">{{name}}</p>
<p class="h5">({{standard}})</p>
@@ -33,7 +32,15 @@
{{/if}}
</a>
{{#unless ../readonly}}
<a title="Delete this URL" class="delete-button" href="{{hrefDelete}}"><span class="sr-only">Delete </span><span class="glyphicon glyphicon-remove"></span></a>
<div class="btn-group options-button text-right">
<button type="button" class="btn btn-info btn-xs dropdown-toggle" data-toggle="dropdown"><span class="sr-only">Options</span><span class="glyphicon glyphicon-cog"></span></button>
<ul class="dropdown-menu pull-right" role="menu">
<li><a href="{{href}}/edit">Edit this task</a></li>
<li><a href="{{href}}/delete">Delete this task</a></li>
<li class="divider"></li>
<li><a href="{{href}}/run" data-test="run-task">Run pa11y</a></li>
</ul>
</div>
{{/unless}}
</li>
{{/each}}

25
view/presenter/ignore.js Normal file
View File

@@ -0,0 +1,25 @@
'use strict';
var standardsArray = require('../../data/standards')();
var rules = createStandardDescriptionMap(standardsArray);
module.exports = presentIgnoreRules;
function presentIgnoreRules (ignore) {
return ignore.map(function (name) {
return {
name: name,
description: rules[name]
};
});
}
function createStandardDescriptionMap (standards) {
var map = {};
standards.forEach(function (standard) {
standard.rules.forEach(function (rule) {
map[rule.name] = rule.description;
});
});
return map;
}

View File

@@ -1,6 +1,7 @@
'use strict';
var _ = require('underscore');
var presentIgnoreRules = require('./ignore');
module.exports = presentResult;
@@ -14,6 +15,9 @@ function presentResult (result) {
// Parse date
result.date = new Date(result.date);
// Enhance the ignored rules
result.ignore = presentIgnoreRules(result.ignore);
// Split out message types
if (result.results) {
var groupedByType = _.groupBy(result.results, 'type');

View File

@@ -1,9 +1,8 @@
'use strict';
var _ = require('underscore');
var presentIgnoreRules = require('./ignore');
var presentResult = require('./result');
var standardsArray = require('../../data/standards')();
var rules = createStandardDescriptionMap(standardsArray);
module.exports = presentTask;
@@ -14,14 +13,10 @@ function presentTask (task) {
task.hrefDelete = '/' + task.id + '/delete';
task.hrefRun = '/' + task.id + '/run';
task.hrefJson = '/' + task.id + '.json';
task.hrefEdit = '/' + task.id + '/edit';
// Enhance the ignored rules
task.ignore = task.ignore.map(function (name) {
return {
name: name,
description: rules[name]
};
});
task.ignore = presentIgnoreRules(task.ignore);
// Present the last result if present
if (task.last_result) {
@@ -31,13 +26,3 @@ function presentTask (task) {
return task;
}
function createStandardDescriptionMap (standards) {
var map = {};
standards.forEach(function (standard) {
standard.rules.forEach(function (rule) {
map[rule.name] = rule.description;
});
});
return map;
}

View File

@@ -3,7 +3,7 @@
Delete {{task.url}} ({{task.standard}})
{{/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">
<legend>Delete URL ({{simplify-url task.url}})</legend>
</div>

87
view/task/edit.html Normal file
View File

@@ -0,0 +1,87 @@
{{#content "title"}}
Edit URL
{{/content}}
{{#edited}}
<div class="col-md-12 clearfix" data-test="alert">
<div class="alert alert-success">
<button aria-hidden="true" data-dismiss="alert" class="close" type="button">×</button>
<strong>Success!</strong>
<p>Your changes have been saved.</p>
</div>
</div>
{{/edited}}
<form role="form" class="col-md-12" action="/{{task.id}}/edit" method="post" data-test="edit-url-form">
<div class="legend">
<h1 class="h2 crunch-top">Edit URL</h1>
</div>
{{#error}}
<div class="col-md-12 clearfix" data-test="error">
<div class="row">
<div class="alert alert-danger">
<strong>Oh my gosh!</strong>
<p>{{.}}</p>
</div>
</div>
</div>
{{/error}}
<div class="form-group clearfix">
<div class="row">
<div class="col-md-8 col-sm-8 col-xs-10">
<label class="control-label" for="new-task-name">Name</label>
<input class="form-control" id="new-task-name" type="text" placeholder="E.g. My Home Page" name="name" value="{{task.name}}"/>
</div>
</div>
</div>
<div class="form-group clearfix">
<div class="row">
<div class="col-md-8 col-sm-8 col-xs-10">
<label class="control-label" for="new-task-url">URL</label>
<input class="form-control" id="new-task-url" type="url" placeholder="E.g. http://mysite.com/" name="url" value="{{task.url}}" disabled/>
</div>
</div>
</div>
<div class="form-group clearfix">
<div class="row">
<div class="col-md-4 col-sm-4 col-xs-6">
<label class="control-label" for="new-task-standard">Standard</label>
<select data-role="new-task-select" class="form-control" id="new-task-standard" name="standard" disabled>
{{#standards}}
<option {{#selected}}selected{{/selected}}>{{title}}</option>
{{/standards}}
</select>
</div>
</div>
</div>
<p class="control-label"><b>Ignore these rules</b> <a target="_blank" href="https://github.com/nature/pa11y/wiki/HTML-CodeSniffer-Rules">(full list of rules here)</a></p>
<div class="standards-lists">
{{#standards}}
<div data-role="standards-list" data-attr="{{title}}" class="form-group">
<p class="control-label rules-list-title ruled"><b>{{title}} Rules</b></p>
<ul class="list-unstyled">
{{#rules}}
<li>
<input class="pull-left" id="{{name}}" type="checkbox" name="ignore[]" value="{{name}}" {{#ignored}}checked{{/ignored}}/>
<label for="{{name}}" title="{{description}}" data-role="rules-tooltip" class="checkbox">
{{name}}
</label>
</li>
{{/rules}}
</ul>
</div>
{{/standards}}
</div>
<button type="submit" class="btn btn-success">Save changes <span class="glyphicon glyphicon-save"></span></button>
<a href="/{{task.id}}/edit" class="btn btn-primary">Undo <span class="glyphicon glyphicon-refresh"></span></a>
</form>

View File

@@ -4,7 +4,7 @@
{{/content}}
{{#added}}
<div class="col-md-12 clearfix">
<div class="col-md-12 clearfix" data-test="alert">
<div class="alert alert-success">
<button aria-hidden="true" data-dismiss="alert" class="close" type="button">×</button>
<strong>Whoop whoop!</strong>
@@ -14,7 +14,7 @@
{{/added}}
{{#running}}
<div class="col-md-12 clearfix">
<div class="col-md-12 clearfix" data-test="alert">
<div class="alert alert-success">
<button aria-hidden="true" data-dismiss="alert" class="close" type="button">×</button>
<strong>New results incoming!</strong>
@@ -41,7 +41,7 @@
{{#if mainResult}}
{{> result}}
{{else}}
<div class="col-md-12">
<div class="col-md-12" data-test="alert">
<div class="alert alert-info">
<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>