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 10 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
105 changes: 105 additions & 0 deletions AxeObserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import axeCore from 'axe-core'
import debounce from 'lodash.debounce'

// 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)
}

// The AxeObserver class takes a violationsCallback, which is invoked with an
// array of observed violations.
export default class AxeObserver {
constructor(
violationsCallback,
axeConfiguration = axeInstance => {
axeInstance.configure({
reporter: 'v2',
checks: [
{
id: 'color-contrast',
options: {
// Prevent axe from automatically scrolling
noScroll: true
}
}
]
})
}
) {
if (typeof violationsCallback !== 'function') {
throw new Error(
'The AxeObserver constructor requires a violationsCallback'
)
}

this._violationsCallback = violationsCallback

this.observe = this.observe.bind(this)
this.disconnect = this.disconnect.bind(this)
this._auditTargetNode = this._auditTargetNode.bind(this)

this._mutationObservers = []
this._alreadyReportedIncidents = new Set()

axeConfiguration(axeCore)
}
observe(targetNode, debounceMs = 1000, maxWaitMs = debounceMs * 5) {
if (!targetNode) {
throw new Error('AxeObserver.observe requires a targetNode')
}

const scheduleAudit = debounce(
() => requestIdleCallback(() => this._auditTargetNode(targetNode)),
debounceMs,
{ leading: true, maxWait: maxWaitMs }
)
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) {
const response = await axeCore.run(targetNode)

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) {
this._violationsCallback(violationsToReport)
}
}
}
47 changes: 47 additions & 0 deletions README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 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
import('agnostic-axe').then(({ AxeObserver, logViolations }) => {
const MyAxeObserver = new AxeObserver(logViolations)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend accepting an object of options. That will save you from breaking the API later on.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, agreed. Updated both the constructor & observe method to support that.

MyAxeObserver.observe(document)
})
```

> Be sure to only run the module in your development environment. Else your application will use more resources than necessary when in production.

In the example above, once the `observe` method has been invoked, `MyAxeObserver` 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.

```js
MyAxeObserver.disconnect()
```

### Configuration

The `AxeObserver` constructor takes two parameters:

- `violationsCallback` (required). A function that is invoked with an array of violations, as reported by [axe-core](https://github.com/dequelabs/axe-core). To log violations to the console, simply pass the `logViolations` function exported by this module.
- `axeConfiguration` (optional). A function that is invoked with the instance of [axe-core](https://github.com/dequelabs/axe-core) used by module. It can be used to [configure axe-core](https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#api-name-axeconfigure). See the source code for the default configuration used.

The `AxeObserver.observe` method takes two parameters:

- `targetNode` (required). The node that should be observed & analyzed.
- `debounceMs` (optional, defaults to `1000`). The number of milliseconds that updates should cease before an analysis is performed.
- `maxWaitMs` (optional, defaults to `debounceMs * 5`). Number of milliseconds after which an analysis will be performed, even if the targetNode is still updating.

## 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).
4 changes: 4 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import AxeObserver from './AxeObserver.js'
import logViolations from './logViolations.js'

export default { AxeObserver, logViolations }
98 changes: 98 additions & 0 deletions logViolations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import axeCore from 'axe-core'

const boldCourier = 'font-weight:bold;font-family:Courier;'
const critical = 'color:red;font-weight:bold;'
const serious = 'color:red;font-weight:normal;'
const moderate = 'color:orange;font-weight:bold;'
const minor = 'color:orange;font-weight:normal;'
const defaultReset = 'font-color:black;font-weight:normal;'

// The logViolations function takes an array of violations and logs them to the
// console in a nice format. Its code is copied from the `react-axe` module.
// Ref: https://github.com/dequelabs/react-axe
export default function logViolations(violations) {
if (violations.length) {
console.group('%cNew aXe issues', serious)
violations.forEach(function(result) {
var fmt
switch (result.impact) {
case 'critical':
fmt = critical
break
case 'serious':
fmt = serious
break
case 'moderate':
fmt = moderate
break
case 'minor':
fmt = minor
break
default:
fmt = minor
break
}
console.groupCollapsed(
'%c%s: %c%s %s',
fmt,
result.impact,
defaultReset,
result.help,
result.helpUrl
)
result.nodes.forEach(function(node) {
failureSummary(node, 'any')
failureSummary(node, 'none')
})
console.groupEnd()
})
console.groupEnd()
}
}

function logElement(node, logFn) {
var el = document.querySelector(node.target.toString())
if (!el) {
logFn('Selector: %c%s', boldCourier, node.target.toString())
} else {
logFn('Element: %o', el)
}
}

function logHtml(node) {
console.log('HTML: %c%s', boldCourier, node.html)
}

function logFailureMessage(node, key) {
var message = axeCore._audit.data.failureSummaries[key].failureMessage(
node[key].map(function(check) {
return check.message || ''
})
)

console.error(message)
}

function failureSummary(node, key) {
if (node[key].length > 0) {
logElement(node, console.groupCollapsed)
logHtml(node)
logFailureMessage(node, key)

var relatedNodes = []
node[key].forEach(function(check) {
relatedNodes = relatedNodes.concat(check.relatedNodes)
})

if (relatedNodes.length > 0) {
console.groupCollapsed('Related nodes')
relatedNodes.forEach(function(relatedNode) {
logElement(relatedNode, console.log)
logHtml(relatedNode)
})
console.groupEnd()
}

console.groupEnd()
}
}
Loading