-
-
Notifications
You must be signed in to change notification settings - Fork 8.7k
[docs] decision: BiDi events are awaited with expect_* context managers #17671
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
base: trunk
Are you sure you want to change the base?
Changes from all commits
0c1288e
1f5f5db
fff5751
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,144 @@ | ||
| # 17671. BiDi events are awaited with `expect_*` context managers | ||
|
|
||
| - Status: Proposed | ||
| - Date: 2026-06-11 | ||
| - Discussion: https://github.com/SeleniumHQ/selenium/pull/17671 | ||
|
|
||
| ## Context | ||
|
|
||
| WebDriver BiDi is event-driven: navigation, network traffic, console output, user | ||
| prompts, and downloads all surface as asynchronous events. Today the only way a binding | ||
| exposes them is a fire-and-forget callback registration β e.g. Python's | ||
| `add_event_handler(event, callback)`, Java's `addListener`, JavaScript's `.on(...)`. | ||
|
|
||
| To *wait for* an event that a user action triggers (click a button, wait for the matching | ||
| network response), users must register a callback, stash the event into a shared variable | ||
| or queue, perform the action, then poll/wait. This has two problems: | ||
|
|
||
| 1. **A time-of-check/time-of-use race.** If the event fires between "perform the action" | ||
|
Member
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. Are you saying there's a race condition based on implementation or that users need to remember to add the handler before the action they want to handle? Requiring users to do the latter does seems natural and obvious for the callback pattern we are using. But if it is the former, I want to make sure I understand how that would happen. |
||
| and "start waiting", it is lost. Correctly ordered code must subscribe *before* the | ||
| action β which the callback pattern does not make natural or obvious. | ||
| 2. **No predicate.** Users hand-write loops that inspect each event to find the one they | ||
|
Member
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. For 99% use cases awaiting a single event is good. What if I want to await 2 events?.. Not sure, may be we should think about it in more generic way. Challenge ourself.
|
||
| care about (a specific URL, a console error), reinventing the same filter every time. | ||
|
|
||
| Playwright solved this with `with page.expect_event(...) as info: action()`, where the | ||
| listener is armed on `__enter__` (before the action) and `info.value` blocks on | ||
| `__exit__` until a matching event arrives or the timeout elapses. This is the single most | ||
| common asynchronous pattern in browser automation, and Selenium has no equivalent. | ||
|
Comment on lines
+24
to
+27
Member
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. Is there a way to make this section more general? This is specific to Python. Below, in the section of the options considered, the pattern |
||
|
|
||
| The forces: the BiDi event surface already exists in every binding; the wire protocol | ||
| needs nothing new; the only question is the *shape* of the waiting API and whether it is | ||
| consistent across bindings. | ||
|
|
||
| ## Decision | ||
|
|
||
| Every binding exposes an **`expect_*` family of context-manager (or block) helpers** that | ||
| arm a one-shot, predicate-filtered subscription before the user action and resolve to the | ||
| captured event after it. | ||
|
Comment on lines
+35
to
+37
Member
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. The comment I made before applies to this section. |
||
|
|
||
| Normative requirements for all bindings: | ||
|
|
||
| - The subscription is registered when the scope is entered, **before** the user action | ||
| runs, eliminating the race. | ||
| - The helper accepts an optional **predicate/matcher** and an optional **timeout** | ||
| (defaulting to the binding's standard wait timeout). For URL-shaped events a string | ||
| **glob** is also accepted. | ||
| - On scope exit the captured event is retrieved synchronously; a timeout raises the | ||
| binding's standard timeout error. | ||
| - A reusable **`Subscription`** primitive underlies the helpers: register/unregister are | ||
| decoupled, detach is idempotent and lock-guarded, and a captured-event queue backs the | ||
| wait. Bindings MAY expose this primitive directly. | ||
| - Existing callback registration (`add_event_handler`/`addListener`/`.on`) stays; this is | ||
| additive. | ||
|
|
||
| Concrete shorthands each binding SHOULD provide (built on the generic primitive): | ||
| `expect_request`, `expect_response`, `expect_console_message`, `expect_navigation` | ||
| (see [17676](17676-navigation-awaited-with-expect-helpers.md)), `expect_user_prompt` | ||
| (see [17672](17672-user-prompts-handled-through-typed-handler-api.md)), `expect_download`, | ||
| and β for tabs/windows the page opens itself β `expect_page` / `expect_popup` over | ||
| `browsingContext.contextCreated`, returning the new context as a handle object | ||
| (see [17681](17681-browsing-contexts-exposed-as-handle-objects.md)). | ||
|
Comment on lines
+54
to
+60
Member
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. I would prefer to define this naming for all bindings as part of this ADR. This would mean we are all on the same page. As I mentioned before, I do not mind using the same naming Playwright uses.
Member
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. expect/wait/assert/verify/until Different semantics - different naming. |
||
|
|
||
| Code sketch β Python (reference implementation): | ||
|
|
||
| ```python | ||
| # Generic primitive | ||
| with driver.script.expect_event("log.entryAdded") as info: | ||
| button.click() | ||
| entry = info.value | ||
|
|
||
| # Typed shorthands with predicate or glob | ||
| with driver.network.expect_response("**/api/search**") as info: | ||
| search_button.click() | ||
| assert info.value.status == 200 | ||
|
|
||
| with driver.network.expect_request(lambda r: r.method == "POST") as info: | ||
| submit.click() | ||
|
|
||
| with driver.script.expect_console_message(lambda m: m.type == "error") as info: | ||
| driver.execute_script("console.error('boom')") | ||
| print(info.value.text) | ||
| ``` | ||
|
|
||
| Code sketch β other bindings (idiomatic shape, same semantics): | ||
|
|
||
| ```java | ||
| // Java β try-with-resources arms before the action | ||
| try (var expectation = driver.network().expectResponse(url -> url.contains("/api/"))) { | ||
| button.click(); | ||
| Response response = expectation.value(); | ||
| } | ||
| ``` | ||
|
|
||
| ```javascript | ||
| // JavaScript β async block form | ||
| const response = await driver.network().expectResponse('**/api/**', async () => { | ||
| await button.click(); | ||
| }); | ||
| ``` | ||
|
|
||
| ## Considered options | ||
|
|
||
| - **`expect_*` context managers (chosen)** β arms before the action by construction; | ||
| matches the dominant Playwright idiom users already know; predicate + timeout built in. | ||
| - **Document "subscribe first, then act" with the existing callbacks** β no new API, but | ||
| leaves the race as a footgun, provides no predicate or timeout, and every user | ||
| re-implements the capture-and-wait boilerplate. Rejected: solves nothing structurally. | ||
| - **A future/promise returned from a `waitForEvent(...)` call** β viable in async | ||
| bindings, but in synchronous bindings it reintroduces the race (the call to start | ||
| waiting happens after the action) unless wrapped in a block anyway. Rejected as the | ||
| primary shape; bindings MAY offer it as a secondary convenience where idiomatic. | ||
|
Comment on lines
+107
to
+110
Member
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. This ADR's section is the confusing part for me. My understanding is to follow the idea from Playwright, specifically the naming they use. |
||
|
|
||
| ## Consequences | ||
|
|
||
| - Users get race-free, predicate-filtered event waiting with no boilerplate; the most | ||
| common async pattern becomes a one-liner. | ||
| - Each binding gains a small reusable `Subscription` primitive; implementing it surfaces | ||
| and forces fixes to event-dispatch thread-safety (callback maps must be lock-guarded, | ||
| subscribe/unsubscribe I/O must not be held under a dispatch lock). | ||
|
Comment on lines
+116
to
+118
Member
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. Would it be necessary to specify how concurrency is handled? What happens if two events that match enter at the same time? If we don't specify it, we could end up with inconsistent behavior across language binding implementations.
Member
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. Agree. I think it is good that execution the same code repeatedly should produce the same results (execution path). Events come to us ordered (via websocket), we can/should deliver to the end accordingly.
Contributor
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. I think identifying how handling of two events that match the same filter is important. I will help design and implement that internal working of the proposed API. |
||
| - The other `expect_*`-based decisions (navigation, downloads, user prompts, and the | ||
| `expect_page`/`expect_popup` handles in | ||
| [17681](17681-browsing-contexts-exposed-as-handle-objects.md)) build on this primitive, so | ||
| this record should land first. | ||
| - The thread-safety fixes this surfaces (lock-guarded callback maps, non-busy-wait command | ||
| completion, bounded event dispatch) are also the prerequisite for the multi-thread | ||
| concurrency contract in [17681](17681-browsing-contexts-exposed-as-handle-objects.md). | ||
| - No deprecations. Existing callback APIs are unaffected. | ||
|
|
||
| ## Binding status | ||
|
|
||
| | Binding | Status | Notes / tracking link | | ||
| |------------|-------------|-----------------------------------------------------------------| | ||
| | Java | pending | | | ||
| | Python | in progress | `expect_*` + `Subscription` implemented; PR pending | | ||
| | Ruby | pending | | | ||
| | .NET | pending | | | ||
| | JavaScript | pending | | | ||
|
|
||
| ## Appendix | ||
|
|
||
| The BiDi events backing the shorthands already exist in the spec and are emitted by | ||
| current browsers: `network.beforeRequestSent`, `network.responseStarted`, | ||
| `network.responseCompleted`, `log.entryAdded`, `browsingContext.userPromptOpened`, | ||
| `browsingContext.downloadWillBegin`/`downloadEnd`, and the navigation events. No new wire | ||
| protocol is required β this decision is purely about the binding-side waiting API. | ||
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.
This ARD is very Python-idiomatic, since
expect_*is what Playwright is doing for Python. Could we name this ADR so that the concept applies to all language bindings and avoid keywords specific to a particular programming language?