From 0c1288efdeea5db28be43509642dca4d52672ae5 Mon Sep 17 00:00:00 2001 From: AutomatedTester Date: Thu, 11 Jun 2026 19:26:26 +0100 Subject: [PATCH 1/3] [docs] decision: BiDi events are awaited with expect_* context managers --- ...ts-awaited-with-expect-context-managers.md | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 docs/decisions/0001-bidi-events-awaited-with-expect-context-managers.md diff --git a/docs/decisions/0001-bidi-events-awaited-with-expect-context-managers.md b/docs/decisions/0001-bidi-events-awaited-with-expect-context-managers.md new file mode 100644 index 0000000000000..3b0b51d2eeb45 --- /dev/null +++ b/docs/decisions/0001-bidi-events-awaited-with-expect-context-managers.md @@ -0,0 +1,137 @@ +# 0001. 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" + 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 + 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. + +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. + +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 [0006](0006-navigation-awaited-with-expect-helpers.md)), `expect_user_prompt` +(see [0002](0002-user-prompts-handled-through-typed-handler-api.md)), and +`expect_download`. + +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. + +## 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). +- The other `expect_*`-based decisions (navigation, downloads, user prompts) build on this + primitive, so this record should land first. +- 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. From 1f5f5db4eadbf1b05f44482334ba61b78e99135a Mon Sep 17 00:00:00 2001 From: AutomatedTester Date: Mon, 15 Jun 2026 10:09:23 +0100 Subject: [PATCH 2/3] [docs] 0001: add expect_page/expect_popup shorthands and link to handle-object decision (0008) --- ...events-awaited-with-expect-context-managers.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/decisions/0001-bidi-events-awaited-with-expect-context-managers.md b/docs/decisions/0001-bidi-events-awaited-with-expect-context-managers.md index 3b0b51d2eeb45..8621aa9493bbf 100644 --- a/docs/decisions/0001-bidi-events-awaited-with-expect-context-managers.md +++ b/docs/decisions/0001-bidi-events-awaited-with-expect-context-managers.md @@ -54,8 +54,10 @@ Normative requirements for all bindings: Concrete shorthands each binding SHOULD provide (built on the generic primitive): `expect_request`, `expect_response`, `expect_console_message`, `expect_navigation` (see [0006](0006-navigation-awaited-with-expect-helpers.md)), `expect_user_prompt` -(see [0002](0002-user-prompts-handled-through-typed-handler-api.md)), and -`expect_download`. +(see [0002](0002-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 [0008](0008-browsing-contexts-exposed-as-handle-objects.md)). Code sketch — Python (reference implementation): @@ -114,8 +116,13 @@ const response = await driver.network().expectResponse('**/api/**', async () => - 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). -- The other `expect_*`-based decisions (navigation, downloads, user prompts) build on this - primitive, so this record should land first. +- The other `expect_*`-based decisions (navigation, downloads, user prompts, and the + `expect_page`/`expect_popup` handles in + [0008](0008-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 [0008](0008-browsing-contexts-exposed-as-handle-objects.md). - No deprecations. Existing callback APIs are unaffected. ## Binding status From fff57513dedb19830b82b2aa364913fc728b9af2 Mon Sep 17 00:00:00 2001 From: AutomatedTester Date: Fri, 19 Jun 2026 16:55:12 +0100 Subject: [PATCH 3/3] [docs] 17671: rename ADR file to PR number; renumber cross-references Rename 0001 -> 17671 and point all cross-references (to 17672/17676/17681) at their PR-number filenames. --- ...i-events-awaited-with-expect-context-managers.md} | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename docs/decisions/{0001-bidi-events-awaited-with-expect-context-managers.md => 17671-bidi-events-awaited-with-expect-context-managers.md} (93%) diff --git a/docs/decisions/0001-bidi-events-awaited-with-expect-context-managers.md b/docs/decisions/17671-bidi-events-awaited-with-expect-context-managers.md similarity index 93% rename from docs/decisions/0001-bidi-events-awaited-with-expect-context-managers.md rename to docs/decisions/17671-bidi-events-awaited-with-expect-context-managers.md index 8621aa9493bbf..f292f17cf38da 100644 --- a/docs/decisions/0001-bidi-events-awaited-with-expect-context-managers.md +++ b/docs/decisions/17671-bidi-events-awaited-with-expect-context-managers.md @@ -1,4 +1,4 @@ -# 0001. BiDi events are awaited with `expect_*` context managers +# 17671. BiDi events are awaited with `expect_*` context managers - Status: Proposed - Date: 2026-06-11 @@ -53,11 +53,11 @@ Normative requirements for all bindings: Concrete shorthands each binding SHOULD provide (built on the generic primitive): `expect_request`, `expect_response`, `expect_console_message`, `expect_navigation` -(see [0006](0006-navigation-awaited-with-expect-helpers.md)), `expect_user_prompt` -(see [0002](0002-user-prompts-handled-through-typed-handler-api.md)), `expect_download`, +(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 [0008](0008-browsing-contexts-exposed-as-handle-objects.md)). +(see [17681](17681-browsing-contexts-exposed-as-handle-objects.md)). Code sketch — Python (reference implementation): @@ -118,11 +118,11 @@ const response = await driver.network().expectResponse('**/api/**', async () => subscribe/unsubscribe I/O must not be held under a dispatch lock). - The other `expect_*`-based decisions (navigation, downloads, user prompts, and the `expect_page`/`expect_popup` handles in - [0008](0008-browsing-contexts-exposed-as-handle-objects.md)) build on this primitive, so + [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 [0008](0008-browsing-contexts-exposed-as-handle-objects.md). + concurrency contract in [17681](17681-browsing-contexts-exposed-as-handle-objects.md). - No deprecations. Existing callback APIs are unaffected. ## Binding status