-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add basic readme
- Loading branch information
Juliette Pretot
committed
Nov 21, 2019
1 parent
9882f26
commit cf289f9
Showing
2 changed files
with
212 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# Agnostic Axe | ||
|
||
Test your web application with the [axe-core](https://github.com/dequelabs/axe-core) accessibility testing library. Results will show in the browser console. | ||
|
||
## Installation | ||
|
||
You can install the module from NPM. | ||
|
||
```sh | ||
npm install --save-dev agnostic-axe | ||
``` | ||
|
||
## Usage | ||
|
||
```js | ||
if (process.env.NODE_ENV !== 'production') { | ||
import('agnostic-axe').then(AxeReporter => { | ||
const MyAxeReporter = new AxeReporter() | ||
MyAxeReporter.observe(document) | ||
}) | ||
} | ||
``` | ||
|
||
Be sure to only run the module in your development environment (as shown in the code above) or else your application will use more resources than necessary when in production. | ||
|
||
Once the `observe` method has been invoked, `MyAxeReporter` starts reporting accessibility defects to the browser console. It continously observes the passed node for changes. If a change has been detected, it will reanalyze the node and report any new accessibility defects. | ||
|
||
To stop observing changes, one can call the `disconnect` method. | ||
|
||
### Configuration | ||
|
||
The `AxeReporter` `observe` method takes three parameters: | ||
|
||
- `targetNode` (required). The node that should be observed & analyzed. | ||
- `debounceMs` (optional, defaults to `1000`). The number of milliseconds to wait for component updates to cease before performing an analysis of all the changes. | ||
- `axeOptions` (optional, defaults to `{}`). It is a configuration object for [axe-core](https://github.com/dequelabs/axe-core). Read about the object at https://github.com/dequelabs/axe-core/blob/master/doc/API.md#api-name-axeOptionsure. Note that `agnostic-axe` always runs [axe-core](https://github.com/dequelabs/axe-core) with the `reporter: 'v2'` option. | ||
|
||
## Credits | ||
|
||
Agnostic axe itself is merely a wrapper around [axe-core](https://github.com/dequelabs/axe-core) that employs a `MutationObserver` to detect DOM changes automatically. Most of its logic for formatting violations return by [axe-core](https://github.com/dequelabs/axe-core) is taken from [react-axe](https://github.com/dequelabs/react-axe). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,172 @@ | ||
// noop | ||
import axe from 'axe-core' | ||
|
||
// If requestIdleCallback is not supported, we fallback to setTimeout | ||
// Ref: https://developers.google.com/web/updates/2015/08/using-requestidlecallback | ||
const requestIdleCallback = | ||
window.requestIdleCallback || | ||
function(callback) { | ||
setTimeout(callback, 1) | ||
} | ||
|
||
// Define own debounce function to avoid excess dependencies | ||
// Ref: https://chrisboakes.com/how-a-javascript-debounce-function-works/ | ||
function debounce(callback, wait) { | ||
let timeout | ||
return (...args) => { | ||
clearTimeout(timeout) | ||
timeout = setTimeout(() => callback.apply(this, args), wait) | ||
} | ||
} | ||
|
||
class AxeReporter { | ||
constructor() { | ||
this.observe = this.observe.bind(this) | ||
this.disconnect = this.disconnect.bind(this) | ||
this._auditTargetNode = this._auditTargetNode.bind(this) | ||
|
||
this._mutationObservers = [] | ||
this._alreadyReportedIncidents = new Set() | ||
} | ||
observe(targetNode, debounceMs = 1000, axeOptions = {}) { | ||
if (!targetNode) { | ||
throw new Error('AxeReporter.observe requires a targetNode') | ||
} | ||
|
||
const scheduleAudit = () => | ||
requestIdleCallback(() => this._auditTargetNode(targetNode, axeOptions)) | ||
const mutationObserverCallback = debounce(scheduleAudit, debounceMs) | ||
const mutationObserver = new window.MutationObserver( | ||
mutationObserverCallback | ||
) | ||
|
||
// observe changes | ||
mutationObserver.observe(targetNode, { | ||
attributes: true, | ||
subtree: true | ||
}) | ||
|
||
this._mutationObservers.push(mutationObserver) | ||
|
||
// run initial audit | ||
scheduleAudit(targetNode) | ||
} | ||
disconnect() { | ||
this._mutationObservers.forEach(mutationObserver => { | ||
mutationObserver.disconnect() | ||
}) | ||
} | ||
async _auditTargetNode(targetNode, axeOptions) { | ||
const response = await axe.run(targetNode, { | ||
...axeOptions, | ||
// Require version reporter 2. | ||
reporter: 'v2' | ||
}) | ||
|
||
const violationsToReport = response.violations.filter(violation => { | ||
const filteredNodes = violation.nodes.filter(node => { | ||
const key = node.target.toString() + violation.id | ||
|
||
const wasAlreadyReported = this._alreadyReportedIncidents.has(key) | ||
|
||
if (wasAlreadyReported) { | ||
// filter out this violation for this node | ||
return false | ||
} else { | ||
// add to alreadyReportedIncidents as we'll report it now | ||
this._alreadyReportedIncidents.add(key) | ||
return true | ||
} | ||
}) | ||
|
||
return filteredNodes.length > 0 | ||
}) | ||
|
||
const hasViolationsToReport = violationsToReport.length > 0 | ||
|
||
if (hasViolationsToReport) { | ||
AxeReporter.reportViolations(violationsToReport) | ||
} | ||
} | ||
static REPORT_STYLES = { | ||
critical: 'color:red; font-weight:bold;', | ||
serious: 'color:red; font-weight:normal;', | ||
moderate: 'color:orange; font-weight:bold;', | ||
minor: 'color:orange; font-weight:normal;', | ||
elementLog: 'font-weight:bold; font-family:Courier;', | ||
defaultReset: 'font-color:black; font-weight:normal;' | ||
} | ||
static reportViolations(violations) { | ||
console.group('%cNew aXe issues', AxeReporter.REPORT_STYLES.serious) | ||
violations.forEach(violation => { | ||
const format = | ||
AxeReporter.REPORT_STYLES[violation.impact] || | ||
AxeReporter.REPORT_STYLES.minor | ||
console.groupCollapsed( | ||
'%c%s: %c%s %s', | ||
format, | ||
violation.impact, | ||
AxeReporter.REPORT_STYLES.defaultReset, | ||
violation.help, | ||
violation.helpUrl | ||
) | ||
console.groupEnd() | ||
violation.nodes.forEach(node => { | ||
AxeReporter.reportFailureSummary(node, 'any') | ||
AxeReporter.reportFailureSummary(node, 'none') | ||
}) | ||
}) | ||
console.groupEnd() | ||
} | ||
static reportFailureSummary(node, key) { | ||
// This method based off react-axe's failureSummary | ||
// Ref: https://github.com/dequelabs/react-axe | ||
function logElement(node, logFn) { | ||
const elementInDocument = document.querySelector(node.target.toString()) | ||
if (!elementInDocument) { | ||
logFn( | ||
'Selector: %c%s', | ||
AxeReporter.REPORT_STYLES.elementLog, | ||
node.target.toString() | ||
) | ||
} else { | ||
logFn('Element: %o', elementInDocument) | ||
} | ||
} | ||
|
||
function logHtml(node) { | ||
console.log('HTML: %c%s', AxeReporter.REPORT_STYLES.elementLog, node.html) | ||
} | ||
|
||
function logFailureMessage(node, key) { | ||
const message = axe._audit.data.failureSummaries[key].failureMessage( | ||
node[key].map(check => check.message || '') | ||
) | ||
|
||
console.error(message) | ||
} | ||
|
||
if (node[key].length > 0) { | ||
logElement(node, console.groupCollapsed) | ||
logHtml(node) | ||
logFailureMessage(node, key) | ||
|
||
const relatedNodes = node[key].reduce( | ||
(accumulator, check) => [...accumulator, ...check.relatedNodes], | ||
[] | ||
) | ||
|
||
if (relatedNodes.length > 0) { | ||
console.groupCollapsed('Related nodes') | ||
relatedNodes.forEach(relatedNode => { | ||
logElement(relatedNode, console.log) | ||
logHtml(relatedNode) | ||
}) | ||
console.groupEnd() | ||
} | ||
|
||
console.groupEnd() | ||
} | ||
} | ||
} | ||
|
||
export default AxeReporter |