Skip to content

[py][bidi]: add speculation module#17162

Open
navin772 wants to merge 4 commits intoSeleniumHQ:trunkfrom
navin772:speculation-py-bidi
Open

[py][bidi]: add speculation module#17162
navin772 wants to merge 4 commits intoSeleniumHQ:trunkfrom
navin772:speculation-py-bidi

Conversation

@navin772
Copy link
Member

@navin772 navin772 commented Mar 2, 2026

🔗 Related Issues

💥 What does this PR do?

Adds the speculation module to python bindings - https://wicg.github.io/nav-speculation/prefetch.html#speculation-module

🔧 Implementation Notes

💡 Additional Considerations

🔄 Types of changes

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

Copilot AI review requested due to automatic review settings March 2, 2026 15:56
@qodo-code-review
Copy link
Contributor

Review Summary by Qodo

Add speculation module to Python BiDi bindings

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Adds speculation module to Python BiDi bindings
• Implements prefetch status event handling and subscription management
• Provides event handler registration, removal, and cleanup methods
• Includes comprehensive test coverage for prefetch status events

Grey Divider

File Changes

1. py/selenium/webdriver/common/bidi/speculation.py ✨ Enhancement +198/-0

Speculation module implementation with event handling

• Introduces PreloadingStatus class with status constants (PENDING, READY, SUCCESS, FAILURE)
• Implements PrefetchStatusUpdatedParams class for event parameter deserialization with validation
• Creates PrefetchStatusUpdated event class for BiDi event mapping
• Implements Speculation class with event handler management, subscription tracking, and
 thread-safe operations

py/selenium/webdriver/common/bidi/speculation.py


2. py/selenium/webdriver/remote/webdriver.py ✨ Enhancement +31/-0

Add speculation property to WebDriver class

• Imports Speculation class from BiDi speculation module
• Adds _speculation instance variable initialization in __init__
• Implements speculation property with lazy initialization and WebSocket connection setup
• Includes docstring with usage examples for the speculation module

py/selenium/webdriver/remote/webdriver.py


3. py/test/selenium/webdriver/common/bidi_speculation_tests.py 🧪 Tests +189/-0

Comprehensive test suite for speculation module

• Tests speculation module initialization
• Validates prefetch status events with pending and ready statuses
• Tests navigation to prefetched pages and success status events
• Tests failure event handling for non-existent resources
• Verifies event handler unsubscription and cleanup functionality
• Tests error handling for invalid event names

py/test/selenium/webdriver/common/bidi_speculation_tests.py


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Mar 2, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (2) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Missing speculationRules fixture 🐞 Bug ✓ Correctness
Description
The new speculation tests navigate to bidi/speculationRules.html and call
addSpeculationRulesAndLink(), but this PR does not add that HTML/JS fixture. The test web server
returns 404 for missing files, so tests will fail (or error on undefined JS) before any BiDi events
can be asserted.
Code

py/test/selenium/webdriver/common/bidi_speculation_tests.py[R25-55]

+def _add_speculation_rules_and_link(driver, prefetch_url):
+    driver.execute_script(f"addSpeculationRulesAndLink('{prefetch_url}')")
+
+
+def test_speculation_module_initialized(driver):
+    assert driver.speculation is not None
+
+
+def test_prefetch_status_updated_with_pending_and_ready_events(driver, pages):
+    """Test that prefetch status updated events are received with pending and ready statuses."""
+    events_received = []
+
+    def on_prefetch_status_updated(event):
+        events_received.append(event)
+
+    callback_id = driver.speculation.add_event_handler("prefetch_status_updated", on_prefetch_status_updated)
+
+    try:
+        url = pages.url("bidi/speculationRules.html")
+        prefetch_url = pages.url("bidi/emptyPage.html")
+        driver.browsing_context.navigate(
+            context=driver.current_window_handle,
+            url=url,
+            wait=ReadinessState.COMPLETE,
+        )
+
+        _add_speculation_rules_and_link(driver, prefetch_url)
+
+        # Wait for at least two events (pending + ready)
+        WebDriverWait(driver, 10).until(lambda _: len(events_received) >= 2)
+
Evidence
The tests hard-depend on bidi/speculationRules.html and on a JS function
addSpeculationRulesAndLink(...) being present in that page. The Python test webserver serves files
from common/src/web and returns a 404 when the requested file does not exist, which will prevent
navigation and therefore prevent any prefetch events from being generated.

