Skip to content

Commit

Permalink
add code + readme
Browse files Browse the repository at this point in the history
add basic readme
  • Loading branch information
Juliette Pretot committed Nov 20, 2019
1 parent 9882f26 commit 346faa7
Show file tree
Hide file tree
Showing 2 changed files with 212 additions and 1 deletion.
36 changes: 36 additions & 0 deletions README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# 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 an optional second parameter, `axeOptions`. 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 and the see agnostic-axe source code for the default configuration.

## 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).
177 changes: 176 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1 +1,176 @@
// 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)
}

class AxeReporter {
constructor() {
this.observe = this.observe.bind(this)
this.disconnect = this.disconnect.bind(this)
this._scheduleAudit = this._scheduleAudit.bind(this)
this._auditTargetNode = this._auditTargetNode.bind(this)

this._mutationObservers = []
this._alreadyReportedIncidents = new Set()
}
observe(
targetNode,
// Default options taken from react-axe.
// Ref: https://github.com/dequelabs/react-axe
axeOptions = {
reporter: 'v2',
checks: [
{
id: 'color-contrast',
options: {
noScroll: true
}
}
]
}
) {
if (!targetNode) {
throw new Error('AxeReporter.observe requires a targetNode')
}

const mutationObserver = new window.MutationObserver(() =>
this._scheduleAudit(targetNode, axeOptions)
)

// observe changes
mutationObserver.observe(targetNode, {
attributes: true,
subtree: true
})

this._mutationObservers.push(mutationObserver)

// run initial audit
this._scheduleAudit(targetNode, axeOptions)
}
disconnect() {
this._mutationObservers.forEach(mutationObserver => {
mutationObserver.disconnect()
})
}
_scheduleAudit(targetNode, axeOptions) {
requestIdleCallback(() => this._auditTargetNode(targetNode, axeOptions), {
timeout: 1000
})
}
async _auditTargetNode(targetNode, axeOptions) {
const response = await axe.run(targetNode, axeOptions)

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

0 comments on commit 346faa7

Please sign in to comment.