Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial version #1

Merged
merged 12 commits into from
Nov 22, 2019
Merged
Show file tree
Hide file tree
Changes from 5 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# distribution
dist

# Logs
logs
*.log
Expand Down
40 changes: 40 additions & 0 deletions README.MD
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).
174 changes: 174 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
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 = debounce(
() =>
requestIdleCallback(() =>
this._auditTargetNode(targetNode, axeOptions)
),
debounceMs
)
const mutationObserver = new window.MutationObserver(scheduleAudit)

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