Skip to content

Commit

Permalink
Merge pull request #1 from juliettepretot/develop
Browse files Browse the repository at this point in the history
Add initial version
  • Loading branch information
jul-sh authored Nov 22, 2019
2 parents d61c1ad + 93be81f commit b6ee22d
Show file tree
Hide file tree
Showing 7 changed files with 9,292 additions and 0 deletions.
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
112 changes: 112 additions & 0 deletions AxeObserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
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,
{
axeCoreConfiguration = {
reporter: 'v2',
checks: [
{
id: 'color-contrast',
options: {
// Prevent axe from automatically scrolling
noScroll: true
}
}
]
},
axeCoreInstanceCallback
}
) {
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()

// Allow for registering plugins etc
if (typeof axeInstanceCallback === 'function') {
axeCoreInstanceCallback(axeCore)
}

// Configure axe
axeCore.configure(axeCoreConfiguration)
}
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)
}
}
}
50 changes: 50 additions & 0 deletions README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 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)
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.
- `options` (optional). An object with that supports the following configuration keys:
- `axeCoreConfiguration` (optional). A configuration object for axe-core. Read about the object here in the [axe-core docs](https://github.com/dequelabs/axe-core/blob/master/doc/API.md#api-name-axeconfigure) and see the `agnostic-axe` source code for the default options used.
- `axeCoreInstanceCallback` (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 for complex interactions with the [axe-core API](https://github.com/dequelabs/axe-core/blob/develop/doc/API.md) such as registering plugins.

The `AxeObserver.observe` method takes two parameters:

- `targetNode` (required). The node that should be observed & analyzed.
- `options` (optional). An object with that supports the following configuration keys:
- `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

0 comments on commit b6ee22d

Please sign in to comment.