summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/speculation-rules/prefetch
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/speculation-rules/prefetch')
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/anonymous-client.https.html20
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/cross-origin-cookies.https.html41
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/different-initiators-2.https.html52
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/different-initiators.sub.https.html81
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/document-rules.https.html317
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/duplicate-urls.https.html21
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/initiators-a-element.sub.https.html79
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/initiators-iframe-location-href.sub.https.html48
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/initiators-window-open.sub.https.html68
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/invalid-rules.https.html18
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/multiple-url.https.html21
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-delivery-type.tentative.https.html59
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-requestStart-responseStart.https.html52
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-sizes.https.html60
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/README.txt1
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/prefetch-single-with-hint.https.html337
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/prefetch-single.https.html314
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/out-of-document-rule-set.https.html152
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/prefetch-single.https.html28
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/prefetch-status.https.html30
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/prefetch-traverse-reload.sub.html74
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/redirect-url.https.html18
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-from-rules.https.html143
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-not-accepted.https.html53
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/referrer-policy.https.html88
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/authenticate.py32
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/cookies.py41
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/executor.sub.html13
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch.py16
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch_nvs_hint.py34
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/redirect.py3
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/ruleset.py49
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/sw.js1
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/utils.sub.js176
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/same-origin-cookies.https.html39
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/user-pass.https.html44
36 files changed, 2623 insertions, 0 deletions
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/anonymous-client.https.html b/testing/web-platform/tests/speculation-rules/prefetch/anonymous-client.https.html
new file mode 100644
index 0000000000..dfa48f02ab
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/anonymous-client.https.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<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/utils.sub.js"></script>
+<script>
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let agent = await spawnWindow(t);
+ let nextUrl = agent.getExecutorURL({ hostname: PREFETCH_PROXY_BYPASS_HOST, page: 2 });
+ await agent.forceSinglePrefetch(nextUrl, { requires: ["anonymous-client-ip-when-cross-origin"] });
+ await agent.navigate(nextUrl);
+
+ let requestHeaders = await agent.getRequestHeaders();
+ assert_in_array(requestHeaders.purpose, ["", "prefetch"], "The vendor-specific header Purpose, if present, must be 'prefetch'.");
+ assert_equals(requestHeaders.sec_purpose, "prefetch;anonymous-client-ip");
+ }, "test anonymous-client url prefetch for cross origin pages");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/cross-origin-cookies.https.html b/testing/web-platform/tests/speculation-rules/prefetch/cross-origin-cookies.https.html
new file mode 100644
index 0000000000..c3911919f0
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/cross-origin-cookies.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src='/resources/testdriver-vendor.js'></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script>
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ await test_driver.delete_all_cookies();
+
+ let executor = 'cookies.py';
+ let agent = await spawnWindow(t, { executor });
+ let response_cookies = await agent.getResponseCookies();
+ let request_cookies = await agent.getRequestCookies();
+ assert_equals(request_cookies["count"], undefined);
+ assert_equals(request_cookies["type"], undefined);
+ assert_equals(response_cookies["count"], "1");
+ assert_equals(response_cookies["type"], "navigate");
+
+ let nextUrl = agent.getExecutorURL({ executor, hostname: PREFETCH_PROXY_BYPASS_HOST, page: 2 });
+ await agent.forceSinglePrefetch(nextUrl, { requires: ["anonymous-client-ip-when-cross-origin"] });
+ await agent.forceSinglePrefetch(nextUrl);
+ await agent.navigate(nextUrl);
+
+ response_cookies = await agent.getResponseCookies();
+ request_cookies = await agent.getRequestCookies();
+ assert_equals(request_cookies["count"], undefined);
+ assert_equals(request_cookies["type"], undefined);
+ assert_equals(response_cookies["count"], "1");
+ assert_equals(response_cookies["type"], "prefetch");
+
+ let requestHeaders = await agent.getRequestHeaders();
+ assert_equals(requestHeaders.sec_purpose, "prefetch;anonymous-client-ip");
+
+ }, "speculation rules based prefetch should not use cookies for cross origin urls.");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/different-initiators-2.https.html b/testing/web-platform/tests/speculation-rules/prefetch/different-initiators-2.https.html
new file mode 100644
index 0000000000..1242ebbfb4
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/different-initiators-2.https.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+// Regression test for https://crbug.com/1431804.
+promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ "Speculation Rules not supported");
+
+ const win = await spawnWindow(t, { protocol: 'https' });
+ const nextUrl = win.getExecutorURL({ protocol: 'https', page: 2 });
+
+ // Navigate `win` from Document #1 -> #2 (nextUrl) -> #3 (tempUrl) ->
+ // #4 (nextUrl),
+ // Start speculation rules prefetch from #1, and
+ // Try using the prefetched result for the navigation #3 -> #4.
+ // The Documents #2 and #4 are different, but the same RenderFrameHost is
+ // used before https://crbug.com/936696 is done.
+
+ await win.forceSinglePrefetch(nextUrl);
+
+ // Register a SW for `nextUrl` -- this is a trick to make the prefetched
+ // result to put in `PrefetchService::prefetches_ready_to_serve_` in
+ // Chromium implementation but actually not used by this navigation.
+ const r = await service_worker_unregister_and_register(
+ t, 'resources/sw.js', nextUrl);
+ await wait_for_state(t, r.installing, 'activated');
+
+ // Navigate #1 -> #2.
+ // This doesn't use the prefetched result due to the ServiceWorker.
+ await win.navigate(nextUrl);
+
+ // Unregister the SW.
+ await service_worker_unregister(t, nextUrl);
+
+ // Navigate #2 -> #3 -> #4.
+ const tempUrl = win.getExecutorURL({ protocol: 'https', page: 3 });
+ await win.navigate(tempUrl);
+ await win.navigate(nextUrl);
+
+ const headers = await win.execute_script(() => {
+ return requestHeaders;
+ }, []);
+ assert_not_prefetched(headers,
+ "Prefetch should not work for different initiators.");
+}, "Prefetches from different initiator Documents with same RenderFrameHost");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/different-initiators.sub.https.html b/testing/web-platform/tests/speculation-rules/prefetch/different-initiators.sub.https.html
new file mode 100644
index 0000000000..c35ccde8bb
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/different-initiators.sub.https.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<meta name="variant" content="?cross-site-1">
+<meta name="variant" content="?cross-site-2">
+<meta name="variant" content="?same-site">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+// Regression test for https://crbug.com/1423234.
+promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ "Speculation Rules not supported");
+
+ // Open 2 windows.
+ const hostname1 =
+ location.search === '?cross-site-1' ? '{{hosts[alt][www]}}' : undefined;
+ const hostname2 =
+ location.search === '?cross-site-2' ? '{{hosts[alt][www]}}' : undefined;
+ const initiator1 = await spawnWindow(
+ t, { protocol: 'https', hostname: hostname1 });
+ const initiator2 = await spawnWindow(
+ t, { protocol: 'https', hostname: hostname2 });
+
+ // Start speculation rules prefetch from `initiator1`.
+ const nextUrl = initiator1.getExecutorURL({ protocol: 'https', page: 2 });
+ await initiator1.forceSinglePrefetch(nextUrl);
+
+ // Register a SW for `nextUrl` -- this is a trick to make the prefetched
+ // result to put in `PrefetchService::prefetches_ready_to_serve_` in
+ // Chromium implementation but actually not used by this navigation.
+ const r = await service_worker_unregister_and_register(
+ t, 'resources/sw.js', nextUrl);
+ await wait_for_state(t, r.installing, 'activated');
+
+ // Navigate `initiator1`.
+ // This doesn't use the prefetched result due to the ServiceWorker.
+ await initiator1.navigate(nextUrl);
+
+ // Navigate `initiator1` away from `nextUrl`.
+ const headers1 = await initiator1.execute_script(() => {
+ window.executor.suspend(() => {
+ location.href = 'about:blank';
+ });
+ return requestHeaders;
+ }, []);
+
+ // Unregister the SW.
+ await service_worker_unregister(t, nextUrl);
+
+ // Navigate `initiator2`.
+ // This shouldn't use the prefetched result because the initiator Documents
+ // (even sites) are different.
+ await initiator2.execute_script((url) => {
+ window.executor.suspend(() => {
+ location.href = url;
+ });
+ }, [nextUrl]);
+
+ // Note: while the Window for `initiator2` remains open, the executor ID of
+ // the page is the ID of `nextUrl`, which is `initiator1.context_id`.
+ // So `initiator1` is used below for manipulating the Window for `initiator2`.
+ assert_equals(
+ await initiator1.execute_script(() => location.href),
+ nextUrl.toString(),
+ "expected navigation to reach destination URL");
+
+ const headers2 = await initiator1.execute_script(() => {
+ return requestHeaders;
+ }, []);
+
+ assert_not_prefetched(headers1,
+ "Prefetch should not work due to ServiceWorker.");
+
+ assert_not_prefetched(headers2,
+ "Prefetch should not work for different initiators.");
+}, "Cross-initiator prefetches using ServiceWorker tricks");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/document-rules.https.html b/testing/web-platform/tests/speculation-rules/prefetch/document-rules.https.html
new file mode 100644
index 0000000000..701987c431
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/document-rules.https.html
@@ -0,0 +1,317 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script src="/common/subset-tests-by-key.js"></script>
+
+<meta name="variant" content="?include=defaultPredicate">
+<meta name="variant" content="?include=hrefMatches">
+<meta name="variant" content="?include=and">
+<meta name="variant" content="?include=or">
+<meta name="variant" content="?include=not">
+<meta name="variant" content="?include=invalidPredicate">
+<meta name="variant" content="?include=linkInShadowTree">
+<meta name="variant" content="?include=linkHrefChanged">
+<meta name="variant" content="?include=newRuleSetAdded">
+<meta name="variant" content="?include=selectorMatches">
+<meta name="variant" content="?include=selectorMatchesScopingRoot">
+<meta name="variant" content="?include=selectorMatchesInShadowTree">
+<meta name="variant" content="?include=selectorMatchesDisplayNone">
+<meta name="variant" content="?include=selectorMatchesDisplayLocked">
+<meta name="variant" content="?include=unslottedLink">
+<meta name="variant" content="?include=immediateMutation">
+
+<body>
+<script>
+ subsetTestByKey('defaultPredicate', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ const url = getPrefetchUrl();
+ addLink(url);
+ insertDocumentRule();
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url), 1);
+ }, 'test document rule with no predicate');
+
+ subsetTestByKey('hrefMatches', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule({ href_matches: '*\\?uuid=*&foo=bar' });
+
+ const url_1 = getPrefetchUrl({foo: 'bar'});
+ addLink(url_1);
+ const url_2 = getPrefetchUrl({foo: 'buzz'});
+ addLink(url_2)
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url_1), 1);
+ assert_equals(await isUrlPrefetched(url_2), 0);
+ }, 'test href_matches document rule');
+
+ subsetTestByKey('and', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule({
+ 'and': [
+ { href_matches: '*\\?*foo=bar*' },
+ { href_matches: '*\\?*fizz=buzz*' }]
+ });
+
+ const url_1 = getPrefetchUrl({foo: 'bar'});
+ const url_2 = getPrefetchUrl({fizz: 'buzz'});
+ const url_3 = getPrefetchUrl({foo: 'bar', fizz: 'buzz'});
+ [url_1, url_2, url_3].forEach(url => addLink(url));
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url_1), 0);
+ assert_equals(await isUrlPrefetched(url_2), 0);
+ assert_equals(await isUrlPrefetched(url_3), 1);
+ }, 'test document rule with conjunction predicate');
+
+ subsetTestByKey('or', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule({
+ 'or': [
+ { href_matches: '*\\?*foo=bar*' },
+ { href_matches: '*\\?*fizz=buzz*' }]
+ });
+
+ const url_1 = getPrefetchUrl({ foo: 'buzz' });
+ const url_2 = getPrefetchUrl({ fizz: 'buzz' });
+ const url_3 = getPrefetchUrl({ foo: 'bar'});
+ [url_1, url_2, url_3].forEach(url => addLink(url));
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url_1), 0);
+ assert_equals(await isUrlPrefetched(url_2), 1);
+ assert_equals(await isUrlPrefetched(url_3), 1);
+ }, 'test document rule with disjunction predicate');
+
+ subsetTestByKey('not', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ "Speculation Rules not supported");
+
+ insertDocumentRule({ not: { href_matches: '*\\?uuid=*&foo=bar' } });
+
+ const url_1 = getPrefetchUrl({foo: 'bar'});
+ addLink(url_1);
+ const url_2 = getPrefetchUrl({foo: 'buzz'});
+ addLink(url_2)
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url_1), 0);
+ assert_equals(await isUrlPrefetched(url_2), 1);
+ }, 'test document rule with negation predicate');
+
+ subsetTestByKey('invalidPredicate', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ const url = getPrefetchUrl();
+ addLink(url);
+ insertDocumentRule({invalid: 'predicate'});
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url), 0);
+ }, 'invalid predicate should not throw error or start prefetch');
+
+ subsetTestByKey('linkInShadowTree', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule();
+
+ // Create shadow root.
+ const shadowHost = document.createElement('div');
+ document.body.appendChild(shadowHost);
+ const shadowRoot = shadowHost.attachShadow({mode: 'open'});
+
+ const url = getPrefetchUrl();
+ addLink(url, shadowRoot);
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url), 1);
+ }, 'test that matching link in a shadow tree is prefetched');
+
+ subsetTestByKey('linkHrefChanged', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule({href_matches: "*\\?*foo=bar*"});
+
+ const url = getPrefetchUrl();
+ const link = addLink(url);
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 0);
+
+ const matching_url = getPrefetchUrl({foo: 'bar'});
+ link.href = matching_url;
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(matching_url), 1);
+ }, 'test that changing the href of an invalid link to a matching value triggers a prefetch');
+
+ subsetTestByKey('newRuleSetAdded', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule({href_matches: "*\\?*foo=bar*"});
+ const url = getPrefetchUrl({fizz: "buzz"});
+ addLink(url);
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 0);
+
+ insertDocumentRule({href_matches: "*\\?*fizz=buzz*"});
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 1);
+ }, 'test that adding a second rule set triggers prefetch');
+
+ subsetTestByKey('selectorMatches', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule({ selector_matches: 'a.important-link' });
+
+ const url_1 = getPrefetchUrl({foo: 'bar'});
+ const importantLink = addLink(url_1);
+ importantLink.className = 'important-link';
+ const url_2 = getPrefetchUrl({foo: 'buzz'});
+ addLink(url_2)
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url_1), 1);
+ assert_equals(await isUrlPrefetched(url_2), 0);
+ }, 'test selector_matches document rule');
+
+ subsetTestByKey('selectorMatchesScopingRoot', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule({ selector_matches: ':root > body > a' });
+
+ const url_1 = getPrefetchUrl({ foo: 'bar' });
+ addLink(url_1);
+
+ const url_2 = getPrefetchUrl({ foo: 'buzz' });
+ const extraContainer = document.createElement('div');
+ document.body.appendChild(extraContainer);
+ addLink(url_2, extraContainer);
+
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url_1), 1);
+ assert_equals(await isUrlPrefetched(url_2), 0);
+ }, 'test selector_matches with :root');
+
+ // 'selector_matches' should never match with a link inside a shadow tree
+ // because the scoping root used when matching is always the document.
+ subsetTestByKey('selectorMatchesInShadowTree', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule({ selector_matches: 'a.important-link' });
+
+ // Create shadow root.
+ const shadowHost = document.createElement('div');
+ document.body.appendChild(shadowHost);
+ const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
+
+ const url = getPrefetchUrl();
+ const link = addLink(url, shadowRoot);
+ link.className = 'important-link';
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url), 0);
+ }, 'test selector_matches with link inside shadow tree');
+
+ subsetTestByKey('selectorMatchesDisplayNone', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ const style = document.createElement('style');
+ style.innerText = ".important-section { display: none; }";
+ document.head.appendChild(style);
+ insertDocumentRule();
+
+ const importantSection = document.createElement('div');
+ importantSection.className = 'important-section';
+ document.body.appendChild(importantSection);
+ const url = getPrefetchUrl();
+ addLink(url, importantSection);
+
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 0);
+
+ style.remove();
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 1);
+ }, 'test selector_matches with link inside display:none container');
+
+ subsetTestByKey('selectorMatchesDisplayLocked', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ const style = document.createElement('style');
+ style.innerText = ".important-section { content-visibility: hidden; }";
+ document.head.appendChild(style);
+ insertDocumentRule({ selector_matches: '.important-section a' });
+
+ const importantSection = document.createElement('div');
+ importantSection.className = 'important-section';
+ document.body.appendChild(importantSection);
+ const url = getPrefetchUrl();
+ addLink(url, importantSection);
+
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 0);
+
+ style.remove();
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 1);
+ }, 'test selector_matches with link inside display locked container');
+
+ subsetTestByKey('unslottedLink', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule();
+
+ // Create shadow root.
+ const shadowHost = document.createElement('div');
+ document.body.appendChild(shadowHost);
+ const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
+
+ // Add unslotted link.
+ const url = getPrefetchUrl();
+ addLink(url, shadowHost);
+
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 0);
+ }, 'test that unslotted link never matches document rule');
+
+ subsetTestByKey('immediateMutation', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ // Add a link and allow it to get its style computed.
+ // (Double RAF lets this happen normally.)
+ const url = getPrefetchUrl();
+ const link = addLink(url, document.body);
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
+
+ // Add a document rule and then immediately change the DOM to make it match.
+ insertDocumentRule({ selector_matches: '.late-class *' });
+ document.body.className = 'late-class';
+
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 1);
+ }, 'test that selector_matches predicates respect changes immediately');
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/duplicate-urls.https.html b/testing/web-platform/tests/speculation-rules/prefetch/duplicate-urls.https.html
new file mode 100644
index 0000000000..179bbdfd68
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/duplicate-urls.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<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/utils.sub.js"></script>
+
+<script>
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let urls = Array(5).fill(getPrefetchUrlList(1)[0]);
+ insertSpeculationRules({ prefetch: [{ source: 'list', urls: urls }] });
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ let prefetched_count = (await Promise.all(urls.map(isUrlPrefetched))).reduce(
+ (count, was_prefetched) => count + (was_prefetched ? 1 : 0), 0);
+
+ assert_equals(prefetched_count, 1, "url should be prefetched just once.");
+ }, "browser should remove duplicate urls from prefetch buffer.");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/initiators-a-element.sub.https.html b/testing/web-platform/tests/speculation-rules/prefetch/initiators-a-element.sub.https.html
new file mode 100644
index 0000000000..bac5eb7cb7
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/initiators-a-element.sub.https.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<meta name="variant" content="?cross-site">
+<meta name="variant" content="?same-site">
+<meta name="timeout" content="long">
+<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/utils.sub.js"></script>
+<script>
+ // In https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate,
+ // `sourceDocument` (instead of `navigable`'s active document) should be
+ // used as the referring document for prefetch.
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const win = await spawnWindow(t, { protocol: 'https' });
+
+ const hostname =
+ location.search === '?cross-site' ? '{{hosts[alt][www]}}' : undefined;
+ const nextUrl = win.getExecutorURL({ protocol: 'https', hostname, page: 2 });
+
+ await win.forceSinglePrefetch(nextUrl);
+
+ // sourceDocument == `win`'s Document == active document of window being
+ // navigated.
+ await win.execute_script((url) => {
+ window.executor.suspend(() => {
+ const a = document.createElement('a');
+ a.setAttribute('href', url);
+ document.body.appendChild(a);
+ a.click();
+ });
+ }, [nextUrl]);
+
+ assert_equals(
+ await win.execute_script(() => location.href),
+ nextUrl.toString(),
+ "expected navigation to reach destination URL");
+
+ assert_prefetched(await win.getRequestHeaders());
+ }, `<a>`);
+
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const win = await spawnWindow(t, { protocol: 'https' });
+
+ const hostname =
+ location.search === '?cross-site' ? '{{hosts[alt][www]}}' : undefined;
+ const nextUrl = win.getExecutorURL({ protocol: 'https', hostname, page: 2 });
+
+ await win.forceSinglePrefetch(nextUrl);
+
+ // sourceDocument == `win`'s Document != active document of window being
+ // navigated, since the window being navigated is a new window.
+ await win.execute_script((url) => {
+ window.executor.suspend(() => {
+ const a = document.createElement('a');
+ a.setAttribute('href', url);
+ a.setAttribute('target', '_blank');
+ document.body.appendChild(a);
+ a.click();
+ });
+ }, [nextUrl]);
+
+ // Below, the scripts given to `win.execute_script()` are executed on the
+ // `nextUrl` page in the new window, because `window.executor.suspend()`
+ // above made `win`'s original page stop processing `execute_script()`,
+ // while the new page of `nextUrl` in the new window starts processing
+ // `execute_script()` for the same ID.
+ assert_equals(
+ await win.execute_script(() => location.href),
+ nextUrl.toString(),
+ "expected navigation to reach destination URL");
+
+ assert_prefetched(await win.getRequestHeaders());
+ }, `<a target="blank">`);
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/initiators-iframe-location-href.sub.https.html b/testing/web-platform/tests/speculation-rules/prefetch/initiators-iframe-location-href.sub.https.html
new file mode 100644
index 0000000000..9d6702d4b7
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/initiators-iframe-location-href.sub.https.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<meta name="variant" content="?cross-site">
+<meta name="variant" content="?same-site">
+<meta name="timeout" content="long">
+<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/utils.sub.js"></script>
+<script>
+ // In https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate,
+ // `sourceDocument` (instead of `navigable`'s active document) should be
+ // used as the referring document for prefetch.
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const win = await spawnWindow(t, { protocol: 'https' });
+
+ const hostname =
+ location.search === '?cross-site' ? '{{hosts[alt][www]}}' : undefined;
+ const nextUrl = win.getExecutorURL({ protocol: 'https', hostname, page: 2 });
+
+ await win.forceSinglePrefetch(nextUrl);
+
+ // In https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate,
+ // `sourceDocument` is the incumbent Document and thus `win`'s Document.
+ // `navigable`'s active document is `iframe`'s Document.
+ await win.execute_script((url) => {
+ window.executor.suspend(() => {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.contentWindow.location.href = url;
+ });
+ }, [nextUrl]);
+
+ // Below, the scripts given to `win.execute_script()` are executed on the
+ // `nextUrl` page in the iframe, because `window.executor.suspend()` above
+ // made `win`'s original page stop processing `execute_script()`,
+ // while the new page of `nextUrl` in the iframe starts processing
+ // `execute_script()` for the same ID.
+ assert_equals(
+ await win.execute_script(() => location.href),
+ nextUrl.toString(),
+ "expected navigation to reach destination URL");
+
+ assert_prefetched(await win.getRequestHeaders());
+ }, `location.href across iframe`);
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/initiators-window-open.sub.https.html b/testing/web-platform/tests/speculation-rules/prefetch/initiators-window-open.sub.https.html
new file mode 100644
index 0000000000..f786df077d
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/initiators-window-open.sub.https.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<meta name="variant" content="?cross-site">
+<meta name="variant" content="?same-site">
+<meta name="timeout" content="long">
+<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/utils.sub.js"></script>
+<script>
+ // In https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate,
+ // `sourceDocument` (instead of `navigable`'s active document) should be
+ // used as the referring document for prefetch.
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const win = await spawnWindow(t, { protocol: 'https' });
+
+ const hostname =
+ location.search === '?cross-site' ? '{{hosts[alt][www]}}' : undefined;
+ const nextUrl = win.getExecutorURL({ protocol: 'https', hostname, page: 2 });
+
+ await win.forceSinglePrefetch(nextUrl);
+
+ await win.execute_script((url) => {
+ window.executor.suspend(() => {
+ window.open(url, "_blank");
+ });
+ }, [nextUrl]);
+
+ // Below, the scripts given to `win.execute_script()` are executed on the
+ // `nextUrl` page in the new window, because `window.executor.suspend()`
+ // above made `win`'s original page stop processing `execute_script()`,
+ // while the new page of `nextUrl` in the new window starts processing
+ // `execute_script()` for the same ID. Same for below.
+ assert_equals(
+ await win.execute_script(() => location.href),
+ nextUrl.toString(),
+ "expected navigation to reach destination URL");
+
+ assert_prefetched(await win.getRequestHeaders());
+ }, `window.open()`);
+
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const win = await spawnWindow(t, { protocol: 'https' });
+
+ const hostname =
+ location.search === '?cross-site' ? '{{hosts[alt][www]}}' : undefined;
+ const nextUrl = win.getExecutorURL({ protocol: 'https', hostname, page: 2 });
+
+ await win.forceSinglePrefetch(nextUrl);
+
+ await win.execute_script((url) => {
+ window.executor.suspend(() => {
+ window.open(url, "_blank", "noopener");
+ });
+ }, [nextUrl]);
+
+ assert_equals(
+ await win.execute_script(() => location.href),
+ nextUrl.toString(),
+ "expected navigation to reach destination URL");
+
+ assert_prefetched(await win.getRequestHeaders());
+ }, `window.open(noopener)`);
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/invalid-rules.https.html b/testing/web-platform/tests/speculation-rules/prefetch/invalid-rules.https.html
new file mode 100644
index 0000000000..573f3c0b0f
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/invalid-rules.https.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<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/utils.sub.js"></script>
+<script>
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let agent = await spawnWindow(t);
+ let nextUrl = agent.getExecutorURL({ page: 2 });
+ await agent.forceSinglePrefetch(nextUrl, { invalid_key: "value" });
+ await agent.navigate(nextUrl);
+
+ assert_not_prefetched(await agent.getRequestHeaders());
+ }, "an unrecognized key in a prefetch rule should prevent it from being fetched");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/multiple-url.https.html b/testing/web-platform/tests/speculation-rules/prefetch/multiple-url.https.html
new file mode 100644
index 0000000000..dd9916632f
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/multiple-url.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<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/utils.sub.js"></script>
+
+<script>
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let urls = getPrefetchUrlList(5);
+ insertSpeculationRules({ prefetch: [{ source: 'list', urls: urls }] });
+ await new Promise(resolve => t.step_timeout(resolve, 3000));
+
+ let prefetched_count = (await Promise.all(urls.map(isUrlPrefetched))).reduce(
+ (count, was_prefetched) => count + (was_prefetched ? 1 : 0), 0);
+
+ assert_greater_than_equal(prefetched_count, 2, "At least two urls should be prefetched to pass the test.");
+ }, "browser should be able to prefetch multiple urls");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-delivery-type.tentative.https.html b/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-delivery-type.tentative.https.html
new file mode 100644
index 0000000000..cee8e55f12
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-delivery-type.tentative.https.html
@@ -0,0 +1,59 @@
+<!-- TODO(crbug/1358591): Rename this file from "tentative" once
+`WICG/nav-speculation#180` is merged. -->
+<!DOCTYPE html>
+<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/utils.sub.js"></script>
+
+<meta name="variant" content="?prefetch=true&bypass_cache=true">
+<meta name="variant" content="?prefetch=false&bypass_cache=true">
+<meta name="variant" content="?prefetch=true&bypass_cache=false">
+<meta name="variant" content="?prefetch=false&bypass_cache=false">
+
+<script>
+const prefetchEnabled = (Object.fromEntries(
+ new URLSearchParams(location.search)).prefetch === "true");
+const bypassCache = (Object.fromEntries(
+ new URLSearchParams(location.search)).bypass_cache === "true");
+
+promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ // Some meaningless query param to avoid cached response.
+ const prefetchUrl =
+ bypassCache ? agent.getExecutorURL({ a: "b" }) : agent.getExecutorURL();
+
+ if (prefetchEnabled)
+ await agent.forceSinglePrefetch(prefetchUrl);
+
+ await agent.navigate(prefetchUrl);
+
+ if (prefetchEnabled)
+ assert_prefetched(await agent.getRequestHeaders(),
+ `Prefetch ${prefetchUrl.href} should work.`);
+ else
+ assert_not_prefetched(await agent.getRequestHeaders(),
+ `${prefetchUrl.href} should not be prefetched.`);
+
+ await agent.execute_script(
+ () => window.entries = performance.getEntriesByType('navigation'));
+
+ // Expects one entry, whose `deliveryType` is "navigational-prefetch" for
+ // the prefetched request, and "" for the non-prefetched.
+ //
+ // TODO(crbug/1317756): Currently the initial prefetch request bypasses the
+ // HTTP cache, making `deliveryType` always an empty string for non-prefetch
+ // request. Expand test coverage when `net::LOAD_DISABLE_CACHE` is removed.
+ assert_equals(await agent.execute_script(() => window.entries.length), 1,
+ 'Wrong number of entries');
+ const deliveryType =
+ await agent.execute_script(() => window.entries[0].deliveryType);
+ const expectedDeliveryType = prefetchEnabled ? 'navigational-prefetch' : '';
+ assert_equals(deliveryType, expectedDeliveryType);
+
+ }, `PerformanceNavigationTiming.deliveryType test, same origin prefetch.`);
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-requestStart-responseStart.https.html b/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-requestStart-responseStart.https.html
new file mode 100644
index 0000000000..062d7265d8
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-requestStart-responseStart.https.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<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/utils.sub.js"></script>
+
+<meta name="variant" content="">
+<meta name="variant" content="?prefetch=true">
+
+<script>
+const searchParams = new URLSearchParams(location.search);
+const prefetchEnabled = searchParams.has('prefetch');
+
+promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ // Some meaningless query param to avoid cached response.
+ const prefetchUrl = agent.getExecutorURL({ a: "b" });
+
+ if (prefetchEnabled)
+ await agent.forceSinglePrefetch(prefetchUrl);
+
+ await agent.navigate(prefetchUrl);
+
+ if (prefetchEnabled) {
+ assert_prefetched(await agent.getRequestHeaders(),
+ `Prefetch ${prefetchUrl.href} should work.`);
+ } else {
+ assert_not_prefetched(await agent.getRequestHeaders(),
+ `${prefetchUrl.href} should not be prefetched.`);
+ }
+
+ const entries = await agent.execute_script(
+ () => performance.getEntriesByType('navigation'));
+ assert_equals(entries.length, 1, 'Wrong number of navigation entries');
+ const entry = entries[0];
+
+ // Events timeline:
+ // ... -> connectEnd --> requestStart --> responseStart --> ...
+ if (prefetchEnabled) {
+ assert_equals(entry.connectEnd, entry.requestStart);
+ assert_equals(entry.requestStart, entry.responseStart);
+ } else {
+ assert_less_than_equal(entry.connectEnd, entry.requestStart);
+ assert_less_than_equal(entry.requestStart, entry.responseStart);
+ }
+
+ }, "PerformanceNavigationTiming.requestStart/responseStart test, same origin prefetch.");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-sizes.https.html b/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-sizes.https.html
new file mode 100644
index 0000000000..19c254ca1d
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-sizes.https.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<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/utils.sub.js"></script>
+
+<meta name="variant" content="">
+<meta name="variant" content="?bypass_cache=true">
+<meta name="variant" content="?prefetch=true">
+<meta name="variant" content="?prefetch=true&bypass_cache=true">
+
+<script>
+const searchParams = new URLSearchParams(location.search);
+const prefetchEnabled = searchParams.has('prefetch');
+const bypassCache = searchParams.has('bypass_cache');
+
+// Header size: https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-transfersize
+const headerSize = 300;
+
+promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ // Some meaningless query param to avoid cached response.
+ const prefetchUrl =
+ bypassCache ? agent.getExecutorURL({ a: "b" }) : agent.getExecutorURL();
+
+ if (prefetchEnabled)
+ await agent.forceSinglePrefetch(prefetchUrl);
+
+ await agent.navigate(prefetchUrl);
+
+ if (prefetchEnabled)
+ assert_prefetched(await agent.getRequestHeaders(),
+ `Prefetch ${prefetchUrl.href} should work.`);
+ else
+ assert_not_prefetched(await agent.getRequestHeaders(),
+ `${prefetchUrl.href} should not be prefetched.`);
+
+ await agent.execute_script(
+ () => window.entries = performance.getEntriesByType('navigation'));
+
+ // TODO(crbug/1317756): Currently the initial prefetch request bypasses the
+ // HTTP cache. Expand test coverage for cache and cache+revalidation cases.
+ //
+ // We do not assert the exact size of `resources/executor.sub.html` since it
+ // would be a headache to update this test everytime executor.sub.html
+ // changes.
+ assert_equals(await agent.execute_script(() => window.entries.length), 1,
+ 'Wrong number of entries');
+ const entry =
+ await agent.execute_script(() => window.entries[0]);
+ const bodySize = entry.encodedBodySize;
+ assert_greater_than(bodySize, 0);
+ assert_equals(entry.transferSize, headerSize + bodySize);
+ assert_equals(entry.decodedBodySize, bodySize);
+ }, `PerformanceNavigationTiming.transferSize/encodedBodySize/decodedBodySize test, same origin prefetch.`);
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/README.txt b/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/README.txt
new file mode 100644
index 0000000000..60ac226f8c
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/README.txt
@@ -0,0 +1 @@
+Web Platform Tests for No-Vary-Search support in prefetch cache.
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/prefetch-single-with-hint.https.html b/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/prefetch-single-with-hint.https.html
new file mode 100644
index 0000000000..d62788caba
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/prefetch-single-with-hint.https.html
@@ -0,0 +1,337 @@
+<!DOCTYPE html>
+<title>Use for navigation the requested prefetched response annotated with No-Vary-Search hint, if
+No-Vary-Search headers also match during navigation</title>
+<meta charset="utf-8">
+
+<meta name="variant" content="?1-1">
+<meta name="variant" content="?2-2">
+<meta name="variant" content="?3-3">
+<meta name="variant" content="?4-4">
+<meta name="variant" content="?5-5">
+<meta name="variant" content="?6-6">
+<meta name="variant" content="?7-7">
+<meta name="variant" content="?8-8">
+<meta name="variant" content="?9-9">
+<meta name="variant" content="?10-10">
+<meta name="variant" content="?11-11">
+<meta name="variant" content="?12-12">
+<meta name="variant" content="?13-13">
+<meta name="variant" content="?14-14">
+<meta name="variant" content="?15-15">
+<meta name="variant" content="?16-16">
+<meta name="variant" content="?17-17">
+<meta name="variant" content="?18-18">
+<meta name="variant" content="?19-19">
+<meta name="variant" content="?20-20">
+<meta name="variant" content="?21-21">
+<meta name="variant" content="?22-22">
+<meta name="variant" content="?23-23">
+<meta name="variant" content="?24-24">
+<meta name="variant" content="?25-last">
+
+<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/utils.sub.js"></script>
+<script src="/common/subset-tests.js"></script>
+
+<script>
+ function addNoVarySearchHeaderUsingQueryParam(url, value){
+ // Use nvs_header query parameter to ask the wpt server
+ // to populate No-Vary-Search response header.
+ if(value){
+ url.searchParams.append("nvs_header", value);
+ }
+ }
+
+ /*
+ remoteAgent: the RemoteContext instance used to communicate between the
+ test and the window where prefetch/navigation is happening
+ noVarySearchHeaderValue: the value of No-Vary-Search header to be populated
+ for the prefetched response
+ noVarySearchHintValue: the value of No-Vary-Search hint passed in
+ as expects_no_vary_search hint in prefetch speculation rules.
+ prefetchQuery: query params to be added to prefetchExecutor url and prefetched
+ navigateQuery: query params to be added to prefetchExecutor url and navigated to
+ */
+ async function prefetchAndNavigate(remoteAgent, noVarySearchHeaderValue, noVarySearchHintValue, prefetchQuery, navigateQuery){
+ /*
+ Flow:
+ * prefetch prefetch_nvs_hint.py?uuid=...&nvs_header=...&otherqueryparams
+ * the prefetch request above includes no_vary_search_hint in the speculation
+ rules
+ * the server blocks progress on this prefetch request on the server side so
+ from the browser perspective the server is "thinking"
+ * the test starts navigation to
+ prefetch_nvs_hint.py?uuid=...&nvs_header=...&otherdifferentqueryparams.
+ This navigation matches by No-Vary-Search hint the above in
+ progress prefetch.
+ * the test fetches prefetch_nvs_hint.py?uuid=...&unblock="unblock"
+ which unblocks the in progress prefetch so that the in-progress
+ navigation can continue
+ */
+ const prefetch_nvs_hint_server_page = "prefetch_nvs_hint.py";
+ const prefetchUrl = remoteAgent.getExecutorURL({executor:prefetch_nvs_hint_server_page});
+ const navigateToUrl = new URL(prefetchUrl);
+ // Add query params to the url to be prefetched.
+ const additionalPrefetchedUrlSearchParams = new URLSearchParams(prefetchQuery);
+ addNoVarySearchHeaderUsingQueryParam(prefetchUrl, noVarySearchHeaderValue);
+ additionalPrefetchedUrlSearchParams.forEach((value, key) => {
+ prefetchUrl.searchParams.append(key, value);
+ });
+
+ await remoteAgent.forceSinglePrefetch(prefetchUrl,
+ {expects_no_vary_search:noVarySearchHintValue});
+
+ // Add new query params to navigateToUrl to match No-Vary-Search test case.
+ const additionalNavigateToUrlSearchParams = new URLSearchParams(navigateQuery);
+ addNoVarySearchHeaderUsingQueryParam(navigateToUrl, noVarySearchHeaderValue);
+ additionalNavigateToUrlSearchParams.forEach((value, key) => {
+ navigateToUrl.searchParams.append(key, value);
+ });
+ // Url used by fetch in order to unblock the prefetched url
+ const nvshint_unblock_url = remoteAgent.getExecutorURL(
+ {executor:prefetch_nvs_hint_server_page, unblock:"unblock"});
+ await remoteAgent.execute_script((unblock_url) => {
+ onbeforeunload = (event) => {
+ fetch(unblock_url);
+ };
+ }, [nvshint_unblock_url]);
+
+ // Try navigating to a non-exact prefetched URL that matches by
+ // No-Vary-Search hint
+ // Wait for the navigation to finish
+ await remoteAgent.navigate(navigateToUrl);
+ }
+
+ function prefetch_no_vary_search_test(description, noVarySearch, noVarySearchHint, prefetchQuery, navigateQuery, shouldUsePrefetch){
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+ const agent = await spawnWindow(t, {});
+ await prefetchAndNavigate(agent,
+ noVarySearch,
+ noVarySearchHint,
+ prefetchQuery,
+ navigateQuery);
+
+ if(shouldUsePrefetch){
+ assert_prefetched(await agent.getRequestHeaders(),
+ "Navigation didn't use the prefetched response!");
+ }
+ else{
+ assert_not_prefetched(await agent.getRequestHeaders(),
+ "Navigation used the prefetched response!");
+ }
+ }, description);
+ }
+
+ // Test inputs:
+ // - description: a description of the test.
+ // - noVarySearch: No-Vary-Search header value for the response.
+ // - noVarySearchHint: No-Vary-Search hint to include in prefetch
+ // speculation rules
+ // - prefetchQuery: added to query part of prefetch-executor when prefetching
+ // - navigateQuery: added to query part of prefetch-executor when navigating
+ // - shouldUsePrefetch: if the test case expects the prefetched entry to be
+ // used or not.
+ [{description:"Use in-flight prefetch as query parameter b has the same value.",
+ noVarySearch: 'params=("a")',
+ noVarySearchHint: 'params=("a")',
+ prefetchQuery: "a=2&b=3",
+ navigateQuery: "b=3",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use in-flight prefetch as there is no No-Vary-Search hint.",
+ noVarySearch: 'params=("a")',
+ noVarySearchHint: '',
+ prefetchQuery: "a=2&b=3",
+ navigateQuery: "b=3",
+ shouldUsePrefetch: false},
+
+ {description:"Don't use in-flight prefetch as the prefetched URL has the extra \"a\" query parameter.",
+ noVarySearch: 'params=("b")',
+ noVarySearchHint: 'params=("b")',
+ prefetchQuery: "a=2&b=3",
+ navigateQuery: "b=2",
+ shouldUsePrefetch: false},
+
+ {description:"Use in-flight prefetch as the URLs do not vary by a and b.",
+ noVarySearch: 'params=("a" "b")',
+ noVarySearchHint: 'params=("a" "b")',
+ prefetchQuery: "a=2&b=3",
+ navigateQuery: "b=2",
+ shouldUsePrefetch: true},
+
+ {description:"Do not use in-flight prefetch as the navigation URL has" +
+ " a different value for the \"b\" query parameter.",
+ noVarySearch: 'params=("a" "b")',
+ noVarySearchHint: 'params=("a")',
+ prefetchQuery: "a=2&b=3",
+ navigateQuery: "b=2",
+ shouldUsePrefetch: false},
+
+ {description:"Use in-flight prefetch as the URLs have the same values for all keys, only differing by order.",
+ noVarySearch: "key-order",
+ noVarySearchHint: "key-order",
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5&b=3",
+ navigateQuery: "d=6&a=3&b=5&b=3&c=5&a=4",
+ shouldUsePrefetch: true},
+
+ {description:"Use in-flight prefetch as the URLs have the same values for all keys, only differing by order and using ?1 for specifying a true value.",
+ noVarySearch: "key-order=?1",
+ noVarySearchHint: "key-order=?1",
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5&b=3",
+ navigateQuery: "d=6&a=3&b=5&b=3&c=5&a=4",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use in-flight prefetch as key-order is set to false and the URLs are not identical.",
+ noVarySearch: "key-order=?0",
+ noVarySearchHint: "key-order=?1",
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5&b=3",
+ navigateQuery: "d=6&a=3&b=5&b=3&c=5&a=4",
+ shouldUsePrefetch: false},
+
+ {description:"Use in-flight prefetch as all query parameters except c can be ignored.",
+ noVarySearch: 'params, except=("c")',
+ noVarySearchHint: 'params, except=("c")',
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Use in-flight prefetch as all query parameters except c can be ignored." +
+ " Only the last except matters.",
+ noVarySearch: 'params, except=("b"), except=("c")',
+ noVarySearchHint: 'params, except=("b"), except=("c")',
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use in-flight prefetch as even though all query parameters" +
+ " except c can be ignored, c has different value.",
+ noVarySearch: 'params, except=("c")',
+ noVarySearchHint: "params",
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=5",
+ shouldUsePrefetch: false},
+
+ {description:"Use in-flight prefetch as even though all query parameters" +
+ " except c and d can be ignored, c value matches and d value matches.",
+ noVarySearch: 'params, except=("c" "d")',
+ noVarySearchHint: 'params, except=("c" "d")',
+ prefetchQuery: "b=5&a=3&d=6&c=5",
+ navigateQuery: "d=6&a=1&b=2&c=5",
+ shouldUsePrefetch: true},
+
+ {description:"Use in-flight prefetch as even though all query parameters except" +
+ " c and d can be ignored, c value matches and d value matches." +
+ " Some query parameters to be ignored appear multiple times in the query.",
+ noVarySearch: 'params, except=("c" "d")',
+ noVarySearchHint: 'params',
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5",
+ navigateQuery: "d=6&a=1&a=2&b=2&b=3&c=5",
+ shouldUsePrefetch: true},
+
+ {description:"Use in-flight prefetch as all query parameters except c can be ignored." +
+ " Allow extension via parameters.",
+ noVarySearch: 'params, except=("c";unknown)',
+ noVarySearchHint: 'params, except=("c";unknown)',
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Use in-flight prefetch as query parameter c can be ignored." +
+ " Allow extension via parameters.",
+ noVarySearch: 'params=("c";unknown)',
+ noVarySearchHint: 'params=("c";unknown)',
+ prefetchQuery: "a=2&b=2&c=5",
+ navigateQuery: "a=2&c=3&b=2",
+ shouldUsePrefetch: true},
+
+ {description:"Use in-flight prefetch as the URLs have the values in different order for a." +
+ " Allow extension via parameters.",
+ noVarySearch: "key-order;unknown",
+ noVarySearchHint: "key-order;unknown",
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5&b=3",
+ navigateQuery: "d=6&a=3&b=5&b=3&c=5&a=4",
+ shouldUsePrefetch: true},
+
+ {description:"Use in-flight prefetch as the URLs do not vary on any query parameters." +
+ " Allow extension via parameters.",
+ noVarySearch: "params;unknown",
+ noVarySearchHint: "params;unknown",
+ prefetchQuery: "",
+ navigateQuery: "b=4&c=5",
+ shouldUsePrefetch: true},
+
+ {description:"Use in-flight prefetch as all query parameters except c can be ignored." +
+ " Allow extension via parameters.",
+ noVarySearch: 'params;unknown, except=("c");unknown',
+ noVarySearchHint: 'params;unknown, except=("c");unknown',
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use the in-flight prefetched URL. Empty No-Vary-Search means default URL variance." +
+ " The prefetched and the navigated URLs have to be the same.",
+ noVarySearch: "",
+ noVarySearchHint: "params",
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: false},
+
+ {description:"Use the in-flight prefetch. Empty No-Vary-Search means default URL variance." +
+ " The prefetched and the navigated URLs have to be the same.",
+ noVarySearch: "",
+ noVarySearchHint: "",
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "b=5&a=3&d=6&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Use the in-flight prefetch. Empty No-Vary-Search means default URL variance." +
+ " The prefetched and the navigated URLs have to be the same.",
+ noVarySearch: "",
+ noVarySearchHint: "",
+ prefetchQuery: "",
+ navigateQuery: "",
+ shouldUsePrefetch: true},
+
+ {description:"Use the in-flight prefetch. Non-ASCII key - 2 UTF-8 code units." +
+ " Don't vary the response on the non-ASCII key.",
+ noVarySearch: 'params=("%C2%A2")',
+ noVarySearchHint: 'params=("%C2%A2")',
+ prefetchQuery: "¢=3",
+ navigateQuery: "¢=4",
+ shouldUsePrefetch: true},
+
+ {description:"Use the in-flight prefetch. Non-ASCII key - 2 UTF-8 code units." +
+ " Don't vary the response on the non-ASCII key.",
+ noVarySearch: 'params=("%C2%A2")',
+ noVarySearchHint: 'params=("%C2%A2")',
+ prefetchQuery: "a=2&¢=3",
+ navigateQuery: "¢=4&a=2",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use the in-flight prefetch. Non-ASCII key - 2 UTF-8 code units." +
+ " Vary the response on the non-ASCII key.",
+ noVarySearch: 'params, except=("%C2%A2")',
+ noVarySearchHint: 'params',
+ prefetchQuery: "¢=3",
+ navigateQuery: "¢=4",
+ shouldUsePrefetch: false},
+
+ {description:"Use the in-flight prefetch. Non-ASCII key - 2 UTF-8 code units." +
+ " Vary the response on the non-ASCII key.",
+ noVarySearch: 'params, except=("%C2%A2")',
+ noVarySearchHint: 'params, except=("%C2%A2")',
+ prefetchQuery: "¢=3&a=4",
+ navigateQuery: "a=5&¢=3",
+ shouldUsePrefetch: true},
+
+ ].forEach(({description, noVarySearch, noVarySearchHint, prefetchQuery, navigateQuery, shouldUsePrefetch}) => {
+ subsetTest(prefetch_no_vary_search_test,
+ description, noVarySearch, noVarySearchHint, prefetchQuery, navigateQuery,
+ shouldUsePrefetch);
+ });
+
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/prefetch-single.https.html b/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/prefetch-single.https.html
new file mode 100644
index 0000000000..fdbb617135
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/prefetch-single.https.html
@@ -0,0 +1,314 @@
+<!DOCTYPE html>
+<title>Prefetched response including No-Vary-Search headers is used during navigation</title>
+<meta charset="utf-8">
+
+<meta name="variant" content="?1-1">
+<meta name="variant" content="?2-2">
+<meta name="variant" content="?3-3">
+<meta name="variant" content="?4-4">
+<meta name="variant" content="?5-5">
+<meta name="variant" content="?6-6">
+<meta name="variant" content="?7-7">
+<meta name="variant" content="?8-8">
+<meta name="variant" content="?9-9">
+<meta name="variant" content="?10-10">
+<meta name="variant" content="?11-11">
+<meta name="variant" content="?12-12">
+<meta name="variant" content="?13-13">
+<meta name="variant" content="?14-14">
+<meta name="variant" content="?15-15">
+<meta name="variant" content="?16-16">
+<meta name="variant" content="?17-17">
+<meta name="variant" content="?18-18">
+<meta name="variant" content="?19-19">
+<meta name="variant" content="?20-20">
+<meta name="variant" content="?21-21">
+<meta name="variant" content="?22-22">
+<meta name="variant" content="?23-23">
+<meta name="variant" content="?24-24">
+<meta name="variant" content="?25-25">
+<meta name="variant" content="?26-26">
+<meta name="variant" content="?27-27">
+<meta name="variant" content="?28-28">
+<meta name="variant" content="?29-29">
+<meta name="variant" content="?30-last">
+
+<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/utils.sub.js"></script>
+<script src="/common/subset-tests.js"></script>
+
+<script>
+ function addNoVarySearchHeaderUsingPipe(url, value){
+ // Use server pipes https://web-platform-tests.org/writing-tests/server-pipes.html
+ // to populate No-Vary-Search response header.
+ // The "," and ")" characters need to be escaped by using backslash
+ // (see https://web-platform-tests.org/writing-tests/server-pipes.html).
+ // E.g. params=("a") becomes params=("a"\), params=("a"),key-order becomes
+ // params=("a"\)\,key-order etc.
+ url.searchParams.append("pipe",
+ `header(No-Vary-Search,${value.replaceAll(/[,)]/g, '\\$&')})`);
+ }
+
+ /*
+ remoteAgent: the RemoteContext instance used to communicate between the
+ test and the window where prefetch/navigation is happening
+ noVarySearchHeaderValue: the value of No-Vary-Search header to be populated
+ for the prefetched response
+ prefetchQuery: query params to be added to prefetchExecutor url and prefetched
+ navigateQuery: query params to be added to prefetchExecutor url and navigated to
+ */
+ async function prefetchAndNavigate(remoteAgent, noVarySearchHeaderValue, prefetchQuery, navigateQuery){
+ const nextUrl = remoteAgent.getExecutorURL();
+ const navigateToUrl = new URL(nextUrl);
+ // Add query params to the url to be prefetched.
+ const additionalPrefetchedUrlSearchParams = new URLSearchParams(prefetchQuery);
+ addNoVarySearchHeaderUsingPipe(nextUrl, noVarySearchHeaderValue);
+ additionalPrefetchedUrlSearchParams.forEach((value, key) => {
+ nextUrl.searchParams.append(key, value);
+ });
+
+ await remoteAgent.forceSinglePrefetch(nextUrl);
+
+ // Add new query params to navigateToUrl to match No-Vary-Search test case.
+ const additionalNavigateToUrlSearchParams = new URLSearchParams(navigateQuery);
+ addNoVarySearchHeaderUsingPipe(navigateToUrl, noVarySearchHeaderValue);
+ additionalNavigateToUrlSearchParams.forEach((value, key) => {
+ navigateToUrl.searchParams.append(key, value);
+ });
+ await remoteAgent.navigate(navigateToUrl);
+ }
+
+ function prefetch_no_vary_search_test(description, noVarySearch, prefetchQuery, navigateQuery, shouldUsePrefetch){
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+ const agent = await spawnWindow(t, {});
+ await prefetchAndNavigate(agent,
+ noVarySearch,
+ prefetchQuery,
+ navigateQuery);
+
+ if(shouldUsePrefetch){
+ assert_prefetched(await agent.getRequestHeaders(),
+ "Navigation didn't use the prefetched response!");
+ }
+ else{
+ assert_not_prefetched(await agent.getRequestHeaders(),
+ "Navigation used the prefetched response!");
+ }
+ }, description);
+ }
+
+ // Test inputs:
+ // - description: a description of the test.
+ // - no-vary-search: No-Vary-Search header value for the response.
+ // - prefetch-query: added to query part of prefetch-executor when prefetching
+ // - navigate-query: added to query part of prefetch-executor when navigating
+ // - shouldUsePrefetch: if the test case expects the prefetched entry to be
+ // used or not.
+ [{description:"Use prefetched response as query parameter b has the same value.",
+ noVarySearch: 'params=("a")',
+ prefetchQuery: "a=2&b=3",
+ navigateQuery: "b=3",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use prefetched response as query parameter b has different value.",
+ noVarySearch: 'params("a")',
+ prefetchQuery: "a=2&b=3",
+ navigateQuery: "b=2",
+ shouldUsePrefetch: false},
+
+ {description:"Use prefetched response as the URLs do not vary by a and b.",
+ noVarySearch: 'params=("a" "b")',
+ prefetchQuery: "a=2&b=3",
+ navigateQuery: "b=2",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as the URLs do not vary on any query parameters.",
+ noVarySearch: "params",
+ prefetchQuery: "a=2&b=3",
+ navigateQuery: "b=4&c=5",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as the URLs do not vary on any query parameters.",
+ noVarySearch: "params",
+ prefetchQuery: "",
+ navigateQuery: "b=4&c=5",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use prefetched response as the URLs have different value for c.",
+ noVarySearch: "key-order",
+ prefetchQuery: "c=4&b=3&a=2",
+ navigateQuery: "a=2&c=5&b=3",
+ shouldUsePrefetch: false},
+
+ {description:"Don't use prefetched response as the URLs have the values in different order for a.",
+ noVarySearch: "key-order",
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5&b=3",
+ navigateQuery: "d=6&a=4&b=5&b=3&c=5&a=3",
+ shouldUsePrefetch: false},
+
+ {description:"Use prefetched response as the URLs have the same values for a.",
+ noVarySearch: "key-order",
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5&b=3",
+ navigateQuery: "d=6&a=3&b=5&b=3&c=5&a=4",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as the URLs have the same values for a.",
+ noVarySearch: "key-order=?1",
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5&b=3",
+ navigateQuery: "d=6&a=3&b=5&b=3&c=5&a=4",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use prefetched response as key-order is set to false and the URLs are not identical.",
+ noVarySearch: "key-order=?0",
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5&b=3",
+ navigateQuery: "d=6&a=3&b=5&b=3&c=5&a=4",
+ shouldUsePrefetch: false},
+
+ {description:"Use prefetched response as query parameter c can be ignored.",
+ noVarySearch: 'params=("c")',
+ prefetchQuery: "a=2&b=2&c=5",
+ navigateQuery: "a=2&c=3&b=2",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as query parameter a can be ignored.",
+ noVarySearch: 'params=("a")',
+ prefetchQuery: "a=2",
+ navigateQuery: "",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as query parameter a can be ignored.",
+ noVarySearch: 'params=("a")',
+ prefetchQuery: "",
+ navigateQuery: "a=2",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as all query parameters except c can be ignored.",
+ noVarySearch: 'params, except=("c")',
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as all query parameters except c can be ignored." +
+ " Only the last except matters.",
+ noVarySearch: 'params, except=("b"), except=("c")',
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use prefetched response as even though all query parameters" +
+ " except c can be ignored, c has different value.",
+ noVarySearch: 'params, except=("c")',
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=5",
+ shouldUsePrefetch: false},
+
+ {description:"Use prefetched response as even though all query parameters" +
+ " except c and d can be ignored, c value matches and d value matches.",
+ noVarySearch: 'params, except=("c" "d")',
+ prefetchQuery: "b=5&a=3&d=6&c=5",
+ navigateQuery: "d=6&a=1&b=2&c=5",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as even though all query parameters except" +
+ " c and d can be ignored, c value matches and d value matches." +
+ " Some query parameters to be ignored appear multiple times in the query.",
+ noVarySearch: 'params, except=("c" "d")',
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5",
+ navigateQuery: "d=6&a=1&a=2&b=2&b=3&c=5",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as all query parameters except c can be ignored." +
+ " Allow extension via parameters.",
+ noVarySearch: 'params, except=("c";unknown)',
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as query parameter c can be ignored." +
+ " Allow extension via parameters.",
+ noVarySearch: 'params=("c";unknown)',
+ prefetchQuery: "a=2&b=2&c=5",
+ navigateQuery: "a=2&c=3&b=2",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as the URLs have the values in different order for a." +
+ " Allow extension via parameters.",
+ noVarySearch: "key-order;unknown",
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5&b=3",
+ navigateQuery: "d=6&a=3&b=5&b=3&c=5&a=4",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as the URLs do not vary on any query parameters." +
+ " Allow extension via parameters.",
+ noVarySearch: "params;unknown",
+ prefetchQuery: "",
+ navigateQuery: "b=4&c=5",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as all query parameters except c can be ignored." +
+ " Allow extension via parameters.",
+ noVarySearch: 'params;unknown, except=("c");unknown',
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use the prefetched URL. Empty No-Vary-Search means default URL variance." +
+ " The prefetched and the navigated URLs have to be the same.",
+ noVarySearch: "",
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: false},
+
+ {description:"Use the prefetched URL. Empty No-Vary-Search means default URL variance." +
+ " The prefetched and the navigated URLs have to be the same.",
+ noVarySearch: "",
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "b=5&a=3&d=6&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Use the prefetched URL. Empty No-Vary-Search means default URL variance." +
+ " The prefetched and the navigated URLs have to be the same.",
+ noVarySearch: "",
+ prefetchQuery: "",
+ navigateQuery: "",
+ shouldUsePrefetch: true},
+
+ {description:"Use the prefetched URL. Non-ASCII key - 2 UTF-8 code units." +
+ " Don't vary the response on the non-ASCII key.",
+ noVarySearch: 'params=("%C2%A2")',
+ prefetchQuery: "¢=3",
+ navigateQuery: "¢=4",
+ shouldUsePrefetch: true},
+
+ {description:"Use the prefetched URL. Non-ASCII key - 2 UTF-8 code units." +
+ " Don't vary the response on the non-ASCII key.",
+ noVarySearch: 'params=("%C2%A2")',
+ prefetchQuery: "a=2&¢=3",
+ navigateQuery: "¢=4&a=2",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use the prefetched URL. Non-ASCII key - 2 UTF-8 code units." +
+ " Vary the response on the non-ASCII key.",
+ noVarySearch: 'params, except=("%C2%A2")',
+ prefetchQuery: "¢=3",
+ navigateQuery: "¢=4",
+ shouldUsePrefetch: false},
+
+ {description:"Use the prefetched URL. Non-ASCII key - 2 UTF-8 code units." +
+ " Vary the response on the non-ASCII key.",
+ noVarySearch: 'params, except=("%C2%A2")',
+ prefetchQuery: "¢=3&a=4",
+ navigateQuery: "a=5&¢=3",
+ shouldUsePrefetch: true},
+
+ ].forEach(({description, noVarySearch, prefetchQuery, navigateQuery, shouldUsePrefetch}) => {
+ subsetTest(prefetch_no_vary_search_test,
+ description, noVarySearch, prefetchQuery, navigateQuery,
+ shouldUsePrefetch);
+ });
+
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/out-of-document-rule-set.https.html b/testing/web-platform/tests/speculation-rules/prefetch/out-of-document-rule-set.https.html
new file mode 100644
index 0000000000..9f2c311715
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/out-of-document-rule-set.https.html
@@ -0,0 +1,152 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/subset-tests-by-key.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<meta name="variant" content="?include=BaseCase">
+<meta name="variant" content="?include=FollowRedirect">
+<meta name="variant" content="?include=RelativeUrlForSpeculationRulesSet">
+<meta name="variant" content="?include=RelativeUrlForCandidate">
+<meta name="variant" content="?include=UseNonUTF8EncodingForSpeculationRulesSet">
+<meta name="variant" content="?include=FailCORS">
+<meta name="variant" content="?include=FailToParseSpeculationRulesHeader">
+<meta name="variant" content="?include=InnerListInSpeculationRulesHeader">
+<meta name="variant" content="?include=EmptyRuleSet">
+<meta name="variant" content="?include=FailToParseRuleSet">
+<meta name="variant" content="?include=InvalidUrlForSpeculationRulesSet">
+<meta name="variant" content="?include=StatusCode199">
+<meta name="variant" content="?include=StatusCode404">
+<meta name="variant" content="?include=InvalidMimeType">
+
+<script>
+ async function runSpeculationRulesFetchTest(t, options) {
+ options = {
+ // Whether a prefetch is expected to succeed.
+ shouldPrefetch: true,
+ // Status code to be returned in the response.
+ status: 200,
+ // Whether a redirect must be followed to reach the rule set.
+ redirect: false,
+ // Whether to use relative URLs for the candidates in the rule set.
+ useRelativeUrlForCandidate: false,
+ // Whether to use relative URL for the rule set in SpeculationRules header.
+ useRelativeUrlForSpeculationRulesSet: false,
+ // Whether to use UTF-8 encoding for the rule set.
+ useUtf8EncodingForSpeculationRulesSet: true,
+ // Whether to force the response to cause a CORS failure.
+ failCors: false,
+ // Whether to use a valid SpeculationRules header format.
+ useValidSpeculationRulesHeaderValue: true,
+ // Whether to use an inner list of URLS in SpeculationRules header.
+ useInnerListInSpeculationRulesHeaderValue: false,
+ // Whether to return an empty response.
+ useEmptySpeculationRulesSet: false,
+ // Wheter to return a rule set with valid JSON format
+ useValidJsonForSpeculationRulesSet: true,
+ // Wheter to use a valid URL for the rule set in SpeculationRules header.
+ useValidUrlForSpeculationRulesSet: true,
+ // Wheter to use the valid "application/speculationrules-json" MIME type for the rule set.
+ useValidMimeTypeForSpeculationRulesSet: true,
+ ...options
+ };
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported.");
+
+ let page = 2;
+ let uuid = token();
+ let executor_url = new URL(`executor.sub.html`, SR_PREFETCH_UTILS_URL).toString();
+ if (options.useRelativeUrlForCandidate) {
+ executor_url = `executor.sub.html`;
+ }
+ let speculation_rule_set_url = `ruleset.py?url=${executor_url}&uuid=${uuid}&page=${page}&status=${options.status}&valid_mime=${options.useValidMimeTypeForSpeculationRulesSet}&valid_json=${options.useValidJsonForSpeculationRulesSet}&empty_json=${options.useEmptySpeculationRulesSet}&fail_cors=${options.failCors}&valid_encoding=${options.useUtf8EncodingForSpeculationRulesSet}&redirect=${options.redirect}`;
+ if (!options.useRelativeUrlForSpeculationRulesSet) {
+ let base_url = new URL(SR_PREFETCH_UTILS_URL);
+ base_url.hostname = PREFETCH_PROXY_BYPASS_HOST;
+ speculation_rule_set_url = new URL(speculation_rule_set_url, base_url).toString();
+ }
+ if (!options.useValidUrlForSpeculationRulesSet) {
+ speculation_rule_set_url = "http://:80/";
+ }
+
+ let speculation_rules_header = `header(Speculation-Rules,"${speculation_rule_set_url}")`;
+ if (!options.useValidSpeculationRulesHeaderValue) {
+ speculation_rules_header = `header(Speculation-Rules, x y z)`;
+ }
+ else if (options.useInnerListInSpeculationRulesHeaderValue) {
+ speculation_rules_header = `header(Speculation-Rules, \\("${speculation_rule_set_url}" "xyz.com/rule-set.json"\\))`;
+ }
+
+ let agent = await spawnWindow(t, {pipe: speculation_rules_header}, uuid);
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ // Passing non-ascii character '÷' as part of the next URL to check if we always decode the speculation rules set using utf-8 or not. This character is encoded differently in utf-8 and windows-1250
+ let nextUrl = agent.getExecutorURL({ page, str: decodeURIComponent('%C3%B7')});
+ await agent.navigate(nextUrl);
+
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ let test_case_desc = JSON.stringify(options);
+ if (options.shouldPrefetch)
+ assert_prefetched(await agent.getRequestHeaders(), `Prefetch should work for request ${test_case_desc}.`);
+ else
+ assert_not_prefetched(await agent.getRequestHeaders(), `Prefetch should not work for request ${test_case_desc}.`);
+ }
+
+ subsetTestByKey('BaseCase', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {});
+ }, "Base case.");
+
+ subsetTestByKey('FollowRedirect', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {redirect: true});
+ }, "It should follow redirects and fetch the speculation rules set.");
+
+ subsetTestByKey('RelativeUrlForSpeculationRulesSet', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {useRelativeUrlForSpeculationRulesSet: true});
+ }, "It should fetch a speculation rules set using its relative URL.");
+
+ subsetTestByKey('RelativeUrlForCandidate', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {useRelativeUrlForCandidate: true, shouldPrefetch: false});
+ }, "It should resolve the relative candidate URLs in the speculation rules set based on the speculation rules set's URL");
+
+ subsetTestByKey('UseNonUTF8EncodingForSpeculationRulesSet', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {useUtf8EncodingForSpeculationRulesSet: false, shouldPrefetch: false});
+ }, "The speculation rules set should always be encoded using UTF-8.");
+
+ subsetTestByKey('FailCORS', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {failCors: true, shouldPrefetch: false});
+ }, "It should reject the speculation rules set if CORS fails.");
+
+ subsetTestByKey('FailToParseSpeculationRulesHeader', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {useValidSpeculationRulesHeaderValue: false, shouldPrefetch: false});
+ }, "It should reject the speculation rules set if it fails to parse the SpeculationRules header.");
+
+ subsetTestByKey('InnerListInSpeculationRulesHeader', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {useInnerListInSpeculationRulesHeaderValue: true, shouldPrefetch: false});
+ }, "It should reject the speculation rules passed as inner list in the SpeculationRules header.");
+
+ subsetTestByKey('EmptyRuleSet', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {useEmptySpeculationRulesSet: true, shouldPrefetch: false});
+ }, "It should reject an empty speculation rules set.");
+
+ subsetTestByKey('FailToParseRuleSet', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {useValidJsonForSpeculationRulesSet: false, shouldPrefetch: false});
+ }, "It should reject the speculation rules set if it cannot parse it.");
+
+ subsetTestByKey('InvalidUrlForSpeculationRulesSet', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {useValidUrlForSpeculationRulesSet: false, shouldPrefetch: false});
+ }, "It should reject the speculation rules set with invalid URL.");
+
+ subsetTestByKey('StatusCode199', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {status: 199, shouldPrefetch: false});
+ }, "It should reject the speculation rules set with unsuccessful status code.");
+
+ subsetTestByKey('StatusCode404', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {status: 404, shouldPrefetch: false});
+ }, "It should reject the speculation rules set with unsuccessful status code.");
+
+ subsetTestByKey('InvalidMimeType', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {useValidMimeTypeForSpeculationRulesSet: false, shouldPrefetch: false});
+ }, "It should reject the speculation rules set with invalid MIME type.");
+
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/prefetch-single.https.html b/testing/web-platform/tests/speculation-rules/prefetch/prefetch-single.https.html
new file mode 100644
index 0000000000..42f75d0c29
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/prefetch-single.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<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/utils.sub.js"></script>
+<meta name="variant" content="?from_protocol=http&to_protocol=http">
+<meta name="variant" content="?from_protocol=http&to_protocol=https">
+<meta name="variant" content="?from_protocol=https&to_protocol=http">
+<meta name="variant" content="?from_protocol=https&to_protocol=https">
+<script>
+ // This is split across four test variants due to the test timeouts.
+ let { from_protocol, to_protocol } = Object.fromEntries(new URLSearchParams(location.search));
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let agent = await spawnWindow(t, { protocol: from_protocol });
+ let nextUrl = agent.getExecutorURL({ protocol: to_protocol, page: 2 });
+ await agent.forceSinglePrefetch(nextUrl);
+ await agent.navigate(nextUrl);
+
+ if (to_protocol == "https") {
+ assert_prefetched(await agent.getRequestHeaders(), "Prefetch should work for HTTPS urls.");
+ } else {
+ assert_not_prefetched(await agent.getRequestHeaders(), "Prefetch should not work for HTTP urls.");
+ }
+ }, `test single ${to_protocol} url prefetch from a ${from_protocol} url`);
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/prefetch-status.https.html b/testing/web-platform/tests/speculation-rules/prefetch/prefetch-status.https.html
new file mode 100644
index 0000000000..6835a55ee9
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/prefetch-status.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<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/utils.sub.js"></script>
+
+<meta name="variant" content="?status=200&should_prefetch=true">
+<meta name="variant" content="?status=250&should_prefetch=true">
+<meta name="variant" content="?status=299&should_prefetch=true">
+<meta name="variant" content="?status=400&should_prefetch=false">
+<meta name="variant" content="?status=500&should_prefetch=false">
+
+<script>
+ // This is split across four test variants due to the test timeouts.
+ let { status, should_prefetch } = Object.fromEntries(new URLSearchParams(location.search));
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let agent = await spawnWindow(t);
+ let nextUrl = agent.getExecutorURL({ page: 2, pipe: `status(${status})` });
+ await agent.forceSinglePrefetch(nextUrl);
+ await agent.navigate(nextUrl);
+
+ if (should_prefetch == 'true')
+ assert_prefetched(await agent.getRequestHeaders(), `Prefetch should work for request status:${status}.`);
+ else
+ assert_not_prefetched(await agent.getRequestHeaders(), `Prefetch should not work for request statue:${status}.`);
+ }, "Check that only prefetched requests with status in 200-299 range are used.");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/prefetch-traverse-reload.sub.html b/testing/web-platform/tests/speculation-rules/prefetch/prefetch-traverse-reload.sub.html
new file mode 100644
index 0000000000..ec6a7cd926
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/prefetch-traverse-reload.sub.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<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="/websockets/constants.sub.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script>
+promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let agent = await spawnWindow(t, { protocol: 'https', pipe: 'header(Cache-Control, no-store)' });
+ let previousUrl = await agent.execute_script(() => location.href);
+ await agent.execute_script(async () => {
+ window.preventBfcache = new WebSocket('wss://{{ports[wss][0]}}/echo');
+ });
+
+ let nextUrl = agent.getExecutorURL({ protocol: 'https', page: 2 });
+ await agent.navigate(nextUrl);
+
+ await agent.forceSinglePrefetch(previousUrl);
+ await agent.execute_script(() => {
+ window.executor.suspend(() => history.go(-1));
+ });
+
+ assert_equals(previousUrl, await agent.execute_script(() => location.href));
+ assert_prefetched(await agent.getRequestHeaders(), "traversal should use prefetch");
+}, "prefetches can be used for traversal navigations");
+
+promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let agent = await spawnWindow(t, { protocol: 'https', pipe: 'header(Cache-Control, no-store)' });
+ let previousUrl = await agent.execute_script(() => location.href);
+ await agent.execute_script(async () => {
+ window.preventBfcache = new WebSocket('wss://{{ports[wss][0]}}/echo');
+ });
+
+ let nextUrl = agent.getExecutorURL({ protocol: 'https', page: 2 });
+ await agent.navigate(nextUrl);
+
+ await agent.forceSinglePrefetch(previousUrl);
+ // In https://html.spec.whatwg.org/multipage/nav-history-apis.html#delta-traverse,
+ // `sourceDocument` is `History`'s relevant global object's associated
+ // Document. In this case, it's `iframe.contentDocument`, and thus the
+ // prefetch from `win`'s Document (iframe's parent Document) isn't used.
+ await agent.execute_script(() => {
+ window.executor.suspend(() => {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.contentWindow.history.go(-1);
+ });
+ });
+
+ assert_equals(previousUrl, await agent.execute_script(() => location.href));
+ assert_not_prefetched(await agent.getRequestHeaders(),
+ "prefetch from different Document should not be used");
+}, "History's Document is used for traversal navigations");
+
+promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let agent = await spawnWindow(t, { protocol: 'https', pipe: 'header(Cache-Control, no-store)' });
+ let previousUrl = await agent.execute_script(() => location.href);
+ await agent.forceSinglePrefetch(previousUrl);
+ await agent.execute_script(() => {
+ window.executor.suspend(() => location.reload());
+ });
+
+ assert_equals(previousUrl, await agent.execute_script(() => location.href));
+ assert_prefetched(await agent.getRequestHeaders(), "reload should use prefetch");
+}, "prefetches can be used for reload navigations");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/redirect-url.https.html b/testing/web-platform/tests/speculation-rules/prefetch/redirect-url.https.html
new file mode 100644
index 0000000000..07db405dc3
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/redirect-url.https.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<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/utils.sub.js"></script>
+
+<script>
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let url = getRedirectUrl();
+ insertSpeculationRules({ prefetch: [{ source: 'list', urls: [url] }] });
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url), 1, "redirected url should be prefetched");
+ }, "browser should be able to prefetch redirected urls");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-from-rules.https.html b/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-from-rules.https.html
new file mode 100644
index 0000000000..bbb0343509
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-from-rules.https.html
@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<title>Prefetch with the referrer policy specified in speculation rules</title>
+
+<!--Split test cases due to the use of timeouts in speculation rules test utilities.-->
+<meta name="variant" content="?1-1">
+<meta name="variant" content="?2-2">
+<meta name="variant" content="?3-3">
+<meta name="variant" content="?4-4">
+<meta name="variant" content="?5-5">
+<meta name="variant" content="?6-6">
+<meta name="variant" content="?7-last">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/subset-tests.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<script>
+"use strict";
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("strict-origin-when-cross-origin");
+ const expectedReferrer = agent.getExecutorURL().origin + "/";
+
+ const nextURL = agent.getExecutorURL({ page: 2 });
+ await agent.forceSinglePrefetch(nextURL, { referrer_policy: "strict-origin" });
+ await agent.navigate(nextURL);
+
+ const headers = await agent.getRequestHeaders();
+ assert_prefetched(headers, "must be prefetched");
+ assert_equals(headers.referer, expectedReferrer, "must send the origin as the referrer");
+}, 'with "strict-origin" referrer policy in rule set overriding "strict-origin-when-cross-origin" of referring page');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ const next_url = agent.getExecutorURL({ page: 2 });
+ await agent.execute_script((url) => {
+ const a = addLink(url);
+ a.referrerPolicy = 'no-referrer';
+ insertDocumentRule(undefined, { referrer_policy: 'strict-origin' });
+ }, [next_url]);
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ await agent.navigate(next_url);
+
+ const headers = await agent.getRequestHeaders();
+ assert_prefetched(headers, 'must be prefetched');
+ const expected_referrer = next_url.origin + '/';
+ assert_equals(headers.referer, expected_referrer, 'must send the origin as the referrer');
+}, 'with "strict-origin" referrer policy in rule set override "no-referrer" of link');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("unsafe-url");
+
+ const nextURL = agent.getExecutorURL({ hostname: PREFETCH_PROXY_BYPASS_HOST, page: 2 });
+ await agent.forceSinglePrefetch(
+ nextURL, { referrer_policy: "no-referrer", requires: ["anonymous-client-ip-when-cross-origin"] });
+ await agent.navigate(nextURL);
+
+ // This referring page's referrer policy would not be eligible for
+ // cross-site prefetching, but setting a sufficiently strict policy in the
+ // rule allows for prefetching.
+ const headers = await agent.getRequestHeaders();
+ assert_prefetched(headers, "must be prefetched");
+ assert_equals(headers.referer, '', "must send no referrer");
+}, 'with "no-referrer" referrer policy in rule set overriding "unsafe-url" of cross-site referring page');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("strict-origin-when-cross-origin");
+
+ const nextURL = agent.getExecutorURL({ page: 2 });
+ await agent.forceSinglePrefetch(nextURL, { referrer_policy: "no-referrrrrrrer" });
+ await agent.navigate(nextURL);
+
+ const headers = await agent.getRequestHeaders();
+ assert_not_prefetched(headers, "must not be prefetched");
+}, 'unrecognized policies invalidate the rule');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("strict-origin-when-cross-origin");
+
+ const nextURL = agent.getExecutorURL({ page: 2 });
+ await agent.forceSinglePrefetch(nextURL, { referrer_policy: "never" });
+ await agent.navigate(nextURL);
+
+ const headers = await agent.getRequestHeaders();
+ assert_not_prefetched(headers, "must not be prefetched");
+}, 'treat legacy referrer policy values as invalid');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("strict-origin");
+ const expectedReferrer = agent.getExecutorURL().origin + "/";
+
+ const nextURL = agent.getExecutorURL({ hostname: PREFETCH_PROXY_BYPASS_HOST, page: 2 });
+ await agent.forceSinglePrefetch(
+ nextURL, { referrer_policy: "unsafe-url", requires: ["anonymous-client-ip-when-cross-origin"] });
+ await agent.navigate(nextURL);
+
+ // This referring page's referrer policy would normally make it eligible for
+ // cross-site prefetching, but setting an unacceptable policy in the rule
+ // makes it ineligible.
+ const headers = await agent.getRequestHeaders();
+ assert_not_prefetched(headers, "must not be prefetched");
+ assert_equals(headers.referer, expectedReferrer, "must send the origin as the referrer");
+}, 'with "unsafe-url" referrer policy in rule set overriding "strict-origin" of cross-site referring page');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("strict-origin");
+ const expectedReferrer = agent.getExecutorURL().origin + "/";
+
+ const nextURL = agent.getExecutorURL({ page: 2 });
+ // The empty string is a valid value for "referrer_policy" and will be
+ // treated as if the key were omitted.
+ await agent.forceSinglePrefetch(nextURL, { referrer_policy: "" });
+ await agent.navigate(nextURL);
+
+ const headers = await agent.getRequestHeaders();
+ assert_prefetched(headers, "must be prefetched");
+ assert_equals(headers.referer, expectedReferrer, "must send the origin as the referrer");
+}, 'with empty string referrer policy in rule set defaulting to "strict-origin" of referring page');
+
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-not-accepted.https.html b/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-not-accepted.https.html
new file mode 100644
index 0000000000..d7c003b3ca
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-not-accepted.https.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<title>Prefetch attempts with an unacceptable referrer policy</title>
+
+<!--Split test cases due to the use of timeouts in speculation rules test utilities.-->
+<meta name="variant" content="?1-1">
+<meta name="variant" content="?2-last">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/subset-tests.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<script>
+"use strict";
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("unsafe-url");
+ const expectedReferrer = agent.getExecutorURL().href;
+
+ const nextURL = agent.getExecutorURL({ page: 2 });
+ await agent.forceSinglePrefetch(nextURL);
+ await agent.navigate(nextURL);
+
+ const headers = await agent.getRequestHeaders();
+ // The referrer policy restriction does not apply to same-site prefetch.
+ assert_prefetched(headers, "must be prefetched");
+ assert_equals(headers.referer, expectedReferrer, "must send the full URL as the referrer");
+}, 'with "unsafe-url" referrer policy on same-site referring page');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("unsafe-url");
+ const expectedReferrer = agent.getExecutorURL().href;
+
+ const nextURL = agent.getExecutorURL({ hostname: PREFETCH_PROXY_BYPASS_HOST, page: 2 });
+ // This prefetch attempt should be ignored.
+ await agent.forceSinglePrefetch(
+ nextURL, { requires: ["anonymous-client-ip-when-cross-origin"] });
+ await agent.navigate(nextURL);
+
+ const headers = await agent.getRequestHeaders();
+ assert_not_prefetched(headers, "must not be prefetched");
+ assert_equals(headers.referer, expectedReferrer, "must send the full URL as the referrer");
+}, 'with "unsafe-url" referrer policy on cross-site referring page');
+
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy.https.html b/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy.https.html
new file mode 100644
index 0000000000..1987d2e2ff
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy.https.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<title>Prefetch is done with the referring page's referrer policy</title>
+
+<!--Split test cases due to the use of timeouts in speculation rules test utilities.-->
+<meta name="variant" content="?1-1">
+<meta name="variant" content="?2-2">
+<meta name="variant" content="?3-3">
+<meta name="variant" content="?4-last">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/subset-tests.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<script>
+"use strict";
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("strict-origin-when-cross-origin");
+ const expectedReferrer = agent.getExecutorURL().href;
+
+ const nextURL = agent.getExecutorURL({ page: 2 });
+ await agent.forceSinglePrefetch(nextURL);
+ await agent.navigate(nextURL);
+
+ const headers = await agent.getRequestHeaders();
+ assert_prefetched(headers, "must be prefetched");
+ assert_equals(headers.referer, expectedReferrer, "must send the full URL as the referrer");
+}, 'with "strict-origin-when-cross-origin" referrer policy');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("strict-origin");
+ const expectedReferrer = agent.getExecutorURL().origin + "/";
+
+ const nextURL = agent.getExecutorURL({ page: 2 });
+ await agent.forceSinglePrefetch(nextURL);
+ await agent.navigate(nextURL);
+
+ const headers = await agent.getRequestHeaders();
+ assert_prefetched(headers, "must be prefetched");
+ assert_equals(headers.referer, expectedReferrer, "must send the origin as the referrer");
+}, 'with "strict-origin" referrer policy');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("no-referrer");
+
+ const nextURL = agent.getExecutorURL({ page: 2 });
+ await agent.forceSinglePrefetch(nextURL);
+ await agent.navigate(nextURL);
+
+ const headers = await agent.getRequestHeaders();
+ assert_prefetched(headers, "must be prefetched");
+ assert_equals(headers.referer, '', "must send no referrer");
+}, 'with "no-referrer" referrer policy');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("no-referrer");
+
+ const next_url = agent.getExecutorURL({ page: 2 });
+ await agent.execute_script((url) => {
+ const a = addLink(url);
+ a.referrerPolicy = 'strict-origin';
+ insertDocumentRule();
+ }, [next_url]);
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ await agent.navigate(next_url);
+
+ const headers = await agent.getRequestHeaders();
+ const expected_referrer = next_url.origin + '/';
+ assert_prefetched(headers, 'must be prefetched');
+ assert_equals(headers.referer, expected_referrer);
+}, 'with "strict-origin" link referrer policy overriding "no-referrer" of referring page');
+
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/authenticate.py b/testing/web-platform/tests/speculation-rules/prefetch/resources/authenticate.py
new file mode 100644
index 0000000000..037a7c144e
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/authenticate.py
@@ -0,0 +1,32 @@
+
+def main(request, response):
+ def fmt(x):
+ return f'"{x.decode("utf-8")}"' if x is not None else "undefined"
+
+ purpose = request.headers.get("Purpose", b"").decode("utf-8")
+ sec_purpose = request.headers.get("Sec-Purpose", b"").decode("utf-8")
+
+ headers = [(b"Content-Type", b"text/html"), (b'WWW-Authenticate', 'Basic')]
+ status = 200 if request.auth.username is not None or sec_purpose.startswith(
+ "prefetch") else 401
+
+ content = f'''
+ <!DOCTYPE html>
+ <script src="/common/dispatcher/dispatcher.js"></script>
+ <script src="utils.sub.js"></script>
+ <script>
+ window.requestHeaders = {{
+ purpose: "{purpose}",
+ sec_purpose: "{sec_purpose}"
+ }};
+
+ window.requestCredentials = {{
+ username: {fmt(request.auth.username)},
+ password: {fmt(request.auth.password)}
+ }};
+
+ const uuid = new URLSearchParams(location.search).get('uuid');
+ window.executor = new Executor(uuid);
+ </script>
+ '''
+ return status, headers, content
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/cookies.py b/testing/web-platform/tests/speculation-rules/prefetch/resources/cookies.py
new file mode 100644
index 0000000000..3c2299aa3a
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/cookies.py
@@ -0,0 +1,41 @@
+
+def main(request, response):
+ def get_cookie(key):
+ key = key.encode("utf-8")
+ if key in request.cookies:
+ return f'"{request.cookies[key].value.decode("utf-8")}"'
+ else:
+ return "undefined"
+
+ purpose = request.headers.get("Purpose", b"").decode("utf-8")
+ sec_purpose = request.headers.get("Sec-Purpose", b"").decode("utf-8")
+
+ cookie_count = int(
+ request.cookies[b"count"].value) if b"count" in request.cookies else 0
+ response.set_cookie("count", f"{cookie_count+1}",
+ secure=True, samesite="None")
+ response.set_cookie(
+ "type", "prefetch" if sec_purpose.startswith("prefetch") else "navigate")
+
+ headers = [(b"Content-Type", b"text/html")]
+
+ content = f'''
+ <!DOCTYPE html>
+ <script src="/common/dispatcher/dispatcher.js"></script>
+ <script src="utils.sub.js"></script>
+ <script>
+ window.requestHeaders = {{
+ purpose: "{purpose}",
+ sec_purpose: "{sec_purpose}"
+ }};
+
+ window.requestCookies = {{
+ count: {get_cookie("count")},
+ type: {get_cookie("type")}
+ }};
+
+ const uuid = new URLSearchParams(location.search).get('uuid');
+ window.executor = new Executor(uuid);
+ </script>
+ '''
+ return headers, content
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/executor.sub.html b/testing/web-platform/tests/speculation-rules/prefetch/resources/executor.sub.html
new file mode 100644
index 0000000000..ba1b3acb0c
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/executor.sub.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="utils.sub.js"></script>
+<script>
+window.requestHeaders = {
+ purpose: "{{header_or_default(Purpose, )}}",
+ sec_purpose: "{{header_or_default(Sec-Purpose, )}}",
+ referer: "{{header_or_default(Referer, )}}",
+};
+
+const uuid = new URLSearchParams(location.search).get('uuid');
+window.executor = new Executor(uuid);
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch.py b/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch.py
new file mode 100644
index 0000000000..4a0a7a3602
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch.py
@@ -0,0 +1,16 @@
+from wptserve.handlers import json_handler
+
+@json_handler
+def main(request, response):
+ uuid = request.GET[b"uuid"]
+ prefetch = request.headers.get(
+ "Sec-Purpose", b"").decode("utf-8").startswith("prefetch")
+
+ n = request.server.stash.take(uuid)
+ if n is None:
+ n = 0
+ if prefetch:
+ n += 1
+ request.server.stash.put(uuid, n)
+
+ return n
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch_nvs_hint.py b/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch_nvs_hint.py
new file mode 100644
index 0000000000..09c5d2eb73
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch_nvs_hint.py
@@ -0,0 +1,34 @@
+import time
+
+def main(request, response):
+ uuid = request.GET[b"uuid"]
+ prefetch = request.headers.get(
+ "Sec-Purpose", b"").decode("utf-8").startswith("prefetch")
+ if b"unblock" in request.GET:
+ request.server.stash.put(uuid, 0)
+ return ''
+
+ if b"nvs_header" in request.GET:
+ nvs_header = request.GET[b"nvs_header"]
+ response.headers.set("No-Vary-Search", nvs_header)
+
+ if prefetch:
+ nvswait = None
+ while nvswait is None:
+ time.sleep(0.1)
+ nvswait = request.server.stash.take(uuid)
+
+ content = (f'<!DOCTYPE html>\n'
+ f'<script src="/common/dispatcher/dispatcher.js"></script>\n'
+ f'<script src="utils.sub.js"></script>\n'
+ f'<script>\n'
+ f' window.requestHeaders = {{\n'
+ f' purpose: "{request.headers.get("Purpose", b"").decode("utf-8")}",\n'
+ f' sec_purpose: "{request.headers.get("Sec-Purpose", b"").decode("utf-8")}",\n'
+ f' referer: "{request.headers.get("Referer", b"").decode("utf-8")}",\n'
+ f' }};\n'
+ f' const uuid = new URLSearchParams(location.search).get("uuid");\n'
+ f' window.executor = new Executor(uuid);\n'
+ f'</script>\n')
+
+ return content
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/redirect.py b/testing/web-platform/tests/speculation-rules/prefetch/resources/redirect.py
new file mode 100644
index 0000000000..de7a4af987
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/redirect.py
@@ -0,0 +1,3 @@
+def main(request, response):
+ new_url = request.url.replace("redirect", "prefetch").encode("utf-8")
+ return 301, [(b"Location", new_url)], b""
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/ruleset.py b/testing/web-platform/tests/speculation-rules/prefetch/resources/ruleset.py
new file mode 100644
index 0000000000..97de1cc1a0
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/ruleset.py
@@ -0,0 +1,49 @@
+def main(request, response):
+ url = request.GET[b"url"].decode("utf-8")
+ uuid = request.GET[b"uuid"].decode("utf-8")
+ page = request.GET[b"page"].decode("utf-8")
+ valid_json = request.GET[b"valid_json"].decode("utf-8")
+ empty_json = request.GET[b"empty_json"].decode("utf-8")
+ fail_cors = request.GET[b"fail_cors"].decode("utf-8")
+ valid_encoding = request.GET[b"valid_encoding"].decode("utf-8")
+ redirect = request.GET[b"redirect"].decode("utf-8")
+ sec_fetch_dest = request.headers[b"Sec-Fetch-Dest"].decode(
+ "utf-8").lower() if b"Sec-Fetch-Dest" in request.headers else None
+ content_type = b"application/speculationrules+json" if request.GET[
+ b"valid_mime"].decode("utf-8") == "true" else b"application/json"
+ status = int(request.GET[b"status"])
+
+ if redirect == "true":
+ new_url = request.url.replace("redirect=true",
+ "redirect=false").encode("utf-8")
+ return 301, [(b"Location", new_url),
+ (b'Access-Control-Allow-Origin', b'*')], b""
+
+ encoding = "utf-8" if valid_encoding == "true" else "windows-1250"
+ content_type += f'; charset={encoding}'.encode('utf-8')
+ strparam = b'\xc3\xb7'.decode('utf-8')
+
+ content = f'''
+ {{
+ "prefetch": [
+ {{
+ "source":"list",
+ "urls":["{url}?uuid={uuid}&page={page}&str={strparam}"],
+ "requires":["anonymous-client-ip-when-cross-origin"]
+ }}
+ ]
+ }}
+ '''
+ if empty_json == "true":
+ content = ""
+ elif valid_json != "true":
+ content = "invalid json"
+ elif sec_fetch_dest is None or sec_fetch_dest != "script":
+ content = "normal document"
+
+ headers = [(b"Content-Type", content_type)]
+ if fail_cors != "true":
+ origin = request.headers[
+ b"Origin"] if b"Origin" in request.headers else b'*'
+ headers.append((b'Access-Control-Allow-Origin', origin))
+ return status, headers, content.encode(encoding)
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/sw.js b/testing/web-platform/tests/speculation-rules/prefetch/resources/sw.js
new file mode 100644
index 0000000000..db774f9d5b
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/sw.js
@@ -0,0 +1 @@
+self.addEventListener('fetch', () => event.respondWith(fetch(event.request)));
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/utils.sub.js b/testing/web-platform/tests/speculation-rules/prefetch/resources/utils.sub.js
new file mode 100644
index 0000000000..9b3b630733
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/utils.sub.js
@@ -0,0 +1,176 @@
+/**
+ * Utilities for initiating prefetch via speculation rules.
+ */
+
+// Resolved URL to find this script.
+const SR_PREFETCH_UTILS_URL = new URL(document.currentScript.src, document.baseURI);
+// Hostname for cross origin urls.
+const PREFETCH_PROXY_BYPASS_HOST = "{{hosts[alt][]}}";
+
+class PrefetchAgent extends RemoteContext {
+ constructor(uuid, t) {
+ super(uuid);
+ this.t = t;
+ }
+
+ getExecutorURL(options = {}) {
+ let {hostname, username, password, protocol, executor, ...extra} = options;
+ let params = new URLSearchParams({uuid: this.context_id, ...extra});
+ if(executor === undefined) {
+ executor = "executor.sub.html";
+ }
+ let url = new URL(`${executor}?${params}`, SR_PREFETCH_UTILS_URL);
+ if(hostname !== undefined) {
+ url.hostname = hostname;
+ }
+ if(username !== undefined) {
+ url.username = username;
+ }
+ if(password !== undefined) {
+ url.password = password;
+ }
+ if(protocol !== undefined) {
+ url.protocol = protocol;
+ url.port = protocol === "https" ? "{{ports[https][0]}}" : "{{ports[http][0]}}";
+ }
+ return url;
+ }
+
+ // Requests prefetch via speculation rules.
+ //
+ // In the future, this should also use browser hooks to force the prefetch to
+ // occur despite heuristic matching, etc., and await the completion of the
+ // prefetch.
+ async forceSinglePrefetch(url, extra = {}) {
+ await this.execute_script((url, extra) => {
+ insertSpeculationRules({ prefetch: [{source: 'list', urls: [url], ...extra}] });
+ }, [url, extra]);
+ return new Promise(resolve => this.t.step_timeout(resolve, 2000));
+ }
+
+ async navigate(url) {
+ await this.execute_script((url) => {
+ window.executor.suspend(() => {
+ location.href = url;
+ });
+ }, [url]);
+ url.username = '';
+ url.password = '';
+ assert_equals(
+ await this.execute_script(() => location.href),
+ url.toString(),
+ "expected navigation to reach destination URL");
+ await this.execute_script(() => {});
+ }
+
+ async getRequestHeaders() {
+ return this.execute_script(() => requestHeaders);
+ }
+
+ async getResponseCookies() {
+ return this.execute_script(() => {
+ let cookie = {};
+ document.cookie.split(/\s*;\s*/).forEach((kv)=>{
+ let [key, value] = kv.split(/\s*=\s*/);
+ cookie[key] = value;
+ });
+ return cookie;
+ });
+ }
+
+ async getRequestCookies() {
+ return this.execute_script(() => window.requestCookies);
+ }
+
+ async getRequestCredentials() {
+ return this.execute_script(() => window.requestCredentials);
+ }
+
+ async setReferrerPolicy(referrerPolicy) {
+ return this.execute_script(referrerPolicy => {
+ const meta = document.createElement("meta");
+ meta.name = "referrer";
+ meta.content = referrerPolicy;
+ document.head.append(meta);
+ }, [referrerPolicy]);
+ }
+
+ async getDeliveryType(){
+ return this.execute_script(() => {
+ return performance.getEntriesByType("navigation")[0].deliveryType;
+ });
+ }
+}
+
+// Produces a URL with a UUID which will record when it's prefetched.
+// |extra_params| can be specified to add extra search params to the generated
+// URL.
+function getPrefetchUrl(extra_params={}) {
+ let params = new URLSearchParams({ uuid: token(), ...extra_params });
+ return new URL(`prefetch.py?${params}`, SR_PREFETCH_UTILS_URL);
+}
+
+// Produces n URLs with unique UUIDs which will record when they are prefetched.
+function getPrefetchUrlList(n) {
+ return Array.from({ length: n }, () => getPrefetchUrl());
+}
+
+function getRedirectUrl() {
+ let params = new URLSearchParams({uuid: token()});
+ return new URL(`redirect.py?${params}`, SR_PREFETCH_UTILS_URL);
+}
+
+async function isUrlPrefetched(url) {
+ let response = await fetch(url, {redirect: 'follow'});
+ assert_true(response.ok);
+ return response.json();
+}
+
+// Must also include /common/utils.js and /common/dispatcher/dispatcher.js to use this.
+async function spawnWindow(t, options = {}, uuid = token()) {
+ let agent = new PrefetchAgent(uuid, t);
+ let w = window.open(agent.getExecutorURL(options), '_blank', options);
+ t.add_cleanup(() => w.close());
+ return agent;
+}
+
+function insertSpeculationRules(body) {
+ let script = document.createElement('script');
+ script.type = 'speculationrules';
+ script.textContent = JSON.stringify(body);
+ document.head.appendChild(script);
+}
+
+// Creates and appends <a href=|href|> to |insertion point|. If
+// |insertion_point| is not specified, document.body is used.
+function addLink(href, insertion_point=document.body) {
+ const a = document.createElement('a');
+ a.href = href;
+ insertion_point.appendChild(a);
+ return a;
+}
+
+// Inserts a prefetch document rule with |predicate|. |predicate| can be
+// undefined, in which case the default predicate will be used (i.e. all links
+// in document will match).
+function insertDocumentRule(predicate, extra_options={}) {
+ insertSpeculationRules({
+ prefetch: [{
+ source: 'document',
+ eagerness: 'eager',
+ where: predicate,
+ ...extra_options
+ }]
+ });
+}
+
+function assert_prefetched (requestHeaders, description) {
+ assert_in_array(requestHeaders.purpose, ["", "prefetch"], "The vendor-specific header Purpose, if present, must be 'prefetch'.");
+ assert_in_array(requestHeaders.sec_purpose,
+ ["prefetch", "prefetch;anonymous-client-ip"], description);
+}
+
+function assert_not_prefetched (requestHeaders, description){
+ assert_equals(requestHeaders.purpose, "", description);
+ assert_equals(requestHeaders.sec_purpose, "", description);
+}
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/same-origin-cookies.https.html b/testing/web-platform/tests/speculation-rules/prefetch/same-origin-cookies.https.html
new file mode 100644
index 0000000000..1d60a4bee0
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/same-origin-cookies.https.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src='/resources/testdriver-vendor.js'></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script>
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ await test_driver.delete_all_cookies();
+
+ let executor = 'cookies.py';
+ let agent = await spawnWindow(t, { executor });
+ let response_cookies = await agent.getResponseCookies();
+ let request_cookies = await agent.getRequestCookies();
+ assert_equals(request_cookies["count"], undefined);
+ assert_equals(request_cookies["type"], undefined);
+ assert_equals(response_cookies["count"], "1");
+ assert_equals(response_cookies["type"], "navigate");
+
+ let nextUrl = agent.getExecutorURL({ executor, page: 2 });
+ await agent.forceSinglePrefetch(nextUrl);
+ await agent.navigate(nextUrl);
+
+ response_cookies = await agent.getResponseCookies();
+ request_cookies = await agent.getRequestCookies();
+ assert_equals(request_cookies["count"], "1");
+ assert_equals(request_cookies["type"], "navigate");
+ assert_equals(response_cookies["count"], "2");
+ assert_equals(response_cookies["type"], "prefetch");
+
+ assert_prefetched(await agent.getRequestHeaders());
+
+ }, "speculation rules based prefetch should use cookies for same origin urls.");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/user-pass.https.html b/testing/web-platform/tests/speculation-rules/prefetch/user-pass.https.html
new file mode 100644
index 0000000000..94748f1eac
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/user-pass.https.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<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/utils.sub.js"></script>
+<meta name="variant" content="?cross-origin=true">
+<meta name="variant" content="?cross-origin=false">
+<script>
+ let cross_origin = Object.fromEntries(new URLSearchParams(location.search))["cross-origin"] === "true";
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let executor = "authenticate.py";
+ let credentials = { username: "user", password: "pass" };
+ let agent = await spawnWindow(t, { executor, ...credentials });
+ let request_credentials = await agent.getRequestCredentials();
+ assert_equals(request_credentials.username, credentials.username);
+ assert_equals(request_credentials.password, credentials.password);
+
+ let host = cross_origin ? { hostname: PREFETCH_PROXY_BYPASS_HOST } : {};
+ let nextUrl = agent.getExecutorURL({ page: 2, executor, ...host });
+ await agent.forceSinglePrefetch(nextUrl, { requires: ["anonymous-client-ip-when-cross-origin"] });
+ await agent.navigate(nextUrl);
+
+ let requestHeaders = await agent.getRequestHeaders();
+ request_credentials = await agent.getRequestCredentials();
+ if (cross_origin) {
+ assert_equals(request_credentials.username, undefined);
+ assert_equals(request_credentials.password, undefined);
+
+ assert_in_array(requestHeaders.purpose, ["", "prefetch"]);
+ assert_equals(requestHeaders.sec_purpose, "prefetch;anonymous-client-ip");
+ }
+ else {
+ assert_equals(request_credentials.username, credentials.username);
+ assert_equals(request_credentials.password, credentials.password);
+
+ assert_prefetched(await agent.getRequestHeaders());
+ }
+
+ }, "test www-authenticate basic does not forward credentials to cross-origin pages.");
+</script>