-
Notifications
You must be signed in to change notification settings - Fork 2.3k
feat(a11yPlugin): add support for Tenon.io #1867
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like it. I did keep |
||
* // 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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', There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.