Skip to content

[py] Align driver.script handlers with script ADR - #18270

Open
AutomatedTester wants to merge 4 commits into
trunkfrom
py-script-adr-handlers
Open

AutomatedTester wants to merge 4 commits into
trunkfrom
py-script-adr-handlers

Conversation

@AutomatedTester

@AutomatedTester AutomatedTester commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

Requested by David · project thread

🔗 Related Issues

Implements part of the script and logging ADR #17776 (still Proposed) for Python. Follows #18264.

💥 What does this PR do?

Fixes three gaps between driver.script and the ADR:

  1. Handlers return objects (decision 5). add_console_message_handler, add_javascript_error_handler and add_dom_mutation_handler return a Registration. It's an int subclass, the same trick PinnedScript(str) uses, so code that stores or compares the id keeps working. The removers take it back.
  2. window_handle= instead of context_id= (decision 8). execute() takes window_handle=. context_id= still works but raises a DeprecationWarning, and passing both raises TypeError.
  3. One set of handler names (decisions 3 and 6). The ADR names stay: add_console_message_handler and add_javascript_error_handler. They now deliver one shaped payload each, ConsoleMessage and ScriptError, and each payload names the window_handle it came from. add_console_handler, add_error_handler and their removers are deprecated in favour of the ADR names.

🔧 Implementation Notes

The two old name sets delivered different payloads. This PR merges them without breaking anyone:

  • ConsoleMessage and ScriptError stay dataclasses and gain window_handle (plus type on ConsoleMessage). The fields the ADR doesn't name are documented as deprecated: source, line_number, column_number and stack_trace.
  • add_console_message_handler and add_javascript_error_handler deliver subclasses of both the ADR payload and the generated ConsoleLogEntry / JavascriptLogEntry. isinstance checks, dataclass helpers and the old field values (type_, the {realm, context} source dict, text/level on errors) all keep working.
  • ScriptError.stacktrace is a StackTrace. It's a dict subclass holding the protocol stack trace, so existing ["callFrames"] and json.dumps code still works. str() gives the formatted frames and .frames lists them.
  • The deprecated common.bidi.script module is unchanged. driver.script passes its own payload builders to LogHandlerRegistry.add_handler(payload=...).

🤖 AI assistance

  • No substantial AI assistance used
  • AI assisted (complete below)
    • Tool(s): Claude Code
    • What was generated: the code, the tests and this description
    • I reviewed all AI output and can explain the change

💡 Additional Considerations

  • Not yet warned on access: the deprecated payload fields are documented as deprecated but don't raise a warning when read. Warning on attribute access would mean giving up the dataclass compatibility.
  • Iframes: a log entry names the context of the document that logged it, so an iframe gives the frame's context. The binding resolves that to the top-level window with browsingContext.getTree the first time it sees a context, and caches the answer because context ids are never reused. frame carries the iframe's id. window_handle is None only if the context is gone before the lookup. Lookups run on the event callback's thread, never on the websocket reader thread.
  • Tested with unit tests (new ones cover the payloads, deprecations and Registration) and with the BiDi script, log, quit and integration browser tests on Chromium 1243, by overlaying these files on the 4.52 wheel. That run passed: 114 tests, including a new iframe console test. Bazel was not run, and Firefox was not tested.
  • Not done here (follow-ups):
    • Handlers and pin don't take window_handle= yet (decision 8).
    • DomMutation doesn't carry window_handle yet.
    • ConsoleMessage.args are still protocol remote values.
    • add_event_handler and sandbox (decisions 4 and 9) aren't implemented.
  • Overlap with [py] Add driver.windows per ADR 17681 #18269: that PR changes the same execute() signature to accept a Window for context_id. Whichever PR lands second needs to move that onto window_handle.

🔄 Types of changes

  • New feature (non-breaking change which adds functionality and tests!)

Generated by Claude Code

