Skip to content

Commit

Permalink
seperate the AxeObserver and log function
Browse files Browse the repository at this point in the history
# Conflicts:
#	README.MD
  • Loading branch information
Juliette Pretot committed Nov 21, 2019
1 parent 8bc9dd6 commit 4c24b1a
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 187 deletions.
111 changes: 111 additions & 0 deletions AxeObserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import axeCore 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)
}
}

// The AxeObserver class takes a violationsCallback, which is invoked with an
// array of observed violations.
export default class AxeObserver {
constructor(
violationsCallback,
axeConfiguration = {
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()

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

const scheduleAudit = debounce(
() => requestIdleCallback(() => this._auditTargetNode(targetNode)),
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) {
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)
}
}
}
13 changes: 7 additions & 6 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,27 @@ npm install --save-dev agnostic-axe
## Usage

```js
import('agnostic-axe').then(AxeReporter => {
const MyAxeReporter = new AxeReporter()
MyAxeReporter.observe(document)
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.
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.
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
MyAxeReporter.disconnect()
MyAxeObserver.disconnect()
```

### Configuration

The `AxeReporter` constructor takes one parameter:
The `AxeObserver` constructor takes two parameters:

- `violationsCallback` (required). A function that invoked with the violations 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.
- `axeOptions` (optional). 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. To see the default configuration used by `agnostic-axe`, see the source code.

The `AxeReporter.observe` method takes two parameters:
Expand Down
184 changes: 3 additions & 181 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,182 +1,4 @@
import axe from 'axe-core'
import AxeObserver from './AxeObserver.js'
import logViolations from './logViolations.js'

// 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(
axeConfiguration = {
reporter: 'v2',
checks: [
{
id: 'color-contrast',
options: {
// Prevent axe from automatically scrolling
noScroll: true
}
}
]
}
) {
this.observe = this.observe.bind(this)
this.disconnect = this.disconnect.bind(this)
this._auditTargetNode = this._auditTargetNode.bind(this)

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

axe.configure(axeConfiguration)
}
observe(targetNode, debounceMs = 1000) {
if (!targetNode) {
throw new Error('AxeReporter.observe requires a targetNode')
}

const scheduleAudit = debounce(
() => requestIdleCallback(() => this._auditTargetNode(targetNode)),
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) {
const response = await axe.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) {
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
export default { AxeObserver, logViolations }
Loading

0 comments on commit 4c24b1a

Please sign in to comment.