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}}
+