├── test ├── fixtures │ ├── 123 │ └── testing ├── expected │ ├── custom_options │ └── default_options ├── pages │ ├── test1.html │ └── morepages │ │ └── test2.html └── a11y_test.js ├── .travis.yml ├── .jshintrc ├── .gitignore ├── LICENSE-MIT ├── package.json ├── Gruntfile.js ├── README.md └── tasks └── a11y.js /test/fixtures/123: -------------------------------------------------------------------------------- 1 | 1 2 3 -------------------------------------------------------------------------------- /test/fixtures/testing: -------------------------------------------------------------------------------- 1 | Testing -------------------------------------------------------------------------------- /test/expected/custom_options: -------------------------------------------------------------------------------- 1 | Testing: 1 2 3 !!! -------------------------------------------------------------------------------- /test/expected/default_options: -------------------------------------------------------------------------------- 1 | Testing, 1 2 3. -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: 2 | - node_js 3 | node_js: 4 | - "0.10" 5 | -------------------------------------------------------------------------------- /test/pages/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | My test page 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/pages/morepages/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | My other test page 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "boss": true, 11 | "eqnull": true, 12 | "node": true 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Joao Figueiredo 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-a11y", 3 | "description": "Grunt wrapper for a11y", 4 | "version": "0.1.5", 5 | "homepage": "https://github.com/lucalanca/grunt-a11y", 6 | "author": { 7 | "name": "Joao Figueiredo", 8 | "email": "lucalanca2@hotmail.com", 9 | "url": "lucalanca.github.io" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/lucalanca/grunt-a11y.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/lucalanca/grunt-a11y/issues" 17 | }, 18 | "license": "MIT", 19 | "engines": { 20 | "node": ">= 0.8.0" 21 | }, 22 | "scripts": { 23 | "test": "grunt test" 24 | }, 25 | "devDependencies": { 26 | "grunt": "^1.0.1", 27 | "grunt-contrib-clean": "^1.0.0", 28 | "grunt-contrib-jshint": "^0.12.0", 29 | "grunt-contrib-nodeunit": "^1.0.0" 30 | }, 31 | "peerDependencies": { 32 | "grunt": "^1.0.1" 33 | }, 34 | "keywords": [ 35 | "gruntplugin", "accessibility", "a11y" 36 | ], 37 | "dependencies": { 38 | "a11y": "^0.5.0", 39 | "chalk": "^1.0.0", 40 | "globby": "^4.0.0", 41 | "indent-string": "^2.1.0", 42 | "log-symbols": "^1.0.1", 43 | "protocolify": "^1.0.2", 44 | "q": "^1.1.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt-a11y 3 | * https://github.com/lucalanca/grunt-a11y 4 | * 5 | * Copyright (c) 2014 Joao Figueiredo 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | module.exports = function(grunt) { 12 | 13 | // Project configuration. 14 | grunt.initConfig({ 15 | jshint: { 16 | all: [ 17 | 'Gruntfile.js', 18 | 'tasks/*.js', 19 | '<%= nodeunit.tests %>' 20 | ], 21 | options: { 22 | jshintrc: '.jshintrc' 23 | } 24 | }, 25 | 26 | // Before generating any new files, remove any previously-created files. 27 | clean: { 28 | tests: ['tmp'] 29 | }, 30 | 31 | // Configuration to be run (and then tested). 32 | a11y: { 33 | default_options: { 34 | options: { 35 | urls: ['www.google.com'] 36 | } 37 | }, 38 | setViewportSize: { 39 | options: { 40 | urls: ['www.google.com'], 41 | viewportSize: '360x640' 42 | } 43 | }, 44 | simulate_fail_warn: { 45 | options: { 46 | urls: ['www.google.com'] 47 | } 48 | }, 49 | simulate_fail_fatal: { 50 | options: { 51 | urls: ['www.google.com'], 52 | failOnError: true 53 | } 54 | }, 55 | no_glob_pattern: { 56 | options: { 57 | urls: ['test/pages/test1.html'] 58 | } 59 | }, 60 | glob_pattern: { 61 | options: { 62 | urls: ['test/pages/**/*.html'] 63 | } 64 | } 65 | }, 66 | 67 | // Unit tests. 68 | nodeunit: { 69 | tests: ['test/*_test.js'] 70 | } 71 | 72 | }); 73 | 74 | // Actually load this plugin's task(s). 75 | grunt.loadTasks('tasks'); 76 | 77 | // These plugins provide necessary tasks. 78 | grunt.loadNpmTasks('grunt-contrib-jshint'); 79 | grunt.loadNpmTasks('grunt-contrib-clean'); 80 | grunt.loadNpmTasks('grunt-contrib-nodeunit'); 81 | 82 | // Whenever the "test" task is run, first clean the "tmp" dir, then run this 83 | // plugin's task(s), then test the result. 84 | grunt.registerTask('test', ['clean', 'a11y', 'nodeunit']); 85 | 86 | // By default, lint and run all tests. 87 | grunt.registerTask('default', ['jshint', 'test']); 88 | 89 | }; 90 | -------------------------------------------------------------------------------- /test/a11y_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var grunt = require('grunt'); 4 | var path = require('path'); 5 | var exec = require('child_process').exec; 6 | var execOptions = { 7 | cwd: path.join(__dirname, '..') 8 | }; 9 | 10 | 11 | /* 12 | ======== A Handy Little Nodeunit Reference ======== 13 | https://github.com/caolan/nodeunit 14 | 15 | Test methods: 16 | test.expect(numAssertions) 17 | test.done() 18 | Test assertions: 19 | test.ok(value, [message]) 20 | test.equal(actual, expected, [message]) 21 | test.notEqual(actual, expected, [message]) 22 | test.deepEqual(actual, expected, [message]) 23 | test.notDeepEqual(actual, expected, [message]) 24 | test.strictEqual(actual, expected, [message]) 25 | test.notStrictEqual(actual, expected, [message]) 26 | test.throws(block, [error], [message]) 27 | test.doesNotThrow(block, [error], [message]) 28 | test.ifError(value) 29 | */ 30 | 31 | exports.a11y = { 32 | setUp: function(done) { 33 | // setup here if necessary 34 | done(); 35 | }, 36 | default_options: function(test) { 37 | // test.expect(1); 38 | 39 | // var actual = grunt.file.read('tmp/default_options'); 40 | var expected = grunt.file.read('test/expected/default_options'); 41 | // test.equal(actual, expected, 'should describe what the default behavior is.'); 42 | test.equal(1, 1, 'as'); 43 | 44 | test.done(); 45 | }, 46 | custom_options: function(test) { 47 | // test.expect(1); 48 | 49 | // var actual = grunt.file.read('tmp/custom_options'); 50 | var expected = grunt.file.read('test/expected/custom_options'); 51 | // test.equal(actual, expected, 'should describe what the custom option(s) behavior is.'); 52 | test.equal(1, 1, 'as'); 53 | test.done(); 54 | }, 55 | no_glob_pattern: function(test) { 56 | test.expect(1); 57 | exec('grunt a11y:no_glob_pattern', execOptions, function(error, stdout) { 58 | test.equal( 59 | stdout.indexOf('Done, without errors') > -1, 60 | true, 61 | 'Should support ordinary urls' 62 | ); 63 | test.done(); 64 | }); 65 | }, 66 | glob_pattern: function(test) { 67 | test.expect(2); 68 | exec('grunt a11y:glob_pattern', execOptions, function(error, stdout) { 69 | test.equal( 70 | stdout.indexOf('Done, without errors') > -1, 71 | true, 72 | 'Should support glob patterns as urls' 73 | ); 74 | test.equal( 75 | stdout.split('Report for').length === 3, 76 | true, 77 | 'Should discover two files' 78 | ); 79 | test.done(); 80 | }); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grunt-a11y [![Build Status](https://travis-ci.org/lucalanca/grunt-a11y.svg?branch=master)](https://travis-ci.org/lucalanca/grunt-a11y) [![devDependency Status](https://david-dm.org/lucalanca/grunt-a11y/dev-status.svg)](https://david-dm.org/lucalanca/grunt-a11y#info=devDependencies) [![Dependency Status](https://david-dm.org/lucalanca/grunt-a11y.svg)](https://david-dm.org/lucalanca/grunt-a11y) 2 | 3 | > Grunt wrapper for [a11y](https://github.com/addyosmani/a11y), automate your accessibility audits 4 | 5 | ## Getting Started 6 | This plugin requires Grunt `~0.4.5` 7 | 8 | If you haven't used [Grunt](http://gruntjs.com/) before, be sure to check out the [Getting Started](http://gruntjs.com/getting-started) guide, as it explains how to create a [Gruntfile](http://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins. Once you're familiar with that process, you may install this plugin with this command: 9 | 10 | ```shell 11 | npm install grunt-a11y --save-dev 12 | ``` 13 | 14 | Once the plugin has been installed, it may be enabled inside your Gruntfile with this line of JavaScript: 15 | 16 | ```js 17 | grunt.loadNpmTasks('grunt-a11y'); 18 | ``` 19 | 20 | ## The "a11y" task 21 | 22 | ### Overview 23 | In your project's Gruntfile, add a section named `a11y` to the data object passed into `grunt.initConfig()`. 24 | 25 | ```js 26 | grunt.initConfig({ 27 | a11y: { 28 | dev: { 29 | options: { 30 | urls: ['www.twitter.com', 'www.google.com', 'dist/**/*.html'] 31 | } 32 | } 33 | } 34 | }); 35 | ``` 36 | 37 | ### Options 38 | 39 | #### options.urls 40 | Type: `Array` 41 | Default value: `[]` 42 | 43 | An Array of strings representing the url's to process. Supports globbing. 44 | 45 | #### options.failOnError 46 | Type: `Boolean` 47 | Default value: `false` 48 | 49 | If set to true, the grunt task fails when there is an accessibility error in one of the audits. 50 | 51 | #### options.viewportSize 52 | Type: `String` 53 | Default value: `1024x768` 54 | 55 | Sets the viewport size 56 | 57 | #### options.vebose 58 | Type: `Boolean` 59 | Default value: `false` 60 | 61 | Sets the viewport size 62 | 63 | 64 | ## Contributing 65 | In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using [Grunt](http://gruntjs.com/). 66 | 67 | 68 | ### Contributors 69 | - [lucalanca](https://github.com/lucalanca) 70 | - [jrcryer](https://github.com/jrcryer) 71 | 72 | ## Release History 73 | 74 | 0.1.5 Adds `verbose` flag. [#19](https://github.com/lucalanca/grunt-a11y/pull/19) Thanks to [@srescio](https://github.com/srescio). 75 | 76 | 0.1.4 Makes `protocolify` and `globby` part of dependencies. [#13](https://github.com/lucalanca/grunt-a11y/pull/13)) Thanks to [@shortdiv](https://github.com/shortdiv) 77 | 78 | 0.1.3 Added globbing possibility to `urls`option (See [#12](https://github.com/lucalanca/grunt-a11y/issues/12)) Thanks to [@peterhaldbaek](https://github.com/peterhaldbaek) 79 | 80 | 0.1.2 Added `viewportSize` option (See [#10](https://github.com/lucalanca/grunt-a11y/issues/10)) 81 | 82 | 0.1.1 Added `failOnError` option (See [#2](https://github.com/lucalanca/grunt-a11y/issues/2)) 83 | 84 | 0.1.0 First release just wrapping a11y 85 | -------------------------------------------------------------------------------- /tasks/a11y.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt-a11y 3 | * https://github.com/lucalanca/grunt-a11y 4 | * 5 | * Copyright (c) 2014 Joao Figueiredo 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | var a11y = require('a11y'); 12 | var chalk = require('chalk'); 13 | var indent = require('indent-string'); 14 | var logSymbols = require('log-symbols'); 15 | var Q = require('q'); 16 | var globby = require('globby'); 17 | var protocolify = require('protocolify'); 18 | 19 | var fail = chalk.bold.red; 20 | var log = chalk.blue; 21 | var pass = chalk.green; 22 | 23 | module.exports = function(grunt) { 24 | 25 | /** 26 | * A promise-based wrapper on a11y. 27 | * @param {String} url 28 | * @param {String} viewportSize 29 | * @param {Boolean} verbose 30 | * @return {Promise} 31 | */ 32 | function a11yPromise (url, viewportSize, verbose) { 33 | var deferred = Q.defer(); 34 | a11y(url, {viewportSize: viewportSize}, function (err, reports) { 35 | if (err) { 36 | deferred.reject(new Error(err)); 37 | } else { 38 | deferred.resolve({url: url, reports: reports, verbose: verbose}); 39 | } 40 | }); 41 | return deferred.promise; 42 | } 43 | 44 | /** 45 | * Utility function that logs an audit in the console and 46 | * returns a boolean with the validity of the audit. 47 | * 48 | * @param {String} url 49 | * @param {a11y reports} reports 50 | * @return {Boolean} if the audit is valid 51 | */ 52 | function logReports (url, reports, verbose) { 53 | var passes = ''; 54 | var failures = ''; 55 | 56 | grunt.log.writeln(chalk.underline(chalk.cyan('\nReport for ' + url + '\n'))); 57 | reports.audit.forEach(function (el) { 58 | if (el.result === 'PASS') { 59 | passes += logSymbols.success + ' ' + el.heading + '\n'; 60 | } 61 | 62 | if (el.result === 'FAIL') { 63 | failures += logSymbols.error + ' ' + el.heading + '\n'; 64 | failures += el.elements + '\n\n'; 65 | } 66 | }); 67 | 68 | grunt.log.writeln(indent(failures, ' ', 2)); 69 | grunt.log.writeln(indent(passes , ' ', 2)); 70 | 71 | if (verbose) { 72 | grunt.log.writeln('VERBOSE OUTPUT\n', reports.audit); 73 | } 74 | 75 | return !failures.length; 76 | } 77 | 78 | // Please see the Grunt documentation for more information regarding task 79 | // creation: http://gruntjs.com/creating-tasks 80 | 81 | grunt.registerMultiTask('a11y', 'Grunt wrapper for a11y', function() { 82 | var done = this.async(); 83 | // Merge task-specific and/or target-specific options with these defaults. 84 | var options = this.options({ 85 | urls: [], 86 | failOnError: false, 87 | viewportSize: '1024x768', 88 | verbose: false 89 | }); 90 | 91 | var urls = globby.sync(options.urls, { 92 | nonull: true 93 | }).map(protocolify); 94 | var a11yPromises = urls.map(function (url) { 95 | return a11yPromise(url, options.viewportSize, options.verbose); 96 | }); 97 | 98 | a11yPromises.forEach(function (f) { 99 | f.then(function (audit) { 100 | var valid = logReports(audit.url, audit.reports, audit.verbose); 101 | if (!valid) { 102 | if (options.failOnError) { 103 | grunt.fail.fatal('FATAL: Audit failed for ' + audit.url); 104 | } else { 105 | grunt.log.error('WARN: Audit failed for ' + audit.url); 106 | } 107 | } 108 | }).catch(function (error) { 109 | if (options.failOnError) { 110 | grunt.fail.fatal(error); 111 | } else { 112 | grunt.log.error(error); 113 | } 114 | }); 115 | }); 116 | 117 | Q.all(a11yPromises).then(done); 118 | }); 119 | 120 | }; 121 | --------------------------------------------------------------------------------