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 | |
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')
251 files changed, 11797 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> diff --git a/testing/web-platform/tests/speculation-rules/prerender/about-blank-iframes.html b/testing/web-platform/tests/speculation-rules/prerender/about-blank-iframes.html new file mode 100644 index 0000000000..af289197c6 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/about-blank-iframes.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<title>Test about:blank iframes prerendering state</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const channel = new PrerenderChannel('test-channel', uid); + + const gotMessage = new Promise(resolve => { + channel.addEventListener('message', e => { + resolve(e.data); + }, {once: true}); + }); + + // Make the window to start the prerender. + const url = `resources/about-blank-iframes.html?uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const msg = await gotMessage; + assert_equals(msg, 'PASS'); +}, 'about:blank iframes'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/accept-client-hint-cache.https.html b/testing/web-platform/tests/speculation-rules/prerender/accept-client-hint-cache.https.html new file mode 100644 index 0000000000..a07f47cfd4 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/accept-client-hint-cache.https.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<title>Test Prerender pages maintain their own Client Hint Caches</title> +<meta name="timeout" content="long"> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/client-hints/resources/open-and-add-load-event.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test( + async t => { + const uid = token(); + const initialization_bc = new PrerenderChannel(`test-channel`, uid); + t.add_cleanup(_ => initialization_bc.close()); + + const gotMessage = new Promise(resolve => { + initialization_bc.addEventListener('message', e => { + resolve(e.data); + }, {once: true}); + }); + + // Initialization. For the active pages, it has the client hint cache of + // "sec-ch-ua-bitness". + await open_and_add_load_event('resources/accept-ch.html'); + window.open( + `resources/echo-client-hints-received.py?uid=${uid}`, '_blank', + 'noopener'); + + const result = await gotMessage; + assert_equals(result.result, 'PASSED', result.reason); + }, + 'Prerender page should maintain their own client hints cache and do not propagate it to' + + 'the global cache until activation'); + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/activation-start.html b/testing/web-platform/tests/speculation-rules/prerender/activation-start.html new file mode 100644 index 0000000000..7aee20c346 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/activation-start.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<title>PerformanceNavigationTiming's activationStart in prerendered page</title> +<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="/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const ACTIVATION_DELAY = 10; + + const rcHelper = new RemoteContextHelper(); + const referrerRC = await rcHelper.addWindow(undefined, { features: 'noopener' }); + const prerenderedRC = await addPrerenderRC(referrerRC); + const iframeRC = await prerenderedRC.addIframe(); + + assert_equals( + await getActivationStart(prerenderedRC), + 0, + 'activationStart must be 0 while prerendering' + ); + + assert_equals( + await getActivationStart(iframeRC), + 0, + 'activationStart must be 0 while prerendering the iframe' + ); + + // Wait ACTIVATION_DELAY ms before activation. + await new Promise(resolve => t.step_timeout(resolve, ACTIVATION_DELAY)); + + await activatePrerenderRC(referrerRC, prerenderedRC); + + assert_greater_than_equal( + await getActivationStart(prerenderedRC), + ACTIVATION_DELAY, + 'activationStart after activation must be greater than or equal to ' + + 'ACTIVATION_DELAY' + ); + + assert_greater_than_equal( + await getActivationStart(iframeRC), + ACTIVATION_DELAY, + 'activationStart after activation must be greater than or equal to ' + + 'ACTIVATION_DELAY in the iframe' + ); +}); + +// A utility to both extract activationStart from the prerendered +// RemoteContextWrapper, and also check that it shows up in toJSON(). +async function getActivationStart(prerenderedRC) { + const [activationStart, activationStartInToJSON] = await prerenderedRC.executeScript(() => { + const entry = performance.getEntriesByType("navigation")[0]; + return [entry.activationStart, entry.toJSON().activationStart]; + }); + + assert_equals( + activationStart, + activationStartInToJSON, + "activationStart value must be available in the result of toJSON()" + ); + + return activationStart; +} +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/blob_object_url.html b/testing/web-platform/tests/speculation-rules/prerender/blob_object_url.html new file mode 100644 index 0000000000..49fb7c9e2c --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/blob_object_url.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<title>Same-origin prerendering page can create a url for the given +objects</title> +<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.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const {exec} = await create_prerendered_page(t); + + const result = await exec(async () => { + const blob_contents = "test blob contents"; + const blob = new Blob([blob_contents]); + const url = URL.createObjectURL(blob); + const fetched_content = await fetch(url).then(response => response.text()); + URL.revokeObjectURL(url); + return fetched_content === blob_contents ? "PASS" : "FAIL"; + }); + + // Start prerendering a page that attempts to create a url for a blob. + assert_equals( + result, "PASS", + 'prerendering page should be able to create a url for blob and fetch it.'); +}, 'prerendering page should be able create url'); + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/cache-storage.https.html b/testing/web-platform/tests/speculation-rules/prerender/cache-storage.https.html new file mode 100644 index 0000000000..76add1edd8 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/cache-storage.https.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<title>Same-origin prerendering can access cache storage</title> +<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.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const cacheName = token(); + const cache = await caches.open(cacheName); + await cache.add('resources/cache.txt'); + t.add_cleanup(() => caches.delete(cacheName)); + const response = await cache.match('resources/cache.txt'); + const cacheText = await (response ? response.text() : 'primary cache match failed'); + + const {exec} = await create_prerendered_page(t); + const result = await exec(async cacheName => { + const cache = await caches.open(cacheName); + const match = await cache.match('cache.txt'); + return await match.text(); + }, [cacheName]); + + assert_equals( + result, cacheText, + 'prerendering page should be able to read from cache storage.'); +}, 'prerendering page should be able to access cache storage') + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/clients-matchall.https.html b/testing/web-platform/tests/speculation-rules/prerender/clients-matchall.https.html new file mode 100644 index 0000000000..31fcc90391 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/clients-matchall.https.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<title>Service Worker: Clients.matchAll with a prerender page</title> +<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="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const workerUrl = 'resources/clients-matchall-service-worker.js'; + const pageUrl = `resources/prerendered-page.html?uid=${uid}`; + + // Start a service worker. + const registration = + await service_worker_unregister_and_register(t, workerUrl, workerUrl); + t.add_cleanup(_ => registration.unregister()); + + // Observe the message from a prerendered page. + const bc = new PrerenderChannel('prerender-channel', uid); + t.add_cleanup(_ => bc.close()); + const messagePromise = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, {once:true}); + }); + + startPrerendering(pageUrl); + + const result = await messagePromise; + assert_equals(result, 'prerender success'); + + // The service worker returns the list of client urls exposed by + // Clients#matchAll(). + const message = await new Promise(resolve => { + navigator.serviceWorker.onmessage = resolve; + get_newest_worker(registration).postMessage('invoke clients.matchAll()'); + }); + + assert_array_equals( + message.data, + [ window.location.href, new URL(pageUrl, location).toString() ]); +}, 'The client urls (including a prerender page) are exposed by ' + + 'Clients#matchAll()'); + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/cookies.https.html b/testing/web-platform/tests/speculation-rules/prerender/cookies.https.html new file mode 100644 index 0000000000..2f02197fb5 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/cookies.https.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<title>Same-origin prerendering can access cookies</title> +<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="/cookie-store/resources/cookie-test-helpers.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +cookie_test(async t => { + const {exec} = await create_prerendered_page(t); + const initiator_cookie = 'initiator_cookie=exist'; + const prerender_cookie = 'prerender_cookie=exist'; + + document.cookie = initiator_cookie; + const result = await exec(() => { + const result = document.cookie; + document.cookie = "prerender_cookie=exist;path=/;"; + return result; + }); + + assert_equals( + result, initiator_cookie, + 'prerendering page should be able to read from document cookies.'); + + assert_equals( + document.cookie, initiator_cookie + '; ' + prerender_cookie, + 'prerendering page should be able to write to document cookies'); +}, 'prerendering page should be able to access cookies'); + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/credentialed-prerender-not-opt-in.html b/testing/web-platform/tests/speculation-rules/prerender/credentialed-prerender-not-opt-in.html new file mode 100644 index 0000000000..697382a6dc --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/credentialed-prerender-not-opt-in.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<title>same-site cross-origin prerendering not opt in</title> +<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/get-host-info.sub.js"></script> +<script src="/common/dispatcher/dispatcher.js"></script> +<script src="/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + const referrerRC = await rcHelper.addWindow({origin: 'HTTPS_ORIGIN'}, { features: 'noopener' }); + const prerenderedRC = await addPrerenderRC(referrerRC, {origin: 'HTTPS_REMOTE_ORIGIN'}); + + // Because the prerender doesn't use opt-in header, it is expected to be canceled. + // And the navigation is expected to create another page instead of activation. + referrerRC.navigateTo(prerenderedRC.url); + assert_equals(await getActivationStart(prerenderedRC), 0); +}); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/credentialed-prerender-opt-in.html b/testing/web-platform/tests/speculation-rules/prerender/credentialed-prerender-opt-in.html new file mode 100644 index 0000000000..91626bafce --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/credentialed-prerender-opt-in.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<title>same-site cross-origin prerendering opt in</title> +<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/get-host-info.sub.js"></script> +<script src="/common/dispatcher/dispatcher.js"></script> +<script src="/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + const referrerRC = await rcHelper.addWindow({origin: 'HTTPS_ORIGIN'}, { features: 'noopener' }); + const prerenderedRC = await addPrerenderRC(referrerRC, {origin: 'HTTPS_REMOTE_ORIGIN', headers: [['Supports-Loading-Mode', 'credentialed-prerender']] }); + + await activatePrerenderRC(referrerRC, prerenderedRC); +}); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/cross-origin-iframe.html b/testing/web-platform/tests/speculation-rules/prerender/cross-origin-iframe.html new file mode 100644 index 0000000000..0fc92fcce1 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/cross-origin-iframe.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<!-- +Tests for cross-origin iframes `document.prerendering` state. + +This file cannot be upstreamed to WPT until: +* The specification describes the loading of cross-origin iframes. The test + expects that they are not loaded during prerendering. +--> +<title>Load a cross-origin document in a prerendered iframe</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/utils.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/cross-origin-iframe.html?uid=${uid}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting iframe loaded', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'finished waiting iframe loaded', + prerendering: false + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + bc.close(); + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `cross-origin iframes should not load until activation`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/cross-origin-isolated.https.html b/testing/web-platform/tests/speculation-rules/prerender/cross-origin-isolated.https.html new file mode 100644 index 0000000000..9d5d8b3693 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/cross-origin-isolated.https.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<title>Allow crossOriginIsolated in prerendered page</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const testChannel = new PrerenderChannel('test-channel', uid); + t.add_cleanup(() => { + testChannel.close(); + }); + const gotMessage = new Promise(resolve => { + testChannel.addEventListener('message', e => resolve(e.data), {once: true}); + }); + + startPrerendering(`resources/cross-origin-isolated.https.html?uid=${uid}&target_hint=${getTargetHint()}`); + assert_true(await gotMessage); +}, 'Allow crossOriginIsolated in prerendered page'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-elem-inline-speculation-rules.html b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-elem-inline-speculation-rules.html new file mode 100644 index 0000000000..7f33c32793 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-elem-inline-speculation-rules.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<meta name="timeout" content="long"> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + // The key used for storing a test result in the server. + const key = token(); + + // Open the test runner in a popup - it will prerender itself, record the + // test results, and send them back to this harness. + const url = + `resources/csp-script-src-elem-inline-speculation-rules.html?key=${key}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + // Wait until the test sends us the results. + const result = await nextValueFromServer(key); + + assert_equals(result, "true", "initial document.prerendering"); +}, 'Test if CSP script-src-elem inline-speculation-rules permits inline speculationrules.'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-inline-speculation-rules.html b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-inline-speculation-rules.html new file mode 100644 index 0000000000..1d5b35c30f --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-inline-speculation-rules.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<meta name="timeout" content="long"> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + // The key used for storing a test result in the server. + const key = token(); + + // Open the test runner in a popup - it will prerender itself, record the + // test results, and send them back to this harness. + const url = + `resources/csp-script-src-inline-speculation-rules.html?key=${key}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + // Wait until the test sends us the results. + const result = await nextValueFromServer(key); + + assert_equals(result, "true", "initial document.prerendering"); +}, 'Test if CSP script-src inline-speculation-rules permits inline speculationrules.'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-self.html b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-self.html new file mode 100644 index 0000000000..86cf9def4a --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-self.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<meta name="timeout" content="long"> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + // The key used for storing a test result in the server. + const key = token(); + + // Open the test runner in a popup - it will prerender itself, record the + // test results, and send them back to this harness. + const url = + `resources/csp-script-src-self.html?key=${key}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + // Wait until the test sends us the results. + const result = await nextValueFromServer(key); + + assert_equals(result, "blocked by script-src-elem", "csp block"); +}, 'Test if CSP script-src self does not permit inline speculationrules.'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-strict-dynamic.html b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-strict-dynamic.html new file mode 100644 index 0000000000..d5d8079401 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-strict-dynamic.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<meta name="timeout" content="long"> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + // The key used for storing a test result in the server. + const key = token(); + + // Open the test runner in a popup - it will prerender itself, record the + // test results, and send them back to this harness. + const url = + `resources/csp-script-src-strict-dynamic.html?key=${key}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + // Wait until the test sends us the results. + const result = await nextValueFromServer(key); + + assert_equals(result, "true", ""); +}, 'Test if CSP script-src strict-dynamic allows inline speculationrules injected from the permitted scripts.'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-unsafe-inline.html b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-unsafe-inline.html new file mode 100644 index 0000000000..b9b3092a12 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-unsafe-inline.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<meta name="timeout" content="long"> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + // The key used for storing a test result in the server. + const key = token(); + + // Open the test runner in a popup - it will prerender itself, record the + // test results, and send them back to this harness. + const url = + `resources/csp-script-src-unsafe-inline.html?key=${key}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + // Wait until the test sends us the results. + const result = await nextValueFromServer(key); + + assert_equals(result, "true", "initial document.prerendering"); +}, 'Test if CSP script-src unsafe-inline permits inline speculationrules.'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/fetch-blob.html b/testing/web-platform/tests/speculation-rules/prerender/fetch-blob.html new file mode 100644 index 0000000000..3158b0f1a0 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/fetch-blob.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<title>Same-origin prerendering can access blobs</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const opt = {}; + const init_opt = {}; + const rule_extras = {'target_hint': getTargetHint()}; + const {exec} = await create_prerendered_page(t, opt, init_opt, rule_extras); + const result = await exec(async () => { + const blob = await (await fetch('cache.txt')).blob(); + const reader = new FileReader(); + reader.readAsText(blob); + return new Promise(function(resolve, reject) { + reader.onload = () => resolve(reader.result); + reader.onerror = () => reject(reader.error); + }); + }); + const expected = "Hello, Prerender API!"; + + // Start prerendering a page that attempts to access the blob. + assert_equals( + result, expected, + 'prerendering page should be able to read from blobs.'); +}, 'prerendering page should be able to access blobs'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/fetch-intercepted-by-service-worker.https.html b/testing/web-platform/tests/speculation-rules/prerender/fetch-intercepted-by-service-worker.https.html new file mode 100644 index 0000000000..68db1b537f --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/fetch-intercepted-by-service-worker.https.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<title>Service worker intercepts a fetch request coming from a prerendered page</title> +<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="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + + const PAGE_URL = `resources/fetch-intercepted-by-service-worker.html?uid=${uid}`; + const WORKER_URL = 'resources/fetch-intercept-worker.js'; + + const registration = + await service_worker_unregister_and_register(t, WORKER_URL, PAGE_URL); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + + const bc = new PrerenderChannel('prerender-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + startPrerendering(PAGE_URL); + + const result = await gotMessage; + assert_equals( + result, 'intercepted by service worker', + "fetch() should go through a service worker's fetch event handler"); +}, 'fetch() in a prerendering page should go through a service worker'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/iframe-added-post-activation.html b/testing/web-platform/tests/speculation-rules/prerender/iframe-added-post-activation.html new file mode 100644 index 0000000000..d22b511e10 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/iframe-added-post-activation.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<!-- +Tests that an iframe added after activation starts out with +document.prerendering false. +--> +<title>iframe added after activation</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const channel = new PrerenderChannel('test-channel', uid); + const messageQueue = new BroadcastMessageQueue(channel); + t.add_cleanup(_ => channel.close()); + + // Make the window to start the prerender. + const url = `resources/iframe-added-post-activation.html?uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + // Wait for done. + const msg = await messageQueue.nextMessage(); + assert_equals(msg, 'PASS'); +}, 'iframe added after activation has false document.prerendering'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/indexeddb.html b/testing/web-platform/tests/speculation-rules/prerender/indexeddb.html new file mode 100644 index 0000000000..f8240014de --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/indexeddb.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<title>Same-origin prerendering can access Indexed Database</title> +<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/indexedb-utils.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const db = await openIndexedDatabase(t); + assert_not_equals(db, null, 'Failed to open database.'); + await addData(db, INITIATOR_KEY, INITIATOR_VALUE); + + const {exec} = await create_prerendered_page(t); + + const result = await exec(async () => { + await import_script_to_prerendered_page("indexedb-utils.js"); + const db = await openIndexedDatabase(); + + await addData(db, PRERENDER_KEY, PRERENDER_VALUE); + const result = await readData(db, INITIATOR_KEY); + db.close(); + return result; + }); + + assert_equals( + result, INITIATOR_VALUE, + 'prerendering page should be able to read from Indexed DataBase'); + const initiatorReadResult = await readData(db, PRERENDER_KEY); + assert_equals( + initiatorReadResult, PRERENDER_VALUE, + 'prerendering page should be able to write to Indexed DataBase'); + db.close(); +}, 'prerendering page should be able to access Indexed DataBase') +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/local-storage.html b/testing/web-platform/tests/speculation-rules/prerender/local-storage.html new file mode 100644 index 0000000000..73f760cfb0 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/local-storage.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<title>Same-origin prerendering can access localStorage</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid1 = token(); + const uid2 = token(); + + // A promise to wait until a prerendered page writes data with the "prerender" + // key in the local storage. + const write_promise = new Promise((resolve, reject) => { + window.addEventListener("storage", event => { + if (event.key !== 'prerender') { + reject("wrong key"); + } else { + resolve(); + } + }, { once: true }); + }); + + const opt = {}; + const init_opt = {}; + const rule_extras = {'target_hint': getTargetHint()}; + + window.localStorage.setItem('initial', uid1); + const {exec} = await create_prerendered_page(t, opt, init_opt, rule_extras); + const result = await exec(uid2 => { + window.localStorage.setItem('prerender', uid2); + return window.localStorage.getItem('initial'); + }, [uid2]) + + // Start prerendering a page that attempts to access localStorage API. + assert_equals( + result, uid1, + 'prerendering page should be able to read from local storage'); + + await write_promise; + assert_equals( + window.localStorage.getItem('prerender'), uid2, + 'prerendering page should be able to write to local storage'); +}, 'prerendering page should be able to access local storage'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/main-frame-navigation.https.html b/testing/web-platform/tests/speculation-rules/prerender/main-frame-navigation.https.html new file mode 100644 index 0000000000..2391f72197 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/main-frame-navigation.https.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<title>Test about: main frame navigation in a prerendered page</title> +<meta name="timeout" content="long"> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('result', uid); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + const url = `resources/main-frame-navigation.html?uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + + assert_equals(result.onprerenderingchangeCalled, true, + 'prerenderingchange event should be called after activation.'); + assert_equals(result.prerenderingValueBeforeActivation, true, + 'document.prerendering should be true prior to activation.'); + assert_equals(result.prerenderingValueAfterActivation, false, + 'document.prerendering should be false after activation.'); +}, 'Test document.prerendering and prerenderingchange event in the navigated ' + + 'page'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/media-autoplay.html b/testing/web-platform/tests/speculation-rules/prerender/media-autoplay.html new file mode 100644 index 0000000000..f5d3291ffd --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/media-autoplay.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<title>Same-origin prerendering can trigger autoplay</title> +<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.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const {exec, activate} = await create_prerendered_page(t); + await exec(() => { + const video = document.createElement('video'); + video.src = '/media/A4.mp4'; + video.autoplay = true; + video.muted = true; + window.video = video; + document.body.appendChild(video); + }); + + await new Promise(resolve => t.step_timeout(resolve, 500)); + + const before_activation = await exec(() => ({ + readyState: video.readyState, + paused: video.paused, + currentTime: video.currentTime + })); + + await activate(); + await new Promise(resolve => t.step_timeout(resolve, 500)); + const after_activation = await exec(() => ({ + readyState: video.readyState, + paused: video.paused, + currentTime: video.currentTime + })); + + assert_equals(before_activation.paused, false); + assert_equals(before_activation.currentTime, 0); + assert_equals(after_activation.paused, false); + assert_greater_than(before_activation.currentTime, 0); +}, "media autoplay should defer playaback"); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/navigation-api-location-replace.html b/testing/web-platform/tests/speculation-rules/prerender/navigation-api-location-replace.html new file mode 100644 index 0000000000..e3ecf1b72b --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/navigation-api-location-replace.html @@ -0,0 +1,61 @@ +<!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="/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + const referrerRC = await rcHelper.addWindow(undefined, { features: 'noopener' }); + assert_equals(await referrerRC.executeScript(() => navigation.entries().length), 1); + let referrerRCCurrentId = await referrerRC.executeScript(() => navigation.currentEntry.id); + + const prerenderedRC = await addPrerenderRC(referrerRC); + let activationStateBeforeActivation = await prerenderedRC.executeScript(() => { + return { + entries: navigation.entries().map(e => ({ id: e.id, })), + activationEntryId: navigation.activation.entry?.id, + activationFromId: navigation.activation.from?.id, + activationNavigationType : navigation.activation.navigationType, + } + }); + assert_equals(activationStateBeforeActivation.entries.length, 1); + assert_equals(activationStateBeforeActivation.activationFromId, referrerRCCurrentId); + assert_equals(activationStateBeforeActivation.activationEntryId, activationStateBeforeActivation.entries[0].id); + assert_equals(activationStateBeforeActivation.activationNavigationType, "push"); + + // Save the current entry before activation. + await prerenderedRC.executeScript(() => window.currentEntryBeforeActivation = navigation.currentEntry); + + await activatePrerenderRC(referrerRC, prerenderedRC, url => { + location.replace(url); + }); + + let activationStateAfterActivation = await prerenderedRC.executeScript(() => { + return { + entries: navigation.entries().map(e => ({ id: e.id, })), + activationEntryId: navigation.activation.entry?.id, + activationFromId: navigation.activation.from?.id, + activationNavigationType : navigation.activation.navigationType, + } + }); + assert_equals(activationStateAfterActivation.entries.length, 1); + assert_equals(activationStateAfterActivation.activationFromId, referrerRCCurrentId); + assert_equals(activationStateAfterActivation.activationEntryId, activationStateAfterActivation.entries[0].id); + assert_equals(activationStateAfterActivation.activationNavigationType, "replace"); + + let currentEntryIdentity = await prerenderedRC.executeScript(() => { + return window.currentEntryBeforeActivation === navigation.currentEntry && + navigation.currentEntry === navigation.entries()[navigation.entries().length - 1]; + }); + assert_true(currentEntryIdentity); +},`navigation.entries() and navigation.activation should be updated on activation and handle replacing correctly`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/navigation-api-multiple-entries.html b/testing/web-platform/tests/speculation-rules/prerender/navigation-api-multiple-entries.html new file mode 100644 index 0000000000..e8e0f87459 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/navigation-api-multiple-entries.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/utils.js"></script> +<script src="/common/dispatcher/dispatcher.js"></script> +<script src="/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + + const referrerRC1 = await rcHelper.addWindow(undefined, { features: 'noopener' }); + const referrerRC2 = await referrerRC1.navigateToNew(); + const referrerRC3 = await referrerRC2.navigateToNew(); + assert_equals(await referrerRC3.executeScript(() => navigation.entries().length), 3); + + let referrerRC3CurrentId = await referrerRC3.executeScript(() => navigation.currentEntry.id); + + const prerenderedRC = await addPrerenderRC(referrerRC3); + let activationStateBeforeActivation = await prerenderedRC.executeScript(() => { + return { + entries: navigation.entries().map(e => ({ id: e.id, })), + activationEntryId: navigation.activation.entry?.id, + activationFromId: navigation.activation.from?.id, + activationNavigationType : navigation.activation.navigationType, + } + }); + assert_equals(activationStateBeforeActivation.entries.length, 1); + assert_equals(activationStateBeforeActivation.activationFromId, referrerRC3CurrentId); + assert_equals(activationStateBeforeActivation.activationEntryId, activationStateBeforeActivation.entries[0].id); + assert_equals(activationStateBeforeActivation.activationNavigationType, "push"); + + await activatePrerenderRC(referrerRC3, prerenderedRC); + + let activationStateAfterActivation = await prerenderedRC.executeScript(() => { + return { + entries: navigation.entries().map(e => ({ id: e.id, })), + activationEntryId: navigation.activation.entry?.id, + activationFromId: navigation.activation.from?.id, + activationNavigationType : navigation.activation.navigationType, + } + }); + assert_equals(activationStateAfterActivation.entries.length, 4); + assert_equals(activationStateAfterActivation.activationFromId, activationStateAfterActivation.entries[2].id); + assert_equals(activationStateAfterActivation.activationEntryId, activationStateAfterActivation.entries[3].id); + assert_equals(activationStateAfterActivation.activationNavigationType, "push"); +},`navigation.entries() and navigation.activation should be updated on activation - multiple entries`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/navigation-api.html b/testing/web-platform/tests/speculation-rules/prerender/navigation-api.html new file mode 100644 index 0000000000..fd25e94b5b --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/navigation-api.html @@ -0,0 +1,60 @@ +<!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="/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + const referrerRC = await rcHelper.addWindow(undefined, { features: 'noopener' }); + assert_equals(await referrerRC.executeScript(() => navigation.entries().length), 1); + let referrerRCCurrentId = await referrerRC.executeScript(() => navigation.currentEntry.id); + + const prerenderedRC = await addPrerenderRC(referrerRC); + let activationStateBeforeActivation = await prerenderedRC.executeScript(() => { + return { + entries: navigation.entries().map(e => ({ id: e.id, })), + activationEntryId: navigation.activation.entry?.id, + activationFromId: navigation.activation.from?.id, + activationNavigationType : navigation.activation.navigationType, + } + }); + assert_equals(activationStateBeforeActivation.entries.length, 1); + assert_equals(activationStateBeforeActivation.activationFromId, referrerRCCurrentId); + assert_equals(activationStateBeforeActivation.activationEntryId, activationStateBeforeActivation.entries[0].id); + assert_equals(activationStateBeforeActivation.activationNavigationType, "push"); + + // Save the current entry before activation. + await prerenderedRC.executeScript(() => window.currentEntryBeforeActivation = navigation.currentEntry); + + await activatePrerenderRC(referrerRC, prerenderedRC); + + let activationStateAfterActivation = await prerenderedRC.executeScript(() => { + return { + entries: navigation.entries().map(e => ({ id: e.id, })), + activationEntryId: navigation.activation.entry?.id, + activationFromId: navigation.activation.from?.id, + activationNavigationType : navigation.activation.navigationType, + } + }); + assert_equals(activationStateAfterActivation.entries.length, 2); + assert_equals(activationStateAfterActivation.activationFromId, activationStateAfterActivation.entries[0].id); + assert_equals(activationStateAfterActivation.activationFromId, referrerRCCurrentId); + assert_equals(activationStateAfterActivation.activationEntryId, activationStateAfterActivation.entries[1].id); + assert_equals(activationStateAfterActivation.activationNavigationType, "push"); + + let currentEntryIdentity = await prerenderedRC.executeScript(() => { + return window.currentEntryBeforeActivation === navigation.currentEntry && + navigation.currentEntry === navigation.entries()[navigation.entries().length - 1]; + }); + assert_true(currentEntryIdentity); +},`navigation.entries() and navigation.activation should be updated on activation`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/navigation-intercepted-by-service-worker.https.html b/testing/web-platform/tests/speculation-rules/prerender/navigation-intercepted-by-service-worker.https.html new file mode 100644 index 0000000000..2c40aba0e9 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/navigation-intercepted-by-service-worker.https.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<title>Service worker intercepts a navigation and starts prerendering</title> +<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="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + + const PAGE_URL = `resources/non-existent-page.html?should-intercept&uid=${uid}`; + const WORKER_URL = 'resources/fetch-intercept-worker.js'; + + const registration = + await service_worker_unregister_and_register(t, WORKER_URL, PAGE_URL); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + + const bc = new PrerenderChannel('prerender-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + startPrerendering(PAGE_URL); + + const result = await gotMessage; + assert_equals(result, 'prerender success'); +}, 'navigation should be intercepted by a service worker'); + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/navigator-plugins.html b/testing/web-platform/tests/speculation-rules/prerender/navigator-plugins.html new file mode 100644 index 0000000000..4d7fa4902a --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/navigator-plugins.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<title>Same-origin prerendering can access navigator.plugins</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => { + assertSpeculationRulesIsSupported(); + assert_implements_optional( + 'plugins' in navigator, 'navigator.plugins is not provided.' + ); +}); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('prerender-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + // Start prerendering a page that attempts to access the navigator.plugins. + startPrerendering(`resources/navigator-plugins.html?uid=${uid}`); + const result = await gotMessage; + const plugins = JSON.parse(result); + assert_equals(plugins.length, navigator.plugins.length); + for (let i = 0; i < plugins.length; ++i) { + const expected_plugin = navigator.plugins[i]; + assert_equals(plugins[i].pluginLength, expected_plugin.length); + for (let j = 0; j < plugins[i].pluginLength; ++j) { + assert_equals(plugins[i].pluginTypes[j], expected_plugin[j].type, + `type of navigator.plugins[${i}].plugin[${j}]`); + } + } +}, 'prerendering page should be able to access the navigator.plugins'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/prefetch.https.html b/testing/web-platform/tests/speculation-rules/prerender/prefetch.https.html new file mode 100644 index 0000000000..42e4fa4786 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/prefetch.https.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.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.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const {tryToActivate, getNetworkRequestCount} = + await create_prerendered_page(t, {}, {prefetch: true}, {}); + + assert_equals(await tryToActivate(), 'activated'); + assert_equals(await getNetworkRequestCount(), '1', 'Network request count'); +}, "Prerender navigation requests should use prefetched results"); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/prerender-while-prerender.html b/testing/web-platform/tests/speculation-rules/prerender/prerender-while-prerender.html new file mode 100644 index 0000000000..8aa80baf52 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/prerender-while-prerender.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/utils.js"></script> +<script src="/common/dispatcher/dispatcher.js"></script> +<script src="/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +// We attempted to write this test using `RemoteContextHelper`. See +// https://github.com/web-platform-tests/wpt/blob/23ed0c7015082f21dd29dd09a545e2979dc3e08c/speculation-rules/prerender/prerender-while-prerender.html. +// +// However, that ended up being flaky in Chromium. See +// https://bugs.chromium.org/p/chromium/issues/detail?id=1517319. +// +// We're unsure yet if that flakiness was due to a test bug or an implementation +// bug. In case it is due to a test bug, we are currently trying the following +// uglier `PrerenderChannel` version. If it is still flaky, then probably it is +// an implementation bug, and we can revert to the prettier +// `RemoteContextHelper` version. + +promise_test(async t => { + const uid = token(); + const channel = new PrerenderChannel("result", uid); + const result = new Promise(r => channel.addEventListener("message", e => r(e.data))); + + window.open( + `resources/prerender-while-prerender-initiator.html?uid=${uid}`, + "_blank", + "noopener" + ); + + assert_equals( + await result, + false, + "document.prerendering in the inner page must be false" + ); + + channel.close(); +}, "Prerendering inside a prerender doesn't work, and navigating inside prerenderingchange is fine"); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-from-rules.html b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-from-rules.html new file mode 100644 index 0000000000..7e3b65fe61 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-from-rules.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<meta name="timeout" content="long"> +<title>Prerender with the referrer policy specified in speculation rules</title> + +<!--Split test cases due to the use of timeouts in test utilities.--> +<meta name="variant" content="?1-1"> +<meta name="variant" content="?2-last"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/dispatcher/dispatcher.js"></script> +<script src="/common/subset-tests.js"></script> +<script src="/common/utils.js"></script> +<script src="/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +subsetTest(promise_test, async t => { + const {exec, tryToActivate} = await create_prerendered_page( + t, {}, + {referrer_policy: 'strict-origin-when-cross-origin'}, + {referrer_policy: 'strict-origin'}); + + const actualReferrer = await exec(() => { return document.referrer; }); + const expectedReferrer = location.origin + "/"; + assert_equals(actualReferrer, expectedReferrer, 'must send the origin as the referrer'); + + const result = await tryToActivate(); + assert_equals(result, 'activated'); +}, 'with "strict-origin" referrer policy in rule set overriding "strict-origin-when-cross-origin" of referring page'); + +subsetTest(promise_test, async t => { + const {exec, tryToActivate} = await create_prerendered_page( + t, {}, + {referrer_policy: 'strict-origin-when-cross-origin'}, + {referrer_policy: 'no-referrrrrrrer'}); + const result = await tryToActivate(); + assert_equals(result, 'discarded'); +}, 'unrecognized policies invalidate the rule'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-mismatch.html b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-mismatch.html new file mode 100644 index 0000000000..fa2d424660 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-mismatch.html @@ -0,0 +1,92 @@ +<!DOCTYPE html> +<title>Referrer policy mismatches must be allowed between prerendering and activation</title> +<meta name="timeout" content="long"> +<meta name="referrer" content="strict-origin-when-cross-origin"> +<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="/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + const referrerRC = await rcHelper.addWindow(undefined, { features: "noopener" }); + await setReferrerPolicy(referrerRC, "strict-origin-when-cross-origin"); + const prerenderedRC = await addPrerenderRC(referrerRC); + + const referrerURL = await referrerRC.executeScript(() => location.href); + + assert_equals(await prerenderedRC.executeScript(() => document.prerendering), true); + assert_equals(await prerenderedRC.executeScript(() => document.referrer), referrerURL); + + await activatePrerenderRC(referrerRC, prerenderedRC, url => { + const a = document.createElement("a"); + a.href = url; + a.referrerPolicy = "no-referrer"; + a.click(); + }); + + assert_equals(await prerenderedRC.executeScript(() => document.prerendering), false); + assert_equals(await prerenderedRC.executeScript(() => document.referrer), referrerURL); +}, 'prerendered with "strict-origin-when-cross-origin", activated with "no-referrer"'); + +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + const referrerRC = await rcHelper.addWindow(undefined, { features: "noopener" }); + await setReferrerPolicy(referrerRC, "strict-origin-when-cross-origin"); + const prerenderedRC = await addPrerenderRC(referrerRC); + + const referrerURL = await referrerRC.executeScript(() => location.href); + + assert_equals(await prerenderedRC.executeScript(() => document.prerendering), true); + assert_equals(await prerenderedRC.executeScript(() => document.referrer), referrerURL); + + await activatePrerenderRC(referrerRC, prerenderedRC, url => { + const a = document.createElement("a"); + a.href = url; + a.referrerPolicy = "strict-origin"; + a.click(); + }); + + assert_equals(await prerenderedRC.executeScript(() => document.prerendering), false); + assert_equals(await prerenderedRC.executeScript(() => document.referrer), referrerURL); +}, 'prerendered with "strict-origin-when-cross-origin", activated with "strict-origin"'); + +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + const referrerRC = await rcHelper.addWindow(undefined, { features: "noopener" }); + await setReferrerPolicy(referrerRC, "strict-origin"); + const prerenderedRC = await addPrerenderRC(referrerRC); + + const referrerURL = await referrerRC.executeScript(() => location.href); + const referrerOrigin = (new URL(referrerURL)).origin + "/"; + + assert_equals(await prerenderedRC.executeScript(() => document.prerendering), true); + assert_equals(await prerenderedRC.executeScript(() => document.referrer), referrerOrigin); + + await activatePrerenderRC(referrerRC, prerenderedRC, url => { + const a = document.createElement("a"); + a.href = url; + a.referrerPolicy = "unsafe-url"; + a.click(); + }); + + assert_equals(await prerenderedRC.executeScript(() => document.prerendering), false); + assert_equals(await prerenderedRC.executeScript(() => document.referrer), referrerOrigin); +}, 'prerendered with "strict-origin", activated with "unsafe-url"'); + +function setReferrerPolicy(referrerRC, referrerPolicy) { + return referrerRC.executeScript(referrerPolicy => { + const meta = document.createElement("meta"); + meta.name = "referrer"; + meta.content = referrerPolicy; + document.head.append(meta); + }, [referrerPolicy]); +} +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-no-referrer.html b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-no-referrer.html new file mode 100644 index 0000000000..a0d1763f95 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-no-referrer.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<meta name="referrer" content="no-referrer"> +<meta name="timeout" content="long"> +<title>Test noreferrer</title> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<script src="resources/referrer-test.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +// Tests that the referrer on the prerendering navigation request is not sent +// when the triggering page's referrer policy is set to no-referrer. +promise_test(async t => { + await referrer_test('(none)', token()); +}, 'no referrer'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-origin.html b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-origin.html new file mode 100644 index 0000000000..95eb8ba72f --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-origin.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<meta name="referrer" content="origin"> +<meta name="timeout" content="long"> +<title>Test origin referrer</title> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<script src="resources/referrer-test.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +// Tests that the referrer on the prerendering navigation request is the +// triggering page's origin when the referrer policy is set to origin. +// Note that "origin" is a lax referrer policy, but since this is a same-site +// prerender, it does not cause the prerender to be aborted. +promise_test(async t => { + const expected = new URL('', window.origin).href; + await referrer_test(expected, token()); +}, 'origin referrer'); + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-strict-origin.html b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-strict-origin.html new file mode 100644 index 0000000000..0861012fa4 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-strict-origin.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<meta name="referrer" content="strict-origin"> +<meta name="timeout" content="long"> +<title>Test strict-origin referrer</title> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<script src="resources/referrer-test.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +// Tests that the referrer on the prerendering navigation request is the +// triggering page's origin when the referrer policy is set to strict-origin. +promise_test(async t => { + const expected = new URL('', window.origin).href; + await referrer_test(expected, token()); +}, 'strict-origin referrer'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/referrer.html b/testing/web-platform/tests/speculation-rules/prerender/referrer.html new file mode 100644 index 0000000000..c0ecc845b5 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/referrer.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<title>Test default referrer</title> +<meta name="timeout" content="long"> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<script src="resources/referrer-test.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +// Tests that the referrer on the prerendering navigation request is the +// triggering page's URL by default. +promise_test(async t => { + await referrer_test(window.location.href, token()); +}, 'default referrer'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/register-service-worker.https.html b/testing/web-platform/tests/speculation-rules/prerender/register-service-worker.https.html new file mode 100644 index 0000000000..58245c24d3 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/register-service-worker.https.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<title>Registration of a new service worker in a prerendered page</title> +<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="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +// To make sure the service worker registered by the prerendered page starts up, +// this test sends messages as the following sequence: +// prerendered page => service worker => prerendered page => main page. +promise_test(async t => { + const uid = token(); + + const PAGE_URL = `resources/register-service-worker.html?uid=${uid}`; + + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + window.open(PAGE_URL, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + {event: 'started waiting ServiceWorker.register', prerendering: true}, + {event: 'prerendering change', prerendering: false}, + {event: 'service worker registered', prerendering: false}, + {event: 'finished waiting ServiceWorker.register', prerendering: false}, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, 'New service worker should be registered in a prerendered page'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/remove-script-element.html b/testing/web-platform/tests/speculation-rules/prerender/remove-script-element.html new file mode 100644 index 0000000000..31f5f4bb4b --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/remove-script-element.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<iframe id="iframe"></iframe> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +async_test(t => { + const doc = iframe.contentDocument; + const script = doc.createElement('script'); + script.type = 'speculationrules'; + script.text = `{"prerender": [{"source": "list", "urls": [] }] }`; + doc.head.appendChild(script); + iframe.remove(); + t.step_timeout(() => { + document.head.appendChild(script); + t.done(); + }, 0); +}, 'Removing speculationrules script from detached document should not crash'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/about-blank-iframes.html b/testing/web-platform/tests/speculation-rules/prerender/resources/about-blank-iframes.html new file mode 100644 index 0000000000..db99d586e2 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/about-blank-iframes.html @@ -0,0 +1,115 @@ +<!doctype html> + +<title>about:blank iframe initiator and prerendered page</title> +<script src="/common/get-host-info.sub.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="utils.js"></script> +<script> +// Called by iframe-nav-to-about-blank in case empty.html loads before +// we can add the proper onload handler below. +let iframeLoadedEmptyHtml = false; +function onIframeLoadedEmptyHtml() { + iframeLoadedEmptyHtml = true; +} +</script> +<body> +<iframe id="iframe-no-src"></iframe> +<iframe id="iframe-srcdoc" srcdoc="<!doctype html><p>wow look</p>"></iframe> +<iframe id="iframe-nav-to-about-blank" src="empty.html" + onload="onIframeLoadedEmptyHtml();"></iframe> +<script> + +// When loaded without the "?prerendering" param, this document +// is called the "intiator page". It starts a prerender to the same +// URL, but with the "?prerendering" param. +// +// When loaded with the "?prerendering" param, this document is +// the "prerendered page". It tells the initiator page when it is +// ready to activate, and messages the main test page when it is +// finished. + +// Runs the logic of the prerendered page. +// +// The prerendered page has about:blank/srcdoc iframes and tests their +// document.prerendering state before and after activation. +async function main() { + // The iframe-no-src iframe has no src attribute. + const iframeNoSrc = document.querySelector('#iframe-no-src'); + + // The iframe-srcdoc iframe has a srcdoc attribute. + const iframeSrcdoc = document.querySelector('#iframe-srcdoc'); + + // The iframe-nav-to-about-blank first navigates to a non-about:blank URL, + // then sets src to about:blank. + const iframeNavToAboutBlank = + document.querySelector('#iframe-nav-to-about-blank'); + + await new Promise((resolve, reject) => { + iframeNavToAboutBlank.addEventListener('load', (e) => { + if (iframeNavToAboutBlank.src != 'about:blank') + iframeNavToAboutBlank.src = 'about:blank'; + else + resolve(); + }); + + // In case the original navigation to empty.html already finished before we + // added the event listener above, navigate to about:blank now. + if (iframeLoadedEmptyHtml) + iframeNavToAboutBlank.src = 'about:blank'; + }); + + // Collect the documents. + let frames = [ + {doc: document, name: 'main'}, + {doc: iframeNoSrc.contentDocument, name: 'iframeNoSrc'}, + {doc: iframeSrcdoc.contentDocument, name: 'iframeSrcdoc'}, + {doc: iframeNavToAboutBlank.contentDocument, name: 'iframeNavToAboutBlank'} + ]; + + // Test document.prerendering pre-activation for each document. + for (let i = 0; i < frames.length; i++) { + assert_true(frames[i].doc.prerendering, + `document.prerendering pre-activation: ${frames[i].name}`); + } + + // Resolves with [bool, bool, bool, ...] upon activation. Each `bool` is the + // value of document.prerendering in the prerenderingchange event for the + // corresponding document. + let activationPromises = frames.map(x => { + return new Promise((resolve, reject) => { + x.doc.addEventListener('prerenderingchange', (e) => { + resolve(document.prerendering); + }); + }); + }); + + // Ask to activate. + const prerenderChannel = new PrerenderChannel('prerender-channel'); + prerenderChannel.postMessage('readyToActivate'); + + // Test document.prerendering post-activation for each document. + let activationResults = await Promise.all(activationPromises); + for (let i = 0; i < activationResults.length; i++) { + assert_false(activationResults[i], + `document.prerendering in prerenderingchange for ${frames[i].name}`); + } +} + +// See comment at the top of this file. +const params = new URLSearchParams(location.search); +const isPrerendering = params.has('prerendering'); +if (!isPrerendering) { + loadInitiatorPage(); +} else { + // For the prerendering page, run main() then message the test page with the + // result. + const testChannel = new PrerenderChannel('test-channel'); + main().then(() => { + testChannel.postMessage('PASS'); + }).catch((e) => { + testChannel.postMessage('FAIL: ' + e); + }); +} +</script> +</body> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/accept-ch.html b/testing/web-platform/tests/speculation-rules/prerender/resources/accept-ch.html new file mode 100644 index 0000000000..1a23661a44 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/accept-ch.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<body> + This page does nothing but update the ACCEPT-CH headers. +</body> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/accept-ch.html.headers b/testing/web-platform/tests/speculation-rules/prerender/resources/accept-ch.html.headers new file mode 100644 index 0000000000..6c56648705 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/accept-ch.html.headers @@ -0,0 +1,2 @@ +Accept-CH: sec-ch-ua-bitness +Access-Control-Allow-Origin: *
\ No newline at end of file diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/audio-setSinkId.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/audio-setSinkId.https.html new file mode 100644 index 0000000000..1121b20915 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/audio-setSinkId.https.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<audio controls id="beat" src="./bear-av1-opus.mp4" loop></audio> +<script> + +// The main test page (restriction-audio-setSinkId.https.html) loads the +// initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const params = new URLSearchParams(location.search); +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + const rule_extras = {'target_hint': getTargetHint()}; + loadInitiatorPage(rule_extras); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + + const promise = beat.setSinkId( + params.get('sinkId') === 'invalid' ? 'fakeId' : ''); + // The spec, https://wicg.github.io/nav-speculation/prerendering.html#audio-output-patch, + // mentions selectAudioOutput() but this test uses setSinkId() function. + prerenderEventCollector.start(promise, 'Audio.setSinkId'); +} +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/background-fetch.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/background-fetch.https.html new file mode 100644 index 0000000000..659a821e35 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/background-fetch.https.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +// The main test page (restriction-background-fetch.https.html) loads the +// initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const params = new URLSearchParams(location.search); +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + const rule_extras = {'target_hint': getTargetHint()}; + loadInitiatorPage(rule_extras); +} else { + async function loadPrerenderPage() { + const prerenderEventCollector = new PrerenderEventCollector(); + const scope = `resources/`; + const registration = await navigator.serviceWorker.getRegistration(scope); + const fetch_promise = registration.backgroundFetch.fetch( + 'my-fetch', '/', {icons: [{src: '/'}]}); + prerenderEventCollector.start(fetch_promise, 'backgroundFetch.fetch'); + } + loadPrerenderPage(); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/background-sync.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/background-sync.https.html new file mode 100644 index 0000000000..dd452aa345 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/background-sync.https.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +// The main test page (restriction-background-sync.tentative.https.html) +// loads the initiator page, then the initiator page will prerender itself +// with the `prerendering` parameter. +const params = new URLSearchParams(location.search); +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + const rule_extras = {'target_hint': getTargetHint()}; + loadInitiatorPage(rule_extras); +} else { + async function loadPrerenderPage() { + const prerenderEventCollector = new PrerenderEventCollector(); + const scope = `resources/`; + const registration = await navigator.serviceWorker.getRegistration(scope); + const register_promise = registration.periodicSync.register( + 'periodic', { minInterval: 1000 }); + prerenderEventCollector.start(register_promise, 'periodicSync.register'); + } + loadPrerenderPage(); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/battery-status.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/battery-status.https.html new file mode 100644 index 0000000000..dc1e745cbe --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/battery-status.https.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +// The main test page (restriction-battery-status.html) loads the initiator +// page, then the initiator page will prerender itself with the `prerendering` +// parameter. +const params = new URLSearchParams(location.search); +if (!params.has('prerendering')) { + const rule_extras = {'target_hint': getTargetHint()}; + loadInitiatorPage(rule_extras); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + prerenderEventCollector.start(navigator.getBattery(), 'navigator.getBattery'); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/bear-av1-opus.mp4 b/testing/web-platform/tests/speculation-rules/prerender/resources/bear-av1-opus.mp4 Binary files differnew file mode 100644 index 0000000000..64a5e1ffcc --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/bear-av1-opus.mp4 diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/bluetooth-access.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/bluetooth-access.https.html new file mode 100644 index 0000000000..b14b46c09c --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/bluetooth-access.https.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-bluetooth.https.html) loads the initiator +// page, then the initiator page will prerender itself with the `prerendering` +// parameter. +if (!params.has('prerendering')) { + loadInitiatorPage(); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + // The spec, https://wicg.github.io/nav-speculation/prerendering.html#web-bluetooth-patch, + // mentions getDevices() and requestDevice() but this test uses + // getAvailability() instead of them. + prerenderEventCollector.start(navigator.bluetooth.getAvailability(), + 'navigator.bluetooth.getAvailability'); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/broadcast-channel.html b/testing/web-platform/tests/speculation-rules/prerender/resources/broadcast-channel.html new file mode 100644 index 0000000000..14980720a1 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/broadcast-channel.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="utils.js"></script> +<script src="deferred-promise-utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-broadcast-channel.html) loads the initiator +// page, then the initiator page will prerender itself with the `prerendering` +// parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + + // Create separate channels as a sender channel cannot receive a message sent + // by itself. + const sender = new BroadcastChannel('test'); + const receiver = new BroadcastChannel('test'); + + const promise = new Promise(resolve => { + receiver.onmessage = e => { + prerenderEventCollector.addEvent(`received message: ${e.data}`); + resolve(); + }; + }); + sender.postMessage('hello'); + + prerenderEventCollector.start(promise, 'BroadcastChannel'); +} +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/cache.txt b/testing/web-platform/tests/speculation-rules/prerender/resources/cache.txt new file mode 100644 index 0000000000..89164cfabe --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/cache.txt @@ -0,0 +1 @@ +Hello, Prerender API!
\ No newline at end of file diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/clients-matchall-service-worker.js b/testing/web-platform/tests/speculation-rules/prerender/resources/clients-matchall-service-worker.js new file mode 100644 index 0000000000..634b13160a --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/clients-matchall-service-worker.js @@ -0,0 +1,11 @@ +onmessage = e => { + // Collect all client URLs in this origin. + const options = { includeUncontrolled: true, type: 'all' }; + const promise = self.clients.matchAll(options) + .then(clients => { + const client_urls = []; + clients.forEach(client => client_urls.push(client.url)); + e.source.postMessage(client_urls); + }); + e.waitUntil(promise); +}; diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-iframe-src.html b/testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-iframe-src.html new file mode 100644 index 0000000000..77fa9bc208 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-iframe-src.html @@ -0,0 +1,7 @@ +<!doctype html> +<body> +<script> +window.parent.postMessage(`document.prerendering: ${document.prerendering}`, + '*'); +</script> +</body> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-iframe.html b/testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-iframe.html new file mode 100644 index 0000000000..10a3b62f0e --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-iframe.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<body> +<script> + +async function main() { + const crossOriginUrl = + new URL('cross-origin-iframe-src.html', + get_host_info()['HTTPS_REMOTE_ORIGIN'] + + window.location.pathname); + + // Start loading a cross-origin iframe. The iframe messages us with the + // value of its document.prerendering, which should be false since load + // is delayed until after activation. + const crossOriginIframe = document.createElement('iframe'); + + const gotMessage = new Promise((resolve, reject) => { + window.addEventListener('message', (e) => { + if (e.data == 'document.prerendering: false') + resolve(); + else + reject('bad message: ' + e.data); + }); + }); + + crossOriginIframe.src = crossOriginUrl.href; + document.body.appendChild(crossOriginIframe); + + // To give the test a chance to fail by giving enough time if it loads the + // cross-origin iframe instead of deferring, wait for a same-origin iframe to + // load before proceeding with the test. + await createFrame('empty.html'); + + // Start the event collector to trigger activation. + const prerenderEventCollector = new PrerenderEventCollector(); + prerenderEventCollector.start(gotMessage, + 'iframe loaded'); + +} + +// The main test page (cross-origin-iframe.https.html) loads the initiator +// page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const params = new URLSearchParams(location.search); +if (!params.has('prerendering')) { + const rule_extras = {'target_hint': getTargetHint()}; + loadInitiatorPage(rule_extras); +} else { + main(); +} +</script> +</body> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-isolated-iframe.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-isolated-iframe.https.html new file mode 100644 index 0000000000..acad3c3e64 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-isolated-iframe.https.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> +assert_true(document.prerendering); +parent.postMessage( + {name: 'crossOriginIsolated', value: self.crossOriginIsolated}, '*'); +</script> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-isolated-iframe.https.html.headers b/testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-isolated-iframe.https.html.headers new file mode 100644 index 0000000000..b227e843ae --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-isolated-iframe.https.html.headers @@ -0,0 +1,2 @@ +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Resource-Policy: cross-origin
\ No newline at end of file diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-isolated.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-isolated.https.html new file mode 100644 index 0000000000..4fdc6c9541 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-isolated.https.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="utils.js"></script> +<body> +<script> +window.onmessage = function(e) { + assert_equals(e.data.name, 'crossOriginIsolated'); + assert_true(e.data.value); + + const testChannel = new PrerenderChannel('test-channel'); + testChannel.postMessage(self.crossOriginIsolated); + testChannel.close(); +}; + +assert_true(document.prerendering); +const frame = document.createElement('iframe'); +frame.src = './cross-origin-isolated-iframe.https.html'; +document.body.append(frame); +</script> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-isolated.https.html.headers b/testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-isolated.https.html.headers new file mode 100644 index 0000000000..5f8621ef83 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-isolated.https.html.headers @@ -0,0 +1,2 @@ +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Opener-Policy: same-origin diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-elem-inline-speculation-rules.html b/testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-elem-inline-speculation-rules.html new file mode 100644 index 0000000000..19aff92206 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-elem-inline-speculation-rules.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> + +<head> + <meta http-equiv="Content-Security-Policy" content="script-src-elem 'self' 'inline-speculation-rules'"> +</head> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="utils.js"></script> +<script src="csp-script-src.js"></script> +<script> + const params = new URLSearchParams(location.search); + writeValueToServer(params.get('key'), "csp is ignored unexpectedly"); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-inline-speculation-rules.html b/testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-inline-speculation-rules.html new file mode 100644 index 0000000000..febfbd01ba --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-inline-speculation-rules.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> + +<head> + <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'inline-speculation-rules'"> +</head> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="utils.js"></script> +<script src="csp-script-src.js"></script> +<script> + const params = new URLSearchParams(location.search); + writeValueToServer(params.get('key'), "csp is ignored unexpectedly"); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-self.html b/testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-self.html new file mode 100644 index 0000000000..8dc382068a --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-self.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> + +<head> + <!-- disallow inline script --> + <meta http-equiv="Content-Security-Policy" content="script-src 'self'"> +</head> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="utils.js"></script> +<script src="csp-script-src.js"></script> +<script> + const params = new URLSearchParams(location.search); + writeValueToServer(params.get('key'), "csp is ignored unexpectedly"); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-strict-dynamic.html b/testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-strict-dynamic.html new file mode 100644 index 0000000000..00db373c47 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-strict-dynamic.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> + +<head> + <meta http-equiv="Content-Security-Policy" + content="script-src 'nonce-1' 'nonce-2' 'nonce-3' 'nonce-4' 'nonce-5' 'strict-dynamic'"> +</head> +<script src="/common/utils.js" nonce="1"></script> +<script src="/resources/testharness.js" nonce="2"></script> +<script src="/resources/testharnessreport.js" nonce="3"></script> +<script src="utils.js" nonce="4"></script> +<script src="csp-script-src.js" nonce="5"></script> +<script> + const params = new URLSearchParams(location.search); + writeValueToServer(params.get('key'), "csp is ignored unexpectedly"); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-unsafe-inline.html b/testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-unsafe-inline.html new file mode 100644 index 0000000000..d2f010dc56 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-unsafe-inline.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> + +<head> + <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'"> +</head> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="utils.js"></script> +<script src="csp-script-src.js"></script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src.js b/testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src.js new file mode 100644 index 0000000000..6107819a36 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src.js @@ -0,0 +1,60 @@ +const params = new URLSearchParams(location.search); + +// Take a key used for storing a test result in the server. +const key = params.get('key'); + +// Take a target hint to decide a target context for prerendering. +const rule_extras = {'target_hint': getTargetHint()}; + +// Speculation rules injection is blocked in the csp-script-src 'self' test. +const block = location.pathname.endsWith('csp-script-src-self.html'); + +// The main test page (csp-script-src-*.html) in the parent directory) will load +// this page only with the "key" parameter. This page will then try prerendering +// itself with the "run-test" parameter. When "run-test" is in the URL we'll +// actually start the test process and record the results to send back to the +// main test page. We do this because the main test page cannot navigate itself +// but it also cannot open a popup to a prerendered browsing context so the +// prerender triggering and activation must both happen in this popup. +const run_test = params.has('run-test'); +if (!run_test) { + // Generate a new stash key so we can communicate with the prerendered page + // about when to close the popup. + const done_key = token(); + const url = new URL(document.URL); + url.searchParams.append('run-test', ''); + url.searchParams.append('done-key', done_key); + + if (block) { + // Observe `securitypolicyviolation` event that will be triggered by + // startPrerendering(). + document.addEventListener('securitypolicyviolation', e => { + if (e.effectiveDirective != 'script-src' && + e.effectiveDirective != 'script-src-elem') { + const message = 'unexpected effective directive: ' + e.effectiveDirective; + writeValueToServer(key, message).then(() => { window.close(); }); + } else { + const message = 'blocked by ' + e.effectiveDirective; + writeValueToServer(key, message).then(() => { window.close(); }); + } + }); + } + + startPrerendering(url.toString(), rule_extras); + + // Wait until the prerendered page signals us it's ready to close. + nextValueFromServer(done_key).then(() => { + window.close(); + }); +} else { + if (block) { + writeValueToServer(key, 'unexpected prerendering'); + } else { + // Tell the harness the initial document.prerendering value. + writeValueToServer(key, document.prerendering); + + // Tell the prerendering initiating page test being finished. + const done_key = params.get('done-key'); + writeValueToServer(done_key, "done"); + } +} diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/dedicated-worker.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/dedicated-worker.https.html new file mode 100644 index 0000000000..570d4b33a1 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/dedicated-worker.https.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-dedicated-worker.https.html) loads the +// initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + const promise = new Promise((resolve, reject) => { + try { + const worker = new Worker('dedicated-worker.js'); + worker.addEventListener('message', _ => resolve()); + worker.addEventListener('error', _ => reject('Error on worker')); + } catch (e) { + reject(`Worker construction error: ${e.toString()}`); + } + }); + prerenderEventCollector.start(promise, 'worker construction'); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/dedicated-worker.js b/testing/web-platform/tests/speculation-rules/prerender/resources/dedicated-worker.js new file mode 100644 index 0000000000..d8029556be --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/dedicated-worker.js @@ -0,0 +1 @@ +postMessage('readyToActivate'); diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/deferred-promise-utils.js b/testing/web-platform/tests/speculation-rules/prerender/resources/deferred-promise-utils.js new file mode 100644 index 0000000000..19bc981a2a --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/deferred-promise-utils.js @@ -0,0 +1,74 @@ +/** + * This file co-works with a html file and utils.js to test a promise that + * should be deferred during prerendering. + * + * Usage example: + * Suppose the html is "prerender-promise-test.html" + * On prerendering page, prerender-promise-test.html?prerendering: + * const prerenderEventCollector = new PrerenderEventCollector(); + * const promise = {a promise that should be deferred during prerendering}; + * prerenderEventCollector.start(promise, {promise name}); + * + * On the initiator page, prerender-promise-test.html: + * execute + * `loadInitiatorPage();` + */ + +// Collects events that happen relevant to a prerendering page. +// An event is added when: +// 1. start() is called. +// 2. a prerenderingchange event is dispatched on this document. +// 3. the promise passed to start() is resolved. +// 4. addEvent() is called manually. +class PrerenderEventCollector { + constructor() { + this.eventsSeen_ = []; + new PrerenderChannel('close').addEventListener('message', () => { + window.close(); + }); + } + + // Adds an event to `eventsSeen_` along with the prerendering state of the + // page. + addEvent(eventMessage) { + this.eventsSeen_.push( + {event: eventMessage, prerendering: document.prerendering}); + } + + // Starts collecting events until the promise resolves. Triggers activation by + // telling the initiator page that it is ready for activation. + async start(promise, promiseName) { + assert_true(document.prerendering); + this.addEvent(`started waiting ${promiseName}`); + promise + .then( + () => { + this.addEvent(`finished waiting ${promiseName}`); + }, + (error) => { + if (error instanceof Error) + error = error.name; + this.addEvent(`${promiseName} rejected: ${error}`); + }) + .finally(() => { + // Used to communicate with the main test page. + const testChannel = new PrerenderChannel('test-channel'); + // Send the observed events back to the main test page. + testChannel.postMessage(this.eventsSeen_); + testChannel.close(); + }); + document.addEventListener('prerenderingchange', () => { + this.addEvent('prerendering change'); + }); + + // Post a task to give the implementation a chance to fail in case it + // resolves a promise without waiting for activation. + setTimeout(() => { + // Used to communicate with the initiator page. + const prerenderChannel = new PrerenderChannel('prerender-channel'); + // Inform the initiator page that this page is ready to be activated. + prerenderChannel.postMessage('readyToActivate'); + prerenderChannel.close(); + }, 0); + } +} diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/deprecated-broadcast-channel.py b/testing/web-platform/tests/speculation-rules/prerender/resources/deprecated-broadcast-channel.py new file mode 100644 index 0000000000..62631d922f --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/deprecated-broadcast-channel.py @@ -0,0 +1,28 @@ +import json +import time +def main(request, response): + uid = request.GET.first(b"uid") + name = request.GET.first(b"name") + time.sleep(0.1) + + messagesByName = [] + if request.method == 'POST': + with request.server.stash.lock: + messages = request.server.stash.take(uid) or {} + if name in messages: + messagesByName = messages[name] + + messagesByName.append(json.loads(request.body)) + messages[name] = messagesByName + request.server.stash.put(uid, messages) + response.status = 204 + else: + with request.server.stash.lock: + messages = request.server.stash.take(uid) or {} + if name in messages: + messagesByName = messages[name] + + request.server.stash.put(uid, messages) + response.status = 200 + response.headers['Content-Type'] = 'application/json' + response.content = json.dumps(messagesByName) diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/do-nothing-worker.js b/testing/web-platform/tests/speculation-rules/prerender/resources/do-nothing-worker.js new file mode 100644 index 0000000000..49ceb2648a --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/do-nothing-worker.js @@ -0,0 +1 @@ +// Do nothing. diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/echo-client-hints-received.py b/testing/web-platform/tests/speculation-rules/prerender/resources/echo-client-hints-received.py new file mode 100644 index 0000000000..26bd48d007 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/echo-client-hints-received.py @@ -0,0 +1,88 @@ +""" Handle the initiator navigation request and attach the received client info +to the returned page. +""" + + +import textwrap + +html_template = """ +<!DOCTYPE html> +<html> +<head> +<title>echo client hints on prerendering page</title> +</head> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<body> +<script> + +// Allow generator to add the received CH information into this script. +%s +const params = new URLSearchParams(location.search); +const uid = params.get('uid'); + +// Performs the check below on initiator pages: +// 1. The client did not send server_received_full_version_list when fetching +// the initiator page. +// If the check fails, it will ask the main test page to terminate the test. +// Otherwise, it will: +// 1. Initiate a prerendering action. And the prerendering page will perform +// some checks. +// 2. Wait for the prerendering page to pass all checks and send a signal back. +// 3. Activate the prerendered page. +async function load_as_initiator_page() { + if (!server_received_bitness || server_received_full_version_list) { + // The initial headers are not as expected. Terminate the test. + failTest( + `unexpected initial headers. + bitness: ${server_received_bitness}, + full_version: ${server_received_full_version_list}`, + uid); + return; + } + const prerendering_url = + `./echo-prerender-page-client-hints-received.py?uid=${uid}`; + // Wait for the prerendered page to be ready for activation. + const bc = new PrerenderChannel('prerender-channel', uid); + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, {once: true}); + }); + startPrerendering(prerendering_url); + + data = await gotMessage; + if (data == 'ready for activation') { + window.location = prerendering_url; + } else { + failTest(`Initial page received unexpected result: ${data}`, uid); + } +} + +load_as_initiator_page(); + +</script> +</body> +</html> +""" + +def translate_to_js(val: bool) -> str: + if isinstance(val, bool): + return "true" if val else "false" + return "" + +def main(request, response): + response.status = 200 + + # Insert the received hints into script. + content = html_template % ( + textwrap.dedent( + f""" + const server_received_bitness = + {translate_to_js(b"sec-ch-ua-bitness" in request.headers)}; + const server_received_full_version_list = + {translate_to_js(b"sec-ch-ua-full-version-list" in + request.headers)}; + """ + ) + ) + response.content = content.encode("utf-8") diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/echo-prerender-page-client-hints-received.py b/testing/web-platform/tests/speculation-rules/prerender/resources/echo-prerender-page-client-hints-received.py new file mode 100644 index 0000000000..b32dcec8f6 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/echo-prerender-page-client-hints-received.py @@ -0,0 +1,96 @@ +""" Handle the prerendering navigation request and insert the received client +info to the returned page. +""" + +import textwrap + +html_template = """ +<!DOCTYPE html> +<html> +<head> +<title>echo client hints on prerendering page</title> +</head> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<body> +<script> + +// Allow generator to add the received CH information into this script. +%s + +const params = new URLSearchParams(location.search); +const uid = params.get('uid'); + +// Performs the following checks on prerendering pages: +// 1. The client did not send server_received_full_version_list when fetching +// the prerendering main resource. +// 2. The request initiated by the prerendering page is sent with +// sec-ch-ua-full-version-list attached, because the server asked the +// prerendering page to attach this hint for the following requests. +// If any of the checks fails, it will ask the main test page to terminate +// the test. +// Otherwise, it asks the initiator page to perform activation, and informs +// the main test page of the test success upon being activated. This is used +// to verify that the initiator page's client hints cache is not modified by +// the prerendering page, i.e., the initiator page does not attach +// sec-ch-ua-full-version-list to the requests. +async function load_as_prerendering_page() { + // The first prerendering request should not contain the field of + // sec-ch-ua-full-version-list, as prerender is initiated by the initial + // page. + if (!server_received_bitness || server_received_full_version_list) { + failTest( + `Prerender page saw unexpected request headers. + bitness: ${server_received_bitness}, + full_version: ${server_received_full_version_list}`, + uid); + } + const r = + await fetch('../resources/echo-subresource-client-hints-received.py'); + if (r.status != 200 || r.headers.get('server_received_bitness') !== 'true' || + r.headers.get('server_received_full_version_list') !== 'true') { + failTest( + `Prerender page saw unexpected headers while fetching + sub-resources. + bitness: ${r.headers.get('server_received_bitness')}, + full_version: ${ + r.headers.get('server_received_full_version_list')}`, + uid); + } else { + document.onprerenderingchange = () => { + const bc = new PrerenderChannel('test-channel', uid); + // Send the result to the test runner page. + bc.postMessage({result: 'PASSED'}); + }; + const bc = new PrerenderChannel('prerender-channel', uid); + bc.postMessage('ready for activation'); + } +} + +load_as_prerendering_page(); +</script> +</body> +</html> +""" + +def translate_to_js(val: bool) -> str: + if isinstance(val, bool): + return "true" if val else "false" + return "" + +def main(request, response): + response.headers.set(b"Accept-CH", "sec-ch-ua-full-version-list") + response.status = 200 + + # Insert the received hints into script. + content = html_template % ( + textwrap.dedent( + f""" + const server_received_bitness = + {translate_to_js(b"sec-ch-ua-bitness" in request.headers)}; + const server_received_full_version_list = + {translate_to_js(b"sec-ch-ua-full-version-list" in + request.headers)}; + """ + ) + ) + response.content = content.encode("utf-8") diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/echo-referrer.py b/testing/web-platform/tests/speculation-rules/prerender/resources/echo-referrer.py new file mode 100644 index 0000000000..38b54291ca --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/echo-referrer.py @@ -0,0 +1,27 @@ +"""A page that echoes the Referrer header value via BroadcastChannel. +""" + + +def main(request, response): + referrer = request.headers.get(b"referer") + uid = request.GET.first(b"uid") + + if referrer is None: + referrer = b"(none)" + + html = u''' +<html> +<head> +<title>Echo referrer</title> +</head> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<body> +<script> +const bc = new PrerenderChannel('prerender-channel', '%s'); +bc.postMessage({referrer: '%s'}); +</script> +</body> +</html> +''' + return (200, [("Content-Type", b"text/html")], + html % (uid.decode("utf-8"), referrer.decode("utf-8"))) diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/echo-subresource-client-hints-received.py b/testing/web-platform/tests/speculation-rules/prerender/resources/echo-subresource-client-hints-received.py new file mode 100644 index 0000000000..bb0d128e50 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/echo-subresource-client-hints-received.py @@ -0,0 +1,15 @@ +""" Handle the sub-resource requests and attach the received client info to +the response. +""" + + +def main(request, response): + response.status = 200 + + # Echo the received CH headers. + response.headers.set( + b"server_received_bitness", + "true" if b"sec-ch-ua-bitness" in request.headers else "false") + response.headers.set( + b"server_received_full_version_list", "true" + if b"sec-ch-ua-full-version-list" in request.headers else "false") diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/empty.html b/testing/web-platform/tests/speculation-rules/prerender/resources/empty.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/empty.html diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/encrypted-media.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/encrypted-media.https.html new file mode 100644 index 0000000000..1c3a3ab0ea --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/encrypted-media.https.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> +const params = new URLSearchParams(location.search); + +// The main test page (restriction-encrypted-media*.https.html) loads the +// initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + + const config = [{ + initDataTypes: ['keyids', 'webm' ,'cenc'], + audioCapabilities: [ + {contentType: 'audio/mp4; codecs="mp4a.40.2"'}, + {contentType: 'audio/webm; codecs="opus"'} + ] + }]; + + const fakeConfig = [{ + initDataTypes: ['fakeidt'], + audioCapabilities: [{contentType: 'audio/fake; codecs="mp4a.40.2"'}] + }]; + + const promise = + navigator.requestMediaKeySystemAccess('org.w3.clearkey', + params.get('config') === 'support' ? config : fakeConfig); + prerenderEventCollector.start( + promise, 'navigator.requestMediaKeySystemAccess'); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/exec.html b/testing/web-platform/tests/speculation-rules/prerender/resources/exec.html new file mode 100644 index 0000000000..1eebaa73d0 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/exec.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<head> + <script src="/common/utils.js"></script> + <script src="/common/dispatcher/dispatcher.js"></script> + <script> + const params = new URLSearchParams(window.location.search); + const uuid = params.get('uuid'); + const discard_uuid = params.get('discard_uuid') || uuid; + const referrer_policy = params.get('referrer_policy'); + if (referrer_policy) { + const meta = document.createElement('meta'); + meta.name = 'referrer'; + meta.content = referrer_policy; + document.head.append(meta); + } + new Executor(document.prerendering ? uuid : discard_uuid).execute(); + </script> +</head> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/exec.py b/testing/web-platform/tests/speculation-rules/prerender/resources/exec.py new file mode 100644 index 0000000000..de0636117d --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/exec.py @@ -0,0 +1,26 @@ +from wptserve.utils import isomorphic_decode +import os + +def main(request, response): + purpose = request.headers.get(b"purpose") + if (purpose == b'prefetch' and b"code" in request.GET): + code = int(request.GET.first(b"code")) + else: + code = 200 + + if b"uuid" in request.GET: + path = '/speculation-rules/prerender/resources/exec.py' + uuid = request.GET.first(b"uuid") + with request.server.stash.lock: + count = request.server.stash.take(uuid, path) or 0 + if b"get-fetch-count" in request.GET: + response.status = 200 + response.content = '%d' % count + request.server.stash.put(uuid, count, path) + return + count += 1 + request.server.stash.put(uuid, count, path) + + with open(os.path.join(os.path.dirname(isomorphic_decode(__file__)), "exec.html"), u"r") as fn: + response.content = fn.read() + response.status = code diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/fetch-intercept-worker.js b/testing/web-platform/tests/speculation-rules/prerender/resources/fetch-intercept-worker.js new file mode 100644 index 0000000000..f6e056a8eb --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/fetch-intercept-worker.js @@ -0,0 +1,9 @@ +self.addEventListener('fetch', e => { + if (e.request.url.includes('should-intercept')) { + if (e.request.destination === 'document') { + e.respondWith(fetch('./prerendered-page.html')); + } else { + e.respondWith(new Response('intercepted by service worker')); + } + } +}); diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/fetch-intercepted-by-service-worker.html b/testing/web-platform/tests/speculation-rules/prerender/resources/fetch-intercepted-by-service-worker.html new file mode 100644 index 0000000000..46aefb4e15 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/fetch-intercepted-by-service-worker.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); +const uid = params.get('uid'); + +async function startFetch() { + assert_true(document.prerendering); + + const response = await fetch('cache.txt?should-intercept'); + const body = await response.text(); + + const bc = new PrerenderChannel('prerender-channel', uid); + bc.postMessage(body); + bc.close(); +} + +startFetch(); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/file-picker.html b/testing/web-platform/tests/speculation-rules/prerender/resources/file-picker.html new file mode 100644 index 0000000000..22245a49bc --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/file-picker.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script> + +assert_true(document.prerendering); + +const params = new URLSearchParams(location.search); +const uid = params.get('uid'); + +async function showFilePicker(){ + const bc = new PrerenderChannel('prerender-channel', uid); + + try { + const _ = await window.showOpenFilePicker() + bc.postMessage('unexpected success'); + } catch (err){ + bc.postMessage(err.name); + } finally { + bc.close(); + } +} + +showFilePicker(); + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/generic-sensor.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/generic-sensor.https.html new file mode 100644 index 0000000000..26f4ac7dcd --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/generic-sensor.https.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-sensor-*-https.html) loads the initiator +// page, then the initiator page will prerender itself with the `prerendering` +// parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + const rule_extras = {'target_hint': getTargetHint()}; + loadInitiatorPage(rule_extras); +} else { + const sensorName = params.get('sensorName'); + const sensorType = window[sensorName]; + const sensor = new sensorType; + sensor.start(); + + const promise = new Promise((resolve) => { + // Sensor TCs only test the async result for Sensor.start() regardless of + // success/fail results, because sensors can vary depending on the device. + sensor.onactivate = function () { resolve(); } + sensor.onerror = function (e) { resolve(); } + }); + + const prerenderEventCollector = new PrerenderEventCollector(); + prerenderEventCollector.start(promise, sensorName + " test"); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/idle-detection.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/idle-detection.https.html new file mode 100644 index 0000000000..4f4554983a --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/idle-detection.https.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script> + +assert_true(document.prerendering); + +const params = new URLSearchParams(location.search); +const uid = params.get('uid'); + +async function requestIdleDetectionPermission() { + const bc = new PrerenderChannel('prerender-channel', uid); + + try { + const _ = await IdleDetector.requestPermission(); + bc.postMessage('unexpected success'); + } catch (err){ + bc.postMessage(err.name); + } finally { + bc.close(); + } +} + +requestIdleDetectionPermission(); + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/iframe-added-post-activation.html b/testing/web-platform/tests/speculation-rules/prerender/resources/iframe-added-post-activation.html new file mode 100644 index 0000000000..10a48df58c --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/iframe-added-post-activation.html @@ -0,0 +1,57 @@ +<!doctype html> + +<title>iframe added post activation: initiator and prerendered page</title> +<script src="/common/get-host-info.sub.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="utils.js"></script> +<body> +<script> + +// When loaded without the "?prerendering" param, this document is called the +// "intiator page". It starts a prerender to the same URL, but with the +// "?prerendering" param. +// +// When loaded with the "?prerendering" param, this document is the "prerendered +// page". It tells the initiator page when it is ready to activate, and messages +// the main test page when it is finished. + +// main() runs the logic of the prerendered page. +// +// On activation, adds an iframe and tests its document.prerendering state. +async function main() { + const activated = new Promise((resolve, reject) => { + document.addEventListener('prerenderingchange', (e) => { + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + resolve(iframe.contentDocument.prerendering) + }); + }); + + // Ask to activate. + const prerenderChannel = new PrerenderChannel('prerender-channel'); + prerenderChannel.postMessage('readyToActivate'); + + // Check that document.prerendering is false in the iframe. + const iframePrerendering = await activated; + assert_true(iframePrerendering === false, + 'document.prerendering in iframe should be false'); +} + +// See comment at the top of this file. +const params = new URLSearchParams(location.search); +const isPrerendering = params.has('prerendering'); +if (!isPrerendering) { + loadInitiatorPage(); +} else { + // For the prerendering page, run main() then message the test page with the + // result. + const testChannel = new PrerenderChannel('test-channel'); + main().then(() => { + testChannel.postMessage('PASS'); + }).catch((e) => { + testChannel.postMessage('FAIL: ' + e); + }); +} +</script> +</body> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/indexedb-utils.js b/testing/web-platform/tests/speculation-rules/prerender/resources/indexedb-utils.js new file mode 100644 index 0000000000..7c57000d5c --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/indexedb-utils.js @@ -0,0 +1,61 @@ +const STORAGE_NAME = 'prerender_test'; +const INITIATOR_KEY = 'initiator'; +const INITIATOR_VALUE = INITIATOR_KEY + '_set'; +const PRERENDER_KEY = 'prerender'; +const PRERENDER_VALUE = PRERENDER_KEY + '_set'; + +async function openIndexedDatabase(t) { + return new Promise(resolve => { + const request = window.indexedDB.open(STORAGE_NAME); + if (t) + t.add_cleanup(() => new Promise(resolve => { + window.indexedDB.deleteDatabase(STORAGE_NAME); + resolve(); + })); + request.onupgradeneeded = e => { + const db = e.target.result; + const objectStore = + db.createObjectStore(STORAGE_NAME, {autoIncrement: true}); + objectStore.createIndex('key', 'key', {unique: true}); + }; + request.onerror = e => { + resolve(null); + }; + request.onsuccess = e => { + resolve(e.target.result); + }; + }); +} + +async function addData(db, key, value) { + return new Promise((resolve, reject) => { + const transaction = db.transaction(STORAGE_NAME, 'readwrite'); + const request = + transaction.objectStore(STORAGE_NAME).add({'key': key, 'value': value}); + // Use `transaction.oncomplete` rather than `request.onsuccess` to ensure + // that IndexedDB has flushed to disk. + transaction.oncomplete = e => { + resolve(true); + }; + transaction.onerror = e => { + reject(e.target.error); + }; + }); +} + +async function readData(db, key) { + return new Promise((resolve, reject) => { + const transaction = db.transaction(STORAGE_NAME); + const request = transaction.objectStore(STORAGE_NAME).index('key').get(key); + request.onsuccess = e => { + if (e.target.result) { + resolve(e.target.result.value); + } else { + reject(new Error('Empty result.')); + } + }; + request.onerror = e => { + reject(e.target.error); + }; + }); +} diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/key-value-store.py b/testing/web-platform/tests/speculation-rules/prerender/resources/key-value-store.py new file mode 100644 index 0000000000..1ab609f11b --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/key-value-store.py @@ -0,0 +1,27 @@ +"""Key-Value store server. + +The request takes "key=" and "value=" URL parameters. The key must be UUID +generated by token(). + +- When only the "key=" is specified, serves a 200 response whose body contains + the stored value specified by the key. If the stored value doesn't exist, + serves a 200 response with an empty body. +- When both the "key=" and "value=" are specified, stores the pair and serves + a 200 response without body. +""" + + +def main(request, response): + key = request.GET.get(b"key") + value = request.GET.get(b"value", None) + + # Store the value. + if value: + request.server.stash.put(key, value) + return (200, [], b"") + + # Get the value. + data = request.server.stash.take(key) + if not data: + return (200, [], b"") + return (200, [], data) diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/main-frame-navigation.html b/testing/web-platform/tests/speculation-rules/prerender/resources/main-frame-navigation.html new file mode 100644 index 0000000000..8781edef66 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/main-frame-navigation.html @@ -0,0 +1,84 @@ +<!DOCTYPE html> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +// The main test page loads the initiator page, then the initiator page will +// prerender itself with the `prerendering` parameter. The prerendered page will +// trigger a main frame navigation with the `navigation` parameter. +const params = new URLSearchParams(location.search); +const uid = params.get('uid'); + +const isPrerendering = params.has('prerendering'); +const isNavigation = params.has('navigation'); + +if (isPrerendering && isNavigation) { + assert_true(document.prerendering); + + const result = { + // Check the value of document.prerendering now and after activation. + prerenderingValueBeforeActivation: document.prerendering, + prerenderingValueAfterActivation: null, + + // True if the prerenderingchange event is fired. + onprerenderingchangeCalled: false, + }; + + window.addEventListener('load', () => { + const prerenderChannel = new PrerenderChannel('prerender-channel', uid); + prerenderChannel.postMessage('readyToActivate'); + prerenderChannel.close(); + }); + + document.addEventListener('prerenderingchange', (e) => { + assert_false(document.prerendering); + + const entry = performance.getEntriesByType('navigation')[0]; + assert_greater_than_equal(entry.activationStart, 0, + 'activationStart must be greater than 0') + + result.onprerenderingchangeCalled = true; + result.prerenderingValueAfterActivation = document.prerendering; + + const resultChannel = new PrerenderChannel('result', uid); + resultChannel.postMessage(result); + resultChannel.close(); + window.close(); + }); +} else if (isPrerendering) { + assert_true(document.prerendering); + + location.href = location.href + '&navigation'; +} else { + assert_false(document.prerendering); + + const prerenderingUrl = location.href + '&prerendering'; + + const prerenderChannel = new PrerenderChannel('prerender-channel', uid); + const readyToActivate = new Promise((resolve, reject) => { + prerenderChannel.addEventListener('message', e => { + if (e.data === 'readyToActivate') { + resolve(); + } else { + reject(`The initiator page receives an unsupported message: ${e.data}`); + } + }); + }); + + // Activate the page when prerendering is ready. + readyToActivate.then(() => { + window.location = prerenderingUrl.toString(); + }).catch(e => { + const resultChannel = new PrerenderChannel('result', uid); + resultChannel.postMessage( + `Failed to navigate the prerendered page: ${e.toString()}`); + resultChannel.close(); + window.close(); + }); + + startPrerendering(prerenderingUrl); +} +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/media-autoplay-attribute.html b/testing/web-platform/tests/speculation-rules/prerender/resources/media-autoplay-attribute.html new file mode 100644 index 0000000000..c14df77154 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/media-autoplay-attribute.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<audio id="audio" autoplay loop></audio> +<video id="video" autoplay type="video/mp4"></video> +<script> +const params = new URLSearchParams(location.search); +// The main test page (restriction-media-auto-play-attribute.html) loads the +// initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + const rule_extras = {'target_hint': getTargetHint()}; + loadInitiatorPage(rule_extras); +} else { + const mediaType = params.get('type'); + const media = document.getElementById(mediaType); + media.src = "./bear-av1-opus.mp4"; + + const prerenderEventCollector = new PrerenderEventCollector(); + const promise = new Promise((resolve, reject) => { + media.onloadedmetadata = () => { + prerenderEventCollector.addEvent( + 'fired loadedmetadata event after prerendering is activated'); + resolve(); + }; + media.onloadstart = () => { + // Wait some time to give the test a chance to load the data and fail the + // test. + setTimeout(() => { + prerenderEventCollector.start(promise, 'Autoplay'); + }, 100); + }; + media.onerror = reject; + }); + +} +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/media-device-info.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/media-device-info.https.html new file mode 100644 index 0000000000..14d2ff78c9 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/media-device-info.https.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-media-device-info.https.html) loads the +// initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + const rule_extras = {'target_hint': getTargetHint()}; + loadInitiatorPage(rule_extras); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + prerenderEventCollector.start( + navigator.mediaDevices.enumerateDevices(), + 'navigator.mediaDevices.enumerateDevices'); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/media-devices-access.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/media-devices-access.https.html new file mode 100644 index 0000000000..6739605f03 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/media-devices-access.https.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> + +<script> +const params = new URLSearchParams(location.search); + +// The main test page (restriction-media-{camera, microphone}.https.html) loads +// the initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + const rule_extras = {'target_hint': getTargetHint()}; + loadInitiatorPage(); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + const promise = navigator.mediaDevices.getUserMedia({ + audio: params.get("audio") === "true", + video: params.get("video") === "true" + }); + prerenderEventCollector.start(promise, 'navigator.mediaDevices.getUserMedia'); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/media-play.html b/testing/web-platform/tests/speculation-rules/prerender/resources/media-play.html new file mode 100644 index 0000000000..f4c0af9579 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/media-play.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<audio id="audio" loop></audio> +<video id="video" type="video/mp4"></video> +<script> +const params = new URLSearchParams(location.search); +// The main test page (restriction-media-play.html) loads the initiator page, +// then the initiator page will prerender itself with the `prerendering` +// parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + const rule_extras = {'target_hint': getTargetHint()}; + loadInitiatorPage(rule_extras); +} else { + const mediaType = params.get('type'); + const media = document.getElementById(mediaType); + media.src = "./bear-av1-opus.mp4"; + + const prerenderEventCollector = new PrerenderEventCollector(); + const promise = new Promise((resolve, reject) => { + media.play(); + + media.onloadedmetadata = () => { + prerenderEventCollector.addEvent( + 'fired loadedmetadata event after prerendering is activated'); + resolve(); + }; + media.onloadstart = () => { + // Wait some time to give the test a chance to load the data and fail the + // test. + setTimeout(() => { + prerenderEventCollector.start(promise, 'Media.Play'); + }, 100); + }; + media.onerror = reject; + }); +} +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/message-boxes.html b/testing/web-platform/tests/speculation-rules/prerender/resources/message-boxes.html new file mode 100644 index 0000000000..8e0a6e874f --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/message-boxes.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script> +function runAlertTest() { + window.alert('Hello! Preprendering!'); + return 'no block'; +} + +function runConfirmTest() { + const result = window.confirm('Are you preprendering page?'); + return 'the return value is ' + (result === true ? 'yes' : 'no'); +} + +function runPromptTest() { + const result = window.prompt('Are you preprendering page?', + 'the default value'); + return 'the return value is ' + (result === null ? 'null' : result); +} + +const params = new URLSearchParams(location.search); + +const uid = params.get('uid'); + +// The main test page (restriction-message-boxes.html) loads the +// initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +if (isPrerendering) { + // Test web APIs on the pages. + const bc = new PrerenderChannel('prerender-channel', uid); + assert_true(document.prerendering); + if (params.has('alert')) { + bc.postMessage(runAlertTest()); + } else if (params.has('confirm')) { + bc.postMessage(runConfirmTest()); + } else if (params.has('prompt')) { + bc.postMessage(runPromptTest()); + } + bc.close(); + window.close(); +} else { + // Initiator pages should prerender the prerendering page. + const url = new URL(document.URL); + url.searchParams.append('prerendering', ''); + startPrerendering(url); +} +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/midi.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/midi.https.html new file mode 100644 index 0000000000..0cd98a1693 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/midi.https.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> + +<script> +const params = new URLSearchParams(location.search); + +// The main test page (restriction-midi.https.html) loads the initiator page, +// then the initiator page will prerender itself with the `prerendering` +// parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + const promise = new Promise((resolve, reject) => { + navigator.requestMIDIAccess({sysex: params.get("sysex") === "true"}).then( + _ => { resolve(); }, + e => { + // Chromium rejects the promise on trybots with an error like: + // ALSA lib seq_hw.c:457:(snd_seq_hw_open) open /dev/snd/seq failed: + // Permission denied + // + // See https://crbug.com/371230 for a similar bug. + // + // Just ignore any errors for now. The test passes if the promise + // settles after activation. + resolve(); + }); + }); + prerenderEventCollector.start(promise, 'navigator.requestMIDIAccess'); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/navigator-plugins.html b/testing/web-platform/tests/speculation-rules/prerender/resources/navigator-plugins.html new file mode 100644 index 0000000000..dcb9302d8d --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/navigator-plugins.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script> +const params = new URLSearchParams(location.search); +const uid = params.get('uid'); + +const bc = new PrerenderChannel('prerender-channel', uid); +assert_true(document.prerendering); + +const plugins = new Array(); +for (let i = 0; i < navigator.plugins.length; ++i) { + const plugin = navigator.plugins[i]; + const types = Array.from(plugin, x => x.type); + plugins[i] = {pluginLength: plugin.length, pluginTypes: types}; +} +bc.postMessage(JSON.stringify(plugins)); +bc.close(); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/notification-before-activation.html b/testing/web-platform/tests/speculation-rules/prerender/resources/notification-before-activation.html new file mode 100644 index 0000000000..16aab488a5 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/notification-before-activation.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-notification.https.html) loads the +// initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + prerenderEventCollector.addEvent( + `Notification permission is ${Notification.permission}`); + const promise = Notification.requestPermission() + .then(permission => { + prerenderEventCollector.addEvent(`permission was ${permission}`); + if (permission !== "granted") return; + const displayPromise = new Promise((resolve, reject) => { + const notification = new Notification("Prerender Notification"); + notification.onshow = () => { + prerenderEventCollector.addEvent('notification displayed'); + resolve("Displayed"); + }; + notification.onerror = () => { + reject("Error on displaying notification"); + }; + }); + return displayPromise; + }); + prerenderEventCollector.start(promise, 'notification'); +} + +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/notification-on-activation.html b/testing/web-platform/tests/speculation-rules/prerender/resources/notification-on-activation.html new file mode 100644 index 0000000000..0a85746b04 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/notification-on-activation.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<script src="utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-notification.https.html) loads the initiator +// page, then the initiator page will prerender itself with the `prerendering` +// parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + // Used to communicate with the initiator page. + const prerenderChannel = new PrerenderChannel('prerender-channel'); + // Used to communicate with the main test page. + const testChannel = new PrerenderChannel('test-channel'); + + window.addEventListener('load', () => { + // Inform the initiator page that this page is ready to be activated. + prerenderChannel.postMessage('readyToActivate'); + prerenderChannel.close(); + }); + + document.addEventListener('prerenderingchange', () => { + // Accessing the Notification API is allowed after the prerendering state + // changed. + const permission = Notification.permission; + const notification = new Notification('New Notification'); + + notification.onerror = function(_) { + testChannel.postMessage('notification error'); + testChannel.close(); + } + notification.onshow = function() { + testChannel.postMessage('notification showed'); + notification.close(); + testChannel.close(); + }; + }); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/post-message-prerendering-completion-notification.html b/testing/web-platform/tests/speculation-rules/prerender/resources/post-message-prerendering-completion-notification.html new file mode 100644 index 0000000000..77fa9bc208 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/post-message-prerendering-completion-notification.html @@ -0,0 +1,7 @@ +<!doctype html> +<body> +<script> +window.parent.postMessage(`document.prerendering: ${document.prerendering}`, + '*'); +</script> +</body> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/postmessage-to-client-worker.js b/testing/web-platform/tests/speculation-rules/prerender/resources/postmessage-to-client-worker.js new file mode 100644 index 0000000000..4aff84f336 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/postmessage-to-client-worker.js @@ -0,0 +1,3 @@ +self.onmessage = e => { + e.source.postMessage('postmessage to client'); +}; diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/postmessage-to-service-worker.html b/testing/web-platform/tests/speculation-rules/prerender/resources/postmessage-to-service-worker.html new file mode 100644 index 0000000000..198ef64b2c --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/postmessage-to-service-worker.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-service-worker-postmessage.https.html) loads +// the initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + assert_not_equals(navigator.serviceWorker.controller, null, + 'prerendered page should be controlled'); + + const prerenderEventCollector = new PrerenderEventCollector(); + + // Promise to wait for a reply from the service worker. + const messagePromise = new Promise(resolve => { + navigator.serviceWorker.onmessage = e => { + prerenderEventCollector.addEvent(e.data); + resolve(); + }; + }); + navigator.serviceWorker.controller.postMessage('postmessage to worker'); + + prerenderEventCollector.start(messagePromise, 'ServiceWorker.postMessage'); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/prerender-response-code.html b/testing/web-platform/tests/speculation-rules/prerender/resources/prerender-response-code.html new file mode 100644 index 0000000000..c3a680bba8 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/prerender-response-code.html @@ -0,0 +1,22 @@ +<head> +<script src="/common/utils.js"></script> +<script src="./utils.js"></script> +<script> + const search = new URLSearchParams(location.search); + const uid = search.get('uid'); + const uid1 = token(); + const uid2 = token(); + const bc = new BroadcastChannel(uid); + + window.onload = async () => { + bc.addEventListener('message', ({data}) => { + if (data === 'close') + window.close(); + else if (data === 'activate') + location.href = url; + }) + + startPrerendering(`/speculation-rules/prerender/resources/dual-exec.html?uid1=${uid1}&uid2=${uid2}`); + }; +</script> +</head>
\ No newline at end of file diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/prerender-state.html b/testing/web-platform/tests/speculation-rules/prerender/resources/prerender-state.html new file mode 100644 index 0000000000..34a59f07ee --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/prerender-state.html @@ -0,0 +1,85 @@ +<!DOCTYPE html> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// Take a key used for storing a test result in the server. +const key = params.get('key'); + +// The main test page (state-and-event.html in the parent directory) will load +// this page only with the "key" parameter. This page will then prerender +// itself with the "run-test" parameter. When "run-test" is in the URL we'll +// actually start the test process and record the results to send back to the +// main test page. We do this because the main test page cannot navigate itself +// but it also cannot open a popup to a prerendered browsing context so the +// prerender triggering and activation must both happen in this popup. +const run_test = params.has('run-test'); +if (!run_test) { + assert_false(document.prerendering); + + // Generate a new stash key so we can communicate with the prerendered page + // about when to activate it. + const activate_key = token(); + const url = new URL(document.URL); + url.searchParams.append('run-test', ''); + url.searchParams.append('activate-key', activate_key); + startPrerendering(url.toString()); + + // Wait until the prerendered page signals us it's time to activate, then + // navigate to it. + nextValueFromServer(activate_key).then(() => { + window.location = url.toString(); + }); +} else { + assert_true(document.prerendering); + + const activate_key = params.get('activate-key'); + const result = { + // Check the types of the members on document. + prerenderingTypeOf: typeof(document.prerendering), + onprerenderingChangeTypeOf: typeof(document.onprerenderingchange), + + // Check the value of document.prerendering now and after activation. + prerenderingValueBeforeActivate: document.prerendering, + prerenderingValueAfterActivate: null, + + // Track when the prerenderingchange event is fired. + onprerenderingchangeCalledBeforeActivate: false, + onprerenderingchangeCalledAfterActivate: false, + + // Tracks the properties on the prerenderingchange event. + eventBubbles: null, + eventCancelable: null + }; + + let did_load = false; + + addEventListener('load', () => { + did_load = true; + + // Tell the harness we've finished loading so we can proceed to activation. + writeValueToServer(activate_key, 'did_load'); + }); + + document.addEventListener('prerenderingchange', (e) => { + assert_false(document.prerendering); + result.eventBubbles = e.bubbles; + result.eventCancelable = e.cancelable; + + if (did_load) { + result.onprerenderingchangeCalledAfterActivate = true; + result.prerenderingValueAfterActivate = document.prerendering; + writeValueToServer(key, JSON.stringify(result)).then(() => { + window.close(); + }); + } else { + result.onprerenderingchangeCalledBeforeActivate = true; + } + }); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/prerender-while-prerender-initiator.html b/testing/web-platform/tests/speculation-rules/prerender/resources/prerender-while-prerender-initiator.html new file mode 100644 index 0000000000..1764271581 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/prerender-while-prerender-initiator.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Prerender while prerendering test: referrer page, opened in a window</title> +<script src="utils.js"></script> + +<script type="module"> +const params = new URLSearchParams(location.search); +const uid = params.get("uid"); +const outerURL = `prerender-while-prerender-outer.html?uid=${uid}`; + +startPrerendering(outerURL); + +const channel = new PrerenderChannel("start-test", uid); +await new Promise(r => channel.addEventListener("message", r)); +channel.close(); + +location.href = outerURL; +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/prerender-while-prerender-inner.html b/testing/web-platform/tests/speculation-rules/prerender/resources/prerender-while-prerender-inner.html new file mode 100644 index 0000000000..60b6c64ded --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/prerender-while-prerender-inner.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Prerender while prerendering test: inner maybe-prerendered page</title> + +<script> +"use strict"; +window.onFirstScriptExecutedPrerendering = document.prerendering; +</script> + +<script src="utils.js"></script> + +<script type="module"> +const params = new URLSearchParams(location.search); +const uid = params.get("uid"); + +const channel = new PrerenderChannel("result", uid); +channel.postMessage(window.onFirstScriptExecutedPrerendering); +channel.close(); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/prerender-while-prerender-outer.html b/testing/web-platform/tests/speculation-rules/prerender/resources/prerender-while-prerender-outer.html new file mode 100644 index 0000000000..313c34667f --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/prerender-while-prerender-outer.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Prerender while prerendering test: outer prerendered page</title> +<script src="utils.js"></script> + +<script type="module"> +const params = new URLSearchParams(location.search); +const uid = params.get("uid"); +const innerURL = `prerender-while-prerender-inner.html?uid=${uid}`; + +startPrerendering(innerURL); +// Wait 5 seconds for any potential prerender to start and get to the point +// where it would send a message that would cause a failure. +await new Promise(r => setTimeout(r, 5_000)); + +document.addEventListener("prerenderingchange", () => { + location.href = innerURL; +}); + +const channel = new PrerenderChannel("start-test", uid); +channel.postMessage("ready to start"); +channel.close(); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/prerendered-iframe.html b/testing/web-platform/tests/speculation-rules/prerender/resources/prerendered-iframe.html new file mode 100644 index 0000000000..dcdfb9b65b --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/prerendered-iframe.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<title>Prerendered iframe</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<body> +<script> +assert_true(document.prerendering); + +const params = new URLSearchParams(location.search); +const uid = params.get('uid'); + +const bc = new PrerenderChannel('iframe-channel', uid); +bc.postMessage('prerender success'); +bc.close(); +</script> +</body> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/prerendered-page.html b/testing/web-platform/tests/speculation-rules/prerender/resources/prerendered-page.html new file mode 100644 index 0000000000..e6ab00788b --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/prerendered-page.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<title>Prerendered page</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<body> +<script> +assert_true(document.prerendering); + +const params = new URLSearchParams(location.search); +const uid = params.get('uid'); + +const bc = new PrerenderChannel('prerender-channel', uid); +bc.postMessage('prerender success'); +bc.close(); +</script> +</body> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/presentation-request.html b/testing/web-platform/tests/speculation-rules/prerender/resources/presentation-request.html new file mode 100644 index 0000000000..18475a3d67 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/presentation-request.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="utils.js"></script> +<script> + +assert_true(document.prerendering); + +async function startPresentationRequest() { + const bc = new PrerenderChannel('prerender-channel'); + const presentationRequest = new PresentationRequest( + 'https://example.com/presentation.html'); + + try { + const _ = await presentationRequest.start(); + bc.postMessage('unexpected success'); + } catch (err) { + bc.postMessage('request failed'); + } finally { + bc.close(); + } +} + +startPresentationRequest(); + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/prompt-by-before-unload-inner-frame.html b/testing/web-platform/tests/speculation-rules/prerender/resources/prompt-by-before-unload-inner-frame.html new file mode 100644 index 0000000000..ba59ca7960 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/prompt-by-before-unload-inner-frame.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script> +const params = new URLSearchParams(location.search); +const uid = params.get('uid'); + +window.onload = function(e) { + const bc = new PrerenderChannel('inner-channel', uid); + bc.postMessage('a new page is loaded'); + bc.close(); +} +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/prompt-by-before-unload.html b/testing/web-platform/tests/speculation-rules/prerender/resources/prompt-by-before-unload.html new file mode 100644 index 0000000000..8cfe09a41f --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/prompt-by-before-unload.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<div id="target"></div> +<iframe id="i" srcdoc="<html><body>Hello</body></html>"></iframe> +<script> + +assert_true(document.prerendering); + +const params = new URLSearchParams(location.search); +const uid = params.get('uid'); + +i.contentWindow.onbeforeunload = function(e) { + // Call preventDefault() or set `returnValue` to trigger the prompt + // on beforeunload event. + // The prompt actually doesn't show up in a prerendered page and + // unload proceeds. + e.preventDefault(); + e.returnValue = 'You have a return value.'; +} + +async function navigateWindowLocation() { + const bc = new PrerenderChannel('inner-channel', uid); + const promise = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + bc.close(); + }, { + once: true + }); + }); + i.contentWindow.location.href = `prompt-by-before-unload-inner-frame.html?uid=${uid}`; + return promise; +} + +async function asyncPromptOnBeforeUnload() { + const bc = new PrerenderChannel('prerender-channel', uid); + try { + const result = await navigateWindowLocation(); + if (result == 'a new page is loaded') + bc.postMessage('unloaded without the prompt by beforeunload.'); + else + bc.postMessage('unexpected result.'); + } catch (err) { + bc.postMessage(err); + } finally { + bc.close(); + } +} + +asyncPromptOnBeforeUnload(); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/push.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/push.https.html new file mode 100644 index 0000000000..30eb563ab7 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/push.https.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-push.https.html) loads the initiator page, +// then the initiator page will prerender itself with the `prerendering` +// parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + async function loadPrerenderPage() { + const prerenderEventCollector = new PrerenderEventCollector(); + const scope = `resources/`; + const registration = await navigator.serviceWorker.getRegistration(scope); + const subscribe_promise = registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: new TextEncoder().encode('0987654321') + }); + prerenderEventCollector.start(subscribe_promise, 'pushManager.subscribe'); + } + loadPrerenderPage(); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/referrer-test.js b/testing/web-platform/tests/speculation-rules/prerender/resources/referrer-test.js new file mode 100644 index 0000000000..5091b6403c --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/referrer-test.js @@ -0,0 +1,15 @@ +async function referrer_test(expected, uid) { + const bc = new PrerenderChannel('prerender-channel', uid); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, {once: true}); + }); + + // Start prerendering a page that will echo its referrer. + startPrerendering(`resources/echo-referrer.py?uid=${uid}`); + + const result = await gotMessage; + assert_equals(result.referrer, expected, 'referrer'); +} diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/register-service-worker.html b/testing/web-platform/tests/speculation-rules/prerender/resources/register-service-worker.html new file mode 100644 index 0000000000..ccdf220573 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/register-service-worker.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<body> +<script> +const params = new URLSearchParams(location.search); +const uid = params.get('uid'); + +const SCOPE = './'; +const WORKER_URL = './do-nothing-worker.js'; + +async function unregisterServiceWorker(scope) { + const registration = await navigator.serviceWorker.getRegistration(scope); + if (!registration) + return; + return registration.unregister(); +} + +// The main test page (register-service-worker.https.html) loads the +// initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + unregisterServiceWorker(SCOPE); + loadInitiatorPage(); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + const promise = navigator.serviceWorker.register(WORKER_URL, {scope: SCOPE}) + .then(registration => { + prerenderEventCollector.addEvent('service worker registered'); + return registration.unregister(); + }); + prerenderEventCollector.start(promise, 'ServiceWorker.register'); +} + +</script> +</body> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/request-picture-in-picture.html b/testing/web-platform/tests/speculation-rules/prerender/resources/request-picture-in-picture.html new file mode 100644 index 0000000000..9aea3d33d5 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/request-picture-in-picture.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<video id="target" + onloadstart="loadstart()" src="/media/test.ogv"></video> +<script> + +assert_true(document.prerendering); + +const params = new URLSearchParams(location.search); +const uid = params.get('uid'); + +async function requestPictureInPicture() { + const bc = new PrerenderChannel('prerender-channel', uid); + + try { + await target.requestPictureInPicture(); + bc.postMessage('unexpected success'); + } catch (err) { + if (err.name == 'InvalidStateError') + bc.postMessage('Metadata for the video element are not loaded yet'); + else + bc.postMessage(err.message); + } finally { + bc.close(); + } +} + +function loadstart() { + // Wait some time to give the test a chance to load the data and fail the test. + setTimeout(() => { requestPictureInPicture(); }, 100); +} +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/sandbox-iframe.html b/testing/web-platform/tests/speculation-rules/prerender/resources/sandbox-iframe.html new file mode 100644 index 0000000000..478dfccb3a --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/sandbox-iframe.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<body> +<script> + +async function main() { + // Start loading a sandbox iframe, which is treated as cross-origin iframe. + // The iframe messages us with the value of its document.prerendering, + // which should be false since load is delayed until after activation. + const sandboxIframe = document.createElement('iframe'); + sandboxIframe.sandbox = 'allow-scripts'; + + const gotMessage = new Promise((resolve, reject) => { + window.addEventListener('message', (e) => { + if (e.data === 'document.prerendering: false') + resolve(); + else + reject('bad message: ' + e.data); + }); + }); + + sandboxIframe.src = 'post-message-prerendering-completion-notification.html'; + document.body.appendChild(sandboxIframe); + + // To give the test a chance to fail by giving enough time if it loads the + // cross-origin iframe instead of deferring, wait for a same-origin iframe to + // load before proceeding with the test. + await createFrame('empty.html'); + + // Start the event collector to trigger activation. + const prerenderEventCollector = new PrerenderEventCollector(); + prerenderEventCollector.start(gotMessage, 'iframe loaded'); +} + +// The main test page (prerender/sandbox-iframe.html) loads the initiator +// page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const params = new URLSearchParams(location.search); +if (!params.has('prerendering')) { + loadInitiatorPage(); +} else { + main(); +} +</script> +</body> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/screen-capture.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/screen-capture.https.html new file mode 100644 index 0000000000..1304b9d74b --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/screen-capture.https.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script> + +assert_true(document.prerendering); + +async function invokeScreenCaptureAPI(){ + const bc = new PrerenderChannel('prerender-channel'); + + try { + await navigator.mediaDevices.getDisplayMedia(); + bc.postMessage('unexpected success'); + } catch (err){ + bc.postMessage(err.name); + } finally { + bc.close(); + } +} + +invokeScreenCaptureAPI(); + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/screen-orientation-lock.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/screen-orientation-lock.https.html new file mode 100644 index 0000000000..a152e34490 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/screen-orientation-lock.https.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-screen-orientation-lock.https.html) loads the +// initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + prerenderEventCollector.start( + screen.orientation.lock('portrait'), 'screen.orientation.lock'); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/service-worker-unregister.html b/testing/web-platform/tests/speculation-rules/prerender/resources/service-worker-unregister.html new file mode 100644 index 0000000000..a78775a5a2 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/service-worker-unregister.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<body> +<script type="module"> + +const params = new URLSearchParams(location.search); +const uid = params.get('uid'); + +// The main test page (restriction-service-worker-unregister.https.html) loads +// the initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + const registration = + await navigator.serviceWorker.getRegistration(location.href); + + const prerenderEventCollector = new PrerenderEventCollector(); + const promise = registration.unregister() + .then(registration => { + prerenderEventCollector.addEvent('service worker unregistered'); + }); + prerenderEventCollector.start( + promise, 'ServiceWorkerRegistration.unregister'); +} + +</script> +</body> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/service-worker-update.html b/testing/web-platform/tests/speculation-rules/prerender/resources/service-worker-update.html new file mode 100644 index 0000000000..d9a9273526 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/service-worker-update.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<body> +<script type="module"> + +const params = new URLSearchParams(location.search); +const uid = params.get('uid'); + +// The main test page (restriction-service-worker-update.https.html) loads the +// initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + const registration = + await navigator.serviceWorker.getRegistration(location.href); + + const prerenderEventCollector = new PrerenderEventCollector(); + const promise = registration.update() + .then(registration => { + prerenderEventCollector.addEvent('service worker updated'); + }); + prerenderEventCollector.start( + promise, 'ServiceWorkerRegistration.update'); +} + +</script> +</body> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/service-worker.js b/testing/web-platform/tests/speculation-rules/prerender/resources/service-worker.js new file mode 100644 index 0000000000..763d55764a --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/service-worker.js @@ -0,0 +1,11 @@ +self.addEventListener("fetch", async e => { + if (e.request.url.endsWith("ping")) + e.respondWith(new Response('pong')); + else if (e.request.url.endsWith("client")) { + e.respondWith((async () => { + const client = await clients.get(e.clientId); + const clientInfo = client ? {id: e.clientId, visibilityState: client.visibilityState, focused: client.focused} : null; + return new Response(JSON.stringify({clientInfo}), {headers: {'Content-Type': 'application/json'}}); + })()); + } +}); diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/session-history-harness.js b/testing/web-platform/tests/speculation-rules/prerender/resources/session-history-harness.js new file mode 100644 index 0000000000..619ee3aa92 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/session-history-harness.js @@ -0,0 +1,75 @@ +// We don't have the test harness in this context, so we roll our own +// which communicates with our initiator which is actually running the tests. + +function assert(condition, message) { + if (!condition) { + throw new Error("Assertion failed: " + message); + } +} + +// Run a test after activation. +document.addEventListener("prerenderingchange", async (_) => { + // history.length is racy on activation. Wait *100ms* as a workaround. + // See crbug.com/1222893. + await new Promise((resolve) => { + window.setTimeout(resolve, 100); + }); + + const urlParams = new URLSearchParams(window.location.search); + const testName = urlParams.get("testName"); + const uid = urlParams.get("uid"); + const testChannel = new PrerenderChannel( + `test-channel-${testName}`, uid + ); + + try { + const activationTestFn = testName + "Activation"; + const testFn = window[activationTestFn]; + if (!testFn) { + testChannel.postMessage("Missing test: " + testName); + return; + } + testFn(); + testChannel.postMessage("Passed"); + } catch (e) { + testChannel.postMessage( + "Failed: " + e.name + ": " + e.message, + ); + } finally { + testChannel.close(); + } +}) + +if (document.prerendering) { + window.onload = async () => { + const urlParams = new URLSearchParams(window.location.search); + const testName = urlParams.get("testName"); + const uid = urlParams.get("uid"); + const prerenderChannel = new PrerenderChannel( + `prerender-channel-${testName}`, uid + ); + + // The document load event is not finished at this point, so navigations + // would be done with replacement. This interferes with our tests. We wait + // for the next task before navigating to avoid this. + await new Promise((resolve) => { + window.setTimeout(resolve); + }); + + try { + let testFn = window[testName]; + if (!testFn) { + prerenderChannel.postMessage("Missing test: " + testName); + return; + } + await testFn(); + prerenderChannel.postMessage("Passed"); + } catch (e) { + prerenderChannel.postMessage( + "Failed: " + e.name + ": " + e.message, + ); + } finally { + prerenderChannel.close(); + } + }; +} diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/session-history-initiator.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/session-history-initiator.https.html new file mode 100644 index 0000000000..f6d5eb555c --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/session-history-initiator.https.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="session-history-harness.js"></script> +<body> + <script> + const urlParams = new URLSearchParams(window.location.search); + const prerender = urlParams.get("prerender"); + const testName = urlParams.get("testName"); + const uid = urlParams.get("uid"); + + const prerenderChannel = new PrerenderChannel( + `prerender-channel-${testName}`, uid + ); + const testChannel = new PrerenderChannel(`test-channel-${testName}`, uid); + + // Activate when a test sends a "activate" message. + testChannel.addEventListener("message", (e) => { + assert(e.data === "activate"); + window.location.href = `${prerender}?testName=${testName}&uid=${uid}`; + }); + + // Runs before and after the history manipulation in the prerender page to confirm + // that the session history of the initiator page is not affected by any history + // changes in the prerender page. + function assertInitialHistoryState() { + assert(history.length == 1, "Initial history length"); + assert(!history.state, "Initial history state"); + } + + async function startPrerenderingAndWaitTestResult() { + const message = new Promise((resolve) => { + prerenderChannel.addEventListener( + "message", + (e) => { + resolve(e.data); + }, + { once: true } + ); + }); + + assertInitialHistoryState(); + + startPrerendering(`${prerender}?testName=${testName}&uid=${uid}`); + const testResult = await message; + + assertInitialHistoryState(); + + return testResult; + } + + (async () => { + try { + testChannel.postMessage(await startPrerenderingAndWaitTestResult()); + } catch (e) { + testChannel.postMessage("Failed: " + e.name + ": " + e.message); + } finally { + prerenderChannel.close(); + } + })(); + </script> +</body> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/session-history-prerender.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/session-history-prerender.https.html new file mode 100644 index 0000000000..b02865c1bc --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/session-history-prerender.https.html @@ -0,0 +1,149 @@ +<!DOCTYPE html> +<!-- + "Activation" suffix in these test names communicates to the test harness that + this part of the test is run post-activation. +--> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="session-history-harness.js"></script> +<script src="session-history-test-util.js"></script> +<body> + <script> + function testHistoryPushStateInPrerender() { + assert(history.length == 1, "Initial history length"); + assert(!history.state, "Initial history state"); + + history.pushState("teststate", null, null); + + assert(history.length == 1, "History length unchanged"); + assert(history.state == "teststate", "Update state"); + } + + function testHistoryReplaceStateInPrerender() { + assert(history.length == 1, "Initial history length"); + assert(!history.state, "Initial history state"); + + history.replaceState("teststate", null, null); + + assert(history.length == 1, "History length unchanged"); + assert(history.state == "teststate", "Update state"); + } + + function testLocationAssignInPrerender() { + assert(history.length == 1, "Initial history length"); + const initialLocation = location.href; + location.assign("#test"); + + assert(history.length == 1, "History length unchanged"); + assert(location.href != initialLocation, "Update location"); + } + + function testLocationReplaceInPrerender() { + assert(history.length == 1, "Initial history length"); + const initialLocation = location.href; + location.replace("#test"); + + assert(history.length == 1, "History length unchanged"); + assert(location.href != initialLocation, "Update location"); + } + + function testSetLocationHrefInPrerender() { + assert(history.length == 1, "Initial history length"); + const initialLocation = location.href; + location.href = "#test"; + + assert(history.length == 1, "History length unchanged"); + assert(location.href != initialLocation, "Update location"); + } + + function testSyntheticAnchorClickInPrerender() { + assert(history.length == 1, "Initial history length"); + const initialLocation = location.href; + + const anchor = document.createElement("a"); + anchor.href = "#test"; + document.body.appendChild(anchor); + + anchor.click(); + + assert(history.length == 1, "History length unchanged"); + assert(location.href != initialLocation, "Update location"); + } + + function testHistoryLengthInPrerender() { + assert(history.length == 1, "Initial history length"); + } + + function testHistoryLengthInPrerenderActivation() { + assert(history.length == 2, "History length after activation"); + + // TODO(http://crbug.com/1220992): Test whether calling history.back() + // after activation should go back to the initiator page correctly. + // We might need a non-trivial refactoring to test this scenario correctly. + } + + // This test runs testSubfrarmeNavigationInPrerenderInSubframe() in a + // subframe, and waits for a message from a navigated subframe. + async function testSubframeNavigationInPrerender() { + assert(window.parent == window, "not the top frame"); + const params = new URLSearchParams(window.location.search); + const testName = params.get("testName"); + const uid = params.get("uid"); + const resultPromise = waitChannelMessage( + `prerender-channel-${testName}InSubframeAfterNavigation`, uid); + + params.set("testName", testName + "InSubframe"); + const frame = document.createElement("iframe"); + const url = location.pathname + "?" + params.toString(); + frame.src = url; + document.body.appendChild(frame); + const result = await resultPromise; + assert(result == "Passed", result); + } + + function testSubframeNavigationInPrerenderInSubframe() { + assert(window.parent != window, "not in a subframe"); + assert(window.parent == window.top, "the direct parent isn't the top"); + assert(history.length == 1, "Initial history length"); + + const params = new URLSearchParams(window.location.search); + const testName = params.get("testName"); + params.set("testName", testName + "AfterNavigation"); + location.href = location.pathname + "?" + params.toString(); + } + + function testSubframeNavigationInPrerenderInSubframeAfterNavigation() { + assert(window.parent != window, "not in a subframe"); + assert(window.parent == window.top, "the direct parent isn't the top"); + assert(history.length == 1, "History length after subframe navigation"); + } + + // This test runs testSubframeReloadInPrerenderInSubframe() in a + // subframe, and waits for a message from a navigated subframe. + async function testSubframeReloadInPrerender() { + assert(window.parent == window, "not the top frame"); + const params = new URLSearchParams(window.location.search); + const testName = params.get("testName"); + const uid = params.get("uid"); + const resultPromise = waitChannelMessage( + `prerender-channel-${testName}InSubframe`, uid); + + params.set("testName", testName + "InSubframe"); + const frame = document.createElement("iframe"); + const url = location.pathname + "?" + params.toString(); + frame.src = url; + document.body.appendChild(frame); + const result = await resultPromise; + assert(result == "Passed", result); + const second_result = await waitChannelMessage( + `prerender-channel-${testName}InSubframe`, uid); + assert(second_result == "Passed", second_result); + } + + function testSubframeReloadInPrerenderInSubframe() { + assert(window.parent != window, "not in a subframe"); + assert(window.parent == window.top, "the direct parent isn't the top"); + assert(history.length == 1, "Initial history length"); + window.location.reload(); + } + </script> +</body> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/session-history-test-util.js b/testing/web-platform/tests/speculation-rules/prerender/resources/session-history-test-util.js new file mode 100644 index 0000000000..c1ca36dc2f --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/session-history-test-util.js @@ -0,0 +1,40 @@ +// Note: Following utility functions are expected to be used from +// session-history-* test files. + +async function waitChannelMessage(testName, uid) { + const result = new Promise((resolve) => { + const testChannel = new PrerenderChannel(testName, uid); + testChannel.addEventListener( + "message", + (e) => { + testChannel.close(); + resolve(e.data); + }, + { once: true }, + ); + }); + return result; +} + +async function runTestInPrerender(testName, uid) { + const result = waitChannelMessage(`test-channel-${testName}`, uid); + + // Run test in a new window for test isolation. + const prerender = "session-history-prerender.https.html"; + window.open( + `./resources/session-history-initiator.https.html?prerender=${prerender}&testName=${testName}&uid=${uid}`, + "_blank", + "noopener", + ); + return result; +} + +// This will activate the prerendered context created in runTestInPrerender +// and then run the post-activation variation of `testName`. +async function runTestInActivatedPage(testName, uid) { + const testChannel = new PrerenderChannel(`test-channel-${testName}`, uid); + testChannel.postMessage("activate"); + testChannel.close(); + + return waitChannelMessage(`test-channel-${testName}`, uid); +} diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/shared-worker.py b/testing/web-platform/tests/speculation-rules/prerender/resources/shared-worker.py new file mode 100644 index 0000000000..48e5cd9c15 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/shared-worker.py @@ -0,0 +1,11 @@ +def main(request, response): + if b"check" in request.GET: + with request.server.stash.lock: + result = request.server.stash.take(request.GET[b"id"]) + response.headers.set(b"Content-Type", b"text/plain") + return result + else: + with request.server.stash.lock: + request.server.stash.put(request.GET[b"id"], "ok") + response.headers.set(b"Content-Type", b"text/javascript") + return u"onconnect = ({ports: [port]}) => port.postMessage(performance.timeOrigin);"
\ No newline at end of file diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/speech-synthesis.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/speech-synthesis.https.html new file mode 100644 index 0000000000..f7436e42ce --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/speech-synthesis.https.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script src="webspeech.js"></script> + +<script> +const params = new URLSearchParams(location.search); + +// The main test page (restriction-speech-synthesis.https.html) loads the +// initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + const method = params.get('method'); + const prerenderEventCollector = new PrerenderEventCollector(); + const promise = new Promise((resolve, reject) => { + switch(method) { + case 'speak': { + const utter = new SpeechSynthesisUtterance('1'); + // https://wicg.github.io/speech-api/#tts-methods + // This tests that speak() is completed after prerendering activation. + utter.onend = () => { resolve(); } + speechSynthesis.speak(utter); + break; + } + case 'cancel': { + const utter = new SpeechSynthesisUtterance('1'); + // https://wicg.github.io/speech-api/#speechsynthesiserrorevent-attributes + // A cancel method call causes 'canceled' or 'interrupted'. + // This tests if one of them happens after prerendering activation. + utter.onerror = (e) => { + if (e.error == 'canceled' || e.error == 'interrupted') + resolve(); + } + speechSynthesis.speak(utter); + speechSynthesis.cancel(); + break; + } + case 'pause': { + const utter = new SpeechSynthesisUtterance('1'); + utter.onpause = () => { resolve(); } + speechSynthesis.speak(utter); + speechSynthesis.pause(); + // To reset the current status for the next test, it calls cancel(). + speechSynthesis.cancel(); + break; + } + case 'resume': { + const utter = new SpeechSynthesisUtterance('1'); + utter.onresume = () => { resolve(); } + speechSynthesis.speak(utter); + speechSynthesis.pause(); + speechSynthesis.resume(); + break; + } + } + }); + prerenderEventCollector.start(promise, `speechSynthesis.${method}`); +} +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/storage-persist.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/storage-persist.https.html new file mode 100644 index 0000000000..ab5fabd9e3 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/storage-persist.https.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-storage-persist.https.html) loads the +// initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + prerenderEventCollector.start( + navigator.storage.persist(), 'navigator.storage.persist'); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/utils.js b/testing/web-platform/tests/speculation-rules/prerender/resources/utils.js new file mode 100644 index 0000000000..940edcc0ca --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/utils.js @@ -0,0 +1,451 @@ +const STORE_URL = '/speculation-rules/prerender/resources/key-value-store.py'; + +// Starts prerendering for `url`. +// +// `rule_extras` provides additional parameters for the speculation rule used +// to trigger prerendering. +function startPrerendering(url, rule_extras = {}) { + // Adds <script type="speculationrules"> and specifies a prerender candidate + // for the given URL. + // TODO(https://crbug.com/1174978): <script type="speculationrules"> may not + // start prerendering for some reason (e.g., resource limit). Implement a + // WebDriver API to force prerendering. + const script = document.createElement('script'); + script.type = 'speculationrules'; + script.text = JSON.stringify( + {prerender: [{source: 'list', urls: [url], ...rule_extras}]}); + document.head.appendChild(script); +} + +class PrerenderChannel extends EventTarget { + #ids = new Set(); + #url; + #active = true; + + constructor(name, uid = new URLSearchParams(location.search).get('uid')) { + super(); + this.#url = `/speculation-rules/prerender/resources/deprecated-broadcast-channel.py?name=${name}&uid=${uid}`; + (async() => { + while (this.#active) { + // Add the "keepalive" option to avoid fetch() results in unhandled + // rejection with fetch abortion due to window.close(). + // TODO(crbug.com/1356128): After this migration, "keepalive" will not + // be able to extend the lifetime of a Document, such that it cannot be + // used here to guarantee the promise resolution. + const messages = await (await fetch(this.#url, {keepalive: true})).json(); + for (const {data, id} of messages) { + if (!this.#ids.has(id)) + this.dispatchEvent(new MessageEvent('message', {data})); + this.#ids.add(id); + } + } + })(); + } + + close() { + this.#active = false; + } + + set onmessage(m) { + this.addEventListener('message', m) + } + + async postMessage(data) { + const id = new Date().valueOf(); + this.#ids.add(id); + // Add the "keepalive" option to prevent messages from being lost due to + // window.close(). + await fetch(this.#url, {method: 'POST', body: JSON.stringify({data, id}), keepalive: true}); + } +} + +// Reads the value specified by `key` from the key-value store on the server. +async function readValueFromServer(key) { + const serverUrl = `${STORE_URL}?key=${key}`; + const response = await fetch(serverUrl); + if (!response.ok) + throw new Error('An error happened in the server'); + const value = await response.text(); + + // The value is not stored in the server. + if (value === "") + return { status: false }; + + return { status: true, value: value }; +} + +// Convenience wrapper around the above getter that will wait until a value is +// available on the server. +async function nextValueFromServer(key) { + let retry = 0; + while (true) { + // Fetches the test result from the server. + let success = true; + const { status, value } = await readValueFromServer(key).catch(e => { + if (retry++ >= 5) { + throw new Error('readValueFromServer failed'); + } + success = false; + }); + if (!success || !status) { + // The test result has not been stored yet. Retry after a while. + await new Promise(resolve => setTimeout(resolve, 100)); + continue; + } + + return value; + } +} + +// Writes `value` for `key` in the key-value store on the server. +async function writeValueToServer(key, value) { + const serverUrl = `${STORE_URL}?key=${key}&value=${value}`; + await fetch(serverUrl); +} + +// Loads the initiator page, and navigates to the prerendered page after it +// receives the 'readyToActivate' message. +// +// `rule_extras` provides additional parameters for the speculation rule used +// to trigger prerendering. +function loadInitiatorPage(rule_extras = {}) { + // Used to communicate with the prerendering page. + const prerenderChannel = new PrerenderChannel('prerender-channel'); + window.addEventListener('pagehide', () => { + prerenderChannel.close(); + }); + + // We need to wait for the 'readyToActivate' message before navigation + // since the prerendering implementation in Chromium can only activate if the + // response for the prerendering navigation has already been received and the + // prerendering document was created. + const readyToActivate = new Promise((resolve, reject) => { + prerenderChannel.addEventListener('message', e => { + if (e.data != 'readyToActivate') + reject(`The initiator page receives an unsupported message: ${e.data}`); + resolve(e.data); + }); + }); + + const url = new URL(document.URL); + url.searchParams.append('prerendering', ''); + // Prerender a page that notifies the initiator page of the page's ready to be + // activated via the 'readyToActivate'. + startPrerendering(url.toString(), rule_extras); + + // Navigate to the prerendered page after being informed. + readyToActivate.then(() => { + if (rule_extras['target_hint'] === '_blank') { + window.open(url.toString(), '_blank', 'noopener'); + } else { + window.location = url.toString(); + } + }).catch(e => { + const testChannel = new PrerenderChannel('test-channel'); + testChannel.postMessage( + `Failed to navigate the prerendered page: ${e.toString()}`); + testChannel.close(); + window.close(); + }); +} + +// Returns messages received from the given PrerenderChannel +// so that callers do not need to add their own event listeners. +// nextMessage() returns a promise which resolves with the next message. +// +// Usage: +// const channel = new PrerenderChannel('channel-name'); +// const messageQueue = new BroadcastMessageQueue(channel); +// const message1 = await messageQueue.nextMessage(); +// const message2 = await messageQueue.nextMessage(); +// message1 and message2 are the messages received. +class BroadcastMessageQueue { + constructor(c) { + this.messages = []; + this.resolveFunctions = []; + this.channel = c; + this.channel.addEventListener('message', e => { + if (this.resolveFunctions.length > 0) { + const fn = this.resolveFunctions.shift(); + fn(e.data); + } else { + this.messages.push(e.data); + } + }); + } + + // Returns a promise that resolves with the next message from this queue. + nextMessage() { + return new Promise(resolve => { + if (this.messages.length > 0) + resolve(this.messages.shift()) + else + this.resolveFunctions.push(resolve); + }); + } +} + +// Returns <iframe> element upon load. +function createFrame(url) { + return new Promise(resolve => { + const frame = document.createElement('iframe'); + frame.src = url; + frame.onload = () => resolve(frame); + document.body.appendChild(frame); + }); +} + +// `opt` provides additional query params for the prerendered URL. +// `init_opt` provides additional query params for the page that triggers +// the prerender. If `init_opt.prefetch` is set to true, prefetch is also +// triggered before the prerendering. +// `rule_extras` provides additional parameters for the speculation rule used +// to trigger prerendering. +async function create_prerendered_page(t, opt = {}, init_opt = {}, rule_extras = {}) { + const baseUrl = '/speculation-rules/prerender/resources/exec.py'; + const init_uuid = token(); + const prerender_uuid = token(); + const discard_uuid = token(); + const init_remote = new RemoteContext(init_uuid); + const prerender_remote = new RemoteContext(prerender_uuid); + const discard_remote = new RemoteContext(discard_uuid); + + const init_params = new URLSearchParams(baseUrl.search); + init_params.set('uuid', init_uuid); + for (const p in init_opt) + init_params.set(p, init_opt[p]); + window.open(`${baseUrl}?${init_params.toString()}&init`, '_blank', 'noopener'); + + const params = new URLSearchParams(baseUrl.search); + params.set('uuid', prerender_uuid); + params.set('discard_uuid', discard_uuid); + for (const p in opt) + params.set(p, opt[p]); + const url = `${baseUrl}?${params.toString()}`; + + if (init_opt.prefetch) { + await init_remote.execute_script((url, rule_extras) => { + const a = document.createElement('a'); + a.href = url; + a.innerText = 'Activate (prefetch)'; + document.body.appendChild(a); + const rules = document.createElement('script'); + rules.type = "speculationrules"; + rules.text = JSON.stringify( + {prefetch: [{source: 'list', urls: [url], ...rule_extras}]}); + document.head.appendChild(rules); + }, [url, rule_extras]); + + // Wait for the completion of the prefetch. + await new Promise(resolve => t.step_timeout(resolve, 3000)); + } + + await init_remote.execute_script((url, rule_extras) => { + const a = document.createElement('a'); + a.href = url; + a.innerText = 'Activate'; + document.body.appendChild(a); + const rules = document.createElement('script'); + rules.type = "speculationrules"; + rules.text = JSON.stringify({prerender: [{source: 'list', urls: [url], ...rule_extras}]}); + document.head.appendChild(rules); + }, [url, rule_extras]); + + await Promise.any([ + prerender_remote.execute_script(() => { + window.import_script_to_prerendered_page = src => { + const script = document.createElement('script'); + script.src = src; + document.head.appendChild(script); + return new Promise(resolve => script.addEventListener('load', resolve)); + } + }), new Promise(r => t.step_timeout(r, 3000)) + ]); + + t.add_cleanup(() => { + init_remote.execute_script(() => window.close()); + discard_remote.execute_script(() => window.close()); + prerender_remote.execute_script(() => window.close()); + }); + + async function tryToActivate() { + const prerendering = prerender_remote.execute_script(() => new Promise(resolve => { + if (!document.prerendering) + resolve('activated'); + else document.addEventListener('prerenderingchange', () => resolve('activated')); + })); + + const discarded = discard_remote.execute_script(() => Promise.resolve('discarded')); + + init_remote.execute_script(url => { + location.href = url; + }, [url]); + return Promise.any([prerendering, discarded]); + } + + async function activate() { + const prerendering = await tryToActivate(); + if (prerendering !== 'activated') + throw new Error('Should not be prerendering at this point') + } + + // Get the number of network requests for the prerendered page URL. + async function getNetworkRequestCount() { + return await (await fetch(url + '&get-fetch-count')).text(); + } + + return { + exec: (fn, args) => prerender_remote.execute_script(fn, args), + activate, + tryToActivate, + getNetworkRequestCount + }; +} + + +function test_prerender_restricted(fn, expected, label) { + promise_test(async t => { + const {exec} = await create_prerendered_page(t); + let result = null; + try { + await exec(fn); + result = "OK"; + } catch (e) { + result = e.name; + } + + assert_equals(result, expected); + }, label); +} + +function test_prerender_defer(fn, label) { + promise_test(async t => { + const {exec, activate} = await create_prerendered_page(t); + let activated = false; + const deferred = exec(fn); + + const post = new Promise(resolve => + deferred.then(result => { + assert_true(activated, "Deferred operation should occur only after activation"); + resolve(result); + })); + + await activate(); + activated = true; + await post; + }, label); +} + +/** + * Starts prerendering a page from the given referrer `RemoteContextWrapper`, + * using `<script type="speculationrules">`. + * + * See + * /html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js + * for more details on the `RemoteContextWrapper` framework, and supported fields for extraConfig. + * + * The returned `RemoteContextWrapper` for the prerendered remote + * context will have an extra `url` property, which is used by + * @see activatePrerenderRC. (Most `RemoteContextWrapper` uses should not care + * about the URL, but prerendering is unique in that you need to navigate to + * a prerendered page after creating it.) + * + * @param {RemoteContextWrapper} referrerRemoteContext + * @param {RemoteContextConfig|object} extraConfig + * @returns {Promise<RemoteContextWrapper>} + */ +function addPrerenderRC(referrerRemoteContext, extraConfig) { + return referrerRemoteContext.helper.createContext({ + executorCreator(url) { + return referrerRemoteContext.executeScript(url => { + const script = document.createElement("script"); + script.type = "speculationrules"; + script.textContent = JSON.stringify({ + prerender: [ + { + source: "list", + urls: [url] + } + ] + }); + document.head.append(script); + }, [url]); + }, extraConfig + }); +} + +/** + * Activates a prerendered RemoteContextWrapper `prerenderedRC` by navigating + * the referrer RemoteContextWrapper `referrerRC` to it. If the navigation does + * not result in a prerender activation, the returned + * promise will be rejected with a testharness.js AssertionError. + * + * See + * /html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js + * for more on the RemoteContext helper framework. + * + * @param {RemoteContextWrapper} referrerRC - The referrer + * `RemoteContextWrapper` in which the prerendering was triggered, + * probably via `addPrerenderRC()`. + * @param {RemoteContextWrapper} prerenderedRC - The `RemoteContextWrapper` + * pointing to the prerendered content. This is monitored to ensure the + * navigation results in a prerendering activation. + * @param {(string) => Promise<undefined>} [navigateFn] - An optional function + * to customize the navigation. It will be passed the URL of the prerendered + * content, and will run as a script in `referrerRC` (see + * `RemoteContextWrapper.prototype.executeScript`). If not given, navigation + * will be done via the `location.href` setter (see + * `RemoteContextWrapper.prototype.navigateTo`). + * @returns {Promise<undefined>} + */ +async function activatePrerenderRC(referrerRC, prerenderedRC, navigateFn) { + // Store a promise that will fulfill when the prerenderingchange event fires. + await prerenderedRC.executeScript(() => { + window.activatedPromise = new Promise(resolve => { + document.addEventListener("prerenderingchange", () => resolve("activated")); + }); + }); + + if (navigateFn === undefined) { + referrerRC.navigateTo(prerenderedRC.url); + } else { + referrerRC.navigate(navigateFn, [prerenderedRC.url]); + } + + // Wait until that event fires. If the activation fails and a normal + // navigation happens instead, then prerenderedRC will start pointing to that + // other page, where window.activatedPromise is undefined. In that case this + // assert will fail since undefined !== "activated". + assert_equals( + await prerenderedRC.executeScript(() => window.activatedPromise), + "activated", + "The prerendered page must be activated; instead a normal navigation happened." + ); +} + +async function getActivationStart(prerenderedRC) { + return await prerenderedRC.executeScript(() => { + const entry = performance.getEntriesByType("navigation")[0]; + return entry.activationStart; + });; +} + +// Used by the opened window, to tell the main test runner to terminate a +// failed test. +function failTest(reason, uid) { + const bc = new PrerenderChannel('test-channel', uid); + bc.postMessage({result: 'FAILED', reason}); + bc.close(); +} + +// Retrieves a target hint from URLSearchParams of the current window and +// returns it. Throw an Error if it doesn't have the valid target hint param. +function getTargetHint() { + const params = new URLSearchParams(window.location.search); + const target_hint = params.get('target_hint'); + if (target_hint === null) + throw new Error('window.location does not have a target hint param'); + if (target_hint !== '_self' && target_hint !== '_blank') + throw new Error('window.location does not have a valid target hint param'); + return target_hint; +} diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/wake-lock.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/wake-lock.https.html new file mode 100644 index 0000000000..4e0d6076a6 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/wake-lock.https.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-wake-lock.https.html) loads the +// initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + const promise = new Promise((resolve, reject) => { + navigator.wakeLock.request('screen') + .then(() => { + reject('unexpected success'); + }) + .catch((e) => { + prerenderEventCollector.addEvent('navigator.wakeLock.request failed'); + }); + + document.addEventListener('prerenderingchange', () => { + prerenderEventCollector.addEvent( + 'requesting navigator.wakeLock.request on prerendering change'); + navigator.wakeLock.request('screen') + .then(function() { + lock => lock.release(); resolve(); + }); + }); + }); + prerenderEventCollector.start(promise, 'navigator.wakeLock.request test'); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/web-hid.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/web-hid.https.html new file mode 100644 index 0000000000..e9531293ea --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/web-hid.https.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-web-hid.https.html) loads the initiator page, +// then the initiator page will prerender itself with the `prerendering` +// parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + prerenderEventCollector.start( + navigator.hid.getDevices(), 'navigator.hid.getDevices'); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/web-locks.html b/testing/web-platform/tests/speculation-rules/prerender/resources/web-locks.html new file mode 100644 index 0000000000..621dd18b4d --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/web-locks.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-web-locks.html) loads the initiator page, +// then the initiator page will prerender itself with the `prerendering` +// parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + const method = params.get('method'); + const prerenderEventCollector = new PrerenderEventCollector(); + let promise; + switch(method) { + case 'request': + promise = navigator.locks.request('prerender-test-lock', lock => {}); + break; + case 'query': + promise = navigator.locks.query(); + break; + } + prerenderEventCollector.start(promise, `navigator.locks.${method}`); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/web-nfc.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/web-nfc.https.html new file mode 100644 index 0000000000..61207ab346 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/web-nfc.https.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-web-nfc.https.html) loads the initiator page, +// then the initiator page will prerender itself with the `prerendering` +// parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + const promise = new Promise(async (resolve) => { + // We expect an error from NDEFReader.write() and scan() since we don't + // enable NFC HW. + const ndef = new NDEFReader(); + const result1 = await ndef.write("Test") + .then(() => 'ndef.write() unexpectedly succeeded') + .catch(e => 'ndef.write() failed'); + prerenderEventCollector.addEvent(result1); + const result2 = await ndef.scan() + .then(() => 'ndef.scan() unexpectedly succeeded') + .catch(e => 'ndef.scan() failed'); + prerenderEventCollector.addEvent(result2); + resolve(); + }); + prerenderEventCollector.start(promise, 'NDEFReader.[write|scan]'); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/web-serial.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/web-serial.https.html new file mode 100644 index 0000000000..cfae1372c2 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/web-serial.https.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-web-serial.https.html) loads the +// initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + // The spec, https://wicg.github.io/nav-speculation/prerendering.html#patch-serial, + // says that Web Serial API has delay while prerendering for + // requestPort(). As this test uses getPorts(), it's a tentative test. + prerenderEventCollector.start( + navigator.serial.getPorts(), 'navigator.serial.getPorts'); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/web-share.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/web-share.https.html new file mode 100644 index 0000000000..f4e2f30471 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/web-share.https.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script> + +assert_true(document.prerendering); + +async function invokeWebShareAPI(){ + const bc = new PrerenderChannel('prerender-channel'); + + try { + const _ = await navigator.share({url: 'https://a.test'}); + bc.postMessage('unexpected success'); + } catch (err){ + bc.postMessage(err.name); + } finally { + bc.close(); + } +} + +invokeWebShareAPI(); + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/web-usb.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/web-usb.https.html new file mode 100644 index 0000000000..555825a81c --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/web-usb.https.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-web-usb.https.html) loads the initiator +// page, then the initiator page will prerender itself with the `prerendering` +// parameter. +if (!params.has('prerendering')) { + loadInitiatorPage(); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + prerenderEventCollector.start( + navigator.usb.getDevices(), 'navigator.usb.getDevices'); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/web-xr-immersive-vr-session.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/web-xr-immersive-vr-session.https.html new file mode 100644 index 0000000000..decb33657a --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/web-xr-immersive-vr-session.https.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +// The main test page (restriction-web-xr-immersive-vr-session.https.html) loads +// the initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const params = new URLSearchParams(location.search); +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + const rule_extras = {'target_hint': getTargetHint()}; + loadInitiatorPage(rule_extras); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + prerenderEventCollector.start( + navigator.xr.requestSession('immersive-vr'), + `XRSession.requestSession('immersive-vr')`); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/web-xr-inline-session.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/web-xr-inline-session.https.html new file mode 100644 index 0000000000..cde1f15a2a --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/web-xr-inline-session.https.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +// The main test page (restriction-web-xr-inline-session.https.html) loads the +// initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const params = new URLSearchParams(location.search); +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + const rule_extras = {'target_hint': getTargetHint()}; + loadInitiatorPage(rule_extras); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + prerenderEventCollector.start( + navigator.xr.requestSession('inline'), + `XRSession.requestSession('inline')`); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/window-move.html b/testing/web-platform/tests/speculation-rules/prerender/resources/window-move.html new file mode 100644 index 0000000000..0c5888c957 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/window-move.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-window-move.html) loads the +// initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +function tryRun(func) { + try { + func(); + } catch (e) { + const testChannel = new PrerenderChannel('test-channel'); + testChannel.postMessage({status: 'FAIL: ' + e}); + } +} + +if (!isPrerendering) { + // Ensure that the primary page can move this window. + tryRun(() => { + const expectedPosition = {x: screen.availLeft + 1, y: screen.availTop + 1}; + window.moveTo(expectedPosition.x, expectedPosition.y); + assert_equals(window.screenX, expectedPosition.x, 'x position for primary'); + assert_equals(window.screenY, expectedPosition.y, 'y position for primary'); + }); + // Start prerendering a page which tries to move this window. + loadInitiatorPage(); +} else { + const prevPosition = {x: window.screenX, y: window.screenY}; + tryRun( + () => { + // Try to move this window, and should not succeed. + const moveToOrMoveBy = params.get('move'); + switch (moveToOrMoveBy) { + case 'moveTo': + window.moveTo(screen.availLeft + 10, screen.availTop + 10); + break; + case 'moveBy': + window.moveBy(screen.availLeft + 10 - window.screenX, + screen.availTop + 10 - window.screenY); + break; + default: + assert_unreached(`wrong parameter: ${moveToOrMoveBy}`); + } + } + ); + + const bc = new PrerenderChannel('test-channel'); + bc.postMessage({ + 'status': 'PASS', + 'prevPosition': prevPosition, + 'newPosition': {x: window.screenX, y: window.screenY} + }); + bc.close(); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/window-open-during-prerendering.html b/testing/web-platform/tests/speculation-rules/prerender/resources/window-open-during-prerendering.html new file mode 100644 index 0000000000..a314d5005b --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/window-open-during-prerendering.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="utils.js"></script> +<script> + +// This file is loaded twice. First this is loaded as a page to trigger +// prerendering and then loaded as a prerendering page. + +function runAsTriggerPage() { + assert_false(document.prerendering); + startPrerendering(location.href + '&prerendering=true'); + + // Close this window for cleanup after the prerendering page runs the test. + const bc = new PrerenderChannel('result'); + bc.onmessage = e => window.close(); +} + +function runAsPrerenderingPage() { + assert_true(document.prerendering); + + // Attempt to open a window during prerendering. + const win = window.open('empty.html', '_blank'); + + // Send the result to the test runner page. + const bc = new PrerenderChannel('result'); + if (win) { + bc.postMessage('opened'); + win.close(); + } else { + bc.postMessage('failed to open'); + } +} + +if (new URLSearchParams(location.search).has('prerendering')) { + runAsPrerenderingPage(); +} else { + runAsTriggerPage(); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/window-open-in-prerenderingchange.html b/testing/web-platform/tests/speculation-rules/prerender/resources/window-open-in-prerenderingchange.html new file mode 100644 index 0000000000..f32126f93d --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/window-open-in-prerenderingchange.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="utils.js"></script> +<script> + +// This file is loaded twice. First this is loaded as a page to trigger +// prerendering and then loaded as a prerendering page. The trigger page +// activates the prerendering page. + +// Runs as the trigger page. This page starts prerendering and waits for signal +// from the prerendering page. After the signal, this page starts activation. +function runAsTriggerPage() { + assert_false(document.prerendering); + + // Start prerendering. + const prerendering_url = location.href + '&prerendering=true'; + startPrerendering(prerendering_url); + + // Activate the prerendering page once it gets ready. + const bc = new PrerenderChannel('activation-ready'); + bc.onmessage = () => { window.location = prerendering_url }; +} + +// Runs as prerendeirng page. First this page waits for the load event and +// signals to the trigger page for starting activation. Then, this page fires +// the prerenderingchange event and tests window.open() in the event. +function runAsPrerenderingPage() { + assert_true(document.prerendering); + + window.onload = () => { + assert_true(document.prerendering); + + // Notify the trigger page of activation ready. + const bc = new PrerenderChannel('activation-ready'); + bc.postMessage('ready for activation'); + } + + new PrerenderChannel('close').addEventListener('message', () => { + window.close(); + }); + document.onprerenderingchange = () => { + assert_false(document.prerendering); + + // Attempt to open a window in the prerenderingchange event. + const win = window.open('empty.html', '_blank'); + + // Send the result to the test runner page. + const bc = new PrerenderChannel('result'); + if (win) { + bc.postMessage('opened'); + win.close(); + } else { + bc.postMessage('failed to open'); + } + }; +} + +if (new URLSearchParams(location.search).has('prerendering')) { + runAsPrerenderingPage(); +} else { + runAsTriggerPage(); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/window-resize.html b/testing/web-platform/tests/speculation-rules/prerender/resources/window-resize.html new file mode 100644 index 0000000000..8b6172ee16 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/window-resize.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="utils.js"></script> +<script> + +function tryRun(func) { + try { + func(); + } catch (e) { + const testChannel = new PrerenderChannel('test-channel'); + testChannel.postMessage({status: 'FAIL: ' + e}); + } +} + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-window-resize.html) loads the +// initiator page, then the initiator page will prerender itself with the +// `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +if (!isPrerendering) { + // Ensure that the primary page can resize this window. + tryRun(() => { + const expectedRect = { + width: window.outerWidth - 1, + height: window.outerHeight - 1 + }; + window.resizeTo(expectedRect.width, expectedRect.height); + assert_equals(window.outerWidth, expectedRect.width, 'width for primary'); + assert_equals( + window.outerHeight, expectedRect.height, 'height for primary'); + }); + + // Start prerendering a page which tries to resize this window. + loadInitiatorPage(); +} else { + const prevRect = {width: window.outerWidth, height: window.outerHeight}; + tryRun(() => { + // Try to resize this window, and should not succeed. + const resizeToOrResizeBy = params.get('resize'); + switch (resizeToOrResizeBy) { + case 'resizeTo': + window.resizeTo(prevRect.width + 1, prevRect.height + 1); + break; + case 'resizeBy': + window.resizeBy(1, 1); + break; + default: + assert_unreached(`wrong parameter: ${resizeToOrResizeBy}`); + } + }); + + const bc = new PrerenderChannel('test-channel'); + bc.postMessage({ + 'status': 'PASS', + 'prevRect': prevRect, + 'newRect': {width: window.outerWidth, height: window.outerHeight} + }); + bc.close(); +} + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/windowclient-navigate-on-iframe.html b/testing/web-platform/tests/speculation-rules/prerender/resources/windowclient-navigate-on-iframe.html new file mode 100644 index 0000000000..cc49e202c3 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/windowclient-navigate-on-iframe.html @@ -0,0 +1,97 @@ +<!DOCTYPE html> +<title>WindowClient.navigate() on a prerendered iframe</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<body> +<script> +// The main test page loads the initiator page, then the initiator page will +// prerender itself with the `prerendering` parameter and add an iframe. Once +// the prerendered iframe is ready, post a message to a service worker to call +// WindowClient.navigate(). + +const params = new URLSearchParams(location.search); +const prerendering = params.has('prerendering'); +const navigationUrl = params.get('navigationUrl'); +const uid = params.get('uid'); + +const IFRAME_URL = `prerendered-iframe.html?uid=${uid}`; + +function addIframe() { + const iframe = document.createElement('iframe'); + iframe.src = IFRAME_URL; + document.body.appendChild(iframe); +} + +// If the navigation is expected to be deferred, wait to navigate to +// `navigationUrl` until a prerendered iframe is activated by +// PrerenderEventCollector. The result of the navigation is sent to +// "navigation-channel" PrerenderChannel and the prerendering states and events +// is sent to "test-channel" PrerenderChannel by PrerenderEventCollector. +async function startNavigationToCrossOriginUrl() { + assert_not_equals(new URL(navigationUrl).origin, window.location.origin); + + const navigationPromise = new Promise(resolve => { + const bc = new PrerenderChannel('navigation-channel', uid); + bc.addEventListener('message', e => { + assert_equals(e.data, 'navigate() succeeded'); + resolve() + bc.close(); + }); + bc.postMessage(JSON.stringify({ + navigationUrl: navigationUrl, + clientUrl: new URL(IFRAME_URL, window.location).toString(), + respondTo: 'navigation-channel', + })); + }); + + const prerenderEventCollector = new PrerenderEventCollector(); + prerenderEventCollector.start(navigationPromise, 'navigation on iframe'); +} + +// If the navigation is expected to succeed without delay, the navigation result +// is directly sent to "test-channel" PrerenderChannel. +function startNavigationToSameOriginUrl() { + assert_equals(new URL(navigationUrl).origin, window.location.origin); + + const bc = new PrerenderChannel('navigation-channel', uid); + bc.postMessage(JSON.stringify({ + navigationUrl: navigationUrl, + clientUrl: new URL(IFRAME_URL, window.location).toString(), + respondTo: 'test-channel', + })); + bc.close(); +} + +if (prerendering) { + assert_not_equals(null, navigator.serviceWorker.controller, + 'should be controlled by a service worker'); + + const bc = new PrerenderChannel('iframe-channel', uid); + const readyPromise = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + addIframe(); + + readyPromise.then(result => { + assert_equals(result, 'prerender success'); + + if (new URL(navigationUrl).origin === window.location.origin) { + startNavigationToSameOriginUrl(); + } else { + startNavigationToCrossOriginUrl(); + } + + bc.close(); + }); +} else { + loadInitiatorPage(); +} +</script> +</body> diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/windowclient-navigate-worker.js b/testing/web-platform/tests/speculation-rules/prerender/resources/windowclient-navigate-worker.js new file mode 100644 index 0000000000..8a17b13bbb --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/windowclient-navigate-worker.js @@ -0,0 +1,38 @@ +importScripts("/speculation-rules/prerender/resources/utils.js"); + +const params = new URLSearchParams(location.search); +const uid = params.get('uid'); + +const bc = new PrerenderChannel('navigation-channel', uid); + +bc.onmessage = async e => { + const data = JSON.parse(e.data); + const navigationUrl = data.navigationUrl; + const clientUrl = data.clientUrl; + const respondTo = data.respondTo; + + const clients = await self.clients.matchAll(); + const client = clients.find(c => c.url == clientUrl); + if (!client) { + const bc = new PrerenderChannel(respondTo, uid); + bc.postMessage('Client was not found'); + bc.close(); + return; + } + + let result; + try { + await client.navigate(navigationUrl); + result = 'navigate() succeeded'; + } catch (e) { + if (e instanceof TypeError) { + result = 'navigate() failed with TypeError'; + } else { + result = 'navigate() failed with unknown error'; + } + } finally { + const bc = new PrerenderChannel(respondTo, uid); + bc.postMessage(result); + bc.close(); + } +}; diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/worker-post-timeOrigin.js b/testing/web-platform/tests/speculation-rules/prerender/resources/worker-post-timeOrigin.js new file mode 100644 index 0000000000..86c7d3136d --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/worker-post-timeOrigin.js @@ -0,0 +1 @@ +postMessage(performance.timeOrigin); diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/workers-in-cross-origin-iframe.html b/testing/web-platform/tests/speculation-rules/prerender/resources/workers-in-cross-origin-iframe.html new file mode 100644 index 0000000000..8f27533ed1 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/resources/workers-in-cross-origin-iframe.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<title>Construction of Web Workers is deferred</title> +<script src="utils.js"></script> +<body> +<script type="module"> + +const bc = new PrerenderChannel('test-channel'); +const worker = new Worker('worker-post-timeOrigin.js'); +worker.onerror = e => bc.postMessage('Fail'); +await new Promise(resolve => worker.onmessage = resolve); +bc.postMessage('Success'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/speculation-rules/prerender/response-code-non-successful.html b/testing/web-platform/tests/speculation-rules/prerender/response-code-non-successful.html new file mode 100644 index 0000000000..05d997a008 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/response-code-non-successful.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<title>Check that non-successful responses result in discarding the prerender</title> +<meta name="variant" content="?code=204"> +<meta name="variant" content="?code=205"> +<meta name="variant" content="?code=402"> +<meta name="variant" content="?code=404"> +<meta name="variant" content="?code=500"> +<meta name="variant" content="?code=503"> +<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.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +const params = new URLSearchParams(window.location.search); +const code = params.get('code'); + +promise_test(async t => { + const {exec, tryToActivate} = await create_prerendered_page(t, {code}); + const result = await tryToActivate(); + assert_equals(result, 'discarded'); +},`Responses with code ${code} should be discarded`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/response-code-successful.html b/testing/web-platform/tests/speculation-rules/prerender/response-code-successful.html new file mode 100644 index 0000000000..0e8433b3b2 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/response-code-successful.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<title>Check that successful responses result in activating the prerender</title> +<meta name="variant" content="?code=200"> +<meta name="variant" content="?code=201"> +<meta name="variant" content="?code=202"> +<meta name="variant" content="?code=203"> +<meta name="variant" content="?code=232"> +<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.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +const params = new URLSearchParams(window.location.search); +const code = params.get('code'); + +promise_test(async t => { + const {exec, tryToActivate} = await create_prerendered_page(t, {code}); + const result = await tryToActivate(); + assert_equals(result, 'activated'); +},`Responses with code ${code} should be activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-audio-setSinkId-with-invalid-sinkId.https.tentative.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-audio-setSinkId-with-invalid-sinkId.https.tentative.html new file mode 100644 index 0000000000..f4a362894c --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-audio-setSinkId-with-invalid-sinkId.https.tentative.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<title> +Access to the setSinkId of the Audio API with an invalid value is deferred +</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + const url = `resources/audio-setSinkId.https.html?sinkId=invalid&uid=${uid}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting Audio.setSinkId', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'Audio.setSinkId rejected: NotFoundError', + prerendering: false + }, + ]; + // The spec, https://wicg.github.io/nav-speculation/prerendering.html#audio-output-patch, + // mentions selectAudioOutput() but this test uses setSinkId() function. + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event${i}`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering${i}`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the setSinkId of Audio API with the invalid sinkId should be + deferred until the prerendered page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-audio-setSinkId.https.tentative.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-audio-setSinkId.https.tentative.html new file mode 100644 index 0000000000..a3b6db5041 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-audio-setSinkId.https.tentative.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<title>Access to the setSinkId of the Audio API is deferred</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + const url = `resources/audio-setSinkId.https.html?sinkId=default&uid=${uid}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting Audio.setSinkId', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'finished waiting Audio.setSinkId', + prerendering: false + }, + ]; + // The spec, https://wicg.github.io/nav-speculation/prerendering.html#audio-output-patch, + // mentions selectAudioOutput() but this test uses setSinkId() function. + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event${i}`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering${i}`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the setSinkId of Audio API should be deferred until the + prerendered page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-background-fetch.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-background-fetch.https.html new file mode 100644 index 0000000000..466e886dcb --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-background-fetch.https.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<title>Access to the Background Fetch API is deferred</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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/utils.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + // We grant the permission here to make a more discerning test because + // backgroundFetch.fetch() waits until the permission is granted, which + // is deferred during prerendering so the test would trivially pass without + // the permission. + await test_driver.set_permission({ name: "background-fetch" }, "granted"); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + // Install the service worker first to test backgroundFetch.fetch in the + // prerendering page. + const scope = 'resources/'; + const script = 'resources/do-nothing-worker.js'; + const registration = + await service_worker_unregister_and_register(t, script, scope); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + + const url = `resources/background-fetch.https.html?uid=${uid}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + {event: 'started waiting backgroundFetch.fetch', prerendering: true}, + {event: 'prerendering change', prerendering: false}, + {event: 'finished waiting backgroundFetch.fetch', prerendering: false}, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `The access to the Background Fetch API should be deferred until the + prerendered page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-background-sync.tentative.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-background-sync.tentative.https.html new file mode 100644 index 0000000000..53dbb56d99 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-background-sync.tentative.https.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<title>Access to the Background Sync API is deferred</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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/utils.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + // We grant the permission here to make a more discerning test because + // periodicSync.register() waits until the permission is granted, which + // is deferred during prerendering so the test would trivially pass without + // the permission. + await test_driver.set_permission({name: 'periodic-background-sync'}, 'granted'); + + // Install the service worker first to test periodicSync.register in the + // prerendering page. + const scope = `resources/`; + const script = `resources/do-nothing-worker.js`; + const registration = + await service_worker_unregister_and_register(t, script, scope); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + + const url = `resources/background-sync.https.html?uid=${uid}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + {event: 'started waiting periodicSync.register', prerendering: true}, + {event: 'prerendering change', prerendering: false}, + {event: 'finished waiting periodicSync.register', prerendering: false}, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `The access to the Background Sync API should be deferred until the + prerendered page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-battery-status.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-battery-status.https.html new file mode 100644 index 0000000000..995b3d85d9 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-battery-status.https.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<title>Access to the Battery Status API is deferred</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/battery-status.https.html?uid=${uid}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + {event: 'started waiting navigator.getBattery', prerendering: true}, + {event: 'prerendering change', prerendering: false}, + {event: 'finished waiting navigator.getBattery', prerendering: false}, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + bc.close(); + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the Battery Status API should be deferred until the + prerendered page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-bluetooth.tentative.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-bluetooth.tentative.https.html new file mode 100644 index 0000000000..b642e87ef5 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-bluetooth.tentative.https.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<title>Access to the Bluetooth API is deferred</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/bluetooth-access.https.html?uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting navigator.bluetooth.getAvailability', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'finished waiting navigator.bluetooth.getAvailability', + prerendering: false + }, + ]; + // The spec, https://wicg.github.io/nav-speculation/prerendering.html#web-bluetooth-patch, + // mentions getDevices() and requestDevice() but this test uses + // getAvailability() instead of them. + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + bc.close(); + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the Bluetooth API should be deferred until the + prerendered page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-broadcast-channel.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-broadcast-channel.html new file mode 100644 index 0000000000..167f5f9bc4 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-broadcast-channel.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<title>BroadcastChannel#postMessage is deferred</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + const url = `resources/broadcast-channel.html?uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting BroadcastChannel', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'received message: hello', + prerendering: false + }, + { + event: 'finished waiting BroadcastChannel', + prerendering: false + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event${i}`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering${i}`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `BroadcastChannel#postMessage should be deferred until the prerendered ` + + `page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-dedicated-worker.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-dedicated-worker.https.html new file mode 100644 index 0000000000..05cfc39379 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-dedicated-worker.https.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<title>Access to the Dedicated Worker API is deferred</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/dedicated-worker.https.html?uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting worker construction', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'finished waiting worker construction', + prerendering: false + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `The access to the Dedicated Worker API should be deferred until the + prerendered page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-encrypted-media-unsupported-config.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-encrypted-media-unsupported-config.https.html new file mode 100644 index 0000000000..312f979d08 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-encrypted-media-unsupported-config.https.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<title>Access to the Encrypted Media API is deferred with unsupported config +</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/encrypted-media.https.html?config=unsupport&uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting navigator.requestMediaKeySystemAccess', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'navigator.requestMediaKeySystemAccess rejected: ' + + 'NotSupportedError', + prerendering: false + } + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event${i}`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering${i}`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the Encrypted Media API should be deferred with the + unsupported configurations until the prerendered page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-encrypted-media.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-encrypted-media.https.html new file mode 100644 index 0000000000..33836372c8 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-encrypted-media.https.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<title>Access to the Encrypted Media API is deferred</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/encrypted-media.https.html?config=support&uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting navigator.requestMediaKeySystemAccess', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'finished waiting navigator.requestMediaKeySystemAccess', + prerendering: false + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event${i}`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering${i}`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the Encrypted Media API should be deferred until the + prerendered page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-focus.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-focus.html new file mode 100644 index 0000000000..b606d1f8bf --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-focus.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<title>Prerendering documents are not focused</title> +<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.js"></script> + +<body> +<input type="text" id = "prerenderTextField"> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + document.getElementById('prerenderTextField').focus(); + assert_true( + document.hasFocus(), + 'Initial document should have focus.'); + + const {exec} = await create_prerendered_page(t); + const result = await exec(() => { + const element = document.createElement('input'); + element.setAttribute('type', 'text'); + document.body.appendChild(element); + element.focus(); + + // Post the focus and active states to the initiator page. + return { + activeElementUpdated: document.activeElement === element, + documentHasFocus: document.hasFocus() + }; + }) + + assert_true(result.activeElementUpdated, 'Active element has been updated'); + assert_false(result.documentHasFocus, 'Document should not have focus'); +}, 'Prerendering document should update the active element but not have focus'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-idle-detection.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-idle-detection.https.html new file mode 100644 index 0000000000..953d3dded4 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-idle-detection.https.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<title>Prerendering cannot invoke the Idle Detection API</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('prerender-channel', uid); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + // Start prerendering a page that attempts to invoke the Idle Detection API. + // This API is activation-gated so it's expected to fail: + // https://wicg.github.io/nav-speculation/prerendering.html#activation-gated + startPrerendering(`resources/idle-detection.https.html?uid=${uid}`); + const result = await gotMessage; + assert_equals(result, 'NotAllowedError'); + bc.close(); +}, `prerendering pages should not be able to invoke the Idle Detection API`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-local-file-system-access.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-local-file-system-access.https.html new file mode 100644 index 0000000000..8aecf98b24 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-local-file-system-access.https.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<title>Same-origin prerendering cannot access local file system</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('prerender-channel', uid); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + // Start prerendering a page that attempts to show a local file picker. + startPrerendering(`resources/file-picker.html?uid=${uid}`); + const result = await gotMessage; + assert_equals(result, 'SecurityError') +}, `prerendering pages should not be able to access the local file system ` + + `via the File System Access API`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-media-auto-play-attribute.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-auto-play-attribute.html new file mode 100644 index 0000000000..10db174db9 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-auto-play-attribute.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<!-- +The test aligns with the spec on which behavior is deferred. The test asserts +that media resources are not loaded during prerendering, but it's possible the +spec will allow loading and only disallow playback. +--> +<title>Access to the Autoplay of the Media is deferred</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +function RunTest(type, description) { + promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/media-autoplay-attribute.html?type=${type}&uid=${uid}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting Autoplay', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'fired loadedmetadata event after prerendering is activated', + prerendering: false + }, + { + event: 'finished waiting Autoplay', + prerendering: false + }, + ]; + assert_equals(result.length, expected.length, `${type}`); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event${i}`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering${i}`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); + }, description); +} + +RunTest('audio', `autoplay of the audio media should be deferred until the prerendered page is activated`); + +RunTest('video', `autoplay of the video media should be deferred until the prerendered page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-media-camera.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-camera.https.html new file mode 100644 index 0000000000..5435c737cc --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-camera.https.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<title>Access to the Camera of the user media device is deferred</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/media-devices-access.https.html?video=true&uid=${uid}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + // According to spec, gUM will resolve only if the window has focus. + window.focus(); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting navigator.mediaDevices.getUserMedia', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'finished waiting navigator.mediaDevices.getUserMedia', + prerendering: false + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event${i}`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering${i}`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the camera of the user media should be deferred until the + prerendered page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-media-device-info.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-device-info.https.html new file mode 100644 index 0000000000..f725145aa1 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-device-info.https.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<title>Access to the Media Device Info is deferred</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/media-device-info.https.html?uid=${uid}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting navigator.mediaDevices.enumerateDevices', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'finished waiting navigator.mediaDevices.enumerateDevices', + prerendering: false + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + bc.close(); + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the Media Device Info should be deferred until the prerendered + page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-media-microphone.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-microphone.https.html new file mode 100644 index 0000000000..36607ed95e --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-microphone.https.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<title>Access to the Microphone of the user media device is deferred</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/media-devices-access.https.html?&audio=true&uid=${uid}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + // According to spec, gUM will resolve only if the window has focus. + window.focus(); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting navigator.mediaDevices.getUserMedia', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'finished waiting navigator.mediaDevices.getUserMedia', + prerendering: false + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event${i}`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering${i}`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the Microphone of the user media should be deferred until the + prerendered page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-media-play.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-play.html new file mode 100644 index 0000000000..3db8c22c41 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-play.html @@ -0,0 +1,73 @@ +<!DOCTYPE html> +<!-- +The test aligns with the spec on which behavior is deferred. The test asserts +that media resources are not loaded during prerendering, but it's possible the +spec will allow loading and only disallow playback. +--> +<title>Access to the Play of the Media is deferred</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +function RunTest(type, description) { + promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + const url = `resources/media-play.html?type=${type}&uid=${uid}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting Media.Play', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'fired loadedmetadata event after prerendering is activated', + prerendering: false + }, + { + event: 'finished waiting Media.Play', + prerendering: false + }, + ]; + + assert_equals(result.length, expected.length, `${type}`); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event${i}`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering${i}`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); + }, description); +} + +RunTest('audio', `play of the audio media should be deferred until the prerendered page is activated`); + +RunTest('video', `play of the video media should be deferred until the prerendered page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-message-boxes.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-message-boxes.html new file mode 100644 index 0000000000..a40df1acc2 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-message-boxes.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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +function runTest(test_file, expectation, description) { + promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('prerender-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + // Open a new window to test the message box. + window.open(`${test_file}&uid=${uid}`, '_blank', 'noopener'); + + // Wait for the message from the prerendering page. + assert_equals(await gotMessage, expectation); + }, description); +} + +// Test that a page invokes the alert modal during prerendering. +runTest( + 'resources/message-boxes.html?alert', + 'no block', + 'alert() does not display the modal and returns immediately'); + +// Test that a page invokes the confirm modal during prerendering. +runTest( + 'resources/message-boxes.html?confirm', + 'the return value is no', + 'confirm() does not display the modal and returns immediately'); + +// Test that a page invokes the prompt modal during prerendering. +runTest( + 'resources/message-boxes.html?prompt', + 'the return value is null', + 'prompt() does not display the modal and returns immediately'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-midi-sysex.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-midi-sysex.https.html new file mode 100644 index 0000000000..3201267901 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-midi-sysex.https.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<title>Access to the Midi API with sysex=true is deferred</title> +<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/utils.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + // Grant the permission here to make a more discerning test because + // navigator.requestMIDIAccess() waits until the permission is granted, which + // is deferred during prerendering so the test would trivially pass without + // the permission. + await test_driver.set_permission( + {name: 'midi', sysex: true}, 'granted'); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/midi.https.html?sysex=true&uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting navigator.requestMIDIAccess', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'finished waiting navigator.requestMIDIAccess', + prerendering: false + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event${i}`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering${i}`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the Midi API should be deferred until the prerendered page is + activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-midi.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-midi.https.html new file mode 100644 index 0000000000..49c3082d61 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-midi.https.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<title>Access to the Midi API with sysex=false is deferred</title> +<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/utils.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + // Grant the permission here to make a more discerning test because + // navigator.requestMIDIAccess() waits until the permission is granted, which + // is deferred during prerendering so the test would trivially pass without + // the permission. + await test_driver.set_permission( + {name: 'midi', sysex: false}, 'granted'); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/midi.https.html?sysex=false&uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting navigator.requestMIDIAccess', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'finished waiting navigator.requestMIDIAccess', + prerendering: false + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event${i}`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering${i}`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the Midi API should be deferred until the prerendered page is + activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-notification.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-notification.https.html new file mode 100644 index 0000000000..78d7d06ecb --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-notification.https.html @@ -0,0 +1,105 @@ +<!DOCTYPE html> +<!-- +https://wicg.github.io/nav-speculation/prerendering.html#patch-notifications +TODO(https://crbug.com/1198110): Add the following tests: +* Test the constructor returns synchronously while the creation of the + notification is deferred until activation. +--> +<title>Access to the Notification API before and after prerender activation</title> +<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/utils.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + await test_driver.set_permission({ + name: 'notifications' + }, 'granted'); + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/notification-on-activation.html?uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + assert_equals(result, 'notification showed'); +}, `it is allowed to access the notification API in the prerenderingchange + event`); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + await test_driver.set_permission({ + name: 'notifications' + }, 'granted'); + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/notification-before-activation.html?uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + + const expected = [{ + event: 'Notification permission is default', + prerendering: true + }, + { + event: 'started waiting notification', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'permission was granted', + prerendering: false + }, + { + event: 'notification displayed', + prerendering: false + }, + { + event: 'finished waiting notification', + prerendering: false + }, + ]; + + length = Math.min(result.length, expected.length); + let i = 0; + for (i = 0; i < length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + assert_equals(i, expected.length); + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, +`Displaying Notification should be deferred until the prerendered page is + activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-presentation-request.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-presentation-request.https.html new file mode 100644 index 0000000000..0dc961c0c8 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-presentation-request.https.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<title>Same-origin prerendering cannot start presentation</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('prerender-channel', uid); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + // Start prerendering a page that attempts to start presentation. + startPrerendering(`resources/presentation-request.html?uid=${uid}`); + + const result = await gotMessage; + assert_equals(result, 'request failed'); +}, 'prerendering page cannot start presentation'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-prompt-by-before-unload.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-prompt-by-before-unload.html new file mode 100644 index 0000000000..63a4b11fc3 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-prompt-by-before-unload.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<title>Prerendering cannot invoke the prompt generated by the + beforeunload event</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('prerender-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + // Start prerendering a page that attempts to invoke + // the prompt generated by the beforeunload event. + // It is activation-gated and expects to fail. + startPrerendering(`resources/prompt-by-before-unload.html?uid=${uid}`); + + const result = await gotMessage; + assert_equals(result, + 'unloaded without the prompt by beforeunload.'); +}, 'Prerendering cannot invoke the prompt by the beforeunload event.'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-push.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-push.https.html new file mode 100644 index 0000000000..708a0fed2f --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-push.https.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<title>Access to the Push API is deferred</title> +<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/utils.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + // We grant the permission here to make a more discerning test because + // pushManager.subscribe() waits until the permission is granted, which + // is deferred during prerendering so the test would trivially pass without + // the permission. + await test_driver.set_permission({ name: 'push', userVisibleOnly: true }, + 'granted'); + + // Install the service worker first to test pushManager.subscribe in the + // prerendering page. + const scope = `resources/`; + const script = `resources/do-nothing-worker.js`; + const registration = + await service_worker_unregister_and_register(t, script, scope); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + + const url = `resources/push.https.html?uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + {event: 'started waiting pushManager.subscribe', prerendering: true}, + {event: 'prerendering change', prerendering: false}, + {event: 'finished waiting pushManager.subscribe', prerendering: false}, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `The access to the Push API should be deferred until the prerendered page is + activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-request-picture-in-picture.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-request-picture-in-picture.html new file mode 100644 index 0000000000..743254d3d9 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-request-picture-in-picture.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<title>Prerendering cannot invoke + HTMLVideoElement.requestPictureInPicture</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('prerender-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + // Start prerendering a page that attempts to invoke + // HTMLVideoElement.requestPictureInPicture. This API needs + // transient activation. So it's expected to fail. + startPrerendering(`resources/request-picture-in-picture.html?uid=${uid}`); + + const result = await gotMessage; + assert_equals(result, 'Metadata for the video element are not loaded yet'); +}, 'prerendering page cannot invoke' + + 'HTMLVideoElement.requestPictureInPicture'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-screen-capture.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-screen-capture.https.html new file mode 100644 index 0000000000..f1ad821a00 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-screen-capture.https.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<title>Prerendering cannot invoke the Screen Capture API</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('prerender-channel', uid); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + // Start prerendering a page that attempts to invoke the Screen Capture API. + // This API is activated-gated so it's expected to fail: + // https://wicg.github.io/nav-speculation/prerendering.html#implicitly-restricted + const rule_extras = {'target_hint': getTargetHint()}; + startPrerendering( + `resources/screen-capture.https.html?uid=${uid}`, rule_extras); + const result = await gotMessage; + assert_equals(result, 'InvalidStateError'); + bc.close(); +}, `prerendering pages should not be able to invoke the Screen Capture API`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-screen-orientation-lock.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-screen-orientation-lock.https.html new file mode 100644 index 0000000000..f62740f5ce --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-screen-orientation-lock.https.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<title>Access to the Screen Orientation Lock API is deferred</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + const url = `resources/screen-orientation-lock.https.html?uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + {event: 'started waiting screen.orientation.lock', prerendering: true}, + {event: 'prerendering change', prerendering: false}, + {event: 'finished waiting screen.orientation.lock', prerendering: false}, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `The access to the Screen Orienation Lock API should be deferred until the + prerendered page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-accelerometer.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-accelerometer.https.html new file mode 100644 index 0000000000..72e3e86bce --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-accelerometer.https.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<title>Access to the Accelerometer API is deferred</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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/utils.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + // We grant the permission here to make a more discerning test because + // Accelerometer() waits until the permission is granted, which + // is deferred during prerendering so the test would trivially pass without + // the permission. + await test_driver.set_permission({ name: 'accelerometer' }, 'granted'); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/generic-sensor.https.html?sensorName=Accelerometer&uid=${uid}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting Accelerometer test', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'finished waiting Accelerometer test', + prerendering: false + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the Accelerometer API should be deferred until the prerendered + page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-ambient-light-sensor.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-ambient-light-sensor.https.html new file mode 100644 index 0000000000..b398c4bd6c --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-ambient-light-sensor.https.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<title>Access to the Ambient Light Sensor API is deferred</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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/utils.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + // We grant the permission here to make a more discerning test because + // AmbientLightSensor() waits until the permission is granted, which + // is deferred during prerendering so the test would trivially pass without + // the permission. + await test_driver.set_permission({ name: 'ambient-light-sensor' }, 'granted'); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/generic-sensor.https.html?sensorName=AmbientLightSensor&uid=${uid}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting AmbientLightSensor test', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'finished waiting AmbientLightSensor test', + prerendering: false + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the Ambient Light Sensor API should be deferred until the + prerendered page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-gyroscope.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-gyroscope.https.html new file mode 100644 index 0000000000..48e018e018 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-gyroscope.https.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<title>Access to the Gyroscope API is deferred</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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/utils.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + // We grant the permission here to make a more discerning test because + // Gyroscope() waits until the permission is granted, which + // is deferred during prerendering so the test would trivially pass without + // the permission. + await test_driver.set_permission({ name: 'gyroscope' }, 'granted'); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/generic-sensor.https.html?sensorName=Gyroscope&uid=${uid}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting Gyroscope test', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'finished waiting Gyroscope test', + prerendering: false + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the Gyroscope API should be deferred until the prerendered + page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-magnetometer.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-magnetometer.https.html new file mode 100644 index 0000000000..928a73f572 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-magnetometer.https.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<title>Access to the Magnetometer API is deferred</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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/utils.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + // We grant the permission here to make a more discerning test because + // Magnetometer() waits until the permission is granted, which + // is deferred during prerendering so the test would trivially pass without + // the permission. + await test_driver.set_permission({ name: 'magnetometer' }, 'granted'); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/generic-sensor.https.html?sensorName=Magnetometer&uid=${uid}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting Magnetometer test', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'finished waiting Magnetometer test', + prerendering: false + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the Magnetometer API should be deferred until the prerendered + page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-postmessage.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-postmessage.https.html new file mode 100644 index 0000000000..a837f6b0e7 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-postmessage.https.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<title>postMessage() between service worker and prerendered page</title> +<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="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +const uid = token(); + +const PAGE_URL = `resources/postmessage-to-service-worker.html?uid=${uid}`; +const WORKER_URL = 'resources/postmessage-to-client-worker.js'; + +// Message sequence: prerendered page => service worker => prerendered page => +// main page +promise_test(async t => { + const registration = + await service_worker_unregister_and_register(t, WORKER_URL, PAGE_URL); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + window.open(PAGE_URL, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + {event: 'started waiting ServiceWorker.postMessage', prerendering: true}, + {event: 'prerendering change', prerendering: false}, + {event: 'postmessage to client', prerendering: false}, + {event: 'finished waiting ServiceWorker.postMessage', prerendering: false}, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, 'ServiceWorker#postMessage() from a prerendered page should be deferred ' + + 'until page activation.'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-unregister.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-unregister.https.html new file mode 100644 index 0000000000..2b83e3858f --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-unregister.https.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<title>ServiceWorkerRegistration.unregister in a prerendered page</title> +<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="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +const uid = token(); + +const PAGE_URL = `resources/service-worker-unregister.html?uid=${uid}`; +const WORKER_URL = 'resources/do-nothing-worker.js'; + +promise_test(async t => { + const registration = + await navigator.serviceWorker.register(WORKER_URL, {scope: PAGE_URL}); + t.add_cleanup(_ => registration.unregister()); + + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + window.open(PAGE_URL, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + {event: 'started waiting ServiceWorkerRegistration.unregister', prerendering: true}, + {event: 'prerendering change', prerendering: false}, + {event: 'service worker unregistered', prerendering: false}, + {event: 'finished waiting ServiceWorkerRegistration.unregister', prerendering: false}, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, 'ServiceWorkerRegistration.unregister() should be deferred in a ' + + 'prerendered page'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-update.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-update.https.html new file mode 100644 index 0000000000..b2c164603d --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-update.https.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<title>ServiceWorkerRegistration.update in a prerendered page</title> +<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="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +const uid = token(); + +const PAGE_URL = `resources/service-worker-update.html?uid=${uid}`; +const WORKER_URL = 'resources/do-nothing-worker.js'; + +promise_test(async t => { + const registration = + await navigator.serviceWorker.register(WORKER_URL, {scope: PAGE_URL}); + t.add_cleanup(_ => registration.unregister()); + + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + window.open(PAGE_URL, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + {event: 'started waiting ServiceWorkerRegistration.update', prerendering: true}, + {event: 'prerendering change', prerendering: false}, + {event: 'service worker updated', prerendering: false}, + {event: 'finished waiting ServiceWorkerRegistration.update', prerendering: false}, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, 'ServiceWorkerRegistration.update() should be deferred in a prerendered ' + + 'page'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-speech-synthesis.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-speech-synthesis.html new file mode 100644 index 0000000000..9dbdf9fd45 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-speech-synthesis.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<title>Access to the speech synthesis is deferred</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +function RunTest(method, description) { + promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/speech-synthesis.https.html?method=${method}&uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: `started waiting speechSynthesis.${method}`, + prerendering: true + }, + { + event: `prerendering change`, + prerendering: false + }, + { + event: `finished waiting speechSynthesis.${method}`, + prerendering: false + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event${i}`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering${i}`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); + }, description); +} + +RunTest('speak', `speechSynthesis.speak(utterance) should be deferred until the prerendered page is activated`); +RunTest('cancel', `speechSynthesis.cancel() should be deferred until the prerendered page is activated`); +RunTest('pause', `speechSynthesis.pause() should be deferred until the prerendered page is activated`); +RunTest('resume', `speechSynthesis.resume() should be deferred until the prerendered page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-storage-persist.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-storage-persist.https.html new file mode 100644 index 0000000000..5ccbff391b --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-storage-persist.https.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<title>Access to storage.persist() is deferred</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/storage-persist.https.html?uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting navigator.storage.persist', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'finished waiting navigator.storage.persist', + prerendering: false + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + bc.close(); + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the storage.persist() should be deferred until the prerendered + page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-wake-lock.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-wake-lock.https.html new file mode 100644 index 0000000000..5db64be5dc --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-wake-lock.https.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<title>Access to the Wake Lock API is deferred</title> +<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/utils.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + + // We grant the permission here to make a more discerning test because + // navigator.wakeLock.request() waits until the permission is granted, which + // is deferred during prerendering so the test would trivially pass without + // the permission. + await test_driver.set_permission({ name: 'screen-wake-lock' }, 'granted'); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/wake-lock.https.html?uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting navigator.wakeLock.request test', + prerendering: true + }, + { + event: 'navigator.wakeLock.request failed', + prerendering: true + }, + { + event: 'requesting navigator.wakeLock.request on prerendering change', + prerendering: false + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'finished waiting navigator.wakeLock.request test', + prerendering: false + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + bc.close(); + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the Wake Lock API should be deferred until the prerendered + page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-web-hid.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-hid.https.html new file mode 100644 index 0000000000..32aabde45e --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-hid.https.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<title>Access to the Web HID API is deferred</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/web-hid.https.html?uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + {event: 'started waiting navigator.hid.getDevices', prerendering: true}, + {event: 'prerendering change', prerendering: false}, + {event: 'finished waiting navigator.hid.getDevices', prerendering: false}, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + bc.close(); + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the Web HID API should be deferred until the prerendered + page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-web-locks.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-locks.https.html new file mode 100644 index 0000000000..a3976cad6f --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-locks.https.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<title>Access to the Web Locks API is deferred</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +function RunTest(method, description) { + promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/web-locks.html?method=${method}&uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + {event: `started waiting navigator.locks.${method}`, prerendering: true}, + {event: 'prerendering change', prerendering: false}, + {event: `finished waiting navigator.locks.${method}`, prerendering: false}, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + bc.close(); + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); + }, description); +} + +RunTest(`request`, `navigator.locks.request should be deferred until the prerendered page is activated`); +RunTest(`query`, `navigator.locks.query should be deferred until the prerendered page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-web-nfc.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-nfc.https.html new file mode 100644 index 0000000000..605c908892 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-nfc.https.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<title>Access to the Web NFC API is deferred</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/web-nfc.https.html?uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + {event: 'started waiting NDEFReader.[write|scan]', prerendering: true}, + {event: 'prerendering change', prerendering: false}, + {event: 'ndef.write() failed', prerendering: false}, + {event: 'ndef.scan() failed', prerendering: false}, + {event: 'finished waiting NDEFReader.[write|scan]', prerendering: false}, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `The access to the Web NFC API should be deferred until the prerendered + page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-web-serial.tentative.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-serial.tentative.https.html new file mode 100644 index 0000000000..96b7fe6da1 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-serial.tentative.https.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<title>Access to the Web Serial API is deferred</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/web-serial.https.html?uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + {event: 'started waiting navigator.serial.getPorts', prerendering: true}, + {event: 'prerendering change', prerendering: false}, + {event: 'finished waiting navigator.serial.getPorts', prerendering: false}, + ]; + // The spec, https://wicg.github.io/nav-speculation/prerendering.html#patch-serial, + // says that Web Serial API has delay while prerendering for + // requestPort(). As this test uses getPorts(), it's a tentative test. + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + bc.close(); + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the Web Serial API should be deferred until the prerendered + page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-web-share.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-share.https.html new file mode 100644 index 0000000000..e9b22d58ef --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-share.https.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<title>Prerendering cannot invoke the Web Share API</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('prerender-channel', uid); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + // Start prerendering a page that attempts to invoke the Web Share API. + // This API is activated-gated so it's expected to fail: + // https://wicg.github.io/nav-speculation/prerendering.html#activation-gated + startPrerendering(`resources/web-share.https.html?uid=${uid}`); + const result = await gotMessage; + assert_equals(result, 'NotAllowedError'); + bc.close(); +}, `prerendering pages should not be able to invoke the Web Share API`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-web-usb.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-usb.https.html new file mode 100644 index 0000000000..8fd5c4fa76 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-usb.https.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<title>Access to the Web USB API is deferred</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/web-usb.https.html?uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + {event: 'started waiting navigator.usb.getDevices', prerendering: true}, + {event: 'prerendering change', prerendering: false}, + {event: 'finished waiting navigator.usb.getDevices', prerendering: false}, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + bc.close(); + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `the access to the Web USB API should be deferred until the prerendered + page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-web-xr-immersive-vr-session.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-xr-immersive-vr-session.https.html new file mode 100644 index 0000000000..094bdbd1a0 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-xr-immersive-vr-session.https.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<title>Access to the WebXR immersive-vr session API is deferred</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + const url = `resources/web-xr-immersive-vr-session.https.html?uid=${uid}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: `started waiting XRSession.requestSession('immersive-vr')`, + prerendering: true + }, + { + event: `prerendering change`, + prerendering: false + }, + { + event: `XRSession.requestSession('immersive-vr') rejected: SecurityError`, + prerendering: false + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `The access to the WebXR immersive-vr session API should be deferred until + the prerendered page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-web-xr-inline-session.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-xr-inline-session.https.html new file mode 100644 index 0000000000..398f2cb504 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-xr-inline-session.https.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<title>Access to the WebXR inline session API is deferred</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + const url = `resources/web-xr-inline-session.https.html?uid=${uid}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: `started waiting XRSession.requestSession('inline')`, + prerendering: true + }, + { + event: `prerendering change`, + prerendering: false + }, + { + event: `finished waiting XRSession.requestSession('inline')`, + prerendering: false + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `The access to the WebXR inline session API should be deferred until the + prerendered page is activated`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-window-move.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-window-move.html new file mode 100644 index 0000000000..232760db36 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-window-move.html @@ -0,0 +1,48 @@ +<!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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +// moveTo and moveBy operations should be ignored. +// See https://github.com/jeremyroman/alternate-loading-modes/issues/73. +['moveTo', 'moveBy'].forEach(moveFunc => { + promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, {once: true}); + }); + + const url = `resources/window-move.html?move=${moveFunc}&uid=${uid}`; + + // We have to open a new window to run the test, since a window that was + // not created by window.open() cannot be moved. + window.open(url, '_blank', + `left=${window.screen.availLeft}, + top=${window.screen.availTop}, + width=${window.screen.availWidth / 2}, + height=${window.screen.availHeight / 2}, + noopener`); + + const result = await gotMessage; + assert_equals(result.status, 'PASS'); + assert_equals( + result.prevPosition.x, result.newPosition.x, + 'x position for prerendering'); + assert_equals( + result.prevPosition.y, result.newPosition.y, + 'y position for prerendering'); + }, `a prerendering page cannot move its window by executing ${moveFunc}.`); +}); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-window-open.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-window-open.html new file mode 100644 index 0000000000..7397d31e65 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-window-open.html @@ -0,0 +1,40 @@ +<!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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +function runTest(test_file, expectation, description) { + promise_test(async t => { + const uid = token(); + // Run test in a new window for test isolation. + window.open(`${test_file}?uid=${uid}`, '_blank', 'noopener'); + + // Wait until the prerendered page sends the result. + const bc = new PrerenderChannel('result', uid); + t.add_cleanup(() => { + new PrerenderChannel('close', uid).postMessage('close'); + }) + const result = await new Promise(r => bc.addEventListener('message', e => r(e.data))); + assert_equals(result, expectation); + }, description); +} + +// Test that a page opens a window during prerendering. +runTest( + 'resources/window-open-during-prerendering.html', + 'failed to open', + 'window.open() should fail during prerendering'); + +// Test that a page opens a window in the prerenderingchange event. +runTest( + 'resources/window-open-in-prerenderingchange.html', + 'opened', + 'window.open() should succeed in the prerenderingchange event'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-window-resize.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-window-resize.html new file mode 100644 index 0000000000..a2466c48b3 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-window-resize.html @@ -0,0 +1,49 @@ +<!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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +// ResizeTo and ResizeBy operations should be ignored. +// See https://github.com/jeremyroman/alternate-loading-modes/issues/73. +['resizeTo', 'resizeBy'].forEach(resizeFunc => { + promise_test( + async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, {once: true}); + }); + + const url = `resources/window-resize.html?resize=${resizeFunc}&uid=${uid}`; + + // We have to open a new window to run the test, since a window that was + // not created by window.open() cannot be resized. + window.open( + url, '_blank', + `width=${window.screen.availWidth / 2},height=${ + window.screen.availHeight / 2},noopener`); + + const result = await gotMessage; + assert_equals(result.status, 'PASS'); + assert_equals( + result.prevRect.width, result.newRect.width, + 'width for prerendering'); + assert_equals( + result.prevRect.height, result.newRect.height, + 'height for prerendering'); + }, + `a prerendering page cannot resize its window by executing ${ + resizeFunc}.`); +}); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/restrictions.html b/testing/web-platform/tests/speculation-rules/prerender/restrictions.html new file mode 100644 index 0000000000..817a1995bc --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/restrictions.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<title>Same-origin prerendering cannot access some APIs</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +test_prerender_restricted( + () => navigator.clipboard.writeText(location.href), + "NotAllowedError", "prerendering pages should not be able to access the clipboard via the Async Clipboard API"); + +test_prerender_restricted(async () => { + const canvas = document.createElement('canvas'); + document.body.appendChild(canvas); + await canvas.requestPointerLock(); +}, "WrongDocumentError", "prerendering pages should not be able to access the pointer-lock API"); + +test_prerender_restricted(async () => { + const div = document.createElement('div'); + document.body.appendChild(div); + await div.requestFullscreen(); +}, "TypeError", "prerendering pages should not be able to access the FullScreen API"); + +test_prerender_defer(() => new Promise( + resolve => navigator.geolocation.getCurrentPosition(p => resolve(p.toString()))), + "Geolocation API should be deferred"); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/sandbox-iframe.html b/testing/web-platform/tests/speculation-rules/prerender/sandbox-iframe.html new file mode 100644 index 0000000000..469b8aadc3 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/sandbox-iframe.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<!-- +Tests for cross-origin iframes due to sandbox flags is deferred properly. + +This file cannot be upstreamed to WPT until: +* The specification describes the loading of cross-origin iframes. The test + expects that they are not loaded during prerendering. +--> +<title>Load a prerendered iframe with sandbox attributes</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/utils.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + const messageQueue = new BroadcastMessageQueue(bc); + t.add_cleanup(_ => bc.close()); + + const url = `resources/sandbox-iframe.html?uid=${uid}`; + window.open(url, '_blank', 'noopener'); + + const result = await messageQueue.nextMessage(); + const expected = [ + { + event: 'started waiting iframe loaded', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'finished waiting iframe loaded', + prerendering: false + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `same-origin sandbox iframes should not load until activation`); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/script-supports-speculationrules.html b/testing/web-platform/tests/speculation-rules/prerender/script-supports-speculationrules.html new file mode 100644 index 0000000000..2dc856fce5 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/script-supports-speculationrules.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<title>HTMLScriptElement.supports speculationrules</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +test(function() { + assert_true(HTMLScriptElement.supports('speculationrules')); +}, 'HTMLScriptElement.supports returns true for \'speculationrules\''); + +test(function() { + assert_false(HTMLScriptElement.supports(' speculationrules')); + assert_false(HTMLScriptElement.supports('speculationrules ')); + assert_false(HTMLScriptElement.supports('Speculationrules')); + assert_false(HTMLScriptElement.supports('SpeculationRules')); + assert_false(HTMLScriptElement.supports('speculationRules')); + assert_false(HTMLScriptElement.supports('speculation-rules')); +}, 'HTMLScriptElement.supports returns false for unsupported types'); + +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/service-workers.https.html b/testing/web-platform/tests/speculation-rules/prerender/service-workers.https.html new file mode 100644 index 0000000000..0f907c35aa --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/service-workers.https.html @@ -0,0 +1,116 @@ +<!DOCTYPE html> +<title>Service Workers APIs with prerendering</title> +<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="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +// This delay is to prevent a race condition which can cause false passes - +// a service worker might take some time to install, and if activation is too quick it might +// end up occuring after activation by chance. +const ACTIVATION_DELAY = 500; + +promise_test(async t => { + const reg = await service_worker_unregister_and_register( + t, "./resources/service-worker.js", `resources/`); + + t.add_cleanup(() => reg.unregister()); + const {exec} = await create_prerendered_page(t); + const {text, prerendering} = await exec(async () => { + const text = await (await fetch(`ping`)).text(); + return {text, prerendering: document.prerendering}; + }); + + assert_true(prerendering); + assert_equals(text, 'pong'); +}, 'A prerendered page should be able to access an existing Service Worker'); + +promise_test(async t => { + const {exec, activate} = await create_prerendered_page(t); + const scope = `./${token()}/`; + await exec(async scope => { + window.serviceWorkerInstalled = new Promise(resolve => { + navigator.serviceWorker.register('./service-worker.js', {scope}) + .then(reg => { + reg.unregister(); + resolve({prerendering: document.prerendering}); + }); + }); + }, [scope]); + + await new Promise(resolve => t.step_timeout(resolve, ACTIVATION_DELAY)); + + await activate(); + + const {prerendering} = await exec(async () => { return await window.serviceWorkerInstalled}); + assert_false(prerendering, 'Service Worker Installation should occur after activation'); +}, 'Registering a new service worker from a prerendered page should be delayed'); + +promise_test(async t => { + const uid = token(); + const reg = await service_worker_unregister_and_register( + t, "./resources/service-worker.js", `./resources/${uid}/`); + t.add_cleanup(() => reg.unregister()); + + const {exec, activate} = await create_prerendered_page(t); + await exec(async uid => { + window.serviceWorkerUnregistered = (async () => { + const regs = await navigator.serviceWorker.getRegistrations(); + const reg = regs.find(r => r.scope.includes(uid)); + await reg.unregister(); + return {prerendering: document.prerendering}; + })(); + }, [uid]); + + await new Promise(resolve => t.step_timeout(resolve, ACTIVATION_DELAY)); + + await activate(); + + const {prerendering} = await exec(() => window.serviceWorkerUnregistered); + assert_false(prerendering, 'Service Worker deregistration should occur after activation'); +}, 'Unregistering an exsiting service worker from a prerendered page should be delayed'); + +promise_test(async t => { + const uid = token(); + const reg = await service_worker_unregister_and_register( + t, "./resources/service-worker.js", `./resources/${uid}/`); + t.add_cleanup(() => reg.unregister()); + + const {exec, activate} = await create_prerendered_page(t); + await exec(async uid => { + window.serviceWorkerUpdated = (async () => { + const regs = await navigator.serviceWorker.getRegistrations(); + const reg = regs.find(r => r.scope.includes(uid)); + await reg.update(); + return {prerendering: document.prerendering}; + })(); + }, [uid]); + + await new Promise(resolve => t.step_timeout(resolve, ACTIVATION_DELAY)); + + await activate(); + + const {prerendering} = await exec(() => window.serviceWorkerUpdated); + assert_false(prerendering, 'Service Worker updates should occur after activation'); +}, 'Updating an exsiting service worker from a prerendered page should be delayed'); + +promise_test(async t => { + const reg = await service_worker_unregister_and_register( + t, "./resources/service-worker.js", 'resources/'); + + t.add_cleanup(() => reg.unregister()); + const {exec} = await create_prerendered_page(t); + const {clientInfo} = await exec(async () => (await fetch(`client`)).json()); + assert_not_equals(clientInfo.id, null); + assert_equals(clientInfo.visibilityState, 'hidden'); + assert_equals(clientInfo.focused, false); +}, 'A prerendered page should be accessible as a hidden & unfocused SW client'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/session-history-activation.https.html b/testing/web-platform/tests/speculation-rules/prerender/session-history-activation.https.html new file mode 100644 index 0000000000..a1ee4a3190 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/session-history-activation.https.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<title>Test history.length</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> +<script src="resources/session-history-test-util.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +const uid = token(); + +promise_test(async () => { + assert_equals( + await runTestInPrerender("testHistoryLengthInPrerender", uid), + "Passed", + "test in prerender" + ); + assert_equals( + await runTestInActivatedPage("testHistoryLengthInPrerender", uid), + "Passed", + "test in activated page" + ); +}, "history.length should be updated after activation"); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/session-history-location.https.html b/testing/web-platform/tests/speculation-rules/prerender/session-history-location.https.html new file mode 100644 index 0000000000..f32bec2bf9 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/session-history-location.https.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<title>Test history.length</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> +<script src="resources/session-history-test-util.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +const uid = token(); + +promise_test(async () => { + assert_equals( + await runTestInPrerender("testLocationAssignInPrerender", uid), + "Passed" + ); +}, "location.assign navigates independently with replacement in a prerender"); + +promise_test(async () => { + assert_equals( + await runTestInPrerender("testLocationReplaceInPrerender", uid), + "Passed" + ); +}, "location.replace navigates independently in a prerender"); + +promise_test(async () => { + assert_equals( + await runTestInPrerender("testSetLocationHrefInPrerender", uid), + "Passed" + ); +}, "Setting location.href navigates independently with replacement in a prerender"); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/session-history-navigation.https.html b/testing/web-platform/tests/speculation-rules/prerender/session-history-navigation.https.html new file mode 100644 index 0000000000..46bd414819 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/session-history-navigation.https.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<title>Test history.length</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> +<script src="resources/session-history-test-util.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +const uid = token(); + +// We test only a fragment-navigation because other kinds of the main frame navigation +// in prerender cancels prerendering. +promise_test(async () => { + assert_equals( + await runTestInPrerender("testSyntheticAnchorClickInPrerender", uid), + "Passed" + ); +}, "Synthetic anchor click navigates independently with replacement in a prerender"); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/session-history-pushstate.https.html b/testing/web-platform/tests/speculation-rules/prerender/session-history-pushstate.https.html new file mode 100644 index 0000000000..89a0276984 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/session-history-pushstate.https.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<title>Test history.length</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> +<script src="resources/session-history-test-util.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +const uid = token(); + +promise_test(async () => { + assert_equals( + await runTestInPrerender("testHistoryPushStateInPrerender", uid), + "Passed" + ); +}, "history.pushState navigates independently with replacement in a prerender"); + +promise_test(async () => { + assert_equals( + await runTestInPrerender("testHistoryReplaceStateInPrerender", uid), + "Passed" + ); +}, "history.replaceState navigates independently in a prerender"); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/session-history-subframe-navigation.https.html b/testing/web-platform/tests/speculation-rules/prerender/session-history-subframe-navigation.https.html new file mode 100644 index 0000000000..db3d9f8347 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/session-history-subframe-navigation.https.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<title>Test history.length during a subframe navigation</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> +<script src="resources/session-history-test-util.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +const uid = token(); + +promise_test(async () => { + assert_equals( + await runTestInPrerender("testSubframeNavigationInPrerender", uid), + "Passed" + ); +}, "Subframe navigation in prerender replaces the session entry"); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/session-history-subframe-reload.https.html b/testing/web-platform/tests/speculation-rules/prerender/session-history-subframe-reload.https.html new file mode 100644 index 0000000000..1caf360f1b --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/session-history-subframe-reload.https.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<title>Test history.length and reloading a subframe</title> +<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="../resources/utils.js"></script> +<script src="resources/utils.js"></script> +<script src="resources/session-history-test-util.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +const uid = token(); + +promise_test(async () => { + assert_equals( + await runTestInPrerender("testSubframeReloadInPrerender", uid), + "Passed" + ); +}, "Subframe reload works in prerendered page"); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/state-and-event.html b/testing/web-platform/tests/speculation-rules/prerender/state-and-event.html new file mode 100644 index 0000000000..7e8e7fd0a9 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/state-and-event.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<meta name="timeout" content="long"> +<script src="/common/utils.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + // The key used for storing a test result in the server. + const key = token(); + + // Open the test runner in a popup - it will prerender itself, record the + // test results, and send them back to this harness. + const url = `resources/prerender-state.html?key=${key}`; + window.open(url, '_blank', 'noopener'); + + // Wait until the test sends us the results. + let result = await nextValueFromServer(key); + result = JSON.parse(result); + + assert_equals(result.prerenderingTypeOf, "boolean", + "typeof(document.prerendering) is 'boolean'."); + assert_equals(result.onprerenderingChangeTypeOf, "object", + "typeof(document.onprerenderingchange) is 'object'."); + + assert_equals( + result.onprerenderingchangeCalledBeforeActivate, false, + "prerenderingchange event should not be called prior to activation."); + assert_equals( + result.prerenderingValueBeforeActivate, true, + "document.prerendering should be true prior to activation."); + + assert_equals(result.onprerenderingchangeCalledAfterActivate, true, + "prerenderingchange event should be called after activation."); + assert_equals(result.prerenderingValueAfterActivate, false, + "document.prerendering should be false after activation."); + assert_equals(result.eventBubbles, false, + "prerenderingchange event.bubbles should be false."); + assert_equals(result.eventCancelable, false, + "prerenderingchange event.cancelable should be false."); +}, 'Test document.prerendering and its change event.'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/visibility-state.html b/testing/web-platform/tests/speculation-rules/prerender/visibility-state.html new file mode 100644 index 0000000000..e9e8548c4f --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/visibility-state.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<title>visibilityState must be updated after prerendering</title> +<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="/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + const referrerRC = await rcHelper.addWindow(undefined, { features: 'noopener' }); + const prerenderedRC = await addPrerenderRC(referrerRC); + + assert_equals(await prerenderedRC.executeScript(() => document.visibilityState), "hidden"); + + await activatePrerenderRC(referrerRC, prerenderedRC); + + assert_equals(await prerenderedRC.executeScript(() => document.visibilityState), "visible"); +}); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate-to-cross-origin-url-on-iframe.https.html b/testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate-to-cross-origin-url-on-iframe.https.html new file mode 100644 index 0000000000..1bfc2e0eb0 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate-to-cross-origin-url-on-iframe.https.html @@ -0,0 +1,83 @@ +<!-- + This file cannot be upstreamed to WPT until: + * Cross-origin iframe loading is specified. The test expects that cross-origin + iframe loading is deferred. +--> +<!DOCTYPE html> +<title>WindowClient.navigate() to cross-origin url in a prerendered iframe</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/utils.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +const PAGE_URL = 'resources/windowclient-navigate-on-iframe.html'; +const WORKER_URL = 'resources/windowclient-navigate-worker.js'; +const SCOPE = 'resources/'; +const CROSS_ORIGIN_DESTINATION = + get_host_info()['HTTPS_REMOTE_ORIGIN'] + + base_path() + 'resources/empty.html'; + +promise_test(async t => { + const uid = token(); + + const registration = await service_worker_unregister_and_register( + t, `${WORKER_URL}?uid=${uid}`, SCOPE); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + // PAGE_URL starts a prerender of a page that makes an iframe, then asks the + // service worker to navigate the iframe to `navigationUrl` via + // `WindowClient.navigate(url)`. The cross-origin url triggers + // PrerenderEventCollector to wait to navigate to `navigationUrl` on an iframe + // until the prerendered iframe is activated. + window.open( + `${PAGE_URL}?navigationUrl=${CROSS_ORIGIN_DESTINATION}&uid=${uid}`, + '_blank', + 'noopener'); + + const navigationResult = await gotMessage; + const expected = [ + { + event: 'started waiting navigation on iframe', + prerendering: true + }, + { + event: 'prerendering change', + prerendering: false + }, + { + event: 'finished waiting navigation on iframe', + prerendering: false + }, + ]; + assert_equals(navigationResult.length, expected.length); + for (let i = 0; i < navigationResult.length; i++) { + assert_equals(navigationResult[i].event, expected[i].event, `event[${i}]`); + assert_equals(navigationResult[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, 'WindowClient.navigate() to a cross-origin URL on a prerendered iframe ' + + 'should be deferred'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate-to-same-origin-url-on-iframe.https.html b/testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate-to-same-origin-url-on-iframe.https.html new file mode 100644 index 0000000000..5f53e97685 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate-to-same-origin-url-on-iframe.https.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<title>WindowClient.navigate() to same-origin url in a prerendered iframe</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/utils.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +const PAGE_URL = 'resources/windowclient-navigate-on-iframe.html'; +const WORKER_URL = 'resources/windowclient-navigate-worker.js'; +const SCOPE = 'resources/'; +const SAME_ORIGIN_DESTINATION = + get_host_info()['HTTPS_ORIGIN'] + base_path() + 'resources/empty.html'; + +promise_test(async t => { + const uid = token(); + + const registration = await service_worker_unregister_and_register( + t, `${WORKER_URL}?uid=${uid}`, SCOPE); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, registration.installing, 'activated'); + + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + // PAGE_URL starts a prerender of a page that makes an iframe, then asks the + // service worker to navigate the iframe to `navigationUrl` via + // `WindowClient.navigate(url)`. + window.open( + `${PAGE_URL}?navigationUrl=${SAME_ORIGIN_DESTINATION}&uid=${uid}`, + '_blank', + 'noopener'); + + const navigationResult = await gotMessage; + assert_equals(navigationResult, 'navigate() succeeded', + 'should succeed to finish navigation test'); + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, 'WindowClient.navigate() to a same-origin URL on a prerendered iframe ' + + 'should succeed'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate.https.html b/testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate.https.html new file mode 100644 index 0000000000..608584636a --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate.https.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<title>WindowClient.navigate() for prerendered main page</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/utils.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +const uid = token(); + +const PAGE_URL = `resources/prerendered-page.html?uid=${uid}`; +const WORKER_URL = `resources/windowclient-navigate-worker.js?uid=${uid}`; +const SAME_ORIGIN_DESTINATION = + get_host_info()['HTTPS_ORIGIN'] + base_path() + 'resources/empty.html'; + +promise_test(async t => { + const registration = + await service_worker_unregister_and_register(t, WORKER_URL, PAGE_URL); + t.add_cleanup(() => registration.unregister()); + await wait_for_state(t, get_newest_worker(registration), 'activated'); + + const readyChannel = new PrerenderChannel('prerender-channel', uid); + t.add_cleanup(_ => readyChannel.close()); + + const readyPromise = new Promise(resolve => { + readyChannel.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + startPrerendering(PAGE_URL); + + const prerenderResult = await readyPromise; + assert_equals(prerenderResult, 'prerender success', + 'should succeed to prerender a page'); + + const resultChannel = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => resultChannel.close()); + + const navigationPromise = new Promise(resolve => { + resultChannel.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + + // Asks the service worker to navigate the prerendered page to `navigationUrl` + // via `WindowClient.navigate(url)`. + const bc = new PrerenderChannel('navigation-channel', uid); + bc.postMessage(JSON.stringify({ + navigationUrl: SAME_ORIGIN_DESTINATION, + clientUrl: new URL(PAGE_URL, window.location).toString(), + respondTo: 'test-channel', + })); + bc.close(); + + const navigationResult = await navigationPromise; + assert_equals(navigationResult, 'navigate() failed with TypeError', + 'should fail the navigation with TypeError'); +}, 'WindowClient.navigate() for a prerendered main page should throw a' + + 'TypeError'); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/workers-in-cross-origin-iframe.html b/testing/web-platform/tests/speculation-rules/prerender/workers-in-cross-origin-iframe.html new file mode 100644 index 0000000000..260bc8eacb --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/workers-in-cross-origin-iframe.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<title>Construction of Web Workers in cross-origin iframe is deferred</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.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.js"></script> + +<!-- This is a regression test for https://crbug.com/1424250 --> +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, {once: true}); + }); + + // This cross-origin iframe starts a dedicated worker and sends a message to + // this document once loading the worker is completed. + const crossOriginUrl = + new URL(`resources/workers-in-cross-origin-iframe.html?uid=${uid}`, + get_host_info()['HTTPS_REMOTE_ORIGIN'] + + window.location.pathname); + + // Start prerendering. Loading the cross-origin iframe in a prerendered page + // will be deferred until prerender activation. + const {exec, activate} = await create_prerendered_page(t); + await exec(crossOriginUrl => { + const iframe = document.createElement('iframe'); + iframe.src = crossOriginUrl; + document.body.appendChild(iframe); + }, [crossOriginUrl.href]); + + // Activate. This resumes loading the cross-origin iframe. + await activate(); + + // Wait for the completion of the worker creation. + assert_equals(await gotMessage, 'Success'); +}, "Dedicated workers in cross-origin iframe should be loaded after " + + "activation"); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prerender/workers.html b/testing/web-platform/tests/speculation-rules/prerender/workers.html new file mode 100644 index 0000000000..3abae233a5 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prerender/workers.html @@ -0,0 +1,95 @@ +<!DOCTYPE html> +<title>Construction of Web Workers is deferred</title> +<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.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const {exec, activate} = await create_prerendered_page(t); + const workerURL = new URL('resources/worker-post-timeOrigin.js', location.href).toString(); + await exec(workerURL => { + window.worker = new Worker(workerURL); + window.waitForWorker = new Promise(resolve => worker.addEventListener('message', e => { + resolve({ + prerendering: document.prerendering, + activationStart: performance.getEntriesByType('navigation')[0].activationStart, + workerLoad: performance.getEntriesByName(workerURL)[0].startTime, + workerStart: e.data}); + }), workerURL); + }, [workerURL]); + + // We want to prevent false success by waiting for enough time to make sure the worker is not + // yet initialized + await new Promise(resolve => t.step_timeout(resolve, 500)); + await activate(); + const {workerStart, activationStart, workerLoad, prerendering} = await exec(() => window.waitForWorker); + assert_false(prerendering, "DedicatedWorker should be suspended until activated"); + assert_greater_than(activationStart, workerLoad, "Loading the worker script should not be delayed"); + assert_greater_than(workerStart, activationStart, "Starting the worker should be delayed"); +}, "Dedicated workers should be loaded in suspended state until activated"); + +promise_test(async t => { + const {exec, activate} = await create_prerendered_page(t); + const workerURL = new URL(`resources/shared-worker.py?id=${token()}`, location.href).toString(); + await exec(workerURL => { + window.worker = new SharedWorker(workerURL, 'dummy'); + window.worker.port.start(); + window.waitForSharedWorkerLoadingReport = + fetch(workerURL + "&check=true") + .then(r => t.text()) + .then(text => text === 'ok' && document.prerendering); + window.waitForWorker = new Promise(resolve => worker.port.addEventListener('message', e => { + resolve({ + prerendering: document.prerendering, + activationStart: performance.getEntriesByType('navigation')[0].activationStart, + workerLoad: performance.getEntriesByName(workerURL)[0]?.startTime, + workerStart: e.data}); + }), workerURL); + }, [workerURL]); + + await new Promise(resolve => t.step_timeout(resolve, 300)); + await activate(); + const {workerStart, activationStart, workerLoad, prerendering} = await exec(() => window.waitForWorker); + assert_false(prerendering, "SharedWorker should be suspended until activated"); + assert_greater_than(activationStart, workerLoad, "Loading the worker script should not be delayed"); + assert_greater_than(workerStart, activationStart, "Starting the worker should be delayed"); +}, "Shared workers should be loaded in suspended state until activated"); + +promise_test(async t => { + const {exec, activate} = await create_prerendered_page(t); + const workerURL = new URL(`resources/shared-worker.py?id=${token()}`, location.href).toString(); + const workerStartTime1 = await new Promise(resolve => { + const worker = new SharedWorker(workerURL, 'worker'); + worker.port.start(); + worker.port.addEventListener('message', e => resolve(e.data)); + }); + + await exec(workerURL => { + window.worker = new SharedWorker(workerURL, 'worker'); + window.worker.port.start(); + window.waitForWorker = new Promise(resolve => worker.port.addEventListener('message', e => { + resolve({ + prerendering: document.prerendering, + activationStart: performance.getEntriesByType('navigation')[0].activationStart, + workerLoad: performance.getEntriesByName(workerURL)[0]?.startTime, + workerStartTime2: e.data}); + }), workerURL); + }, [workerURL]); + + await new Promise(resolve => t.step_timeout(resolve, 300)); + await activate(); + const {workerStartTime2, activationStart, workerLoad, prerendering} = await exec(() => window.waitForWorker); + assert_true(prerendering, "An existing SharedWorker should be accessible while prerendering"); + assert_greater_than(activationStart, workerLoad, "Loading the worker script should not be delayed"); + assert_equals(workerStartTime1, workerStartTime2, "The prerendered page should connect to the existing worker"); + assert_greater_than(activationStart, workerStartTime2, "Starting the worker should be done before activation"); +}, "Existing shared workers should be accessible before activation"); +</script> diff --git a/testing/web-platform/tests/speculation-rules/resources/utils.js b/testing/web-platform/tests/speculation-rules/resources/utils.js new file mode 100644 index 0000000000..cb72f44681 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/resources/utils.js @@ -0,0 +1,8 @@ +window.assertSpeculationRulesIsSupported = () => { + assert_implements( + 'supports' in HTMLScriptElement, + 'HTMLScriptElement.supports must be supported'); + assert_implements( + HTMLScriptElement.supports('speculationrules'), + '<script type="speculationrules"> must be supported'); +}; |