-
-
Notifications
You must be signed in to change notification settings - Fork 8.7k
[adr] script and logging async/event API on driver.script #17776
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
c0cfe40
5905691
309ef00
7437fd9
67d8d4e
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,200 @@ | ||
| # 17776. The script and logging async/event API is defined on `driver.script` | ||
|
|
||
| - Status: Proposed | ||
| - Discussion: https://github.com/SeleniumHQ/selenium/pull/17776 | ||
|
|
||
| ## Context | ||
|
|
||
| `driver.script` is the high-level API for two things: running scripts, and subscribing to the | ||
| script- and log-domain events a page emits β console messages, uncaught JavaScript errors, and DOM | ||
| mutations. The bindings diverge across all of it: which capabilities exist, what each is named, what | ||
| a handler returns, and what event data reaches the user. This record fixes the whole surface and its | ||
| behavior so the bindings converge. | ||
|
|
||
| | Binding | Current BiDi `script` surface | | ||
| |------------|-------------------------------| | ||
| | Java | `pin` registers a preload script (auto-runs); `execute` runs a source string; `unpin`. Console and JavaScript-error handlers as two subscriptions returning a `long` id; DOM-mutation (attributes only). No run-by-handle pin. | | ||
| | JavaScript | As Java, entirely async. | | ||
| | Python | `pin` returns a typed handle and `execute` takes source *or* that handle; no `preload`. Console and error over one filtered subscription (`int` id), in two payload shapes; DOM-mutation over attribute, child-list, and character-data. | | ||
| | Ruby | Console and JavaScript-error handlers only (one filtered subscription, `int` id); no `execute` / `pin` / `preload`, no DOM-mutation. | | ||
| | .NET | No high-level surface β only the raw low-level module. | | ||
|
|
||
| ## Decision | ||
|
|
||
| The API covers two things, both on one accessor, `driver.script`: **running scripts** (decisions | ||
| 1β2) and **subscribing to page events** β console messages, uncaught errors, DOM mutations, and any | ||
| event a page-side script defines (decisions 3β7); a final decision (8) says where each one runs β a | ||
| window handle for events, a realm for `execute`. The accessor is protocol-neutral β nothing | ||
| below returns a BiDi type β and hosts the console and error events itself, not a separate `log`. Each | ||
| decision states what every binding provides; call shapes are per-language idiom. | ||
|
|
||
| **Running scripts** | ||
|
|
||
| 1. **`execute(script, *args)` runs a script and returns its result β there is no `execute_async`.** | ||
| `script` is a source string or a handle from `pin`. A returned promise is awaited, so one method | ||
| covers both sync and async, and async-versus-sync is a per-language return-type detail, not a | ||
| second method. Passing a handle invokes the pinned script without resending its source β the | ||
| original point of pinning β so a large atom travels once and each call carries only the invocation. | ||
|
|
||
| ```ruby | ||
| driver.script.execute("return document.title") | ||
| ``` | ||
| ```java | ||
| driver.script().execute("return document.title"); | ||
| ``` | ||
|
|
||
| 2. **`pin(source)` registers a script the browser keeps on every future page** β run before the | ||
| page's own scripts on each navigation and in every new browsing context for the rest of the | ||
| session. It does not apply to the already-loaded page; `execute` runs things now. It returns a | ||
|
qodo-code-review[bot] marked this conversation as resolved.
|
||
| handle: pass it to `execute` to invoke the pinned script without resending the source, or to | ||
| `unpin` to discard it. A pinned callable is defined on each load and invoked on demand through its | ||
| handle; a self-running script simply takes effect on each load. | ||
|
|
||
| ```ruby | ||
| displayed = driver.script.pin("(el) => el.offsetParent !== null") # the atom rides on every future page | ||
| driver.get("https://example.com") | ||
| driver.script.execute(displayed, driver.find_element(id: "menu")) # invoked by handle; atom not resent | ||
| driver.script.unpin(displayed) | ||
| ``` | ||
| ```java | ||
| PinnedScript displayed = driver.script().pin("(el) => el.offsetParent !== null"); | ||
| driver.get("https://example.com"); | ||
| driver.script().execute(displayed, driver.findElement(By.id("menu"))); | ||
| driver.script().unpin(displayed); | ||
| ``` | ||
|
|
||
| **Subscribing to events** | ||
|
|
||
| 3. **Console messages and JavaScript errors are separate handlers**, even though one `log` event | ||
| feeds both β a console subscriber receives console output, not the page's uncaught errors. | ||
| **DOM-mutation handlers select which changes to observe** β attribute, child-list, | ||
| character-data, or any combination; with none named, all are observed. | ||
|
|
||
| ```ruby | ||
| driver.script.add_console_message_handler { |m| log(m.text) } | ||
| driver.script.add_javascript_error_handler { |e| log(e.stacktrace) } | ||
| driver.script.add_dom_mutation_handler(types: [:attributes]) { |m| log(m.attribute_name) } | ||
| ``` | ||
| ```java | ||
| driver.script().addConsoleMessageHandler(m -> log(m.getText())); | ||
| driver.script().addJavaScriptErrorHandler(e -> log(e.getStacktrace())); | ||
| driver.script().addDomMutationHandler(m -> log(m.getAttributeName()), MutationType.ATTRIBUTES); | ||
| ``` | ||
|
|
||
| 4. **`add_event_handler(name, script)` subscribes to events a page-side script defines.** The script | ||
| is handed a callback; each call delivers its argument to the handler unchanged β the raw value the | ||
| script emitted, not a shaped payload. This is the general mechanism the DOM-mutation handler is a | ||
| specialization of, exposed for events no binding pre-defines. The handler runs the script now and, | ||
| unless limited to the current context, keeps it on every future load so it survives navigation; | ||
| `remove_event_handler` detaches the subscription and stops future loads. | ||
|
Comment on lines
+87
to
+89
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. 2. Handler persistence unclear Decision 4 says add_event_handler βruns the script nowβ and also persists on βevery future loadβ unless βlimited to the current context,β but the ADR never defines how a caller requests document-only vs navigation-persistent behavior (Decision 8 only defines browsing/user context scoping). Agent Prompt
|
||
|
|
||
| ```ruby | ||
| h = driver.script.add_event_handler("paint", | ||
| "(emit) => new PerformanceObserver(l => emit(l.getEntries())).observe({type: 'paint'})") do |entries| | ||
| log(entries) | ||
| end | ||
| driver.script.remove_event_handler(h) | ||
| ``` | ||
| ```java | ||
| Registration h = driver.script().addEventHandler("paint", | ||
| "(emit) => new PerformanceObserver(l => emit(l.getEntries())).observe({type: 'paint'})", | ||
| entries -> log(entries)); | ||
| driver.script().removeEventHandler(h); | ||
| ``` | ||
|
|
||
| 5. **Registering returns an object, not a bare id.** `pin` and each `add_*` handler return an object, | ||
| so a registration can carry more than an identifier; the object is passed back to its remover β | ||
| `unpin`, `remove_*_handler` β to detach it, and `pin`'s object is also what `execute` takes to run | ||
| the pinned script. | ||
|
|
||
| ```ruby | ||
| handler = driver.script.add_console_message_handler { |m| log(m.text) } | ||
| driver.script.remove_console_message_handler(handler) | ||
| ``` | ||
| ```java | ||
| Registration handler = driver.script().addConsoleMessageHandler(m -> log(m.getText())); | ||
| driver.script().removeConsoleMessageHandler(handler); | ||
| ``` | ||
|
|
||
| 6. **Each event the API defines carries a shaped payload β not the raw protocol entry β and every | ||
| payload names its origin.** The origin is the source realm (always present) and the window handle | ||
| it belongs to (present for a tab-level event; a worker-realm event carries the realm alone). Beyond | ||
| origin, a console message carries level, text, type, and arguments; a JavaScript error carries its | ||
| message and stack trace; a DOM mutation carries the target element, the mutation kind, and the | ||
| fields for that kind β an attribute name with its old and new value, the old and new character | ||
| data, or the added and removed nodes. Only the JavaScript error carries a stack trace; a console | ||
| message does not. `add_event_handler` is the exception to the shaping: it delivers whatever its | ||
| script emits, unshaped (decision 4). | ||
|
|
||
| ```ruby | ||
| driver.script.add_console_message_handler { |m| log(m.level, m.text, m.realm) } | ||
| driver.script.add_javascript_error_handler { |e| log(e.message, e.stacktrace) } | ||
| ``` | ||
| ```java | ||
| driver.script().addConsoleMessageHandler(m -> log(m.getLevel(), m.getText(), m.getRealm())); | ||
| driver.script().addJavaScriptErrorHandler(e -> log(e.getMessage(), e.getStacktrace())); | ||
| ``` | ||
|
|
||
| 7. **An uncaught exception in a handler is logged, not raised** β a passive monitor must not fail the | ||
| session or stall the page. | ||
|
|
||
| ```ruby | ||
| driver.script.add_console_message_handler { |m| raise "boom" } # logged; the session continues | ||
| ``` | ||
| ```java | ||
| driver.script().addConsoleMessageHandler(m -> { throw new RuntimeException("boom"); }); // logged | ||
| ``` | ||
|
|
||
| **Scoping** | ||
|
|
||
| 8. **Events attach to a window handle; `execute` runs in a realm.** A handler defaults to the current | ||
|
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. Shouldn't we add |
||
| top-level navigable and takes an optional **window handle** to watch another tab. `execute` | ||
| defaults to the current realm and takes an optional **realm** β a frame, a web worker, or a | ||
| service worker, not only a tab β since a script can run wherever one of those exists. How window | ||
| handles, realms, and user contexts relate more fully is out of scope for this record. | ||
|
|
||
| ```ruby | ||
| driver.script.add_console_message_handler(window_handle: tab) { |m| log(m.text) } # tab: a window handle | ||
| driver.script.execute("return self.location.href", realm: worker) # realm: frame/worker/tab | ||
| ``` | ||
| ```java | ||
| driver.script().addConsoleMessageHandler(tab, m -> log(m.getText())); // tab: a window handle | ||
| driver.script().execute("return self.location.href", worker); // worker: a frame/worker/tab realm | ||
|
Comment on lines
+161
to
+162
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. 1. Realm/args ambiguity Decision 1 defines execute(script, *args), but Decision 8 adds an optional realm and the Java
example execute("β¦", worker) is indistinguishable from passing worker as the first script
argument. This underspecifies the API shape and will likely produce incompatible binding signatures
and/or incorrect execution context selection.
Agent Prompt
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 think this should be clarified as well. |
||
| ``` | ||
|
|
||
| ## Considered options | ||
|
|
||
| - **A separate `log` accessor for the console and error events.** | ||
| - These come from the BiDi `log` domain, so a `log` accessor mirrors the protocol and reads naturally to someone thinking in spec terms. But it splits console and error away from the DOM-mutation event and the scripts they are used alongside, spreading one workflow over two objects; and `log` already means browser logs in Selenium (`manage().logs()`, log levels), so `driver.log` would be read as that. Keeping everything on `script` avoids both. | ||
| - **A separate `preload` command alongside `pin`.** | ||
| - It looks like two behaviors β `pin` a script to call by handle, versus `preload` one that runs on every page β so two names seem to fit, and today's bindings lean that way (Java and JavaScript `pin` auto-runs, Python `pin` is a handle). But it is one mechanism: `pin` keeps a script on every page, and whether it runs on its own or waits to be called is a property of the script, not the command β a pinned helper that only defines a function is exactly run-on-demand, invoked through `execute`. A second command would split one concept across two names for no behavior the first cannot express. | ||
| - **A single merged console/error handler.** | ||
| - One subscription maps directly to the `log.entryAdded` event that carries both, so it is the least to implement. But a user almost always wants console output or uncaught errors, not both interleaved, and a merged handler makes every subscriber re-filter by type; splitting moves that demultiplex into the binding, done once instead of by every user. | ||
| - **Returning a bare id from `add`.** | ||
| - It is what every binding returns now, and an integer is the least it can hand back. But once code holds an id, anything more we later want β removing through the handle, exposing what it observes, disabling it β needs a new call or parameter, where an object absorbs it behind the same return. The cost is one type per registration the binding already has internally. | ||
| - **Observing attribute mutations only.** | ||
| - It is what most bindings ship, and attribute changes are the common case. But `MutationObserver` reports child-list and character-data natively, so attributes-only discards capability the browser already provides, and Python already exposes the full set. Selecting types per handler keeps the common case a one-liner without capping the rest. | ||
| - **Exposing the raw protocol entry as the payload.** | ||
| - A direct passthrough β no shaping code, and it tracks the spec automatically. But the raw entry differs field by field across the `log` and `script` domains and leaves the consumer to resolve the origin and the fields these events are read for; a shaped payload gives the same fields in every binding and extracts them once. | ||
| - **A single `context` argument shared by `execute` and the handlers.** | ||
| - One scope parameter everywhere is simplest to describe. But the two targets differ: an event attaches to a top-level navigable β a tab β while `execute` can run in a frame, a web worker, or a service worker, which are realms. Collapsing both into one `context` blurs a distinction users have to get right, so events take a window handle and `execute` takes a realm. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - Convergence is mostly reclassifying what exists: Java, JavaScript, and Python keep `pin` but return | ||
| a registration object instead of a bare id and settle on one behavior β a script the browser keeps | ||
| on every page, invoked through `execute` when it only defines a callable; Python also drops its | ||
| second payload shape; Ruby adds `execute`, `pin`, and the DOM-mutation handler; .NET builds the | ||
| high-level surface it lacks. | ||
| - `add_event_handler` is new surface every binding adds; the DOM-mutation handler becomes its first | ||
| built-in specialization (a pinned observer plus payload shaping) rather than a separate mechanism, | ||
| and custom async events no longer force a drop to the low-level module. | ||
| - `execute` overlaps the existing `execute_script`; whether it supersedes that method is a separate | ||
| migration decision, not settled here. | ||
|
Comment on lines
+192
to
+193
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. Wouldn't this just be applying the deprecation policy to |
||
| - Scoping adds a parameter, not just a rename: handlers take an optional window handle and `execute` | ||
| an optional realm, where most bindings expose only the current one today. How window handles, | ||
| realms, and user contexts relate is out of scope here. | ||
| - Preload removal is future-only: `unpin` (and detaching a handler) stops the script from arming | ||
| later loads but does not undo what already ran β an observer injected into the current page keeps | ||
| running until navigation. A handler that must stop cleanly on the live page has to build teardown | ||
| into the injected script; the record does not promise retroactive removal. | ||
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.
What if a user wants to
pina script only for the current browser context? Is that a scenario we should consider?