Skip to content

Commit

Permalink
use a queue of pending audits to run on idle
Browse files Browse the repository at this point in the history
  • Loading branch information
Juliette Pretot committed Nov 29, 2019
1 parent 2289f19 commit 2928661
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 57 deletions.
49 changes: 49 additions & 0 deletions AuditQueue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { rIC as requestIdleCallback } from 'idlize/idle-callback-polyfills.mjs'

export default class AuditQueue {
constructor() {
this._pendingAudits = []
this._isRunning = false

this.run = this.run.bind(this)
this._scheduleAudits = this._scheduleAudits.bind(this)
}
run(auditCallback) {
// Returns a promise that resolves to the result of the auditCallback.
const resultPromise = new Promise()

this._pendingAudits.push({ auditCallback, resultPromise })
if (!this._isRunning) this._scheduleAudits()

return resultPromise
}
_scheduleAudits() {
this._isRunning = true
requestIdleCallback(async IdleDeadline => {
// Run pending audits as long as they exist & we have time.
while (
this._pendingAudits.length > 0 &&
IdleDeadline.timeRemaining() > 0
) {
// Only run one audit as a time, as axe-core does not allow for
// concurrent runs.
// Ref: https://github.com/storybookjs/storybook/pull/4086
const { auditCallback, resultPromise } = this._pendingAudits[0]
// Run the audit and resolve the associated promise.
const result = await auditCallback()
resultPromise.resolve(result)

// Once an audit has run, remove it from the queue.
this._pendingAudits.shift()
}

if (this._pendingAudits.length > 0) {
// If pending audits remain, schedule them for the next idle phase.
this._scheduleAudits()
} else {
// The queue is empty, we're no longer running
this._isRunning = false
}
})
}
}
46 changes: 13 additions & 33 deletions AxeObserver.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
import axeCore from 'axe-core'
import PQueue from 'p-queue'

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

// axe-core cannot be run concurrently. There are no plans to implement this
// functionality or a queue. Hence we use PQueue to queue calls ourselves.
// Ref: https://github.com/storybookjs/storybook/pull/4086
const axeQueue = new PQueue({ concurrency: 1 })
import AuditQueue from './AuditQueue.js'

// The AxeObserver class takes a violationsCallback, which is invoked with an
// array of observed violations.
Expand Down Expand Up @@ -45,15 +32,18 @@ export default class AxeObserver {

this.observe = this.observe.bind(this)
this.disconnect = this.disconnect.bind(this)
this._scheduleAudit = this._scheduleAudit.bind(this)
this._auditNode = this._auditNode.bind(this)

this._alreadyReportedIncidents = new Set()
this._mutationObserver = new window.MutationObserver(mutationRecords => {
mutationRecords.forEach(mutationRecord => {
this._scheduleAudit(mutationRecord.target)
this._auditNode(mutationRecord.target)
})
})

// AuditQueue sequentially runs audits when the browser is idle.
this._auditQueue = new AuditQueue()

// Allow for registering plugins etc
if (typeof axeInstanceCallback === 'function') {
axeCoreInstanceCallback(axeCore)
Expand All @@ -73,27 +63,17 @@ export default class AxeObserver {
})

// run initial audit on the whole targetNode
this._scheduleAudit(targetNode)
this._auditNode(targetNode)
}
disconnect() {
this.mutationObserver.disconnect()
}
async _scheduleAudit(node) {
const response = await axeQueue.add(
() =>
new Promise(resolve => {
requestIdleCallback(
() => {
// Since audits are scheduled asynchronously, it can happen that
// the node is no longer connected. We cannot analyze it then.
node.isConnected ? axeCore.run(node).then(resolve) : resolve(null)
},
// Time after which an audit will be performed, even if it may
// negatively affect performance.
{ timeout: 1000 }
)
})
)
async _auditNode(node) {
const response = await this._auditQueue.run(async () => {
// Since audits are scheduled asynchronously, it can happen that
// the node is no longer connected. We cannot analyze it then.
return node.isConnected ? axeCore.run(node) : null
})

if (!response) return

Expand Down
30 changes: 7 additions & 23 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@
},
"dependencies": {
"axe-core": "^3.4.0",
"p-queue": "^6.2.1"
"idlize": "^0.1.1"
}
}

0 comments on commit 2928661

Please sign in to comment.