diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/speculation-rules/prefetch | |
parent | Initial commit. (diff) | |
download | firefox-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')
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> |