Repository navigation
Wait for the DOM to settle before the audit runs - #44
Merged
Merged
Conversation
MatheusRich
force-pushed
the
wait-for-the-dom-to-settle
branch
from
September 23, 2026 12:55
4076157 to
ad90f50
Compare
seanpdoyle
reviewed
Sep 23, 2026
MatheusRich
force-pushed
the
wait-for-the-dom-to-settle
branch
from
September 23, 2026 14:29
ad90f50 to
9e3b0d8
Compare
MatheusRich
force-pushed
the
resolve-session-at-audit-time
branch
from
September 23, 2026 14:29
9448c0a to
74fa47d
Compare
MatheusRich
force-pushed
the
wait-for-the-dom-to-settle
branch
from
September 23, 2026 15:48
9e3b0d8 to
a89dd55
Compare
MatheusRich
marked this pull request as ready for review
September 23, 2026 16:52
seanpdoyle
approved these changes
Sep 23, 2026
MatheusRich
force-pushed
the
resolve-session-at-audit-time
branch
from
September 23, 2026 17:55
74fa47d to
5e6cfba
Compare
The audit starts as soon as the Capybara action returns. A page that renders on the client keeps changing after that point, so the audit reads the page in its previous state. A router transition that commits one frame after the click, or content that a later fetch injects, is never audited. The violations that the new state introduces pass in silence, which is the worst result for an audit tool. The test suite of this gem already contains one instance of the problem. The example `flunks on violations detected after #click_button` submits a form and audits the page before the new document arrives. It fails in isolation with `selenium_chrome_headless`, and in about one full run in three. This commit waits for the DOM before the audit. A `MutationObserver` records when the document last changed. The auditor reads that value until the document stays unchanged for `quiet_period_ms` milliseconds. A page that never settles is audited at the `quiet_period_timeout_ms` deadline, so the wait cannot block the suite. Both values are class attributes, so a suite can tune them. The auditor reads the value in short round trips, and not in one long `evaluate_async_script` call. A page load discards the document that such a script runs in, and the callback is then lost until the driver reports a script timeout. The wait also happens before `axe` is installed, because a page load between the install and the audit leaves `axe` in the document that goes away. The audit also reports the error that `axe` rejects with. Before this commit, a rejected `axe.run` never called the callback, and the driver raised a script timeout after its own deadline. The wait costs about 115ms per audit. For the three spec files that exist today, the suite goes from 3.4s to 8.3s with `selenium_chrome_headless`, from 12.8s to 17.7s with `cuprite`, and from 33.1s to 38.1s with `playwright`.
MatheusRich
force-pushed
the
wait-for-the-dom-to-settle
branch
from
September 23, 2026 18:04
a89dd55 to
109d556
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacks on PR #39.
Problem
The audit starts as soon as the Capybara action returns. A page that renders on the client keeps changing after that point, so the audit reads the page in its previous state and misses the violations that the new state introduces. We hit this in a React application, where the router commits the new page a frame after the click returns, but the bug is not framework specific. The suite of this gem has one instance of it already:
flunks on violations detected after #click_buttonaudits the page before the new document arrives, and it fails every time in isolation onselenium_chrome_headless.Proposed solution
Wait for the DOM to stop changing before the audit. A
MutationObserverrecords when the document last changed, and the auditor polls until the document stays quiet forquiet_period_msmilliseconds (default 100), or until thequiet_period_timeout_msdeadline (default 2000), so a page that never settles cannot block the suite. Both are class attributes onAxeAuditor. The commit message explains the shape: the auditor polls in short round trips because a navigation kills a longevaluate_async_script, the wait runs before theaxeinstall, and a rejectedaxe.runnow reports its error instead of a script timeout.The cost is about 115 milliseconds per audit. The rspec suite goes from 3.4s to 8.3s on selenium, from 12.8s to 17.7s on cuprite, and from 33.1s to 38.1s on playwright. That cost is the part to weigh:
quiet_period_mscould start lower, or the wait could be opt-in, but then the silent miss stays the default.One open question. Some applications need a wait of their own before the audit, for example Playwright's network idle, or the presence of a specific element. Today the only way to add one is to replace the auditor and override a private method. Should the gem expose a public hook for that, or is the DOM quiet period enough?
A playwright options fix that this branch carried at first is now its own PR #45.