[docs] ADR: Selenium waits for interaction readiness before it interacts - #17886
[docs] ADR: Selenium waits for interaction readiness before it interacts#17886AutomatedTester wants to merge 2 commits into
Conversation
181658f to
d03f066
Compare
Proposes that Selenium wait for interaction readiness before it acts: click, send_keys, clear and submit wait for the element to be actionable by default on a BiDi-enabled session, bounded by a new readiness timeout and raising today's interaction errors with the diagnosis appended. Classic sessions are unchanged, and a session toggle disables it. Readiness is modelled in three composable layers - a pending-work ledger, DOM settledness, and element actionability - also exposed as explicit protocol-neutral waits, with the semantics defined once in a shared JavaScript atom injected as a BiDi preload script, and framed as a prototype of a proposed BiDi quiescence module. Status: Proposed.
d03f066 to
e71ef1c
Compare
titusfortner
left a comment
There was a problem hiding this comment.
Having a better way to wait before acting will be a huge improvement, but I'd like more flexibility in how this functionality is provided to users. Some of my thoughts suggestions on how this would work below. These all assume that users enable bidi, even for classic methods that have not yet been re-implemented with BiDi.
- Actionable should be a replacement for "implicit wait" when bidi is enabled, rather than its own thing.
- Actionable still needs to default to 0 to maintain expectations from existing code. Honestly, I don't see how we force this through without breaking users existing code. Maybe a major release?
- Provide snapshot access to actionable so users can integrate into their existing explicit wait code without needing to rewrite.
- Provide events for settledness that users can subscribe to with a add_dom_settled_handler.
- Provide snapshots for settledness so users can wrap with whatever waiting strategy they want.
- Have settledness be a 4th supported PageLoadStrategy that we manage locally for navigations and clicks.
- Allow navigation and click methods to override page load strategy per action.
Have different atoms for each action (that take only the arguments applicable to the feature without requiring additional client code).
| default behavior of the ordinary interaction commands; the semantics are defined once, implemented | ||
| once, and also exposed as explicit waits. | ||
|
|
||
| **1. Readiness is modelled in three composable layers.** |
There was a problem hiding this comment.
These aren't three composable layers: settled builds on pending work, but actionable is completely independent (which is what decision 2 depends on). Conflating these makes some of the other decisions less clear.
| not yet ready: the page is still hydrating, a spinner is still animating, a modal is fading in over | ||
| the button, a list is re-rendering under the pointer. WebDriver's own actionability checks run at | ||
| the moment of the command — click scrolls into view and verifies the in-view centre point is | ||
| pointer-interactable — and then either succeeds or throws. They do not wait, and they do not |
There was a problem hiding this comment.
Couple important clarifications here. The spec does say to wait for interactability on send keys and clear, using the implicit wait timeout. Additionally, Chrome waits for the element to be displayed before clicking, so we can't assume that nothing is happening currently with an implicit wait value set.
| interaction point, and not moving. Which checks apply depends on the interaction: `type` and | ||
| `clear` additionally require editable; `drop` and `screenshot` do not require enabled. | ||
|
|
||
| **2. Interaction commands wait for actionability by default when the session has BiDi enabled.** |
There was a problem hiding this comment.
Waiting for actionable will stack with the interactability waits remote ends already run under the implicit wait timeout as I mentioned above. I don't think we can make this default behavior without causing problems for users. As such, I think it makes the most sense to replace implicit wait setting with this behavior because we don't want to send an implicit wait value to the remote end if we are also executing this actionable wait. But we'll still need to keep implicit wait value at 0 by default.
| named, nothing new is reachable off the driver — 17670 constrains what the API *says*, and it stays | ||
| silent about BiDi here. | ||
|
|
||
| **3. A new `readiness` session timeout bounds the implicit wait, and failures keep their current |
There was a problem hiding this comment.
Agree failures should keep their current type ,and since the timeout raises client-side we could even use per-type subclasses so readiness timeouts are distinguishable without breaking existing rescues. As per decision 2 comment I don't think we want a new toggle or timeout value.
| appended to the message. Existing `catch`/`except` blocks keep working; a test that fails today | ||
| fails the same way, later and with a better message. | ||
|
|
||
| **4. Two explicit entry points remain, both on the driver, both protocol-neutral.** Names follow |
There was a problem hiding this comment.
The pattern of driver.method(element) breaks object orientation, and would work better either acting on the element object, or in the driver.script domain. The latter seems the easier change, especially since I don't think we want either of these methods to be the primary way users access this functionality. I'd much rather see us integrate them better into our existing API, and then provide more flexibility beyond just the remote-end controlled wait. Maybe we can discuss what these might look like on Slack.
| The pending-work ledger is an input to the other two (`requirePendingQuiet`), not a third public | ||
| entry point — "are there pending timers?" is not a question users should have to ask. | ||
|
|
||
| **5. An explicit wait that times out raises the binding's existing timeout error, and the error |
There was a problem hiding this comment.
I don't think we want to limit exposure to this functionality to two remote-end managed wait methods. The plan for script module is to have a synchronous script execution method and then support async behavior with handlers. So the examples in your python repo won't fit what we're planning to support.
I think we want to provide more powerful access to settledness via a callable method similar to add_dom_mutation_handler, and there is a lot of value to providing access to a snapshot of the record for any client library to manage with their own waiting strategies instead of forcing a single option.
| to fix. A non-blocking inspection call returning the same snapshot without waiting is available for | ||
| diagnostics and for users building their own conditions. | ||
|
|
||
| **6. One implementation, shared by all bindings.** The readiness oracle is a single JavaScript |
There was a problem hiding this comment.
This is conflating a shared implementation with a single atom combining all the functionality described here. We can share the functionality with multiple atoms that perform specific functions with specific arguments that are directly applicable so the bindings don't need to add additional logic beyond passing arguments to the atom.
- correct the Context: Element Clear and Element Send Keys do wait on the implicit wait timeout; the gap is the weak predicate, click having no wait, the shared budget, and the absent diagnosis - separate the two readiness signals; actionability no longer presented as a layer stacked on settledness - gate on a se:interactionReadiness capability and replace the implicit wait instead of adding a second timeout that stacks with the remote end's - take over element-location waiting on the client where the implicit wait is no longer forwarded - raise readiness-specific subclasses of the existing interaction errors - drop the driver-level wait methods; expose snapshots and a settledness handler in the script domain instead - add a locally managed settled page load strategy and per-action override - split the single atom into narrow per-signal entry points
📄 The decision, its rationale, considered options and consequences are in the record file this PR
adds; read it there. The sections below are proposal notes and review logistics.
Revised after review (a9f895c):
an earlier revision proposed that readiness waiting become the default for every BiDi session. It no
longer does. Readiness is enabled by a new
se:interactionReadinesscapability, off by default, andwhen enabled it replaces the implicit wait rather than adding a second timeout beside it. Making
it the default is now a follow-up decision, probably release-gated.
🔗 Related
this is high-level, protocol-neutral API with BiDi as an implementation mechanism that must not
leak into signatures. No method signature changes and no BiDi type is named; the behavior is
capability-gated.
py-quiescence-bidi-preload— the readiness oracle as a JavaScript atom registered as a BiDi preload script, with ~1,500 lines
of behavioral tests. Evidence that the semantics are implementable and testable; not the
proposed API shape (see divergences below).
quiescencemodule; no working-group issue filed yet.📝 Proposal notes
A correction the record now carries. The first revision claimed WebDriver does not wait before
interacting. That is wrong, and the corrected version is a stronger argument:
keyboard-interactable is "any element that has a focusable area, is a
bodyelement, or is thedocument element" — invisible, covered, mid-animation and
aria-disabledelements all satisfy it,and interactable is satisfied the same way. The timeout is initially
0, and when set it is alsospent on element retrieval. So the gap is not that WebDriver never waits: it is that the predicate is
too weak to prevent the failures users hit, it is absent for
click, it cannot say why it expired,it shares one budget with element location, and remote ends improvise past it (chromedriver waits for
displayed before clicking).
Why the capability replaces the implicit wait instead of adding a timeout. For
send_keysandclearthe remote end already runs its own interactability wait on the implicit wait timeout. Aseparate readiness timeout would stack with it and hand a user who set an implicit wait a total they
never asked for. So when the capability is on, Selenium stops forwarding a non-zero implicit wait and
spends that budget client-side on a predicate that is actually useful.
The consequence that needs a decision. Because the implicit wait is no longer forwarded,
findElementloses its remote-end retry, so the record proposes Selenium take over element-locationwaiting on the client for these sessions. That trades local spinning at the remote end for polling
round-trips. It is decision 2 and considered option 8, and it is the part of the design I would most
like confirmed.
What the wait covers. Actionability only — visible, enabled (including
aria-disabledand thefieldset/legendexception), editable where the interaction writes, in the viewport, unobstructed,not moving. It deliberately does not require page settledness: an application that long-polls or
animates continuously would pay a settle timeout on every click. The low-level Actions API does not
auto-wait either.
How users reach the rest. There is no new wait method on the driver or on the element. The wait
is inside the interaction commands; the underlying state is reachable as a snapshot (composable with
an existing
WebDriverWait) and as a settledness handler following the existingdriver.script.add_dom_mutation_handlerpattern — preload script,ChannelValue,script.onMessage— so no client library is forced into Selenium's waiting strategy to use Selenium's readiness data.
Concrete names and signatures are deliberately left open.
Also proposed: a
settledpage load strategy, managed locally becausepageLoadStrategyis avalidated
none/eager/normalcapability, plus a per-action strategy override.Compatibility. Nothing changes for a session that does not set the capability. Where it is set,
an expired wait raises a readiness-specific subclass of what the command raises today
(
ElementClickInterceptedException,ElementNotInteractableException), so existingcatch/except/rescueblocks keep matching while the readiness case stays distinguishable.Deliberately out of scope, as follow-up decisions: making readiness the default for BiDi sessions
and in which release; a variant for classic sessions (considered option 10 argues it cannot be done
at full fidelity without a preload); deprecating
ExpectedConditions.elementToBeClickableandequivalents; the concrete snapshot and handler signatures; the BiDi module proposal itself.
Divergences from the reference implementation. It exposes two
driver.*wait methods and onecombined atom, which decisions 4, 5 and 8 replace. The capability, the implicit-wait replacement, the
client-side element-location retry, the error subclasses and the
settledstrategy are proposed hereand not yet built.
Cross-binding impact. Every binding gains the capability, the interaction-path change, the
snapshot and handler surface, the error subclasses, the page load strategy value, and the packaging
wiring to ship a JavaScript resource; bindings that already ship atoms have most of the last part.
Because the capability is off by default, existing tests are unaffected — each binding instead needs
new coverage for on, off, and preload registration failing.
🗣 Discussion
Questions I would most like input on:
findElementretry on the client the intendedreading of "readiness replaces implicit wait", or should this be scoped to interaction only and
the implicit wait still forwarded?
value makes failing suites slow; a low one makes the feature look unreliable on slow CI.
separate discussion.
passing test into a timeout. Is "best-effort, documented limits, off unless asked for" enough?
capability exists — which is not the population that flakes. What would make default-on
acceptable: a major release, a release cycle of feedback, something else?
Not yet discussed at a TLC meeting — requesting an agenda slot.
📌 Tracking
Tracking issue: (linked on acceptance)