py/test/selenium/webdriver/common/bidi_speculation_tests.py[25-55]
py/test/selenium/webdriver/common/webserver.py[88-105]

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 speculation BiDi tests navigate to `bidi/speculationRules.html` and execute `addSpeculationRulesAndLink(...)`, but the PR does not include this HTML/JS fixture. The test webserver will return 404 for missing files, causing the tests to fail early.
### Issue Context
The webserver serves from `common/src/web` and returns 404 if the file doesn’t exist.
### Fix Focus Areas
- common/src/web/bidi/speculationRules.html[1-200]
- py/test/selenium/webdriver/common/bidi_speculation_tests.py[25-60]

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



Remediation recommended

2. No parity evidence for speculation 📘 Rule violation ⛯ Reliability
Description
This PR adds a new user-visible Python BiDi surface (WebDriver.speculation) but provides no
evidence of cross-binding comparison or parity follow-up tracking. This risks protocol-level
behavior drift across bindings.
Code

py/selenium/webdriver/remote/webdriver.py[R1201-1228]

+    @property
+    def speculation(self) -> Speculation:
+        """Returns a speculation module object for BiDi speculation commands.
+
+        The speculation module contains commands for managing the remote end
+        behavior for prefetches, prerenders, and speculation rules.
+
+        Returns:
+            An object containing access to BiDi speculation events.
+
+        Examples:
+            ```
+            from selenium.webdriver.common.bidi.speculation import PreloadingStatus
+
+            events = []
+            callback_id = driver.speculation.add_event_handler("prefetch_status_updated", events.append)
+            # ... trigger prefetch ...
+            driver.speculation.remove_event_handler("prefetch_status_updated", callback_id)
+            ```
+        """
+        if not self._websocket_connection:
+            self._start_bidi()
+
+        assert self._websocket_connection is not None
+        if self._speculation is None:
+            self._speculation = Speculation(self._websocket_connection)
+
+        return self._speculation
Evidence
PR Compliance ID 5 requires evidence of cross-binding comparison or a parity follow-up for
user-visible behavior changes; the PR adds a new speculation module entrypoint but the PR
description only mentions adding it to Python with no parity note or follow-up reference.

AGENTS.md
py/selenium/webdriver/remote/webdriver.py[1201-1228]

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

## Issue description
This PR introduces a new user-visible Python BiDi surface (`WebDriver.speculation`) but does not provide evidence of cross-binding comparison or parity follow-up tracking.
## Issue Context
Compliance requires that user-visible behavior changes be validated against at least one other binding, or that parity follow-up work is tracked for shared/protocol-level changes.
## Fix Focus Areas
- py/selenium/webdriver/remote/webdriver.py[1201-1228]

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


3. Tests use “what” comments 📘 Rule violation ✓ Correctness
Description
Several new comments in tests restate obvious steps (what the code does) rather than documenting
rationale. This reduces maintainability because the intent/why is not captured when the
implementation changes.
Code

py/test/selenium/webdriver/common/bidi_speculation_tests.py[R53-55]

+        # Wait for at least two events (pending + ready)
+        WebDriverWait(driver, 10).until(lambda _: len(events_received) >= 2)
+
Evidence
PR Compliance ID 9 requires comments to focus on rationale; the added test comment merely narrates
the next step (waiting for two events) without explaining why that wait condition is necessary
(e.g., flake prevention, protocol behavior).

AGENTS.md
py/test/selenium/webdriver/common/bidi_speculation_tests.py[53-55]

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

## Issue description
Some newly added comments narrate obvious actions ("what") instead of documenting rationale ("why"), which can make future maintenance harder.
## Issue Context
The compliance checklist asks that comments explain intent/constraints rather than restating code.
## Fix Focus Areas
- py/test/selenium/webdriver/common/bidi_speculation_tests.py[53-55]

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


4. Unsubscribe test likely flaky 🐞 Bug ⛯ Reliability
Description
test_can_unsubscribe_from_prefetch_status_updated unsubscribes after waiting for only one event,
then asserts the event count never increases. Because BiDi events are delivered on background
threads, additional in-flight events from the first prefetch can arrive after
remove_event_handler, making the equality assertion intermittently fail.
Code

