From c367423c85a7120b6e006b5c8beae9d46b1becd1 Mon Sep 17 00:00:00 2001 From: Steven Wei Date: Mon, 6 Oct 2025 18:22:28 -0700 Subject: [PATCH 01/17] add testdriver extension for speculation module and event, and add wpt and infrastructure tests --- .../prefetch_status_updated.https.html | 221 +++++++++++++++++ .../resources/bidi-speculation-helper.js | 45 ++++ .../bidi/speculation/resources/target.html | 10 + resources/testdriver.js | 39 +++ tools/wptrunner/wptrunner/testdriver-extra.js | 37 ++- .../bidi/external/speculation/__init__.py | 0 .../bidi/external/speculation/conftest.py | 45 ++++ .../prefetch_status_updated/__init__.py | 0 .../prefetch_status_updated/invalid.py | 26 ++ .../prefetch_status_updated.py | 224 ++++++++++++++++++ 10 files changed, 645 insertions(+), 2 deletions(-) create mode 100644 infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html create mode 100644 infrastructure/testdriver/bidi/speculation/resources/bidi-speculation-helper.js create mode 100644 infrastructure/testdriver/bidi/speculation/resources/target.html create mode 100644 webdriver/tests/bidi/external/speculation/__init__.py create mode 100644 webdriver/tests/bidi/external/speculation/conftest.py create mode 100644 webdriver/tests/bidi/external/speculation/prefetch_status_updated/__init__.py create mode 100644 webdriver/tests/bidi/external/speculation/prefetch_status_updated/invalid.py create mode 100644 webdriver/tests/bidi/external/speculation/prefetch_status_updated/prefetch_status_updated.py diff --git a/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html b/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html new file mode 100644 index 00000000000000..67ff707fff3408 --- /dev/null +++ b/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html @@ -0,0 +1,221 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/infrastructure/testdriver/bidi/speculation/resources/bidi-speculation-helper.js b/infrastructure/testdriver/bidi/speculation/resources/bidi-speculation-helper.js new file mode 100644 index 00000000000000..0f5fa3ae2e3a34 --- /dev/null +++ b/infrastructure/testdriver/bidi/speculation/resources/bidi-speculation-helper.js @@ -0,0 +1,45 @@ +'use strict'; + +/** + * Helper functions for speculation rules BiDi testdriver tests + */ + +/** + * Waits until the document has finished loading. + * @returns {Promise} Resolves if the document is already completely + * loaded or when the 'onload' event is fired. + */ +function waitForDocumentReady() { + return new Promise(resolve => { + if (document.readyState === 'complete') { + resolve(); + } + + window.addEventListener('load', () => { + resolve(); + }, {once: true}); + }); +} + +/** + * Adds speculation rules and a corresponding link to the page. + * @param {Object} speculationRules - The speculation rules object to add + * @param {string} targetUrl - The URL to add as a link + * @param {string} linkText - The text content of the link (optional) + * @returns {Object} Object containing the created script and link elements + */ +function addSpeculationRulesAndLink(speculationRules, targetUrl, linkText = 'Test Link') { + // Add speculation rules script exactly like the working test + const script = document.createElement('script'); + script.type = 'speculationrules'; + script.textContent = JSON.stringify(speculationRules); + document.head.appendChild(script); + + // Also add a link to the page (some implementations might need this) + const link = document.createElement('a'); + link.href = targetUrl; + link.textContent = linkText; + document.body.appendChild(link); + + return { script, link }; +} \ No newline at end of file diff --git a/infrastructure/testdriver/bidi/speculation/resources/target.html b/infrastructure/testdriver/bidi/speculation/resources/target.html new file mode 100644 index 00000000000000..a64b648bca709b --- /dev/null +++ b/infrastructure/testdriver/bidi/speculation/resources/target.html @@ -0,0 +1,10 @@ + + + + Prefetch Target Page + + +

This is a prefetch target page

+

This page is used as a target for prefetch testing.

+ + \ No newline at end of file diff --git a/resources/testdriver.js b/resources/testdriver.js index 5b390dedeb72bb..622232bd03b0d3 100644 --- a/resources/testdriver.js +++ b/resources/testdriver.js @@ -769,6 +769,33 @@ }, } }, + speculation: { + prefetch_status_updated: { + subscribe: async function(params = {}) { + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.speculation + .prefetch_status_updated.subscribe(params); + }, + + on: function(callback) { + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.speculation + .prefetch_status_updated.on(callback); + }, + + once: function() { + assertBidiIsEnabled(); + return new Promise(resolve => { + const remove_handler = + window.test_driver_internal.bidi.speculation + .prefetch_status_updated.on(event => { + resolve(event); + remove_handler(); + }); + }); + } + } + }, /** * `emulation `_ module. */ @@ -2271,6 +2298,18 @@ throw new Error( "bidi.permissions.set_permission() is not implemented by testdriver-vendor.js"); } + }, + speculation: { + prefetch_status_updated: { + async subscribe() { + throw new Error( + 'bidi.speculation.prefetch_status_updated.subscribe is not implemented by testdriver-vendor.js'); + }, + on() { + throw new Error( + 'bidi.speculation.prefetch_status_updated.on is not implemented by testdriver-vendor.js'); + } + }, } }, diff --git a/tools/wptrunner/wptrunner/testdriver-extra.js b/tools/wptrunner/wptrunner/testdriver-extra.js index 4d26a14097f530..07dfec07a22f88 100644 --- a/tools/wptrunner/wptrunner/testdriver-extra.js +++ b/tools/wptrunner/wptrunner/testdriver-extra.js @@ -228,6 +228,21 @@ } }; + const subscribe_global = async function (params) { + const action_result = await create_action("bidi.session.subscribe", { + // Subscribe to all contexts. + ...params + }); + const subscription_id = action_result["subscription"]; + + return async ()=>{ + console.log("!!@@## unsubscribing") + await create_action("bidi.session.unsubscribe", { + // Default to subscribing to the window's events. + subscriptions: [subscription_id] + }); + } + }; window.test_driver_internal.in_automation = true; window.test_driver_internal.bidi.bluetooth.handle_request_device_prompt = @@ -656,5 +671,23 @@ window.test_driver_internal.clear_display_features = function(context=null) { return create_context_action("clear_display_features", context, {}); - } -})(); + }; + + window.test_driver_internal.bidi.speculation.prefetch_status_updated.subscribe = + function(params) { + return subscribe_global( + {params, events: ['speculation.prefetchStatusUpdated']} + ); + }; + + window.test_driver_internal.bidi.speculation.prefetch_status_updated.on = + function(callback) { + const on_event = (event) => { + callback(event.payload); + }; + event_target.addEventListener( + 'speculation.prefetchStatusUpdated', on_event); + return () => event_target.removeEventListener( + 'speculation.prefetchStatusUpdated', on_event); + }; +})(); \ No newline at end of file diff --git a/webdriver/tests/bidi/external/speculation/__init__.py b/webdriver/tests/bidi/external/speculation/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/webdriver/tests/bidi/external/speculation/conftest.py b/webdriver/tests/bidi/external/speculation/conftest.py new file mode 100644 index 00000000000000..0b367db4792f32 --- /dev/null +++ b/webdriver/tests/bidi/external/speculation/conftest.py @@ -0,0 +1,45 @@ +import pytest +from typing import Any, Mapping + +from webdriver.bidi.modules.script import ContextTarget + + +@pytest.fixture +def speculation_rules_helper(bidi_session): + """Helper for adding speculation rules to a page.""" + + async def add_speculation_rules(context: Mapping[str, Any], rules: str): + """Add speculation rules script to the page.""" + await bidi_session.script.evaluate( + expression=f""" + const script = document.createElement('script'); + script.type = 'speculationrules'; + script.textContent = `{rules}`; + document.head.appendChild(script); + """, + target=ContextTarget(context["context"]), + await_promise=False + ) + + return add_speculation_rules + + +@pytest.fixture +def add_prefetch_link(bidi_session): + """Helper for adding links to the page that can be targeted by speculation rules.""" + + async def add_link(context: Mapping[str, Any], href: str, text: str = "Test Link", link_id: str = "prefetch-page"): + """Add a link to the page for prefetch targeting.""" + await bidi_session.script.evaluate( + expression=f""" + const link = document.createElement('a'); + link.href = '{href}'; + link.textContent = '{text}'; + link.id = '{link_id}'; + document.body.appendChild(link); + """, + target={"context": context["context"]}, + await_promise=False + ) + + return add_link \ No newline at end of file diff --git a/webdriver/tests/bidi/external/speculation/prefetch_status_updated/__init__.py b/webdriver/tests/bidi/external/speculation/prefetch_status_updated/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/webdriver/tests/bidi/external/speculation/prefetch_status_updated/invalid.py b/webdriver/tests/bidi/external/speculation/prefetch_status_updated/invalid.py new file mode 100644 index 00000000000000..a2b6ced5ee9ea7 --- /dev/null +++ b/webdriver/tests/bidi/external/speculation/prefetch_status_updated/invalid.py @@ -0,0 +1,26 @@ +import pytest + +from webdriver.bidi.error import InvalidArgumentException, NoSuchFrameException + +pytestmark = pytest.mark.asyncio + + +@pytest.mark.asyncio +async def test_invalid_event_name(bidi_session): + """Test that subscribing to invalid speculation events raises an error.""" + + with pytest.raises(InvalidArgumentException): + await bidi_session.session.subscribe( + events=["speculation.invalidEvent"] + ) + + +@pytest.mark.asyncio +async def test_subscribe_with_invalid_context(bidi_session): + """Test subscribing to prefetch events with invalid context.""" + + with pytest.raises(NoSuchFrameException): + await bidi_session.session.subscribe( + events=["speculation.prefetchStatusUpdated"], + contexts=["invalid-context-id"] + ) \ No newline at end of file diff --git a/webdriver/tests/bidi/external/speculation/prefetch_status_updated/prefetch_status_updated.py b/webdriver/tests/bidi/external/speculation/prefetch_status_updated/prefetch_status_updated.py new file mode 100644 index 00000000000000..2489a4e1ca462c --- /dev/null +++ b/webdriver/tests/bidi/external/speculation/prefetch_status_updated/prefetch_status_updated.py @@ -0,0 +1,224 @@ +import pytest + +pytestmark = pytest.mark.asyncio + +@pytest.mark.asyncio +async def test_subscribe_to_prefetch_status_updated( + bidi_session, subscribe_events, new_tab, url +): + """Test basic subscription to prefetch status updated events.""" + + # Subscribe to prefetch status updated events + await subscribe_events(events=["speculation.prefetchStatusUpdated"]) + + # Navigate to a test page + test_url = url("/common/blank.html", protocol="https") + await bidi_session.browsing_context.navigate( + context=new_tab["context"], + url=test_url, + wait="complete", + ) + + assert True + + +@pytest.mark.asyncio +async def test_speculation_rules_generate_ready_events( + bidi_session, subscribe_events, new_tab, url, wait_for_events, speculation_rules_helper, add_prefetch_link +): + """Test that speculation rules generate prefetch events with proper pending->ready sequence.""" + + await subscribe_events(events=["speculation.prefetchStatusUpdated"]) + + test_url = url("/common/blank.html", protocol="https") + await bidi_session.browsing_context.navigate( + context=new_tab["context"], + url=test_url, + wait="none", + ) + + # Add speculation rules to trigger immediate prefetching + prefetch_target = url("/common/dummy.xml", protocol="https") + speculation_rules = f'''{{ + "prefetch": [{{ + "where": {{ "href_matches": "{prefetch_target}" }}, + "eagerness": "immediate" + }}] + }}''' + + # Set up event waiter before triggering prefetch + with wait_for_events(["speculation.prefetchStatusUpdated"]) as waiter: + # Add speculation rules and link to trigger prefetching + await speculation_rules_helper(new_tab, speculation_rules) + await add_prefetch_link(new_tab, prefetch_target) + + # Wait for pending and ready events + events = await waiter.get_events(lambda events: len(events) == 2) + + # Verify all events have correct structure and sequence + assert len(events) == 2, f"Expected 2 prefetch events (pending and ready), got {len(events)}" + assert events == [ + ("speculation.prefetchStatusUpdated", { + "url": prefetch_target, + "status": "pending", + "context": new_tab["context"] + }), + ("speculation.prefetchStatusUpdated", { + "url": prefetch_target, + "status": "ready", + "context": new_tab["context"] + }) + ], f"Events don't match expected sequence: {events}" + + +@pytest.mark.asyncio +async def test_speculation_rules_generate_events_with_navigation( + bidi_session, subscribe_events, new_tab, url, wait_for_events, speculation_rules_helper, add_prefetch_link +): + """Test that speculation rules generate prefetch events with navigation and success event after using prefetched page.""" + + await subscribe_events(events=["speculation.prefetchStatusUpdated"]) + + test_url = url("/common/blank.html", protocol="https") + await bidi_session.browsing_context.navigate( + context=new_tab["context"], + url=test_url, + wait="none", + ) + + # Add speculation rules to trigger immediate prefetching + prefetch_target = url("/common/dummy.xml", protocol="https") + speculation_rules = f'''{{ + "prefetch": [{{ + "where": {{ "href_matches": "{prefetch_target}" }}, + "eagerness": "immediate" + }}] + }}''' + + # Set up event waiter before triggering prefetch - capture all events through navigation + with wait_for_events(["speculation.prefetchStatusUpdated"]) as waiter: + # Add speculation rules and link to trigger prefetching + await speculation_rules_helper(new_tab, speculation_rules) + await add_prefetch_link(new_tab, prefetch_target) + + # Wait for pending and ready events first + events = await waiter.get_events(lambda events: len(events) >= 2) + + # Verify we got pending and ready events + assert len(events) == 2, f"Expected 2 prefetch events (pending and ready), got {len(events)}" + assert events == [ + ("speculation.prefetchStatusUpdated", { + "url": prefetch_target, + "status": "pending", + "context": new_tab["context"] + }), + ("speculation.prefetchStatusUpdated", { + "url": prefetch_target, + "status": "ready", + "context": new_tab["context"] + }) + ], f"Events don't match expected sequence: {events}" + + # Now navigate to the prefetched page to potentially trigger success event + # Navigate by clicking the link (user-initiated navigation to trigger success event) + await bidi_session.script.evaluate( + expression=""" + const prefetchLink = document.getElementById('prefetch-page'); + if (prefetchLink) { + prefetchLink.click(); + } + """, + target={"context": new_tab["context"]}, + await_promise=False + ) + + # Wait for success event after navigation to the prefetched page + all_events = await waiter.get_events(lambda events: len(events) >= 3) + + # Verify all events have correct structure and sequence + assert len(all_events) == 3, f"Expected 3 prefetch events (pending, ready, and success), got {len(all_events)}" + assert all_events == [ + ("speculation.prefetchStatusUpdated", { + "url": prefetch_target, + "status": "pending", + "context": new_tab["context"] + }), + ("speculation.prefetchStatusUpdated", { + "url": prefetch_target, + "status": "ready", + "context": new_tab["context"] + }), + ("speculation.prefetchStatusUpdated", { + "url": prefetch_target, + "status": "success", + "context": new_tab["context"] + }) + ], f"Events don't match expected sequence: {events}" + + +@pytest.mark.asyncio +async def test_speculation_rules_generate_failure_events( + bidi_session, subscribe_events, new_tab, url, wait_for_events, speculation_rules_helper, add_prefetch_link +): + """Test that speculation rules generate pending and failure events for failed prefetch.""" + + await subscribe_events(events=["speculation.prefetchStatusUpdated"]) + + test_url = url("/common/blank.html", protocol="https") + await bidi_session.browsing_context.navigate( + context=new_tab["context"], + url=test_url, + wait="none", + ) + + # Create a target that will return 404 - use a non-existent path + failed_target = url("/nonexistent/path/that/will/404.xml", protocol="https") + + # Add speculation rules to trigger immediate prefetching of 404 page + speculation_rules = f'''{{ + "prefetch": [{{ + "where": {{ "href_matches": "{failed_target}" }}, + "eagerness": "immediate" + }}] + }}''' + + # Set up event waiter before triggering prefetch + with wait_for_events(["speculation.prefetchStatusUpdated"]) as waiter: + # Add speculation rules and link to trigger prefetching + await speculation_rules_helper(new_tab, speculation_rules) + await add_prefetch_link(new_tab, failed_target) + + # Wait for events (pending and failure) + events = await waiter.get_events(lambda events: len(events) >= 2) + + # Verify all events have correct structure and sequence + assert len(events) == 2, f"Expected 2 prefetch events (pending and failure), got {len(events)}" + assert events == [ + ("speculation.prefetchStatusUpdated", { + "url": failed_target, + "status": "pending", + "context": new_tab["context"] + }), + ("speculation.prefetchStatusUpdated", { + "url": failed_target, + "status": "failure", + "context": new_tab["context"] + }) + ], f"Events don't match expected sequence: {events}" + +@pytest.mark.asyncio +async def test_unsubscribe_from_prefetch_status_updated( + bidi_session +): + """Test unsubscribing from prefetch status updated events.""" + + # Subscribe to prefetch status updated events + subscription_result = await bidi_session.session.subscribe( + events=["speculation.prefetchStatusUpdated"] + ) + subscription_id = subscription_result["subscription"] + + # Unsubscribe immediately + await bidi_session.session.unsubscribe(subscriptions=[subscription_id]) + + assert True \ No newline at end of file From 386f50d881c537557c0607d9721c6e8cfe21e0f4 Mon Sep 17 00:00:00 2001 From: Steven Wei Date: Mon, 6 Oct 2025 18:40:17 -0700 Subject: [PATCH 02/17] remove console logs --- .../prefetch_status_updated.https.html | 44 +------------------ 1 file changed, 2 insertions(+), 42 deletions(-) diff --git a/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html b/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html index 67ff707fff3408..da74f0297e29b6 100644 --- a/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html +++ b/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html @@ -11,13 +11,10 @@ \ No newline at end of file From 7b480feba8079e51b9b51acf38c1c10f1cee58d2 Mon Sep 17 00:00:00 2001 From: Steven Wei Date: Fri, 31 Oct 2025 16:45:54 -0700 Subject: [PATCH 11/17] fail prefetch at network level --- .../bidi/speculation/prefetch_status_updated.https.html | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html b/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html index 4595d979f7af98..94d229fe3b7038 100644 --- a/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html +++ b/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html @@ -116,9 +116,10 @@ promise_test(async t => { const receivedEvents = new Set(); - const expectedStatuses = new Set(['pending', 'failure']); + const expectedStatuses = new Set(['failure']); - const errorUrl = window.location.origin + "/infrastructure/testdriver/bidi/speculation/resources/nonexistent-404-page.html"; + // Set error url to fail at network layer and only expect failure event + const errorUrl = "http://0.0.0.0:1/test.html"; // Create a promise that resolves when we receive the 'failure' event const failureEventPromise = new Promise((resolve, reject) => { @@ -151,9 +152,9 @@ assert_true( receivedStatuses.size === expectedStatuses.size && [...expectedStatuses].every(status => receivedStatuses.has(status)), - 'Should have received pending and failure events' + 'Should have received only failure event' ); - }, "prefetch_status_updated event with prefetch failure", { timeout: 30000 }); + }, "prefetch_status_updated event with prefetch failure"); \ No newline at end of file From d493553aa732a04a6e2d7b7c5ae64380aa1c5f57 Mon Sep 17 00:00:00 2001 From: Steven Wei Date: Fri, 31 Oct 2025 16:59:31 -0700 Subject: [PATCH 12/17] add debugging to failure test --- .../bidi/speculation/prefetch_status_updated.https.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html b/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html index 94d229fe3b7038..1e227b3ef64fc4 100644 --- a/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html +++ b/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html @@ -152,7 +152,7 @@ assert_true( receivedStatuses.size === expectedStatuses.size && [...expectedStatuses].every(status => receivedStatuses.has(status)), - 'Should have received only failure event' + 'Should have received only failure event. Expected: [${[...expectedStatuses].join(', ')}], Received: [${[...receivedStatuses].join(', ')}]' ); }, "prefetch_status_updated event with prefetch failure"); From a103beb750a5d5a6501b5d62f3c7d2a2d91b44f9 Mon Sep 17 00:00:00 2001 From: Steven Wei Date: Fri, 31 Oct 2025 17:10:46 -0700 Subject: [PATCH 13/17] typo --- .../bidi/speculation/prefetch_status_updated.https.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html b/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html index 1e227b3ef64fc4..f3966337648fe4 100644 --- a/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html +++ b/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html @@ -152,7 +152,7 @@ assert_true( receivedStatuses.size === expectedStatuses.size && [...expectedStatuses].every(status => receivedStatuses.has(status)), - 'Should have received only failure event. Expected: [${[...expectedStatuses].join(', ')}], Received: [${[...receivedStatuses].join(', ')}]' + `Should have received only failure event. Expected: [${[...expectedStatuses].join(', ')}], Received: [${[...receivedStatuses].join(', ')}]` ); }, "prefetch_status_updated event with prefetch failure"); From 05d3b7552a8a52d8678729beb577203a990379bf Mon Sep 17 00:00:00 2001 From: Steven Wei Date: Fri, 31 Oct 2025 18:48:36 -0700 Subject: [PATCH 14/17] subscribe and unsubscribe per each test to avoid contamination in CI --- .../prefetch_status_updated.https.html | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html b/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html index f3966337648fe4..a50d6c4be2ca76 100644 --- a/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html +++ b/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html @@ -11,12 +11,15 @@ \ No newline at end of file From 145030107db3afa185bd94acb594c92b0906b426 Mon Sep 17 00:00:00 2001 From: Steven Wei Date: Fri, 31 Oct 2025 19:03:15 -0700 Subject: [PATCH 15/17] remove unncessary logging --- .../bidi/speculation/prefetch_status_updated.https.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html b/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html index a50d6c4be2ca76..e5b430c30eab79 100644 --- a/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html +++ b/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html @@ -164,12 +164,10 @@ await failureEventPromise; // Assert that we received the expected events - const receivedStatusesArray = [...receivedStatuses]; - const expectedStatusesArray = [...expectedStatuses]; assert_true( receivedStatuses.size === expectedStatuses.size && [...expectedStatuses].every(status => receivedStatuses.has(status)), - `Should have received only failure event. Expected: [${expectedStatusesArray.join(', ')}], Received: [${receivedStatusesArray.join(', ')}]` + 'Should have received only failure event.' ); t.add_cleanup(async () => { From 15f8a4b604e39a52d948dad4bda63ddeefdb7f72 Mon Sep 17 00:00:00 2001 From: Steven Wei Date: Fri, 31 Oct 2025 19:22:07 -0700 Subject: [PATCH 16/17] reverted back to array from set() due to serialization issue in macOS infra --- .../prefetch_status_updated.https.html | 48 ++++++++----------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html b/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html index e5b430c30eab79..d9113e3d6c1151 100644 --- a/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html +++ b/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html @@ -18,16 +18,18 @@ // Subscribe for this test only const unsubscribe = await test_driver.bidi.speculation.prefetch_status_updated.subscribe(); - // Multiple events for the same status can be sent for network retries, etc. - const receivedStatuses = new Set(); - const expectedStatuses = new Set(['pending', 'ready']); + const receivedStatuses = []; + const expectedStatuses = ['pending', 'ready']; const targetUrl = window.location.origin + "/infrastructure/testdriver/bidi/speculation/resources/target.html"; // Create a promise that resolves when we receive the 'ready' event const readyEventPromise = new Promise((resolve, reject) => { const removeHandler = test_driver.bidi.speculation.prefetch_status_updated.on((event) => { - receivedStatuses.add(event.status); + // Multiple events for the same status can be sent in cases of network retries, etc. + if (!receivedStatuses.includes(event.status)) { + receivedStatuses.push(event.status); + } // When we receive the ready event, clean up and resolve if (event.status === 'ready') { @@ -53,11 +55,7 @@ await readyEventPromise; // Assert that we received the expected events - assert_true( - receivedStatuses.size === expectedStatuses.size && - [...expectedStatuses].every(status => receivedStatuses.has(status)), - 'Should have received pending and ready events' - ); + assert_array_equals(receivedStatuses, expectedStatuses, 'Should have received pending and ready events'); t.add_cleanup(async () => { await unsubscribe(); @@ -70,9 +68,8 @@ // Subscribe for this test only const unsubscribe = await test_driver.bidi.speculation.prefetch_status_updated.subscribe(); - // Multiple events for the same status can be sent for network retries, etc. - const receivedStatuses = new Set(); - const expectedStatuses = new Set(['pending', 'ready', 'success']); + const receivedStatuses = []; + const expectedStatuses = ['pending', 'ready', 'success']; let newWindow = null; // Create prefetch rules for our target page in resources (different URL to avoid caching) @@ -81,7 +78,10 @@ // Create a promise that resolves when we receive the 'success' event const successEventPromise = new Promise((resolve, reject) => { const removeHandler = test_driver.bidi.speculation.prefetch_status_updated.on((event) => { - receivedStatuses.add(event.status); + // Multiple events for the same status can be sent for network retries, etc. + if (!receivedStatuses.includes(event.status)) { + receivedStatuses.push(event.status); + } // When we receive the ready event, navigate to trigger success if (event.status === 'ready') { @@ -109,11 +109,7 @@ await successEventPromise; // Assert that we received the expected events - assert_true( - receivedStatuses.size === expectedStatuses.size && - [...expectedStatuses].every(status => receivedStatuses.has(status)), - 'Should have received pending, ready, and success events' - ); + assert_array_equals(receivedStatuses, expectedStatuses, 'Should have received pending, ready, and success events'); t.add_cleanup(async () => { await unsubscribe(); @@ -129,9 +125,8 @@ // Subscribe for this test only const unsubscribe = await test_driver.bidi.speculation.prefetch_status_updated.subscribe(); - // Multiple events for the same status can be sent for network retries, etc. - const receivedStatuses = new Set(); - const expectedStatuses = new Set(['failure']); + const receivedStatuses = []; + const expectedStatuses = ['failure']; // Set error url to fail at network layer and only expect failure event const errorUrl = "http://0.0.0.0:1/test.html"; @@ -140,7 +135,10 @@ const failureEventPromise = new Promise((resolve, reject) => { const removeHandler = test_driver.bidi.speculation.prefetch_status_updated.on((event) => { console.log('Failure test - Received event with status:', event.status, 'for URL:', event.url); - receivedStatuses.add(event.status); + // Multiple events for the same status can be sent for network retries, etc. + if (!receivedStatuses.includes(event.status)) { + receivedStatuses.push(event.status); + } // When we receive the failure event, we're done if (event.status === 'failure') { @@ -164,11 +162,7 @@ await failureEventPromise; // Assert that we received the expected events - assert_true( - receivedStatuses.size === expectedStatuses.size && - [...expectedStatuses].every(status => receivedStatuses.has(status)), - 'Should have received only failure event.' - ); + assert_array_equals(receivedStatuses, expectedStatuses, 'Should have received only failure event'); t.add_cleanup(async () => { await unsubscribe(); From 4a4adfb46fe19930c52b9e858b6f21259245b968 Mon Sep 17 00:00:00 2001 From: Steven Wei Date: Tue, 4 Nov 2025 11:58:34 -0800 Subject: [PATCH 17/17] fix lint errors --- .../prefetch_status_updated.https.html | 303 +++++----- .../resources/bidi-speculation-helper.js | 82 ++- .../bidi/speculation/resources/target.html | 18 +- .../bidi/external/speculation/conftest.py | 67 +-- .../prefetch_status_updated.py | 551 ++++++++---------- 5 files changed, 458 insertions(+), 563 deletions(-) diff --git a/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html b/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html index d9113e3d6c1151..242b60dc7ee336 100644 --- a/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html +++ b/infrastructure/testdriver/bidi/speculation/prefetch_status_updated.https.html @@ -1,173 +1,132 @@ - - - - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/infrastructure/testdriver/bidi/speculation/resources/bidi-speculation-helper.js b/infrastructure/testdriver/bidi/speculation/resources/bidi-speculation-helper.js index 0f5fa3ae2e3a34..a73a2b8c48235b 100644 --- a/infrastructure/testdriver/bidi/speculation/resources/bidi-speculation-helper.js +++ b/infrastructure/testdriver/bidi/speculation/resources/bidi-speculation-helper.js @@ -1,45 +1,39 @@ -'use strict'; - -/** - * Helper functions for speculation rules BiDi testdriver tests - */ - -/** - * Waits until the document has finished loading. - * @returns {Promise} Resolves if the document is already completely - * loaded or when the 'onload' event is fired. - */ -function waitForDocumentReady() { - return new Promise(resolve => { - if (document.readyState === 'complete') { - resolve(); - } - - window.addEventListener('load', () => { - resolve(); - }, {once: true}); - }); -} - -/** - * Adds speculation rules and a corresponding link to the page. - * @param {Object} speculationRules - The speculation rules object to add - * @param {string} targetUrl - The URL to add as a link - * @param {string} linkText - The text content of the link (optional) - * @returns {Object} Object containing the created script and link elements - */ -function addSpeculationRulesAndLink(speculationRules, targetUrl, linkText = 'Test Link') { - // Add speculation rules script exactly like the working test - const script = document.createElement('script'); - script.type = 'speculationrules'; - script.textContent = JSON.stringify(speculationRules); - document.head.appendChild(script); - - // Also add a link to the page (some implementations might need this) - const link = document.createElement('a'); - link.href = targetUrl; - link.textContent = linkText; - document.body.appendChild(link); - - return { script, link }; +'use strict'; +/** + * Helper functions for speculation rules BiDi testdriver tests + */ +/** + * Waits until the document has finished loading. + * @returns {Promise} Resolves if the document is already completely + * loaded or when the 'onload' event is fired. + */ +function waitForDocumentReady() { + return new Promise(resolve => { + if (document.readyState === 'complete') { + resolve(); + } + window.addEventListener('load', () => { + resolve(); + }, {once: true}); + }); +} +/** + * Adds speculation rules and a corresponding link to the page. + * @param {Object} speculationRules - The speculation rules object to add + * @param {string} targetUrl - The URL to add as a link + * @param {string} linkText - The text content of the link (optional) + * @returns {Object} Object containing the created script and link elements + */ +function addSpeculationRulesAndLink(speculationRules, targetUrl, linkText = 'Test Link') { + // Add speculation rules script exactly like the working test + const script = document.createElement('script'); + script.type = 'speculationrules'; + script.textContent = JSON.stringify(speculationRules); + document.head.appendChild(script); + // Also add a link to the page (some implementations might need this) + const link = document.createElement('a'); + link.href = targetUrl; + link.textContent = linkText; + document.body.appendChild(link); + return { script, link }; } \ No newline at end of file diff --git a/infrastructure/testdriver/bidi/speculation/resources/target.html b/infrastructure/testdriver/bidi/speculation/resources/target.html index a64b648bca709b..0b3032ecb9b7c6 100644 --- a/infrastructure/testdriver/bidi/speculation/resources/target.html +++ b/infrastructure/testdriver/bidi/speculation/resources/target.html @@ -1,10 +1,10 @@ - - - - Prefetch Target Page - - -

This is a prefetch target page

-

This page is used as a target for prefetch testing.

- + + + + Prefetch Target Page + + +

This is a prefetch target page

+

This page is used as a target for prefetch testing.

+ \ No newline at end of file diff --git a/webdriver/tests/bidi/external/speculation/conftest.py b/webdriver/tests/bidi/external/speculation/conftest.py index 8adca501ce3ee0..82245e902ba0ef 100644 --- a/webdriver/tests/bidi/external/speculation/conftest.py +++ b/webdriver/tests/bidi/external/speculation/conftest.py @@ -1,37 +1,32 @@ -import pytest -from typing import Any, Mapping - -from webdriver.bidi.modules.script import ContextTarget - -@pytest.fixture -def add_speculation_rules_and_link(bidi_session): - """Helper for adding both speculation rules and a prefetch link to a page.""" - - async def add_rules_and_link(context: Mapping[str, Any], rules: str, href: str, text: str = "Test Link", link_id: str = "prefetch-page"): - """Add speculation rules and a corresponding link to the page.""" - # Add speculation rules first - await bidi_session.script.evaluate( - expression=f""" - const script = document.createElement('script'); - script.type = 'speculationrules'; - script.textContent = `{rules}`; - document.head.appendChild(script); - """, - target=ContextTarget(context["context"]), - await_promise=False - ) - - # Then add the link - await bidi_session.script.evaluate( - expression=f""" - const link = document.createElement('a'); - link.href = '{href}'; - link.textContent = '{text}'; - link.id = '{link_id}'; - document.body.appendChild(link); - """, - target={"context": context["context"]}, - await_promise=False - ) - +import pytest +from typing import Any, Mapping +from webdriver.bidi.modules.script import ContextTarget +@pytest.fixture +def add_speculation_rules_and_link(bidi_session): + """Helper for adding both speculation rules and a prefetch link to a page.""" + async def add_rules_and_link(context: Mapping[str, Any], rules: str, href: str, text: str = "Test Link", link_id: str = "prefetch-page"): + """Add speculation rules and a corresponding link to the page.""" + # Add speculation rules first + await bidi_session.script.evaluate( + expression=f""" + const script = document.createElement('script'); + script.type = 'speculationrules'; + script.textContent = `{rules}`; + document.head.appendChild(script); + """, + target=ContextTarget(context["context"]), + await_promise=False + ) + # Then add the link + await bidi_session.script.evaluate( + expression=f""" + const link = document.createElement('a'); + link.href = '{href}'; + link.textContent = '{text}'; + link.id = '{link_id}'; + document.body.appendChild(link); + """, + target={"context": context["context"]}, + await_promise=False + ) return add_rules_and_link \ No newline at end of file diff --git a/webdriver/tests/bidi/external/speculation/prefetch_status_updated/prefetch_status_updated.py b/webdriver/tests/bidi/external/speculation/prefetch_status_updated/prefetch_status_updated.py index 71a02500cf8bef..89892d8043bf75 100644 --- a/webdriver/tests/bidi/external/speculation/prefetch_status_updated/prefetch_status_updated.py +++ b/webdriver/tests/bidi/external/speculation/prefetch_status_updated/prefetch_status_updated.py @@ -1,303 +1,250 @@ -import pytest -from webdriver.error import TimeoutException - -pytestmark = pytest.mark.asyncio - -@pytest.mark.asyncio -async def test_speculation_rules_generate_ready_events( - bidi_session, subscribe_events, new_tab, url, wait_for_events, add_speculation_rules_and_link -): - '''Test that speculation rules generate prefetch events with proper pending->ready sequence.''' - - await subscribe_events(events=["speculation.prefetchStatusUpdated"]) - - test_url = url("/common/blank.html", protocol="https") - await bidi_session.browsing_context.navigate( - context=new_tab["context"], - url=test_url, - wait="none", - ) - - # Add speculation rules to trigger immediate prefetching - prefetch_target = url("/common/dummy.xml", protocol="https") - speculation_rules = f'''{{ - "prefetch": [{{ - "where": {{ "href_matches": "{prefetch_target}" }}, - "eagerness": "immediate" - }}] - }}''' - - # Set up event waiter before triggering prefetch - with wait_for_events(["speculation.prefetchStatusUpdated"]) as waiter: - # Add speculation rules and link to trigger prefetching - await add_speculation_rules_and_link(new_tab, speculation_rules, prefetch_target) - - # Wait for pending and ready events - events = await waiter.get_events(lambda events: len(events) == 2) - - # Verify all events have correct structure and sequence - assert events == [ - ("speculation.prefetchStatusUpdated", { - "url": prefetch_target, - "status": "pending", - "context": new_tab["context"] - }), - ("speculation.prefetchStatusUpdated", { - "url": prefetch_target, - "status": "ready", - "context": new_tab["context"] - }) - ], f"Events don't match expected sequence: {events}" - - -@pytest.mark.asyncio -async def test_speculation_rules_generate_events_with_navigation( - bidi_session, subscribe_events, new_tab, url, wait_for_events, add_speculation_rules_and_link -): - '''Test that speculation rules generate prefetch events with navigation and success event after using prefetched page.''' - - await subscribe_events(events=["speculation.prefetchStatusUpdated"]) - - test_url = url("/common/blank.html", protocol="https") - await bidi_session.browsing_context.navigate( - context=new_tab["context"], - url=test_url, - wait="none", - ) - - # Add speculation rules to trigger immediate prefetching - prefetch_target = url("/common/dummy.xml", protocol="https") - speculation_rules = f'''{{ - "prefetch": [{{ - "where": {{ "href_matches": "{prefetch_target}" }}, - "eagerness": "immediate" - }}] - }}''' - - # Set up event waiter before triggering prefetch - capture all events through navigation - with wait_for_events(["speculation.prefetchStatusUpdated"]) as waiter: - # Add speculation rules and link to trigger prefetching - await add_speculation_rules_and_link(new_tab, speculation_rules, prefetch_target) - - # Wait for pending and ready events first - events = await waiter.get_events(lambda events: len(events) >= 2) - - # Verify we got pending and ready events - assert events == [ - ("speculation.prefetchStatusUpdated", { - "url": prefetch_target, - "status": "pending", - "context": new_tab["context"] - }), - ("speculation.prefetchStatusUpdated", { - "url": prefetch_target, - "status": "ready", - "context": new_tab["context"] - }) - ], f"Events don't match expected sequence: {events}" - - with wait_for_events(["speculation.prefetchStatusUpdated"]) as waiter: - # Now navigate to the prefetched page to potentially trigger success event - # Navigate by clicking the link (user-initiated navigation to trigger success event) - await bidi_session.script.evaluate( - expression=''' - const prefetchLink = document.getElementById('prefetch-page'); - if (prefetchLink) { - prefetchLink.click(); - } - ''', - target={"context": new_tab["context"]}, - await_promise=False - ) - - # Wait for success event after navigation to the prefetched page - success_event = await waiter.get_events(lambda events: len(events) >= 1) - - # Verify success event has correct structure and sequence - assert success_event == [ - ("speculation.prefetchStatusUpdated", { - "url": prefetch_target, - "status": "success", - "context": new_tab["context"] - }) - ], f"Success event doesn't match expected sequence: {success_event}" - - -@pytest.mark.asyncio -async def test_speculation_rules_generate_failure_events( - bidi_session, subscribe_events, new_tab, url, wait_for_events, add_speculation_rules_and_link -): - '''Test that speculation rules generate pending and failure events for failed prefetch.''' - - await subscribe_events(events=["speculation.prefetchStatusUpdated"]) - - test_url = url("/common/blank.html", protocol="https") - await bidi_session.browsing_context.navigate( - context=new_tab["context"], - url=test_url, - wait="none", - ) - - # Create a target that will return 404 - use a non-existent path - failed_target = url("/nonexistent/path/that/will/404.xml", protocol="https") - - # Add speculation rules to trigger immediate prefetching of 404 page - speculation_rules = f'''{{ - "prefetch": [{{ - "where": {{ "href_matches": "{failed_target}" }}, - "eagerness": "immediate" - }}] - }}''' - - # Set up event waiter before triggering prefetch - with wait_for_events(["speculation.prefetchStatusUpdated"]) as waiter: - # Add speculation rules and link to trigger prefetching - await add_speculation_rules_and_link(new_tab, speculation_rules, failed_target) - - # Wait for events (pending and failure) - events = await waiter.get_events(lambda events: len(events) >= 2) - - # Verify all events have correct structure and sequence - assert events == [ - ("speculation.prefetchStatusUpdated", { - "url": failed_target, - "status": "pending", - "context": new_tab["context"] - }), - ("speculation.prefetchStatusUpdated", { - "url": failed_target, - "status": "failure", - "context": new_tab["context"] - }) - ], f"Events don't match expected sequence: {events}" - -@pytest.mark.asyncio -async def test_subscribe_unsubscribe_event_emission( - bidi_session, subscribe_events, new_tab, url, wait_for_events, add_speculation_rules_and_link -): - '''Test that events are emitted when subscribed and not emitted when unsubscribed.''' - - test_url = url("/common/blank.html", protocol="https") - await bidi_session.browsing_context.navigate( - context=new_tab["context"], - url=test_url, - wait="complete", - ) - - prefetch_target = url("/common/dummy.xml", protocol="https") - speculation_rules = f'''{{ - "prefetch": [{{ - "where": {{ "href_matches": "{prefetch_target}" }}, - "eagerness": "immediate" - }}] - }}''' - - # Phase 1: Subscribe to specific event and assert events are emitted - subscription_result = await subscribe_events(events=["speculation.prefetchStatusUpdated"]) - subscription_id = subscription_result["subscription"] - - # Trigger prefetch and collect events - with wait_for_events(["speculation.prefetchStatusUpdated"]) as waiter: - await add_speculation_rules_and_link(new_tab, speculation_rules, prefetch_target) - - # Wait for events to be emitted - events = await waiter.get_events(lambda events: len(events) >= 2) - - # Phase 2: Unsubscribe and assert events are NOT emitted - await bidi_session.session.unsubscribe(subscriptions=[subscription_id]) - - # Reload the page to get a clean state - await bidi_session.browsing_context.navigate( - context=new_tab["context"], - url=test_url, - wait="complete", - ) - - # Trigger another prefetch after unsubscribing - prefetch_target_2 = url("/common/square.png", protocol="https") - speculation_rules_2 = f'''{{ - "prefetch": [{{ - "where": {{ "href_matches": "{prefetch_target_2}" }}, - "eagerness": "immediate" - }}] - }}''' - - # Set up waiter but don't expect any events this time - with wait_for_events(["speculation.prefetchStatusUpdated"]) as waiter: - await add_speculation_rules_and_link(new_tab, speculation_rules_2, prefetch_target_2) - - with pytest.raises(TimeoutException): - await waiter.get_events(lambda events: len(events) >= 1, timeout=0.5) - -@pytest.mark.asyncio -async def test_subscribe_unsubscribe_module_subscription( - bidi_session, subscribe_events, new_tab, url, wait_for_events, add_speculation_rules_and_link -): - '''Test that module subscription ('speculation') works for subscribe/unsubscribe.''' - - test_url = url("/common/blank.html", protocol="https") - await bidi_session.browsing_context.navigate( - context=new_tab["context"], - url=test_url, - wait="complete", - ) - - prefetch_target = url("/common/dummy.xml", protocol="https") - speculation_rules = f'''{{ - "prefetch": [{{ - "where": {{ "href_matches": "{prefetch_target}" }}, - "eagerness": "immediate" - }}] - }}''' - - # Phase 1: Subscribe to module events and assert events are emitted - subscription_result = await subscribe_events(events=["speculation"]) # Module subscription - subscription_id = subscription_result["subscription"] - - # Trigger prefetch and collect events - with wait_for_events(["speculation.prefetchStatusUpdated"]) as waiter: - await add_speculation_rules_and_link(new_tab, speculation_rules, prefetch_target) - - # Wait for events to be emitted - events = await waiter.get_events(lambda events: len(events) >= 2) - - # Phase 2: Unsubscribe from module and assert events are NOT emitted - await bidi_session.session.unsubscribe(subscriptions=[subscription_id]) - - # Reload the page to get a clean state - await bidi_session.browsing_context.navigate( - context=new_tab["context"], - url=test_url, - wait="complete", - ) - - # Trigger another prefetch after unsubscribing from module - prefetch_target_2 = url("/common/square.png", protocol="https") - speculation_rules_2 = f'''{{ - "prefetch": [{{ - "where": {{ "href_matches": "{prefetch_target_2}" }}, - "eagerness": "immediate" - }}] - }}''' - - # Set up waiter but don't expect any events this time - with wait_for_events(["speculation.prefetchStatusUpdated"]) as waiter: - await add_speculation_rules_and_link(new_tab, speculation_rules_2, prefetch_target_2) - - with pytest.raises(TimeoutException): - await waiter.get_events(lambda events: len(events) >= 1, timeout=0.5) - -@pytest.mark.asyncio -async def test_unsubscribe_from_prefetch_status_updated( - bidi_session -): - '''Test unsubscribing from prefetch status updated events.''' - - # Subscribe to prefetch status updated events - subscription_result = await bidi_session.session.subscribe( - events=["speculation.prefetchStatusUpdated"] - ) - subscription_id = subscription_result["subscription"] - - # Unsubscribe immediately +import pytest +from webdriver.error import TimeoutException +pytestmark = pytest.mark.asyncio +@pytest.mark.asyncio +async def test_speculation_rules_generate_ready_events( + bidi_session, subscribe_events, new_tab, url, wait_for_events, add_speculation_rules_and_link +): + '''Test that speculation rules generate prefetch events with proper pending->ready sequence.''' + await subscribe_events(events=["speculation.prefetchStatusUpdated"]) + test_url = url("/common/blank.html", protocol="https") + await bidi_session.browsing_context.navigate( + context=new_tab["context"], + url=test_url, + wait="none", + ) + # Add speculation rules to trigger immediate prefetching + prefetch_target = url("/common/dummy.xml", protocol="https") + speculation_rules = f'''{{ + "prefetch": [{{ + "where": {{ "href_matches": "{prefetch_target}" }}, + "eagerness": "immediate" + }}] + }}''' + # Set up event waiter before triggering prefetch + with wait_for_events(["speculation.prefetchStatusUpdated"]) as waiter: + # Add speculation rules and link to trigger prefetching + await add_speculation_rules_and_link(new_tab, speculation_rules, prefetch_target) + # Wait for pending and ready events + events = await waiter.get_events(lambda events: len(events) == 2) + # Verify all events have correct structure and sequence + assert events == [ + ("speculation.prefetchStatusUpdated", { + "url": prefetch_target, + "status": "pending", + "context": new_tab["context"] + }), + ("speculation.prefetchStatusUpdated", { + "url": prefetch_target, + "status": "ready", + "context": new_tab["context"] + }) + ], f"Events don't match expected sequence: {events}" +@pytest.mark.asyncio +async def test_speculation_rules_generate_events_with_navigation( + bidi_session, subscribe_events, new_tab, url, wait_for_events, add_speculation_rules_and_link +): + '''Test that speculation rules generate prefetch events with navigation and success event after using prefetched page.''' + await subscribe_events(events=["speculation.prefetchStatusUpdated"]) + test_url = url("/common/blank.html", protocol="https") + await bidi_session.browsing_context.navigate( + context=new_tab["context"], + url=test_url, + wait="none", + ) + # Add speculation rules to trigger immediate prefetching + prefetch_target = url("/common/dummy.xml", protocol="https") + speculation_rules = f'''{{ + "prefetch": [{{ + "where": {{ "href_matches": "{prefetch_target}" }}, + "eagerness": "immediate" + }}] + }}''' + # Set up event waiter before triggering prefetch - capture all events through navigation + with wait_for_events(["speculation.prefetchStatusUpdated"]) as waiter: + # Add speculation rules and link to trigger prefetching + await add_speculation_rules_and_link(new_tab, speculation_rules, prefetch_target) + # Wait for pending and ready events first + events = await waiter.get_events(lambda events: len(events) >= 2) + # Verify we got pending and ready events + assert events == [ + ("speculation.prefetchStatusUpdated", { + "url": prefetch_target, + "status": "pending", + "context": new_tab["context"] + }), + ("speculation.prefetchStatusUpdated", { + "url": prefetch_target, + "status": "ready", + "context": new_tab["context"] + }) + ], f"Events don't match expected sequence: {events}" + with wait_for_events(["speculation.prefetchStatusUpdated"]) as waiter: + # Now navigate to the prefetched page to potentially trigger success event + # Navigate by clicking the link (user-initiated navigation to trigger success event) + await bidi_session.script.evaluate( + expression=''' + const prefetchLink = document.getElementById('prefetch-page'); + if (prefetchLink) { + prefetchLink.click(); + } + ''', + target={"context": new_tab["context"]}, + await_promise=False + ) + # Wait for success event after navigation to the prefetched page + success_event = await waiter.get_events(lambda events: len(events) >= 1) + # Verify success event has correct structure and sequence + assert success_event == [ + ("speculation.prefetchStatusUpdated", { + "url": prefetch_target, + "status": "success", + "context": new_tab["context"] + }) + ], f"Success event doesn't match expected sequence: {success_event}" +@pytest.mark.asyncio +async def test_speculation_rules_generate_failure_events( + bidi_session, subscribe_events, new_tab, url, wait_for_events, add_speculation_rules_and_link +): + '''Test that speculation rules generate pending and failure events for failed prefetch.''' + await subscribe_events(events=["speculation.prefetchStatusUpdated"]) + test_url = url("/common/blank.html", protocol="https") + await bidi_session.browsing_context.navigate( + context=new_tab["context"], + url=test_url, + wait="none", + ) + # Create a target that will return 404 - use a non-existent path + failed_target = url("/nonexistent/path/that/will/404.xml", protocol="https") + # Add speculation rules to trigger immediate prefetching of 404 page + speculation_rules = f'''{{ + "prefetch": [{{ + "where": {{ "href_matches": "{failed_target}" }}, + "eagerness": "immediate" + }}] + }}''' + # Set up event waiter before triggering prefetch + with wait_for_events(["speculation.prefetchStatusUpdated"]) as waiter: + # Add speculation rules and link to trigger prefetching + await add_speculation_rules_and_link(new_tab, speculation_rules, failed_target) + # Wait for events (pending and failure) + events = await waiter.get_events(lambda events: len(events) >= 2) + # Verify all events have correct structure and sequence + assert events == [ + ("speculation.prefetchStatusUpdated", { + "url": failed_target, + "status": "pending", + "context": new_tab["context"] + }), + ("speculation.prefetchStatusUpdated", { + "url": failed_target, + "status": "failure", + "context": new_tab["context"] + }) + ], f"Events don't match expected sequence: {events}" +@pytest.mark.asyncio +async def test_subscribe_unsubscribe_event_emission( + bidi_session, subscribe_events, new_tab, url, wait_for_events, add_speculation_rules_and_link +): + '''Test that events are emitted when subscribed and not emitted when unsubscribed.''' + test_url = url("/common/blank.html", protocol="https") + await bidi_session.browsing_context.navigate( + context=new_tab["context"], + url=test_url, + wait="complete", + ) + prefetch_target = url("/common/dummy.xml", protocol="https") + speculation_rules = f'''{{ + "prefetch": [{{ + "where": {{ "href_matches": "{prefetch_target}" }}, + "eagerness": "immediate" + }}] + }}''' + # Phase 1: Subscribe to specific event and assert events are emitted + subscription_result = await subscribe_events(events=["speculation.prefetchStatusUpdated"]) + subscription_id = subscription_result["subscription"] + # Trigger prefetch and collect events + with wait_for_events(["speculation.prefetchStatusUpdated"]) as waiter: + await add_speculation_rules_and_link(new_tab, speculation_rules, prefetch_target) + # Wait for events to be emitted + events = await waiter.get_events(lambda events: len(events) >= 2) + # Phase 2: Unsubscribe and assert events are NOT emitted + await bidi_session.session.unsubscribe(subscriptions=[subscription_id]) + # Reload the page to get a clean state + await bidi_session.browsing_context.navigate( + context=new_tab["context"], + url=test_url, + wait="complete", + ) + # Trigger another prefetch after unsubscribing + prefetch_target_2 = url("/common/square.png", protocol="https") + speculation_rules_2 = f'''{{ + "prefetch": [{{ + "where": {{ "href_matches": "{prefetch_target_2}" }}, + "eagerness": "immediate" + }}] + }}''' + # Set up waiter but don't expect any events this time + with wait_for_events(["speculation.prefetchStatusUpdated"]) as waiter: + await add_speculation_rules_and_link(new_tab, speculation_rules_2, prefetch_target_2) + with pytest.raises(TimeoutException): + await waiter.get_events(lambda events: len(events) >= 1, timeout=0.5) +@pytest.mark.asyncio +async def test_subscribe_unsubscribe_module_subscription( + bidi_session, subscribe_events, new_tab, url, wait_for_events, add_speculation_rules_and_link +): + '''Test that module subscription ('speculation') works for subscribe/unsubscribe.''' + test_url = url("/common/blank.html", protocol="https") + await bidi_session.browsing_context.navigate( + context=new_tab["context"], + url=test_url, + wait="complete", + ) + prefetch_target = url("/common/dummy.xml", protocol="https") + speculation_rules = f'''{{ + "prefetch": [{{ + "where": {{ "href_matches": "{prefetch_target}" }}, + "eagerness": "immediate" + }}] + }}''' + # Phase 1: Subscribe to module events and assert events are emitted + subscription_result = await subscribe_events(events=["speculation"]) # Module subscription + subscription_id = subscription_result["subscription"] + # Trigger prefetch and collect events + with wait_for_events(["speculation.prefetchStatusUpdated"]) as waiter: + await add_speculation_rules_and_link(new_tab, speculation_rules, prefetch_target) + # Wait for events to be emitted + events = await waiter.get_events(lambda events: len(events) >= 2) + # Phase 2: Unsubscribe from module and assert events are NOT emitted + await bidi_session.session.unsubscribe(subscriptions=[subscription_id]) + # Reload the page to get a clean state + await bidi_session.browsing_context.navigate( + context=new_tab["context"], + url=test_url, + wait="complete", + ) + # Trigger another prefetch after unsubscribing from module + prefetch_target_2 = url("/common/square.png", protocol="https") + speculation_rules_2 = f'''{{ + "prefetch": [{{ + "where": {{ "href_matches": "{prefetch_target_2}" }}, + "eagerness": "immediate" + }}] + }}''' + # Set up waiter but don't expect any events this time + with wait_for_events(["speculation.prefetchStatusUpdated"]) as waiter: + await add_speculation_rules_and_link(new_tab, speculation_rules_2, prefetch_target_2) + with pytest.raises(TimeoutException): + await waiter.get_events(lambda events: len(events) >= 1, timeout=0.5) +@pytest.mark.asyncio +async def test_unsubscribe_from_prefetch_status_updated( + bidi_session +): + '''Test unsubscribing from prefetch status updated events.''' + # Subscribe to prefetch status updated events + subscription_result = await bidi_session.session.subscribe( + events=["speculation.prefetchStatusUpdated"] + ) + subscription_id = subscription_result["subscription"] + # Unsubscribe immediately await bidi_session.session.unsubscribe(subscriptions=[subscription_id]) \ No newline at end of file