-
Notifications
You must be signed in to change notification settings - Fork 6
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
2.0.0: Only audit nodes that have changed #8
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
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(getAuditResult) { | ||
// Returns a promise that resolves to the result of the auditCallback. | ||
return new Promise((resolve, reject) => { | ||
const runAudit = async () => { | ||
try { | ||
const result = await getAuditResult() | ||
resolve(await result) | ||
} catch (error) { | ||
reject(error) | ||
} | ||
} | ||
|
||
this._pendingAudits.push(runAudit) | ||
if (!this._isRunning) this._scheduleAudits() | ||
}) | ||
} | ||
_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 at a time, as axe-core does not allow for | ||
// concurrent runs. | ||
// Ref: https://github.com/dequelabs/axe-core/issues/1041 | ||
const runAudit = this._pendingAudits[0] | ||
await runAudit() | ||
|
||
// 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 | ||
} | ||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,5 @@ | ||
import axeCore from 'axe-core' | ||
import debounce from 'lodash.debounce' | ||
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. | ||
|
@@ -46,10 +32,17 @@ export default class AxeObserver { | |
|
||
this.observe = this.observe.bind(this) | ||
this.disconnect = this.disconnect.bind(this) | ||
this._auditTargetNode = this._auditTargetNode.bind(this) | ||
this._auditNode = this._auditNode.bind(this) | ||
|
||
this._mutationObservers = [] | ||
this._alreadyReportedIncidents = new Set() | ||
this._mutationObserver = new window.MutationObserver(mutationRecords => { | ||
mutationRecords.forEach(mutationRecord => { | ||
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') { | ||
|
@@ -59,36 +52,30 @@ export default class AxeObserver { | |
// Configure axe | ||
axeCore.configure(axeCoreConfiguration) | ||
} | ||
observe(targetNode, { debounceMs = 1000, maxWaitMs = debounceMs * 5 } = {}) { | ||
observe(targetNode) { | ||
if (!targetNode) { | ||
throw new Error('AxeObserver.observe requires a targetNode') | ||
} | ||
|
||
const scheduleAudit = debounce( | ||
() => requestIdleCallback(() => this._auditTargetNode(targetNode)), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once the debounced function fired, we also scheduled the whole node audit via In the new version, smaller chunks of work are used. They get executed for only as long as the idle period lasts. |
||
debounceMs, | ||
{ leading: true, maxWait: maxWaitMs } | ||
) | ||
const mutationObserver = new window.MutationObserver(scheduleAudit) | ||
|
||
// observe changes | ||
mutationObserver.observe(targetNode, { | ||
this._mutationObserver.observe(targetNode, { | ||
attributes: true, | ||
subtree: true | ||
}) | ||
|
||
this._mutationObservers.push(mutationObserver) | ||
|
||
// run initial audit | ||
scheduleAudit(targetNode) | ||
// run initial audit on the whole targetNode | ||
this._auditNode(targetNode) | ||
} | ||
disconnect() { | ||
this._mutationObservers.forEach(mutationObserver => { | ||
mutationObserver.disconnect() | ||
}) | ||
this.mutationObserver.disconnect() | ||
} | ||
async _auditTargetNode(targetNode) { | ||
const response = await axeQueue.add(() => axeCore.run(targetNode)) | ||
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 | ||
|
||
const violationsToReport = response.violations.filter(violation => { | ||
const filteredNodes = violation.nodes.filter(node => { | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Audit each mutated node individually