Skip to content
This repository has been archived by the owner on Jul 29, 2024. It is now read-only.

Commit

Permalink
feat(a11yPlugin): add support for Tenon.io
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcy Sutton authored and juliemr committed Feb 27, 2015
1 parent d26dc64 commit 13d34c9
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 70 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"html-entities": "~1.1.1",
"accessibility-developer-tools": "~2.6.0"
},
"devDependencies": {
"expect.js": "~0.2.0",
Expand Down
253 changes: 191 additions & 62 deletions plugins/accessibility/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [{
Expand All @@ -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
Expand All @@ -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;
}
}

Expand Down
7 changes: 7 additions & 0 deletions plugins/accessibility/spec/failureConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}]
Expand Down
7 changes: 7 additions & 0 deletions plugins/accessibility/spec/successConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}]
Expand Down
7 changes: 2 additions & 5 deletions scripts/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<input ng-model="firstName" type="text" class="ng-pristine ng-valid ng-touched">'+
'\n\t\t<input ng-model="lastName" type="text" class="ng-pristine ng-untouched ng-valid">'
message: '3 elements failed:'
},
{
message: '1 element failed:'+
'\n\t\t<img src="http://example.com/img.jpg">'
message: '1 element failed:'
}]);

executor.execute();
1 change: 1 addition & 0 deletions testapp/accessibility/badMarkup.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
<br>
Hello {{firstName}} {{lastName}}
<img src="http://example.com/img.jpg">
<select ng-options="v as ('v' + v.version + (v.isSnapshot ? ' (snapshot)' : '')) group by getGroupName(v) for v in docs_versions" ng-model="docs_version" ng-change="jumpToDocsVersion(docs_version)" class="docs-version-jump ng-pristine ng-valid ng-touched"><optgroup label="Latest"><option value="object:8" label="v1.4.0-local (snapshot)" selected="selected">v1.4.0-local (snapshot)</option><option value="object:15" label="v1.3.14">v1.3.14</option><option value="object:55" label="v1.2.28">v1.2.28</option><option value="object:86" label="v1.1.5">v1.1.5</option><option value="object:92" label="v1.0.8">v1.0.8</option></optgroup><optgroup label="v1.4.x"><option value="object:9" label="v1.4.0-beta.5">v1.4.0-beta.5</option><option value="object:10" label="v1.4.0-beta.4">v1.4.0-beta.4</option><option value="object:11" label="v1.4.0-beta.3">v1.4.0-beta.3</option><option value="object:12" label="v1.4.0-beta.2">v1.4.0-beta.2</option><option value="object:13" label="v1.4.0-beta.1">v1.4.0-beta.1</option><option value="object:14" label="v1.4.0-beta.0">v1.4.0-beta.0</option></optgroup><optgroup label="v1.3.x"><option value="object:16" label="v1.3.13">v1.3.13</option><option value="object:17" label="v1.3.12">v1.3.12</option><option value="object:18" label="v1.3.11">v1.3.11</option><option value="object:19" label="v1.3.10">v1.3.10</option><option value="object:20" label="v1.3.9">v1.3.9</option><option value="object:21" label="v1.3.8">v1.3.8</option><option value="object:22" label="v1.3.7">v1.3.7</option><option value="object:23" label="v1.3.6">v1.3.6</option><option value="object:24" label="v1.3.5">v1.3.5</option><option value="object:25" label="v1.3.4">v1.3.4</option><option value="object:26" label="v1.3.3">v1.3.3</option><option value="object:27" label="v1.3.2">v1.3.2</option><option value="object:28" label="v1.3.1">v1.3.1</option><option value="object:29" label="v1.3.0">v1.3.0</option><option value="object:30" label="v1.3.0-rc.5">v1.3.0-rc.5</option><option value="object:31" label="v1.3.0-rc.4">v1.3.0-rc.4</option><option value="object:32" label="v1.3.0-rc.3">v1.3.0-rc.3</option><option value="object:33" label="v1.3.0-rc.2">v1.3.0-rc.2</option><option value="object:34" label="v1.3.0-rc.1">v1.3.0-rc.1</option><option value="object:35" label="v1.3.0-rc.0">v1.3.0-rc.0</option><option value="object:36" label="v1.3.0-beta.19">v1.3.0-beta.19</option><option value="object:37" label="v1.3.0-beta.18">v1.3.0-beta.18</option><option value="object:38" label="v1.3.0-beta.17">v1.3.0-beta.17</option><option value="object:39" label="v1.3.0-beta.16">v1.3.0-beta.16</option><option value="object:40" label="v1.3.0-beta.15">v1.3.0-beta.15</option><option value="object:41" label="v1.3.0-beta.14">v1.3.0-beta.14</option><option value="object:42" label="v1.3.0-beta.13">v1.3.0-beta.13</option><option value="object:43" label="v1.3.0-beta.12">v1.3.0-beta.12</option><option value="object:44" label="v1.3.0-beta.11">v1.3.0-beta.11</option><option value="object:45" label="v1.3.0-beta.10">v1.3.0-beta.10</option><option value="object:46" label="v1.3.0-beta.9">v1.3.0-beta.9</option><option value="object:47" label="v1.3.0-beta.8">v1.3.0-beta.8</option><option value="object:48" label="v1.3.0-beta.7">v1.3.0-beta.7</option><option value="object:49" label="v1.3.0-beta.6">v1.3.0-beta.6</option><option value="object:50" label="v1.3.0-beta.5">v1.3.0-beta.5</option><option value="object:51" label="v1.3.0-beta.4">v1.3.0-beta.4</option><option value="object:52" label="v1.3.0-beta.3">v1.3.0-beta.3</option><option value="object:53" label="v1.3.0-beta.2">v1.3.0-beta.2</option><option value="object:54" label="v1.3.0-beta.1">v1.3.0-beta.1</option></optgroup><optgroup label="v1.2.x"><option value="object:56" label="v1.2.27">v1.2.27</option><option value="object:57" label="v1.2.26">v1.2.26</option><option value="object:58" label="v1.2.25">v1.2.25</option><option value="object:59" label="v1.2.24">v1.2.24</option><option value="object:60" label="v1.2.23">v1.2.23</option><option value="object:61" label="v1.2.22">v1.2.22</option><option value="object:62" label="v1.2.21">v1.2.21</option><option value="object:63" label="v1.2.20">v1.2.20</option><option value="object:64" label="v1.2.19">v1.2.19</option><option value="object:65" label="v1.2.18">v1.2.18</option><option value="object:66" label="v1.2.17">v1.2.17</option><option value="object:67" label="v1.2.16">v1.2.16</option><option value="object:68" label="v1.2.15">v1.2.15</option><option value="object:69" label="v1.2.14">v1.2.14</option><option value="object:70" label="v1.2.13">v1.2.13</option><option value="object:71" label="v1.2.12">v1.2.12</option><option value="object:72" label="v1.2.11">v1.2.11</option><option value="object:73" label="v1.2.10">v1.2.10</option><option value="object:74" label="v1.2.9">v1.2.9</option><option value="object:75" label="v1.2.8">v1.2.8</option><option value="object:76" label="v1.2.7">v1.2.7</option><option value="object:77" label="v1.2.6">v1.2.6</option><option value="object:78" label="v1.2.5">v1.2.5</option><option value="object:79" label="v1.2.4">v1.2.4</option><option value="object:80" label="v1.2.3">v1.2.3</option><option value="object:81" label="v1.2.2">v1.2.2</option><option value="object:82" label="v1.2.1">v1.2.1</option><option value="object:83" label="v1.2.0">v1.2.0</option><option value="object:84" label="v1.2.0-rc.3">v1.2.0-rc.3</option><option value="object:85" label="v1.2.0-rc.2">v1.2.0-rc.2</option></optgroup><optgroup label="v1.1.x"><option value="object:87" label="v1.1.4">v1.1.4</option><option value="object:88" label="v1.1.3">v1.1.3</option><option value="object:89" label="v1.1.2">v1.1.2</option><option value="object:90" label="v1.1.1">v1.1.1</option><option value="object:91" label="v1.1.0">v1.1.0</option></optgroup><optgroup label="v1.0.x"><option value="object:93" label="v1.0.7">v1.0.7</option><option value="object:94" label="v1.0.6">v1.0.6</option><option value="object:95" label="v1.0.5">v1.0.5</option><option value="object:96" label="v1.0.4">v1.0.4</option><option value="object:97" label="v1.0.3">v1.0.3</option><option value="object:98" label="v1.0.2">v1.0.2</option><option value="object:99" label="v1.0.1">v1.0.1</option><option value="object:100" label="v1.0.0">v1.0.0</option><option value="object:101" label="v1.0.0-rc2">v1.0.0-rc2</option></optgroup></select>
</body>
</html>
4 changes: 2 additions & 2 deletions testapp/accessibility/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!DOCTYPE html>

<html ng-app="xApp">
<html ng-app="xApp" lang="en">
<head>
<meta charset="utf-8">
<title>Angular.js Example</title>
Expand All @@ -18,6 +18,6 @@
<input ng-model="lastName" type="text" id="lastName" />
<br>
Hello {{firstName}} {{lastName}}
<img src="http://example.com/img.jpg" alt="{{firstName}} {{lastName}}">
<img src="http://example.com/img.jpg" alt="Firstname Lastname">
</body>
</html>

0 comments on commit 13d34c9

Please sign in to comment.