summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/speculation-rules
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/anonymous-client.https.html20
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/cross-origin-cookies.https.html41
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/different-initiators-2.https.html52
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/different-initiators.sub.https.html81
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/document-rules.https.html317
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/duplicate-urls.https.html21
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/initiators-a-element.sub.https.html79
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/initiators-iframe-location-href.sub.https.html48
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/initiators-window-open.sub.https.html68
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/invalid-rules.https.html18
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/multiple-url.https.html21
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-delivery-type.tentative.https.html59
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-requestStart-responseStart.https.html52
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-sizes.https.html60
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/README.txt1
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/prefetch-single-with-hint.https.html337
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/prefetch-single.https.html314
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/out-of-document-rule-set.https.html152
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/prefetch-single.https.html28
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/prefetch-status.https.html30
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/prefetch-traverse-reload.sub.html74
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/redirect-url.https.html18
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-from-rules.https.html143
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-not-accepted.https.html53
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/referrer-policy.https.html88
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/authenticate.py32
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/cookies.py41
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/executor.sub.html13
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch.py16
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch_nvs_hint.py34
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/redirect.py3
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/ruleset.py49
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/sw.js1
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/resources/utils.sub.js176
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/same-origin-cookies.https.html39
-rw-r--r--testing/web-platform/tests/speculation-rules/prefetch/user-pass.https.html44
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/about-blank-iframes.html35
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/accept-client-hint-cache.https.html40
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/activation-start.html70
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/cache-storage.https.html35
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/clients-matchall.https.html52
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/cookies.https.html37
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/credentialed-prerender-not-opt-in.html26
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/credentialed-prerender-opt-in.html22
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/cross-origin-iframe.html63
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/cross-origin-isolated.https.html28
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/csp-script-src-elem-inline-speculation-rules.tentative.html28
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/csp-script-src-inline-speculation-rules.tentative.html28
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/csp-script-src-self.html28
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/csp-script-src-strict-dynamic.html28
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/csp-script-src-unsafe-inline.html28
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/fetch-blob.html34
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/fetch-intercepted-by-service-worker.https.html44
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/iframe-added-post-activation.html33
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/indexeddb.html43
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/local-storage.html49
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/main-frame-navigation.https.html39
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/media-autoplay.html47
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/navigation-intercepted-by-service-worker.https.html42
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/navigator-plugins.tentative.html52
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/navigator-subapp.https.tentative.html31
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/prefetch.https.html21
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/referrer-policy-from-rules.html43
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/referrer-policy-mismatch.html90
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/referrer-policy-no-referrer.html21
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/referrer-policy-origin.html24
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/referrer-policy-strict-origin.html22
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/referrer.html20
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/register-service-worker.https.html53
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/remove-script-element.html23
-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
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/response-code-non-successful.html30
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/response-code-successful.html28
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-audio-setSinkId-with-invalid-sinkId.https.tentative.html59
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-audio-setSinkId.https.tentative.html58
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-background-fetch.https.html66
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-background-sync.tentative.https.html66
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-battery-status.https.html47
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-bluetooth.tentative.https.html59
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-broadcast-channel.html60
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-dedicated-worker.https.html56
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-encrypted-media-unsupported-config.https.html57
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-encrypted-media.https.html55
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-focus.html40
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-idle-detection.https.html35
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-local-file-system-access.https.html32
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-media-auto-play-attribute.html69
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-media-camera.https.html57
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-media-device-info.https.html55
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-media-microphone.https.html57
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-media-play.html71
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-message-boxes.html51
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-midi-sysex.https.html64
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-midi.https.html64
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-notification.https.html106
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-presentation-request.https.html33
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-prompt-by-before-unload.html35
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-push.https.html67
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-request-picture-in-picture.html36
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-screen-capture.https.html35
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-screen-orientation-lock.https.html48
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-accelerometer.https.html64
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-ambient-light-sensor.https.html64
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-gyroscope.https.html64
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-magnetometer.https.html64
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-postmessage.https.html59
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-unregister.https.html56
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-update.https.html56
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-speech-synthesis.html61
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-storage-persist.https.html55
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-wake-lock.https.html72
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-web-hid.https.html47
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-web-locks.https.html50
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-web-nfc.https.html49
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-web-serial.tentative.https.html49
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-web-share.https.html34
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-web-usb.https.html46
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-web-xr-immersive-vr-session.https.html57
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-web-xr-inline-session.https.html57
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-window-move.html49
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-window-open.html41
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restriction-window-resize.html50
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/restrictions.html34
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/sandbox-iframe.html57
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/script-supports-speculationrules.html21
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/service-workers.https.html117
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/session-history-activation.https.html28
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/session-history-location.https.html36
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/session-history-navigation.https.html24
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/session-history-pushstate.https.html29
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/session-history-subframe-navigation.https.html22
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/session-history-subframe-reload.https.html22
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/state-and-event.html48
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/visibility-state.html25
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/web-database.https.html72
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate-to-cross-origin-url-on-iframe.https.html82
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate-to-same-origin-url-on-iframe.https.html56
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate.https.html71
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/workers-in-cross-origin-iframe.html51
-rw-r--r--testing/web-platform/tests/speculation-rules/prerender/workers.html96
241 files changed, 11291 insertions, 0 deletions
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/anonymous-client.https.html b/testing/web-platform/tests/speculation-rules/prefetch/anonymous-client.https.html
new file mode 100644
index 0000000000..dfa48f02ab
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/anonymous-client.https.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script>
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let agent = await spawnWindow(t);
+ let nextUrl = agent.getExecutorURL({ hostname: PREFETCH_PROXY_BYPASS_HOST, page: 2 });
+ await agent.forceSinglePrefetch(nextUrl, { requires: ["anonymous-client-ip-when-cross-origin"] });
+ await agent.navigate(nextUrl);
+
+ let requestHeaders = await agent.getRequestHeaders();
+ assert_in_array(requestHeaders.purpose, ["", "prefetch"], "The vendor-specific header Purpose, if present, must be 'prefetch'.");
+ assert_equals(requestHeaders.sec_purpose, "prefetch;anonymous-client-ip");
+ }, "test anonymous-client url prefetch for cross origin pages");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/cross-origin-cookies.https.html b/testing/web-platform/tests/speculation-rules/prefetch/cross-origin-cookies.https.html
new file mode 100644
index 0000000000..c3911919f0
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/cross-origin-cookies.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src='/resources/testdriver-vendor.js'></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script>
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ await test_driver.delete_all_cookies();
+
+ let executor = 'cookies.py';
+ let agent = await spawnWindow(t, { executor });
+ let response_cookies = await agent.getResponseCookies();
+ let request_cookies = await agent.getRequestCookies();
+ assert_equals(request_cookies["count"], undefined);
+ assert_equals(request_cookies["type"], undefined);
+ assert_equals(response_cookies["count"], "1");
+ assert_equals(response_cookies["type"], "navigate");
+
+ let nextUrl = agent.getExecutorURL({ executor, hostname: PREFETCH_PROXY_BYPASS_HOST, page: 2 });
+ await agent.forceSinglePrefetch(nextUrl, { requires: ["anonymous-client-ip-when-cross-origin"] });
+ await agent.forceSinglePrefetch(nextUrl);
+ await agent.navigate(nextUrl);
+
+ response_cookies = await agent.getResponseCookies();
+ request_cookies = await agent.getRequestCookies();
+ assert_equals(request_cookies["count"], undefined);
+ assert_equals(request_cookies["type"], undefined);
+ assert_equals(response_cookies["count"], "1");
+ assert_equals(response_cookies["type"], "prefetch");
+
+ let requestHeaders = await agent.getRequestHeaders();
+ assert_equals(requestHeaders.sec_purpose, "prefetch;anonymous-client-ip");
+
+ }, "speculation rules based prefetch should not use cookies for cross origin urls.");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/different-initiators-2.https.html b/testing/web-platform/tests/speculation-rules/prefetch/different-initiators-2.https.html
new file mode 100644
index 0000000000..1242ebbfb4
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/different-initiators-2.https.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+// Regression test for https://crbug.com/1431804.
+promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ "Speculation Rules not supported");
+
+ const win = await spawnWindow(t, { protocol: 'https' });
+ const nextUrl = win.getExecutorURL({ protocol: 'https', page: 2 });
+
+ // Navigate `win` from Document #1 -> #2 (nextUrl) -> #3 (tempUrl) ->
+ // #4 (nextUrl),
+ // Start speculation rules prefetch from #1, and
+ // Try using the prefetched result for the navigation #3 -> #4.
+ // The Documents #2 and #4 are different, but the same RenderFrameHost is
+ // used before https://crbug.com/936696 is done.
+
+ await win.forceSinglePrefetch(nextUrl);
+
+ // Register a SW for `nextUrl` -- this is a trick to make the prefetched
+ // result to put in `PrefetchService::prefetches_ready_to_serve_` in
+ // Chromium implementation but actually not used by this navigation.
+ const r = await service_worker_unregister_and_register(
+ t, 'resources/sw.js', nextUrl);
+ await wait_for_state(t, r.installing, 'activated');
+
+ // Navigate #1 -> #2.
+ // This doesn't use the prefetched result due to the ServiceWorker.
+ await win.navigate(nextUrl);
+
+ // Unregister the SW.
+ await service_worker_unregister(t, nextUrl);
+
+ // Navigate #2 -> #3 -> #4.
+ const tempUrl = win.getExecutorURL({ protocol: 'https', page: 3 });
+ await win.navigate(tempUrl);
+ await win.navigate(nextUrl);
+
+ const headers = await win.execute_script(() => {
+ return requestHeaders;
+ }, []);
+ assert_not_prefetched(headers,
+ "Prefetch should not work for different initiators.");
+}, "Prefetches from different initiator Documents with same RenderFrameHost");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/different-initiators.sub.https.html b/testing/web-platform/tests/speculation-rules/prefetch/different-initiators.sub.https.html
new file mode 100644
index 0000000000..c35ccde8bb
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/different-initiators.sub.https.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<meta name="variant" content="?cross-site-1">
+<meta name="variant" content="?cross-site-2">
+<meta name="variant" content="?same-site">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+// Regression test for https://crbug.com/1423234.
+promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ "Speculation Rules not supported");
+
+ // Open 2 windows.
+ const hostname1 =
+ location.search === '?cross-site-1' ? '{{hosts[alt][www]}}' : undefined;
+ const hostname2 =
+ location.search === '?cross-site-2' ? '{{hosts[alt][www]}}' : undefined;
+ const initiator1 = await spawnWindow(
+ t, { protocol: 'https', hostname: hostname1 });
+ const initiator2 = await spawnWindow(
+ t, { protocol: 'https', hostname: hostname2 });
+
+ // Start speculation rules prefetch from `initiator1`.
+ const nextUrl = initiator1.getExecutorURL({ protocol: 'https', page: 2 });
+ await initiator1.forceSinglePrefetch(nextUrl);
+
+ // Register a SW for `nextUrl` -- this is a trick to make the prefetched
+ // result to put in `PrefetchService::prefetches_ready_to_serve_` in
+ // Chromium implementation but actually not used by this navigation.
+ const r = await service_worker_unregister_and_register(
+ t, 'resources/sw.js', nextUrl);
+ await wait_for_state(t, r.installing, 'activated');
+
+ // Navigate `initiator1`.
+ // This doesn't use the prefetched result due to the ServiceWorker.
+ await initiator1.navigate(nextUrl);
+
+ // Navigate `initiator1` away from `nextUrl`.
+ const headers1 = await initiator1.execute_script(() => {
+ window.executor.suspend(() => {
+ location.href = 'about:blank';
+ });
+ return requestHeaders;
+ }, []);
+
+ // Unregister the SW.
+ await service_worker_unregister(t, nextUrl);
+
+ // Navigate `initiator2`.
+ // This shouldn't use the prefetched result because the initiator Documents
+ // (even sites) are different.
+ await initiator2.execute_script((url) => {
+ window.executor.suspend(() => {
+ location.href = url;
+ });
+ }, [nextUrl]);
+
+ // Note: while the Window for `initiator2` remains open, the executor ID of
+ // the page is the ID of `nextUrl`, which is `initiator1.context_id`.
+ // So `initiator1` is used below for manipulating the Window for `initiator2`.
+ assert_equals(
+ await initiator1.execute_script(() => location.href),
+ nextUrl.toString(),
+ "expected navigation to reach destination URL");
+
+ const headers2 = await initiator1.execute_script(() => {
+ return requestHeaders;
+ }, []);
+
+ assert_not_prefetched(headers1,
+ "Prefetch should not work due to ServiceWorker.");
+
+ assert_not_prefetched(headers2,
+ "Prefetch should not work for different initiators.");
+}, "Cross-initiator prefetches using ServiceWorker tricks");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/document-rules.https.html b/testing/web-platform/tests/speculation-rules/prefetch/document-rules.https.html
new file mode 100644
index 0000000000..701987c431
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/document-rules.https.html
@@ -0,0 +1,317 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script src="/common/subset-tests-by-key.js"></script>
+
+<meta name="variant" content="?include=defaultPredicate">
+<meta name="variant" content="?include=hrefMatches">
+<meta name="variant" content="?include=and">
+<meta name="variant" content="?include=or">
+<meta name="variant" content="?include=not">
+<meta name="variant" content="?include=invalidPredicate">
+<meta name="variant" content="?include=linkInShadowTree">
+<meta name="variant" content="?include=linkHrefChanged">
+<meta name="variant" content="?include=newRuleSetAdded">
+<meta name="variant" content="?include=selectorMatches">
+<meta name="variant" content="?include=selectorMatchesScopingRoot">
+<meta name="variant" content="?include=selectorMatchesInShadowTree">
+<meta name="variant" content="?include=selectorMatchesDisplayNone">
+<meta name="variant" content="?include=selectorMatchesDisplayLocked">
+<meta name="variant" content="?include=unslottedLink">
+<meta name="variant" content="?include=immediateMutation">
+
+<body>
+<script>
+ subsetTestByKey('defaultPredicate', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ const url = getPrefetchUrl();
+ addLink(url);
+ insertDocumentRule();
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url), 1);
+ }, 'test document rule with no predicate');
+
+ subsetTestByKey('hrefMatches', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule({ href_matches: '*\\?uuid=*&foo=bar' });
+
+ const url_1 = getPrefetchUrl({foo: 'bar'});
+ addLink(url_1);
+ const url_2 = getPrefetchUrl({foo: 'buzz'});
+ addLink(url_2)
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url_1), 1);
+ assert_equals(await isUrlPrefetched(url_2), 0);
+ }, 'test href_matches document rule');
+
+ subsetTestByKey('and', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule({
+ 'and': [
+ { href_matches: '*\\?*foo=bar*' },
+ { href_matches: '*\\?*fizz=buzz*' }]
+ });
+
+ const url_1 = getPrefetchUrl({foo: 'bar'});
+ const url_2 = getPrefetchUrl({fizz: 'buzz'});
+ const url_3 = getPrefetchUrl({foo: 'bar', fizz: 'buzz'});
+ [url_1, url_2, url_3].forEach(url => addLink(url));
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url_1), 0);
+ assert_equals(await isUrlPrefetched(url_2), 0);
+ assert_equals(await isUrlPrefetched(url_3), 1);
+ }, 'test document rule with conjunction predicate');
+
+ subsetTestByKey('or', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule({
+ 'or': [
+ { href_matches: '*\\?*foo=bar*' },
+ { href_matches: '*\\?*fizz=buzz*' }]
+ });
+
+ const url_1 = getPrefetchUrl({ foo: 'buzz' });
+ const url_2 = getPrefetchUrl({ fizz: 'buzz' });
+ const url_3 = getPrefetchUrl({ foo: 'bar'});
+ [url_1, url_2, url_3].forEach(url => addLink(url));
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url_1), 0);
+ assert_equals(await isUrlPrefetched(url_2), 1);
+ assert_equals(await isUrlPrefetched(url_3), 1);
+ }, 'test document rule with disjunction predicate');
+
+ subsetTestByKey('not', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ "Speculation Rules not supported");
+
+ insertDocumentRule({ not: { href_matches: '*\\?uuid=*&foo=bar' } });
+
+ const url_1 = getPrefetchUrl({foo: 'bar'});
+ addLink(url_1);
+ const url_2 = getPrefetchUrl({foo: 'buzz'});
+ addLink(url_2)
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url_1), 0);
+ assert_equals(await isUrlPrefetched(url_2), 1);
+ }, 'test document rule with negation predicate');
+
+ subsetTestByKey('invalidPredicate', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ const url = getPrefetchUrl();
+ addLink(url);
+ insertDocumentRule({invalid: 'predicate'});
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url), 0);
+ }, 'invalid predicate should not throw error or start prefetch');
+
+ subsetTestByKey('linkInShadowTree', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule();
+
+ // Create shadow root.
+ const shadowHost = document.createElement('div');
+ document.body.appendChild(shadowHost);
+ const shadowRoot = shadowHost.attachShadow({mode: 'open'});
+
+ const url = getPrefetchUrl();
+ addLink(url, shadowRoot);
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url), 1);
+ }, 'test that matching link in a shadow tree is prefetched');
+
+ subsetTestByKey('linkHrefChanged', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule({href_matches: "*\\?*foo=bar*"});
+
+ const url = getPrefetchUrl();
+ const link = addLink(url);
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 0);
+
+ const matching_url = getPrefetchUrl({foo: 'bar'});
+ link.href = matching_url;
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(matching_url), 1);
+ }, 'test that changing the href of an invalid link to a matching value triggers a prefetch');
+
+ subsetTestByKey('newRuleSetAdded', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule({href_matches: "*\\?*foo=bar*"});
+ const url = getPrefetchUrl({fizz: "buzz"});
+ addLink(url);
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 0);
+
+ insertDocumentRule({href_matches: "*\\?*fizz=buzz*"});
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 1);
+ }, 'test that adding a second rule set triggers prefetch');
+
+ subsetTestByKey('selectorMatches', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule({ selector_matches: 'a.important-link' });
+
+ const url_1 = getPrefetchUrl({foo: 'bar'});
+ const importantLink = addLink(url_1);
+ importantLink.className = 'important-link';
+ const url_2 = getPrefetchUrl({foo: 'buzz'});
+ addLink(url_2)
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url_1), 1);
+ assert_equals(await isUrlPrefetched(url_2), 0);
+ }, 'test selector_matches document rule');
+
+ subsetTestByKey('selectorMatchesScopingRoot', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule({ selector_matches: ':root > body > a' });
+
+ const url_1 = getPrefetchUrl({ foo: 'bar' });
+ addLink(url_1);
+
+ const url_2 = getPrefetchUrl({ foo: 'buzz' });
+ const extraContainer = document.createElement('div');
+ document.body.appendChild(extraContainer);
+ addLink(url_2, extraContainer);
+
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url_1), 1);
+ assert_equals(await isUrlPrefetched(url_2), 0);
+ }, 'test selector_matches with :root');
+
+ // 'selector_matches' should never match with a link inside a shadow tree
+ // because the scoping root used when matching is always the document.
+ subsetTestByKey('selectorMatchesInShadowTree', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule({ selector_matches: 'a.important-link' });
+
+ // Create shadow root.
+ const shadowHost = document.createElement('div');
+ document.body.appendChild(shadowHost);
+ const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
+
+ const url = getPrefetchUrl();
+ const link = addLink(url, shadowRoot);
+ link.className = 'important-link';
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url), 0);
+ }, 'test selector_matches with link inside shadow tree');
+
+ subsetTestByKey('selectorMatchesDisplayNone', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ const style = document.createElement('style');
+ style.innerText = ".important-section { display: none; }";
+ document.head.appendChild(style);
+ insertDocumentRule();
+
+ const importantSection = document.createElement('div');
+ importantSection.className = 'important-section';
+ document.body.appendChild(importantSection);
+ const url = getPrefetchUrl();
+ addLink(url, importantSection);
+
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 0);
+
+ style.remove();
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 1);
+ }, 'test selector_matches with link inside display:none container');
+
+ subsetTestByKey('selectorMatchesDisplayLocked', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ const style = document.createElement('style');
+ style.innerText = ".important-section { content-visibility: hidden; }";
+ document.head.appendChild(style);
+ insertDocumentRule({ selector_matches: '.important-section a' });
+
+ const importantSection = document.createElement('div');
+ importantSection.className = 'important-section';
+ document.body.appendChild(importantSection);
+ const url = getPrefetchUrl();
+ addLink(url, importantSection);
+
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 0);
+
+ style.remove();
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 1);
+ }, 'test selector_matches with link inside display locked container');
+
+ subsetTestByKey('unslottedLink', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ insertDocumentRule();
+
+ // Create shadow root.
+ const shadowHost = document.createElement('div');
+ document.body.appendChild(shadowHost);
+ const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
+
+ // Add unslotted link.
+ const url = getPrefetchUrl();
+ addLink(url, shadowHost);
+
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 0);
+ }, 'test that unslotted link never matches document rule');
+
+ subsetTestByKey('immediateMutation', promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ 'Speculation Rules not supported');
+
+ // Add a link and allow it to get its style computed.
+ // (Double RAF lets this happen normally.)
+ const url = getPrefetchUrl();
+ const link = addLink(url, document.body);
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
+
+ // Add a document rule and then immediately change the DOM to make it match.
+ insertDocumentRule({ selector_matches: '.late-class *' });
+ document.body.className = 'late-class';
+
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ assert_equals(await isUrlPrefetched(url), 1);
+ }, 'test that selector_matches predicates respect changes immediately');
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/duplicate-urls.https.html b/testing/web-platform/tests/speculation-rules/prefetch/duplicate-urls.https.html
new file mode 100644
index 0000000000..179bbdfd68
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/duplicate-urls.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<script>
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let urls = Array(5).fill(getPrefetchUrlList(1)[0]);
+ insertSpeculationRules({ prefetch: [{ source: 'list', urls: urls }] });
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ let prefetched_count = (await Promise.all(urls.map(isUrlPrefetched))).reduce(
+ (count, was_prefetched) => count + (was_prefetched ? 1 : 0), 0);
+
+ assert_equals(prefetched_count, 1, "url should be prefetched just once.");
+ }, "browser should remove duplicate urls from prefetch buffer.");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/initiators-a-element.sub.https.html b/testing/web-platform/tests/speculation-rules/prefetch/initiators-a-element.sub.https.html
new file mode 100644
index 0000000000..bac5eb7cb7
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/initiators-a-element.sub.https.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<meta name="variant" content="?cross-site">
+<meta name="variant" content="?same-site">
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script>
+ // In https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate,
+ // `sourceDocument` (instead of `navigable`'s active document) should be
+ // used as the referring document for prefetch.
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const win = await spawnWindow(t, { protocol: 'https' });
+
+ const hostname =
+ location.search === '?cross-site' ? '{{hosts[alt][www]}}' : undefined;
+ const nextUrl = win.getExecutorURL({ protocol: 'https', hostname, page: 2 });
+
+ await win.forceSinglePrefetch(nextUrl);
+
+ // sourceDocument == `win`'s Document == active document of window being
+ // navigated.
+ await win.execute_script((url) => {
+ window.executor.suspend(() => {
+ const a = document.createElement('a');
+ a.setAttribute('href', url);
+ document.body.appendChild(a);
+ a.click();
+ });
+ }, [nextUrl]);
+
+ assert_equals(
+ await win.execute_script(() => location.href),
+ nextUrl.toString(),
+ "expected navigation to reach destination URL");
+
+ assert_prefetched(await win.getRequestHeaders());
+ }, `<a>`);
+
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const win = await spawnWindow(t, { protocol: 'https' });
+
+ const hostname =
+ location.search === '?cross-site' ? '{{hosts[alt][www]}}' : undefined;
+ const nextUrl = win.getExecutorURL({ protocol: 'https', hostname, page: 2 });
+
+ await win.forceSinglePrefetch(nextUrl);
+
+ // sourceDocument == `win`'s Document != active document of window being
+ // navigated, since the window being navigated is a new window.
+ await win.execute_script((url) => {
+ window.executor.suspend(() => {
+ const a = document.createElement('a');
+ a.setAttribute('href', url);
+ a.setAttribute('target', '_blank');
+ document.body.appendChild(a);
+ a.click();
+ });
+ }, [nextUrl]);
+
+ // Below, the scripts given to `win.execute_script()` are executed on the
+ // `nextUrl` page in the new window, because `window.executor.suspend()`
+ // above made `win`'s original page stop processing `execute_script()`,
+ // while the new page of `nextUrl` in the new window starts processing
+ // `execute_script()` for the same ID.
+ assert_equals(
+ await win.execute_script(() => location.href),
+ nextUrl.toString(),
+ "expected navigation to reach destination URL");
+
+ assert_prefetched(await win.getRequestHeaders());
+ }, `<a target="blank">`);
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/initiators-iframe-location-href.sub.https.html b/testing/web-platform/tests/speculation-rules/prefetch/initiators-iframe-location-href.sub.https.html
new file mode 100644
index 0000000000..9d6702d4b7
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/initiators-iframe-location-href.sub.https.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<meta name="variant" content="?cross-site">
+<meta name="variant" content="?same-site">
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script>
+ // In https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate,
+ // `sourceDocument` (instead of `navigable`'s active document) should be
+ // used as the referring document for prefetch.
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const win = await spawnWindow(t, { protocol: 'https' });
+
+ const hostname =
+ location.search === '?cross-site' ? '{{hosts[alt][www]}}' : undefined;
+ const nextUrl = win.getExecutorURL({ protocol: 'https', hostname, page: 2 });
+
+ await win.forceSinglePrefetch(nextUrl);
+
+ // In https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate,
+ // `sourceDocument` is the incumbent Document and thus `win`'s Document.
+ // `navigable`'s active document is `iframe`'s Document.
+ await win.execute_script((url) => {
+ window.executor.suspend(() => {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.contentWindow.location.href = url;
+ });
+ }, [nextUrl]);
+
+ // Below, the scripts given to `win.execute_script()` are executed on the
+ // `nextUrl` page in the iframe, because `window.executor.suspend()` above
+ // made `win`'s original page stop processing `execute_script()`,
+ // while the new page of `nextUrl` in the iframe starts processing
+ // `execute_script()` for the same ID.
+ assert_equals(
+ await win.execute_script(() => location.href),
+ nextUrl.toString(),
+ "expected navigation to reach destination URL");
+
+ assert_prefetched(await win.getRequestHeaders());
+ }, `location.href across iframe`);
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/initiators-window-open.sub.https.html b/testing/web-platform/tests/speculation-rules/prefetch/initiators-window-open.sub.https.html
new file mode 100644
index 0000000000..f786df077d
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/initiators-window-open.sub.https.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<meta name="variant" content="?cross-site">
+<meta name="variant" content="?same-site">
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script>
+ // In https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate,
+ // `sourceDocument` (instead of `navigable`'s active document) should be
+ // used as the referring document for prefetch.
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const win = await spawnWindow(t, { protocol: 'https' });
+
+ const hostname =
+ location.search === '?cross-site' ? '{{hosts[alt][www]}}' : undefined;
+ const nextUrl = win.getExecutorURL({ protocol: 'https', hostname, page: 2 });
+
+ await win.forceSinglePrefetch(nextUrl);
+
+ await win.execute_script((url) => {
+ window.executor.suspend(() => {
+ window.open(url, "_blank");
+ });
+ }, [nextUrl]);
+
+ // Below, the scripts given to `win.execute_script()` are executed on the
+ // `nextUrl` page in the new window, because `window.executor.suspend()`
+ // above made `win`'s original page stop processing `execute_script()`,
+ // while the new page of `nextUrl` in the new window starts processing
+ // `execute_script()` for the same ID. Same for below.
+ assert_equals(
+ await win.execute_script(() => location.href),
+ nextUrl.toString(),
+ "expected navigation to reach destination URL");
+
+ assert_prefetched(await win.getRequestHeaders());
+ }, `window.open()`);
+
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const win = await spawnWindow(t, { protocol: 'https' });
+
+ const hostname =
+ location.search === '?cross-site' ? '{{hosts[alt][www]}}' : undefined;
+ const nextUrl = win.getExecutorURL({ protocol: 'https', hostname, page: 2 });
+
+ await win.forceSinglePrefetch(nextUrl);
+
+ await win.execute_script((url) => {
+ window.executor.suspend(() => {
+ window.open(url, "_blank", "noopener");
+ });
+ }, [nextUrl]);
+
+ assert_equals(
+ await win.execute_script(() => location.href),
+ nextUrl.toString(),
+ "expected navigation to reach destination URL");
+
+ assert_prefetched(await win.getRequestHeaders());
+ }, `window.open(noopener)`);
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/invalid-rules.https.html b/testing/web-platform/tests/speculation-rules/prefetch/invalid-rules.https.html
new file mode 100644
index 0000000000..573f3c0b0f
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/invalid-rules.https.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script>
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let agent = await spawnWindow(t);
+ let nextUrl = agent.getExecutorURL({ page: 2 });
+ await agent.forceSinglePrefetch(nextUrl, { invalid_key: "value" });
+ await agent.navigate(nextUrl);
+
+ assert_not_prefetched(await agent.getRequestHeaders());
+ }, "an unrecognized key in a prefetch rule should prevent it from being fetched");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/multiple-url.https.html b/testing/web-platform/tests/speculation-rules/prefetch/multiple-url.https.html
new file mode 100644
index 0000000000..dd9916632f
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/multiple-url.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<script>
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let urls = getPrefetchUrlList(5);
+ insertSpeculationRules({ prefetch: [{ source: 'list', urls: urls }] });
+ await new Promise(resolve => t.step_timeout(resolve, 3000));
+
+ let prefetched_count = (await Promise.all(urls.map(isUrlPrefetched))).reduce(
+ (count, was_prefetched) => count + (was_prefetched ? 1 : 0), 0);
+
+ assert_greater_than_equal(prefetched_count, 2, "At least two urls should be prefetched to pass the test.");
+ }, "browser should be able to prefetch multiple urls");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-delivery-type.tentative.https.html b/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-delivery-type.tentative.https.html
new file mode 100644
index 0000000000..cee8e55f12
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-delivery-type.tentative.https.html
@@ -0,0 +1,59 @@
+<!-- TODO(crbug/1358591): Rename this file from "tentative" once
+`WICG/nav-speculation#180` is merged. -->
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<meta name="variant" content="?prefetch=true&bypass_cache=true">
+<meta name="variant" content="?prefetch=false&bypass_cache=true">
+<meta name="variant" content="?prefetch=true&bypass_cache=false">
+<meta name="variant" content="?prefetch=false&bypass_cache=false">
+
+<script>
+const prefetchEnabled = (Object.fromEntries(
+ new URLSearchParams(location.search)).prefetch === "true");
+const bypassCache = (Object.fromEntries(
+ new URLSearchParams(location.search)).bypass_cache === "true");
+
+promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ // Some meaningless query param to avoid cached response.
+ const prefetchUrl =
+ bypassCache ? agent.getExecutorURL({ a: "b" }) : agent.getExecutorURL();
+
+ if (prefetchEnabled)
+ await agent.forceSinglePrefetch(prefetchUrl);
+
+ await agent.navigate(prefetchUrl);
+
+ if (prefetchEnabled)
+ assert_prefetched(await agent.getRequestHeaders(),
+ `Prefetch ${prefetchUrl.href} should work.`);
+ else
+ assert_not_prefetched(await agent.getRequestHeaders(),
+ `${prefetchUrl.href} should not be prefetched.`);
+
+ await agent.execute_script(
+ () => window.entries = performance.getEntriesByType('navigation'));
+
+ // Expects one entry, whose `deliveryType` is "navigational-prefetch" for
+ // the prefetched request, and "" for the non-prefetched.
+ //
+ // TODO(crbug/1317756): Currently the initial prefetch request bypasses the
+ // HTTP cache, making `deliveryType` always an empty string for non-prefetch
+ // request. Expand test coverage when `net::LOAD_DISABLE_CACHE` is removed.
+ assert_equals(await agent.execute_script(() => window.entries.length), 1,
+ 'Wrong number of entries');
+ const deliveryType =
+ await agent.execute_script(() => window.entries[0].deliveryType);
+ const expectedDeliveryType = prefetchEnabled ? 'navigational-prefetch' : '';
+ assert_equals(deliveryType, expectedDeliveryType);
+
+ }, `PerformanceNavigationTiming.deliveryType test, same origin prefetch.`);
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-requestStart-responseStart.https.html b/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-requestStart-responseStart.https.html
new file mode 100644
index 0000000000..062d7265d8
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-requestStart-responseStart.https.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<meta name="variant" content="">
+<meta name="variant" content="?prefetch=true">
+
+<script>
+const searchParams = new URLSearchParams(location.search);
+const prefetchEnabled = searchParams.has('prefetch');
+
+promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ // Some meaningless query param to avoid cached response.
+ const prefetchUrl = agent.getExecutorURL({ a: "b" });
+
+ if (prefetchEnabled)
+ await agent.forceSinglePrefetch(prefetchUrl);
+
+ await agent.navigate(prefetchUrl);
+
+ if (prefetchEnabled) {
+ assert_prefetched(await agent.getRequestHeaders(),
+ `Prefetch ${prefetchUrl.href} should work.`);
+ } else {
+ assert_not_prefetched(await agent.getRequestHeaders(),
+ `${prefetchUrl.href} should not be prefetched.`);
+ }
+
+ const entries = await agent.execute_script(
+ () => performance.getEntriesByType('navigation'));
+ assert_equals(entries.length, 1, 'Wrong number of navigation entries');
+ const entry = entries[0];
+
+ // Events timeline:
+ // ... -> connectEnd --> requestStart --> responseStart --> ...
+ if (prefetchEnabled) {
+ assert_equals(entry.connectEnd, entry.requestStart);
+ assert_equals(entry.requestStart, entry.responseStart);
+ } else {
+ assert_less_than_equal(entry.connectEnd, entry.requestStart);
+ assert_less_than_equal(entry.requestStart, entry.responseStart);
+ }
+
+ }, "PerformanceNavigationTiming.requestStart/responseStart test, same origin prefetch.");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-sizes.https.html b/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-sizes.https.html
new file mode 100644
index 0000000000..19c254ca1d
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/navigation-timing-sizes.https.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<meta name="variant" content="">
+<meta name="variant" content="?bypass_cache=true">
+<meta name="variant" content="?prefetch=true">
+<meta name="variant" content="?prefetch=true&bypass_cache=true">
+
+<script>
+const searchParams = new URLSearchParams(location.search);
+const prefetchEnabled = searchParams.has('prefetch');
+const bypassCache = searchParams.has('bypass_cache');
+
+// Header size: https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-transfersize
+const headerSize = 300;
+
+promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'),
+ "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ // Some meaningless query param to avoid cached response.
+ const prefetchUrl =
+ bypassCache ? agent.getExecutorURL({ a: "b" }) : agent.getExecutorURL();
+
+ if (prefetchEnabled)
+ await agent.forceSinglePrefetch(prefetchUrl);
+
+ await agent.navigate(prefetchUrl);
+
+ if (prefetchEnabled)
+ assert_prefetched(await agent.getRequestHeaders(),
+ `Prefetch ${prefetchUrl.href} should work.`);
+ else
+ assert_not_prefetched(await agent.getRequestHeaders(),
+ `${prefetchUrl.href} should not be prefetched.`);
+
+ await agent.execute_script(
+ () => window.entries = performance.getEntriesByType('navigation'));
+
+ // TODO(crbug/1317756): Currently the initial prefetch request bypasses the
+ // HTTP cache. Expand test coverage for cache and cache+revalidation cases.
+ //
+ // We do not assert the exact size of `resources/executor.sub.html` since it
+ // would be a headache to update this test everytime executor.sub.html
+ // changes.
+ assert_equals(await agent.execute_script(() => window.entries.length), 1,
+ 'Wrong number of entries');
+ const entry =
+ await agent.execute_script(() => window.entries[0]);
+ const bodySize = entry.encodedBodySize;
+ assert_greater_than(bodySize, 0);
+ assert_equals(entry.transferSize, headerSize + bodySize);
+ assert_equals(entry.decodedBodySize, bodySize);
+ }, `PerformanceNavigationTiming.transferSize/encodedBodySize/decodedBodySize test, same origin prefetch.`);
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/README.txt b/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/README.txt
new file mode 100644
index 0000000000..60ac226f8c
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/README.txt
@@ -0,0 +1 @@
+Web Platform Tests for No-Vary-Search support in prefetch cache.
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/prefetch-single-with-hint.https.html b/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/prefetch-single-with-hint.https.html
new file mode 100644
index 0000000000..d62788caba
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/prefetch-single-with-hint.https.html
@@ -0,0 +1,337 @@
+<!DOCTYPE html>
+<title>Use for navigation the requested prefetched response annotated with No-Vary-Search hint, if
+No-Vary-Search headers also match during navigation</title>
+<meta charset="utf-8">
+
+<meta name="variant" content="?1-1">
+<meta name="variant" content="?2-2">
+<meta name="variant" content="?3-3">
+<meta name="variant" content="?4-4">
+<meta name="variant" content="?5-5">
+<meta name="variant" content="?6-6">
+<meta name="variant" content="?7-7">
+<meta name="variant" content="?8-8">
+<meta name="variant" content="?9-9">
+<meta name="variant" content="?10-10">
+<meta name="variant" content="?11-11">
+<meta name="variant" content="?12-12">
+<meta name="variant" content="?13-13">
+<meta name="variant" content="?14-14">
+<meta name="variant" content="?15-15">
+<meta name="variant" content="?16-16">
+<meta name="variant" content="?17-17">
+<meta name="variant" content="?18-18">
+<meta name="variant" content="?19-19">
+<meta name="variant" content="?20-20">
+<meta name="variant" content="?21-21">
+<meta name="variant" content="?22-22">
+<meta name="variant" content="?23-23">
+<meta name="variant" content="?24-24">
+<meta name="variant" content="?25-last">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="../resources/utils.sub.js"></script>
+<script src="/common/subset-tests.js"></script>
+
+<script>
+ function addNoVarySearchHeaderUsingQueryParam(url, value){
+ // Use nvs_header query parameter to ask the wpt server
+ // to populate No-Vary-Search response header.
+ if(value){
+ url.searchParams.append("nvs_header", value);
+ }
+ }
+
+ /*
+ remoteAgent: the RemoteContext instance used to communicate between the
+ test and the window where prefetch/navigation is happening
+ noVarySearchHeaderValue: the value of No-Vary-Search header to be populated
+ for the prefetched response
+ noVarySearchHintValue: the value of No-Vary-Search hint passed in
+ as expects_no_vary_search hint in prefetch speculation rules.
+ prefetchQuery: query params to be added to prefetchExecutor url and prefetched
+ navigateQuery: query params to be added to prefetchExecutor url and navigated to
+ */
+ async function prefetchAndNavigate(remoteAgent, noVarySearchHeaderValue, noVarySearchHintValue, prefetchQuery, navigateQuery){
+ /*
+ Flow:
+ * prefetch prefetch_nvs_hint.py?uuid=...&nvs_header=...&otherqueryparams
+ * the prefetch request above includes no_vary_search_hint in the speculation
+ rules
+ * the server blocks progress on this prefetch request on the server side so
+ from the browser perspective the server is "thinking"
+ * the test starts navigation to
+ prefetch_nvs_hint.py?uuid=...&nvs_header=...&otherdifferentqueryparams.
+ This navigation matches by No-Vary-Search hint the above in
+ progress prefetch.
+ * the test fetches prefetch_nvs_hint.py?uuid=...&unblock="unblock"
+ which unblocks the in progress prefetch so that the in-progress
+ navigation can continue
+ */
+ const prefetch_nvs_hint_server_page = "prefetch_nvs_hint.py";
+ const prefetchUrl = remoteAgent.getExecutorURL({executor:prefetch_nvs_hint_server_page});
+ const navigateToUrl = new URL(prefetchUrl);
+ // Add query params to the url to be prefetched.
+ const additionalPrefetchedUrlSearchParams = new URLSearchParams(prefetchQuery);
+ addNoVarySearchHeaderUsingQueryParam(prefetchUrl, noVarySearchHeaderValue);
+ additionalPrefetchedUrlSearchParams.forEach((value, key) => {
+ prefetchUrl.searchParams.append(key, value);
+ });
+
+ await remoteAgent.forceSinglePrefetch(prefetchUrl,
+ {expects_no_vary_search:noVarySearchHintValue});
+
+ // Add new query params to navigateToUrl to match No-Vary-Search test case.
+ const additionalNavigateToUrlSearchParams = new URLSearchParams(navigateQuery);
+ addNoVarySearchHeaderUsingQueryParam(navigateToUrl, noVarySearchHeaderValue);
+ additionalNavigateToUrlSearchParams.forEach((value, key) => {
+ navigateToUrl.searchParams.append(key, value);
+ });
+ // Url used by fetch in order to unblock the prefetched url
+ const nvshint_unblock_url = remoteAgent.getExecutorURL(
+ {executor:prefetch_nvs_hint_server_page, unblock:"unblock"});
+ await remoteAgent.execute_script((unblock_url) => {
+ onbeforeunload = (event) => {
+ fetch(unblock_url);
+ };
+ }, [nvshint_unblock_url]);
+
+ // Try navigating to a non-exact prefetched URL that matches by
+ // No-Vary-Search hint
+ // Wait for the navigation to finish
+ await remoteAgent.navigate(navigateToUrl);
+ }
+
+ function prefetch_no_vary_search_test(description, noVarySearch, noVarySearchHint, prefetchQuery, navigateQuery, shouldUsePrefetch){
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+ const agent = await spawnWindow(t, {});
+ await prefetchAndNavigate(agent,
+ noVarySearch,
+ noVarySearchHint,
+ prefetchQuery,
+ navigateQuery);
+
+ if(shouldUsePrefetch){
+ assert_prefetched(await agent.getRequestHeaders(),
+ "Navigation didn't use the prefetched response!");
+ }
+ else{
+ assert_not_prefetched(await agent.getRequestHeaders(),
+ "Navigation used the prefetched response!");
+ }
+ }, description);
+ }
+
+ // Test inputs:
+ // - description: a description of the test.
+ // - noVarySearch: No-Vary-Search header value for the response.
+ // - noVarySearchHint: No-Vary-Search hint to include in prefetch
+ // speculation rules
+ // - prefetchQuery: added to query part of prefetch-executor when prefetching
+ // - navigateQuery: added to query part of prefetch-executor when navigating
+ // - shouldUsePrefetch: if the test case expects the prefetched entry to be
+ // used or not.
+ [{description:"Use in-flight prefetch as query parameter b has the same value.",
+ noVarySearch: 'params=("a")',
+ noVarySearchHint: 'params=("a")',
+ prefetchQuery: "a=2&b=3",
+ navigateQuery: "b=3",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use in-flight prefetch as there is no No-Vary-Search hint.",
+ noVarySearch: 'params=("a")',
+ noVarySearchHint: '',
+ prefetchQuery: "a=2&b=3",
+ navigateQuery: "b=3",
+ shouldUsePrefetch: false},
+
+ {description:"Don't use in-flight prefetch as the prefetched URL has the extra \"a\" query parameter.",
+ noVarySearch: 'params=("b")',
+ noVarySearchHint: 'params=("b")',
+ prefetchQuery: "a=2&b=3",
+ navigateQuery: "b=2",
+ shouldUsePrefetch: false},
+
+ {description:"Use in-flight prefetch as the URLs do not vary by a and b.",
+ noVarySearch: 'params=("a" "b")',
+ noVarySearchHint: 'params=("a" "b")',
+ prefetchQuery: "a=2&b=3",
+ navigateQuery: "b=2",
+ shouldUsePrefetch: true},
+
+ {description:"Do not use in-flight prefetch as the navigation URL has" +
+ " a different value for the \"b\" query parameter.",
+ noVarySearch: 'params=("a" "b")',
+ noVarySearchHint: 'params=("a")',
+ prefetchQuery: "a=2&b=3",
+ navigateQuery: "b=2",
+ shouldUsePrefetch: false},
+
+ {description:"Use in-flight prefetch as the URLs have the same values for all keys, only differing by order.",
+ noVarySearch: "key-order",
+ noVarySearchHint: "key-order",
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5&b=3",
+ navigateQuery: "d=6&a=3&b=5&b=3&c=5&a=4",
+ shouldUsePrefetch: true},
+
+ {description:"Use in-flight prefetch as the URLs have the same values for all keys, only differing by order and using ?1 for specifying a true value.",
+ noVarySearch: "key-order=?1",
+ noVarySearchHint: "key-order=?1",
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5&b=3",
+ navigateQuery: "d=6&a=3&b=5&b=3&c=5&a=4",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use in-flight prefetch as key-order is set to false and the URLs are not identical.",
+ noVarySearch: "key-order=?0",
+ noVarySearchHint: "key-order=?1",
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5&b=3",
+ navigateQuery: "d=6&a=3&b=5&b=3&c=5&a=4",
+ shouldUsePrefetch: false},
+
+ {description:"Use in-flight prefetch as all query parameters except c can be ignored.",
+ noVarySearch: 'params, except=("c")',
+ noVarySearchHint: 'params, except=("c")',
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Use in-flight prefetch as all query parameters except c can be ignored." +
+ " Only the last except matters.",
+ noVarySearch: 'params, except=("b"), except=("c")',
+ noVarySearchHint: 'params, except=("b"), except=("c")',
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use in-flight prefetch as even though all query parameters" +
+ " except c can be ignored, c has different value.",
+ noVarySearch: 'params, except=("c")',
+ noVarySearchHint: "params",
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=5",
+ shouldUsePrefetch: false},
+
+ {description:"Use in-flight prefetch as even though all query parameters" +
+ " except c and d can be ignored, c value matches and d value matches.",
+ noVarySearch: 'params, except=("c" "d")',
+ noVarySearchHint: 'params, except=("c" "d")',
+ prefetchQuery: "b=5&a=3&d=6&c=5",
+ navigateQuery: "d=6&a=1&b=2&c=5",
+ shouldUsePrefetch: true},
+
+ {description:"Use in-flight prefetch as even though all query parameters except" +
+ " c and d can be ignored, c value matches and d value matches." +
+ " Some query parameters to be ignored appear multiple times in the query.",
+ noVarySearch: 'params, except=("c" "d")',
+ noVarySearchHint: 'params',
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5",
+ navigateQuery: "d=6&a=1&a=2&b=2&b=3&c=5",
+ shouldUsePrefetch: true},
+
+ {description:"Use in-flight prefetch as all query parameters except c can be ignored." +
+ " Allow extension via parameters.",
+ noVarySearch: 'params, except=("c";unknown)',
+ noVarySearchHint: 'params, except=("c";unknown)',
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Use in-flight prefetch as query parameter c can be ignored." +
+ " Allow extension via parameters.",
+ noVarySearch: 'params=("c";unknown)',
+ noVarySearchHint: 'params=("c";unknown)',
+ prefetchQuery: "a=2&b=2&c=5",
+ navigateQuery: "a=2&c=3&b=2",
+ shouldUsePrefetch: true},
+
+ {description:"Use in-flight prefetch as the URLs have the values in different order for a." +
+ " Allow extension via parameters.",
+ noVarySearch: "key-order;unknown",
+ noVarySearchHint: "key-order;unknown",
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5&b=3",
+ navigateQuery: "d=6&a=3&b=5&b=3&c=5&a=4",
+ shouldUsePrefetch: true},
+
+ {description:"Use in-flight prefetch as the URLs do not vary on any query parameters." +
+ " Allow extension via parameters.",
+ noVarySearch: "params;unknown",
+ noVarySearchHint: "params;unknown",
+ prefetchQuery: "",
+ navigateQuery: "b=4&c=5",
+ shouldUsePrefetch: true},
+
+ {description:"Use in-flight prefetch as all query parameters except c can be ignored." +
+ " Allow extension via parameters.",
+ noVarySearch: 'params;unknown, except=("c");unknown',
+ noVarySearchHint: 'params;unknown, except=("c");unknown',
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use the in-flight prefetched URL. Empty No-Vary-Search means default URL variance." +
+ " The prefetched and the navigated URLs have to be the same.",
+ noVarySearch: "",
+ noVarySearchHint: "params",
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: false},
+
+ {description:"Use the in-flight prefetch. Empty No-Vary-Search means default URL variance." +
+ " The prefetched and the navigated URLs have to be the same.",
+ noVarySearch: "",
+ noVarySearchHint: "",
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "b=5&a=3&d=6&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Use the in-flight prefetch. Empty No-Vary-Search means default URL variance." +
+ " The prefetched and the navigated URLs have to be the same.",
+ noVarySearch: "",
+ noVarySearchHint: "",
+ prefetchQuery: "",
+ navigateQuery: "",
+ shouldUsePrefetch: true},
+
+ {description:"Use the in-flight prefetch. Non-ASCII key - 2 UTF-8 code units." +
+ " Don't vary the response on the non-ASCII key.",
+ noVarySearch: 'params=("%C2%A2")',
+ noVarySearchHint: 'params=("%C2%A2")',
+ prefetchQuery: "¢=3",
+ navigateQuery: "¢=4",
+ shouldUsePrefetch: true},
+
+ {description:"Use the in-flight prefetch. Non-ASCII key - 2 UTF-8 code units." +
+ " Don't vary the response on the non-ASCII key.",
+ noVarySearch: 'params=("%C2%A2")',
+ noVarySearchHint: 'params=("%C2%A2")',
+ prefetchQuery: "a=2&¢=3",
+ navigateQuery: "¢=4&a=2",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use the in-flight prefetch. Non-ASCII key - 2 UTF-8 code units." +
+ " Vary the response on the non-ASCII key.",
+ noVarySearch: 'params, except=("%C2%A2")',
+ noVarySearchHint: 'params',
+ prefetchQuery: "¢=3",
+ navigateQuery: "¢=4",
+ shouldUsePrefetch: false},
+
+ {description:"Use the in-flight prefetch. Non-ASCII key - 2 UTF-8 code units." +
+ " Vary the response on the non-ASCII key.",
+ noVarySearch: 'params, except=("%C2%A2")',
+ noVarySearchHint: 'params, except=("%C2%A2")',
+ prefetchQuery: "¢=3&a=4",
+ navigateQuery: "a=5&¢=3",
+ shouldUsePrefetch: true},
+
+ ].forEach(({description, noVarySearch, noVarySearchHint, prefetchQuery, navigateQuery, shouldUsePrefetch}) => {
+ subsetTest(prefetch_no_vary_search_test,
+ description, noVarySearch, noVarySearchHint, prefetchQuery, navigateQuery,
+ shouldUsePrefetch);
+ });
+
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/prefetch-single.https.html b/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/prefetch-single.https.html
new file mode 100644
index 0000000000..fdbb617135
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/no-vary-search/prefetch-single.https.html
@@ -0,0 +1,314 @@
+<!DOCTYPE html>
+<title>Prefetched response including No-Vary-Search headers is used during navigation</title>
+<meta charset="utf-8">
+
+<meta name="variant" content="?1-1">
+<meta name="variant" content="?2-2">
+<meta name="variant" content="?3-3">
+<meta name="variant" content="?4-4">
+<meta name="variant" content="?5-5">
+<meta name="variant" content="?6-6">
+<meta name="variant" content="?7-7">
+<meta name="variant" content="?8-8">
+<meta name="variant" content="?9-9">
+<meta name="variant" content="?10-10">
+<meta name="variant" content="?11-11">
+<meta name="variant" content="?12-12">
+<meta name="variant" content="?13-13">
+<meta name="variant" content="?14-14">
+<meta name="variant" content="?15-15">
+<meta name="variant" content="?16-16">
+<meta name="variant" content="?17-17">
+<meta name="variant" content="?18-18">
+<meta name="variant" content="?19-19">
+<meta name="variant" content="?20-20">
+<meta name="variant" content="?21-21">
+<meta name="variant" content="?22-22">
+<meta name="variant" content="?23-23">
+<meta name="variant" content="?24-24">
+<meta name="variant" content="?25-25">
+<meta name="variant" content="?26-26">
+<meta name="variant" content="?27-27">
+<meta name="variant" content="?28-28">
+<meta name="variant" content="?29-29">
+<meta name="variant" content="?30-last">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="../resources/utils.sub.js"></script>
+<script src="/common/subset-tests.js"></script>
+
+<script>
+ function addNoVarySearchHeaderUsingPipe(url, value){
+ // Use server pipes https://web-platform-tests.org/writing-tests/server-pipes.html
+ // to populate No-Vary-Search response header.
+ // The "," and ")" characters need to be escaped by using backslash
+ // (see https://web-platform-tests.org/writing-tests/server-pipes.html).
+ // E.g. params=("a") becomes params=("a"\), params=("a"),key-order becomes
+ // params=("a"\)\,key-order etc.
+ url.searchParams.append("pipe",
+ `header(No-Vary-Search,${value.replaceAll(/[,)]/g, '\\$&')})`);
+ }
+
+ /*
+ remoteAgent: the RemoteContext instance used to communicate between the
+ test and the window where prefetch/navigation is happening
+ noVarySearchHeaderValue: the value of No-Vary-Search header to be populated
+ for the prefetched response
+ prefetchQuery: query params to be added to prefetchExecutor url and prefetched
+ navigateQuery: query params to be added to prefetchExecutor url and navigated to
+ */
+ async function prefetchAndNavigate(remoteAgent, noVarySearchHeaderValue, prefetchQuery, navigateQuery){
+ const nextUrl = remoteAgent.getExecutorURL();
+ const navigateToUrl = new URL(nextUrl);
+ // Add query params to the url to be prefetched.
+ const additionalPrefetchedUrlSearchParams = new URLSearchParams(prefetchQuery);
+ addNoVarySearchHeaderUsingPipe(nextUrl, noVarySearchHeaderValue);
+ additionalPrefetchedUrlSearchParams.forEach((value, key) => {
+ nextUrl.searchParams.append(key, value);
+ });
+
+ await remoteAgent.forceSinglePrefetch(nextUrl);
+
+ // Add new query params to navigateToUrl to match No-Vary-Search test case.
+ const additionalNavigateToUrlSearchParams = new URLSearchParams(navigateQuery);
+ addNoVarySearchHeaderUsingPipe(navigateToUrl, noVarySearchHeaderValue);
+ additionalNavigateToUrlSearchParams.forEach((value, key) => {
+ navigateToUrl.searchParams.append(key, value);
+ });
+ await remoteAgent.navigate(navigateToUrl);
+ }
+
+ function prefetch_no_vary_search_test(description, noVarySearch, prefetchQuery, navigateQuery, shouldUsePrefetch){
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+ const agent = await spawnWindow(t, {});
+ await prefetchAndNavigate(agent,
+ noVarySearch,
+ prefetchQuery,
+ navigateQuery);
+
+ if(shouldUsePrefetch){
+ assert_prefetched(await agent.getRequestHeaders(),
+ "Navigation didn't use the prefetched response!");
+ }
+ else{
+ assert_not_prefetched(await agent.getRequestHeaders(),
+ "Navigation used the prefetched response!");
+ }
+ }, description);
+ }
+
+ // Test inputs:
+ // - description: a description of the test.
+ // - no-vary-search: No-Vary-Search header value for the response.
+ // - prefetch-query: added to query part of prefetch-executor when prefetching
+ // - navigate-query: added to query part of prefetch-executor when navigating
+ // - shouldUsePrefetch: if the test case expects the prefetched entry to be
+ // used or not.
+ [{description:"Use prefetched response as query parameter b has the same value.",
+ noVarySearch: 'params=("a")',
+ prefetchQuery: "a=2&b=3",
+ navigateQuery: "b=3",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use prefetched response as query parameter b has different value.",
+ noVarySearch: 'params("a")',
+ prefetchQuery: "a=2&b=3",
+ navigateQuery: "b=2",
+ shouldUsePrefetch: false},
+
+ {description:"Use prefetched response as the URLs do not vary by a and b.",
+ noVarySearch: 'params=("a" "b")',
+ prefetchQuery: "a=2&b=3",
+ navigateQuery: "b=2",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as the URLs do not vary on any query parameters.",
+ noVarySearch: "params",
+ prefetchQuery: "a=2&b=3",
+ navigateQuery: "b=4&c=5",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as the URLs do not vary on any query parameters.",
+ noVarySearch: "params",
+ prefetchQuery: "",
+ navigateQuery: "b=4&c=5",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use prefetched response as the URLs have different value for c.",
+ noVarySearch: "key-order",
+ prefetchQuery: "c=4&b=3&a=2",
+ navigateQuery: "a=2&c=5&b=3",
+ shouldUsePrefetch: false},
+
+ {description:"Don't use prefetched response as the URLs have the values in different order for a.",
+ noVarySearch: "key-order",
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5&b=3",
+ navigateQuery: "d=6&a=4&b=5&b=3&c=5&a=3",
+ shouldUsePrefetch: false},
+
+ {description:"Use prefetched response as the URLs have the same values for a.",
+ noVarySearch: "key-order",
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5&b=3",
+ navigateQuery: "d=6&a=3&b=5&b=3&c=5&a=4",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as the URLs have the same values for a.",
+ noVarySearch: "key-order=?1",
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5&b=3",
+ navigateQuery: "d=6&a=3&b=5&b=3&c=5&a=4",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use prefetched response as key-order is set to false and the URLs are not identical.",
+ noVarySearch: "key-order=?0",
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5&b=3",
+ navigateQuery: "d=6&a=3&b=5&b=3&c=5&a=4",
+ shouldUsePrefetch: false},
+
+ {description:"Use prefetched response as query parameter c can be ignored.",
+ noVarySearch: 'params=("c")',
+ prefetchQuery: "a=2&b=2&c=5",
+ navigateQuery: "a=2&c=3&b=2",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as query parameter a can be ignored.",
+ noVarySearch: 'params=("a")',
+ prefetchQuery: "a=2",
+ navigateQuery: "",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as query parameter a can be ignored.",
+ noVarySearch: 'params=("a")',
+ prefetchQuery: "",
+ navigateQuery: "a=2",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as all query parameters except c can be ignored.",
+ noVarySearch: 'params, except=("c")',
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as all query parameters except c can be ignored." +
+ " Only the last except matters.",
+ noVarySearch: 'params, except=("b"), except=("c")',
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use prefetched response as even though all query parameters" +
+ " except c can be ignored, c has different value.",
+ noVarySearch: 'params, except=("c")',
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=5",
+ shouldUsePrefetch: false},
+
+ {description:"Use prefetched response as even though all query parameters" +
+ " except c and d can be ignored, c value matches and d value matches.",
+ noVarySearch: 'params, except=("c" "d")',
+ prefetchQuery: "b=5&a=3&d=6&c=5",
+ navigateQuery: "d=6&a=1&b=2&c=5",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as even though all query parameters except" +
+ " c and d can be ignored, c value matches and d value matches." +
+ " Some query parameters to be ignored appear multiple times in the query.",
+ noVarySearch: 'params, except=("c" "d")',
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5",
+ navigateQuery: "d=6&a=1&a=2&b=2&b=3&c=5",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as all query parameters except c can be ignored." +
+ " Allow extension via parameters.",
+ noVarySearch: 'params, except=("c";unknown)',
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as query parameter c can be ignored." +
+ " Allow extension via parameters.",
+ noVarySearch: 'params=("c";unknown)',
+ prefetchQuery: "a=2&b=2&c=5",
+ navigateQuery: "a=2&c=3&b=2",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as the URLs have the values in different order for a." +
+ " Allow extension via parameters.",
+ noVarySearch: "key-order;unknown",
+ prefetchQuery: "b=5&a=3&a=4&d=6&c=5&b=3",
+ navigateQuery: "d=6&a=3&b=5&b=3&c=5&a=4",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as the URLs do not vary on any query parameters." +
+ " Allow extension via parameters.",
+ noVarySearch: "params;unknown",
+ prefetchQuery: "",
+ navigateQuery: "b=4&c=5",
+ shouldUsePrefetch: true},
+
+ {description:"Use prefetched response as all query parameters except c can be ignored." +
+ " Allow extension via parameters.",
+ noVarySearch: 'params;unknown, except=("c");unknown',
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use the prefetched URL. Empty No-Vary-Search means default URL variance." +
+ " The prefetched and the navigated URLs have to be the same.",
+ noVarySearch: "",
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "a=1&b=2&c=3",
+ shouldUsePrefetch: false},
+
+ {description:"Use the prefetched URL. Empty No-Vary-Search means default URL variance." +
+ " The prefetched and the navigated URLs have to be the same.",
+ noVarySearch: "",
+ prefetchQuery: "b=5&a=3&d=6&c=3",
+ navigateQuery: "b=5&a=3&d=6&c=3",
+ shouldUsePrefetch: true},
+
+ {description:"Use the prefetched URL. Empty No-Vary-Search means default URL variance." +
+ " The prefetched and the navigated URLs have to be the same.",
+ noVarySearch: "",
+ prefetchQuery: "",
+ navigateQuery: "",
+ shouldUsePrefetch: true},
+
+ {description:"Use the prefetched URL. Non-ASCII key - 2 UTF-8 code units." +
+ " Don't vary the response on the non-ASCII key.",
+ noVarySearch: 'params=("%C2%A2")',
+ prefetchQuery: "¢=3",
+ navigateQuery: "¢=4",
+ shouldUsePrefetch: true},
+
+ {description:"Use the prefetched URL. Non-ASCII key - 2 UTF-8 code units." +
+ " Don't vary the response on the non-ASCII key.",
+ noVarySearch: 'params=("%C2%A2")',
+ prefetchQuery: "a=2&¢=3",
+ navigateQuery: "¢=4&a=2",
+ shouldUsePrefetch: true},
+
+ {description:"Don't use the prefetched URL. Non-ASCII key - 2 UTF-8 code units." +
+ " Vary the response on the non-ASCII key.",
+ noVarySearch: 'params, except=("%C2%A2")',
+ prefetchQuery: "¢=3",
+ navigateQuery: "¢=4",
+ shouldUsePrefetch: false},
+
+ {description:"Use the prefetched URL. Non-ASCII key - 2 UTF-8 code units." +
+ " Vary the response on the non-ASCII key.",
+ noVarySearch: 'params, except=("%C2%A2")',
+ prefetchQuery: "¢=3&a=4",
+ navigateQuery: "a=5&¢=3",
+ shouldUsePrefetch: true},
+
+ ].forEach(({description, noVarySearch, prefetchQuery, navigateQuery, shouldUsePrefetch}) => {
+ subsetTest(prefetch_no_vary_search_test,
+ description, noVarySearch, prefetchQuery, navigateQuery,
+ shouldUsePrefetch);
+ });
+
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/out-of-document-rule-set.https.html b/testing/web-platform/tests/speculation-rules/prefetch/out-of-document-rule-set.https.html
new file mode 100644
index 0000000000..9f2c311715
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/out-of-document-rule-set.https.html
@@ -0,0 +1,152 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/subset-tests-by-key.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<meta name="variant" content="?include=BaseCase">
+<meta name="variant" content="?include=FollowRedirect">
+<meta name="variant" content="?include=RelativeUrlForSpeculationRulesSet">
+<meta name="variant" content="?include=RelativeUrlForCandidate">
+<meta name="variant" content="?include=UseNonUTF8EncodingForSpeculationRulesSet">
+<meta name="variant" content="?include=FailCORS">
+<meta name="variant" content="?include=FailToParseSpeculationRulesHeader">
+<meta name="variant" content="?include=InnerListInSpeculationRulesHeader">
+<meta name="variant" content="?include=EmptyRuleSet">
+<meta name="variant" content="?include=FailToParseRuleSet">
+<meta name="variant" content="?include=InvalidUrlForSpeculationRulesSet">
+<meta name="variant" content="?include=StatusCode199">
+<meta name="variant" content="?include=StatusCode404">
+<meta name="variant" content="?include=InvalidMimeType">
+
+<script>
+ async function runSpeculationRulesFetchTest(t, options) {
+ options = {
+ // Whether a prefetch is expected to succeed.
+ shouldPrefetch: true,
+ // Status code to be returned in the response.
+ status: 200,
+ // Whether a redirect must be followed to reach the rule set.
+ redirect: false,
+ // Whether to use relative URLs for the candidates in the rule set.
+ useRelativeUrlForCandidate: false,
+ // Whether to use relative URL for the rule set in SpeculationRules header.
+ useRelativeUrlForSpeculationRulesSet: false,
+ // Whether to use UTF-8 encoding for the rule set.
+ useUtf8EncodingForSpeculationRulesSet: true,
+ // Whether to force the response to cause a CORS failure.
+ failCors: false,
+ // Whether to use a valid SpeculationRules header format.
+ useValidSpeculationRulesHeaderValue: true,
+ // Whether to use an inner list of URLS in SpeculationRules header.
+ useInnerListInSpeculationRulesHeaderValue: false,
+ // Whether to return an empty response.
+ useEmptySpeculationRulesSet: false,
+ // Wheter to return a rule set with valid JSON format
+ useValidJsonForSpeculationRulesSet: true,
+ // Wheter to use a valid URL for the rule set in SpeculationRules header.
+ useValidUrlForSpeculationRulesSet: true,
+ // Wheter to use the valid "application/speculationrules-json" MIME type for the rule set.
+ useValidMimeTypeForSpeculationRulesSet: true,
+ ...options
+ };
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported.");
+
+ let page = 2;
+ let uuid = token();
+ let executor_url = new URL(`executor.sub.html`, SR_PREFETCH_UTILS_URL).toString();
+ if (options.useRelativeUrlForCandidate) {
+ executor_url = `executor.sub.html`;
+ }
+ let speculation_rule_set_url = `ruleset.py?url=${executor_url}&uuid=${uuid}&page=${page}&status=${options.status}&valid_mime=${options.useValidMimeTypeForSpeculationRulesSet}&valid_json=${options.useValidJsonForSpeculationRulesSet}&empty_json=${options.useEmptySpeculationRulesSet}&fail_cors=${options.failCors}&valid_encoding=${options.useUtf8EncodingForSpeculationRulesSet}&redirect=${options.redirect}`;
+ if (!options.useRelativeUrlForSpeculationRulesSet) {
+ let base_url = new URL(SR_PREFETCH_UTILS_URL);
+ base_url.hostname = PREFETCH_PROXY_BYPASS_HOST;
+ speculation_rule_set_url = new URL(speculation_rule_set_url, base_url).toString();
+ }
+ if (!options.useValidUrlForSpeculationRulesSet) {
+ speculation_rule_set_url = "http://:80/";
+ }
+
+ let speculation_rules_header = `header(Speculation-Rules,"${speculation_rule_set_url}")`;
+ if (!options.useValidSpeculationRulesHeaderValue) {
+ speculation_rules_header = `header(Speculation-Rules, x y z)`;
+ }
+ else if (options.useInnerListInSpeculationRulesHeaderValue) {
+ speculation_rules_header = `header(Speculation-Rules, \\("${speculation_rule_set_url}" "xyz.com/rule-set.json"\\))`;
+ }
+
+ let agent = await spawnWindow(t, {pipe: speculation_rules_header}, uuid);
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ // Passing non-ascii character '÷' as part of the next URL to check if we always decode the speculation rules set using utf-8 or not. This character is encoded differently in utf-8 and windows-1250
+ let nextUrl = agent.getExecutorURL({ page, str: decodeURIComponent('%C3%B7')});
+ await agent.navigate(nextUrl);
+
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ let test_case_desc = JSON.stringify(options);
+ if (options.shouldPrefetch)
+ assert_prefetched(await agent.getRequestHeaders(), `Prefetch should work for request ${test_case_desc}.`);
+ else
+ assert_not_prefetched(await agent.getRequestHeaders(), `Prefetch should not work for request ${test_case_desc}.`);
+ }
+
+ subsetTestByKey('BaseCase', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {});
+ }, "Base case.");
+
+ subsetTestByKey('FollowRedirect', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {redirect: true});
+ }, "It should follow redirects and fetch the speculation rules set.");
+
+ subsetTestByKey('RelativeUrlForSpeculationRulesSet', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {useRelativeUrlForSpeculationRulesSet: true});
+ }, "It should fetch a speculation rules set using its relative URL.");
+
+ subsetTestByKey('RelativeUrlForCandidate', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {useRelativeUrlForCandidate: true, shouldPrefetch: false});
+ }, "It should resolve the relative candidate URLs in the speculation rules set based on the speculation rules set's URL");
+
+ subsetTestByKey('UseNonUTF8EncodingForSpeculationRulesSet', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {useUtf8EncodingForSpeculationRulesSet: false, shouldPrefetch: false});
+ }, "The speculation rules set should always be encoded using UTF-8.");
+
+ subsetTestByKey('FailCORS', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {failCors: true, shouldPrefetch: false});
+ }, "It should reject the speculation rules set if CORS fails.");
+
+ subsetTestByKey('FailToParseSpeculationRulesHeader', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {useValidSpeculationRulesHeaderValue: false, shouldPrefetch: false});
+ }, "It should reject the speculation rules set if it fails to parse the SpeculationRules header.");
+
+ subsetTestByKey('InnerListInSpeculationRulesHeader', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {useInnerListInSpeculationRulesHeaderValue: true, shouldPrefetch: false});
+ }, "It should reject the speculation rules passed as inner list in the SpeculationRules header.");
+
+ subsetTestByKey('EmptyRuleSet', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {useEmptySpeculationRulesSet: true, shouldPrefetch: false});
+ }, "It should reject an empty speculation rules set.");
+
+ subsetTestByKey('FailToParseRuleSet', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {useValidJsonForSpeculationRulesSet: false, shouldPrefetch: false});
+ }, "It should reject the speculation rules set if it cannot parse it.");
+
+ subsetTestByKey('InvalidUrlForSpeculationRulesSet', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {useValidUrlForSpeculationRulesSet: false, shouldPrefetch: false});
+ }, "It should reject the speculation rules set with invalid URL.");
+
+ subsetTestByKey('StatusCode199', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {status: 199, shouldPrefetch: false});
+ }, "It should reject the speculation rules set with unsuccessful status code.");
+
+ subsetTestByKey('StatusCode404', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {status: 404, shouldPrefetch: false});
+ }, "It should reject the speculation rules set with unsuccessful status code.");
+
+ subsetTestByKey('InvalidMimeType', promise_test, async t => {
+ return runSpeculationRulesFetchTest(t, {useValidMimeTypeForSpeculationRulesSet: false, shouldPrefetch: false});
+ }, "It should reject the speculation rules set with invalid MIME type.");
+
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/prefetch-single.https.html b/testing/web-platform/tests/speculation-rules/prefetch/prefetch-single.https.html
new file mode 100644
index 0000000000..42f75d0c29
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/prefetch-single.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+<meta name="variant" content="?from_protocol=http&to_protocol=http">
+<meta name="variant" content="?from_protocol=http&to_protocol=https">
+<meta name="variant" content="?from_protocol=https&to_protocol=http">
+<meta name="variant" content="?from_protocol=https&to_protocol=https">
+<script>
+ // This is split across four test variants due to the test timeouts.
+ let { from_protocol, to_protocol } = Object.fromEntries(new URLSearchParams(location.search));
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let agent = await spawnWindow(t, { protocol: from_protocol });
+ let nextUrl = agent.getExecutorURL({ protocol: to_protocol, page: 2 });
+ await agent.forceSinglePrefetch(nextUrl);
+ await agent.navigate(nextUrl);
+
+ if (to_protocol == "https") {
+ assert_prefetched(await agent.getRequestHeaders(), "Prefetch should work for HTTPS urls.");
+ } else {
+ assert_not_prefetched(await agent.getRequestHeaders(), "Prefetch should not work for HTTP urls.");
+ }
+ }, `test single ${to_protocol} url prefetch from a ${from_protocol} url`);
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/prefetch-status.https.html b/testing/web-platform/tests/speculation-rules/prefetch/prefetch-status.https.html
new file mode 100644
index 0000000000..6835a55ee9
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/prefetch-status.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<meta name="variant" content="?status=200&should_prefetch=true">
+<meta name="variant" content="?status=250&should_prefetch=true">
+<meta name="variant" content="?status=299&should_prefetch=true">
+<meta name="variant" content="?status=400&should_prefetch=false">
+<meta name="variant" content="?status=500&should_prefetch=false">
+
+<script>
+ // This is split across four test variants due to the test timeouts.
+ let { status, should_prefetch } = Object.fromEntries(new URLSearchParams(location.search));
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let agent = await spawnWindow(t);
+ let nextUrl = agent.getExecutorURL({ page: 2, pipe: `status(${status})` });
+ await agent.forceSinglePrefetch(nextUrl);
+ await agent.navigate(nextUrl);
+
+ if (should_prefetch == 'true')
+ assert_prefetched(await agent.getRequestHeaders(), `Prefetch should work for request status:${status}.`);
+ else
+ assert_not_prefetched(await agent.getRequestHeaders(), `Prefetch should not work for request statue:${status}.`);
+ }, "Check that only prefetched requests with status in 200-299 range are used.");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/prefetch-traverse-reload.sub.html b/testing/web-platform/tests/speculation-rules/prefetch/prefetch-traverse-reload.sub.html
new file mode 100644
index 0000000000..ec6a7cd926
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/prefetch-traverse-reload.sub.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/websockets/constants.sub.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script>
+promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let agent = await spawnWindow(t, { protocol: 'https', pipe: 'header(Cache-Control, no-store)' });
+ let previousUrl = await agent.execute_script(() => location.href);
+ await agent.execute_script(async () => {
+ window.preventBfcache = new WebSocket('wss://{{ports[wss][0]}}/echo');
+ });
+
+ let nextUrl = agent.getExecutorURL({ protocol: 'https', page: 2 });
+ await agent.navigate(nextUrl);
+
+ await agent.forceSinglePrefetch(previousUrl);
+ await agent.execute_script(() => {
+ window.executor.suspend(() => history.go(-1));
+ });
+
+ assert_equals(previousUrl, await agent.execute_script(() => location.href));
+ assert_prefetched(await agent.getRequestHeaders(), "traversal should use prefetch");
+}, "prefetches can be used for traversal navigations");
+
+promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let agent = await spawnWindow(t, { protocol: 'https', pipe: 'header(Cache-Control, no-store)' });
+ let previousUrl = await agent.execute_script(() => location.href);
+ await agent.execute_script(async () => {
+ window.preventBfcache = new WebSocket('wss://{{ports[wss][0]}}/echo');
+ });
+
+ let nextUrl = agent.getExecutorURL({ protocol: 'https', page: 2 });
+ await agent.navigate(nextUrl);
+
+ await agent.forceSinglePrefetch(previousUrl);
+ // In https://html.spec.whatwg.org/multipage/nav-history-apis.html#delta-traverse,
+ // `sourceDocument` is `History`'s relevant global object's associated
+ // Document. In this case, it's `iframe.contentDocument`, and thus the
+ // prefetch from `win`'s Document (iframe's parent Document) isn't used.
+ await agent.execute_script(() => {
+ window.executor.suspend(() => {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.contentWindow.history.go(-1);
+ });
+ });
+
+ assert_equals(previousUrl, await agent.execute_script(() => location.href));
+ assert_not_prefetched(await agent.getRequestHeaders(),
+ "prefetch from different Document should not be used");
+}, "History's Document is used for traversal navigations");
+
+promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let agent = await spawnWindow(t, { protocol: 'https', pipe: 'header(Cache-Control, no-store)' });
+ let previousUrl = await agent.execute_script(() => location.href);
+ await agent.forceSinglePrefetch(previousUrl);
+ await agent.execute_script(() => {
+ window.executor.suspend(() => location.reload());
+ });
+
+ assert_equals(previousUrl, await agent.execute_script(() => location.href));
+ assert_prefetched(await agent.getRequestHeaders(), "reload should use prefetch");
+}, "prefetches can be used for reload navigations");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/redirect-url.https.html b/testing/web-platform/tests/speculation-rules/prefetch/redirect-url.https.html
new file mode 100644
index 0000000000..07db405dc3
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/redirect-url.https.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<script>
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let url = getRedirectUrl();
+ insertSpeculationRules({ prefetch: [{ source: 'list', urls: [url] }] });
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ assert_equals(await isUrlPrefetched(url), 1, "redirected url should be prefetched");
+ }, "browser should be able to prefetch redirected urls");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-from-rules.https.html b/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-from-rules.https.html
new file mode 100644
index 0000000000..bbb0343509
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-from-rules.https.html
@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<title>Prefetch with the referrer policy specified in speculation rules</title>
+
+<!--Split test cases due to the use of timeouts in speculation rules test utilities.-->
+<meta name="variant" content="?1-1">
+<meta name="variant" content="?2-2">
+<meta name="variant" content="?3-3">
+<meta name="variant" content="?4-4">
+<meta name="variant" content="?5-5">
+<meta name="variant" content="?6-6">
+<meta name="variant" content="?7-last">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/subset-tests.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<script>
+"use strict";
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("strict-origin-when-cross-origin");
+ const expectedReferrer = agent.getExecutorURL().origin + "/";
+
+ const nextURL = agent.getExecutorURL({ page: 2 });
+ await agent.forceSinglePrefetch(nextURL, { referrer_policy: "strict-origin" });
+ await agent.navigate(nextURL);
+
+ const headers = await agent.getRequestHeaders();
+ assert_prefetched(headers, "must be prefetched");
+ assert_equals(headers.referer, expectedReferrer, "must send the origin as the referrer");
+}, 'with "strict-origin" referrer policy in rule set overriding "strict-origin-when-cross-origin" of referring page');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ const next_url = agent.getExecutorURL({ page: 2 });
+ await agent.execute_script((url) => {
+ const a = addLink(url);
+ a.referrerPolicy = 'no-referrer';
+ insertDocumentRule(undefined, { referrer_policy: 'strict-origin' });
+ }, [next_url]);
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ await agent.navigate(next_url);
+
+ const headers = await agent.getRequestHeaders();
+ assert_prefetched(headers, 'must be prefetched');
+ const expected_referrer = next_url.origin + '/';
+ assert_equals(headers.referer, expected_referrer, 'must send the origin as the referrer');
+}, 'with "strict-origin" referrer policy in rule set override "no-referrer" of link');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("unsafe-url");
+
+ const nextURL = agent.getExecutorURL({ hostname: PREFETCH_PROXY_BYPASS_HOST, page: 2 });
+ await agent.forceSinglePrefetch(
+ nextURL, { referrer_policy: "no-referrer", requires: ["anonymous-client-ip-when-cross-origin"] });
+ await agent.navigate(nextURL);
+
+ // This referring page's referrer policy would not be eligible for
+ // cross-site prefetching, but setting a sufficiently strict policy in the
+ // rule allows for prefetching.
+ const headers = await agent.getRequestHeaders();
+ assert_prefetched(headers, "must be prefetched");
+ assert_equals(headers.referer, '', "must send no referrer");
+}, 'with "no-referrer" referrer policy in rule set overriding "unsafe-url" of cross-site referring page');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("strict-origin-when-cross-origin");
+
+ const nextURL = agent.getExecutorURL({ page: 2 });
+ await agent.forceSinglePrefetch(nextURL, { referrer_policy: "no-referrrrrrrer" });
+ await agent.navigate(nextURL);
+
+ const headers = await agent.getRequestHeaders();
+ assert_not_prefetched(headers, "must not be prefetched");
+}, 'unrecognized policies invalidate the rule');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("strict-origin-when-cross-origin");
+
+ const nextURL = agent.getExecutorURL({ page: 2 });
+ await agent.forceSinglePrefetch(nextURL, { referrer_policy: "never" });
+ await agent.navigate(nextURL);
+
+ const headers = await agent.getRequestHeaders();
+ assert_not_prefetched(headers, "must not be prefetched");
+}, 'treat legacy referrer policy values as invalid');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("strict-origin");
+ const expectedReferrer = agent.getExecutorURL().origin + "/";
+
+ const nextURL = agent.getExecutorURL({ hostname: PREFETCH_PROXY_BYPASS_HOST, page: 2 });
+ await agent.forceSinglePrefetch(
+ nextURL, { referrer_policy: "unsafe-url", requires: ["anonymous-client-ip-when-cross-origin"] });
+ await agent.navigate(nextURL);
+
+ // This referring page's referrer policy would normally make it eligible for
+ // cross-site prefetching, but setting an unacceptable policy in the rule
+ // makes it ineligible.
+ const headers = await agent.getRequestHeaders();
+ assert_not_prefetched(headers, "must not be prefetched");
+ assert_equals(headers.referer, expectedReferrer, "must send the origin as the referrer");
+}, 'with "unsafe-url" referrer policy in rule set overriding "strict-origin" of cross-site referring page');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("strict-origin");
+ const expectedReferrer = agent.getExecutorURL().origin + "/";
+
+ const nextURL = agent.getExecutorURL({ page: 2 });
+ // The empty string is a valid value for "referrer_policy" and will be
+ // treated as if the key were omitted.
+ await agent.forceSinglePrefetch(nextURL, { referrer_policy: "" });
+ await agent.navigate(nextURL);
+
+ const headers = await agent.getRequestHeaders();
+ assert_prefetched(headers, "must be prefetched");
+ assert_equals(headers.referer, expectedReferrer, "must send the origin as the referrer");
+}, 'with empty string referrer policy in rule set defaulting to "strict-origin" of referring page');
+
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-not-accepted.https.html b/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-not-accepted.https.html
new file mode 100644
index 0000000000..d7c003b3ca
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy-not-accepted.https.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<title>Prefetch attempts with an unacceptable referrer policy</title>
+
+<!--Split test cases due to the use of timeouts in speculation rules test utilities.-->
+<meta name="variant" content="?1-1">
+<meta name="variant" content="?2-last">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/subset-tests.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<script>
+"use strict";
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("unsafe-url");
+ const expectedReferrer = agent.getExecutorURL().href;
+
+ const nextURL = agent.getExecutorURL({ page: 2 });
+ await agent.forceSinglePrefetch(nextURL);
+ await agent.navigate(nextURL);
+
+ const headers = await agent.getRequestHeaders();
+ // The referrer policy restriction does not apply to same-site prefetch.
+ assert_prefetched(headers, "must be prefetched");
+ assert_equals(headers.referer, expectedReferrer, "must send the full URL as the referrer");
+}, 'with "unsafe-url" referrer policy on same-site referring page');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("unsafe-url");
+ const expectedReferrer = agent.getExecutorURL().href;
+
+ const nextURL = agent.getExecutorURL({ hostname: PREFETCH_PROXY_BYPASS_HOST, page: 2 });
+ // This prefetch attempt should be ignored.
+ await agent.forceSinglePrefetch(
+ nextURL, { requires: ["anonymous-client-ip-when-cross-origin"] });
+ await agent.navigate(nextURL);
+
+ const headers = await agent.getRequestHeaders();
+ assert_not_prefetched(headers, "must not be prefetched");
+ assert_equals(headers.referer, expectedReferrer, "must send the full URL as the referrer");
+}, 'with "unsafe-url" referrer policy on cross-site referring page');
+
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy.https.html b/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy.https.html
new file mode 100644
index 0000000000..1987d2e2ff
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/referrer-policy.https.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<title>Prefetch is done with the referring page's referrer policy</title>
+
+<!--Split test cases due to the use of timeouts in speculation rules test utilities.-->
+<meta name="variant" content="?1-1">
+<meta name="variant" content="?2-2">
+<meta name="variant" content="?3-3">
+<meta name="variant" content="?4-last">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/subset-tests.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+
+<script>
+"use strict";
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("strict-origin-when-cross-origin");
+ const expectedReferrer = agent.getExecutorURL().href;
+
+ const nextURL = agent.getExecutorURL({ page: 2 });
+ await agent.forceSinglePrefetch(nextURL);
+ await agent.navigate(nextURL);
+
+ const headers = await agent.getRequestHeaders();
+ assert_prefetched(headers, "must be prefetched");
+ assert_equals(headers.referer, expectedReferrer, "must send the full URL as the referrer");
+}, 'with "strict-origin-when-cross-origin" referrer policy');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("strict-origin");
+ const expectedReferrer = agent.getExecutorURL().origin + "/";
+
+ const nextURL = agent.getExecutorURL({ page: 2 });
+ await agent.forceSinglePrefetch(nextURL);
+ await agent.navigate(nextURL);
+
+ const headers = await agent.getRequestHeaders();
+ assert_prefetched(headers, "must be prefetched");
+ assert_equals(headers.referer, expectedReferrer, "must send the origin as the referrer");
+}, 'with "strict-origin" referrer policy');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("no-referrer");
+
+ const nextURL = agent.getExecutorURL({ page: 2 });
+ await agent.forceSinglePrefetch(nextURL);
+ await agent.navigate(nextURL);
+
+ const headers = await agent.getRequestHeaders();
+ assert_prefetched(headers, "must be prefetched");
+ assert_equals(headers.referer, '', "must send no referrer");
+}, 'with "no-referrer" referrer policy');
+
+subsetTest(promise_test, async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ const agent = await spawnWindow(t);
+ await agent.setReferrerPolicy("no-referrer");
+
+ const next_url = agent.getExecutorURL({ page: 2 });
+ await agent.execute_script((url) => {
+ const a = addLink(url);
+ a.referrerPolicy = 'strict-origin';
+ insertDocumentRule();
+ }, [next_url]);
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ await agent.navigate(next_url);
+
+ const headers = await agent.getRequestHeaders();
+ const expected_referrer = next_url.origin + '/';
+ assert_prefetched(headers, 'must be prefetched');
+ assert_equals(headers.referer, expected_referrer);
+}, 'with "strict-origin" link referrer policy overriding "no-referrer" of referring page');
+
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/authenticate.py b/testing/web-platform/tests/speculation-rules/prefetch/resources/authenticate.py
new file mode 100644
index 0000000000..037a7c144e
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/authenticate.py
@@ -0,0 +1,32 @@
+
+def main(request, response):
+ def fmt(x):
+ return f'"{x.decode("utf-8")}"' if x is not None else "undefined"
+
+ purpose = request.headers.get("Purpose", b"").decode("utf-8")
+ sec_purpose = request.headers.get("Sec-Purpose", b"").decode("utf-8")
+
+ headers = [(b"Content-Type", b"text/html"), (b'WWW-Authenticate', 'Basic')]
+ status = 200 if request.auth.username is not None or sec_purpose.startswith(
+ "prefetch") else 401
+
+ content = f'''
+ <!DOCTYPE html>
+ <script src="/common/dispatcher/dispatcher.js"></script>
+ <script src="utils.sub.js"></script>
+ <script>
+ window.requestHeaders = {{
+ purpose: "{purpose}",
+ sec_purpose: "{sec_purpose}"
+ }};
+
+ window.requestCredentials = {{
+ username: {fmt(request.auth.username)},
+ password: {fmt(request.auth.password)}
+ }};
+
+ const uuid = new URLSearchParams(location.search).get('uuid');
+ window.executor = new Executor(uuid);
+ </script>
+ '''
+ return status, headers, content
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/cookies.py b/testing/web-platform/tests/speculation-rules/prefetch/resources/cookies.py
new file mode 100644
index 0000000000..3c2299aa3a
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/cookies.py
@@ -0,0 +1,41 @@
+
+def main(request, response):
+ def get_cookie(key):
+ key = key.encode("utf-8")
+ if key in request.cookies:
+ return f'"{request.cookies[key].value.decode("utf-8")}"'
+ else:
+ return "undefined"
+
+ purpose = request.headers.get("Purpose", b"").decode("utf-8")
+ sec_purpose = request.headers.get("Sec-Purpose", b"").decode("utf-8")
+
+ cookie_count = int(
+ request.cookies[b"count"].value) if b"count" in request.cookies else 0
+ response.set_cookie("count", f"{cookie_count+1}",
+ secure=True, samesite="None")
+ response.set_cookie(
+ "type", "prefetch" if sec_purpose.startswith("prefetch") else "navigate")
+
+ headers = [(b"Content-Type", b"text/html")]
+
+ content = f'''
+ <!DOCTYPE html>
+ <script src="/common/dispatcher/dispatcher.js"></script>
+ <script src="utils.sub.js"></script>
+ <script>
+ window.requestHeaders = {{
+ purpose: "{purpose}",
+ sec_purpose: "{sec_purpose}"
+ }};
+
+ window.requestCookies = {{
+ count: {get_cookie("count")},
+ type: {get_cookie("type")}
+ }};
+
+ const uuid = new URLSearchParams(location.search).get('uuid');
+ window.executor = new Executor(uuid);
+ </script>
+ '''
+ return headers, content
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/executor.sub.html b/testing/web-platform/tests/speculation-rules/prefetch/resources/executor.sub.html
new file mode 100644
index 0000000000..ba1b3acb0c
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/executor.sub.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="utils.sub.js"></script>
+<script>
+window.requestHeaders = {
+ purpose: "{{header_or_default(Purpose, )}}",
+ sec_purpose: "{{header_or_default(Sec-Purpose, )}}",
+ referer: "{{header_or_default(Referer, )}}",
+};
+
+const uuid = new URLSearchParams(location.search).get('uuid');
+window.executor = new Executor(uuid);
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch.py b/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch.py
new file mode 100644
index 0000000000..4a0a7a3602
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch.py
@@ -0,0 +1,16 @@
+from wptserve.handlers import json_handler
+
+@json_handler
+def main(request, response):
+ uuid = request.GET[b"uuid"]
+ prefetch = request.headers.get(
+ "Sec-Purpose", b"").decode("utf-8").startswith("prefetch")
+
+ n = request.server.stash.take(uuid)
+ if n is None:
+ n = 0
+ if prefetch:
+ n += 1
+ request.server.stash.put(uuid, n)
+
+ return n
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch_nvs_hint.py b/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch_nvs_hint.py
new file mode 100644
index 0000000000..09c5d2eb73
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch_nvs_hint.py
@@ -0,0 +1,34 @@
+import time
+
+def main(request, response):
+ uuid = request.GET[b"uuid"]
+ prefetch = request.headers.get(
+ "Sec-Purpose", b"").decode("utf-8").startswith("prefetch")
+ if b"unblock" in request.GET:
+ request.server.stash.put(uuid, 0)
+ return ''
+
+ if b"nvs_header" in request.GET:
+ nvs_header = request.GET[b"nvs_header"]
+ response.headers.set("No-Vary-Search", nvs_header)
+
+ if prefetch:
+ nvswait = None
+ while nvswait is None:
+ time.sleep(0.1)
+ nvswait = request.server.stash.take(uuid)
+
+ content = (f'<!DOCTYPE html>\n'
+ f'<script src="/common/dispatcher/dispatcher.js"></script>\n'
+ f'<script src="utils.sub.js"></script>\n'
+ f'<script>\n'
+ f' window.requestHeaders = {{\n'
+ f' purpose: "{request.headers.get("Purpose", b"").decode("utf-8")}",\n'
+ f' sec_purpose: "{request.headers.get("Sec-Purpose", b"").decode("utf-8")}",\n'
+ f' referer: "{request.headers.get("Referer", b"").decode("utf-8")}",\n'
+ f' }};\n'
+ f' const uuid = new URLSearchParams(location.search).get("uuid");\n'
+ f' window.executor = new Executor(uuid);\n'
+ f'</script>\n')
+
+ return content
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/redirect.py b/testing/web-platform/tests/speculation-rules/prefetch/resources/redirect.py
new file mode 100644
index 0000000000..de7a4af987
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/redirect.py
@@ -0,0 +1,3 @@
+def main(request, response):
+ new_url = request.url.replace("redirect", "prefetch").encode("utf-8")
+ return 301, [(b"Location", new_url)], b""
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/ruleset.py b/testing/web-platform/tests/speculation-rules/prefetch/resources/ruleset.py
new file mode 100644
index 0000000000..97de1cc1a0
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/ruleset.py
@@ -0,0 +1,49 @@
+def main(request, response):
+ url = request.GET[b"url"].decode("utf-8")
+ uuid = request.GET[b"uuid"].decode("utf-8")
+ page = request.GET[b"page"].decode("utf-8")
+ valid_json = request.GET[b"valid_json"].decode("utf-8")
+ empty_json = request.GET[b"empty_json"].decode("utf-8")
+ fail_cors = request.GET[b"fail_cors"].decode("utf-8")
+ valid_encoding = request.GET[b"valid_encoding"].decode("utf-8")
+ redirect = request.GET[b"redirect"].decode("utf-8")
+ sec_fetch_dest = request.headers[b"Sec-Fetch-Dest"].decode(
+ "utf-8").lower() if b"Sec-Fetch-Dest" in request.headers else None
+ content_type = b"application/speculationrules+json" if request.GET[
+ b"valid_mime"].decode("utf-8") == "true" else b"application/json"
+ status = int(request.GET[b"status"])
+
+ if redirect == "true":
+ new_url = request.url.replace("redirect=true",
+ "redirect=false").encode("utf-8")
+ return 301, [(b"Location", new_url),
+ (b'Access-Control-Allow-Origin', b'*')], b""
+
+ encoding = "utf-8" if valid_encoding == "true" else "windows-1250"
+ content_type += f'; charset={encoding}'.encode('utf-8')
+ strparam = b'\xc3\xb7'.decode('utf-8')
+
+ content = f'''
+ {{
+ "prefetch": [
+ {{
+ "source":"list",
+ "urls":["{url}?uuid={uuid}&page={page}&str={strparam}"],
+ "requires":["anonymous-client-ip-when-cross-origin"]
+ }}
+ ]
+ }}
+ '''
+ if empty_json == "true":
+ content = ""
+ elif valid_json != "true":
+ content = "invalid json"
+ elif sec_fetch_dest is None or sec_fetch_dest != "script":
+ content = "normal document"
+
+ headers = [(b"Content-Type", content_type)]
+ if fail_cors != "true":
+ origin = request.headers[
+ b"Origin"] if b"Origin" in request.headers else b'*'
+ headers.append((b'Access-Control-Allow-Origin', origin))
+ return status, headers, content.encode(encoding)
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/sw.js b/testing/web-platform/tests/speculation-rules/prefetch/resources/sw.js
new file mode 100644
index 0000000000..db774f9d5b
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/sw.js
@@ -0,0 +1 @@
+self.addEventListener('fetch', () => event.respondWith(fetch(event.request)));
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/utils.sub.js b/testing/web-platform/tests/speculation-rules/prefetch/resources/utils.sub.js
new file mode 100644
index 0000000000..9b3b630733
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/utils.sub.js
@@ -0,0 +1,176 @@
+/**
+ * Utilities for initiating prefetch via speculation rules.
+ */
+
+// Resolved URL to find this script.
+const SR_PREFETCH_UTILS_URL = new URL(document.currentScript.src, document.baseURI);
+// Hostname for cross origin urls.
+const PREFETCH_PROXY_BYPASS_HOST = "{{hosts[alt][]}}";
+
+class PrefetchAgent extends RemoteContext {
+ constructor(uuid, t) {
+ super(uuid);
+ this.t = t;
+ }
+
+ getExecutorURL(options = {}) {
+ let {hostname, username, password, protocol, executor, ...extra} = options;
+ let params = new URLSearchParams({uuid: this.context_id, ...extra});
+ if(executor === undefined) {
+ executor = "executor.sub.html";
+ }
+ let url = new URL(`${executor}?${params}`, SR_PREFETCH_UTILS_URL);
+ if(hostname !== undefined) {
+ url.hostname = hostname;
+ }
+ if(username !== undefined) {
+ url.username = username;
+ }
+ if(password !== undefined) {
+ url.password = password;
+ }
+ if(protocol !== undefined) {
+ url.protocol = protocol;
+ url.port = protocol === "https" ? "{{ports[https][0]}}" : "{{ports[http][0]}}";
+ }
+ return url;
+ }
+
+ // Requests prefetch via speculation rules.
+ //
+ // In the future, this should also use browser hooks to force the prefetch to
+ // occur despite heuristic matching, etc., and await the completion of the
+ // prefetch.
+ async forceSinglePrefetch(url, extra = {}) {
+ await this.execute_script((url, extra) => {
+ insertSpeculationRules({ prefetch: [{source: 'list', urls: [url], ...extra}] });
+ }, [url, extra]);
+ return new Promise(resolve => this.t.step_timeout(resolve, 2000));
+ }
+
+ async navigate(url) {
+ await this.execute_script((url) => {
+ window.executor.suspend(() => {
+ location.href = url;
+ });
+ }, [url]);
+ url.username = '';
+ url.password = '';
+ assert_equals(
+ await this.execute_script(() => location.href),
+ url.toString(),
+ "expected navigation to reach destination URL");
+ await this.execute_script(() => {});
+ }
+
+ async getRequestHeaders() {
+ return this.execute_script(() => requestHeaders);
+ }
+
+ async getResponseCookies() {
+ return this.execute_script(() => {
+ let cookie = {};
+ document.cookie.split(/\s*;\s*/).forEach((kv)=>{
+ let [key, value] = kv.split(/\s*=\s*/);
+ cookie[key] = value;
+ });
+ return cookie;
+ });
+ }
+
+ async getRequestCookies() {
+ return this.execute_script(() => window.requestCookies);
+ }
+
+ async getRequestCredentials() {
+ return this.execute_script(() => window.requestCredentials);
+ }
+
+ async setReferrerPolicy(referrerPolicy) {
+ return this.execute_script(referrerPolicy => {
+ const meta = document.createElement("meta");
+ meta.name = "referrer";
+ meta.content = referrerPolicy;
+ document.head.append(meta);
+ }, [referrerPolicy]);
+ }
+
+ async getDeliveryType(){
+ return this.execute_script(() => {
+ return performance.getEntriesByType("navigation")[0].deliveryType;
+ });
+ }
+}
+
+// Produces a URL with a UUID which will record when it's prefetched.
+// |extra_params| can be specified to add extra search params to the generated
+// URL.
+function getPrefetchUrl(extra_params={}) {
+ let params = new URLSearchParams({ uuid: token(), ...extra_params });
+ return new URL(`prefetch.py?${params}`, SR_PREFETCH_UTILS_URL);
+}
+
+// Produces n URLs with unique UUIDs which will record when they are prefetched.
+function getPrefetchUrlList(n) {
+ return Array.from({ length: n }, () => getPrefetchUrl());
+}
+
+function getRedirectUrl() {
+ let params = new URLSearchParams({uuid: token()});
+ return new URL(`redirect.py?${params}`, SR_PREFETCH_UTILS_URL);
+}
+
+async function isUrlPrefetched(url) {
+ let response = await fetch(url, {redirect: 'follow'});
+ assert_true(response.ok);
+ return response.json();
+}
+
+// Must also include /common/utils.js and /common/dispatcher/dispatcher.js to use this.
+async function spawnWindow(t, options = {}, uuid = token()) {
+ let agent = new PrefetchAgent(uuid, t);
+ let w = window.open(agent.getExecutorURL(options), '_blank', options);
+ t.add_cleanup(() => w.close());
+ return agent;
+}
+
+function insertSpeculationRules(body) {
+ let script = document.createElement('script');
+ script.type = 'speculationrules';
+ script.textContent = JSON.stringify(body);
+ document.head.appendChild(script);
+}
+
+// Creates and appends <a href=|href|> to |insertion point|. If
+// |insertion_point| is not specified, document.body is used.
+function addLink(href, insertion_point=document.body) {
+ const a = document.createElement('a');
+ a.href = href;
+ insertion_point.appendChild(a);
+ return a;
+}
+
+// Inserts a prefetch document rule with |predicate|. |predicate| can be
+// undefined, in which case the default predicate will be used (i.e. all links
+// in document will match).
+function insertDocumentRule(predicate, extra_options={}) {
+ insertSpeculationRules({
+ prefetch: [{
+ source: 'document',
+ eagerness: 'eager',
+ where: predicate,
+ ...extra_options
+ }]
+ });
+}
+
+function assert_prefetched (requestHeaders, description) {
+ assert_in_array(requestHeaders.purpose, ["", "prefetch"], "The vendor-specific header Purpose, if present, must be 'prefetch'.");
+ assert_in_array(requestHeaders.sec_purpose,
+ ["prefetch", "prefetch;anonymous-client-ip"], description);
+}
+
+function assert_not_prefetched (requestHeaders, description){
+ assert_equals(requestHeaders.purpose, "", description);
+ assert_equals(requestHeaders.sec_purpose, "", description);
+}
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/same-origin-cookies.https.html b/testing/web-platform/tests/speculation-rules/prefetch/same-origin-cookies.https.html
new file mode 100644
index 0000000000..1d60a4bee0
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/same-origin-cookies.https.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src='/resources/testdriver-vendor.js'></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script>
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ await test_driver.delete_all_cookies();
+
+ let executor = 'cookies.py';
+ let agent = await spawnWindow(t, { executor });
+ let response_cookies = await agent.getResponseCookies();
+ let request_cookies = await agent.getRequestCookies();
+ assert_equals(request_cookies["count"], undefined);
+ assert_equals(request_cookies["type"], undefined);
+ assert_equals(response_cookies["count"], "1");
+ assert_equals(response_cookies["type"], "navigate");
+
+ let nextUrl = agent.getExecutorURL({ executor, page: 2 });
+ await agent.forceSinglePrefetch(nextUrl);
+ await agent.navigate(nextUrl);
+
+ response_cookies = await agent.getResponseCookies();
+ request_cookies = await agent.getRequestCookies();
+ assert_equals(request_cookies["count"], "1");
+ assert_equals(request_cookies["type"], "navigate");
+ assert_equals(response_cookies["count"], "2");
+ assert_equals(response_cookies["type"], "prefetch");
+
+ assert_prefetched(await agent.getRequestHeaders());
+
+ }, "speculation rules based prefetch should use cookies for same origin urls.");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/user-pass.https.html b/testing/web-platform/tests/speculation-rules/prefetch/user-pass.https.html
new file mode 100644
index 0000000000..94748f1eac
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prefetch/user-pass.https.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.sub.js"></script>
+<meta name="variant" content="?cross-origin=true">
+<meta name="variant" content="?cross-origin=false">
+<script>
+ let cross_origin = Object.fromEntries(new URLSearchParams(location.search))["cross-origin"] === "true";
+ promise_test(async t => {
+ assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+ let executor = "authenticate.py";
+ let credentials = { username: "user", password: "pass" };
+ let agent = await spawnWindow(t, { executor, ...credentials });
+ let request_credentials = await agent.getRequestCredentials();
+ assert_equals(request_credentials.username, credentials.username);
+ assert_equals(request_credentials.password, credentials.password);
+
+ let host = cross_origin ? { hostname: PREFETCH_PROXY_BYPASS_HOST } : {};
+ let nextUrl = agent.getExecutorURL({ page: 2, executor, ...host });
+ await agent.forceSinglePrefetch(nextUrl, { requires: ["anonymous-client-ip-when-cross-origin"] });
+ await agent.navigate(nextUrl);
+
+ let requestHeaders = await agent.getRequestHeaders();
+ request_credentials = await agent.getRequestCredentials();
+ if (cross_origin) {
+ assert_equals(request_credentials.username, undefined);
+ assert_equals(request_credentials.password, undefined);
+
+ assert_in_array(requestHeaders.purpose, ["", "prefetch"]);
+ assert_equals(requestHeaders.sec_purpose, "prefetch;anonymous-client-ip");
+ }
+ else {
+ assert_equals(request_credentials.username, credentials.username);
+ assert_equals(request_credentials.password, credentials.password);
+
+ assert_prefetched(await agent.getRequestHeaders());
+ }
+
+ }, "test www-authenticate basic does not forward credentials to cross-origin pages.");
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/about-blank-iframes.html b/testing/web-platform/tests/speculation-rules/prerender/about-blank-iframes.html
new file mode 100644
index 0000000000..9cc0ab3792
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/about-blank-iframes.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<!--
+Tests for about:blank iframes `document.prerendering` state
+and prerenderingchange event.
+-->
+<title>Test about:blank iframes prerendering state</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const channel = new PrerenderChannel('test-channel', uid);
+
+ const gotMessage = new Promise(resolve => {
+ channel.addEventListener('message', e => {
+ resolve(e.data);
+ }, {once: true});
+ });
+
+ // Make the window to start the prerender.
+ const url = `resources/about-blank-iframes.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const msg = await gotMessage;
+ assert_equals(msg, 'PASS');
+}, 'about:blank iframes');
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/accept-client-hint-cache.https.html b/testing/web-platform/tests/speculation-rules/prerender/accept-client-hint-cache.https.html
new file mode 100644
index 0000000000..66e2c90c04
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/accept-client-hint-cache.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Test Prerender pages maintain their own Client Hint Caches</title>
+<meta name="timeout" content="long">
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<script src="/client-hints/resources/open-and-add-load-event.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(
+ async t => {
+ const uid = token();
+ const initialization_bc = new PrerenderChannel(`test-channel`, uid);
+ t.add_cleanup(_ => initialization_bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ initialization_bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {once: true});
+ });
+
+ // Initialization. For the active pages, it has the client hint cache of
+ // "sec-ch-ua-bitness".
+ await open_and_add_load_event('resources/accept-ch.html');
+ window.open(
+ `resources/echo-client-hints-received.py?uid=${uid}`, '_blank',
+ 'noopener');
+
+ const result = await gotMessage;
+ assert_equals(result.result, 'PASSED', result.reason);
+ },
+ 'Prerender page should maintain their own client hints cache and do not propagate it to' +
+ 'the global cache until activation');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/activation-start.html b/testing/web-platform/tests/speculation-rules/prerender/activation-start.html
new file mode 100644
index 0000000000..676eca3fa1
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/activation-start.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<title>PerformanceNavigationTiming's activationStart in prerendered page</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js"></script>
+<script src="resources/utils.js"></script>
+
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const ACTIVATION_DELAY = 10;
+
+ const rcHelper = new RemoteContextHelper();
+ const referrerRC = await rcHelper.addWindow(undefined, { features: 'noopener' });
+ const prerenderedRC = await addPrerenderRC(referrerRC);
+ const iframeRC = await prerenderedRC.addIframe();
+
+ assert_equals(
+ await getActivationStart(prerenderedRC),
+ 0,
+ 'activationStart must be 0 while prerendering'
+ );
+
+ assert_equals(
+ await getActivationStart(iframeRC),
+ 0,
+ 'activationStart must be 0 while prerendering the iframe'
+ );
+
+ // Wait ACTIVATION_DELAY ms before activation.
+ await new Promise(resolve => t.step_timeout(resolve, ACTIVATION_DELAY));
+
+ await activatePrerenderRC(referrerRC, prerenderedRC);
+
+ assert_greater_than_equal(
+ await getActivationStart(prerenderedRC),
+ ACTIVATION_DELAY,
+ 'activationStart after activation must be greater than or equal to ' +
+ 'ACTIVATION_DELAY'
+ );
+
+ assert_greater_than_equal(
+ await getActivationStart(iframeRC),
+ ACTIVATION_DELAY,
+ 'activationStart after activation must be greater than or equal to ' +
+ 'ACTIVATION_DELAY in the iframe'
+ );
+});
+
+// A utility to both extract activationStart from the prerendered
+// RemoteContextWrapper, and also check that it shows up in toJSON().
+async function getActivationStart(prerenderedRC) {
+ const [activationStart, activationStartInToJSON] = await prerenderedRC.executeScript(() => {
+ const entry = performance.getEntriesByType("navigation")[0];
+ return [entry.activationStart, entry.toJSON().activationStart];
+ });
+
+ assert_equals(
+ activationStart,
+ activationStartInToJSON,
+ "activationStart value must be available in the result of toJSON()"
+ );
+
+ return activationStart;
+}
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/cache-storage.https.html b/testing/web-platform/tests/speculation-rules/prerender/cache-storage.https.html
new file mode 100644
index 0000000000..b2044a4078
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/cache-storage.https.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<title>Same-origin prerendering can access cache storage</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const cacheName = token();
+ const cache = await caches.open(cacheName);
+ await cache.add('resources/cache.txt');
+ t.add_cleanup(() => caches.delete(cacheName));
+ const response = await cache.match('resources/cache.txt');
+ const cacheText = await (response ? response.text() : 'primary cache match failed');
+
+ const {exec} = await create_prerendered_page(t);
+ const result = await exec(async cacheName => {
+ const cache = await caches.open(cacheName);
+ const match = await cache.match('cache.txt');
+ return await match.text();
+ }, [cacheName]);
+
+ assert_equals(
+ result, cacheText,
+ 'prerendering page should be able to read from cache storage.');
+}, 'prerendering page should be able to access cache storage')
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/clients-matchall.https.html b/testing/web-platform/tests/speculation-rules/prerender/clients-matchall.https.html
new file mode 100644
index 0000000000..bbd9ea2327
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/clients-matchall.https.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll with a prerender page</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const workerUrl = 'resources/clients-matchall-service-worker.js';
+ const pageUrl = `resources/prerendered-page.html?uid=${uid}`;
+
+ // Start a service worker.
+ const registration =
+ await service_worker_unregister_and_register(t, workerUrl, workerUrl);
+ t.add_cleanup(_ => registration.unregister());
+
+ // Observe the message from a prerendered page.
+ const bc = new PrerenderChannel('prerender-channel', uid);
+ t.add_cleanup(_ => bc.close());
+ const messagePromise = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {once:true});
+ });
+
+ startPrerendering(pageUrl);
+
+ const result = await messagePromise;
+ assert_equals(result, 'prerender success');
+
+ // The service worker returns the list of client urls exposed by
+ // Clients#matchAll().
+ const message = await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = resolve;
+ get_newest_worker(registration).postMessage('invoke clients.matchAll()');
+ });
+
+ assert_array_equals(
+ message.data,
+ [ window.location.href, new URL(pageUrl, location).toString() ]);
+}, 'The client urls (including a prerender page) are exposed by ' +
+ 'Clients#matchAll()');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/cookies.https.html b/testing/web-platform/tests/speculation-rules/prerender/cookies.https.html
new file mode 100644
index 0000000000..84cbd1c6d1
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/cookies.https.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<title>Same-origin prerendering can access cookies</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.js"></script>
+<script src="/cookie-store/resources/cookie-test-helpers.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+cookie_test(async t => {
+ const {exec} = await create_prerendered_page(t);
+ const initiator_cookie = 'initiator_cookie=exist';
+ const prerender_cookie = 'prerender_cookie=exist';
+
+ document.cookie = initiator_cookie;
+ const result = await exec(() => {
+ const result = document.cookie;
+ document.cookie = "prerender_cookie=exist;path=/;";
+ return result;
+ });
+
+ assert_equals(
+ result, initiator_cookie,
+ 'prerendering page should be able to read from document cookies.');
+
+ assert_equals(
+ document.cookie, initiator_cookie + '; ' + prerender_cookie,
+ 'prerendering page should be able to write to document cookies');
+}, 'prerendering page should be able to access cookies');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/credentialed-prerender-not-opt-in.html b/testing/web-platform/tests/speculation-rules/prerender/credentialed-prerender-not-opt-in.html
new file mode 100644
index 0000000000..8c797b8022
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/credentialed-prerender-not-opt-in.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<title>same-site cross-origin prerendering not opt in</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js"></script>
+<script src="resources/utils.js"></script>
+
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+ const referrerRC = await rcHelper.addWindow({origin: 'HTTPS_ORIGIN'}, { features: 'noopener' });
+ const prerenderedRC = await addPrerenderRC(referrerRC, {origin: 'HTTPS_REMOTE_ORIGIN'});
+
+ // Because the prerender doesn't use opt-in header, it is expected to be canceled.
+ // And the navigation is expected to create another page instead of activation.
+ referrerRC.navigateTo(prerenderedRC.url);
+ assert_equals(await getActivationStart(prerenderedRC), 0);
+});
+
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/credentialed-prerender-opt-in.html b/testing/web-platform/tests/speculation-rules/prerender/credentialed-prerender-opt-in.html
new file mode 100644
index 0000000000..eb8fd3cdfc
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/credentialed-prerender-opt-in.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<title>same-site cross-origin prerendering opt in</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js"></script>
+<script src="resources/utils.js"></script>
+
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+ const referrerRC = await rcHelper.addWindow({origin: 'HTTPS_ORIGIN'}, { features: 'noopener' });
+ const prerenderedRC = await addPrerenderRC(referrerRC, {origin: 'HTTPS_REMOTE_ORIGIN', headers: [['Supports-Loading-Mode', 'credentialed-prerender']] });
+
+ await activatePrerenderRC(referrerRC, prerenderedRC);
+});
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/cross-origin-iframe.html b/testing/web-platform/tests/speculation-rules/prerender/cross-origin-iframe.html
new file mode 100644
index 0000000000..f4819c207b
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/cross-origin-iframe.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+Tests for cross-origin iframes `document.prerendering` state.
+
+This file cannot be upstreamed to WPT until:
+* The specification describes the loading of cross-origin iframes. The test
+ expects that they are not loaded during prerendering.
+-->
+<title>Load a cross-origin document in a prerendered iframe</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/cross-origin-iframe.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting iframe loaded',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting iframe loaded',
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+ bc.close();
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `cross-origin iframes should not load until activation`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/cross-origin-isolated.https.html b/testing/web-platform/tests/speculation-rules/prerender/cross-origin-isolated.https.html
new file mode 100644
index 0000000000..01dafe00bf
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/cross-origin-isolated.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<title>Allow crossOriginIsolated in prerendered page</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const testChannel = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(() => {
+ testChannel.close();
+ });
+ const gotMessage = new Promise(resolve => {
+ testChannel.addEventListener('message', e => resolve(e.data), {once: true});
+ });
+
+ startPrerendering(`resources/cross-origin-isolated.https.html?uid=${uid}`);
+ assert_true(await gotMessage);
+}, 'Allow crossOriginIsolated in prerendered page');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-elem-inline-speculation-rules.tentative.html b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-elem-inline-speculation-rules.tentative.html
new file mode 100644
index 0000000000..0a3b3b8a70
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-elem-inline-speculation-rules.tentative.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+ <script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ promise_test(async t => {
+ // The key used for storing a test result in the server.
+ const key = token();
+
+ // Open the test runner in a popup - it will prerender itself, record the
+ // test results, and send them back to this harness.
+ const url =
+ `resources/csp-script-src-elem-inline-speculation-rules.html?key=${key}`;
+ window.open(url, '_blank', 'noopener');
+
+ // Wait until the test sends us the results.
+ const result = await nextValueFromServer(key);
+
+ assert_equals(result, "true", "initial document.prerendering");
+ }, 'Test if CSP script-src-elem inline-speculation-rules permits inline speculationrules.');
+ </script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-inline-speculation-rules.tentative.html b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-inline-speculation-rules.tentative.html
new file mode 100644
index 0000000000..923598bd27
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-inline-speculation-rules.tentative.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+ <script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ promise_test(async t => {
+ // The key used for storing a test result in the server.
+ const key = token();
+
+ // Open the test runner in a popup - it will prerender itself, record the
+ // test results, and send them back to this harness.
+ const url =
+ `resources/csp-script-src-inline-speculation-rules.html?key=${key}`;
+ window.open(url, '_blank', 'noopener');
+
+ // Wait until the test sends us the results.
+ const result = await nextValueFromServer(key);
+
+ assert_equals(result, "true", "initial document.prerendering");
+ }, 'Test if CSP script-src inline-speculation-rules permits inline speculationrules.');
+ </script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-self.html b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-self.html
new file mode 100644
index 0000000000..f0f9784666
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-self.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+ <script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ promise_test(async t => {
+ // The key used for storing a test result in the server.
+ const key = token();
+
+ // Open the test runner in a popup - it will prerender itself, record the
+ // test results, and send them back to this harness.
+ const url =
+ `resources/csp-script-src-self.html?key=${key}`;
+ window.open(url, '_blank', 'noopener');
+
+ // Wait until the test sends us the results.
+ const result = await nextValueFromServer(key);
+
+ assert_equals(result, "blocked by script-src-elem", "csp block");
+ }, 'Test if CSP script-src self does not permit inline speculationrules.');
+ </script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-strict-dynamic.html b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-strict-dynamic.html
new file mode 100644
index 0000000000..5e08877a49
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-strict-dynamic.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+ <script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ promise_test(async t => {
+ // The key used for storing a test result in the server.
+ const key = token();
+
+ // Open the test runner in a popup - it will prerender itself, record the
+ // test results, and send them back to this harness.
+ const url =
+ `resources/csp-script-src-strict-dynamic.html?key=${key}`;
+ window.open(url, '_blank', 'noopener');
+
+ // Wait until the test sends us the results.
+ const result = await nextValueFromServer(key);
+
+ assert_equals(result, "true", "");
+ }, 'Test if CSP script-src strict-dynamic allows inline speculationrules injected from the permitted scripts.');
+ </script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-unsafe-inline.html b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-unsafe-inline.html
new file mode 100644
index 0000000000..f6925f59f4
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/csp-script-src-unsafe-inline.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+ <script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ promise_test(async t => {
+ // The key used for storing a test result in the server.
+ const key = token();
+
+ // Open the test runner in a popup - it will prerender itself, record the
+ // test results, and send them back to this harness.
+ const url =
+ `resources/csp-script-src-unsafe-inline.html?key=${key}`;
+ window.open(url, '_blank', 'noopener');
+
+ // Wait until the test sends us the results.
+ const result = await nextValueFromServer(key);
+
+ assert_equals(result, "true", "initial document.prerendering");
+ }, 'Test if CSP script-src unsafe-inline permits inline speculationrules.');
+ </script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/fetch-blob.html b/testing/web-platform/tests/speculation-rules/prerender/fetch-blob.html
new file mode 100644
index 0000000000..7f8d557049
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/fetch-blob.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<title>Same-origin prerendering can access localStorage</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const {exec} = await create_prerendered_page(t);
+ const result = await exec(async () => {
+ const blob = await (await fetch('cache.txt')).blob();
+ const reader = new FileReader();
+ reader.readAsText(blob);
+ return new Promise(function(resolve, reject) {
+ reader.onload = () => resolve(reader.result);
+ reader.onerror = () => reject(reader.error);
+ });
+ });
+ const expected = "Hello, Prerender API!";
+
+ // Start prerendering a page that attempts to access the blob.
+ assert_equals(
+ result, expected,
+ 'prerendering page should be able to read from blobs.');
+}, 'prerendering page should be able to access blobs');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/fetch-intercepted-by-service-worker.https.html b/testing/web-platform/tests/speculation-rules/prerender/fetch-intercepted-by-service-worker.https.html
new file mode 100644
index 0000000000..6c2062a107
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/fetch-intercepted-by-service-worker.https.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<title>Service worker intercepts a fetch request coming from a prerendered page</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+const uid = token();
+
+const PAGE_URL = `resources/fetch-intercepted-by-service-worker.html?uid=${uid}`;
+const WORKER_URL = 'resources/fetch-intercept-worker.js';
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const registration =
+ await service_worker_unregister_and_register(t, WORKER_URL, PAGE_URL);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const bc = new PrerenderChannel('prerender-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ startPrerendering(PAGE_URL);
+
+ const result = await gotMessage;
+ assert_equals(
+ result, 'intercepted by service worker',
+ "fetch() should go through a service worker's fetch event handler");
+}, 'fetch() in a prerendering page should go through a service worker');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/iframe-added-post-activation.html b/testing/web-platform/tests/speculation-rules/prerender/iframe-added-post-activation.html
new file mode 100644
index 0000000000..c4263db87d
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/iframe-added-post-activation.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<!--
+Tests that an iframe added after activation starts out with
+document.prerendering false.
+-->
+<title>iframe added after activation</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const channel = new PrerenderChannel('test-channel', uid);
+ const messageQueue = new BroadcastMessageQueue(channel);
+ t.add_cleanup(_ => channel.close());
+
+ // Make the window to start the prerender.
+ const url = `resources/iframe-added-post-activation.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ // Wait for done.
+ const msg = await messageQueue.nextMessage();
+ assert_equals(msg, 'PASS');
+}, 'iframe added after activation has false document.prerendering');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/indexeddb.html b/testing/web-platform/tests/speculation-rules/prerender/indexeddb.html
new file mode 100644
index 0000000000..40528dce5a
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/indexeddb.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<title>Same-origin prerendering can access Indexed Database</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.js"></script>
+<script src="resources/indexedb-utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const db = await openIndexedDatabase(t);
+ assert_not_equals(db, null, 'Failed to open database.');
+ await addData(db, INITIATOR_KEY, INITIATOR_VALUE);
+
+ const {exec} = await create_prerendered_page(t);
+
+ const result = await exec(async () => {
+ await import_script_to_prerendered_page("indexedb-utils.js");
+ const db = await openIndexedDatabase();
+
+ await addData(db, PRERENDER_KEY, PRERENDER_VALUE);
+ const result = await readData(db, INITIATOR_KEY);
+ db.close();
+ return result;
+ });
+
+ assert_equals(
+ result, INITIATOR_VALUE,
+ 'prerendering page should be able to read from Indexed DataBase');
+ const initiatorReadResult = await readData(db, PRERENDER_KEY);
+ assert_equals(
+ initiatorReadResult, PRERENDER_VALUE,
+ 'prerendering page should be able to write to Indexed DataBase');
+ db.close();
+}, 'prerendering page should be able to access Indexed DataBase')
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/local-storage.html b/testing/web-platform/tests/speculation-rules/prerender/local-storage.html
new file mode 100644
index 0000000000..cd14685054
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/local-storage.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<title>Same-origin prerendering can access localStorage</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid1 = token();
+ const uid2 = token();
+
+ // A promise to wait until a prerendered page writes data with the "prerender"
+ // key in the local storage.
+ const write_promise = new Promise((resolve, reject) => {
+ window.addEventListener("storage", event => {
+ if (event.key !== 'prerender') {
+ reject("wrong key");
+ } else {
+ resolve();
+ }
+ }, { once: true });
+ });
+
+ window.localStorage.setItem('initial', uid1);
+ const {exec} = await create_prerendered_page(t);
+ const result = await exec(uid2 => {
+ window.localStorage.setItem('prerender', uid2);
+ return window.localStorage.getItem('initial');
+ }, [uid2])
+
+ // Start prerendering a page that attempts to access localStorage API.
+ assert_equals(
+ result, uid1,
+ 'prerendering page should be able to read from local storage');
+
+ await write_promise;
+ assert_equals(
+ window.localStorage.getItem('prerender'), uid2,
+ 'prerendering page should be able to write to local storage');
+}, 'prerendering page should be able to access local storage');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/main-frame-navigation.https.html b/testing/web-platform/tests/speculation-rules/prerender/main-frame-navigation.https.html
new file mode 100644
index 0000000000..47d8f2e81a
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/main-frame-navigation.https.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<title>Test about: main frame navigation in a prerendered page</title>
+<meta name="timeout" content="long">
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('result', uid);
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ const url = `resources/main-frame-navigation.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+
+ assert_equals(result.onprerenderingchangeCalled, true,
+ 'prerenderingchange event should be called after activation.');
+ assert_equals(result.prerenderingValueBeforeActivation, true,
+ 'document.prerendering should be true prior to activation.');
+ assert_equals(result.prerenderingValueAfterActivation, false,
+ 'document.prerendering should be false after activation.');
+}, 'Test document.prerendering and prerenderingchange event in the navigated ' +
+ 'page');
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/media-autoplay.html b/testing/web-platform/tests/speculation-rules/prerender/media-autoplay.html
new file mode 100644
index 0000000000..7e6b8d1047
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/media-autoplay.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<title>Same-origin prerendering can trigger autoplay</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ promise_test(async t => {
+ const {exec, activate} = await create_prerendered_page(t);
+ await exec(() => {
+ const video = document.createElement('video');
+ video.src = '/media/A4.mp4';
+ video.autoplay = true;
+ video.muted = true;
+ window.video = video;
+ document.body.appendChild(video);
+ });
+
+ await new Promise(resolve => t.step_timeout(resolve, 500));
+
+ const before_activation = await exec(() => ({
+ readyState: video.readyState,
+ paused: video.paused,
+ currentTime: video.currentTime
+ }));
+
+ await activate();
+ await new Promise(resolve => t.step_timeout(resolve, 500));
+ const after_activation = await exec(() => ({
+ readyState: video.readyState,
+ paused: video.paused,
+ currentTime: video.currentTime
+ }));
+
+ assert_equals(before_activation.paused, false);
+ assert_equals(before_activation.currentTime, 0);
+ assert_equals(after_activation.paused, false);
+ assert_greater_than(before_activation.currentTime, 0);
+ }, "media autoplay should defer playaback");
+</script>
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/navigation-intercepted-by-service-worker.https.html b/testing/web-platform/tests/speculation-rules/prerender/navigation-intercepted-by-service-worker.https.html
new file mode 100644
index 0000000000..916c704767
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/navigation-intercepted-by-service-worker.https.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<title>Service worker intercepts a navigation and starts prerendering</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+const uid = token();
+
+const PAGE_URL = `resources/non-existent-page.html?should-intercept&uid=${uid}`;
+const WORKER_URL = 'resources/fetch-intercept-worker.js';
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const registration =
+ await service_worker_unregister_and_register(t, WORKER_URL, PAGE_URL);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const bc = new PrerenderChannel('prerender-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ startPrerendering(PAGE_URL);
+
+ const result = await gotMessage;
+ assert_equals(result, 'prerender success');
+}, 'navigation should be intercepted by a service worker');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/navigator-plugins.tentative.html b/testing/web-platform/tests/speculation-rules/prerender/navigator-plugins.tentative.html
new file mode 100644
index 0000000000..192d4b3cd1
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/navigator-plugins.tentative.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<!--
+Note that `navigator.plugins` attribute is outdated in the HTML but we
+keep this test until major browsers actually stop supporting it.
+https://html.spec.whatwg.org/C/#dom-navigator-plugins
+-->
+<title>Same-origin prerendering can access navigator.plugins</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => {
+ assertSpeculationRulesIsSupported();
+ assert_implements_optional(
+ 'plugins' in navigator, 'navigator.plugins is not provided.'
+ );
+});
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('prerender-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ // Start prerendering a page that attempts to access the navigator.plugins.
+ startPrerendering(`resources/navigator-plugins.html?uid=${uid}`);
+ const result = await gotMessage;
+ const plugins = JSON.parse(result);
+ assert_equals(plugins.length, navigator.plugins.length);
+ for (let i = 0; i < plugins.length; ++i) {
+ const expected_plugin = navigator.plugins[i];
+ assert_equals(plugins[i].pluginLength, expected_plugin.length);
+ for (let j = 0; j < plugins[i].pluginLength; ++j) {
+ assert_equals(plugins[i].pluginTypes[j], expected_plugin[j].type,
+ `type of navigator.plugins[${i}].plugin[${j}]`);
+ }
+ }
+}, 'prerendering page should be able to access the navigator.plugins');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/navigator-subapp.https.tentative.html b/testing/web-platform/tests/speculation-rules/prerender/navigator-subapp.https.tentative.html
new file mode 100644
index 0000000000..8cfc78ac6e
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/navigator-subapp.https.tentative.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<title>navigator.subApp API test</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async () => {
+ const uid = token();
+ const bc = new PrerenderChannel('prerender-channel', uid);
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ startPrerendering(`resources/subapp.html?uid=${uid}`);
+ const result = await gotMessage;
+ assert_equals(result, 'InvalidStateError');
+}, 'prerendering pages should not be able to access subapp API.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/prefetch.https.html b/testing/web-platform/tests/speculation-rules/prerender/prefetch.https.html
new file mode 100644
index 0000000000..2a1553b3d9
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/prefetch.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const {tryToActivate, getNetworkRequestCount} =
+ await create_prerendered_page(t, {}, {prefetch: true}, {});
+
+ assert_equals(await tryToActivate(), 'activated');
+ assert_equals(await getNetworkRequestCount(), '1', 'Network request count');
+}, "Prerender navigation requests should use prefetched results");
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-from-rules.html b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-from-rules.html
new file mode 100644
index 0000000000..b19c5dddac
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-from-rules.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<title>Prerender with the referrer policy specified in speculation rules</title>
+
+<!--Split test cases due to the use of timeouts in test utilities.-->
+<meta name="variant" content="?1-1">
+<meta name="variant" content="?2-last">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/subset-tests.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js"></script>
+<script src="resources/utils.js"></script>
+
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+subsetTest(promise_test, async t => {
+ const {exec, tryToActivate} = await create_prerendered_page(
+ t, {},
+ {referrer_policy: 'strict-origin-when-cross-origin'},
+ {referrer_policy: 'strict-origin'});
+
+ const actualReferrer = await exec(() => { return document.referrer; });
+ const expectedReferrer = location.origin + "/";
+ assert_equals(actualReferrer, expectedReferrer, 'must send the origin as the referrer');
+
+ const result = await tryToActivate();
+ assert_equals(result, 'activated');
+}, 'with "strict-origin" referrer policy in rule set overriding "strict-origin-when-cross-origin" of referring page');
+
+subsetTest(promise_test, async t => {
+ const {exec, tryToActivate} = await create_prerendered_page(
+ t, {},
+ {referrer_policy: 'strict-origin-when-cross-origin'},
+ {referrer_policy: 'no-referrrrrrrer'});
+ const result = await tryToActivate();
+ assert_equals(result, 'discarded');
+}, 'unrecognized policies invalidate the rule');
+
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-mismatch.html b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-mismatch.html
new file mode 100644
index 0000000000..961b745e8e
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-mismatch.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<title>Referrer policy mismatches must be allowed between prerendering and activation</title>
+<meta name="timeout" content="long">
+<meta name="referrer" content="strict-origin-when-cross-origin">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js"></script>
+<script src="resources/utils.js"></script>
+
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+ const referrerRC = await rcHelper.addWindow(undefined, { features: "noopener" });
+ await setReferrerPolicy(referrerRC, "strict-origin-when-cross-origin");
+ const prerenderedRC = await addPrerenderRC(referrerRC);
+
+ const referrerURL = await referrerRC.executeScript(() => location.href);
+
+ assert_equals(await prerenderedRC.executeScript(() => document.prerendering), true);
+ assert_equals(await prerenderedRC.executeScript(() => document.referrer), referrerURL);
+
+ await activatePrerenderRC(referrerRC, prerenderedRC, url => {
+ const a = document.createElement("a");
+ a.href = url;
+ a.referrerPolicy = "no-referrer";
+ a.click();
+ });
+
+ assert_equals(await prerenderedRC.executeScript(() => document.prerendering), false);
+ assert_equals(await prerenderedRC.executeScript(() => document.referrer), referrerURL);
+}, 'prerendered with "strict-origin-when-cross-origin", activated with "no-referrer"');
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+ const referrerRC = await rcHelper.addWindow(undefined, { features: "noopener" });
+ await setReferrerPolicy(referrerRC, "strict-origin-when-cross-origin");
+ const prerenderedRC = await addPrerenderRC(referrerRC);
+
+ const referrerURL = await referrerRC.executeScript(() => location.href);
+
+ assert_equals(await prerenderedRC.executeScript(() => document.prerendering), true);
+ assert_equals(await prerenderedRC.executeScript(() => document.referrer), referrerURL);
+
+ await activatePrerenderRC(referrerRC, prerenderedRC, url => {
+ const a = document.createElement("a");
+ a.href = url;
+ a.referrerPolicy = "strict-origin";
+ a.click();
+ });
+
+ assert_equals(await prerenderedRC.executeScript(() => document.prerendering), false);
+ assert_equals(await prerenderedRC.executeScript(() => document.referrer), referrerURL);
+}, 'prerendered with "strict-origin-when-cross-origin", activated with "strict-origin"');
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+ const referrerRC = await rcHelper.addWindow(undefined, { features: "noopener" });
+ await setReferrerPolicy(referrerRC, "strict-origin");
+ const prerenderedRC = await addPrerenderRC(referrerRC);
+
+ const referrerURL = await referrerRC.executeScript(() => location.href);
+ const referrerOrigin = (new URL(referrerURL)).origin + "/";
+
+ assert_equals(await prerenderedRC.executeScript(() => document.prerendering), true);
+ assert_equals(await prerenderedRC.executeScript(() => document.referrer), referrerOrigin);
+
+ await activatePrerenderRC(referrerRC, prerenderedRC, url => {
+ const a = document.createElement("a");
+ a.href = url;
+ a.referrerPolicy = "unsafe-url";
+ a.click();
+ });
+
+ assert_equals(await prerenderedRC.executeScript(() => document.prerendering), false);
+ assert_equals(await prerenderedRC.executeScript(() => document.referrer), referrerOrigin);
+}, 'prerendered with "strict-origin", activated with "unsafe-url"');
+
+function setReferrerPolicy(referrerRC, referrerPolicy) {
+ return referrerRC.executeScript(referrerPolicy => {
+ const meta = document.createElement("meta");
+ meta.name = "referrer";
+ meta.content = referrerPolicy;
+ document.head.append(meta);
+ }, [referrerPolicy]);
+}
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-no-referrer.html b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-no-referrer.html
new file mode 100644
index 0000000000..a2bdda92f1
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-no-referrer.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta name="referrer" content="no-referrer">
+<meta name="timeout" content="long">
+<title>Test noreferrer</title>
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<script src="resources/referrer-test.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+// Tests that the referrer on the prerendering navigation request is not sent
+// when the triggering page's referrer policy is set to no-referrer.
+promise_test(async t => {
+ await referrer_test('(none)', token());
+}, 'no referrer');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-origin.html b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-origin.html
new file mode 100644
index 0000000000..47001d66e0
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-origin.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta name="referrer" content="origin">
+<meta name="timeout" content="long">
+<title>Test origin referrer</title>
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<script src="resources/referrer-test.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+// Tests that the referrer on the prerendering navigation request is the
+// triggering page's origin when the referrer policy is set to origin.
+// Note that "origin" is a lax referrer policy, but since this is a same-site
+// prerender, it does not cause the prerender to be aborted.
+promise_test(async t => {
+ const expected = new URL('', window.origin).href;
+ await referrer_test(expected, token());
+}, 'origin referrer');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-strict-origin.html b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-strict-origin.html
new file mode 100644
index 0000000000..2f5d1e935d
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/referrer-policy-strict-origin.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<meta name="referrer" content="strict-origin">
+<meta name="timeout" content="long">
+<title>Test strict-origin referrer</title>
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<script src="resources/referrer-test.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+// Tests that the referrer on the prerendering navigation request is the
+// triggering page's origin when the referrer policy is set to strict-origin.
+promise_test(async t => {
+ const expected = new URL('', window.origin).href;
+ await referrer_test(expected, token());
+}, 'strict-origin referrer');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/referrer.html b/testing/web-platform/tests/speculation-rules/prerender/referrer.html
new file mode 100644
index 0000000000..f10d64aeff
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/referrer.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<title>Test default referrer</title>
+<meta name="timeout" content="long">
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<script src="resources/referrer-test.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+// Tests that the referrer on the prerendering navigation request is the
+// triggering page's URL by default.
+promise_test(async t => {
+ await referrer_test(window.location.href, token());
+}, 'default referrer');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/register-service-worker.https.html b/testing/web-platform/tests/speculation-rules/prerender/register-service-worker.https.html
new file mode 100644
index 0000000000..2edda3df45
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/register-service-worker.https.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<title>Registration of a new service worker in a prerendered page</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+const uid = token();
+
+const PAGE_URL = `resources/register-service-worker.html?uid=${uid}`;
+
+// To make sure the service worker registered by the prerendered page starts up,
+// this test sends messages as the following sequence:
+// prerendered page => service worker => prerendered page => main page.
+promise_test(async t => {
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ window.open(PAGE_URL, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {event: 'started waiting ServiceWorker.register', prerendering: true},
+ {event: 'prerendering change', prerendering: false},
+ {event: 'service worker registered', prerendering: false},
+ {event: 'finished waiting ServiceWorker.register', prerendering: false},
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, 'New service worker should be registered in a prerendered page');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/remove-script-element.html b/testing/web-platform/tests/speculation-rules/prerender/remove-script-element.html
new file mode 100644
index 0000000000..9de7656f50
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/remove-script-element.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.js"></script>
+<head>
+<iframe id="iframe"></iframe>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+async_test(t => {
+ const doc = iframe.contentDocument;
+ const script = doc.createElement('script');
+ script.type = 'speculationrules';
+ script.text = `{"prerender": [{"source": "list", "urls": [] }] }`;
+ doc.head.appendChild(script);
+ iframe.remove();
+ t.step_timeout(() => {
+ document.head.appendChild(script);
+ t.done();
+ }, 0);
+}, 'Removing speculationrules script from detached document should not crash');
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/about-blank-iframes.html b/testing/web-platform/tests/speculation-rules/prerender/resources/about-blank-iframes.html
new file mode 100644
index 0000000000..db99d586e2
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/resources/about-blank-iframes.html
@@ -0,0 +1,115 @@
+<!doctype html>
+
+<title>about:blank iframe initiator and prerendered page</title>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="utils.js"></script>
+<script>
+// Called by iframe-nav-to-about-blank in case empty.html loads before
+// we can add the proper onload handler below.
+let iframeLoadedEmptyHtml = false;
+function onIframeLoadedEmptyHtml() {
+ iframeLoadedEmptyHtml = true;
+}
+</script>
+<body>
+<iframe id="iframe-no-src"></iframe>
+<iframe id="iframe-srcdoc" srcdoc="<!doctype html><p>wow look</p>"></iframe>
+<iframe id="iframe-nav-to-about-blank" src="empty.html"
+ onload="onIframeLoadedEmptyHtml();"></iframe>
+<script>
+
+// When loaded without the "?prerendering" param, this document
+// is called the "intiator page". It starts a prerender to the same
+// URL, but with the "?prerendering" param.
+//
+// When loaded with the "?prerendering" param, this document is
+// the "prerendered page". It tells the initiator page when it is
+// ready to activate, and messages the main test page when it is
+// finished.
+
+// Runs the logic of the prerendered page.
+//
+// The prerendered page has about:blank/srcdoc iframes and tests their
+// document.prerendering state before and after activation.
+async function main() {
+ // The iframe-no-src iframe has no src attribute.
+ const iframeNoSrc = document.querySelector('#iframe-no-src');
+
+ // The iframe-srcdoc iframe has a srcdoc attribute.
+ const iframeSrcdoc = document.querySelector('#iframe-srcdoc');
+
+ // The iframe-nav-to-about-blank first navigates to a non-about:blank URL,
+ // then sets src to about:blank.
+ const iframeNavToAboutBlank =
+ document.querySelector('#iframe-nav-to-about-blank');
+
+ await new Promise((resolve, reject) => {
+ iframeNavToAboutBlank.addEventListener('load', (e) => {
+ if (iframeNavToAboutBlank.src != 'about:blank')
+ iframeNavToAboutBlank.src = 'about:blank';
+ else
+ resolve();
+ });
+
+ // In case the original navigation to empty.html already finished before we
+ // added the event listener above, navigate to about:blank now.
+ if (iframeLoadedEmptyHtml)
+ iframeNavToAboutBlank.src = 'about:blank';
+ });
+
+ // Collect the documents.
+ let frames = [
+ {doc: document, name: 'main'},
+ {doc: iframeNoSrc.contentDocument, name: 'iframeNoSrc'},
+ {doc: iframeSrcdoc.contentDocument, name: 'iframeSrcdoc'},
+ {doc: iframeNavToAboutBlank.contentDocument, name: 'iframeNavToAboutBlank'}
+ ];
+
+ // Test document.prerendering pre-activation for each document.
+ for (let i = 0; i < frames.length; i++) {
+ assert_true(frames[i].doc.prerendering,
+ `document.prerendering pre-activation: ${frames[i].name}`);
+ }
+
+ // Resolves with [bool, bool, bool, ...] upon activation. Each `bool` is the
+ // value of document.prerendering in the prerenderingchange event for the
+ // corresponding document.
+ let activationPromises = frames.map(x => {
+ return new Promise((resolve, reject) => {
+ x.doc.addEventListener('prerenderingchange', (e) => {
+ resolve(document.prerendering);
+ });
+ });
+ });
+
+ // Ask to activate.
+ const prerenderChannel = new PrerenderChannel('prerender-channel');
+ prerenderChannel.postMessage('readyToActivate');
+
+ // Test document.prerendering post-activation for each document.
+ let activationResults = await Promise.all(activationPromises);
+ for (let i = 0; i < activationResults.length; i++) {
+ assert_false(activationResults[i],
+ `document.prerendering in prerenderingchange for ${frames[i].name}`);
+ }
+}
+
+// See comment at the top of this file.
+const params = new URLSearchParams(location.search);
+const isPrerendering = params.has('prerendering');
+if (!isPrerendering) {
+ loadInitiatorPage();
+} else {
+ // For the prerendering page, run main() then message the test page with the
+ // result.
+ const testChannel = new PrerenderChannel('test-channel');
+ main().then(() => {
+ testChannel.postMessage('PASS');
+ }).catch((e) => {
+ testChannel.postMessage('FAIL: ' + e);
+ });
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/accept-ch.html b/testing/web-platform/tests/speculation-rules/prerender/resources/accept-ch.html
new file mode 100644
index 0000000000..1a23661a44
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/resources/accept-ch.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<body>
+ This page does nothing but update the ACCEPT-CH headers.
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/accept-ch.html.headers b/testing/web-platform/tests/speculation-rules/prerender/resources/accept-ch.html.headers
new file mode 100644
index 0000000000..6c56648705
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/resources/accept-ch.html.headers
@@ -0,0 +1,2 @@
+Accept-CH: sec-ch-ua-bitness
+Access-Control-Allow-Origin: * \ No newline at end of file
diff --git a/testing/web-platform/tests/speculation-rules/prerender/resources/audio-setSinkId.https.html b/testing/web-platform/tests/speculation-rules/prerender/resources/audio-setSinkId.https.html
new file mode 100644
index 0000000000..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>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/response-code-non-successful.html b/testing/web-platform/tests/speculation-rules/prerender/response-code-non-successful.html
new file mode 100644
index 0000000000..149971719c
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/response-code-non-successful.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<title>Check that non-successful responses result in discarding the prerender</title>
+<meta name="variant" content="?code=204">
+<meta name="variant" content="?code=205">
+<meta name="variant" content="?code=402">
+<meta name="variant" content="?code=404">
+<meta name="variant" content="?code=500">
+<meta name="variant" content="?code=503">
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+const params = new URLSearchParams(window.location.search);
+const code = params.get('code');
+
+promise_test(async t => {
+ const {exec, tryToActivate} = await create_prerendered_page(t, {code});
+ const result = await tryToActivate();
+ assert_equals(result, 'discarded');
+},`Responses with code ${code} should be discarded`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/response-code-successful.html b/testing/web-platform/tests/speculation-rules/prerender/response-code-successful.html
new file mode 100644
index 0000000000..adf8a8932f
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/response-code-successful.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<title>Check that successful responses result in activating the prerender</title>
+<meta name="variant" content="?code=200">
+<meta name="variant" content="?code=201">
+<meta name="variant" content="?code=202">
+<meta name="variant" content="?code=203">
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+const params = new URLSearchParams(window.location.search);
+const code = params.get('code');
+
+promise_test(async t => {
+ const {exec, tryToActivate} = await create_prerendered_page(t, {code});
+ const result = await tryToActivate();
+ assert_equals(result, 'activated');
+},`Responses with code ${code} should be activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-audio-setSinkId-with-invalid-sinkId.https.tentative.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-audio-setSinkId-with-invalid-sinkId.https.tentative.html
new file mode 100644
index 0000000000..3cdc852802
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-audio-setSinkId-with-invalid-sinkId.https.tentative.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<title>
+Access to the setSinkId of the Audio API with an invalid value is deferred
+</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ const url = `resources/audio-setSinkId.https.html?sinkId=invalid&uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting Audio.setSinkId',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'Audio.setSinkId rejected: NotFoundError',
+ prerendering: false
+ },
+ ];
+ // The spec, https://wicg.github.io/nav-speculation/prerendering.html#audio-output-patch,
+ // mentions selectAudioOutput() but this test uses setSinkId() function.
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event${i}`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering${i}`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the setSinkId of Audio API with the invalid sinkId should be
+ deferred until the prerendered page is activated`);
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-audio-setSinkId.https.tentative.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-audio-setSinkId.https.tentative.html
new file mode 100644
index 0000000000..8ae72860f0
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-audio-setSinkId.https.tentative.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<title>Access to the setSinkId of the Audio API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ const url = `resources/audio-setSinkId.https.html?sinkId=default&uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting Audio.setSinkId',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting Audio.setSinkId',
+ prerendering: false
+ },
+ ];
+ // The spec, https://wicg.github.io/nav-speculation/prerendering.html#audio-output-patch,
+ // mentions selectAudioOutput() but this test uses setSinkId() function.
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event${i}`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering${i}`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the setSinkId of Audio API should be deferred until the
+ prerendered page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-background-fetch.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-background-fetch.https.html
new file mode 100644
index 0000000000..cd1550b819
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-background-fetch.https.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<title>Access to the Background Fetch API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ // We grant the permission here to make a more discerning test because
+ // backgroundFetch.fetch() waits until the permission is granted, which
+ // is deferred during prerendering so the test would trivially pass without
+ // the permission.
+ await test_driver.set_permission({ name: "background-fetch" }, "granted");
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ // Install the service worker first to test backgroundFetch.fetch in the
+ // prerendering page.
+ const scope = 'resources/';
+ const script = 'resources/do-nothing-worker.js';
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const url = `resources/background-fetch.https.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {event: 'started waiting backgroundFetch.fetch', prerendering: true},
+ {event: 'prerendering change', prerendering: false},
+ {event: 'finished waiting backgroundFetch.fetch', prerendering: false},
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `The access to the Background Fetch API should be deferred until the
+ prerendered page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-background-sync.tentative.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-background-sync.tentative.https.html
new file mode 100644
index 0000000000..05f9388f94
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-background-sync.tentative.https.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<title>Access to the Background Sync API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ // We grant the permission here to make a more discerning test because
+ // periodicSync.register() waits until the permission is granted, which
+ // is deferred during prerendering so the test would trivially pass without
+ // the permission.
+ await test_driver.set_permission({name: 'periodic-background-sync'}, 'granted');
+
+ // Install the service worker first to test periodicSync.register in the
+ // prerendering page.
+ const scope = `resources/`;
+ const script = `resources/do-nothing-worker.js`;
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const url = `resources/background-sync.https.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {event: 'started waiting periodicSync.register', prerendering: true},
+ {event: 'prerendering change', prerendering: false},
+ {event: 'finished waiting periodicSync.register', prerendering: false},
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `The access to the Background Sync API should be deferred until the
+ prerendered page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-battery-status.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-battery-status.https.html
new file mode 100644
index 0000000000..80e25b08e7
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-battery-status.https.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<title>Access to the Battery Status API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/battery-status.https.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {event: 'started waiting navigator.getBattery', prerendering: true},
+ {event: 'prerendering change', prerendering: false},
+ {event: 'finished waiting navigator.getBattery', prerendering: false},
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+ bc.close();
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the Battery Status API should be deferred until the
+ prerendered page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-bluetooth.tentative.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-bluetooth.tentative.https.html
new file mode 100644
index 0000000000..243a5d5a74
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-bluetooth.tentative.https.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<title>Access to the Bluetooth API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/bluetooth-access.https.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting navigator.bluetooth.getAvailability',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting navigator.bluetooth.getAvailability',
+ prerendering: false
+ },
+ ];
+ // The spec, https://wicg.github.io/nav-speculation/prerendering.html#web-bluetooth-patch,
+ // mentions getDevices() and requestDevice() but this test uses
+ // getAvailability() instead of them.
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+ bc.close();
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the Bluetooth API should be deferred until the
+ prerendered page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-broadcast-channel.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-broadcast-channel.html
new file mode 100644
index 0000000000..7225e64cf9
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-broadcast-channel.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<title>BroadcastChannel#postMessage is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ const url = `resources/broadcast-channel.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting BroadcastChannel',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'received message: hello',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting BroadcastChannel',
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event${i}`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering${i}`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `BroadcastChannel#postMessage should be deferred until the prerendered ` +
+ `page is activated`);
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-dedicated-worker.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-dedicated-worker.https.html
new file mode 100644
index 0000000000..c58bd3434f
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-dedicated-worker.https.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<title>Access to the Dedicated Worker API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/dedicated-worker.https.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting worker construction',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting worker construction',
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `The access to the Dedicated Worker API should be deferred until the
+ prerendered page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-encrypted-media-unsupported-config.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-encrypted-media-unsupported-config.https.html
new file mode 100644
index 0000000000..6a5cc0e89a
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-encrypted-media-unsupported-config.https.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<title>Access to the Encrypted Media API is deferred with unsupported config
+</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/encrypted-media.https.html?config=unsupport&uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting navigator.requestMediaKeySystemAccess',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'navigator.requestMediaKeySystemAccess rejected: ' +
+ 'NotSupportedError',
+ prerendering: false
+ }
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event${i}`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering${i}`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the Encrypted Media API should be deferred with the
+ unsupported configurations until the prerendered page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-encrypted-media.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-encrypted-media.https.html
new file mode 100644
index 0000000000..9c375f59c9
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-encrypted-media.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<title>Access to the Encrypted Media API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/encrypted-media.https.html?config=support&uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting navigator.requestMediaKeySystemAccess',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting navigator.requestMediaKeySystemAccess',
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event${i}`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering${i}`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the Encrypted Media API should be deferred until the
+ prerendered page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-focus.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-focus.html
new file mode 100644
index 0000000000..1149b8bd09
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-focus.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Prerendering documents are not focused</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<input type="text" id = "prerenderTextField">
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ document.getElementById('prerenderTextField').focus();
+ assert_true(
+ document.hasFocus(),
+ 'Initial document should have focus.');
+
+ const {exec} = await create_prerendered_page(t);
+ const result = await exec(() => {
+ const element = document.createElement('input');
+ element.setAttribute('type', 'text');
+ document.body.appendChild(element);
+ element.focus();
+
+ // Post the focus and active states to the initiator page.
+ return {
+ activeElementUpdated: document.activeElement === element,
+ documentHasFocus: document.hasFocus()
+ };
+ })
+
+ assert_true(result.activeElementUpdated, 'Active element has been updated');
+ assert_false(result.documentHasFocus, 'Document should not have focus');
+}, 'Prerendering document should update the active element but not have focus');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-idle-detection.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-idle-detection.https.html
new file mode 100644
index 0000000000..688ae00ba7
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-idle-detection.https.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<title>Prerendering cannot invoke the Idle Detection API</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('prerender-channel', uid);
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ // Start prerendering a page that attempts to invoke the Idle Detection API.
+ // This API is activation-gated so it's expected to fail:
+ // https://wicg.github.io/nav-speculation/prerendering.html#activation-gated
+ startPrerendering(`resources/idle-detection.https.html?uid=${uid}`);
+ const result = await gotMessage;
+ assert_equals(result, 'NotAllowedError');
+ bc.close();
+}, `prerendering pages should not be able to invoke the Idle Detection API`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-local-file-system-access.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-local-file-system-access.https.html
new file mode 100644
index 0000000000..78aa2da84d
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-local-file-system-access.https.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<title>Same-origin prerendering cannot access local file system</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('prerender-channel', uid);
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ // Start prerendering a page that attempts to show a local file picker.
+ startPrerendering(`resources/file-picker.html?uid=${uid}`);
+ const result = await gotMessage;
+ assert_equals(result, 'SecurityError')
+}, `prerendering pages should not be able to access the local file system ` +
+ `via the File System Access API`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-media-auto-play-attribute.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-auto-play-attribute.html
new file mode 100644
index 0000000000..2fbcc35124
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-auto-play-attribute.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<!--
+The test aligns with the spec on which behavior is deferred. The test asserts
+that media resources are not loaded during prerendering, but it's possible the
+spec will allow loading and only disallow playback.
+-->
+<title>Access to the Autoplay of the Media is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+function RunTest(type, description) {
+ promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/media-autoplay-attribute.html?type=${type}&uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting Autoplay',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'fired loadedmetadata event after prerendering is activated',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting Autoplay',
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length, `${type}`);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event${i}`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering${i}`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+ }, description);
+}
+
+RunTest('audio', `autoplay of the audio media should be deferred until the prerendered page is activated`);
+
+RunTest('video', `autoplay of the video media should be deferred until the prerendered page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-media-camera.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-camera.https.html
new file mode 100644
index 0000000000..78f7dd098d
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-camera.https.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<title>Access to the Camera of the user media device is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/media-devices-access.https.html?video=true&uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+ // According to spec, gUM will resolve only if the window has focus.
+ window.focus();
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting navigator.mediaDevices.getUserMedia',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting navigator.mediaDevices.getUserMedia',
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event${i}`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering${i}`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the camera of the user media should be deferred until the
+ prerendered page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-media-device-info.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-device-info.https.html
new file mode 100644
index 0000000000..2e419c027e
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-device-info.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<title>Access to the Media Device Info is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/media-device-info.https.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting navigator.mediaDevices.enumerateDevices',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting navigator.mediaDevices.enumerateDevices',
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+ bc.close();
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the Media Device Info should be deferred until the prerendered
+ page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-media-microphone.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-microphone.https.html
new file mode 100644
index 0000000000..09385be4b1
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-microphone.https.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<title>Access to the Microphone of the user media device is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/media-devices-access.https.html?&audio=true&uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+ // According to spec, gUM will resolve only if the window has focus.
+ window.focus();
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting navigator.mediaDevices.getUserMedia',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting navigator.mediaDevices.getUserMedia',
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event${i}`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering${i}`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the Microphone of the user media should be deferred until the
+ prerendered page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-media-play.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-play.html
new file mode 100644
index 0000000000..495019d7cc
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-media-play.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<!--
+The test aligns with the spec on which behavior is deferred. The test asserts
+that media resources are not loaded during prerendering, but it's possible the
+spec will allow loading and only disallow playback.
+-->
+<title>Access to the Play of the Media is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+function RunTest(type, description) {
+ promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ const url = `resources/media-play.html?type=${type}&uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting Media.Play',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'fired loadedmetadata event after prerendering is activated',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting Media.Play',
+ prerendering: false
+ },
+ ];
+
+ assert_equals(result.length, expected.length, `${type}`);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event${i}`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering${i}`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+ }, description);
+}
+
+RunTest('audio', `play of the audio media should be deferred until the prerendered page is activated`);
+
+RunTest('video', `play of the video media should be deferred until the prerendered page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-message-boxes.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-message-boxes.html
new file mode 100644
index 0000000000..b494a57257
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-message-boxes.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+function runTest(test_file, expectation, description) {
+ promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('prerender-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ // Open a new window to test the message box.
+ window.open(`${test_file}&uid=${uid}`, '_blank', 'noopener');
+
+ // Wait for the message from the prerendering page.
+ assert_equals(await gotMessage, expectation);
+ }, description);
+}
+
+// Test that a page invokes the alert modal during prerendering.
+runTest(
+ 'resources/message-boxes.html?alert',
+ 'no block',
+ 'alert() does not display the modal and returns immediately');
+
+// Test that a page invokes the confirm modal during prerendering.
+runTest(
+ 'resources/message-boxes.html?confirm',
+ 'the return value is no',
+ 'confirm() does not display the modal and returns immediately');
+
+// Test that a page invokes the prompt modal during prerendering.
+runTest(
+ 'resources/message-boxes.html?prompt',
+ 'the return value is null',
+ 'prompt() does not display the modal and returns immediately');
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-midi-sysex.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-midi-sysex.https.html
new file mode 100644
index 0000000000..a48fbd39c3
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-midi-sysex.https.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<title>Access to the Midi API with sysex=true is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ // Grant the permission here to make a more discerning test because
+ // navigator.requestMIDIAccess() waits until the permission is granted, which
+ // is deferred during prerendering so the test would trivially pass without
+ // the permission.
+ await test_driver.set_permission(
+ {name: 'midi', sysex: true}, 'granted');
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/midi.https.html?sysex=true&uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting navigator.requestMIDIAccess',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting navigator.requestMIDIAccess',
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event${i}`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering${i}`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the Midi API should be deferred until the prerendered page is
+ activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-midi.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-midi.https.html
new file mode 100644
index 0000000000..6338700d7f
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-midi.https.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<title>Access to the Midi API with sysex=false is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ // Grant the permission here to make a more discerning test because
+ // navigator.requestMIDIAccess() waits until the permission is granted, which
+ // is deferred during prerendering so the test would trivially pass without
+ // the permission.
+ await test_driver.set_permission(
+ {name: 'midi', sysex: false}, 'granted');
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/midi.https.html?sysex=false&uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting navigator.requestMIDIAccess',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting navigator.requestMIDIAccess',
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event${i}`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering${i}`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the Midi API should be deferred until the prerendered page is
+ activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-notification.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-notification.https.html
new file mode 100644
index 0000000000..c7428387a4
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-notification.https.html
@@ -0,0 +1,106 @@
+<!DOCTYPE html>
+<!--
+https://wicg.github.io/nav-speculation/prerendering.html#patch-notifications
+TODO(https://crbug.com/1198110): Add the following tests:
+* Test the constructor returns synchronously while the creation of the
+ notification is deferred until activation.
+-->
+<title>Access to the Notification API before and after prerender activation</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ await test_driver.set_permission({
+ name: 'notifications'
+ }, 'granted');
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/notification-on-activation.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ assert_equals(result, 'notification showed');
+}, `it is allowed to access the notification API in the prerenderingchange
+ event`);
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ await test_driver.set_permission({
+ name: 'notifications'
+ }, 'granted');
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/notification-before-activation.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+
+ const expected = [{
+ event: 'Notification permission is default',
+ prerendering: true
+ },
+ {
+ event: 'started waiting notification',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'permission was granted',
+ prerendering: false
+ },
+ {
+ event: 'notification displayed',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting notification',
+ prerendering: false
+ },
+ ];
+
+ length = Math.min(result.length, expected.length);
+ let i = 0;
+ for (i = 0; i < length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+ assert_equals(i, expected.length);
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+},
+`Displaying Notification should be deferred until the prerendered page is
+ activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-presentation-request.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-presentation-request.https.html
new file mode 100644
index 0000000000..5f7742204e
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-presentation-request.https.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>Same-origin prerendering cannot start presentation</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('prerender-channel', uid);
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ // Start prerendering a page that attempts to start presentation.
+ startPrerendering(`resources/presentation-request.html?uid=${uid}`);
+
+ const result = await gotMessage;
+ assert_equals(result, 'request failed');
+}, 'prerendering page cannot start presentation');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-prompt-by-before-unload.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-prompt-by-before-unload.html
new file mode 100644
index 0000000000..16ebe4f39b
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-prompt-by-before-unload.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<title>Prerendering cannot invoke the prompt generated by the
+ beforeunload event</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('prerender-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ // Start prerendering a page that attempts to invoke
+ // the prompt generated by the beforeunload event.
+ // It is activation-gated and expects to fail.
+ startPrerendering(`resources/prompt-by-before-unload.html?uid=${uid}`);
+
+ const result = await gotMessage;
+ assert_equals(result,
+ 'unloaded without the prompt by beforeunload.');
+}, 'Prerendering cannot invoke the prompt by the beforeunload event.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-push.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-push.https.html
new file mode 100644
index 0000000000..ec1ca48da9
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-push.https.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<title>Access to the Push API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ // We grant the permission here to make a more discerning test because
+ // pushManager.subscribe() waits until the permission is granted, which
+ // is deferred during prerendering so the test would trivially pass without
+ // the permission.
+ await test_driver.set_permission({ name: 'push', userVisibleOnly: true },
+ 'granted');
+
+ // Install the service worker first to test pushManager.subscribe in the
+ // prerendering page.
+ const scope = `resources/`;
+ const script = `resources/do-nothing-worker.js`;
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const url = `resources/push.https.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {event: 'started waiting pushManager.subscribe', prerendering: true},
+ {event: 'prerendering change', prerendering: false},
+ {event: 'finished waiting pushManager.subscribe', prerendering: false},
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `The access to the Push API should be deferred until the prerendered page is
+ activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-request-picture-in-picture.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-request-picture-in-picture.html
new file mode 100644
index 0000000000..5bad4fed60
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-request-picture-in-picture.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<title>Prerendering cannot invoke
+ HTMLVideoElement.requestPictureInPicture</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('prerender-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ // Start prerendering a page that attempts to invoke
+ // HTMLVideoElement.requestPictureInPicture. This API needs
+ // transient activation. So it's expected to fail.
+ startPrerendering(`resources/request-picture-in-picture.html?uid=${uid}`);
+
+ const result = await gotMessage;
+ assert_equals(result, 'Metadata for the video element are not loaded yet');
+}, 'prerendering page cannot invoke' +
+ 'HTMLVideoElement.requestPictureInPicture');
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-screen-capture.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-screen-capture.https.html
new file mode 100644
index 0000000000..2cd7fb662f
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-screen-capture.https.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<title>Prerendering cannot invoke the Screen Capture API</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('prerender-channel', uid);
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ // Start prerendering a page that attempts to invoke the Screen Capture API.
+ // This API is activated-gated so it's expected to fail:
+ // https://wicg.github.io/nav-speculation/prerendering.html#implicitly-restricted
+ startPrerendering(`resources/screen-capture.https.html?uid=${uid}`);
+ const result = await gotMessage;
+ assert_equals(result, 'InvalidStateError');
+ bc.close();
+}, `prerendering pages should not be able to invoke the Screen Capture API`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-screen-orientation-lock.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-screen-orientation-lock.https.html
new file mode 100644
index 0000000000..7c2be6acec
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-screen-orientation-lock.https.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<title>Access to the Screen Orientation Lock API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ const url = `resources/screen-orientation-lock.https.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {event: 'started waiting screen.orientation.lock', prerendering: true},
+ {event: 'prerendering change', prerendering: false},
+ {event: 'finished waiting screen.orientation.lock', prerendering: false},
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `The access to the Screen Orienation Lock API should be deferred until the
+ prerendered page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-accelerometer.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-accelerometer.https.html
new file mode 100644
index 0000000000..fdeb7e39be
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-accelerometer.https.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<title>Access to the Accelerometer API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ // We grant the permission here to make a more discerning test because
+ // Accelerometer() waits until the permission is granted, which
+ // is deferred during prerendering so the test would trivially pass without
+ // the permission.
+ await test_driver.set_permission({ name: 'accelerometer' }, 'granted');
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/generic-sensor.https.html?sensorName=Accelerometer&uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting Accelerometer test',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting Accelerometer test',
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the Accelerometer API should be deferred until the prerendered
+ page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-ambient-light-sensor.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-ambient-light-sensor.https.html
new file mode 100644
index 0000000000..63db989bc4
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-ambient-light-sensor.https.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<title>Access to the Ambient Light Sensor API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ // We grant the permission here to make a more discerning test because
+ // AmbientLightSensor() waits until the permission is granted, which
+ // is deferred during prerendering so the test would trivially pass without
+ // the permission.
+ await test_driver.set_permission({ name: 'ambient-light-sensor' }, 'granted');
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/generic-sensor.https.html?sensorName=AmbientLightSensor&uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting AmbientLightSensor test',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting AmbientLightSensor test',
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the Ambient Light Sensor API should be deferred until the
+ prerendered page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-gyroscope.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-gyroscope.https.html
new file mode 100644
index 0000000000..6b8780d7e9
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-gyroscope.https.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<title>Access to the Gyroscope API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ // We grant the permission here to make a more discerning test because
+ // Gyroscope() waits until the permission is granted, which
+ // is deferred during prerendering so the test would trivially pass without
+ // the permission.
+ await test_driver.set_permission({ name: 'gyroscope' }, 'granted');
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/generic-sensor.https.html?sensorName=Gyroscope&uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting Gyroscope test',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting Gyroscope test',
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the Gyroscope API should be deferred until the prerendered
+ page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-magnetometer.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-magnetometer.https.html
new file mode 100644
index 0000000000..c1267bf198
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-sensor-magnetometer.https.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<title>Access to the Magnetometer API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ // We grant the permission here to make a more discerning test because
+ // Magnetometer() waits until the permission is granted, which
+ // is deferred during prerendering so the test would trivially pass without
+ // the permission.
+ await test_driver.set_permission({ name: 'magnetometer' }, 'granted');
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/generic-sensor.https.html?sensorName=Magnetometer&uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting Magnetometer test',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting Magnetometer test',
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the Magnetometer API should be deferred until the prerendered
+ page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-postmessage.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-postmessage.https.html
new file mode 100644
index 0000000000..1f8afb5aa2
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-postmessage.https.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<title>postMessage() between service worker and prerendered page</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+const uid = token();
+
+const PAGE_URL = `resources/postmessage-to-service-worker.html?uid=${uid}`;
+const WORKER_URL = 'resources/postmessage-to-client-worker.js';
+
+// Message sequence: prerendered page => service worker => prerendered page =>
+// main page
+promise_test(async t => {
+ const registration =
+ await service_worker_unregister_and_register(t, WORKER_URL, PAGE_URL);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ window.open(PAGE_URL, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {event: 'started waiting ServiceWorker.postMessage', prerendering: true},
+ {event: 'prerendering change', prerendering: false},
+ {event: 'postmessage to client', prerendering: false},
+ {event: 'finished waiting ServiceWorker.postMessage', prerendering: false},
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, 'ServiceWorker#postMessage() from a prerendered page should be deferred ' +
+ 'until page activation.');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-unregister.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-unregister.https.html
new file mode 100644
index 0000000000..669e7ee0a2
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-unregister.https.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<title>ServiceWorkerRegistration.unregister in a prerendered page</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+const uid = token();
+
+const PAGE_URL = `resources/service-worker-unregister.html?uid=${uid}`;
+const WORKER_URL = 'resources/do-nothing-worker.js';
+
+promise_test(async t => {
+ const registration =
+ await navigator.serviceWorker.register(WORKER_URL, {scope: PAGE_URL});
+ t.add_cleanup(_ => registration.unregister());
+
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ window.open(PAGE_URL, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {event: 'started waiting ServiceWorkerRegistration.unregister', prerendering: true},
+ {event: 'prerendering change', prerendering: false},
+ {event: 'service worker unregistered', prerendering: false},
+ {event: 'finished waiting ServiceWorkerRegistration.unregister', prerendering: false},
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, 'ServiceWorkerRegistration.unregister() should be deferred in a ' +
+ 'prerendered page');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-update.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-update.https.html
new file mode 100644
index 0000000000..92c16e5433
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-service-worker-update.https.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<title>ServiceWorkerRegistration.update in a prerendered page</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+const uid = token();
+
+const PAGE_URL = `resources/service-worker-update.html?uid=${uid}`;
+const WORKER_URL = 'resources/do-nothing-worker.js';
+
+promise_test(async t => {
+ const registration =
+ await navigator.serviceWorker.register(WORKER_URL, {scope: PAGE_URL});
+ t.add_cleanup(_ => registration.unregister());
+
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ window.open(PAGE_URL, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {event: 'started waiting ServiceWorkerRegistration.update', prerendering: true},
+ {event: 'prerendering change', prerendering: false},
+ {event: 'service worker updated', prerendering: false},
+ {event: 'finished waiting ServiceWorkerRegistration.update', prerendering: false},
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, 'ServiceWorkerRegistration.update() should be deferred in a prerendered ' +
+ 'page');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-speech-synthesis.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-speech-synthesis.html
new file mode 100644
index 0000000000..8743d1fd95
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-speech-synthesis.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<title>Access to the speech synthesis is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+function RunTest(method, description) {
+ promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/speech-synthesis.https.html?method=${method}&uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: `started waiting speechSynthesis.${method}`,
+ prerendering: true
+ },
+ {
+ event: `prerendering change`,
+ prerendering: false
+ },
+ {
+ event: `finished waiting speechSynthesis.${method}`,
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event${i}`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering${i}`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+ }, description);
+}
+
+RunTest('speak', `speechSynthesis.speak(utterance) should be deferred until the prerendered page is activated`);
+RunTest('cancel', `speechSynthesis.cancel() should be deferred until the prerendered page is activated`);
+RunTest('pause', `speechSynthesis.pause() should be deferred until the prerendered page is activated`);
+RunTest('resume', `speechSynthesis.resume() should be deferred until the prerendered page is activated`);
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-storage-persist.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-storage-persist.https.html
new file mode 100644
index 0000000000..7d2f04f6d0
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-storage-persist.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<title>Access to storage.persist() is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/storage-persist.https.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting navigator.storage.persist',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting navigator.storage.persist',
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+ bc.close();
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the storage.persist() should be deferred until the prerendered
+ page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-wake-lock.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-wake-lock.https.html
new file mode 100644
index 0000000000..051cc5440f
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-wake-lock.https.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<title>Access to the Wake Lock API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+
+ // We grant the permission here to make a more discerning test because
+ // navigator.wakeLock.request() waits until the permission is granted, which
+ // is deferred during prerendering so the test would trivially pass without
+ // the permission.
+ await test_driver.set_permission({ name: 'screen-wake-lock' }, 'granted');
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/wake-lock.https.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting navigator.wakeLock.request test',
+ prerendering: true
+ },
+ {
+ event: 'navigator.wakeLock.request failed',
+ prerendering: true
+ },
+ {
+ event: 'requesting navigator.wakeLock.request on prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting navigator.wakeLock.request test',
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+ bc.close();
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the Wake Lock API should be deferred until the prerendered
+ page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-web-hid.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-hid.https.html
new file mode 100644
index 0000000000..c96173ac17
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-hid.https.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<title>Access to the Web HID API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/web-hid.https.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {event: 'started waiting navigator.hid.getDevices', prerendering: true},
+ {event: 'prerendering change', prerendering: false},
+ {event: 'finished waiting navigator.hid.getDevices', prerendering: false},
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+ bc.close();
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the Web HID API should be deferred until the prerendered
+ page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-web-locks.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-locks.https.html
new file mode 100644
index 0000000000..02b328d3a3
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-locks.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>Access to the Web Locks API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+function RunTest(method, description) {
+ promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/web-locks.html?method=${method}&uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {event: `started waiting navigator.locks.${method}`, prerendering: true},
+ {event: 'prerendering change', prerendering: false},
+ {event: `finished waiting navigator.locks.${method}`, prerendering: false},
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+ bc.close();
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+ }, description);
+}
+
+RunTest(`request`, `navigator.locks.request should be deferred until the prerendered page is activated`);
+RunTest(`query`, `navigator.locks.query should be deferred until the prerendered page is activated`);
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-web-nfc.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-nfc.https.html
new file mode 100644
index 0000000000..8e0825537e
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-nfc.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<title>Access to the Web NFC API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/web-nfc.https.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {event: 'started waiting NDEFReader.[write|scan]', prerendering: true},
+ {event: 'prerendering change', prerendering: false},
+ {event: 'ndef.write() failed', prerendering: false},
+ {event: 'ndef.scan() failed', prerendering: false},
+ {event: 'finished waiting NDEFReader.[write|scan]', prerendering: false},
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `The access to the Web NFC API should be deferred until the prerendered
+ page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-web-serial.tentative.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-serial.tentative.https.html
new file mode 100644
index 0000000000..1d58bfa547
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-serial.tentative.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<title>Access to the Web Serial API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/web-serial.https.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {event: 'started waiting navigator.serial.getPorts', prerendering: true},
+ {event: 'prerendering change', prerendering: false},
+ {event: 'finished waiting navigator.serial.getPorts', prerendering: false},
+ ];
+ // The spec, https://wicg.github.io/nav-speculation/prerendering.html#patch-serial,
+ // says that Web Serial API has delay while prerendering for
+ // requestPort(). As this test uses getPorts(), it's a tentative test.
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+ bc.close();
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the Web Serial API should be deferred until the prerendered
+ page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-web-share.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-share.https.html
new file mode 100644
index 0000000000..3bc071d5b4
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-share.https.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<title>Prerendering cannot invoke the Web Share API</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('prerender-channel', uid);
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ // Start prerendering a page that attempts to invoke the Web Share API.
+ // This API is activated-gated so it's expected to fail:
+ // https://wicg.github.io/nav-speculation/prerendering.html#activation-gated
+ startPrerendering(`resources/web-share.https.html?uid=${uid}`);
+ const result = await gotMessage;
+ assert_equals(result, 'NotAllowedError');
+ bc.close();
+}, `prerendering pages should not be able to invoke the Web Share API`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-web-usb.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-usb.https.html
new file mode 100644
index 0000000000..9d96a39c9c
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-usb.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<title>Access to the Web USB API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+ const url = `resources/web-usb.https.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {event: 'started waiting navigator.usb.getDevices', prerendering: true},
+ {event: 'prerendering change', prerendering: false},
+ {event: 'finished waiting navigator.usb.getDevices', prerendering: false},
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+ bc.close();
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `the access to the Web USB API should be deferred until the prerendered
+ page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-web-xr-immersive-vr-session.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-xr-immersive-vr-session.https.html
new file mode 100644
index 0000000000..b864718ccd
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-xr-immersive-vr-session.https.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<title>Access to the WebXR immersive-vr session API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ const url = `resources/web-xr-immersive-vr-session.https.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: `started waiting XRSession.requestSession('immersive-vr')`,
+ prerendering: true
+ },
+ {
+ event: `prerendering change`,
+ prerendering: false
+ },
+ {
+ event: `XRSession.requestSession('immersive-vr') rejected: SecurityError`,
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `The access to the WebXR immersive-vr session API should be deferred until
+ the prerendered page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-web-xr-inline-session.https.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-xr-inline-session.https.html
new file mode 100644
index 0000000000..4f308a7543
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-web-xr-inline-session.https.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<title>Access to the WebXR inline session API is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ const url = `resources/web-xr-inline-session.https.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await gotMessage;
+ const expected = [
+ {
+ event: `started waiting XRSession.requestSession('inline')`,
+ prerendering: true
+ },
+ {
+ event: `prerendering change`,
+ prerendering: false
+ },
+ {
+ event: `finished waiting XRSession.requestSession('inline')`,
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `The access to the WebXR inline session API should be deferred until the
+ prerendered page is activated`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-window-move.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-window-move.html
new file mode 100644
index 0000000000..e8011311f6
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-window-move.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+// moveTo and moveBy operations should be ignored.
+// See https://github.com/jeremyroman/alternate-loading-modes/issues/73.
+['moveTo', 'moveBy'].forEach(moveFunc => {
+ promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {once: true});
+ });
+
+ const url = `resources/window-move.html?move=${moveFunc}&uid=${uid}`;
+
+ // We have to open a new window to run the test, since a window that was
+ // not created by window.open() cannot be moved.
+ window.open(url, '_blank',
+ `left=${window.screen.availLeft},
+ top=${window.screen.availTop},
+ width=${window.screen.availWidth / 2},
+ height=${window.screen.availHeight / 2},
+ noopener`);
+
+ const result = await gotMessage;
+ assert_equals(result.status, 'PASS');
+ assert_equals(
+ result.prevPosition.x, result.newPosition.x,
+ 'x position for prerendering');
+ assert_equals(
+ result.prevPosition.y, result.newPosition.y,
+ 'y position for prerendering');
+ }, `a prerendering page cannot move its window by executing ${moveFunc}.`);
+});
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-window-open.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-window-open.html
new file mode 100644
index 0000000000..5de23efc77
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-window-open.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+function runTest(test_file, expectation, description) {
+ promise_test(async t => {
+ const uid = token();
+ // Run test in a new window for test isolation.
+ window.open(`${test_file}?uid=${uid}`, '_blank', 'noopener');
+
+ // Wait until the prerendered page sends the result.
+ const bc = new PrerenderChannel('result', uid);
+ t.add_cleanup(() => {
+ new PrerenderChannel('close', uid).postMessage('close');
+ })
+ const result = await new Promise(r => bc.addEventListener('message', e => r(e.data)));
+ assert_equals(result, expectation);
+ }, description);
+}
+
+// Test that a page opens a window during prerendering.
+runTest(
+ 'resources/window-open-during-prerendering.html',
+ 'failed to open',
+ 'window.open() should fail during prerendering');
+
+// Test that a page opens a window in the prerenderingchange event.
+runTest(
+ 'resources/window-open-in-prerenderingchange.html',
+ 'opened',
+ 'window.open() should succeed in the prerenderingchange event');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restriction-window-resize.html b/testing/web-platform/tests/speculation-rules/prerender/restriction-window-resize.html
new file mode 100644
index 0000000000..20a71b4bdb
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restriction-window-resize.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+// ResizeTo and ResizeBy operations should be ignored.
+// See https://github.com/jeremyroman/alternate-loading-modes/issues/73.
+['resizeTo', 'resizeBy'].forEach(resizeFunc => {
+ promise_test(
+ async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {once: true});
+ });
+
+ const url = `resources/window-resize.html?resize=${resizeFunc}&uid=${uid}`;
+
+ // We have to open a new window to run the test, since a window that was
+ // not created by window.open() cannot be resized.
+ window.open(
+ url, '_blank',
+ `width=${window.screen.availWidth / 2},height=${
+ window.screen.availHeight / 2},noopener`);
+
+ const result = await gotMessage;
+ assert_equals(result.status, 'PASS');
+ assert_equals(
+ result.prevRect.width, result.newRect.width,
+ 'width for prerendering');
+ assert_equals(
+ result.prevRect.height, result.newRect.height,
+ 'height for prerendering');
+ },
+ `a prerendering page cannot resize its window by executing ${
+ resizeFunc}.`);
+});
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/restrictions.html b/testing/web-platform/tests/speculation-rules/prerender/restrictions.html
new file mode 100644
index 0000000000..5019d4b280
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/restrictions.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<title>Same-origin prerendering can access localStorage</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+test_prerender_restricted(
+ () => navigator.clipboard.writeText(location.href),
+ "NotAllowedError", "prerendering pages should not be able to access the clipboard via the Async Clipboard API");
+
+test_prerender_restricted(async () => {
+ const canvas = document.createElement('canvas');
+ document.body.appendChild(canvas);
+ await canvas.requestPointerLock();
+}, "WrongDocumentError", "prerendering pages should not be able to access the pointer-lock API");
+
+test_prerender_restricted(async () => {
+ const div = document.createElement('div');
+ document.body.appendChild(div);
+ await div.requestFullscreen();
+}, "TypeError", "prerendering pages should not be able to access the FullScreen API");
+
+test_prerender_defer(() => new Promise(
+ resolve => navigator.geolocation.getCurrentPosition(p => resolve(p.toString()))),
+ "Geolocation API should be deferred");
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/sandbox-iframe.html b/testing/web-platform/tests/speculation-rules/prerender/sandbox-iframe.html
new file mode 100644
index 0000000000..0f0a259a06
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/sandbox-iframe.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<!--
+Tests for cross-origin iframes due to sandbox flags is deferred properly.
+
+This file cannot be upstreamed to WPT until:
+* The specification describes the loading of cross-origin iframes. The test
+ expects that they are not loaded during prerendering.
+-->
+<title>Load a prerendered iframe with sandbox attributes</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ const messageQueue = new BroadcastMessageQueue(bc);
+ t.add_cleanup(_ => bc.close());
+
+ const url = `resources/sandbox-iframe.html?uid=${uid}`;
+ window.open(url, '_blank', 'noopener');
+
+ const result = await messageQueue.nextMessage();
+ const expected = [
+ {
+ event: 'started waiting iframe loaded',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting iframe loaded',
+ prerendering: false
+ },
+ ];
+ assert_equals(result.length, expected.length);
+ for (let i = 0; i < result.length; i++) {
+ assert_equals(result[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(result[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, `same-origin sandbox iframes should not load until activation`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/script-supports-speculationrules.html b/testing/web-platform/tests/speculation-rules/prerender/script-supports-speculationrules.html
new file mode 100644
index 0000000000..2dc856fce5
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/script-supports-speculationrules.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<title>HTMLScriptElement.supports speculationrules</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(function() {
+ assert_true(HTMLScriptElement.supports('speculationrules'));
+}, 'HTMLScriptElement.supports returns true for \'speculationrules\'');
+
+test(function() {
+ assert_false(HTMLScriptElement.supports(' speculationrules'));
+ assert_false(HTMLScriptElement.supports('speculationrules '));
+ assert_false(HTMLScriptElement.supports('Speculationrules'));
+ assert_false(HTMLScriptElement.supports('SpeculationRules'));
+ assert_false(HTMLScriptElement.supports('speculationRules'));
+ assert_false(HTMLScriptElement.supports('speculation-rules'));
+}, 'HTMLScriptElement.supports returns false for unsupported types');
+
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/service-workers.https.html b/testing/web-platform/tests/speculation-rules/prerender/service-workers.https.html
new file mode 100644
index 0000000000..6a45276057
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/service-workers.https.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<title>Same-origin prerendering can access Indexed Database</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+// This delay is to prevent a race condition which can cause false passes -
+// a service worker might take some time to install, and if activation is too quick it might
+// end up occuring after activation by chance.
+const ACTIVATION_DELAY = 500;
+
+promise_test(async t => {
+ const reg = await service_worker_unregister_and_register(
+ t, "./resources/service-worker.js", `resources/`);
+
+ t.add_cleanup(() => reg.unregister());
+ const {exec} = await create_prerendered_page(t);
+ const {text, prerendering} = await exec(async () => {
+ const text = await (await fetch(`ping`)).text();
+ return {text, prerendering: document.prerendering};
+ });
+
+ assert_true(prerendering);
+ assert_equals(text, 'pong');
+}, 'A prerendered page should be able to access an existing Service Worker');
+
+promise_test(async t => {
+ const {exec, activate} = await create_prerendered_page(t);
+ const scope = `./${token()}/`;
+ await exec(async scope => {
+ window.serviceWorkerInstalled = new Promise(resolve => {
+ navigator.serviceWorker.register('./service-worker.js', {scope})
+ .then(reg => {
+ reg.unregister();
+ resolve({prerendering: document.prerendering});
+ });
+ });
+ }, scope);
+
+ await new Promise(resolve => t.step_timeout(resolve, ACTIVATION_DELAY));
+
+ await activate();
+
+ const {prerendering} = await exec(async () => { return await window.serviceWorkerInstalled});
+ assert_false(prerendering, 'Service Worker Installation should occur after activation');
+}, 'Registering a new service worker from a prerendered page should be delayed');
+
+promise_test(async t => {
+ const uid = token();
+ const reg = await service_worker_unregister_and_register(
+ t, "./resources/service-worker.js", `./resources/${uid}/`);
+ t.add_cleanup(() => reg.unregister());
+
+ const {exec, activate} = await create_prerendered_page(t);
+ await exec(async uid => {
+ window.serviceWorkerUnregistered = (async () => {
+ const regs = await navigator.serviceWorker.getRegistrations();
+ const reg = regs.find(r => r.scope.includes(uid));
+ await reg.unregister();
+ return {prerendering: document.prerendering};
+ })();
+ }, uid);
+
+ await new Promise(resolve => t.step_timeout(resolve, ACTIVATION_DELAY));
+
+ await activate();
+
+ const {prerendering} = await exec(() => window.serviceWorkerUnregistered);
+ assert_false(prerendering, 'Service Worker deregistration should occur after activation');
+}, 'Unregistering an exsiting service worker from a prerendered page should be delayed');
+
+promise_test(async t => {
+ const uid = token();
+ const reg = await service_worker_unregister_and_register(
+ t, "./resources/service-worker.js", `./resources/${uid}/`);
+ t.add_cleanup(() => reg.unregister());
+
+ const {exec, activate} = await create_prerendered_page(t);
+ await exec(async uid => {
+ window.serviceWorkerUpdated = (async () => {
+ const regs = await navigator.serviceWorker.getRegistrations();
+ const reg = regs.find(r => r.scope.includes(uid));
+ await reg.update();
+ return {prerendering: document.prerendering};
+ })();
+ }, uid);
+
+ await new Promise(resolve => t.step_timeout(resolve, ACTIVATION_DELAY));
+
+ await activate();
+
+ const {prerendering} = await exec(() => window.serviceWorkerUpdated);
+ assert_false(prerendering, 'Service Worker updates should occur after activation');
+}, 'Updating an exsiting service worker from a prerendered page should be delayed');
+
+promise_test(async t => {
+ const reg = await service_worker_unregister_and_register(
+ t, "./resources/service-worker.js", 'resources/');
+
+ t.add_cleanup(() => reg.unregister());
+ const {exec} = await create_prerendered_page(t);
+ const {clientInfo} = await exec(async () => (await fetch(`client`)).json());
+ assert_not_equals(clientInfo.id, null);
+ assert_equals(clientInfo.visibilityState, 'hidden');
+ assert_equals(clientInfo.focused, false);
+}, 'A prerendered page should be accessible as a hidden & unfocused SW client');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/session-history-activation.https.html b/testing/web-platform/tests/speculation-rules/prerender/session-history-activation.https.html
new file mode 100644
index 0000000000..574c2783bc
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/session-history-activation.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<title>Test history.length</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<script src="resources/session-history-test-util.js"></script>
+<body>
+ <script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ const uid = token();
+
+ promise_test(async () => {
+ assert_equals(
+ await runTestInPrerender("testHistoryLengthInPrerender", uid),
+ "Passed",
+ "test in prerender"
+ );
+ assert_equals(
+ await runTestInActivatedPage("testHistoryLengthInPrerender", uid),
+ "Passed",
+ "test in activated page"
+ );
+ }, "history.length should be updated after activation");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/session-history-location.https.html b/testing/web-platform/tests/speculation-rules/prerender/session-history-location.https.html
new file mode 100644
index 0000000000..c68a515ec1
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/session-history-location.https.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<title>Test history.length</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<script src="resources/session-history-test-util.js"></script>
+<body>
+ <script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ const uid = token();
+
+ promise_test(async () => {
+ assert_equals(
+ await runTestInPrerender("testLocationAssignInPrerender", uid),
+ "Passed"
+ );
+ }, "location.assign navigates independently with replacement in a prerender");
+
+ promise_test(async () => {
+ assert_equals(
+ await runTestInPrerender("testLocationReplaceInPrerender", uid),
+ "Passed"
+ );
+ }, "location.replace navigates independently in a prerender");
+
+ promise_test(async () => {
+ assert_equals(
+ await runTestInPrerender("testSetLocationHrefInPrerender", uid),
+ "Passed"
+ );
+ }, "Setting location.href navigates independently with replacement in a prerender");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/session-history-navigation.https.html b/testing/web-platform/tests/speculation-rules/prerender/session-history-navigation.https.html
new file mode 100644
index 0000000000..2f4fab7d7b
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/session-history-navigation.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<title>Test history.length</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<script src="resources/session-history-test-util.js"></script>
+<body>
+ <script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ const uid = token();
+
+ // We test only a fragment-navigation because other kinds of the main frame navigation
+ // in prerender cancels prerendering.
+ promise_test(async () => {
+ assert_equals(
+ await runTestInPrerender("testSyntheticAnchorClickInPrerender", uid),
+ "Passed"
+ );
+ }, "Synthetic anchor click navigates independently with replacement in a prerender");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/session-history-pushstate.https.html b/testing/web-platform/tests/speculation-rules/prerender/session-history-pushstate.https.html
new file mode 100644
index 0000000000..9e9b50a77a
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/session-history-pushstate.https.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<title>Test history.length</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<script src="resources/session-history-test-util.js"></script>
+<body>
+ <script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ const uid = token();
+
+ promise_test(async () => {
+ assert_equals(
+ await runTestInPrerender("testHistoryPushStateInPrerender", uid),
+ "Passed"
+ );
+ }, "history.pushState navigates independently with replacement in a prerender");
+
+ promise_test(async () => {
+ assert_equals(
+ await runTestInPrerender("testHistoryReplaceStateInPrerender", uid),
+ "Passed"
+ );
+ }, "history.replaceState navigates independently in a prerender");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/session-history-subframe-navigation.https.html b/testing/web-platform/tests/speculation-rules/prerender/session-history-subframe-navigation.https.html
new file mode 100644
index 0000000000..f5c2558c3d
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/session-history-subframe-navigation.https.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<title>Test history.length during a subframe navigation</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<script src="resources/session-history-test-util.js"></script>
+<body>
+ <script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ const uid = token();
+
+ promise_test(async () => {
+ assert_equals(
+ await runTestInPrerender("testSubframeNavigationInPrerender", uid),
+ "Passed"
+ );
+ }, "Subframe navigation in prerender replaces the session entry");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/session-history-subframe-reload.https.html b/testing/web-platform/tests/speculation-rules/prerender/session-history-subframe-reload.https.html
new file mode 100644
index 0000000000..1908cd5fe2
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/session-history-subframe-reload.https.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<title>Test history.length and reloading a subframe</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<script src="resources/session-history-test-util.js"></script>
+<body>
+ <script>
+ setup(() => assertSpeculationRulesIsSupported());
+
+ const uid = token();
+
+ promise_test(async () => {
+ assert_equals(
+ await runTestInPrerender("testSubframeReloadInPrerender", uid),
+ "Passed"
+ );
+ }, "Subframe reload works in prerendered page");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/state-and-event.html b/testing/web-platform/tests/speculation-rules/prerender/state-and-event.html
new file mode 100644
index 0000000000..b8252121b1
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/state-and-event.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ // The key used for storing a test result in the server.
+ const key = token();
+
+ // Open the test runner in a popup - it will prerender itself, record the
+ // test results, and send them back to this harness.
+ const url = `resources/prerender-state.html?key=${key}`;
+ window.open(url, '_blank', 'noopener');
+
+ // Wait until the test sends us the results.
+ let result = await nextValueFromServer(key);
+ result = JSON.parse(result);
+
+ assert_equals(result.prerenderingTypeOf, "boolean",
+ "typeof(document.prerendering) is 'boolean'.");
+ assert_equals(result.onprerenderingChangeTypeOf, "object",
+ "typeof(document.onprerenderingchange) is 'object'.");
+
+ assert_equals(
+ result.onprerenderingchangeCalledBeforeActivate, false,
+ "prerenderingchange event should not be called prior to activation.");
+ assert_equals(
+ result.prerenderingValueBeforeActivate, true,
+ "document.prerendering should be true prior to activation.");
+
+ assert_equals(result.onprerenderingchangeCalledAfterActivate, true,
+ "prerenderingchange event should be called after activation.");
+ assert_equals(result.prerenderingValueAfterActivate, false,
+ "document.prerendering should be false after activation.");
+ assert_equals(result.eventBubbles, false,
+ "prerenderingchange event.bubbles should be false.");
+ assert_equals(result.eventCancelable, false,
+ "prerenderingchange event.cancelable should be false.");
+}, 'Test document.prerendering and its change event.');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/visibility-state.html b/testing/web-platform/tests/speculation-rules/prerender/visibility-state.html
new file mode 100644
index 0000000000..023ee51293
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/visibility-state.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>visibilityState must be updated after prerendering</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js"></script>
+<script src="resources/utils.js"></script>
+
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+ const referrerRC = await rcHelper.addWindow(undefined, { features: 'noopener' });
+ const prerenderedRC = await addPrerenderRC(referrerRC);
+
+ assert_equals(await prerenderedRC.executeScript(() => document.visibilityState), "hidden");
+
+ await activatePrerenderRC(referrerRC, prerenderedRC);
+
+ assert_equals(await prerenderedRC.executeScript(() => document.visibilityState), "visible");
+});
+</script>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/web-database.https.html b/testing/web-platform/tests/speculation-rules/prerender/web-database.https.html
new file mode 100644
index 0000000000..3ef1141e20
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/web-database.https.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<title>Same-origin prerendering can access Web Database</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+async function insertQuery() {
+ return new Promise(resolve => {
+ 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) {
+ resolve(error);
+ }, function() {
+ resolve("Success");
+ });
+ });
+}
+
+async function selectQuery() {
+ return new Promise(resolve => {
+ const db = openDatabase("test", "1.0", "test database", 1024);
+ db.transaction(function (tx) {
+ tx.executeSql('CREATE TABLE IF NOT EXISTS foo (text)');
+ tx.executeSql('SELECT * FROM foo', [], function (tx, results) {
+ resolve(results.rows.length);
+ });
+ }, function(tx, error) {
+ resolve(error);
+ });
+ });
+}
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('prerender-channel', uid);
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ const insertResult = await insertQuery();
+ assert_equals(insertResult, "Success",
+ 'primary page should be able to execute statements from Web Database.');
+
+ // Start prerendering a page that attempts to access Web Database.
+ startPrerendering(`resources/web-database-access.html?uid=${uid}`);
+ const result = await gotMessage;
+
+ assert_equals(
+ result, "Success",
+ 'prerendering page should be able to read from Web Database');
+
+ const selectResult = await selectQuery();
+ assert_equals(
+ selectResult, 2,
+ 'prerendering page should be able to write to Web Database');
+}, 'prerendering page should be able to access Web Database');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate-to-cross-origin-url-on-iframe.https.html b/testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate-to-cross-origin-url-on-iframe.https.html
new file mode 100644
index 0000000000..a1fb63f77b
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate-to-cross-origin-url-on-iframe.https.html
@@ -0,0 +1,82 @@
+<!--
+ This file cannot be upstreamed to WPT until:
+ * Cross-origin iframe loading is specified. The test expects that cross-origin
+ iframe loading is deferred.
+-->
+<!DOCTYPE html>
+<title>WindowClient.navigate() to cross-origin url in a prerendered iframe</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+const PAGE_URL = 'resources/windowclient-navigate-on-iframe.html';
+const WORKER_URL = 'resources/windowclient-navigate-worker.js';
+const SCOPE = 'resources/';
+const CROSS_ORIGIN_DESTINATION =
+ get_host_info()['HTTPS_REMOTE_ORIGIN'] +
+ base_path() + 'resources/empty.html';
+
+promise_test(async t => {
+ const uid = token();
+
+ const registration = await service_worker_unregister_and_register(
+ t, `${WORKER_URL}?uid=${uid}`, SCOPE);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ // PAGE_URL starts a prerender of a page that makes an iframe, then asks the
+ // service worker to navigate the iframe to `navigationUrl` via
+ // `WindowClient.navigate(url)`. The cross-origin url triggers
+ // PrerenderEventCollector to wait to navigate to `navigationUrl` on an iframe
+ // until the prerendered iframe is activated.
+ window.open(
+ `${PAGE_URL}?navigationUrl=${CROSS_ORIGIN_DESTINATION}&uid=${uid}`,
+ '_blank',
+ 'noopener');
+
+ const navigationResult = await gotMessage;
+ const expected = [
+ {
+ event: 'started waiting navigation on iframe',
+ prerendering: true
+ },
+ {
+ event: 'prerendering change',
+ prerendering: false
+ },
+ {
+ event: 'finished waiting navigation on iframe',
+ prerendering: false
+ },
+ ];
+ assert_equals(navigationResult.length, expected.length);
+ for (let i = 0; i < navigationResult.length; i++) {
+ assert_equals(navigationResult[i].event, expected[i].event, `event[${i}]`);
+ assert_equals(navigationResult[i].prerendering, expected[i].prerendering,
+ `prerendering[${i}]`);
+ }
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, 'WindowClient.navigate() to a cross-origin URL on a prerendered iframe ' +
+ 'should be deferred');
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate-to-same-origin-url-on-iframe.https.html b/testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate-to-same-origin-url-on-iframe.https.html
new file mode 100644
index 0000000000..16989577df
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate-to-same-origin-url-on-iframe.https.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<title>WindowClient.navigate() to same-origin url in a prerendered iframe</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+const PAGE_URL = 'resources/windowclient-navigate-on-iframe.html';
+const WORKER_URL = 'resources/windowclient-navigate-worker.js';
+const SCOPE = 'resources/';
+const SAME_ORIGIN_DESTINATION =
+ get_host_info()['HTTPS_ORIGIN'] + base_path() + 'resources/empty.html';
+
+promise_test(async t => {
+ const uid = token();
+
+ const registration = await service_worker_unregister_and_register(
+ t, `${WORKER_URL}?uid=${uid}`, SCOPE);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const bc = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => bc.close());
+
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ // PAGE_URL starts a prerender of a page that makes an iframe, then asks the
+ // service worker to navigate the iframe to `navigationUrl` via
+ // `WindowClient.navigate(url)`.
+ window.open(
+ `${PAGE_URL}?navigationUrl=${SAME_ORIGIN_DESTINATION}&uid=${uid}`,
+ '_blank',
+ 'noopener');
+
+ const navigationResult = await gotMessage;
+ assert_equals(navigationResult, 'navigate() succeeded',
+ 'should succeed to finish navigation test');
+
+ // Send a close signal to PrerenderEventCollector on the prerendered page.
+ new PrerenderChannel('close', uid).postMessage('');
+}, 'WindowClient.navigate() to a same-origin URL on a prerendered iframe ' +
+ 'should succeed');
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate.https.html b/testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate.https.html
new file mode 100644
index 0000000000..3b335967ef
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/windowclient-navigate.https.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<title>WindowClient.navigate() for prerendered main page</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<body>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+const uid = token();
+
+const PAGE_URL = `resources/prerendered-page.html?uid=${uid}`;
+const WORKER_URL = `resources/windowclient-navigate-worker.js?uid=${uid}`;
+const SAME_ORIGIN_DESTINATION =
+ get_host_info()['HTTPS_ORIGIN'] + base_path() + 'resources/empty.html';
+
+promise_test(async t => {
+ const registration =
+ await service_worker_unregister_and_register(t, WORKER_URL, PAGE_URL);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, get_newest_worker(registration), 'activated');
+
+ const readyChannel = new PrerenderChannel('prerender-channel', uid);
+ t.add_cleanup(_ => readyChannel.close());
+
+ const readyPromise = new Promise(resolve => {
+ readyChannel.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ startPrerendering(PAGE_URL);
+
+ const prerenderResult = await readyPromise;
+ assert_equals(prerenderResult, 'prerender success',
+ 'should succeed to prerender a page');
+
+ const resultChannel = new PrerenderChannel('test-channel', uid);
+ t.add_cleanup(_ => resultChannel.close());
+
+ const navigationPromise = new Promise(resolve => {
+ resultChannel.addEventListener('message', e => {
+ resolve(e.data);
+ }, {
+ once: true
+ });
+ });
+
+ // Asks the service worker to navigate the prerendered page to `navigationUrl`
+ // via `WindowClient.navigate(url)`.
+ const bc = new PrerenderChannel('navigation-channel', uid);
+ bc.postMessage(JSON.stringify({
+ navigationUrl: SAME_ORIGIN_DESTINATION,
+ clientUrl: new URL(PAGE_URL, window.location).toString(),
+ respondTo: 'test-channel',
+ }));
+ bc.close();
+
+ const navigationResult = await navigationPromise;
+ assert_equals(navigationResult, 'navigate() failed with TypeError',
+ 'should fail the navigation with TypeError');
+}, 'WindowClient.navigate() for a prerendered main page should throw a' +
+ 'TypeError');
+</script>
+</body>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/workers-in-cross-origin-iframe.html b/testing/web-platform/tests/speculation-rules/prerender/workers-in-cross-origin-iframe.html
new file mode 100644
index 0000000000..8d79f43a4e
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/workers-in-cross-origin-iframe.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<title>Construction of Web Workers in cross-origin iframe is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<!-- This is a regression test for https://crbug.com/1424250 -->
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const uid = token();
+ const bc = new PrerenderChannel('test-channel', uid);
+ const gotMessage = new Promise(resolve => {
+ bc.addEventListener('message', e => {
+ resolve(e.data);
+ }, {once: true});
+ });
+
+ // This cross-origin iframe starts a dedicated worker and sends a message to
+ // this document once loading the worker is completed.
+ const crossOriginUrl =
+ new URL(`resources/workers-in-cross-origin-iframe.html?uid=${uid}`,
+ get_host_info()['HTTPS_REMOTE_ORIGIN'] +
+ window.location.pathname);
+
+ // Start prerendering. Loading the cross-origin iframe in a prerendered page
+ // will be deferred until prerender activation.
+ const {exec, activate} = await create_prerendered_page(t);
+ await exec(crossOriginUrl => {
+ const iframe = document.createElement('iframe');
+ iframe.src = crossOriginUrl;
+ document.body.appendChild(iframe);
+ }, [crossOriginUrl.href]);
+
+ // Activate. This resumes loading the cross-origin iframe.
+ await activate();
+
+ // Wait for the completion of the worker creation.
+ assert_equals(await gotMessage, 'Success');
+}, "Dedicated workers in cross-origin iframe should be loaded after " +
+ "activation");
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/speculation-rules/prerender/workers.html b/testing/web-platform/tests/speculation-rules/prerender/workers.html
new file mode 100644
index 0000000000..daaedf481c
--- /dev/null
+++ b/testing/web-platform/tests/speculation-rules/prerender/workers.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<title>Construction of Web Workers is deferred</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+<script>
+
+setup(() => assertSpeculationRulesIsSupported());
+
+promise_test(async t => {
+ const {exec, activate} = await create_prerendered_page(t);
+ const workerURL = new URL('resources/worker-post-timeOrigin.js', location.href).toString();
+ await exec(workerURL => {
+ window.worker = new Worker(workerURL);
+ window.waitForWorker = new Promise(resolve => worker.addEventListener('message', e => {
+ resolve({
+ prerendering: document.prerendering,
+ activationStart: performance.getEntriesByType('navigation')[0].activationStart,
+ workerLoad: performance.getEntriesByName(workerURL)[0].startTime,
+ workerStart: e.data});
+ }), workerURL);
+ }, [workerURL]);
+
+ // We want to prevent false success by waiting for enough time to make sure the worker is not
+ // yet initialized
+ await new Promise(resolve => t.step_timeout(resolve, 500));
+ await activate();
+ const {workerStart, activationStart, workerLoad, prerendering} = await exec(() => window.waitForWorker);
+ assert_false(prerendering, "DedicatedWorker should be suspended until activated");
+ assert_greater_than(activationStart, workerLoad, "Loading the worker script should not be delayed");
+ assert_greater_than(workerStart, activationStart, "Starting the worker should be delayed");
+}, "Dedicated workers should be loaded in suspended state until activated");
+
+promise_test(async t => {
+ const {exec, activate} = await create_prerendered_page(t);
+ const workerURL = new URL(`resources/shared-worker.py?id=${token()}`, location.href).toString();
+ await exec(workerURL => {
+ window.worker = new SharedWorker(workerURL, 'dummy');
+ window.worker.port.start();
+ window.waitForSharedWorkerLoadingReport =
+ fetch(workerURL + "&check=true")
+ .then(r => t.text())
+ .then(text => text === 'ok' && document.prerendering);
+ window.waitForWorker = new Promise(resolve => worker.port.addEventListener('message', e => {
+ resolve({
+ prerendering: document.prerendering,
+ activationStart: performance.getEntriesByType('navigation')[0].activationStart,
+ workerLoad: performance.getEntriesByName(workerURL)[0]?.startTime,
+ workerStart: e.data});
+ }), workerURL);
+ }, [workerURL]);
+
+ await new Promise(resolve => t.step_timeout(resolve, 300));
+ await activate();
+ const {workerStart, activationStart, workerLoad, prerendering} = await exec(() => window.waitForWorker);
+ assert_false(prerendering, "SharedWorker should be suspended until activated");
+ assert_greater_than(activationStart, workerLoad, "Loading the worker script should not be delayed");
+ assert_greater_than(workerStart, activationStart, "Starting the worker should be delayed");
+}, "Shared workers should be loaded in suspended state until activated");
+
+promise_test(async t => {
+ const {exec, activate} = await create_prerendered_page(t);
+ const workerURL = new URL(`resources/shared-worker.py?id=${token()}`, location.href).toString();
+ const workerStartTime1 = await new Promise(resolve => {
+ const worker = new SharedWorker(workerURL, 'worker');
+ worker.port.start();
+ worker.port.addEventListener('message', e => resolve(e.data));
+ });
+
+ await exec(workerURL => {
+ window.worker = new SharedWorker(workerURL, 'worker');
+ window.worker.port.start();
+ window.waitForWorker = new Promise(resolve => worker.port.addEventListener('message', e => {
+ resolve({
+ prerendering: document.prerendering,
+ activationStart: performance.getEntriesByType('navigation')[0].activationStart,
+ workerLoad: performance.getEntriesByName(workerURL)[0]?.startTime,
+ workerStartTime2: e.data});
+ }), workerURL);
+ }, [workerURL]);
+
+ await new Promise(resolve => t.step_timeout(resolve, 300));
+ await activate();
+ const {workerStartTime2, activationStart, workerLoad, prerendering} = await exec(() => window.waitForWorker);
+ assert_true(prerendering, "An existing SharedWorker should be accessible while prerendering");
+ assert_greater_than(activationStart, workerLoad, "Loading the worker script should not be delayed");
+ assert_equals(workerStartTime1, workerStartTime2, "The prerendered page should connect to the existing worker");
+ assert_greater_than(activationStart, workerStartTime2, "Starting the worker should be done before activation");
+}, "Existing shared workers should be accessible before activation");
+
+</script>
+</body>