Bring the Python driver.script event handlers in line with the script
and logging ADR (#17776):

- add_*_handler methods return a Registration, an int subclass, so
  existing code that stores the id keeps working.
- execute() takes window_handle=; context_id= still works but is
  deprecated.
- One set of handler names: add_console_message_handler and
  add_javascript_error_handler now deliver ConsoleMessage and
  ScriptError, which name the window handle the event came from.
  add_console_handler, add_error_handler and their removers are
  deprecated in favour of them.
- Payload fields outside the ADR shape (source, line and column
  numbers, stack_trace, type_, and text/level on errors) still read as
  before but raise a DeprecationWarning. ScriptError.stacktrace is a
  StackTrace string that still answers ["callFrames"] with a warning.
@AutomatedTester AutomatedTester self-assigned this Oct 31, 2026
@selenium-ci

Copy link
Copy Markdown
Member

/agentic_describe

@selenium-ci

Copy link
Copy Markdown
Member

/agentic_review

@selenium-ci selenium-ci added C-py Python Bindings B-devtools Includes everything BiDi or Chrome DevTools related labels Oct 31, 2026
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Align Python driver.script handlers with the script ADR

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Align console and JavaScript-error handlers with ADR payloads that identify their window.
• Return removable Registration objects and accept window_handle for script execution.
• Preserve older handler names, arguments, and payload fields with deprecation warnings.
Diagram

graph TD
  Driver["WebDriver"] --> Script["Script API"] --> Registry["Log registry"] --> Builders["Payload builders"] --> Payloads["ADR payloads"] --> Handlers["User handlers"]
  Events["BiDi log events"] --> Registry
  Script --> Execution["BiDi execution"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Separate legacy callback adapters
  • ➕ Could preserve the old handler names' payload shapes without deprecated fields on ADR payloads.
  • ➖ Would maintain parallel payload paths and add more dispatch and migration complexity.

Recommendation: Keep the PR's shared registry and payload-builder approach: it gives driver.script one ADR-facing handler surface while leaving the generated BiDi module's behavior unchanged. Separate legacy adapters offer cleaner new payload classes but increase long-term duplication.

Files changed (6) +650 / -129

Enhancement (2) +369 / -98
_script_handlers.pyBuild ADR handler payloads through the shared log registry +57/-12

Build ADR handler payloads through the shared log registry

• Adds an optional payload builder to log-handler registration so driver.script can shape events without changing generated BiDi handlers. Payload builders now include window handles, stack traces, and legacy field values; pinned-script errors also receive their window handle.

py/private/_script_handlers.py

script.pyAlign the supported Script API and payload types with the ADR +312/-86

Align the supported Script API and payload types with the ADR

• Introduces int-compatible Registration objects, ADR-shaped ConsoleMessage and ScriptError payloads, and a string-compatible StackTrace. Adds window_handle to execute and deprecates context_id, short handler names, and older payload fields while retaining compatibility.

py/selenium/webdriver/remote/script.py

Tests (3) +280 / -30
quit_tests.pyUse window_handle in the BiDi quit test +1/-1

Use window_handle in the BiDi quit test

• Switches script execution in the quit test to the supported window_handle argument.

py/test/selenium/webdriver/common/bidi/quit_tests.py

script_tests.pyExercise ADR payloads and deprecated handler names in browser tests +45/-20

Exercise ADR payloads and deprecated handler names in browser tests

• Updates console and error assertions to use ADR fields and window handles. Verifies Registration returns, handler removal, and continued operation of deprecated handler names.

py/test/selenium/webdriver/common/bidi/script_tests.py

script_tests.pyCover handler compatibility and window-targeted execution +234/-9

Cover handler compatibility and window-targeted execution

• Adds unit coverage for Registration, event payload shaping, deprecated fields and names, stack-trace compatibility, and handler removal. Tests window_handle forwarding for regular and pinned scripts and rejection of conflicting execution arguments.

py/test/unit/selenium/webdriver/remote/script_tests.py

Documentation (1) +1 / -1
webdriver.pyShow the ADR console handler in the driver.script example +1/-1

Show the ADR console handler in the driver.script example

• Changes the WebDriver script-property example to use add_console_message_handler instead of the deprecated short name.

py/selenium/webdriver/remote/webdriver.py

@qodo-code-review

qodo-code-review Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Legacy handlers break payload type checks ✓ Resolved
Description
add_console_message_handler() and add_javascript_error_handler() now build ConsoleMessage and
ScriptError objects instead of the generated ConsoleLogEntry and JavascriptLogEntry objects
previously delivered. Existing callbacks that rely on those public classes or their dataclass
behavior fail, while only the alternate handler names receive deprecation warnings.
Code

py/selenium/webdriver/remote/script.py[R581-584]

+        return _registration(
+            self._script._log_handlers.add_handler(
+                callback, "console", payload=lambda params: console_message_from_log_entry(params, legacy=True)
+            )
Evidence
The manifest documents the prior handlers as delivering generated ConsoleLogEntry and
JavascriptLogEntry instances, while the changed methods directly supply builders returning
ConsoleMessage and ScriptError. The changed deprecation shims apply only to
add_console_handler and add_error_handler, not to the established handler names.

py/selenium/webdriver/remote/script.py[579-584]
py/selenium/webdriver/remote/script.py[608-613]
py/private/bidi_enhancements_manifest.py[934-987]
AGENTS.md: Preserve Public API and ABI Compatibility and Deprecate Before Removal

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The established handler names changed their public callback payload classes without being deprecated, breaking consumers that check or rely on the generated log-entry types.

## Fix Focus Areas
- py/selenium/webdriver/remote/script.py[570-614]
- py/private/_script_handlers.py[384-395]
- py/private/bidi_enhancements_manifest.py[934-987]

## Recommended Fix
Keep the existing handler names delivering their established generated payload classes until a documented deprecation period has elapsed, or add an explicit deprecation warning and a supported migration path before changing their payload types. Ensure the ADR-shaped payload is exposed through a separately versioned or newly introduced API path during the transition.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Error handlers lose raw stack maps ✓ Resolved
Description
script_error_from_log_entry() converts the protocol stackTrace mapping into a StackTrace(str)
even for the legacy payload used by add_javascript_error_handler(), although that type emulates
only indexing and .get. Mapping checks and operations such as in, isinstance(..., dict),
.keys(), .items(), and json.dumps() therefore change behavior or fail for existing handler
consumers, without a deprecation warning from the handler.
Code

py/private/_script_handlers.py[R230-232]

+        stacktrace=_stack_trace(params.get("stackTrace")),
+        window_handle=_window_handle(params),
+        level=params.get("level"),
Evidence
The previous JavascriptLogEntry assigned params.get("stackTrace") directly to stacktrace,
preserving the raw protocol mapping. The changed builder passes _stack_trace(...), which creates
StackTrace(formatted, raw=...); because this string subclass emulates only __getitem__ and
get, other mapping operations do not retain their former behavior. The legacy handler remains
available without a deprecation warning.

py/private/bidi_enhancements_manifest.py[360-386]
py/private/_script_handlers.py[183-186]
py/private/_script_handlers.py[223-232]
py/selenium/webdriver/remote/script.py[599-614]
AGENTS.md: Preserve Public API and ABI Compatibility and Deprecate Before Removal
py/private/bidi_enhancements_manifest.py[364-385]
py/selenium/webdriver/remote/script.py[69-99]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The established JavaScript error handler previously exposed the raw protocol `stackTrace` mapping, but its compatibility payload now exposes a formatted string subclass. Existing mapping consumers can change behavior or fail without a deprecation warning.

## Fix Focus Areas
- py/private/_script_handlers.py[183-186]
- py/private/_script_handlers.py[214-232]
- py/selenium/webdriver/remote/script.py[69-99]
- py/selenium/webdriver/remote/script.py[599-614]

## Recommended Fix
Keep the raw `stackTrace` mapping in the legacy handler payload and use the formatted `StackTrace` string only for the ADR-shaped payload. If changing the legacy field is unavoidable, emit a deprecation warning at handler registration or field access and document a complete migration path for mapping consumers. In that case, make raw-dict key membership and `keys`/`items`/`values` delegate to the raw mapping with warnings, and document the `isinstance(dict)` break or expose the raw dict under a separately named deprecated attribute.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Log bursts flood the browser with lookups ✓ Resolved
Description
WindowResolver.window_of() releases its cache lock before calling _parent_of(), so concurrent
callbacks that miss the same context each issue their own browsingContext.getTree requests. When a
page emits a burst of logs, every handler can repeat the lookup before the first result is cached,
multiplying browser commands and delaying delivery of those callbacks.
Code

py/private/_script_handlers.py[R235-240]

+            with self._lock:
+                cached = self._windows.get(current)
+            if cached is not None:
+                window = cached
+                break
+            parent = self._parent_of(current)
Evidence
The new resolver checks _windows while holding a lock but performs the network lookup after
releasing it, and only stores the answer afterward. The registry resolves each matching event
separately; the WebSocket connection launches each callback in its own thread, allowing many
callbacks to pass the cache check before a command returns.

py/private/_script_handlers.py[227-252]
py/private/_script_handlers.py[512-515]
py/private/_script_handlers.py[543-550]
py/selenium/webdriver/remote/websocket_connection.py[239-246]
py/selenium/webdriver/remote/websocket_connection.py[131-158]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Concurrent log callbacks can all miss the same context cache entry and independently send parent-chain lookup commands before any result is stored.

## Fix Focus Areas
- py/private/_script_handlers.py[227-252]
- py/private/_script_handlers.py[512-515]

## Recommended Fix
Coordinate in-flight resolution per context so one callback performs a lookup while others await and reuse its result. Initialize the shared resolver safely, and add a concurrent-callback test that verifies repeated events for one context do not generate duplicate tree requests.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Conflicting window arguments are accepted 🐞 Bug ≡ Correctness
Description
execute() checks whether window_handle and context_id are non-None, rather than whether both
arguments were supplied. Passing window_handle=None with a non-None context_id silently uses
the deprecated argument, while passing a non-None window_handle with context_id=None also
bypasses the promised TypeError.
Code

py/selenium/webdriver/remote/script.py[R541-543]

+        if context_id is not None:
+            if window_handle is not None:
+                raise TypeError("pass window_handle or context_id, not both")
Evidence
The new signature defaults both keywords to None, and the new guard runs only when `context_id is
not None; inside it, the conflict check also requires window_handle is not None`. The added test
covers only the case where both values are non-None.

py/selenium/webdriver/remote/script.py[507-513]
py/selenium/webdriver/remote/script.py[541-550]
py/test/unit/selenium/webdriver/remote/script_tests.py[181-183]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`execute()` promises to reject callers that supply both window-target arguments, but its `None` defaults make explicit `None` indistinguishable from omission.

## Fix Focus Areas
- py/selenium/webdriver/remote/script.py[507-513]
- py/selenium/webdriver/remote/script.py[541-550]
- py/test/unit/selenium/webdriver/remote/script_tests.py[173-194]

## Recommended Fix
Use a sentinel default to track whether each keyword was supplied, reject both when present, and preserve the current-window default when neither is supplied. Add tests for both combinations where one explicitly supplied argument is `None`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


5. Iframe messages report a frame id as the window ✓ Resolved
Description
_window_handle returns params["source"]["context"] unchanged. For BiDi log entries that is the
browsing context of the realm's document, which for an iframe is the child frame's context id, not
the top-level window handle. Console messages and JavaScript errors raised inside iframes therefore
carry a window_handle that matches no entry in driver.window_handles, so filtering or routing by
window drops them.
Code

py/private/_script_handlers.py[R178-180]

+def _window_handle(params: dict) -> str | None:
+    source = params.get("source")
+    return source.get("context") if isinstance(source, dict) else None
Evidence
_window_handle reads source.context straight from the protocol payload, with no step that walks
up to the top-level context. Both log payload builders use it to fill window_handle. The
ConsoleMessage and ScriptError docstrings promise the handle of the window the event came from. In
WebDriver BiDi, script.Source.context is the browsing context of the realm, and for a script
running in an iframe that is the iframe's own context.

py/private/_script_handlers.py[178-180]
py/private/_script_handlers.py[189-211]
py/selenium/webdriver/remote/script.py[102-118]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`_window_handle` returns the BiDi `source.context`. For iframes that is a child context id, not a window handle, so `ConsoleMessage.window_handle` and `ScriptError.window_handle` can name a value the user never sees in `driver.window_handles`.

## Fix Focus Areas
- py/private/_script_handlers.py[178-180]
- py/selenium/webdriver/remote/script.py[102-118]

## Recommended Fix
Map the context id to its top-level ancestor before exposing it, for example with a cached `browsingContext.getTree` lookup that follows `parent` to the root. If you don't do that, document clearly that iframe events carry the frame's context id, and add a test with an iframe console message.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

6. Dataclass helpers fail on the payload objects ✓ Resolved
Description
ConsoleMessage and ScriptError were @dataclass types and are now plain classes with a custom
__init__/__getattr__. Existing code that calls dataclasses.asdict, dataclasses.replace,
dataclasses.fields or is_dataclass on handler payloads or on ScriptResult.error now raises
TypeError or takes a different branch, with no deprecation period.
Code

py/selenium/webdriver/remote/script.py[191]

+class ScriptError:
Evidence
The diff removes the @dataclass decorators from both public payload types. Only attribute access
is kept compatible through __getattr__, so the dataclass protocol (__dataclass_fields__) that
the standard helpers depend on is gone.

py/selenium/webdriver/remote/script.py[102-270]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ConsoleMessage` and `ScriptError` are no longer dataclasses, which breaks callers that use `dataclasses.asdict`/`replace`/`fields` on them.

## Fix Focus Areas
- py/selenium/webdriver/remote/script.py[102-270]

## Recommended Fix
Keep them as `@dataclass(eq=False)` with the ADR fields declared and the deprecated values held in a `field(repr=False)` dict, keeping the custom `__getattr__`. If that isn't possible, call out the break in the CHANGES/release notes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Linked PRs:
  🌐 PR #17776
  🌐 PR #18264
  🌐 PR #18269
Review mode: Auto: ⚖️ Balanced: Concurrency changes in window resolution require careful correctness review.
✅ Fix-loop round: fix loop

Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Findings visible per group, which tucks the rest behind a View link

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit 9061956 ⚖️ Balanced

Results up to commit f0979d9 🧠 Deep


🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. Legacy handlers break payload type checks ✓ Resolved
Description
add_console_message_handler() and add_javascript_error_handler() now build ConsoleMessage and
ScriptError objects instead of the generated ConsoleLogEntry and JavascriptLogEntry objects
previously delivered. Existing callbacks that rely on those public classes or their dataclass
behavior fail, while only the alternate handler names receive deprecation warnings.
Code

py/selenium/webdriver/remote/script.py[R581-584]

+        return _registration(
+            self._script._log_handlers.add_handler(
+                callback, "console", payload=lambda params: console_message_from_log_entry(params, legacy=True)
+            )
Evidence
The manifest documents the prior handlers as delivering generated ConsoleLogEntry and
JavascriptLogEntry instances, while the changed methods directly supply builders returning
ConsoleMessage and ScriptError. The changed deprecation shims apply only to
add_console_handler and add_error_handler, not to the established handler names.

py/selenium/webdriver/remote/script.py[579-584]
py/selenium/webdriver/remote/script.py[608-613]
py/private/bidi_enhancements_manifest.py[934-987]
AGENTS.md: Preserve Public API and ABI Compatibility and Deprecate Before Removal

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The established handler names changed their public callback payload classes without being deprecated, breaking consumers that check or rely on the generated log-entry types.

## Fix Focus Areas
- py/selenium/webdriver/remote/script.py[570-614]
- py/private/_script_handlers.py[384-395]
- py/private/bidi_enhancements_manifest.py[934-987]

## Recommended Fix
Keep the existing handler names delivering their established generated payload classes until a documented deprecation period has elapsed, or add an explicit deprecation warning and a supported migration path before changing their payload types. Ensure the ADR-shaped payload is exposed through a separately versioned or newly introduced API path during the transition.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Error handlers lose raw stack maps ✓ Resolved
Description
script_error_from_log_entry() converts the protocol stackTrace mapping into a StackTrace(str)
even for the legacy payload used by add_javascript_error_handler(), although that type emulates
only indexing and .get. Mapping checks and operations such as in, isinstance(..., dict),
.keys(), .items(), and json.dumps() therefore change behavior or fail for existing handler
consumers, without a deprecation warning from the handler.
Code

py/private/_script_handlers.py[R230-232]

+        stacktrace=_stack_trace(params.get("stackTrace")),
+        window_handle=_window_handle(params),
+        level=params.get("level"),
Evidence
The previous JavascriptLogEntry assigned params.get("stackTrace") directly to stacktrace,
preserving the raw protocol mapping. The changed builder passes _stack_trace(...), which creates
StackTrace(formatted, raw=...); because this string subclass emulates only __getitem__ and
get, other mapping operations do not retain their former behavior. The legacy handler remains
available without a deprecation warning.

py/private/bidi_enhancements_manifest.py[360-386]
py/private/_script_handlers.py[183-186]
py/private/_script_handlers.py[223-232]
py/selenium/webdriver/remote/script.py[599-614]
AGENTS.md: Preserve Public API and ABI Compatibility and Deprecate Before Removal
py/private/bidi_enhancements_manifest.py[364-385]
py/selenium/webdriver/remote/script.py[69-99]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The established JavaScript error handler previously exposed the raw protocol `stackTrace` mapping, but its compatibility payload now exposes a formatted string subclass. Existing mapping consumers can change behavior or fail without a deprecation warning.

## Fix Focus Areas
- py/private/_script_handlers.py[183-186]
- py/private/_script_handlers.py[214-232]
- py/selenium/webdriver/remote/script.py[69-99]
- py/selenium/webdriver/remote/script.py[599-614]

## Recommended Fix
Keep the raw `stackTrace` mapping in the legacy handler payload and use the formatted `StackTrace` string only for the ADR-shaped payload. If changing the legacy field is unavoidable, emit a deprecation warning at handler registration or field access and document a complete migration path for mapping consumers. In that case, make raw-dict key membership and `keys`/`items`/`values` delegate to the raw mapping with warnings, and document the `isinstance(dict)` break or expose the raw dict under a separately named deprecated attribute.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
3. Conflicting window arguments are accepted 🐞 Bug ≡ Correctness
Description
execute() checks whether window_handle and context_id are non-None, rather than whether both
arguments were supplied. Passing window_handle=None with a non-None context_id silently uses
the deprecated argument, while passing a non-None window_handle with context_id=None also
bypasses the promised TypeError.
Code

py/selenium/webdriver/remote/script.py[R541-543]

+        if context_id is not None:
+            if window_handle is not None:
+                raise TypeError("pass window_handle or context_id, not both")
Evidence
The new signature defaults both keywords to None, and the new guard runs only when `context_id is
not None; inside it, the conflict check also requires window_handle is not None`. The added test
covers only the case where both values are non-None.

py/selenium/webdriver/remote/script.py[507-513]
py/selenium/webdriver/remote/script.py[541-550]
py/test/unit/selenium/webdriver/remote/script_tests.py[181-183]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`execute()` promises to reject callers that supply both window-target arguments, but its `None` defaults make explicit `None` indistinguishable from omission.

## Fix Focus Areas
- py/selenium/webdriver/remote/script.py[507-513]
- py/selenium/webdriver/remote/script.py[541-550]
- py/test/unit/selenium/webdriver/remote/script_tests.py[173-194]

## Recommended Fix
Use a sentinel default to track whether each keyword was supplied, reject both when present, and preserve the current-window default when neither is supplied. Add tests for both combinations where one explicitly supplied argument is `None`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


4. Iframe messages report a frame id as the window ✓ Resolved
Description
_window_handle returns params["source"]["context"] unchanged. For BiDi log entries that is the
browsing context of the realm's document, which for an iframe is the child frame's context id, not
the top-level window handle. Console messages and JavaScript errors raised inside iframes therefore
carry a window_handle that matches no entry in driver.window_handles, so filtering or routing by
window drops them.
Code

py/private/_script_handlers.py[R178-180]

+def _window_handle(params: dict) -> str | None:
+    source = params.get("source")
+    return source.get("context") if isinstance(source, dict) else None
Evidence
_window_handle reads source.context straight from the protocol payload, with no step that walks
up to the top-level context. Both log payload builders use it to fill window_handle. The
ConsoleMessage and ScriptError docstrings promise the handle of the window the event came from. In
WebDriver BiDi, script.Source.context is the browsing context of the realm, and for a script
running in an iframe that is the iframe's own context.

py/private/_script_handlers.py[178-180]
py/private/_script_handlers.py[189-211]
py/selenium/webdriver/remote/script.py[102-118]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`_window_handle` returns the BiDi `source.context`. For iframes that is a child context id, not a window handle, so `ConsoleMessage.window_handle` and `ScriptError.window_handle` can name a value the user never sees in `driver.window_handles`.

## Fix Focus Areas
- py/private/_script_handlers.py[178-180]
- py/selenium/webdriver/remote/script.py[102-118]

## Recommended Fix
Map the context id to its top-level ancestor before exposing it, for example with a cached `browsingContext.getTree` lookup that follows `parent` to the root. If you don't do that, document clearly that iframe events carry the frame's context id, and add a test with an iframe console message.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational
5. Dataclass helpers fail on the payload objects ✓ Resolved
Description
ConsoleMessage and ScriptError were @dataclass types and are now plain classes with a custom
__init__/__getattr__. Existing code that calls dataclasses.asdict, dataclasses.replace,
dataclasses.fields or is_dataclass on handler payloads or on ScriptResult.error now raises
TypeError or takes a different branch, with no deprecation period.
Code

py/selenium/webdriver/remote/script.py[191]

+class ScriptError:
Evidence
The diff removes the @dataclass decorators from both public payload types. Only attribute access
is kept compatible through __getattr__, so the dataclass protocol (__dataclass_fields__) that
the standard helpers depend on is gone.

py/selenium/webdriver/remote/script.py[102-270]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ConsoleMessage` and `ScriptError` are no longer dataclasses, which breaks callers that use `dataclasses.asdict`/`replace`/`fields` on them.

## Fix Focus Areas
- py/selenium/webdriver/remote/script.py[102-270]

## Recommended Fix
Keep them as `@dataclass(eq=False)` with the ADR fields declared and the deprecated values held in a `field(repr=False)` dict, keeping the custom `__getattr__`. If that isn't possible, call out the break in the CHANGES/release notes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 85b17ea ⚖️ Balanced


No changes from previous review

Results up to commit afe6d16 ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Log bursts flood the browser with lookups ✓ Resolved
Description
WindowResolver.window_of() releases its cache lock before calling _parent_of(), so concurrent
callbacks that miss the same context each issue their own browsingContext.getTree requests. When a
page emits a burst of logs, every handler can repeat the lookup before the first result is cached,
multiplying browser commands and delaying delivery of those callbacks.
Code

py/private/_script_handlers.py[R235-240]

+            with self._lock:
+                cached = self._windows.get(current)
+            if cached is not None:
+                window = cached
+                break
+            parent = self._parent_of(current)
Evidence
The new resolver checks _windows while holding a lock but performs the network lookup after
releasing it, and only stores the answer afterward. The registry resolves each matching event
separately; the WebSocket connection launches each callback in its own thread, allowing many
callbacks to pass the cache check before a command returns.

py/private/_script_handlers.py[227-252]
py/private/_script_handlers.py[512-515]
py/private/_script_handlers.py[543-550]
py/selenium/webdriver/remote/websocket_connection.py[239-246]
py/selenium/webdriver/remote/websocket_connection.py[131-158]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Concurrent log callbacks can all miss the same context cache entry and independently send parent-chain lookup commands before any result is stored.

## Fix Focus Areas
- py/private/_script_handlers.py[227-252]
- py/private/_script_handlers.py[512-515]

## Recommended Fix
Coordinate in-flight resolution per context so one callback performs a lookup while others await and reuse its result. Initialize the shared resolver safely, and add a concurrent-callback test that verifies repeated events for one context do not generate duplicate tree requests.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread py/selenium/webdriver/remote/script.py Resolved
Comment thread py/private/_script_handlers.py Resolved
Comment thread py/selenium/webdriver/remote/script.py Resolved
Comment thread py/private/_script_handlers.py Resolved
Comment thread py/selenium/webdriver/remote/script.py Resolved
Review on #18270 pointed out that switching the handler payloads to
plain classes broke code that checks isinstance(entry, ConsoleLogEntry)
or uses dataclass helpers, and that a str stack trace broke dict use.

- ConsoleMessage and ScriptError are dataclasses again, with
  window_handle (and type on ConsoleMessage) added.
- add_console_message_handler and add_javascript_error_handler deliver
  subclasses of both the ADR payload and the generated
  ConsoleLogEntry / JavascriptLogEntry, so the old fields, isinstance
  checks and dataclass helpers keep working.
- StackTrace is the protocol mapping (a dict subclass); str() gives
  the formatted frames and .frames lists them.
@selenium-ci

Copy link
Copy Markdown
Member

/agentic_review

Copy link
Copy Markdown
Member Author

Thanks. Going through the five findings:

1, 2 and 5 (payload type, raw stack map, dataclass helpers): fixed in 85b17ea. ConsoleMessage and ScriptError are dataclasses again. add_console_message_handler and add_javascript_error_handler now deliver subclasses of both the ADR payload and the generated ConsoleLogEntry / JavascriptLogEntry. StackTrace is a dict subclass, so isinstance, dataclasses.asdict, json.dumps and ["callFrames"] all keep working. New unit tests cover each of these.

3 (conflicting window arguments): not changing this. window_handle=None is the default and means "not given", so a call with only context_id is the deprecated spelling working as intended. A non-None window_handle with context_id=None is the supported spelling. The TypeError is only for a call that supplies both, which is what the check does.

4 (iframe context id): real. window_handle comes from the log entry's source.context, which for an iframe is the frame's browsing context id. Fixing that needs the browsing-context tree, so it's listed as a follow-up in the description rather than widening this PR.


Generated by Claude Code

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 85b17ea

A log entry names the browsing context of the document that logged it,
which for an iframe is the frame's context, not a window handle.
ConsoleMessage.window_handle and ScriptError.window_handle now resolve
that context to its top-level window by walking parents with
browsingContext.getTree on first sight and caching the answer (context
ids are never reused). A new frame field carries the iframe's context
when the event did not come from the window's own document.

Lookups run on the event callback's thread, never on the websocket
reader thread, so they cannot block command replies.
@selenium-ci

Copy link
Copy Markdown
Member

/agentic_review

Comment thread py/private/_script_handlers.py Resolved
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit afe6d16

Each event callback runs on its own thread, so a burst of log entries
from a context not yet cached could each send their own
browsingContext.getTree. Serialise cache misses so the first lookup
fills the cache and the rest read it.
@selenium-ci

Copy link
Copy Markdown
Member

/agentic_review

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 9061956

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

B-devtools Includes everything BiDi or Chrome DevTools related C-py Python Bindings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants