summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/speculation-rules/prefetch
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/speculation-rules/prefetch
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/speculation-rules/prefetch')
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/anonymous-client.https.html21
-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.html83
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/document-rules.https.html318
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/duplicate-urls.https.html20
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/fragment.https.html35
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/implicit-source.https.html34
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/initiators-a-element.sub.https.html80
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/initiators-iframe-location-href.sub.https.html56
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/initiators-window-open.sub.https.html69
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/invalid-rules.https.html19
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/multiple-url.https.html22
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-delivery-type.https.html57
-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.html357
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/prefetch-single.https.html315
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/out-of-document-rule-set.https.html154
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/prefetch-single.https.html31
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/prefetch-status.https.html31
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/prefetch-traverse-reload.sub.html71
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/prefetch-uses-cache.sub.https.html69
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/redirect-url.sub.https.html54
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-from-rules.https.html130
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-not-accepted.https.html50
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/referrer-policy.https.html81
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/authenticate.py36
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/cacheable-executor.sub.html13
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/cacheable-executor.sub.html.headers1
-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/executor.sub.html.headers1
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch.py17
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch_nvs_hint.py49
-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.js195
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/same-origin-cookies.https.html74
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/user-pass.https.html46
41 files changed, 2899 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..bf5896b2dd
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/anonymous-client.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.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ promise_test(async t => {
+ 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..a16872fcef
--- /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.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ promise_test(async t => {
+ 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..dc314d22c5
--- /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.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+// Regression test for https://crbug.com/1431804.
+promise_test(async t => {
+ 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..691dfd855e
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/different-initiators.sub.https.html
@@ -0,0 +1,83 @@
+<!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.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+
+<meta name="variant" content="?cross-site-1">
+<meta name="variant" content="?cross-site-2">
+<meta name="variant" content="?same-site">
+
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+// Regression test for https://crbug.com/1423234.
+promise_test(async t => {
+ // 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..805f1cfbc3
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/document-rules.https.html
@@ -0,0 +1,318 @@
+<!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.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">
+<meta name="variant" content="?include=baseURLChangedBySameDocumentNavigation">
+<meta name="variant" content="?include=baseURLChangedByBaseElement">
+<meta name="variant" content="?include=linkToSelfFragment">
+
+<body>
+<script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ subsetTestByKey('defaultPredicate', promise_test, async t => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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 use the shadowRoot as the scoping root when
+ // matching links inside a shadow tree.
+ subsetTestByKey('selectorMatchesInShadowTree', promise_test, async t => {
+ insertDocumentRule({ selector_matches: ':scope 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), 1);
+ }, 'test selector_matches with link inside shadow tree');
+
+ subsetTestByKey('selectorMatchesDisplayNone', promise_test, async t => {
+ 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 => {
+ 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 => {
+ 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 => {
+ // 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');
+
+ const baseURLChangedTestFixture = (testName, modifyBaseURLFunc) => {
+ return subsetTestByKey(testName, promise_test, async t => {
+ const url = getPrefetchUrl();
+ const link = addLink(url);
+ const url_pattern_string = `prefetch.py${url.search}`;
+
+ // Insert a document rule with a url pattern predicate that uses a
+ // relative URL which will not match with |url|, due to |document.baseURI|
+ // being different from |url|'s path.
+ assert_false((new URLPattern(url_pattern_string, document.baseURI)).test(url));
+ insertDocumentRule({ href_matches: url_pattern_string });
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 0);
+
+ // Change the baseURL of the document to |url|. |url| should now be
+ // prefetched.
+ modifyBaseURLFunc(url);
+ assert_true((new URLPattern(url_pattern_string, document.baseURI)).test(url));
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 1);
+ });
+ }
+
+ baseURLChangedTestFixture('baseURLChangedBySameDocumentNavigation', url => {
+ history.pushState({}, "", url);
+ });
+
+ baseURLChangedTestFixture('baseURLChangedByBaseElement', url => {
+ const base = document.createElement('base');
+ base.href = url;
+ document.head.appendChild(base);
+ });
+
+ subsetTestByKey('linkToSelfFragment', promise_test, async t => {
+ const url = getPrefetchUrl();
+ history.pushState({}, "", url);
+ addLink(new URL('#fragment', url));
+ insertDocumentRule();
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 0);
+ }, 'test that a fragment link to the current document does not prefetch');
+
+</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..f9e46a6a38
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/duplicate-urls.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.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ promise_test(async t => {
+ 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 isUrlPrefetched(urls[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/fragment.https.html b/testing/web-platform/tests/speculation-rules/prefetch/fragment.https.html
new file mode 100644
index 0000000000..7b35500303
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/fragment.https.html
@@ -0,0 +1,35 @@
+<!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.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const testUrl = document.URL;
+ const [prefetchUrl, anotherPrefetchUrl] = getPrefetchUrlList(2);
+ try {
+ history.pushState({}, '', prefetchUrl);
+ const urls = [
+ new URL('#fragment', prefetchUrl),
+ new URL('#fragment', anotherPrefetchUrl),
+ ];
+ insertSpeculationRules({prefetch: [{source: 'list', urls}]});
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(prefetchUrl), 0);
+ assert_equals(await isUrlPrefetched(anotherPrefetchUrl), 1);
+ } finally {
+ // We needed to temporarily change the document URL to do the previous
+ // test. Undo that to avoid breaking any other test cases.
+ history.back();
+ await new Promise(resolve => {
+ addEventListener('popstate', () => resolve(), {once: true});
+ });
+ await new Promise(resolve => t.step_timeout(resolve, 0));
+ assert_equals(document.URL, testUrl);
+ }
+}, "fragment links to the current document URL are not prefetched");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/implicit-source.https.html b/testing/web-platform/tests/speculation-rules/prefetch/implicit-source.https.html
new file mode 100644
index 0000000000..b0b1667984
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/implicit-source.https.html
@@ -0,0 +1,34 @@
+<!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.js"></script>
+<script src="resources/utils.sub.js"></script>
+<body>
+<script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ promise_test(async t => {
+ let urls = getPrefetchUrlList(2);
+
+ let a = document.createElement('a');
+ a.className = 'prefetch-me';
+ a.href = urls[1];
+ a.textContent = 'prefetch me!';
+ document.body.appendChild(a);
+ t.add_cleanup(() => a.remove());
+
+ insertSpeculationRules({prefetch: [
+ {urls: [urls[0]]},
+ {where: {selector_matches: '.prefetch-me'}, eagerness: 'immediate'},
+ ]});
+
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ let wasPrefetched = urls.map(isUrlPrefetched);
+ assert_true(!!(await wasPrefetched[0]), 'implicit list rule should have worked');
+ assert_true(!!(await wasPrefetched[1]), 'implicit document rule should have worked');
+ }, 'rules should be accepted without an explicit source');
+</script>
+</body>
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..9cfedb20ef
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/initiators-a-element.sub.https.html
@@ -0,0 +1,80 @@
+<!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.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<meta name="variant" content="?cross-site">
+<meta name="variant" content="?same-site">
+
+<script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ // 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 => {
+ 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 => {
+ 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..0c195ee454
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/initiators-iframe-location-href.sub.https.html
@@ -0,0 +1,56 @@
+<!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.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<meta name="variant" content="?cross-site">
+<meta name="variant" content="?same-site">
+
+<script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ // 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.
+ //
+ // Nonetheless, a prefetch in a top-level window is not suitable to use in an iframe.
+ // In particular, browsers partition storage and cache by top-level site.
+ // If a browser does start allowing these in narrower cases where the partition
+ // would nonetheless be the same, this test might need tweaking.
+ promise_test(async t => {
+ 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_not_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..6923d44dfb
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/initiators-window-open.sub.https.html
@@ -0,0 +1,69 @@
+<!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.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<meta name="variant" content="?cross-site">
+<meta name="variant" content="?same-site">
+
+<script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ // 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 => {
+ 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 => {
+ 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..0fdfacde64
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/invalid-rules.https.html
@@ -0,0 +1,19 @@
+<!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.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ promise_test(async t => {
+ 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..34a8817a98
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/multiple-url.https.html
@@ -0,0 +1,22 @@
+<!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.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ promise_test(async t => {
+ 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.https.html b/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-delivery-type.https.html
new file mode 100644
index 0000000000..abbc8652ec
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-delivery-type.https.html
@@ -0,0 +1,57 @@
+<!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.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>
+setup(() => assertSpeculationRulesIsSupported());
+
+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 => {
+ 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..9c9371c4c3
--- /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.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<meta name="variant" content="?default">
+<meta name="variant" content="?prefetch=true">
+
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+const searchParams = new URLSearchParams(location.search);
+const prefetchEnabled = searchParams.has('prefetch');
+
+promise_test(async t => {
+ 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..b3cca1c22d
--- /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.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<meta name="variant" content="?default">
+<meta name="variant" content="?bypass_cache=true">
+<meta name="variant" content="?prefetch=true">
+<meta name="variant" content="?prefetch=true&bypass_cache=true">
+
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+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 => {
+ 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..1cbd7e8e71
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/prefetch-single-with-hint.https.html
@@ -0,0 +1,357 @@
+<!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">
+<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.js"></script>
+<script src="../resources/utils.sub.js"></script>
+<script src="/common/subset-tests.js"></script>
+
+<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-last">
+
+<script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ /*
+ 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 => {
+ 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. Invalid No-Vary-Search means default URL variance." +
+ " The prefetched and the navigated URLs have to be the same.",
+ noVarySearch: "",
+ noVarySearchHint: "params=(a)",
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "b=5&a=3&d=6&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use the in-flight prefetch. Invalid No-Vary-Search means default URL variance." +
+ " The prefetched and the navigated URLs are not the same.",
+ noVarySearch: "",
+ noVarySearchHint: "params=(a)",
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "b=5&a=4&d=6&c=3",
+ shouldUsePrefetch: false},
+
+ {description:"No-Vary-Search hint must be a string so the speculation rule will be ignored." +
+ " There is no prefetch happening.",
+ noVarySearch: "",
+ noVarySearchHint: 0,
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "b=5&a=3&d=6&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: "",
+ 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..61f9e420fd
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/prefetch-single.https.html
@@ -0,0 +1,315 @@
+<!DOCTYPE html>
+<title>Prefetched response including No-Vary-Search headers is used during navigation</title>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="../../resources/utils.js"></script>
+<script src="../resources/utils.sub.js"></script>
+<script src="/common/subset-tests.js"></script>
+
+<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>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ 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 => {
+ 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..24502ee513
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/out-of-document-rule-set.https.html
@@ -0,0 +1,154 @@
+<!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.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>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ 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
+ };
+
+ 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..15a4466880
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/prefetch-single.https.html
@@ -0,0 +1,31 @@
+<!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.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>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ // 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 => {
+ 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..94d6a4020e
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/prefetch-status.https.html
@@ -0,0 +1,31 @@
+<!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.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>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ // 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 => {
+ 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..2b66db2f8a
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/prefetch-traverse-reload.sub.html
@@ -0,0 +1,71 @@
+<!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.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ 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 => {
+ 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 => {
+ 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/prefetch-uses-cache.sub.https.html b/testing/web-platform/tests/speculation-rules/prefetch/prefetch-uses-cache.sub.https.html
new file mode 100644
index 0000000000..c0e9e73c75
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/prefetch-uses-cache.sub.https.html
@@ -0,0 +1,69 @@
+<!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.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<meta name="variant" content="?same-site">
+<meta name="variant" content="?cross-site">
+
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const is_same_site = location.search === '?same-site';
+ const initiator = await spawnWindow(t);
+ const url1 = initiator.getExecutorURL({
+ hostname: is_same_site ? undefined : '{{hosts[alt][www]}}',
+ executor: 'cacheable-executor.sub.html',
+ page: 1
+ });
+ const url2 = initiator.getExecutorURL({
+ hostname: is_same_site ? undefined : '{{hosts[][]}}',
+ executor: 'cacheable-executor.sub.html',
+ page: 2
+ });
+
+ await initiator.forceSinglePrefetch(url1);
+ initiator.navigate(url2);
+ assert_equals(await initiator.getDeliveryType(), '');
+ assert_not_prefetched(await initiator.getRequestHeaders(),
+ 'Content should not have been prefetched.');
+
+ initiator.navigate(url1);
+ if (is_same_site) {
+ assert_equals(await initiator.getDeliveryType(), 'cache',
+ 'Navigation should have retrieved the response from the HTTP Cache.');
+ // Note: Even though we didn't use a prefetch, the cached response for
+ // |url1| was obtained using a prefetch request, and the recorded headers
+ // at the time of the first request would have Sec-Purpose: 'prefetch'.
+ assert_prefetched(await initiator.getRequestHeaders(),
+ 'The cached response should have been from the initial prefetch request.');
+ } else {
+ assert_equals(await initiator.getDeliveryType(), '',
+ 'Navigation response should not have been from the HTTP Cache.');
+ }
+
+
+ await initiator.forceSinglePrefetch(url2);
+ initiator.navigate(url2);
+ assert_equals(await initiator.getDeliveryType(), 'navigational-prefetch',
+ 'Navigation should have used the prefetch');
+ if (is_same_site) {
+ // Note: Even though we did use a prefetch, the recorded request headers in
+ // the response will not be prefetch headers. This is because the prefetch
+ // request retrieved its response from the HTTP cache, and the response in
+ // the cache was initially obtained from the first navigation to |url2|,
+ // which was not a prefetch.
+ assert_not_prefetched(await initiator.getRequestHeaders(),
+ 'The prefetch request should have used a response from the HTTP cache.');
+ } else {
+ assert_prefetched(await initiator.getRequestHeaders(),
+ 'The prefetch request should have used a fresh response');
+ }
+
+}, 'Test that prefetches use/store responses to/from the HTTP cache.');
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/redirect-url.sub.https.html b/testing/web-platform/tests/speculation-rules/prefetch/redirect-url.sub.https.html
new file mode 100644
index 0000000000..02bb35349f
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/redirect-url.sub.https.html
@@ -0,0 +1,54 @@
+<!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.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ promise_test(async t => {
+ const agent = await spawnWindow(t);
+ // Because `forceSinglePrefetch()` waits for 2 seconds, the redirect and final
+ // responses are expected to be received before `navigate()` below.
+ const nextUrl = new URL('/common/redirect.py', location.href);
+ const finalUrl = agent.getExecutorURL({ page: 2 });
+ nextUrl.searchParams.set('location', finalUrl);
+ await agent.forceSinglePrefetch(nextUrl);
+ await agent.navigate(nextUrl, {expectedDestinationUrl: finalUrl});
+
+ assert_prefetched(await agent.getRequestHeaders(),
+ 'Prefetched response should be used by navigation.');
+ }, 'Redirect and final response received before navigation start');
+
+ promise_test(async t => {
+ const agent = await spawnWindow(t);
+ // Because `forceSinglePrefetch()` waits for 2 seconds, we put 2.5-second delay
+ // here to make the redirect response is received after `navigate()` below.
+ const nextUrl = new URL('/common/slow-redirect.py?delay=2.5', location.href);
+ const finalUrl = agent.getExecutorURL({ page: 2 });
+ nextUrl.searchParams.set('location', finalUrl);
+ await agent.forceSinglePrefetch(nextUrl);
+ await agent.navigate(nextUrl, {expectedDestinationUrl: finalUrl});
+
+ assert_prefetched(await agent.getRequestHeaders(),
+ 'Prefetched response should be used by navigation.');
+ }, 'Same-origin redirect response received after navigation start');
+
+ promise_test(async t => {
+ const agent = await spawnWindow(t);
+ // Because `forceSinglePrefetch()` waits for 2 seconds, we put 2.5-second delay
+ // here to make the redirect response is received after `navigate()` below.
+ const nextUrl = new URL('/common/slow-redirect.py?delay=2.5', location.href);
+ const finalUrl = agent.getExecutorURL({ page: 2, hostname: '{{hosts[alt][www]}}' });
+ nextUrl.searchParams.set('location', finalUrl);
+ await agent.forceSinglePrefetch(nextUrl);
+ await agent.navigate(nextUrl, {expectedDestinationUrl: finalUrl});
+
+ assert_prefetched(await agent.getRequestHeaders(),
+ 'Prefetched response should be used by navigation.');
+ }, 'Cross-site redirect response received after navigation start');
+</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..d4828fca51
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-from-rules.https.html
@@ -0,0 +1,130 @@
+<!DOCTYPE html>
+<title>Prefetch with the referrer policy specified in speculation rules</title>
+<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.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<!--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>
+"use strict";
+
+setup(() => assertSpeculationRulesIsSupported());
+
+subsetTest(promise_test, async t => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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..d8c2bea87a
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-not-accepted.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>Prefetch attempts with an unacceptable referrer policy</title>
+<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.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<!--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>
+"use strict";
+
+setup(() => assertSpeculationRulesIsSupported());
+
+subsetTest(promise_test, async t => {
+ 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 => {
+ 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..8eb24c4581
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy.https.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<title>Prefetch is done with the referring page's referrer policy</title>
+<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.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<!--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>
+"use strict";
+
+setup(() => assertSpeculationRulesIsSupported());
+
+subsetTest(promise_test, async t => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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..8820781709
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/authenticate.py
@@ -0,0 +1,36 @@
+
+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', b'Basic'),
+ (b'Cache-Control', b'no-store')
+ ]
+ 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/cacheable-executor.sub.html b/testing/web-platform/tests/speculation-rules/prefetch/resources/cacheable-executor.sub.html
new file mode 100644
index 0000000000..ba1b3acb0c
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/cacheable-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/cacheable-executor.sub.html.headers b/testing/web-platform/tests/speculation-rules/prefetch/resources/cacheable-executor.sub.html.headers
new file mode 100644
index 0000000000..0ee6ec2ab1
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/cacheable-executor.sub.html.headers
@@ -0,0 +1 @@
+Cache-Control: private, max-age=604800
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..3ba9cd9270
--- /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"), (b"Cache-Control", b"no-store")]
+
+ 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/executor.sub.html.headers b/testing/web-platform/tests/speculation-rules/prefetch/resources/executor.sub.html.headers
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/executor.sub.html.headers
@@ -0,0 +1 @@
+Cache-Control: no-store
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..14ac4d1699
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch.py
@@ -0,0 +1,17 @@
+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")
+ response.headers.set("Cache-Control", "no-store")
+
+ 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..d912eff90a
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch_nvs_hint.py
@@ -0,0 +1,49 @@
+import time
+
+def main(request, response):
+ response.headers.set("Cache-Control", "no-store")
+ uuid = request.GET[b"uuid"]
+ wait_for_prefetch_start_uuid = None
+ if b"wait_for_prefetch_uuid" in request.GET:
+ wait_for_prefetch_start_uuid = request.GET[b"wait_for_prefetch_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"wait_for_prefetch" in request.GET:
+ if wait_for_prefetch_start_uuid is None:
+ return ''
+ wait_for_prefetch = None
+ while wait_for_prefetch is None:
+ time.sleep(0.1)
+ wait_for_prefetch = request.server.stash.take(wait_for_prefetch_start_uuid)
+ 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:
+ if wait_for_prefetch_start_uuid is not None:
+ request.server.stash.put(wait_for_prefetch_start_uuid, 0)
+ 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/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..dd8a9631b4
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/sw.js
@@ -0,0 +1 @@
+self.addEventListener('fetch', event => 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..73624c0c25
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/utils.sub.js
@@ -0,0 +1,195 @@
+/**
+ * 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 = {}, wait_for_completion = true) {
+ await this.execute_script((url, extra) => {
+ insertSpeculationRules({ prefetch: [{source: 'list', urls: [url], ...extra}] });
+ }, [url, extra]);
+ if (!wait_for_completion) {
+ return Promise.resolve();
+ }
+ return new Promise(resolve => this.t.step_timeout(resolve, 2000));
+ }
+
+ // `url` is the URL to navigate.
+ //
+ // `expectedDestinationUrl` is the expected URL after navigation.
+ // When omitted, `url` is used.
+ async navigate(url, {expectedDestinationUrl} = {}) {
+ await this.execute_script((url) => {
+ window.executor.suspend(() => {
+ location.href = url;
+ });
+ }, [url]);
+ if (!expectedDestinationUrl) {
+ expectedDestinationUrl = url;
+ }
+ expectedDestinationUrl.username = '';
+ expectedDestinationUrl.password = '';
+ assert_equals(
+ await this.execute_script(() => location.href),
+ expectedDestinationUrl.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());
+}
+
+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 spawnWindowWithReference(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":agent, "window":w};
+}
+
+// Must also include /common/utils.js and /common/dispatcher/dispatcher.js to use this.
+async function spawnWindow(t, options = {}, uuid = token()) {
+ let agent_window_pair = await spawnWindowWithReference(t, options, uuid);
+ return agent_window_pair.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);
+}
+
+// Use nvs_header query parameter to ask the wpt server
+// to populate No-Vary-Search response header.
+function addNoVarySearchHeaderUsingQueryParam(url, value){
+ if(value){
+ url.searchParams.append("nvs_header", value);
+ }
+}
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..2f93c1ebb4
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/same-origin-cookies.https.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="/resources/testdriver.js"></script>
+<script src='/resources/testdriver-vendor.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.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<!--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>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ subsetTest(promise_test, async t => {
+ 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.");
+
+ // Regression test for https://crbug.com/1524338
+ subsetTest(promise_test, async t => {
+ 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");
+
+ await agent.setReferrerPolicy("no-referrer");
+
+ 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());
+ }, "same origin prefetch with no referrer works when cookies are present.");
+</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..011e6137e6
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/user-pass.https.html
@@ -0,0 +1,46 @@
+<!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.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<meta name="variant" content="?cross-origin=true">
+<meta name="variant" content="?cross-origin=false">
+
+<script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ let cross_origin = Object.fromEntries(new URLSearchParams(location.search))["cross-origin"] === "true";
+ promise_test(async t => {
+ 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>