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

Commit 86f1913

Browse files
author
Marcy Sutton
committed
feat(a11yPlugin): add support for Tenon.io
1 parent fea8f2e commit 86f1913

File tree

7 files changed

+235
-71
lines changed

7 files changed

+235
-71
lines changed

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
"optimist": "~0.6.0",
2525
"q": "1.0.0",
2626
"lodash": "~2.4.1",
27-
"source-map-support": "~0.2.6"
27+
"source-map-support": "~0.2.6",
28+
"entities": "~1.1.1",
29+
"accessibility-developer-tools": "~2.6.0"
2830
},
2931
"devDependencies": {
3032
"expect.js": "~0.2.0",

plugins/accessibility/index.js

+198-63
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
var q = require('q'),
22
fs = require('fs'),
33
path = require('path'),
4-
_ = require('lodash');
4+
request = require('request'),
5+
Entities = require('html-entities').XmlEntities;
56

67
/**
7-
* You can enable this plugin in your config file:
8-
*
9-
* // The Chrome Accessibility Developer Tools are currently
10-
* // the only integration option.
8+
* You can audit your website against the Chrome Accessibility Developer Tools,
9+
* Tenon.io, or both by enabling this plugin in your config file:
1110
*
11+
* // Chrome Accessibility Developer Tools:
1212
* exports.config = {
1313
* ...
1414
* plugins: [{
@@ -17,12 +17,37 @@ var q = require('q'),
1717
* }]
1818
* }
1919
*
20+
* // Tenon.io:
21+
*
22+
* // Read about the Tenon.io settings and API requirements:
23+
* // -http://tenon.io/documentation/overview.php
24+
* // -http://tenon.io/documentation/understanding-request-parameters.php
25+
*
26+
* exports.config = {
27+
* ...
28+
* plugins: [{
29+
* tenonIO: {
30+
* options: {
31+
* key: 'YOUR_API_KEY_HERE', // only required parameter
32+
* level: 'AA' // WCAG AA OR AAA,
33+
* filter: '0', // one of 0, 20, 40, 60, 80, 100, defaults to 0
34+
* certainty: '0', // one of 0, 20, 40, 60, 80, 100, defaults to 0
35+
* store: '0', // 0 or 1, whether Tenon should store your results, depends on your membership plan
36+
* },
37+
* printAll: false, // whether the plugin should log API response
38+
* },
39+
* chromeA11YDevTools: false,
40+
* path: 'node_modules/protractor.plugins/accessiblity'
41+
* }]
42+
* }
43+
*
2044
*/
2145

2246
var AUDIT_FILE = path.join(__dirname, '../../node_modules/accessibility-developer-tools/dist/js/axs_testing.js');
47+
var TENON_URL = 'http://www.tenon.io/api/';
2348

2449
/**
25-
* Checks the information returned by the accessibility audit and
50+
* Checks the information returned by the accessibility audit(s) and
2651
* displays passed/failed results as console output.
2752
*
2853
* @param {Object} config The configuration file for the accessibility plugin
@@ -32,76 +57,186 @@ var AUDIT_FILE = path.join(__dirname, '../../node_modules/accessibility-develope
3257
*/
3358
function teardown(config) {
3459

60+
var audits = [];
61+
3562
if (config.chromeA11YDevTools) {
63+
audits.push(runChromeDevTools(config));
64+
}
65+
if (config.tenonIO) {
66+
audits.push(runTenonIO(config));
67+
}
68+
return q.all(audits).then(function() {
69+
return outputResults();
70+
});
71+
}
3672

37-
var data = fs.readFileSync(AUDIT_FILE, 'utf-8');
38-
data = data + ' return axs.Audit.run();';
73+
var testOut = {failedCount: 0, specResults: []};
74+
var entities = new Entities();
3975

40-
var testOut = {failedCount: 0, specResults: []},
41-
elementPromises = [];
76+
/**
77+
* Audits page source against the Tenon API, if configured. Requires an API key:
78+
* more information about licensing and configuration available at
79+
* http://tenon.io/documentation/overview.php.
80+
*
81+
* @param {Object} config The configuration file for the accessibility plugin
82+
* @return {q.Promise} A promise which resolves to the results of any passed or
83+
* failed tests
84+
* @private
85+
*/
86+
function runTenonIO(config) {
87+
var options = config.tenonIO.options;
88+
89+
return browser.driver.getPageSource().then(function(source) {
90+
// setup response as a deferred promise
91+
var deferred = q.defer();
92+
request.post({
93+
url: TENON_URL,
94+
form: {
95+
key: options.key,
96+
src: source,
97+
level: options.level,
98+
filter: options.filter,
99+
certainty: options.certainty,
100+
store: options.store
101+
}
102+
},
103+
function(err, httpResponse, body) {
104+
if (err) return resolve.reject(new Error(err));
105+
else return deferred.resolve(JSON.parse(body));
106+
});
42107

43-
return browser.executeScript_(data, 'a11y developer tool rules').then(function(results) {
108+
return deferred.promise.then(function(response) {
109+
return processTenonResults(response);
110+
});
111+
});
44112

45-
var audit = results.map(function(result) {
46-
var DOMElements = result.elements;
47-
if (DOMElements !== undefined) {
113+
function processTenonResults(response) {
114+
var numResults = response.resultSet.length;
48115

49-
DOMElements.forEach(function(elem) {
50-
// get elements from WebDriver, add to promises array
51-
elementPromises.push(
52-
elem.getOuterHtml().then(function(text) {
53-
return {
54-
code: result.rule.code,
55-
list: text
56-
};
57-
})
58-
);
59-
});
60-
result.elementCount = DOMElements.length;
61-
}
62-
return result;
116+
testOut.failedCount = numResults;
117+
118+
var testHeader = 'Tenon.io - ';
119+
120+
if(numResults === 0) {
121+
return testOut.specResults.push({
122+
description: testHeader + 'All tests passed!',
123+
assertions: [{
124+
passed: true,
125+
errorMsg: ''
126+
}],
127+
duration: 1
128+
});
129+
}
130+
131+
if (config.tenonIO.printAll) {
132+
console.log('\x1b[32m', testHeader + 'API response', '\x1b[39m');
133+
console.log(response);
134+
}
135+
136+
return response.resultSet.forEach(function(result) {
137+
var errorMsg = result.errorDescription + '\n\n' +
138+
'\t\t' +entities.decode(result.errorSnippet) +
139+
'\n\n\t\t' +result.ref + '\n';
140+
141+
testOut.specResults.push({
142+
description: testHeader + result.errorTitle,
143+
assertions: [{
144+
passed: false,
145+
errorMsg: errorMsg
146+
}],
147+
duration: 1
63148
});
149+
});
150+
}
151+
}
64152

65-
// Wait for element names to be fetched
66-
return q.all(elementPromises).then(function(elementFailures) {
67-
68-
audit.forEach(function(result, index) {
69-
if (result.result === 'FAIL') {
70-
result.passed = false;
71-
testOut.failedCount++;
72-
73-
var label = result.elementCount === 1 ? ' element ' : ' elements ';
74-
result.output = '\n\t\t' + result.elementCount + label + 'failed:';
75-
76-
// match elements returned via promises
77-
// by their failure codes
78-
elementFailures.forEach(function(element, index) {
79-
if (element.code === result.rule.code) {
80-
result.output += '\n\t\t' + elementFailures[index].list;
81-
}
82-
});
83-
result.output += '\n\n\t\t' + result.rule.url;
84-
}
85-
else {
86-
result.passed = true;
87-
result.output = '';
88-
}
89-
90-
testOut.specResults.push({
91-
description: result.rule.heading,
92-
assertions: [{
93-
passed: result.passed,
94-
errorMsg: result.output
95-
}],
96-
duration: 1
97-
});
153+
/**
154+
* Audits page source against the Chrome Accessibility Developer Tools, if configured.
155+
*
156+
* @param {Object} config The configuration file for the accessibility plugin
157+
* @return {q.Promise} A promise which resolves to the results of any passed or
158+
* failed tests
159+
* @private
160+
*/
161+
function runChromeDevTools() {
162+
163+
var data = fs.readFileSync(AUDIT_FILE, 'utf-8');
164+
data = data + ' return axs.Audit.run();';
165+
166+
var elementPromises = [],
167+
elementStringLength = 200;
168+
169+
var testHeader = 'Chrome A11Y - ';
170+
171+
return browser.executeScript_(data, 'a11y developer tool rules').then(function(results) {
172+
173+
var audit = results.map(function(result) {
174+
var DOMElements = result.elements;
175+
if (DOMElements !== undefined) {
176+
177+
DOMElements.forEach(function(elem) {
178+
// get elements from WebDriver, add to promises array
179+
elementPromises.push(
180+
elem.getOuterHtml().then(function(text) {
181+
return {
182+
code: result.rule.code,
183+
list: text.substring(0, elementStringLength)
184+
};
185+
})
186+
);
98187
});
188+
result.elementCount = DOMElements.length;
189+
}
190+
return result;
191+
});
192+
193+
// Wait for element names to be fetched
194+
return q.all(elementPromises).then(function(elementFailures) {
195+
196+
return audit.forEach(function(result, index) {
197+
if (result.result === 'FAIL') {
198+
result.passed = false;
199+
testOut.failedCount++;
99200

100-
if ((testOut.failedCount > 0) || (testOut.specResults.length > 0)) {
101-
return testOut;
201+
var label = result.elementCount === 1 ? ' element ' : ' elements ';
202+
result.output = '\n\t\t' + result.elementCount + label + 'failed:';
203+
204+
// match elements returned via promises
205+
// by their failure codes
206+
elementFailures.forEach(function(element, index) {
207+
if (element.code === result.rule.code) {
208+
result.output += '\n\t\t' + elementFailures[index].list;
209+
}
210+
});
211+
result.output += '\n\n\t\t' + result.rule.url;
212+
}
213+
else {
214+
result.passed = true;
215+
result.output = '';
102216
}
217+
218+
testOut.specResults.push({
219+
description: testHeader + result.rule.heading,
220+
assertions: [{
221+
passed: result.passed,
222+
errorMsg: result.output
223+
}],
224+
duration: 1
225+
});
103226
});
104227
});
228+
});
229+
}
230+
231+
/**
232+
* Output results from either plugin configuration.
233+
*
234+
* @return {object} testOut An object containing number of failures and spec results
235+
* @private
236+
*/
237+
function outputResults() {
238+
if ((testOut.failedCount > 0) || (testOut.specResults.length > 0)) {
239+
return testOut;
105240
}
106241
}
107242

plugins/accessibility/spec/failureConfig.js

+7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ exports.config = {
66
specs: ['fail_spec.js'],
77
baseUrl: env.baseUrl,
88
plugins: [{
9+
tenonIO: {
10+
options: {
11+
key: 'YOUR_API_KEY', // ADD YOUR API KEY HERE
12+
level: 'AA' // WCAG AA OR AAA
13+
},
14+
printAll: false
15+
},
916
chromeA11YDevTools: true,
1017
path: '../index.js'
1118
}]

plugins/accessibility/spec/successConfig.js

+7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ exports.config = {
66
specs: ['success_spec.js'],
77
baseUrl: env.baseUrl,
88
plugins: [{
9+
tenonIO: {
10+
options: {
11+
key: 'YOUR_API_KEY', // ADD YOUR API KEY HERE
12+
level: 'AA' // WCAG AA OR AAA
13+
},
14+
printAll: false
15+
},
916
chromeA11YDevTools: true,
1017
path: "../index.js"
1118
}]

scripts/test.js

+17-5
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,25 @@ executor.addCommandlineTest(
129129
'node lib/cli.js plugins/accessibility/spec/failureConfig.js')
130130
.expectExitCode(1)
131131
.expectErrors([{
132-
message: '2 elements failed:'+
133-
'\n\t\t<input ng-model="firstName" type="text" class="ng-pristine ng-valid ng-touched">'+
134-
'\n\t\t<input ng-model="lastName" type="text" class="ng-pristine ng-untouched ng-valid">'
132+
message: '3 elements failed:'
135133
},
136134
{
137-
message: '1 element failed:'+
138-
'\n\t\t<img src="http://example.com/img.jpg">'
135+
message: '1 element failed:'
136+
},
137+
{
138+
message: 'Language of the document has not been set.'
139+
},
140+
{
141+
message: 'All images must have an alt attribute.'
142+
},
143+
{
144+
message: 'Provide a label for all controls.'
145+
},
146+
{
147+
message: 'Provide a label for all controls.'
148+
},
149+
{
150+
message: 'Provide a label for all controls.'
139151
}]);
140152

141153
executor.execute();

testapp/accessibility/badMarkup.html

+1
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@
1717
<br>
1818
Hello {{firstName}} {{lastName}}
1919
<img src="http://example.com/img.jpg">
20+
<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>
2021
</body>
2122
</html>

testapp/accessibility/index.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<!DOCTYPE html>
22

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

0 commit comments

Comments
 (0)