diff --git a/package.json b/package.json index 9218a4fd2..7d4d4f1dc 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ "optimist": "~0.6.0", "q": "1.0.0", "lodash": "~2.4.1", - "source-map-support": "~0.2.6" + "source-map-support": "~0.2.6", + "entities": "~1.1.1", + "accessibility-developer-tools": "~2.6.0" }, "devDependencies": { "expect.js": "~0.2.0", diff --git a/plugins/accessibility/index.js b/plugins/accessibility/index.js index c5f05beb3..a052208c3 100644 --- a/plugins/accessibility/index.js +++ b/plugins/accessibility/index.js @@ -2,13 +2,14 @@ var q = require('q'), fs = require('fs'), path = require('path'), _ = require('lodash'); + request = require('request'), + Entities = require('html-entities').XmlEntities; /** - * You can enable this plugin in your config file: - * - * // The Chrome Accessibility Developer Tools are currently - * // the only integration option. + * You can audit your website against the Chrome Accessibility Developer Tools, + * Tenon.io, or both by enabling this plugin in your config file: * + * // Chrome Accessibility Developer Tools: * exports.config = { * ... * plugins: [{ @@ -17,12 +18,33 @@ var q = require('q'), * }] * } * + * // Tenon.io: + * + * // Read about the Tenon.io settings and API requirements: + * // -http://tenon.io/documentation/overview.php + * + * exports.config = { + * ... + * plugins: [{ + * tenonIO: { + * options: { + * // See http://tenon.io/documentation/understanding-request-parameters.php + * // options.src will be added by the test. + * }, + * printAll: false, // whether the plugin should log API response + * }, + * chromeA11YDevTools: false, + * path: 'node_modules/protractor/plugins/accessiblity' + * }] + * } + * */ var AUDIT_FILE = path.join(__dirname, '../../node_modules/accessibility-developer-tools/dist/js/axs_testing.js'); +var TENON_URL = 'http://www.tenon.io/api/'; /** - * Checks the information returned by the accessibility audit and + * Checks the information returned by the accessibility audit(s) and * displays passed/failed results as console output. * * @param {Object} config The configuration file for the accessibility plugin @@ -32,76 +54,183 @@ var AUDIT_FILE = path.join(__dirname, '../../node_modules/accessibility-develope */ function teardown(config) { + var audits = []; + if (config.chromeA11YDevTools) { + audits.push(runChromeDevTools(config)); + } + // check for Tenon config and an actual API key, not the placeholder + if (config.tenonIO && /[A-Za-z][0-9]/.test(config.tenonIO.options.key)) { + audits.push(runTenonIO(config)); + } + return q.all(audits).then(function() { + return outputResults(); + }); +} - var data = fs.readFileSync(AUDIT_FILE, 'utf-8'); - data = data + ' return axs.Audit.run();'; +var testOut = {failedCount: 0, specResults: []}; +var entities = new Entities(); - var testOut = {failedCount: 0, specResults: []}, - elementPromises = []; +/** + * Audits page source against the Tenon API, if configured. Requires an API key: + * more information about licensing and configuration available at + * http://tenon.io/documentation/overview.php. + * + * @param {Object} config The configuration file for the accessibility plugin + * @return {q.Promise} A promise which resolves to the results of any passed or + * failed tests + * @private + */ +function runTenonIO(config) { - return browser.executeScript_(data, 'a11y developer tool rules').then(function(results) { + return browser.driver.getPageSource().then(function(source) { - var audit = results.map(function(result) { - var DOMElements = result.elements; - if (DOMElements !== undefined) { + var options = _.assign(config.tenonIO.options, {src: source}); - DOMElements.forEach(function(elem) { - // get elements from WebDriver, add to promises array - elementPromises.push( - elem.getOuterHtml().then(function(text) { - return { - code: result.rule.code, - list: text - }; - }) - ); - }); - result.elementCount = DOMElements.length; - } - return result; + // setup response as a deferred promise + var deferred = q.defer(); + request.post({ + url: TENON_URL, + form: options + }, + function(err, httpResponse, body) { + if (err) { return resolve.reject(new Error(err)); } + else { return deferred.resolve(JSON.parse(body)); } + }); + + return deferred.promise.then(function(response) { + return processTenonResults(response); + }); + }); + + function processTenonResults(response) { + var numResults = response.resultSet.length; + + testOut.failedCount = numResults; + + var testHeader = 'Tenon.io - '; + + if(numResults === 0) { + return testOut.specResults.push({ + description: testHeader + 'All tests passed!', + assertions: [{ + passed: true, + errorMsg: '' + }], + duration: 1 + }); + } + + if (config.tenonIO.printAll) { + console.log('\x1b[32m', testHeader + 'API response', '\x1b[39m'); + console.log(response); + } + + return response.resultSet.forEach(function(result) { + var errorMsg = result.errorDescription + '\n\n' + + '\t\t' +entities.decode(result.errorSnippet) + + '\n\n\t\t' +result.ref + '\n'; + + + testOut.specResults.push({ + description: testHeader + result.errorTitle, + assertions: [{ + passed: false, + errorMsg: errorMsg + }], + duration: 1 }); + }); + } +} - // Wait for element names to be fetched - return q.all(elementPromises).then(function(elementFailures) { - - audit.forEach(function(result, index) { - if (result.result === 'FAIL') { - result.passed = false; - testOut.failedCount++; - - var label = result.elementCount === 1 ? ' element ' : ' elements '; - result.output = '\n\t\t' + result.elementCount + label + 'failed:'; - - // match elements returned via promises - // by their failure codes - elementFailures.forEach(function(element, index) { - if (element.code === result.rule.code) { - result.output += '\n\t\t' + elementFailures[index].list; - } - }); - result.output += '\n\n\t\t' + result.rule.url; - } - else { - result.passed = true; - result.output = ''; - } - - testOut.specResults.push({ - description: result.rule.heading, - assertions: [{ - passed: result.passed, - errorMsg: result.output - }], - duration: 1 - }); +/** + * Audits page source against the Chrome Accessibility Developer Tools, if configured. + * + * @param {Object} config The configuration file for the accessibility plugin + * @return {q.Promise} A promise which resolves to the results of any passed or + * failed tests + * @private + */ +function runChromeDevTools() { + + var data = fs.readFileSync(AUDIT_FILE, 'utf-8'); + data = data + ' return axs.Audit.run();'; + + var elementPromises = [], + elementStringLength = 200; + + var testHeader = 'Chrome A11Y - '; + + return browser.executeScript_(data, 'a11y developer tool rules').then(function(results) { + + var audit = results.map(function(result) { + var DOMElements = result.elements; + if (DOMElements !== undefined) { + + DOMElements.forEach(function(elem) { + // get elements from WebDriver, add to promises array + elementPromises.push( + elem.getOuterHtml().then(function(text) { + return { + code: result.rule.code, + list: text.substring(0, elementStringLength) + }; + }) + ); }); + result.elementCount = DOMElements.length; + } + return result; + }); + + // Wait for element names to be fetched + return q.all(elementPromises).then(function(elementFailures) { + + return audit.forEach(function(result, index) { + if (result.result === 'FAIL') { + result.passed = false; + testOut.failedCount++; - if ((testOut.failedCount > 0) || (testOut.specResults.length > 0)) { - return testOut; + var label = result.elementCount === 1 ? ' element ' : ' elements '; + result.output = '\n\t\t' + result.elementCount + label + 'failed:'; + + // match elements returned via promises + // by their failure codes + elementFailures.forEach(function(element, index) { + if (element.code === result.rule.code) { + result.output += '\n\t\t' + elementFailures[index].list; + } + }); + result.output += '\n\n\t\t' + result.rule.url; + } + else { + result.passed = true; + result.output = ''; } + + testOut.specResults.push({ + description: testHeader + result.rule.heading, + assertions: [{ + passed: result.passed, + errorMsg: result.output + }], + duration: 1 + }); }); }); + }); +} + +/** + * Output results from either plugin configuration. + * + * @return {object} testOut An object containing number of failures and spec results + * @private + */ +function outputResults() { + if ((testOut.failedCount > 0) || (testOut.specResults.length > 0)) { + return testOut; } } diff --git a/plugins/accessibility/spec/failureConfig.js b/plugins/accessibility/spec/failureConfig.js index b40e5e0ee..134318309 100644 --- a/plugins/accessibility/spec/failureConfig.js +++ b/plugins/accessibility/spec/failureConfig.js @@ -6,6 +6,13 @@ exports.config = { specs: ['fail_spec.js'], baseUrl: env.baseUrl, plugins: [{ + tenonIO: { + options: { + key: 'YOUR_API_KEY', // ADD YOUR API KEY HERE + level: 'AA' // WCAG AA OR AAA + }, + printAll: false + }, chromeA11YDevTools: true, path: '../index.js' }] diff --git a/plugins/accessibility/spec/successConfig.js b/plugins/accessibility/spec/successConfig.js index 2d63b0575..a123a8d90 100644 --- a/plugins/accessibility/spec/successConfig.js +++ b/plugins/accessibility/spec/successConfig.js @@ -6,6 +6,13 @@ exports.config = { specs: ['success_spec.js'], baseUrl: env.baseUrl, plugins: [{ + tenonIO: { + options: { + key: 'YOUR_API_KEY', // ADD YOUR API KEY HERE + level: 'AA' // WCAG AA OR AAA + }, + printAll: false + }, chromeA11YDevTools: true, path: "../index.js" }] diff --git a/scripts/test.js b/scripts/test.js index e5b0da98b..dbdde99bf 100755 --- a/scripts/test.js +++ b/scripts/test.js @@ -129,13 +129,10 @@ executor.addCommandlineTest( 'node lib/cli.js plugins/accessibility/spec/failureConfig.js') .expectExitCode(1) .expectErrors([{ - message: '2 elements failed:'+ - '\n\t\t'+ - '\n\t\t' + message: '3 elements failed:' }, { - message: '1 element failed:'+ - '\n\t\t' + message: '1 element failed:' }]); executor.execute(); diff --git a/testapp/accessibility/badMarkup.html b/testapp/accessibility/badMarkup.html index 30af29864..b3df2808b 100644 --- a/testapp/accessibility/badMarkup.html +++ b/testapp/accessibility/badMarkup.html @@ -17,5 +17,6 @@
Hello {{firstName}} {{lastName}} + diff --git a/testapp/accessibility/index.html b/testapp/accessibility/index.html index 410d3b231..57fc37f03 100644 --- a/testapp/accessibility/index.html +++ b/testapp/accessibility/index.html @@ -1,6 +1,6 @@ - + Angular.js Example @@ -18,6 +18,6 @@
Hello {{firstName}} {{lastName}} - {{firstName}} {{lastName}} + Firstname Lastname