py/test/selenium/webdriver/common/bidi_speculation_tests.py[R159-179]

+        _add_speculation_rules_and_link(driver, prefetch_url)
+
+        # Wait for initial events
+        WebDriverWait(driver, 10).until(lambda _: len(events_received) >= 1)
+
+        initial_count = len(events_received)
+
+        # Unsubscribe from events
+        driver.speculation.remove_event_handler("prefetch_status_updated", callback_id)
+
+        # Reload and trigger new speculation rules with different target
+        driver.browsing_context.navigate(
+            context=driver.current_window_handle,
+            url=url,
+            wait=ReadinessState.COMPLETE,
+        )
+
+        second_prefetch_url = pages.url("blank.html")
+        _add_speculation_rules_and_link(driver, second_prefetch_url)
+
+        assert len(events_received) == initial_count
Evidence
The test removes the handler after seeing just one event, but prefetch commonly emits multiple
status updates (pending/ready/success/failure). Separately, the websocket connection dispatches
callbacks on new threads; removing a callback does not cancel already-started callback threads, so
late arrivals can still append to events_received after removal.

py/test/selenium/webdriver/common/bidi_speculation_tests.py[159-179]
py/selenium/webdriver/remote/websocket_connection.py[98-105]
py/selenium/webdriver/remote/websocket_connection.py[144-148]

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 unsubscribe test can fail intermittently because it unsubscribes after only one event, while additional status events may still arrive on background threads.
### Issue Context
WebSocket event callbacks are invoked on separate threads; removing the callback does not cancel already-running callback threads.
### Fix Focus Areas
- py/test/selenium/webdriver/common/bidi_speculation_tests.py[141-183]
- py/selenium/webdriver/remote/websocket_connection.py[98-148]

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


View more (1)
5. Contexts only first subscribe 🐞 Bug ✓ Correctness
Description
Speculation.add_event_handler accepts contexts, but only applies it on the first subscription
for a given BiDi event. Adding a second handler for the same event with different contexts will not
update the remote subscription, potentially causing missing events or surprising behavior.
Code

py/selenium/webdriver/common/bidi/speculation.py[R136-158]

+    def add_event_handler(self, event: str, callback: Callable, contexts: list[str] | None = None) -> int:
+        """Add an event handler for a speculation event.
+
+        Args:
+            event: The event to subscribe to (e.g., "prefetch_status_updated").
+            callback: The callback function to execute on event.
+            contexts: Optional browsing context IDs to subscribe to.
+
+        Returns:
+            Callback id for later removal.
+
+        Raises:
+            ValueError: If the event name is not recognized.
+        """
+        bidi_event, event_class = self._validate_event(event)
+
+        callback_id = self.conn.add_callback(event_class, callback)
+
+        with self._subscription_lock:
+            if bidi_event not in self.subscriptions:
+                self.conn.execute(self._session.subscribe(bidi_event, browsing_contexts=contexts))
+                self.subscriptions[bidi_event] = []
+            self.subscriptions[bidi_event].append(callback_id)
Evidence
The implementation subscribes at most once per BiDi event and passes contexts only in that first
subscribe call. The underlying Session API supports specifying browsing contexts, so callers may
reasonably expect each handler registration’s contexts to be respected (e.g., merged/unioned) or
for the limitation to be documented.

py/selenium/webdriver/common/bidi/speculation.py[136-158]
py/selenium/webdriver/common/bidi/session.py[104-112]

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

## Issue description
`contexts` is accepted per `add_event_handler` call but only affects the first `session.subscribe` call for that BiDi event.
### Issue Context
This can lead to missing events when multiple handlers are registered for the same event but with different context filters.
### Fix Focus Areas
- py/selenium/webdriver/common/bidi/speculation.py[136-184]
- py/selenium/webdriver/common/bidi/session.py[104-123]

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


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@selenium-ci selenium-ci added C-py Python Bindings B-devtools Includes everything BiDi or Chrome DevTools related labels Mar 2, 2026
Comment on lines +25 to +55
def _add_speculation_rules_and_link(driver, prefetch_url):
driver.execute_script(f"addSpeculationRulesAndLink('{prefetch_url}')")


