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

feat(a11yPlugin): add support for Tenon.io #1867

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
"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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

include a link for Tenon here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put it just below in the Tenon section, and the name itself is a URL. Since the Chrome version wasn't linked, I figured we were already covered.

*
* // 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: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm - can we actually just pass the entire options object to tenon, and just modify the 'src'? So here say something like:

tenonIO: {
  options: {} // See http://tenon.io/documentation/understanding-request-parameters.php
              // options.src will be added by the test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it. I did keep printAll outside of that options object because it's related to Protractor's output, not Tenon. I wanted to provide a way for developers to see the entire response for debugging purposes.

* // 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessary, but in the future it would be nice to actually time the calls to Tenon and insert a real duration here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Tenon response we get some info, can we make use of either of these properties?

 responseExecTime: '0.69',
 responseTime: '2015-02-27T19:27:59.060Z',

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

responseExecTime sounds OK - any idea what those units are?

Alternatively, we could just record a start time before we send the request.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Tenon docs it says it's "how long, in seconds, it took for us to test your document". Maybe we could start a timer and add that to it to get an estimation of the total running time? Maybe not necessary though.

});
}

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>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is testing that a long select gets shortened? I don't see that happening anywhere, but maybe I'm missing it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, it was the Chrome Dev Tools that needed shortening. Now it truncates element HTML strings to 200 characters when we return them from WebDriver.

</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>