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/prerender/resources | |
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/prerender/resources')
103 files changed, 3788 insertions, 0 deletions
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> |