def test_speculation_module_initialized(driver):
assert driver.speculation is not None


def test_prefetch_status_updated_with_pending_and_ready_events(driver, pages):
"""Test that prefetch status updated events are received with pending and ready statuses."""
events_received = []

def on_prefetch_status_updated(event):
events_received.append(event)

callback_id = driver.speculation.add_event_handler("prefetch_status_updated", on_prefetch_status_updated)

try:
url = pages.url("bidi/speculationRules.html")
prefetch_url = pages.url("bidi/emptyPage.html")
driver.browsing_context.navigate(
context=driver.current_window_handle,
url=url,
wait=ReadinessState.COMPLETE,
)

_add_speculation_rules_and_link(driver, prefetch_url)

# Wait for at least two events (pending + ready)
WebDriverWait(driver, 10).until(lambda _: len(events_received) >= 2)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Missing speculationrules fixture 🐞 Bug ✓ Correctness

The new speculation tests navigate to bidi/speculationRules.html and call
addSpeculationRulesAndLink(), but this PR does not add that HTML/JS fixture. The test web server
returns 404 for missing files, so tests will fail (or error on undefined JS) before any BiDi events
can be asserted.
Agent Prompt
### Issue description
The speculation BiDi tests navigate to `bidi/speculationRules.html` and execute `addSpeculationRulesAndLink(...)`, but the PR does not include this HTML/JS fixture. The test webserver will return 404 for missing files, causing the tests to fail early.

### Issue Context
The webserver serves from `common/src/web` and returns 404 if the file doesn’t exist.

### Fix Focus Areas
- common/src/web/bidi/speculationRules.html[1-200]
- py/test/selenium/webdriver/common/bidi_speculation_tests.py[25-60]

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

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds initial Python BiDi “speculation” module support (prefetch status events) and exposes it on RemoteWebDriver, along with a new pytest suite intended to validate event subscription behavior.

Changes:

  • Introduces selenium.webdriver.common.bidi.speculation with event parsing and subscribe/unsubscribe helpers.
  • Exposes the module as driver.speculation in Python RemoteWebDriver.
  • Adds Python tests for speculation.prefetchStatusUpdated event handling.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

File Description
py/selenium/webdriver/common/bidi/speculation.py New Speculation BiDi module (event classes + subscription management).
py/selenium/webdriver/remote/webdriver.py Adds driver.speculation property and internal caching.
py/test/selenium/webdriver/common/bidi_speculation_tests.py New end-to-end tests for speculation prefetch status events.

Comment on lines +169 to +179
# Reload and trigger new speculation rules with different target
driver.browsing_context.navigate(
context=driver.current_window_handle,
url=url,
wait=ReadinessState.COMPLETE,
)

second_prefetch_url = pages.url("blank.html")
_add_speculation_rules_and_link(driver, second_prefetch_url)

assert len(events_received) == initial_count
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test triggers a second prefetch after unsubscribing but immediately asserts the event count didn’t change. Because prefetch status events are asynchronous, the assertion can pass even if events arrive slightly later (making the test non-verifying/flaky). Add a short wait after triggering the second prefetch and assert the count remains unchanged over that window (or wait for a condition that would fail if an event is received).

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +109
# Reverse mapping: BiDi event name to event class
_BIDI_TO_CLASS: dict[str, type] = {
"speculation.prefetchStatusUpdated": PrefetchStatusUpdated,
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_EVENT_MAP and _BIDI_TO_CLASS contain the same information in two places. This duplication is easy to get out of sync when adding events (and clear_event_handlers() depends on _BIDI_TO_CLASS to clean up). Derive _BIDI_TO_CLASS from _EVENT_MAP (or store event_class alongside callback IDs) so a single source of truth is used.

Suggested change
# Reverse mapping: BiDi event name to event class
_BIDI_TO_CLASS: dict[str, type] = {
"speculation.prefetchStatusUpdated": PrefetchStatusUpdated,
# Reverse mapping: BiDi event name to event class, derived from _EVENT_MAP
_BIDI_TO_CLASS: dict[str, type] = {
bidi_event_name: event_class for _, (bidi_event_name, event_class) in _EVENT_MAP.items()

Copilot uses AI. Check for mistakes.
Comment on lines +191 to +197
for bidi_event, callback_ids in list(self.subscriptions.items()):
event_class = self._BIDI_TO_CLASS.get(bidi_event)
if event_class:
for callback_id in callback_ids:
self.conn.remove_callback(event_class, callback_id)
self.conn.execute(self._session.unsubscribe(bidi_event))

Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clear_event_handlers() only unsubscribes when event_class is found in _BIDI_TO_CLASS. If the maps ever drift, this will leave remote-end event subscriptions active (and leak callbacks locally). Consider always calling session.unsubscribe(bidi_event) even if the event class can’t be resolved, and/or ensuring the reverse mapping is automatically generated so this can’t happen.

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +99
"""BiDi implementation of the speculation module.

The speculation module contains commands for managing the remote end
behavior for prefetches, prerenders, and speculation rules.
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring says the speculation module provides “commands for managing … prefetches, prerenders, and speculation rules”, but the current Speculation implementation only exposes event subscription helpers. Consider adjusting the wording to match the actual API surface (events-only), or implement the described commands if they’re intended to be part of this module in this PR.

Suggested change
"""BiDi implementation of the speculation module.
The speculation module contains commands for managing the remote end
behavior for prefetches, prerenders, and speculation rules.
"""BiDi interface for the speculation module.
This class exposes helpers for subscribing to and handling speculation
module events (such as prefetch status updates) and managing the
associated event subscriptions.

Copilot uses AI. Check for mistakes.
Comment on lines +1203 to +1209
"""Returns a speculation module object for BiDi speculation commands.

The speculation module contains commands for managing the remote end
behavior for prefetches, prerenders, and speculation rules.

Returns:
An object containing access to BiDi speculation events.
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This property’s docstring describes “commands for managing … prefetches, prerenders, and speculation rules”, but driver.speculation currently only provides event handler registration/removal. Please align the docstring with the actual feature set to avoid confusing users.

Suggested change
"""Returns a speculation module object for BiDi speculation commands.
The speculation module contains commands for managing the remote end
behavior for prefetches, prerenders, and speculation rules.
Returns:
An object containing access to BiDi speculation events.
"""Returns a speculation module object for BiDi speculation events.
The speculation module exposes events related to prefetches, prerenders,
and speculation rules, and allows registering and removing event
handlers for those events.
Returns:
A Speculation object used to register and remove BiDi speculation
event handlers.

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +28
driver.execute_script(f"addSpeculationRulesAndLink('{prefetch_url}')")


Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The helper relies on a page-scoped JavaScript function addSpeculationRulesAndLink(...), but this PR doesn’t add the corresponding test fixture / script. As-is, this will raise a JS error at runtime and the speculation tests will fail. Consider inlining the DOM+speculationrules setup via execute_script (similar to other bindings) or add the missing fixture that defines this function.

Suggested change
driver.execute_script(f"addSpeculationRulesAndLink('{prefetch_url}')")
driver.execute_script(
"""
const prefetchUrl = arguments[0];
// Create speculation rules script
const script = document.createElement('script');
script.type = 'speculationrules';
script.textContent = JSON.stringify({
prefetch: [
{
source: 'document',
href: prefetchUrl,
},
],
});
document.head.appendChild(script);
// Create a link to the prefetched URL so tests can navigate via click
const link = document.createElement('a');
link.href = prefetchUrl;
link.textContent = 'navigate';
document.body.appendChild(link);
""",
prefetch_url,
)

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +45
url = pages.url("bidi/speculationRules.html")
prefetch_url = pages.url("bidi/emptyPage.html")
driver.browsing_context.navigate(
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pages.url("bidi/speculationRules.html") points to a fixture that doesn’t exist under common/src/web/bidi/ in this repo. These tests will fail with a 404 unless that HTML fixture is added (and contains the required speculationrules/link setup).

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +33
def test_speculation_module_initialized(driver):
assert driver.speculation is not None


def test_prefetch_status_updated_with_pending_and_ready_events(driver, pages):
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Speculation BiDi support appears to be unavailable in Firefox (the repo’s .NET speculation test is explicitly ignored for Firefox). These tests should be marked xfail_firefox/skipped accordingly to avoid consistent failures when the Python suite runs against Firefox.

Copilot uses AI. Check for mistakes.
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.

3 participants