summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/speculation-rules/prerender/resources
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/speculation-rules/prerender/resources')
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/about-blank-iframes.html115
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/accept-ch.html4
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/accept-ch.html.headers2
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/audio-setSinkId.https.html27
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/background-fetch.https.html29
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/background-sync.https.html29
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/battery-status.https.html21
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/bear-av1-opus.mp4bin0 -> 50253 bytes
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/bluetooth-access.https.html25
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/broadcast-channel.html35
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/cache.txt1
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/clients-matchall-service-worker.js11
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-iframe-src.html7
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-iframe.html56
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-isolated-iframe.https.html10
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-isolated-iframe.https.html.headers2
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-isolated.https.html21
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-isolated.https.html.headers2
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-elem-inline-speculation-rules.html14
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-inline-speculation-rules.html14
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-self.html15
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-strict-dynamic.html15
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src-unsafe-inline.html10
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src.js57
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/dedicated-worker.https.html31
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/dedicated-worker.js1
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/deferred-promise-utils.js74
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/deprecated-broadcast-channel.py28
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/do-nothing-worker.js1
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/echo-client-hints-received.py88
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/echo-prerender-page-client-hints-received.py96
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/echo-referrer.py27
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/echo-subresource-client-hints-received.py15
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/empty.html0
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/encrypted-media.https.html39
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/exec.html18
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/exec.py26
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/fetch-intercept-worker.js9
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/fetch-intercepted-by-service-worker.html22
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/file-picker.html27
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/generic-sensor.https.html34
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/idle-detection.https.html27
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/iframe-added-post-activation.html57
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/indexedb-utils.js61
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/key-value-store.py27
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/main-frame-navigation.html84
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/media-autoplay-attribute.html40
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/media-device-info.https.html25
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/media-devices-access.https.html26
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/media-play.html41
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/message-boxes.html50
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/midi.https.html37
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/navigator-plugins.html20
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/notification-before-activation.html40
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/notification-on-activation.html44
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/post-message-prerendering-completion-notification.html7
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/postmessage-to-client-worker.js3
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/postmessage-to-service-worker.html34
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/prerender-response-code.html22
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/prerender-state.html85
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/prerendered-iframe.html17
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/prerendered-page.html17
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/presentation-request.html26
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/prompt-by-before-unload-inner-frame.html12
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/prompt-by-before-unload.html53
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/push.https.html31
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/referrer-test.js15
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/register-service-worker.html41
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/request-picture-in-picture.html34
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/sandbox-iframe.html50
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/screen-capture.https.html24
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/screen-orientation-lock.https.html23
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/service-worker-unregister.html34
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/service-worker-update.html33
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/service-worker.js11
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/session-history-harness.js75
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/session-history-initiator.https.html61
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/session-history-prerender.https.html149
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/session-history-test-util.js40
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/shared-worker.py11
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/speech-synthesis.https.html65
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/storage-persist.https.html23
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/subapp.html26
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/utils.js442
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/wake-lock.https.html40
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/web-database-access.html22
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/web-hid.https.html23
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/web-locks.html32
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/web-nfc.https.html36
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/web-serial.https.html27
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/web-share.https.html24
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/web-usb.https.html22
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/web-xr-immersive-vr-session.https.html24
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/web-xr-inline-session.https.html24
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/window-move.html62
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/window-open-during-prerendering.html41
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/window-open-in-prerenderingchange.html65
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/window-resize.html64
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/windowclient-navigate-on-iframe.html97
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/windowclient-navigate-worker.js38
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/worker-post-timeOrigin.js1
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/resources/workers-in-cross-origin-iframe.html15
102 files changed, 3758 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..f53e2f9a8b
--- /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>
+
+const params = new URLSearchParams(location.search);
+
+// 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 isPrerendering = params.has('prerendering');
+
+if (!isPrerendering) {
+ loadInitiatorPage();
+} 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..3f1c072d10
--- /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>
+
+const params = new URLSearchParams(location.search);
+
+// 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 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 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..f11a951818
--- /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>
+
+const params = new URLSearchParams(location.search);
+
+// 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 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 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..eb61117538
--- /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>
+
+const params = new URLSearchParams(location.search);
+
+// The main test page (restriction-battery-status.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.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
new file mode 100644
index 0000000000..64a5e1ffcc
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/resources/bear-av1-opus.mp4
Binary files differ
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..21ccc0ca1b
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/resources/cross-origin-iframe.html
@@ -0,0 +1,56 @@
+<!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')) {
+ loadInitiatorPage();
+} 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..866acaa09b
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/resources/csp-script-src.js
@@ -0,0 +1,57 @@
+const params = new URLSearchParams(location.search);
+
+// Take a key used for storing a test result in the server.
+const key = params.get('key');
+
+// 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());
+
+ // 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..63dc2aca99
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/resources/generic-sensor.https.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>
+<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) {
+ loadInitiatorPage();
+} 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..ebef5b725d
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/resources/media-autoplay-attribute.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>
+<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) {
+ loadInitiatorPage();
+} 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..09e85e77b1
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/resources/media-device-info.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-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) {
+ loadInitiatorPage();
+} 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..462ce53e1b
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/resources/media-devices-access.https.html
@@ -0,0 +1,26 @@
+<!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) {
+ 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..48cd3beb3b
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/resources/media-play.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" 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) {
+ loadInitiatorPage();
+} 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/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/subapp.html b/testing/web-platform/tests/speculation-rules/prerender/resources/subapp.html
new file mode 100644
index 0000000000..8fc4433c0b
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/resources/subapp.html
@@ -0,0 +1,26 @@
+<!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 listSubApps() {
+ const bc = new PrerenderChannel('prerender-channel', uid);
+ try {
+ const _ = await navigator.subApps.list();
+ bc.postMessage('unexpected success');
+ } catch (err){
+ bc.postMessage(err.name);
+ } finally {
+ bc.close();
+ }
+}
+
+listSubApps();
+
+</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..99c2613788
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/resources/utils.js
@@ -0,0 +1,442 @@
+const STORE_URL = '/speculation-rules/prerender/resources/key-value-store.py';
+
+function assertSpeculationRulesIsSupported() {
+ assert_implements(
+ 'supports' in HTMLScriptElement,
+ 'HTMLScriptElement.supports is not supported');
+ assert_implements(
+ HTMLScriptElement.supports('speculationrules'),
+ '<script type="speculationrules"> is not supported');
+}
+
+// Starts prerendering for `url`.
+function startPrerendering(url) {
+ // 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 = `{"prerender": [{"source": "list", "urls": ["${url}"] }] }`;
+ 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().
+ 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.
+function loadInitiatorPage() {
+ // Used to communicate with the prerendering page.
+ const prerenderChannel = new PrerenderChannel('prerender-channel');
+ window.addEventListener('unload', () => {
+ 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());
+
+ // Navigate to the prerendered page after being informed.
+ readyToActivate.then(() => {
+ 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>}
+ */
+async function addPrerenderRC(referrerRemoteContext, extraConfig) {
+ let savedURL;
+ const prerenderedRC = await referrerRemoteContext.helper.createContext({
+ executorCreator(url) {
+ // Save the URL which the remote context helper framework assembled for
+ // us, so that we can attach it to the returned `RemoteContextWrapper`.
+ savedURL = 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
+ });
+
+ prerenderedRC.url = savedURL;
+ return prerenderedRC;
+}
+
+/**
+ * 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();
+}
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-database-access.html b/testing/web-platform/tests/speculation-rules/prerender/resources/web-database-access.html
new file mode 100644
index 0000000000..fb00852caa
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/resources/web-database-access.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="utils.js"></script>
+<script>
+
+const bc = new PrerenderChannel('prerender-channel');
+assert_true(document.prerendering);
+
+let result = "Success";
+const db = openDatabase("test", "1.0", "test database", 1024);
+db.transaction(function (tx) {
+ tx.executeSql('CREATE TABLE IF NOT EXISTS foo (text)');
+ tx.executeSql('INSERT INTO foo (text) VALUES ("bar")');
+}, function(error) {
+ result = error;
+}, function() {
+ bc.postMessage(result);
+ bc.close();
+});
+
+</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..d043c88cdb
--- /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>
+
+const params = new URLSearchParams(location.search);
+
+// 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 isPrerendering = params.has('prerendering');
+
+if (!isPrerendering) {
+ loadInitiatorPage();
+} 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..8f3e01134a
--- /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>
+
+const params = new URLSearchParams(location.search);
+
+// 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 isPrerendering = params.has('prerendering');
+
+if (!isPrerendering) {
+ loadInitiatorPage();
+} 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>