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

Commit 13d34c9

Browse files
Marcy Suttonjuliemr
Marcy Sutton
authored andcommitted
feat(a11yPlugin): add support for Tenon.io
1 parent d26dc64 commit 13d34c9

File tree

7 files changed

+213
-70
lines changed

7 files changed

+213
-70
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+
"html-entities": "~1.1.1",
29+
"accessibility-developer-tools": "~2.6.0"
2830
},
2931
"devDependencies": {
3032
"expect.js": "~0.2.0",

plugins/accessibility/index.js

+191-62
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ var q = require('q'),
22
fs = require('fs'),
33
path = require('path'),
44
_ = require('lodash');
5+
request = require('request'),
6+
Entities = require('html-entities').XmlEntities;
57

68
/**
7-
* You can enable this plugin in your config file:
8-
*
9-
* // The Chrome Accessibility Developer Tools are currently
10-
* // the only integration option.
9+
* You can audit your website against the Chrome Accessibility Developer Tools,
10+
* Tenon.io, or both by enabling this plugin in your config file:
1111
*
12+
* // Chrome Accessibility Developer Tools:
1213
* exports.config = {
1314
* ...
1415
* plugins: [{
@@ -17,12 +18,33 @@ var q = require('q'),
1718
* }]
1819
* }
1920
*
21+
* // Tenon.io:
22+
*
23+
* // Read about the Tenon.io settings and API requirements:
24+
* // -http://tenon.io/documentation/overview.php
25+
*
26+
* exports.config = {
27+
* ...
28+
* plugins: [{
29+
* tenonIO: {
30+
* options: {
31+
* // See http://tenon.io/documentation/understanding-request-parameters.php
32+
* // options.src will be added by the test.
33+
* },
34+
* printAll: false, // whether the plugin should log API response
35+
* },
36+
* chromeA11YDevTools: false,
37+
* path: 'node_modules/protractor/plugins/accessiblity'
38+
* }]
39+
* }
40+
*
2041
*/
2142

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

2446
/**
25-
* Checks the information returned by the accessibility audit and
47+
* Checks the information returned by the accessibility audit(s) and
2648
* displays passed/failed results as console output.
2749
*
2850
* @param {Object} config The configuration file for the accessibility plugin
@@ -32,76 +54,183 @@ var AUDIT_FILE = path.join(__dirname, '../../node_modules/accessibility-develope
3254
*/
3355
function teardown(config) {
3456

57+
var audits = [];
58+
3559
if (config.chromeA11YDevTools) {
60+
audits.push(runChromeDevTools(config));
61+
}
62+
// check for Tenon config and an actual API key, not the placeholder
63+
if (config.tenonIO && /[A-Za-z][0-9]/.test(config.tenonIO.options.key)) {
64+
audits.push(runTenonIO(config));
65+
}
66+
return q.all(audits).then(function() {
67+
return outputResults();
68+
});
69+
}
3670

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

40-
var testOut = {failedCount: 0, specResults: []},
41-
elementPromises = [];
74+
/**
75+
* Audits page source against the Tenon API, if configured. Requires an API key:
76+
* more information about licensing and configuration available at
77+
* http://tenon.io/documentation/overview.php.
78+
*
79+
* @param {Object} config The configuration file for the accessibility plugin
80+
* @return {q.Promise} A promise which resolves to the results of any passed or
81+
* failed tests
82+
* @private
83+
*/
84+
function runTenonIO(config) {
4285

43-
return browser.executeScript_(data, 'a11y developer tool rules').then(function(results) {
86+
return browser.driver.getPageSource().then(function(source) {
4487

45-
var audit = results.map(function(result) {
46-
var DOMElements = result.elements;
47-
if (DOMElements !== undefined) {
88+
var options = _.assign(config.tenonIO.options, {src: source});
4889

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;
90+
// setup response as a deferred promise
91+
var deferred = q.defer();
92+
request.post({
93+
url: TENON_URL,
94+
form: options
95+
},
96+
function(err, httpResponse, body) {
97+
if (err) { return resolve.reject(new Error(err)); }
98+
else { return deferred.resolve(JSON.parse(body)); }
99+
});
100+
101+
return deferred.promise.then(function(response) {
102+
return processTenonResults(response);
103+
});
104+
});
105+
106+
function processTenonResults(response) {
107+
var numResults = response.resultSet.length;
108+
109+
testOut.failedCount = numResults;
110+
111+
var testHeader = 'Tenon.io - ';
112+
113+
if (numResults === 0) {
114+
return testOut.specResults.push({
115+
description: testHeader + 'All tests passed!',
116+
assertions: [{
117+
passed: true,
118+
errorMsg: ''
119+
}],
120+
duration: 1
121+
});
122+
}
123+
124+
if (config.tenonIO.printAll) {
125+
console.log('\x1b[32m', testHeader + 'API response', '\x1b[39m');
126+
console.log(response);
127+
}
128+
129+
return response.resultSet.forEach(function(result) {
130+
var errorMsg = result.errorDescription + '\n\n' +
131+
'\t\t' +entities.decode(result.errorSnippet) +
132+
'\n\n\t\t' +result.ref + '\n';
133+
134+
135+
testOut.specResults.push({
136+
description: testHeader + result.errorTitle,
137+
assertions: [{
138+
passed: false,
139+
errorMsg: errorMsg
140+
}],
141+
duration: 1
63142
});
143+
});
144+
}
145+
}
64146

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-
});
147+
/**
148+
* Audits page source against the Chrome Accessibility Developer Tools, if configured.
149+
*
150+
* @param {Object} config The configuration file for the accessibility plugin
151+
* @return {q.Promise} A promise which resolves to the results of any passed or
152+
* failed tests
153+
* @private
154+
*/
155+
function runChromeDevTools() {
156+
157+
var data = fs.readFileSync(AUDIT_FILE, 'utf-8');
158+
data = data + ' return axs.Audit.run();';
159+
160+
var elementPromises = [],
161+
elementStringLength = 200;
162+
163+
var testHeader = 'Chrome A11Y - ';
164+
165+
return browser.executeScript_(data, 'a11y developer tool rules').then(function(results) {
166+
167+
var audit = results.map(function(result) {
168+
var DOMElements = result.elements;
169+
if (DOMElements !== undefined) {
170+
171+
DOMElements.forEach(function(elem) {
172+
// get elements from WebDriver, add to promises array
173+
elementPromises.push(
174+
elem.getOuterHtml().then(function(text) {
175+
return {
176+
code: result.rule.code,
177+
list: text.substring(0, elementStringLength)
178+
};
179+
})
180+
);
98181
});
182+
result.elementCount = DOMElements.length;
183+
}
184+
return result;
185+
});
186+
187+
// Wait for element names to be fetched
188+
return q.all(elementPromises).then(function(elementFailures) {
189+
190+
return audit.forEach(function(result, index) {
191+
if (result.result === 'FAIL') {
192+
result.passed = false;
193+
testOut.failedCount++;
99194

100-
if ((testOut.failedCount > 0) || (testOut.specResults.length > 0)) {
101-
return testOut;
195+
var label = result.elementCount === 1 ? ' element ' : ' elements ';
196+
result.output = '\n\t\t' + result.elementCount + label + 'failed:';
197+
198+
// match elements returned via promises
199+
// by their failure codes
200+
elementFailures.forEach(function(element, index) {
201+
if (element.code === result.rule.code) {
202+
result.output += '\n\t\t' + elementFailures[index].list;
203+
}
204+
});
205+
result.output += '\n\n\t\t' + result.rule.url;
206+
}
207+
else {
208+
result.passed = true;
209+
result.output = '';
102210
}
211+
212+
testOut.specResults.push({
213+
description: testHeader + result.rule.heading,
214+
assertions: [{
215+
passed: result.passed,
216+
errorMsg: result.output
217+
}],
218+
duration: 1
219+
});
103220
});
104221
});
222+
});
223+
}
224+
225+
/**
226+
* Output results from either plugin configuration.
227+
*
228+
* @return {object} testOut An object containing number of failures and spec results
229+
* @private
230+
*/
231+
function outputResults() {
232+
if ((testOut.failedCount > 0) || (testOut.specResults.length > 0)) {
233+
return testOut;
105234
}
106235
}
107236

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

+2-5
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,10 @@ 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:'
139136
}]);
140137

141138
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)