Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c367423
add testdriver extension for speculation module and event, and add wp…
srwei Oct 7, 2025
27aaa43
Merge branch 'master' into SpeculationRulesPrefetchWebDriverBidi
srwei Oct 7, 2025
386f50d
remove console logs
srwei Oct 7, 2025
41fd8ad
Merge branch 'SpeculationRulesPrefetchWebDriverBidi' of https://githu…
srwei Oct 7, 2025
ef4e35a
update tests, remove subscribe_global to inline instead
srwei Oct 8, 2025
a9fe604
use pytest timeout exception when not expecting events
srwei Oct 8, 2025
df37262
keep prefetch status subscription to specific context
srwei Oct 10, 2025
0503a27
remove unnecessary tests and methods
srwei Oct 22, 2025
c33e41e
remove unused fixture
srwei Oct 22, 2025
05867cb
subscribe per test with cleanup
srwei Oct 31, 2025
e9e1ab2
expect multiple repeated events, change to sets
srwei Oct 31, 2025
2b1741a
add 30 sec timeout to failure test
srwei Oct 31, 2025
7b480fe
fail prefetch at network level
srwei Oct 31, 2025
d493553
add debugging to failure test
srwei Oct 31, 2025
a103beb
typo
srwei Nov 1, 2025
05d3b75
subscribe and unsubscribe per each test to avoid contamination in CI
srwei Nov 1, 2025
1450301
remove unncessary logging
srwei Nov 1, 2025
15f8a4b
reverted back to array from set() due to serialization issue in macOS…
srwei Nov 1, 2025
4a4adfb
fix lint errors
srwei Nov 4, 2025
23c73db
Merge branch 'web-platform-tests:master' into SpeculationRulesPrefetc…
srwei Nov 18, 2025
cc05ab1
added ini for non chrome product test
srwei Nov 21, 2025
8759a0c
Merge branch 'SpeculationRulesPrefetchWebDriverBidi' of https://githu…
srwei Nov 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[prefetch_status_updated.https.html]
expected:
if product != "chrome": ERROR
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you! Looks like all checks are passing now

Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<!DOCTYPE html>
<meta charset="utf-8"/>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/dispatcher/dispatcher.js"></script>
<script src="/common/utils.js"></script>
<script src="/resources/testdriver.js?feature=bidi"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="resources/bidi-speculation-helper.js"></script>
<script>
promise_setup(async () => {
await waitForDocumentReady();
});
promise_test(async t => {
// Subscribe for this test only
const unsubscribe = await test_driver.bidi.speculation.prefetch_status_updated.subscribe();
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) => {
// 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') {
removeHandler();
resolve();
}
});
});
// Create prefetch rules for our target page in resources
const speculationRules = {
prefetch: [{
source: "list",
urls: [targetUrl]
}]
};
// Use helper function to add both speculation rules and link
const { script, link } = addSpeculationRulesAndLink(speculationRules, targetUrl);
// Await the ready event
await readyEventPromise;
// Assert that we received the expected events
assert_array_equals(receivedStatuses, expectedStatuses, 'Should have received pending and ready events');
t.add_cleanup(async () => {
await unsubscribe();
});
}, "prefetch_status_updated event subscription and structure validation");
promise_test(async t => {
// Subscribe for this test only
const unsubscribe = await test_driver.bidi.speculation.prefetch_status_updated.subscribe();
const receivedStatuses = [];
const expectedStatuses = ['pending', 'ready', 'success'];
let newWindow = null;
// Create prefetch rules for our target page in resources (different URL to avoid caching)
const targetUrl = window.location.origin + "/infrastructure/testdriver/bidi/speculation/resources/target.html?test=2";
// 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) => {
// 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') {
// Open the prefetched page in a new window to trigger success
newWindow = window.open(event.url, '_blank');
} else if (event.status === 'success') {
removeHandler();
resolve();
}
});
});
const speculationRules = {
prefetch: [{
source: "list",
urls: [targetUrl]
}]
};
// Use helper function to add both speculation rules and link
const { script, link } = addSpeculationRulesAndLink(speculationRules, targetUrl);
// Await the success event
await successEventPromise;
// Assert that we received the expected events
assert_array_equals(receivedStatuses, expectedStatuses, 'Should have received pending, ready, and success events');
t.add_cleanup(async () => {
await unsubscribe();
if (newWindow && !newWindow.closed) {
newWindow.close();
}
});
}, "prefetch_status_updated event with navigation to success");
promise_test(async t => {
// Subscribe for this test only
const unsubscribe = await test_driver.bidi.speculation.prefetch_status_updated.subscribe();
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";
// Create a promise that resolves when we receive the 'failure' event
const failureEventPromise = new Promise((resolve, reject) => {
const removeHandler = test_driver.bidi.speculation.prefetch_status_updated.on((event) => {
// 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') {
removeHandler();
resolve();
}
});
});
const speculationRules = {
prefetch: [{
source: "list",
urls: [errorUrl]
}]
};
// Use helper function to add both speculation rules and link
const { script, link } = addSpeculationRulesAndLink(speculationRules, errorUrl);
// Await the failure event
await failureEventPromise;
// Assert that we received the expected events
assert_array_equals(receivedStatuses, expectedStatuses, 'Should have received only failure event');
t.add_cleanup(async () => {
await unsubscribe();
});
}, "prefetch_status_updated event with prefetch failure");
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use strict';
/**
* Helper functions for speculation rules BiDi testdriver tests
*/
/**
* Waits until the document has finished loading.
* @returns {Promise<void>} 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 };
}
10 changes: 10 additions & 0 deletions infrastructure/testdriver/bidi/speculation/resources/target.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>Prefetch Target Page</title>
</head>
<body>
<h1>This is a prefetch target page</h1>
<p>This page is used as a target for prefetch testing.</p>
</body>
</html>
84 changes: 84 additions & 0 deletions resources/testdriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,78 @@
},
}
},
/**
* `speculation <https://wicg.github.io/nav-speculation/prefetch.html>`_ module.
*/
speculation: {
/**
* `speculation.PrefetchStatusUpdated <https://wicg.github.io/nav-speculation/prefetch.html#speculation-prefetchstatusupdated-event>`_
* event.
*/
prefetch_status_updated: {
/**
* @typedef {object} PrefetchStatusUpdated
* `speculation.PrefetchStatusUpdatedParameters <https://wicg.github.io/nav-speculation/prefetch.html#cddl-type-speculationprefetchstatusupdatedparameters>`_
* event.
*/

/**
* Subscribes to the event. Events will be emitted only if
* there is a subscription for the event. This method does
* not add actual listeners. To listen to the event, use the
* `on` or `once` methods. The buffered events will be
* emitted before the command promise is resolved.
*
* @param {object} [params] Parameters for the subscription.
* @param {null|Array.<(Context)>} [params.contexts] The
* optional contexts parameter specifies which browsing
* contexts to subscribe to the event on. It should be
* either an array of Context objects, or null. If null, the
* event will be subscribed to globally. If omitted, the
* event will be subscribed to on the current browsing
* context.
* @returns {Promise<(function(): Promise<void>)>} Callback
* for unsubscribing from the created subscription.
*/
subscribe: async function(params = {}) {
assertBidiIsEnabled();
return window.test_driver_internal.bidi.speculation
.prefetch_status_updated.subscribe(params);
},
/**
* Adds an event listener for the event.
*
* @param {function(PrefetchStatusUpdated): void} callback The
* callback to be called when the event is emitted. The
* callback is called with the event object as a parameter.
* @returns {function(): void} A function that removes the
* added event listener when called.
*/
on: function(callback) {
assertBidiIsEnabled();
return window.test_driver_internal.bidi.speculation
.prefetch_status_updated.on(callback);
},
/**
* Adds an event listener for the event that is only called
* once and removed afterward.
*
* @return {Promise<PrefetchStatusUpdated>} The promise which
* is resolved with the event object when the event is emitted.
*/
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 <https://www.w3.org/TR/webdriver-bidi/#module-emulation>`_ module.
*/
Expand Down Expand Up @@ -2337,6 +2409,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');
}
},
}
},

Expand Down
20 changes: 18 additions & 2 deletions tools/wptrunner/wptrunner/testdriver-extra.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,14 +220,12 @@
const subscription_id = action_result["subscription"];

return async ()=>{
console.log("!!@@## unsubscribing")
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, removing this seems like a good idea, sure looks like frustrated debugging left in :)

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 =
Expand Down Expand Up @@ -673,4 +671,22 @@
window.test_driver_internal.set_global_privacy_control = function(gpc, context=null) {
return create_action("set_global_privacy_control", {gpc});
};


window.test_driver_internal.bidi.speculation.prefetch_status_updated.subscribe =
function(params) {
return subscribe(
{...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);
};
})();
Empty file.
32 changes: 32 additions & 0 deletions webdriver/tests/bidi/external/speculation/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +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
)
return add_rules_and_link
Loading