summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/html/browsers
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/html/browsers
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/html/browsers')
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/README.md60
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/broadcast-channel.html23
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/dedicated-worker.html25
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-1.html18
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-2.html22
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-cors.html18
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-redirects.html34
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/shared-worker.html25
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/events.html44
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/focus.html73
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/pushstate.https.html63
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/echo-worker.js16
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/event-recorder.js54
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor-pushstate.html13
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor.html5
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor.js64
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js229
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/inflight-fetch-helper.js47
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js137
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/service-worker.js58
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/slow.py13
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/worker-helper.js28
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-client-postmessage.https.html71
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-claim.https.html71
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-matchall.https.html76
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-controlled-after-restore.https.html54
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-unregister.https.html67
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/storage-events.html101
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/timers.html68
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/001-1.html11
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/001-2.html5
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/001.html35
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/PopStateEvent.html47
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/api-availability.html22
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-0.html35
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-1.html6
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-2.html4
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-3.html6
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-4.html6
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name.html13
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name_cross_origin-0.html34
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name_cross_origin.html13
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name_cross_origin_2.html43
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name_cross_origin_3.html44
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/document-state.https.html133
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/after-load-hash-twice.html38
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/after-load-hash.html32
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/after-load-pushState.html31
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/after-load-replaceState.html30
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/before-load-hash-twice.html29
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/before-load-hash.html27
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/before-load-pushState.html28
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/before-load-replaceState.html26
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/pushState-inside-popstate.html16
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/same-document-traverse-immediate.html38
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/same-document-traverse-wait.html39
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/events.html151
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/hashchange_event.html49
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/history-traversal-navigate-parent-while-child-loading.html30
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/history-traversal-navigates-multiple-frames.html29
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/pagereveal/order-in-bfcache-restore.html70
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/pagereveal/order-in-new-document-navigation.html18
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/pagereveal/order-in-prerender-activation.html40
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/pagereveal/resources/order-in-prerender-activation-popup.html74
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resources/blank1.html8
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resources/blank2.html8
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resources/page-with-fragment.html20
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resources/post_name_on_load.html7
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resume-timer-on-history-back.html146
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-basic.html34
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-fragment-scrolling-cross-origin.html71
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-fragment-scrolling-samedoc.html55
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-navigation-cross-origin.html71
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-navigation-samedoc.html81
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/popstate_event.html48
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/a.html1
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/api-availability-1.html31
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/api-availability-2.html3
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/b.html1
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/c.html1
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/unset_context_name-1.sub.html45
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/unset_context_name_stash.py13
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/same-url.html50
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/scroll-restoration-order.html74
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/srcdoc/consecutive-srcdoc.html85
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/srcdoc/srcdoc-history-entries.html95
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/support/window-name-after-cross-origin-main-frame-navigation-popup.sub.html10
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/support/window-name-after-same-origin-main-frame-navigation-1.sub.html4
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/support/window-name-navigation.sub.html11
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/support/window-name-test.sub.html23
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/unset_context_name.html32
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-cross-origin-aux-frame-navigation.sub.html17
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-cross-origin-main-frame-navigation.sub.html24
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-cross-origin-sub-frame-navigation.sub.html18
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-same-origin-aux-frame-navigation.sub.html17
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-same-origin-main-frame-navigation.html12
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-same-origin-sub-frame-navigation.sub.html18
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/003-1.html7
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/003-2.html9
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/003-3.html4
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/003.html22
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/004-1.html7
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/004-2.html9
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/004-3.html4
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/004.html22
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/005.html16
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/006.html17
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/007.html16
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/008.html17
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/009.html22
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/010.html17
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/011.html21
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/012.html20
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/013.html20
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/014.html21
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/015.html20
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/abort-document-load-1.html32
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/abort-document-load-2.html32
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/abort-document-load.html30
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/about-srcdoc-navigation-blocked.window.js52
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/anchor-fragment-form-submit-longfragment.html29
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/anchor-fragment-form-submit-withpath.html35
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/anchor-fragment-form-submit.html29
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/anchor-jsurl-form-submit.html29
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/child-navigates-parent-cross-origin.window.js90
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/child-navigates-parent-same-origin.window.js24
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/cross-origin-top-navigation-with-user-activation-in-parent.window.js8
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/cross-origin-top-navigation-without-user-activation.window.js8
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/empty-iframe-load-event.html39
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/empty_fragment.html20
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/empty_fragment_iframe.html11
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/failure-check-sequence.https.html76
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-nosrc.html178
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-204-fragment.html144
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-204-pushState-replaceState.html66
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-204.html178
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-aboutblank-navigate-immediately.html124
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-aboutblank-wait-for-load.html134
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/initial-content-replacement.html86
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-event-iframe-element.html64
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-iframe-contentWindow.html44
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-window-open.html35
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/resources/code-injector.html8
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/resources/helpers.js121
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-204-fragment.html63
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-204-pushState-replaceState.html61
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-204.html81
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-aboutblank.html81
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-history-length.html31
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-nourl.html78
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-abort/javascript-url-abort-return-value-string.tentative.html28
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-abort/javascript-url-abort-return-value-undefined.tentative.html25
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-abort/support/iframe-and-links.html18
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-abort/support/set-child-loaded.html5
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-global-scope.html16
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-load-as-html.xhtml37
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-no-beforeunload.window.js80
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-query-fragment-components.html28
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-referrer.window.js38
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-return-value-handling-dynamic.html58
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-return-value-handling.html36
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-security-check-failure.sub.html56
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-security-check-multi-globals.sub.html66
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-security-check-same-origin-domain.sub.html26
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-task-queuing.html58
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/context-for-location-assign.html27
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/context-for-location-href.html27
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/context-for-location.html27
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/context-for-window-open.html28
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/entry/entry.html4
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/entry/target.html2
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/incumbent/empty.html0
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/incumbent/target.html2
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/relevant/empty.html0
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/relevant/target.html2
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/resources/context-helper.js34
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/resources/target.js11
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigate-cross-origin-iframe-to-same-url-with-fragment-fire-load-event.html31
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigate-cross-origin-iframe-to-same-url-with-fragment.html27
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigate-cross-origin-iframe-to-same-url.html22
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigate-to-unparseable-url.html58
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-cross-origin.sub.window.js15
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-data-url.window.js15
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-form-submit-1.html23
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-form-submit-2.html7
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-form-submit.html24
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-javascript-url.window.js15
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin-fragment-1.html9
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin-fragment-2.html9
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin-fragment.html29
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin.window.js15
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/plugin-document.historical.html22
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/redirect-to-about.window.js34
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/redirect-to-data.html75
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/redirect-to-unparseable-url.html71
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/README.md1
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/navigate.window.js23
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/multiple.asis6
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/refresh.py4
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/refreshed.txt1
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/subresource.any.js6
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click-during-load.html32
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click-during-pageshow.html32
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click.html30
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-user-click-during-load.html36
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-user-click-during-pageshow.html36
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-user-click.html34
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-requestsubmit-during-load.html41
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-requestsubmit-during-pageshow.html41
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-requestsubmit.html39
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-button-click-during-load.html45
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-button-click-during-pageshow.html45
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-button-click.html43
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-cross-frame-crossorigin.sub.html42
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-cross-frame.html40
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-during-load.html41
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-during-pageshow.html41
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-popup-crossorigin.sub.html39
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-popup.html37
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit.html39
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/history-pushstate-during-load.html28
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/history-pushstate-during-pageshow.html29
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/history-pushstate.html22
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/iframe-src-during-load.html31
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/iframe-src-during-pageshow.html31
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/iframe-src.html39
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-assign-during-load.html31
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-assign-during-pageshow.html31
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-assign-user-click.html34
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-assign.html25
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter-during-load.html76
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter-during-pageshow.html77
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter-user-click.html82
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter-user-mouseup.html89
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter.html58
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/code-injector.html9
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/helpers.js89
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/message-opener.html8
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/slow-code-injector.html16
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/slow-message-source-with-history-and-location.html16
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-popup-during-load.html67
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-popup-during-pageshow.html67
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-popup.html49
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-self-during-load.html29
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-self-during-pageshow.html29
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-self.html23
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/blank.html1
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-cross-origin-inner.html20
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-destination.html4
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-location-initial.html3
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-location-inner.html5
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-submit-initial.html3
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-submit-inner.html6
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/click.html4
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/document-domain-set-to-site.sub.html7
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/form.html5
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/href.html5
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/message-opener.html8
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/message-parent.html3
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/multi-globals-subframe-1.sub.html10
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/multi-globals-subframe-2.sub.html8
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/no-cache-single-redirect.py21
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/page-that-post-message-to-opener.html6
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/page-with-top-navigating-iframe.html24
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/portable-document-format-sample-valid.pdfbin0 -> 58927 bytes
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/portable-document-format-sample-valid.pdf.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/redirect.py4
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/top-navigating-page.html15
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/wait-for-messages.js15
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/xhtml-and-non-utf-8.xhtml9
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-function-parent-then-fragment.html36
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-function-parent.html18
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-function-src.html18
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-function.html19
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-src-about-blank.html20
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/support/dummy.html3
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/support/location-set.html8
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/support/set-parent-src.html8
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/top-level-data-url.window.js20
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/README.md11
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/anchor-fragment-history-back-on-click.html40
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-nav.html30
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-traversal.html38
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-nav.html44
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-traversal.html37
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-stop.html21
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-nav.html77
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-traversal.html160
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-nav.html66
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-traversal.html94
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-stop.html42
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/forward-to-pruned-entry.html24
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/nav-cancelation-1.html62
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/nav-cancelation-2.sub.html178
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/helpers.mjs52
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/nav-cancelation-2-helper.html18
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/slow.py7
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-cross-document-nav.html41
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-cross-document-traversal.html70
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-same-document-nav.html61
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-same-document-traversal.html55
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-stop.html28
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-cross-document-nav.html42
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-cross-document-traversal.html88
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-nav.html57
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-hashchange.html148
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-pushstate.html137
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-stop.html37
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/read-media/cross-origin-video.html36
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/read-media/pageload-image-in-popup.html31
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/read-media/pageload-image.html31
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/read-media/pageload-video.html28
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/read-media/resources/iframe-document.sub.html5
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/read-text/load-text-plain.html40
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addHTML.window.js20
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addIframe-srcdoc-startOn.window.js24
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addIframe-srcdoc.window.js27
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addIframe.window.js40
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addScripts.window.js19
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-defaults.window.js15
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-extra-config.window.js36
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-features.window.js23
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-invalid-origin.window.js21
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-startOn.window.js19
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-target.window.js17
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWorker.window.js29
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/constructor.window.js38
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/createContext-bad-executorCreator.window.js20
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/getRequestHeaders.window.js18
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/navigateToNew.window.js37
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/navigation-bfcache.window.js35
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/navigation-helpers.window.js28
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/navigation-same-document.window.js39
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/resources/test-helper.js48
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/resources/test-script.js3
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/resources/test-script2.js3
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/executor-common.js14
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/executor-window.js67
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/executor-window.py40
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/executor-worker.js12
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js592
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/resources/has-iframe.html7
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/resources/helpers.js84
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/resources/post-top-opener-on-load.html6
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/001.html16
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/002.html21
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/003.html25
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/004.html23
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/005.html23
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/006.html34
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/007.html37
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/forward-triggers-hashchange.html44
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/fragment-and-encoding-2.html41
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/fragment-and-encoding.html50
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/navigate-helpers.js23
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/replacement-enabled.html69
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-frag-non-utf8-encoded-document.html21
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-frag-percent-encoded.html62
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-position-vertical-lr.html25
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-position-vertical-rl.html25
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-position.html25
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-to-anchor-name.html59
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-to-id-top.html51
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-to-top.html56
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/001.html15
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/002.html15
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/003.html15
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/004.html15
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/005.html15
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/base.html14
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-canceling-1.html35
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-canceling.html222
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-history-back-1.html5
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-history-back.html31
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-navigation-of-parent-1.html2
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-navigation-of-parent-2.html4
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-navigation-of-parent.html31
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-sticky-activation-iframe.html25
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-sticky-activation-manual.html17
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-sticky-activation-popup.html24
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-synchronous.html33
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/navigation-within-beforeunload-1.html10
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/navigation-within-beforeunload-2.html4
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/navigation-within-beforeunload.html29
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/pagehide-on-history-forward-1.html2
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/pagehide-on-history-forward.html19
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt-and-unload-script-closeable.html23
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt-and-unload-script-uncloseable-1.html10
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt-and-unload-script-uncloseable.html24
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/001-1.html10
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/001-2.html1
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/001.html14
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/002-1.html7
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/002.html20
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/003.html20
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/004-1.html28
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/004-2.html5
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/004.html29
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-001.html9
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-002.html9
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-003.html11
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-004.html11
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-005.html22
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-006.html9
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/next.html2
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/001-1.html23
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/001a.html7
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/001b.html5
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/002-1.html32
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/002a.html7
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/002b.html5
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/003-1.html23
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/003a.html8
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/003b.html5
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/004-1.html25
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/004a.html8
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/004b.html18
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/005-1.html13
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/005a.html8
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/005b.html17
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/beforeunload-sticky-destination.html13
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/beforeunload-sticky-start.html10
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/001-1.html4
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/001-2.html2
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/001.html25
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/002-1.html4
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/002.html36
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/003-1.html4
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/003.html33
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/004-1.html5
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/004.html28
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/006-1.html23
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/006-2.html5
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/006.html18
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/007-1.html21
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/007-2.html5
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/007.html28
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/008-1.html4
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/008.html16
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/009-1.html4
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/009.html16
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/pagehide-manual-1.html2
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/pagehide-manual.html5
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/unload-main-frame-cross-origin.window.js34
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/unload-main-frame-same-origin.window.js33
-rw-r--r--testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-child1.html18
-rw-r--r--testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-child2.html5
-rw-r--r--testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-filler.html1
-rw-r--r--testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-grandchild1.html8
-rw-r--r--testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-grandchild2.html5
-rw-r--r--testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-iframe-state.html41
-rw-r--r--testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-only-fully-active.html30
-rw-r--r--testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-remove-iframe.html24
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/001.html339
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/002.html318
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/004.html62
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/005.html47
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/006.html53
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/007.html66
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/008.html40
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/008.js11
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/009-1.html20
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/009-3.html28
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/009-5.html23
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/009.html22
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/010-1.html16
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/010-3.html24
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/010-5.html23
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/010.html22
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/011.html32
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/012.html32
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/blank-new.html5
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/blank-old.html5
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/blank.html8
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/blank2.html13
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/blank3.html11
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_001.html20
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_002.html22
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_003.html26
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_004.html29
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_005.html34
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_006.html30
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_007.html32
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history-associated-with-document.window.js6
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history-state-after-bfcache.window.js43
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history.js35
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_back-1.html15
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_back.html27
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_back_1.html26
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_back_cross_realm_method.html22
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_entry.html11
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward-1.html14
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward-2.html15
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward.html32
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward_1.html26
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward_cross_realm_method.html28
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_cross_realm_method.html23
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_minus.html27
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_no_argument-1.html17
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_no_argument.html27
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_plus.html33
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_to_uri-1.html22
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_to_uri.html30
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_undefined-1.html14
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_undefined.html26
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_zero-1.html17
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_zero.html27
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_zero_which_document.window.js27
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_properties_only_fully_active.html23
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate.html19
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate_err.html18
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate_nooptionalparam.html20
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate_url.html24
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate_url_rewriting.html176
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_replacestate.html20
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_replacestate_err.html18
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_replacestate_nooptionalparam.html20
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/history_state.html24
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/iframe_history_go_0.html40
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/001-1.html72
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/001.html18
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/002-1.html35
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/002.html18
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/filler.html5
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/history.js35
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/history_entry.html12
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_history_unload_prompt_1-1.html18
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_history_unload_prompt_1-manual.html31
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_history_unload_prompt_2-1.html18
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_history_unload_prompt_2-manual.html31
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_session_history_unload_prompt_1-1.html17
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_session_history_unload_prompt_1-manual.html30
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/pushstate-base.html18
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/pushstate-whitespace.html20
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/pushstate.html17
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/replacestate-base.html18
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/replacestate-whitespace.html20
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/replacestate.html17
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/resources/message-opener.sub.html8
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/resources/traverse-during-beforeunload.html12
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/resources/traverse-during-unload.html12
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/resources/url-rewriting-helper.html12
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse-during-beforeunload.html28
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse-during-unload.html28
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_1-1.html16
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_1.html25
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_2-1.html15
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_2.html26
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_3-1.html15
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_3.html26
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_4-1.html15
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_4.html26
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_5-1.html15
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_5.html26
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_unload_1-1.html15
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_unload_1.html28
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_1-1.html12
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_1.html29
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_2-1.html9
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_2.html24
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_onload_1-1.html9
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_onload_1.html27
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_onload_2-1.html9
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_onload_2.html27
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/allow_prototype_cycle_through_location.sub.html197
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/assign_after_load-1.html9
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/assign_after_load-2.html7
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/assign_after_load.html23
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/assign_before_load-1.html7
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/assign_before_load-2.html7
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/assign_before_load.html23
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/cross_origin_joined_frame.sub.html15
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/document_location.html39
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location-non-configurable-toString-valueOf.html42
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location-origin-idna.sub.window.js11
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location-pathname-setter-question-mark.html16
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location-prevent-extensions.html21
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter-non-broken-weird.html35
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter-non-broken.html63
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter-sameish.html26
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter-with-colon.sub.html52
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter.html104
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-no-toString-valueOf.html55
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-cross-origin-domain.sub.html31
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-cross-origin.sub.html28
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-goes-cross-origin-domain.sub.html39
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-same-origin-domain.sub.html30
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-same-origin.html21
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location-stringifier.html24
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location-symbol-toprimitive.html14
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location-tojson.html13
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location-valueof.html15
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location_assign.html26
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location_assign_about_blank-1.html2
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location_assign_about_blank.html24
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location_hash.html62
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location_host.html28
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location_hostname.html33
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location_href.html19
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location_origin.html14
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location_pathname.html22
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location_port.html31
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location_protocol.html25
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location_reload-iframe.html4
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location_reload.html43
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location_reload_javascript_url.html60
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location_replace.html26
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/location_search.html20
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/no-browsing-context.window.js86
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_assign_during_load-1.html7
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_assign_during_load-2.html7
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_assign_during_load-manual.html17
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_location_replace_during_load-1.html10
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_location_replace_during_load-2.html7
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_location_replace_during_load-manual.html17
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_replace_during_load-manual.html17
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_form_submit_assign_during_load-1.html9
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_form_submit_assign_during_load-2.html7
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_form_submit_assign_during_load-manual.html17
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/reload_in_resize-1.html15
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/reload_in_resize-manual.html26
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/per-global.window.js3
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_open_write-1.html19
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_open_write.html26
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_write-1.html4
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_write.html21
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_write_onload-1.html9
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_write_onload.html26
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/reload_post_1-manual.html27
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/resources/post-your-origin.html3
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/resources/post-your-protocol.html4
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/resources/reload_post_1-1.py13
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/same-hash.html101
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/same_origin_frame.html12
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_assign_during_load-1.html10
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_assign_during_load-2.html7
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_assign_during_load.html17
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_location_assign_during_load-1.html13
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_location_assign_during_load-2.html7
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_location_assign_during_load.html16
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_form_submit_assign_during_load-1.html12
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_form_submit_assign_during_load-2.html7
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_form_submit_assign_during_load.html17
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-location-interface/security_location_0.htm27
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload.html37
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload_form-submission-1.html15
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload_form-submission-2.html22
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload_form-submission-dynamic-iframe.html16
-rw-r--r--testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload_form-submission-iframe.html10
-rw-r--r--testing/web-platform/tests/html/browsers/offline/application-cache-api/api_status_checking-manual.html23
-rw-r--r--testing/web-platform/tests/html/browsers/offline/application-cache-api/api_status_downloading-manual.html24
-rw-r--r--testing/web-platform/tests/html/browsers/offline/application-cache-api/api_status_obsolete-manual.html24
-rw-r--r--testing/web-platform/tests/html/browsers/offline/application-cache-api/api_status_updateready-manual.html25
-rw-r--r--testing/web-platform/tests/html/browsers/offline/application-cache-api/api_swapcache-manual.html30
-rw-r--r--testing/web-platform/tests/html/browsers/offline/browser-state/navigator_online_event-manual.https.html46
-rw-r--r--testing/web-platform/tests/html/browsers/offline/browser-state/navigator_online_online.https.html17
-rw-r--r--testing/web-platform/tests/html/browsers/offline/changestonetworkingmodel/original-id.json1
-rw-r--r--testing/web-platform/tests/html/browsers/offline/introduction-4/event_downloading-manual.html23
-rw-r--r--testing/web-platform/tests/html/browsers/offline/introduction-4/event_error-manual.html23
-rw-r--r--testing/web-platform/tests/html/browsers/offline/introduction-4/event_obsolete-manual.html23
-rw-r--r--testing/web-platform/tests/html/browsers/offline/introduction-4/event_updateready-manual.html22
-rw-r--r--testing/web-platform/tests/html/browsers/offline/introduction-4/event_updateready_swapcache-manual.html30
-rw-r--r--testing/web-platform/tests/html/browsers/offline/manifest_main_empty-manual.https.html14
-rw-r--r--testing/web-platform/tests/html/browsers/offline/manifest_notchanged_online-manual.https.html19
-rw-r--r--testing/web-platform/tests/html/browsers/offline/manifest_section_empty-manual.https.html19
-rw-r--r--testing/web-platform/tests/html/browsers/offline/manifest_section_many-manual.https.html19
-rw-r--r--testing/web-platform/tests/html/browsers/offline/resources/css/clock.css1
-rw-r--r--testing/web-platform/tests/html/browsers/offline/resources/css/offline.css5
-rw-r--r--testing/web-platform/tests/html/browsers/offline/resources/css/online.css5
-rw-r--r--testing/web-platform/tests/html/browsers/offline/resources/css/result.css11
-rw-r--r--testing/web-platform/tests/html/browsers/offline/resources/html/clock.html12
-rw-r--r--testing/web-platform/tests/html/browsers/offline/resources/js/clock.js3
-rw-r--r--testing/web-platform/tests/html/browsers/offline/resources/manifest/clock.manifest17
-rw-r--r--testing/web-platform/tests/html/browsers/offline/resources/manifest/section_empty.manifest10
-rw-r--r--testing/web-platform/tests/html/browsers/offline/resources/manifest/section_many.manifest19
-rw-r--r--testing/web-platform/tests/html/browsers/offline/resources/manifest/url_check.manifest17
-rw-r--r--testing/web-platform/tests/html/browsers/offline/section_network_offline-manual.https.html17
-rw-r--r--testing/web-platform/tests/html/browsers/offline/section_network_online-manual.https.html16
-rw-r--r--testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-due-to-document-domain-only.html33
-rw-r--r--testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-caching.html49
-rw-r--r--testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-common.js34
-rw-r--r--testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-length.html45
-rw-r--r--testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-name.html45
-rw-r--r--testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-on-new-window.html25
-rw-r--r--testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects.html718
-rw-r--r--testing/web-platform/tests/html/browsers/origin/cross-origin-objects/frame-with-then.html19
-rw-r--r--testing/web-platform/tests/html/browsers/origin/cross-origin-objects/frame.html51
-rw-r--r--testing/web-platform/tests/html/browsers/origin/cross-origin-objects/location-properties-smoke-test.window.js51
-rw-r--r--testing/web-platform/tests/html/browsers/origin/cross-origin-objects/resources/cross-origin-due-to-document-domain-only-helper.html9
-rw-r--r--testing/web-platform/tests/html/browsers/origin/cross-origin-objects/win-documentdomain.sub.html63
-rw-r--r--testing/web-platform/tests/html/browsers/origin/cross-origin-objects/window-location-and-location-href-cross-realm-set.html106
-rw-r--r--testing/web-platform/tests/html/browsers/origin/inheritance/about-blank-iframe.html28
-rw-r--r--testing/web-platform/tests/html/browsers/origin/inheritance/about-blank-window.html25
-rw-r--r--testing/web-platform/tests/html/browsers/origin/inheritance/about-srcdoc.html29
-rw-r--r--testing/web-platform/tests/html/browsers/origin/inheritance/document-write.https.window.js10
-rw-r--r--testing/web-platform/tests/html/browsers/origin/inheritance/document-write.https.window.js.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/inheritance/javascript-url.html33
-rw-r--r--testing/web-platform/tests/html/browsers/origin/inheritance/resources/document-write.html40
-rw-r--r--testing/web-platform/tests/html/browsers/origin/inheritance/resources/document-write.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/inheritance/resources/iframe-with-about-blank-iframe.html7
-rw-r--r--testing/web-platform/tests/html/browsers/origin/inheritance/resources/iframe-with-about-blank-iframe.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-bad-subdomain.sub.https.html27
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-port.sub.https.html27
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-same.sub.https.html27
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-subdomain-with-redirect.sub.https.html27
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-subdomain.sub.https.html27
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yeswithparams-subdomain.sub.https.html27
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-port.sub.https.html27
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-port.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-same.sub.https.html26
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-same.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-subdomain.sub.https.html27
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-subdomain.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-port.sub.https.html27
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-port.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-same.sub.https.html26
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-same.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-subdomain.sub.https.html27
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-subdomain.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-no-subdomain-child2-yes-subdomain.sub.https.html36
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-no-subdomain-child2-yes-subdomainport.sub.https.html37
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-no-subdomain1-child2-yes-subdomain2.sub.https.html37
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-yes-subdomain-child2-no-port.sub.https.html37
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-yes-subdomain-child2-no-subdomain.sub.https.html38
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-no-subdomain.sub.https.html38
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-no-subdomain.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-no-subdomain2.sub.https.html38
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-no-subdomain2.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomain.sub.https.html38
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomain.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomain2.sub.https.html38
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomain2.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomainport.sub.https.html38
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomainport.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-no-port.sub.https.html36
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-no-port.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-no-subdomain.sub.https.html38
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-no-subdomain.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomain.sub.https.html39
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomain.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomain2.sub.https.html38
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomain2.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomainport.sub.https.html38
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomainport.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/META.yml5
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/README.md30
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/about-blank.https.sub.html63
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/about-blank.https.sub.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/document-domain.sub.https.html52
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/document-domain.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/cross-origin-isolated.sub.https.html28
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/cross-origin-isolated.sub.https.html.headers2
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/csp-sandbox-no.https.html15
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/csp-sandbox-no.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/csp-sandbox-yes.https.html15
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/csp-sandbox-yes.https.html.headers2
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-to-javascript-no.https.html12
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-to-javascript-yes.https.html12
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-to-javascript-yes.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-url-no.https.html12
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-url-yes.https.html12
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-url-yes.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/javascript-url-no.https.html12
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/javascript-url-yes.https.html12
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/javascript-url-yes.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/removed-iframe.sub.https.html27
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/removed-iframe.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/data-to-javascript-test.mjs33
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/data-url-test.mjs13
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/helpers.mjs28
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/javascript-url-test.mjs14
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/sandboxed-iframe-test.sub.mjs20
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/sandboxed-same-origin-iframe-test.sub.mjs20
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-iframe-no.https.html12
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-iframe-yes.https.html12
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-iframe-yes.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-same-origin-iframe-no.https.html12
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-same-origin-iframe-yes.https.html12
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-same-origin-iframe-yes.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/going-back.sub.https.html62
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/going-back.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-same-2-yes-port.sub.https.html41
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-same-2-yes-subdomain.sub.https.html41
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-subdomain-2-yes-subdomain.sub.https.html40
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-subdomain-2-yes-subdomain2.sub.https.html41
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-subdomain-yes-2-subdomain2-no.sub.https.html43
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-yes-subdomain-2-no-subdomain.sub.https.html41
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-yes-1-no-same-2-no-port.sub.https.html41
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-yes-1-no-same-2-no-port.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-yes-1-no-same-2-no-subdomain.sub.https.html41
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-yes-1-no-same-2-no-subdomain.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/insecure-http.sub.html26
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/insecure-http.sub.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups-crash.https.html9
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-no-openee-yes-port.sub.https.html28
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-no-openee-yes-same.sub.https.html27
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-no-openee-yes-subdomain.sub.https.html28
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-port.sub.https.html28
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-port.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-same.sub.https.html27
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-same.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-subdomain.sub.https.html28
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-subdomain.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-port.sub.https.html28
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-port.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-same.sub.https.html27
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-same.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-subdomain.sub.https.html28
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-subdomain.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/regression-1399759.https.sub.html100
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/removing-iframes.sub.https.html46
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/removing-iframes.sub.https.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/README.md6
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/coep-frame.html5
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/coep-frame.html.headers2
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/crashy-popup.sub.html6
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/crashy-popup.sub.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/frame.html12
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/frame.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/helpers.mjs390
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/helpers.mjs.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/send-header-page-script.mjs63
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/send-header-page-script.mjs.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/send-oac-header.py43
-rw-r--r--testing/web-platform/tests/html/browsers/origin/origin-of-data-document.html30
-rw-r--r--testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain.html35
-rw-r--r--testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_access_details.sub.html305
-rw-r--r--testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_setter.html76
-rw-r--r--testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_setter_srcdoc.html84
-rw-r--r--testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/sandboxed-document_domain.html21
-rw-r--r--testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/sandboxed-document_domain.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_frame.html52
-rw-r--r--testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_frame.sub.js65
-rw-r--r--testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_setter_iframe.html12
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/inner-iframe.html13
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/noscript-iframe.html3
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/popup-from-initial-empty-sandboxed-document.window.js46
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/resources/check-sandbox-flags.html8
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/resources/document-open.html14
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/resources/execute-postmessage.html5
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/resources/post-done-to-opener.html3
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/resources/sandbox-inherited-from-initiator-response-helper.html15
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/resources/sandbox-inherited-from-initiator-response-helper.html.headers1
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/resources/sandbox-javascript-window-open.html18
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-allow-same-origin.html30
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-allow-scripts.html29
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-disallow-popups.html39
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-disallow-same-origin.html35
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-disallow-scripts-via-unsandboxed-popup.tentative.html33
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-disallow-scripts.html29
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-document-open-mutation.window.js37
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-document-open.html50
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-inherited-from-initiator-frame.html64
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-inherited-from-initiator-response.html46
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-inherited-from-required-csp.html154
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-initial-empty-document-toward-same-origin.html30
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-javascript-window-open.html19
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-navigation-timing-iframe.tentative.html16
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-navigation-timing.tentative.html29
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-new-execution-context-iframe.html5
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-new-execution-context.html39
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-parse-noscript-ref.html6
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-parse-noscript.html7
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/sandbox-window-open-srcdoc.html52
-rw-r--r--testing/web-platform/tests/html/browsers/sandboxing/window-open-blank-from-different-initiator.html90
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/BarProp.window.js59
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/Document-defaultView.html38
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/Window-document.html25
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/indexed-browsing-contexts-01.html47
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/indexed-browsing-contexts-02.html62
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/indexed-browsing-contexts-03.html30
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/iterator.html11
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/test1.html12
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/test2.html6
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/test3.html8
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/window_length.html51
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/close-method.window.js39
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/closed-attribute.window.js69
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/defaultstatus.html17
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/document-attribute.window.js15
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/focus.window.js15
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_1-1.html20
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_1-2.html2
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_1.html10
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_2-1.html22
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_2.html10
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_3-1.html21
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_3-2.html4
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_3-3.html4
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_3.html9
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_4-1.html21
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_4-2.html4
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_4-3.html4
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_4.html9
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_1-1.html27
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_1-2.html2
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_1-manual.html10
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_2-1.html19
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_2-2.html1
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_2-3.html1
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_2-manual.html10
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/historical.window.js4
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/length-attribute.window.js24
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/name-attribute.window.js18
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/cross-global-npo.html38
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/cross-global-support.html4
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/named-objects.html76
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/navigated-named-objects.window.js67
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/prototype.html94
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/strict-mode-redefine-readonly-property.html17
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/test.html7
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/window-named-properties.html83
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/window-null-names.html20
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/navigate-to-about-blank-while-initial-load-pending.html26
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/noopener-noreferrer-BarProp.window.js23
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/noopener-noreferrer-sizing.window.js17
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/callback.js1
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/close_beforeunload-1.html7
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/close_beforeunload.html16
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/close_pagehide-1.html7
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/close_pagehide.html16
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/close_script_defer-1.html1
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/close_script_defer.html18
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/close_unload-1.html7
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/close_unload.html16
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/creating_browsing_context_test_01.html39
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/no_window_open_when_term_nesting_level_nonzero.window.js113
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/001-1.html2
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/001-2.html16
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/001.html3
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/002-1.html8
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/002-2.html16
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/002.html3
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-is-popup-condition.html149
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-negative-innerwidth-innerheight.html75
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-negative-screenx-screeny.html67
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-negative-top-left.html68
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-negative-width-height.html75
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-height.html91
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-innerheight.html76
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-innerwidth.html75
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-left.html76
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-screenx.html75
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-screeny.html76
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-top.html73
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-width.html91
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-innerheight-innerwidth.html55
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-noopener.html11
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-noreferrer.html11
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-screenx-screeny.html55
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-top-left.html69
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-width-height.html71
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/resources/close-self.html3
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/resources/is-popup-barprop.html15
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/resources/message-opener.html52
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/open-close/resources/tokenization-noopener-noreferrer.js152
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/proxy-getOwnPropertyDescriptor.html126
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/security-window/window-security.https.html200
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/self-et-al.window.js43
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/support/BarProp-target.html17
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/support/closed.html12
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/support/noopener-target.html15
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/support/noreferrer-target.html13
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/support/same-origin-iframe.html8
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/support/sizing-target.html17
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/support/window-open-popup-target.html24
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/support/windowFeature-values-target.html24
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/window-aliases.html28
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/window-indexed-access-vs-named-access.html58
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/window-indexed-properties-delete-no-cache.html31
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/window-indexed-properties-strict.html75
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/window-indexed-properties.html71
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/window-open-defaults.window.js12
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/window-open-invalid-url.html10
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/window-open-noopener.html137
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/window-open-noreferrer.html20
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/window-open-popup-behavior.html51
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/window-open-windowfeatures-values.html72
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/window-opener-unconfigurable.window.js17
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/window-properties.https.html359
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/window-prototype-chain.html32
-rw-r--r--testing/web-platform/tests/html/browsers/the-window-object/window-reuse-in-nested-browsing-contexts.tentative.html158
-rw-r--r--testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/document-tree-child-browsing-context-name-property-set.sub.html26
-rw-r--r--testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-define-own-property-unforgeable-same-origin.html47
-rw-r--r--testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prevent-extensions.html21
-rw-r--r--testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-cross-origin-domain.sub.html29
-rw-r--r--testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-cross-origin.sub.html28
-rw-r--r--testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-goes-cross-origin-domain.sub.html35
-rw-r--r--testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-same-origin-domain.sub.html30
-rw-r--r--testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-same-origin.html21
-rw-r--r--testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-closed.html31
-rw-r--r--testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-multiple.html35
-rw-r--r--testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-noopener.html25
-rw-r--r--testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-noreferrer.html29
-rw-r--r--testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-setter.html33
-rw-r--r--testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-setter.window.js38
-rw-r--r--testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener.html55
-rw-r--r--testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/close-opener.html30
-rw-r--r--testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/message-window-opener.html14
-rw-r--r--testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/multiple-opener.html32
-rw-r--r--testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/no-opener.html16
-rw-r--r--testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/open-closer.html17
-rw-r--r--testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/opener-setter.html23
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_blank-001.html35
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_blank-002.html21
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_blank-003.html20
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_parent-001.html15
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_parent-002.html17
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_parent-003.html17
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_parent-004.html40
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_self-001.html15
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_self-002.html46
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_top-001.html34
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_top-002.html33
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_top-003.html39
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-default-001.html23
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-default-002.html16
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-existing-001.html17
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-001-iframe-1.html5
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-002-iframe.html4
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-002-window.html12
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-003-iframe.html6
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-003-window.html4
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-004-iframe-1.html14
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-004-iframe-2.html9
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_self-001-iframe.html9
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_self-002-iframe.html11
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_top-002-window.html16
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_top-003-iframe-1.html15
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_top-003-iframe-2.html10
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-default-002-iframe.html17
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-existing-001-iframe.html7
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/open-in-_parent.html9
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/open-in-_top.html9
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/post-to-opener.html12
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/post-to-top.html10
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/report-has-opener.html8
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/report-is-top.html10
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context-window.html37
-rw-r--r--testing/web-platform/tests/html/browsers/windows/browsing-context.html48
-rw-r--r--testing/web-platform/tests/html/browsers/windows/clear-window-name.https.html122
-rw-r--r--testing/web-platform/tests/html/browsers/windows/dangling-markup-window-name.html97
-rw-r--r--testing/web-platform/tests/html/browsers/windows/document-domain-nested-navigate.window.js16
-rw-r--r--testing/web-platform/tests/html/browsers/windows/document-domain-nested-set.window.js10
-rw-r--r--testing/web-platform/tests/html/browsers/windows/document-domain-nested.window.js9
-rw-r--r--testing/web-platform/tests/html/browsers/windows/document-domain-removed-iframe.html72
-rw-r--r--testing/web-platform/tests/html/browsers/windows/embedded-opener-a-form.html30
-rw-r--r--testing/web-platform/tests/html/browsers/windows/embedded-opener-remove-frame.html66
-rw-r--r--testing/web-platform/tests/html/browsers/windows/embedded-opener.html32
-rw-r--r--testing/web-platform/tests/html/browsers/windows/iframe-cross-origin-print.sub.html6
-rw-r--r--testing/web-platform/tests/html/browsers/windows/iframe-cross-origin-scaled-print.sub.html17
-rw-r--r--testing/web-platform/tests/html/browsers/windows/iframe-nested-print-ref.html9
-rw-r--r--testing/web-platform/tests/html/browsers/windows/iframe-nested-print.html6
-rw-r--r--testing/web-platform/tests/html/browsers/windows/iframe-nested-scaled-print-ref.html15
-rw-r--r--testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/frameElement-siblings.sub.html42
-rw-r--r--testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/frameElement.sub.html66
-rw-r--r--testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/name-attribute.window.js58
-rw-r--r--testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/frameElement-nested-frame.html7
-rw-r--r--testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/frameElement-sibling-accessed.html16
-rw-r--r--testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/frameElement-sibling-accessor.html33
-rw-r--r--testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/frameElement-window-post.html14
-rw-r--r--testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/post-to-opener.html7
-rw-r--r--testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/post-to-parent.html6
-rw-r--r--testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/window-parent-null.html66
-rw-r--r--testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/window-parent.html44
-rw-r--r--testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/window-top-null.html66
-rw-r--r--testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/window-top.html65
-rw-r--r--testing/web-platform/tests/html/browsers/windows/noreferrer-cross-origin-close-manual.sub.html3
-rw-r--r--testing/web-platform/tests/html/browsers/windows/noreferrer-cross-origin-manual.html10
-rw-r--r--testing/web-platform/tests/html/browsers/windows/noreferrer-cross-origin-window-name-manual.sub.html3
-rw-r--r--testing/web-platform/tests/html/browsers/windows/noreferrer-null-opener.html25
-rw-r--r--testing/web-platform/tests/html/browsers/windows/noreferrer-window-name.html86
-rw-r--r--testing/web-platform/tests/html/browsers/windows/opener-cross-origin-manual.sub.html10
-rw-r--r--testing/web-platform/tests/html/browsers/windows/opener-string.window.js14
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/first-party-to-first-party-cross-partition.sub.html26
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/first-party-to-first-party-same-partition.html26
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/first-party-to-third-party-cross-partition-cross-origin.sub.html27
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/first-party-to-third-party-cross-partition-same-origin.sub.html27
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-first-party-cross-partition-window.html8
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-first-party-same-partition-window.html8
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-cross-origin-iframe.https.html8
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-cross-origin-window.sub.https.html6
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-same-origin-iframe.html8
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-same-origin-window.sub.html6
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-cross-origin-iframe.sub.html17
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-cross-origin-window.https.html8
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-same-origin-iframe.sub.html17
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-same-origin-window.html8
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-cross-origin-iframe-a.sub.html17
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-cross-origin-iframe-b.https.html8
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-cross-origin-window.sub.https.html6
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-same-origin-iframe-a.sub.https.html17
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-same-origin-iframe-b.https.html8
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-same-origin-window.sub.https.html6
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-same-partition-iframe-a.sub.html17
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-same-partition-iframe-b.html8
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-same-partition-window.sub.html6
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-first-party-cross-partition-cross-origin.sub.html29
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-first-party-cross-partition-same-origin.sub.html29
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-third-party-cross-partition-cross-origin.sub.html30
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-third-party-cross-partition-same-origin.sub.html30
-rw-r--r--testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-third-party-same-partition.sub.html30
-rw-r--r--testing/web-platform/tests/html/browsers/windows/resources/browsing-context-window.html7
-rw-r--r--testing/web-platform/tests/html/browsers/windows/resources/document-domain-setter.html7
-rw-r--r--testing/web-platform/tests/html/browsers/windows/resources/echo-window-name.html1
-rw-r--r--testing/web-platform/tests/html/browsers/windows/resources/iframe-nested-cross-origin.html2
-rw-r--r--testing/web-platform/tests/html/browsers/windows/resources/iframe-nested-printing-pass.html9
-rw-r--r--testing/web-platform/tests/html/browsers/windows/resources/message-parent.html5
-rw-r--r--testing/web-platform/tests/html/browsers/windows/resources/nested-post-to-opener.html12
-rw-r--r--testing/web-platform/tests/html/browsers/windows/resources/noreferrer-window-name.html8
-rw-r--r--testing/web-platform/tests/html/browsers/windows/resources/opener-cross-origin-embed.sub.html2
-rw-r--r--testing/web-platform/tests/html/browsers/windows/resources/opener-cross-origin-end.txt1
-rw-r--r--testing/web-platform/tests/html/browsers/windows/resources/opener-cross-origin.html4
-rw-r--r--testing/web-platform/tests/html/browsers/windows/resources/post-to-opener.html8
-rw-r--r--testing/web-platform/tests/html/browsers/windows/resources/restore-window-name-back.sub.html7
-rw-r--r--testing/web-platform/tests/html/browsers/windows/resources/restore-window-name.sub.html43
-rw-r--r--testing/web-platform/tests/html/browsers/windows/resources/target-cross-origin.sub.html3
-rw-r--r--testing/web-platform/tests/html/browsers/windows/resources/window-close-button.html1
-rw-r--r--testing/web-platform/tests/html/browsers/windows/resources/window-name-stash.py13
-rw-r--r--testing/web-platform/tests/html/browsers/windows/resources/window-name.sub.html102
-rw-r--r--testing/web-platform/tests/html/browsers/windows/resources/window-opener.html4
-rw-r--r--testing/web-platform/tests/html/browsers/windows/restore-window-name-manual.https.html20
-rw-r--r--testing/web-platform/tests/html/browsers/windows/targeting-cross-origin-nested-browsing-contexts.html39
-rw-r--r--testing/web-platform/tests/html/browsers/windows/targeting-multiple-cross-origin-manual.sub.html9
-rw-r--r--testing/web-platform/tests/html/browsers/windows/targeting-with-embedded-null-in-target.html34
1125 files changed, 35188 insertions, 0 deletions
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/README.md b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/README.md
new file mode 100644
index 0000000000..afda8f6c8a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/README.md
@@ -0,0 +1,60 @@
+# How to write back-forward cache tests
+
+In the back-forward cache tests, the main test HTML usually:
+
+1. Opens new executor Windows using `window.open()` + `noopener` option,
+ because less isolated Windows (e.g. iframes and `window.open()` without
+ `noopener` option) are often not eligible for back-forward cache (e.g.
+ in Chromium).
+2. Injects scripts to the executor Windows and receives the results via
+ `RemoteContext.execute_script()` by
+ [/common/dispatcher](../../../../common/dispatcher/README.md).
+ Follow the semantics and guideline described there.
+
+Back-forward cache specific helpers are in:
+
+- [resources/executor.html](resources/executor.html):
+ The BFCache-specific executor and contains helpers for executors.
+- [resources/helper.sub.js](resources/helper.sub.js):
+ Helpers for main test HTMLs.
+
+We must ensure that injected scripts are evaluated only after page load
+(more precisely, the first `pageshow` event) and not during navigation,
+to prevent unexpected interference between injected scripts, in-flight fetch
+requests behind `RemoteContext.execute_script()`, navigation and back-forward
+cache. To ensure this,
+
+- Call `await remoteContext.execute_script(waitForPageShow)` before any
+ other scripts are injected to the remote context, and
+- Call `prepareNavigation(callback)` synchronously from the script injected
+ by `RemoteContext.execute_script()`, and trigger navigation on or after the
+ callback is called.
+
+In typical A-B-A scenarios (where we navigate from Page A to Page B and then
+navigate back to Page A, assuming Page A is (or isn't) in BFCache),
+
+- Call `prepareNavigation()` on the executor, and then navigate to B, and then
+ navigate back to Page A.
+- Call `assert_bfcached()` or `assert_not_bfcached()` on the main test HTML, to
+ check the BFCache status. This is important to do to ensure the test would
+ not fail normally and instead result in `PRECONDITION_FAILED` if the page is
+ unexpectedly bfcached/not bfcached.
+- Check other test expectations on the main test HTML,
+
+as in [events.html](./events.html) and `runEventTest()` in
+[resources/helper.sub.js](resources/helper.sub.js).
+
+# Asserting PRECONDITION_FAILED for unexpected BFCache eligibility
+
+Browsers are not actually obliged to put pages in BFCache after navigations, so
+BFCache WPTs shouldn't result in `FAILED` if it expects a certain case to be
+supported by BFCache. But, it is still useful to test those cases in the
+browsers that do support BFCache for that case.
+
+To distinguish genuine failures from just not using BFCache, we use
+`assert_bfcached()` and `assert_not_bfcached()` which result in
+`PRECONDITION_FAILED` rather than `FAILED`. that should be put in the
+expectations for the failing tests (instead of marking it as `FAILED` or
+skipping the test). This means if the test starts passing (e.g. if we start
+BFCaching in the case being tested), we will notice that the output changed from
+`PRECONDITION_FAILED` to `PASS`.
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/broadcast-channel.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/broadcast-channel.html
new file mode 100644
index 0000000000..bc04a5ed7f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/broadcast-channel.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="/common/dispatcher/dispatcher.js"></script>
+<script src="../resources/helper.sub.js"></script>
+<script>
+// Check whether the page is BFCached when there are open BroadcastChannels.
+// See https://github.com/whatwg/html/issues/7219 for other related scenarios.
+runEventTest(
+ {funcBeforeNavigation: () => {
+ window.bc = new BroadcastChannel('foo');
+ }},
+ 'Eligibility (BroadcastChannel)');
+
+// Same as above, but the BroadcastChannels are closed in the pagehide event.
+runEventTest(
+ {funcBeforeNavigation: () => {
+ window.bc = new BroadcastChannel('foo');
+ window.addEventListener('pagehide', () => window.bc.close());
+ }},
+ 'Eligibility (BroadcastChannel closed in the pagehide event)');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/dedicated-worker.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/dedicated-worker.html
new file mode 100644
index 0000000000..b08588a8bd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/dedicated-worker.html
@@ -0,0 +1,25 @@
+<!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/helper.sub.js"></script>
+<script>
+// Check whether the page is BFCached when there are dedicated workers that are
+// already loaded.
+runBfcacheTest({
+ funcBeforeNavigation: async () => {
+ globalThis.worker = new Worker('../resources/echo-worker.js');
+ // Make sure the worker starts before navigation.
+ await WorkerHelper.pingWorker(globalThis.worker);
+ },
+ funcAfterAssertion: async (pageA) => {
+ // Confirm that the worker is still there.
+ assert_equals(
+ await pageA.execute_script(() => WorkerHelper.pingWorker(globalThis.worker)),
+ 'PASS',
+ 'Worker should still work after restored from BFCache');
+ }
+}, 'Eligibility: dedicated workers');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-1.html
new file mode 100644
index 0000000000..6a48d0657b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-1.html
@@ -0,0 +1,18 @@
+<!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/helper.sub.js"></script>
+<script src="../resources/inflight-fetch-helper.js"></script>
+<script>
+// Check whether the page is BFCached when there are in-flight network requests
+// at the time of navigation.
+
+// Successful fetch completion with header received before BFCached.
+runTest(sameOriginUrl + '?delayBeforeBody=2000', false, true,
+ 'Header received before BFCache and body received when in BFCache');
+runTest(sameOriginUrl + '?delayBeforeBody=3500', false, true,
+ 'Header received before BFCache and body received after BFCache');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-2.html
new file mode 100644
index 0000000000..a767c4fa83
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-2.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="../resources/helper.sub.js"></script>
+<script src="../resources/inflight-fetch-helper.js"></script>
+<script>
+// Check whether the page is BFCached when there are in-flight network requests
+// at the time of navigation.
+
+// Successful fetch completion with header received when in BFCache or after
+// BFCache.
+runTest(sameOriginUrl + '?delayBeforeHeader=2000', false, true,
+ 'Header and body received when in BFCache');
+runTest(sameOriginUrl + '?delayBeforeHeader=2000&delayBeforeBody=1500',
+ false, true,
+ 'Header received when in BFCache and body received after BFCache');
+runTest(sameOriginUrl + '?delayBeforeHeader=3500', false, true,
+ 'Header and body received after BFCache');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-cors.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-cors.html
new file mode 100644
index 0000000000..c04089a5e2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-cors.html
@@ -0,0 +1,18 @@
+<!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/helper.sub.js"></script>
+<script src="../resources/inflight-fetch-helper.js"></script>
+<script>
+// Check whether the page is BFCached when there are in-flight network requests
+// at the time of navigation.
+
+// CORS and failing fetch.
+runTest(crossSiteUrl + '?delayBeforeHeader=2000&cors=yes', false, true,
+ 'CORS succeeded when in BFCache');
+runTest(crossSiteUrl + '?delayBeforeHeader=2000', false, false,
+ 'CORS failed when in BFCache');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-redirects.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-redirects.html
new file mode 100644
index 0000000000..b0b49d5f12
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-redirects.html
@@ -0,0 +1,34 @@
+<!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/helper.sub.js"></script>
+<script src="../resources/inflight-fetch-helper.js"></script>
+<script>
+// Check whether the page is BFCached when there are in-flight network requests
+// at the time of navigation.
+
+// Redirects and CSP.
+runTest(
+ '/common/slow-redirect.py?delay=2&location=' +
+ encodeURIComponent(sameOriginUrl),
+ false, true,
+ 'Redirect header received when in BFCache');
+runTest(
+ '/common/slow-redirect.py?delay=2&location=' +
+ encodeURIComponent(sameOriginUrl),
+ true, true,
+ 'Redirect header received when in BFCache w/ CSP passing');
+runTest(
+ '/common/slow-redirect.py?delay=2&location=' +
+ encodeURIComponent(crossSiteUrl + '?cors=yes'),
+ false, true,
+ 'Cross-origin redirect header received when in BFCache');
+runTest(
+ '/common/slow-redirect.py?delay=2&location=' +
+ encodeURIComponent(crossSiteUrl + '?cors=yes'),
+ true, false,
+ 'Cross-origin redirect header received when in BFCache w/ CSP failing');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/shared-worker.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/shared-worker.html
new file mode 100644
index 0000000000..77139fd08a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/shared-worker.html
@@ -0,0 +1,25 @@
+<!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/helper.sub.js"></script>
+<script>
+// Check whether the page is BFCached when there are shared workers that are
+// already loaded.
+runBfcacheTest({
+ funcBeforeNavigation: async () => {
+ globalThis.worker = new SharedWorker('../resources/echo-worker.js');
+ // Make sure the worker starts before navigation.
+ await WorkerHelper.pingWorker(globalThis.worker);
+ },
+ funcAfterAssertion: async (pageA) => {
+ // Confirm that the worker is still there.
+ assert_equals(
+ await pageA.execute_script(() => WorkerHelper.pingWorker(globalThis.worker)),
+ 'PASS',
+ 'SharedWorker should still work after restored from BFCache');
+ }
+}, 'Eligibility: shared workers');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/events.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/events.html
new file mode 100644
index 0000000000..4b1d3e408e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/events.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/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/helper.sub.js"></script>
+<script>
+// Basic event tests.
+runEventTest(
+ {targetOrigin: originSameOrigin},
+ 'SameOrigin');
+
+runEventTest(
+ {targetOrigin: originSameSite},
+ 'SameSite');
+
+runEventTest(
+ {},
+ 'CrossSite');
+
+// beforeunload.
+runEventTest({
+ events: ['pagehide', 'pageshow', 'load', 'beforeunload'],
+ expectedEvents: [
+ 'window.load',
+ 'window.pageshow',
+ 'window.beforeunload',
+ 'window.pagehide.persisted',
+ 'window.pageshow.persisted'
+ ]},
+ 'beforeunload');
+
+// unload.
+runEventTest({
+ events: ['pagehide', 'pageshow', 'load', 'unload'],
+ expectedEvents: [
+ 'window.load',
+ 'window.pageshow',
+ 'window.pagehide.persisted',
+ 'window.pageshow.persisted'
+ ]},
+ 'unload');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/focus.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/focus.html
new file mode 100644
index 0000000000..3901a5417d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/focus.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<meta name="timeout" content="long">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#focused-area-of-the-document">
+<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/helper.sub.js"></script>
+<script>
+// Focus should remain the same and thus blur/focus events shouldn't be fired
+// when page gets into and out of BFCache, as explicitly noted in the spec:
+// https://html.spec.whatwg.org/multipage/interaction.html#focused-area-of-the-document
+// "Even if a document is not fully active and not shown to the user, it can still
+// have a focused area of the document. If a document's fully active state changes,
+// its focused area of the document will stay the same."
+runBfcacheTest({
+ openFunc: (url) => window.open(url + '&events=pagehide,pageshow,load',
+ '_blank', 'noopener'),
+ funcBeforeNavigation: () => {
+ // Create and focus on an <input> before navigation.
+ // Focus/blur events on the <input> are recorded.
+ const textInput = document.createElement('input');
+ textInput.setAttribute('type', 'text');
+ textInput.setAttribute('id', 'toBeFocused');
+ textInput.onfocus = () => {
+ recordEvent('input.focus');
+ };
+ textInput.onblur = () => {
+ recordEvent('input.blur');
+ };
+ document.body.appendChild(textInput);
+ textInput.focus();
+ window.activeElementBeforePageHide = document.activeElement;
+ window.addEventListener('pagehide', () => {
+ window.activeElementOnPageHide = document.activeElement;
+ });
+ },
+ funcAfterAssertion: async (pageA) => {
+ assert_true(
+ await pageA.execute_script(() => {
+ return window.activeElementBeforePageHide ===
+ document.querySelector('#toBeFocused');
+ }),
+ 'activeElement before pagehide');
+
+ assert_true(
+ await pageA.execute_script(() => {
+ return window.activeElementOnPageHide ===
+ document.querySelector('#toBeFocused');
+ }),
+ 'activeElement on pagehide');
+
+ assert_true(
+ await pageA.execute_script(() => {
+ return document.activeElement ===
+ document.querySelector('#toBeFocused');
+ }),
+ 'activeElement after navigation');
+
+ assert_array_equals(
+ await pageA.execute_script(() => getRecordedEvents()),
+ [
+ 'window.load',
+ 'window.pageshow',
+ 'input.focus',
+ 'window.pagehide.persisted',
+ 'window.pageshow.persisted'
+ ],
+ 'blur/focus events should not be fired ' +
+ 'when page gets into and out of BFCache');
+ }
+}, 'Focus should be kept when page gets into and out of BFCache');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/pushstate.https.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/pushstate.https.html
new file mode 100644
index 0000000000..218562254a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/pushstate.https.html
@@ -0,0 +1,63 @@
+<!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/helper.sub.js"></script>
+<script>
+// Tests what happens when doing history navigation to an entry that's created
+// via pushState, with and without BFCache:
+// 1. Navigate to `urlA`.
+// 2. `pushState(urlPushState)`.
+// 3. Navigate to `urlB`.
+// 4. Do a back navigation.
+// With BFCache, the page loaded at Step 1 is restored from BFCache,
+// and is not reloaded from `urlPushState` nor `urlA`.
+// Without BFCache, a page is loaded from `urlPushState`, not from `urlA`.
+// In both cases, `location` and `history.state` are set to those set by
+// `pushState()` in Step 2.
+// See https://github.com/whatwg/html/issues/6207 for more discussion on the
+// specified, implemented and desired behaviors. While this test contradicts
+// the current spec but matches the desired behavior, and the spec will be
+// fixed as part of https://github.com/whatwg/html/pull/6315.
+for (const bfcacheDisabled of [false, true]) {
+ const pushStateExecutorPath =
+ '/html/browsers/browsing-the-web/back-forward-cache/resources/executor-pushstate.html';
+
+ runBfcacheTest({
+ funcBeforeNavigation: async (bfcacheDisabled, pushStateExecutorPath) => {
+ const urlPushState = new URL(location.href);
+ urlPushState.pathname = pushStateExecutorPath;
+ if (bfcacheDisabled) {
+ await disableBFCache();
+ }
+
+ // `pushState(..., urlPushState)` on `urlA`,
+ history.pushState('blue', '', urlPushState.href);
+ },
+ argsBeforeNavigation: [bfcacheDisabled, pushStateExecutorPath],
+ shouldBeCached: !bfcacheDisabled,
+ funcAfterAssertion: async (pageA) => {
+ // We've navigated to `urlB` and back again
+ // (already done within `runBfcacheTest()`).
+ // After the back navigation, `location` etc. should point to
+ // `urlPushState` and the state that's pushed.
+ const urlPushState = location.origin + pushStateExecutorPath +
+ '?uuid=' + pageA.context_id;
+ assert_equals(await pageA.execute_script(() => location.href),
+ urlPushState, 'url');
+ assert_equals(await pageA.execute_script(() => history.state),
+ 'blue', 'history.state');
+
+ if (bfcacheDisabled) {
+ // When the page is not restored from BFCache, the HTML page is loaded
+ // from `urlPushState` (not from `urlA`).
+ assert_true(await pageA.execute_script(() => isLoadedFromPushState),
+ 'document should be loaded from urlPushState');
+ }
+ }
+ }, 'back navigation to pushState()d page (' +
+ (bfcacheDisabled ? 'not ' : '') + 'in BFCache)');
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/echo-worker.js b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/echo-worker.js
new file mode 100644
index 0000000000..3e3ecb52e9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/echo-worker.js
@@ -0,0 +1,16 @@
+// On receiving a message from the parent Document, send back a message to the
+// parent Document. This is used to wait for worker initialization and test
+// that this worker is alive and working.
+
+// For dedicated workers.
+self.addEventListener('message', event => {
+ postMessage(event.data);
+});
+
+// For shared workers.
+onconnect = e => {
+ const port = e.ports[0];
+ port.onmessage = event => {
+ port.postMessage(event.data);
+ }
+};
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/event-recorder.js b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/event-recorder.js
new file mode 100644
index 0000000000..469286a399
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/event-recorder.js
@@ -0,0 +1,54 @@
+// Recording events
+
+const params = new URLSearchParams(window.location.search);
+const uuid = params.get('uuid');
+
+// The recorded events are stored in localStorage rather than global variables
+// to catch events fired just before navigating out.
+function getPushedItems(key) {
+ return JSON.parse(localStorage.getItem(key) || '[]');
+}
+
+function pushItem(key, value) {
+ const array = getPushedItems(key);
+ array.push(value);
+ localStorage.setItem(key, JSON.stringify(array));
+}
+
+window.recordEvent = function(eventName) {
+ pushItem(uuid + '.observedEvents', eventName);
+}
+
+window.getRecordedEvents = function() {
+ return getPushedItems(uuid + '.observedEvents');
+}
+
+// Records events fired on `window` and `document`, with names listed in
+// `eventNames`.
+function startRecordingEvents(eventNames) {
+ for (const eventName of eventNames) {
+ window.addEventListener(eventName, event => {
+ let result = eventName;
+ if (event.persisted) {
+ result += '.persisted';
+ }
+ if (eventName === 'visibilitychange') {
+ result += '.' + document.visibilityState;
+ }
+ recordEvent('window.' + result);
+ });
+ document.addEventListener(eventName, () => {
+ let result = eventName;
+ if (eventName === 'visibilitychange') {
+ result += '.' + document.visibilityState;
+ }
+ recordEvent('document.' + result);
+ });
+ }
+}
+
+// When a comma-separated list of event names are given as the `events`
+// parameter in the URL, start record the events of the given names.
+if (params.get('events')) {
+ startRecordingEvents(params.get('events').split(','));
+}
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor-pushstate.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor-pushstate.html
new file mode 100644
index 0000000000..dcf4a798d0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor-pushstate.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="event-recorder.js" type="module"></script>
+<script src="worker-helper.js" type="module"></script>
+<script type="module">
+// This is mostly the same as `executor.html`, except for
+// `isLoadedFromPushState` is set here, in order to detect whether the page
+// was loaded from `executor.html` or `executor-pushstate.html`.
+// Full executor functionality is still needed to handle remote script
+// execution requests etc.
+window.isLoadedFromPushState = true;
+</script>
+<script src="executor.js" type="module"></script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor.html
new file mode 100644
index 0000000000..2d118bbe2b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor.html
@@ -0,0 +1,5 @@
+<!DOCTYPE HTML>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="event-recorder.js" type="module"></script>
+<script src="worker-helper.js" type="module"></script>
+<script src="executor.js" type="module"></script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor.js b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor.js
new file mode 100644
index 0000000000..67ce068130
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor.js
@@ -0,0 +1,64 @@
+const params = new URLSearchParams(window.location.search);
+const uuid = params.get('uuid');
+
+// Executor and BFCache detection
+
+// When navigating out from this page, always call
+// `prepareNavigation(callback)` synchronously from the script injected by
+// `RemoteContext.execute_script()`, and trigger navigation on or after the
+// callback is called.
+// prepareNavigation() suspends task polling and avoid in-flight fetch
+// requests during navigation that might evict the page from BFCache.
+//
+// When we navigate to the page again, task polling is resumed, either
+// - (BFCache cases) when the pageshow event listener added by
+// prepareNavigation() is executed, or
+// - (Non-BFCache cases) when `Executor.execute()` is called again during
+// non-BFCache page loading.
+//
+// In such scenarios, `assert_bfcached()` etc. in `helper.sub.js` can determine
+// whether the page is restored from BFCache or not, by observing
+// - `isPageshowFired`: whether the pageshow event listener added by the
+// prepareNavigation() before navigating out, and
+// - `loadCount`: whether this inline script is evaluated again.
+// - `isPageshowPersisted` is used to assert that `event.persisted` is true
+// when restored from BFCache.
+
+window.isPageshowFired = false;
+window.isPageshowPersisted = null;
+window.loadCount = parseInt(localStorage.getItem(uuid + '.loadCount') || '0') + 1;
+localStorage.setItem(uuid + '.loadCount', loadCount);
+
+window.pageShowPromise = new Promise(resolve =>
+ window.addEventListener('pageshow', resolve, {once: true}));
+
+const executor = new Executor(uuid);
+
+window.prepareNavigation = function(callback) {
+ window.addEventListener(
+ 'pageshow',
+ (event) => {
+ window.isPageshowFired = true;
+ window.isPageshowPersisted = event.persisted;
+ executor.resume();
+ },
+ {once: true});
+ executor.suspend(callback);
+}
+
+// Try to disable BFCache by acquiring and never releasing a Web Lock.
+// This requires HTTPS.
+// Note: This is a workaround depending on non-specified WebLock+BFCache
+// behavior, and doesn't work on Safari. We might want to introduce a
+// test-only BFCache-disabling API instead in the future.
+// https://github.com/web-platform-tests/wpt/issues/16359#issuecomment-795004780
+// https://crbug.com/1298336
+window.disableBFCache = () => {
+ return new Promise(resolve => {
+ // Use page's UUID as a unique lock name.
+ navigator.locks.request(uuid, () => {
+ resolve();
+ return new Promise(() => {});
+ });
+ });
+};
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js
new file mode 100644
index 0000000000..a1d18d108e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js
@@ -0,0 +1,229 @@
+// Helpers called on the main test HTMLs.
+// Functions in `RemoteContext.execute_script()`'s 1st argument are evaluated
+// on the executors (`executor.html`), and helpers available on the executors
+// are defined in `executor.html`.
+
+const originSameOrigin =
+ location.protocol === 'http:' ?
+ 'http://{{host}}:{{ports[http][0]}}' :
+ 'https://{{host}}:{{ports[https][0]}}';
+const originSameSite =
+ location.protocol === 'http:' ?
+ 'http://{{host}}:{{ports[http][1]}}' :
+ 'https://{{host}}:{{ports[https][1]}}';
+const originCrossSite =
+ location.protocol === 'http:' ?
+ 'http://{{hosts[alt][www]}}:{{ports[http][0]}}' :
+ 'https://{{hosts[alt][www]}}:{{ports[https][0]}}';
+
+const executorPath =
+ '/html/browsers/browsing-the-web/back-forward-cache/resources/executor.html?uuid=';
+
+// Asserts that the executor `target` is (or isn't, respectively)
+// restored from BFCache. These should be used in the following fashion:
+// 1. Call prepareNavigation() on the executor `target`.
+// 2. Navigate the executor to another page.
+// 3. Navigate back to the executor `target`.
+// 4. Call assert_bfcached() or assert_not_bfcached() on the main test HTML.
+async function assert_bfcached(target) {
+ const status = await getBFCachedStatus(target);
+ assert_implements_optional(status === 'BFCached',
+ "Could have been BFCached but actually wasn't");
+}
+
+async function assert_not_bfcached(target) {
+ const status = await getBFCachedStatus(target);
+ assert_implements(status !== 'BFCached',
+ 'Should not be BFCached but actually was');
+}
+
+async function getBFCachedStatus(target) {
+ const [loadCount, isPageshowFired, isPageshowPersisted] =
+ await target.execute_script(() => [
+ window.loadCount, window.isPageshowFired, window.isPageshowPersisted]);
+
+ if (loadCount === 1 && isPageshowFired === true &&
+ isPageshowPersisted === true) {
+ return 'BFCached';
+ } else if (loadCount === 2 && isPageshowFired === false) {
+ return 'Not BFCached';
+ } else {
+ // This can occur for example when this is called before first navigating
+ // away (loadCount = 1, isPageshowFired = false), e.g. when
+ // 1. sending a script for navigation and then
+ // 2. calling getBFCachedStatus() without waiting for the completion of
+ // the script on the `target` page.
+ assert_unreached(
+ `Got unexpected BFCache status: loadCount = ${loadCount}, ` +
+ `isPageshowFired = ${isPageshowFired}, ` +
+ `isPageshowPersisted = ${isPageshowPersisted}`);
+ }
+}
+
+// Always call `await remoteContext.execute_script(waitForPageShow);` after
+// triggering to navigation to the page, to wait for pageshow event on the
+// remote context.
+const waitForPageShow = () => window.pageShowPromise;
+
+// Run a test that navigates A->B->A:
+// 1. Page A is opened by `params.openFunc(url)`.
+// 2. `params.funcBeforeNavigation(params.argsBeforeNavigation)` is executed
+// on page A.
+// 3. The window is navigated to page B on `params.targetOrigin`.
+// 4. The window is back navigated to page A (expecting BFCached).
+//
+// Events `params.events` (an array of strings) are observed on page A and
+// `params.expectedEvents` (an array of strings) is expected to be recorded.
+// See `event-recorder.js` for event recording.
+//
+// Parameters can be omitted. See `defaultParams` below for default.
+function runEventTest(params, description) {
+ const defaultParams = {
+ openFunc(url) {
+ window.open(
+ `${url}&events=${this.events.join(',')}`,
+ '_blank',
+ 'noopener'
+ )
+ },
+ events: ['pagehide', 'pageshow', 'load'],
+ expectedEvents: [
+ 'window.load',
+ 'window.pageshow',
+ 'window.pagehide.persisted',
+ 'window.pageshow.persisted'
+ ],
+ async funcAfterAssertion(pageA) {
+ assert_array_equals(
+ await pageA.execute_script(() => getRecordedEvents()),
+ this.expectedEvents);
+ }
+ }
+ // Apply defaults.
+ params = { ...defaultParams, ...params };
+
+ runBfcacheTest(params, description);
+}
+
+async function navigateAndThenBack(pageA, pageB, urlB,
+ funcBeforeBackNavigation,
+ argsBeforeBackNavigation) {
+ await pageA.execute_script(
+ (url) => {
+ prepareNavigation(() => {
+ location.href = url;
+ });
+ },
+ [urlB]
+ );
+
+ await pageB.execute_script(waitForPageShow);
+ if (funcBeforeBackNavigation) {
+ await pageB.execute_script(funcBeforeBackNavigation,
+ argsBeforeBackNavigation);
+ }
+ await pageB.execute_script(
+ () => {
+ prepareNavigation(() => { history.back(); });
+ }
+ );
+
+ await pageA.execute_script(waitForPageShow);
+}
+
+function runBfcacheTest(params, description) {
+ const defaultParams = {
+ openFunc: url => window.open(url, '_blank', 'noopener'),
+ scripts: [],
+ funcBeforeNavigation: () => {},
+ argsBeforeNavigation: [],
+ targetOrigin: originCrossSite,
+ funcBeforeBackNavigation: () => {},
+ argsBeforeBackNavigation: [],
+ shouldBeCached: true,
+ funcAfterAssertion: () => {},
+ }
+ // Apply defaults.
+ params = {...defaultParams, ...params };
+
+ promise_test(async t => {
+ const pageA = new RemoteContext(token());
+ const pageB = new RemoteContext(token());
+
+ const urlA = executorPath + pageA.context_id;
+ const urlB = params.targetOrigin + executorPath + pageB.context_id;
+
+ // So that tests can refer to these URLs for assertions if necessary.
+ pageA.url = originSameOrigin + urlA;
+ pageB.url = urlB;
+
+ params.openFunc(urlA);
+
+ await pageA.execute_script(waitForPageShow);
+
+ for (const src of params.scripts) {
+ await pageA.execute_script((src) => {
+ const script = document.createElement("script");
+ script.src = src;
+ document.head.append(script);
+ return new Promise(resolve => script.onload = resolve);
+ }, [src]);
+ }
+
+ await pageA.execute_script(params.funcBeforeNavigation,
+ params.argsBeforeNavigation);
+ await navigateAndThenBack(pageA, pageB, urlB,
+ params.funcBeforeBackNavigation,
+ params.argsBeforeBackNavigation);
+
+ if (params.shouldBeCached) {
+ await assert_bfcached(pageA);
+ } else {
+ await assert_not_bfcached(pageA);
+ }
+
+ if (params.funcAfterAssertion) {
+ await params.funcAfterAssertion(pageA, pageB, t);
+ }
+ }, description);
+}
+
+// Call clients.claim() on the service worker
+async function claim(t, worker) {
+ const channel = new MessageChannel();
+ const saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS', 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+ worker.postMessage({type: "claim", port: channel.port2}, [channel.port2]);
+ await saw_message;
+}
+
+// Assigns the current client to a local variable on the service worker.
+async function storeClients(t, worker) {
+ const channel = new MessageChannel();
+ const saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS', 'storeClients');
+ resolve();
+ });
+ });
+ worker.postMessage({type: "storeClients", port: channel.port2}, [channel.port2]);
+ await saw_message;
+}
+
+// Call storedClients.postMessage("") on the service worker
+async function postMessageToStoredClients(t, worker) {
+ const channel = new MessageChannel();
+ const saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS', 'postMessageToStoredClients');
+ resolve();
+ });
+ });
+ worker.postMessage({type: "postMessageToStoredClients",
+ port: channel.port2}, [channel.port2]);
+ await saw_message;
+}
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/inflight-fetch-helper.js b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/inflight-fetch-helper.js
new file mode 100644
index 0000000000..7832003b76
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/inflight-fetch-helper.js
@@ -0,0 +1,47 @@
+// Delay after fetch start:
+// - 0.0 seconds: before BFCache
+// - 2.0 seconds: when in BFCache
+// - 3.5 seconds: after restored from BFCache
+function runTest(urlToFetch, hasCSP, shouldSucceed, description) {
+ runBfcacheTest({
+ funcBeforeNavigation: async (urlToFetch, hasCSP) => {
+ if (hasCSP) {
+ // Set CSP.
+ const meta = document.createElement('meta');
+ meta.setAttribute('http-equiv', 'Content-Security-Policy');
+ meta.setAttribute('content', "connect-src 'self'");
+ document.head.appendChild(meta);
+ }
+
+ // Initiate a `fetch()`.
+ window.fetchPromise = fetch(urlToFetch);
+
+ // Wait for 0.5 seconds to receive response headers for the fetch()
+ // before BFCache, if any.
+ await new Promise(resolve => setTimeout(resolve, 500));
+ },
+ argsBeforeNavigation: [urlToFetch, hasCSP],
+ funcBeforeBackNavigation: () => {
+ // Wait for 2 seconds before back navigating to pageA.
+ return new Promise(resolve => setTimeout(resolve, 2000));
+ },
+ funcAfterAssertion: async (pageA, pageB, t) => {
+ // Wait for fetch() completion and check the result.
+ const result = pageA.execute_script(
+ () => window.fetchPromise.then(r => r.text()));
+ if (shouldSucceed) {
+ assert_equals(
+ await result,
+ 'Body',
+ 'Fetch should complete successfully after restored from BFCache');
+ } else {
+ await promise_rejects_js(t, TypeError, result,
+ 'Fetch should fail after restored from BFCache');
+ }
+ }
+ }, 'Eligibility (in-flight fetch): ' + description);
+}
+
+const url = new URL('../resources/slow.py', location);
+const sameOriginUrl = url.href;
+const crossSiteUrl = originCrossSite + url.pathname;
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js
new file mode 100644
index 0000000000..80c164f560
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js
@@ -0,0 +1,137 @@
+// A collection of helper functions that make use of the `remoteContextHelper`
+// to test BFCache support and behavior.
+
+// Call `prepareForBFCache()` before navigating away from the page. This simply
+// sets a variable in window.
+async function prepareForBFCache(remoteContextHelper) {
+ await remoteContextHelper.executeScript(() => {
+ window.beforeBFCache = true;
+ });
+}
+
+// Call `getBeforeCache()` after navigating back to the page. This returns the
+// value in window.
+async function getBeforeBFCache(remoteContextHelper) {
+ return await remoteContextHelper.executeScript(() => {
+ return window.beforeBFCache;
+ });
+}
+
+// If the value in window is set to true, this means that the page was reloaded,
+// i.e., the page was restored from BFCache.
+// Call `prepareForBFCache()` before navigating away to call this function.
+async function assertImplementsBFCacheOptional(remoteContextHelper) {
+ var beforeBFCache = await getBeforeBFCache(remoteContextHelper);
+ assert_implements_optional(beforeBFCache == true, 'BFCache not supported.');
+}
+
+// Subtracts set `b` from set `a` and returns the result.
+function setMinus(a, b) {
+ const minus = new Set();
+ a.forEach(e => {
+ if (!b.has(e)) {
+ minus.add(e);
+ }
+ });
+ return minus;
+}
+
+// Return a sorted Array from the iterable `s`.
+function sorted(s) {
+ return Array.from(s).sort();
+}
+
+// Assert expected reasons and the reported reasons match.
+function matchReasons(expectedNotRestoredReasonsSet, notRestoredReasonsSet) {
+ const missing = setMinus(
+ expectedNotRestoredReasonsSet, notRestoredReasonsSet, 'Missing reasons');
+ const extra = setMinus(
+ notRestoredReasonsSet, expectedNotRestoredReasonsSet, 'Extra reasons');
+ assert_true(missing.size + extra.size == 0, `Expected: ${sorted(expectedNotRestoredReasonsSet)}\n` +
+ `Got: ${sorted(notRestoredReasonsSet)}\n` +
+ `Missing: ${sorted(missing)}\n` +
+ `Extra: ${sorted(extra)}\n`);
+}
+
+// This function takes a set of reasons and extracts reasons out of it and returns a set of strings.
+// For example, if the input is [{"reason": "error-document"}, {"reason": "masked"}],
+// the output is ["error-document", "masked"].
+function extractReason(reasonSet) {
+ let reasonsExtracted = new Set();
+ for (let reason of reasonSet) {
+ reasonsExtracted.add(reason.reason);
+ }
+ return reasonsExtracted;
+}
+
+// A helper function to assert that the page is not restored from BFCache by
+// checking whether the `beforeBFCache` value from `window` is undefined
+// due to page reload.
+// This function also takes an optional `notRestoredReasons` list which
+// indicates the set of expected reasons that make the page not restored.
+// If the reasons list is undefined, the check will be skipped. Otherwise
+// this check will use the `notRestoredReasons` API, to obtain the reasons
+// in a tree structure, and flatten the reasons before making the order-
+// insensitive comparison.
+// If the API is not available, the function will terminate instead of marking
+// the assertion failed.
+// Call `prepareForBFCache()` before navigating away to call this function.
+async function assertNotRestoredFromBFCache(
+ remoteContextHelper, notRestoredReasons) {
+ var beforeBFCache = await getBeforeBFCache(remoteContextHelper);
+ assert_equals(beforeBFCache, undefined, 'document unexpectedly BFCached');
+
+ // The reason is optional, so skip the remaining test if the
+ // `notRestoredReasons` is not set.
+ if (notRestoredReasons === undefined) {
+ return;
+ }
+
+ let isFeatureEnabled = await remoteContextHelper.executeScript(() => {
+ return 'notRestoredReasons' in
+ performance.getEntriesByType('navigation')[0];
+ });
+
+ // Return if the `notRestoredReasons` API is not available.
+ if (!isFeatureEnabled) {
+ return;
+ }
+
+ let result = await remoteContextHelper.executeScript(() => {
+ return performance.getEntriesByType('navigation')[0].notRestoredReasons;
+ });
+
+ let expectedNotRestoredReasonsSet = new Set(notRestoredReasons);
+ let notRestoredReasonsSet = new Set();
+
+ // Flatten the reasons from the main frame and all the child frames.
+ const collectReason = (node) => {
+ for (let reason of node.reasons) {
+ notRestoredReasonsSet.add(reason.reason);
+ }
+ for (let child of node.children) {
+ collectReason(child);
+ }
+ };
+ collectReason(result);
+ matchReasons(expectedNotRestoredReasonsSet, notRestoredReasonsSet);
+}
+
+// A helper function that combines the steps of setting window property,
+// navigating away and back, and making assertion on whether BFCache is
+// supported.
+// This function can be used to check if the current page is eligible for
+// BFCache.
+async function assertBFCacheEligibility(
+ remoteContextHelper, shouldRestoreFromBFCache) {
+ await prepareForBFCache(remoteContextHelper);
+ // Navigate away and back.
+ const newRemoteContextHelper = await remoteContextHelper.navigateToNew();
+ await newRemoteContextHelper.historyBack();
+
+ if (shouldRestoreFromBFCache) {
+ await assertImplementsBFCacheOptional(remoteContextHelper);
+ } else {
+ await assertNotRestoredFromBFCache(remoteContextHelper);
+ }
+}
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/service-worker.js b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/service-worker.js
new file mode 100644
index 0000000000..ab9a3239ea
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/service-worker.js
@@ -0,0 +1,58 @@
+self.addEventListener('message', function(event) {
+ if (event.data.type == "claim") {
+ self.clients.claim()
+ .then(function(result) {
+ if (result !== undefined) {
+ event.data.port.postMessage(
+ 'FAIL: claim() should be resolved with undefined');
+ return;
+ }
+ event.data.port.postMessage('PASS');
+ })
+ .catch(function(error) {
+ event.data.port.postMessage('FAIL: exception: ' + error.name);
+ });
+ } else if (event.data.type == "storeClients") {
+ self.clients.matchAll()
+ .then(function(result) {
+ self.storedClients = result;
+ event.data.port.postMessage("PASS");
+ });
+ } else if (event.data.type == "postMessageToStoredClients") {
+ for (let client of self.storedClients) {
+ client.postMessage("dummyValue");
+ }
+ event.data.port.postMessage("PASS");
+ } else if (event.data.type == 'storeMessagePort') {
+ let isCloseEventFired = false;
+ const port = event.ports[0];
+ port.start();
+ port.onmessage = (event) => {
+ if (event.data == 'Confirm the ports can communicate') {
+ port.postMessage('Receive message');
+ } else if (event.data == 'Ask if the close event was fired') {
+ port.postMessage(isCloseEventFired);
+ }
+ };
+ port.onclose = () => {
+ isCloseEventFired = true;
+ };
+ }
+ });
+
+self.addEventListener('fetch', e => {
+ if (e.request.url.match(/\/is-controlled/)) {
+ e.respondWith(new Response('controlled'));
+ }
+ else if (e.request.url.match(/\/get-clients-matchall/)) {
+ const options = { includeUncontrolled: true, type: 'all' };
+ e.respondWith(
+ self.clients.matchAll(options)
+ .then(clients => {
+ const client_urls = [];
+ clients.forEach(client => client_urls.push(client.url));
+ return new Response(JSON.stringify(client_urls));
+ })
+ );
+ }
+ });
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/slow.py b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/slow.py
new file mode 100644
index 0000000000..01bb3309b1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/slow.py
@@ -0,0 +1,13 @@
+import time
+
+def main(request, response):
+ delay_before_header = float(request.GET.first(b"delayBeforeHeader", 0)) / 1000
+ delay_before_body = float(request.GET.first(b"delayBeforeBody", 0)) / 1000
+
+ time.sleep(delay_before_header)
+ if b"cors" in request.GET:
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+ response.write_status_headers()
+
+ time.sleep(delay_before_body)
+ response.writer.write_content(b"Body")
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/worker-helper.js b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/worker-helper.js
new file mode 100644
index 0000000000..d5f3a0c814
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/worker-helper.js
@@ -0,0 +1,28 @@
+// Worker-related helper file to be used from executor.html.
+
+// The class `WorkerHelper` is exposed to `globalThis` because this should be
+// used via `eval()`.
+globalThis.WorkerHelper = class {
+ static pingWorker(worker) {
+ return new Promise((resolve, reject) => {
+ const message = 'message ' + Math.random();
+ const onmessage = e => {
+ if (e.data === message) {
+ resolve('PASS');
+ } else {
+ reject('pingWorker: expected ' + message + ' but got ' + e.data);
+ }
+ };
+ worker.onerror = reject;
+ if (worker instanceof Worker) {
+ worker.addEventListener('message', onmessage, {once: true});
+ worker.postMessage(message);
+ } else if (worker instanceof SharedWorker) {
+ worker.port.onmessage = onmessage;
+ worker.port.postMessage(message);
+ } else {
+ reject('Unexpected worker type');
+ }
+ });
+ }
+};
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-client-postmessage.https.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-client-postmessage.https.html
new file mode 100644
index 0000000000..acc682a073
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-client-postmessage.https.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/helper.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+// When a service worker is unregistered when a controlled page is in BFCache,
+// the page can be still restored from BFCache and remain controlled by the
+// service worker.
+promise_test(async t => {
+ // Register a service worker and make this page controlled.
+ const workerUrl =
+ 'resources/service-worker.js?pipe=header(Service-Worker-Allowed,../)';
+ const registration =
+ await service_worker_unregister_and_register(t, workerUrl, './');
+ t.add_cleanup(_ => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const controllerChanged = new Promise(
+ resolve => navigator.serviceWorker.oncontrollerchange = resolve);
+ await claim(t, registration.active);
+ await controllerChanged;
+
+ const pageA = new RemoteContext(token());
+ const pageB = new RemoteContext(token());
+
+ const urlA = location.origin + executorPath + pageA.context_id;
+ const urlB = originCrossSite + executorPath + pageB.context_id;
+
+ // Open `urlA`.
+ window.open(urlA, '_blank', 'noopener');
+ await pageA.execute_script(waitForPageShow);
+
+ assert_true(
+ await pageA.execute_script(
+ () => (navigator.serviceWorker.controller !== null)),
+ 'pageA should be controlled before navigation');
+
+ await storeClients(t, registration.active);
+
+ // Navigate to `urlB`.
+ await pageA.execute_script(
+ (url) => prepareNavigation(() => {
+ location.href = url;
+ }),
+ [urlB]);
+ await pageB.execute_script(waitForPageShow);
+
+ // Posting a message to a client should evict it from the bfcache.
+ await postMessageToStoredClients(t, registration.active);
+
+ // Back navigate and check whether the page is restored from BFCache.
+ await pageB.execute_script(
+ () => {
+ prepareNavigation(() => { history.back(); });
+ }
+ );
+ await pageA.execute_script(waitForPageShow);
+ await assert_not_bfcached(pageA);
+
+ await pageA.execute_script(() => navigator.serviceWorker.ready);
+
+ assert_true(
+ await pageA.execute_script(
+ () => (navigator.serviceWorker.controller !== null)),
+ 'pageA should be controlled after history navigation');
+
+}, 'Client.postMessage while a controlled page is in BFCache');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-claim.https.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-claim.https.html
new file mode 100644
index 0000000000..d9540c221b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-claim.https.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/helper.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+// Calling Clients.claim() on the service worker when a controlled page is in
+// BFCache should evict the page from BFCache, as per
+// https://github.com/w3c/ServiceWorker/issues/1038#issuecomment-291028845.
+promise_test(async t => {
+ const pageA = new RemoteContext(token());
+ const pageB = new RemoteContext(token());
+
+ const urlA = location.origin + executorPath + pageA.context_id;
+ const urlB = originCrossSite + executorPath + pageB.context_id;
+
+ window.open(urlA, '_blank', 'noopener');
+ await pageA.execute_script(waitForPageShow);
+
+ // Register a service worker after `pageA` is loaded to make `pageA`
+ // uncontrolled at this time.
+ const workerUrl =
+ 'resources/service-worker.js?pipe=header(Service-Worker-Allowed,../)';
+ const registration =
+ await service_worker_unregister_and_register(t, workerUrl, './');
+ t.add_cleanup(_ => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Navigate to `urlB`.
+ await pageA.execute_script(
+ (url) => {
+ prepareNavigation(() => { location.href = url; });
+ },
+ [urlB]);
+ await pageB.execute_script(waitForPageShow);
+
+ // Call Clients.claim() on the service worker when `pageA` is in BFCache.
+ const controllerChanged = new Promise(
+ resolve => navigator.serviceWorker.oncontrollerchange = resolve);
+ await claim(t, registration.active);
+ await controllerChanged;
+
+ // `pageA` doesn't appear in matchAll().
+ const clients1 = await (await fetch('/get-clients-matchall')).json();
+ assert_true(clients1.indexOf(urlA) < 0,
+ '1: matchAll() before back navigation');
+
+ // Back navigate and check that the page was evicted from BFCache.
+ await pageB.execute_script(
+ () => {
+ prepareNavigation(() => { history.back(); });
+ }
+ );
+ await pageA.execute_script(waitForPageShow);
+ await assert_not_bfcached(pageA);
+
+ // After back navigation, `pageA` appear in matchAll(), because it was newly
+ // loaded and controlled by the service worker.
+ const clients2 = await (await fetch('/get-clients-matchall')).json();
+ const controlled2 = await pageA.execute_script(
+ () => (navigator.serviceWorker.controller !== null));
+ assert_true(clients2.indexOf(urlA) >= 0,
+ '2: matchAll() just after back navigation');
+ assert_true(controlled2,
+ '2: pageA should be controlled just after back navigation');
+
+}, 'Clients.claim() evicts pages that would be affected from BFCache');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-matchall.https.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-matchall.https.html
new file mode 100644
index 0000000000..069529dbe4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-matchall.https.html
@@ -0,0 +1,76 @@
+<!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/helper.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+promise_test(async t => {
+ // Register a service worker and make this page controlled.
+ const workerUrl =
+ 'resources/service-worker.js?pipe=header(Service-Worker-Allowed,../)';
+ const registration =
+ await service_worker_unregister_and_register(t, workerUrl, './');
+ t.add_cleanup(_ => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const controllerChanged = new Promise(
+ resolve => navigator.serviceWorker.oncontrollerchange = resolve);
+ await claim(t, registration.active);
+ await controllerChanged;
+
+ const pageA = new RemoteContext(token());
+ const pageB = new RemoteContext(token());
+
+ const urlA = location.origin + executorPath + pageA.context_id;
+ const urlB = originCrossSite + executorPath + pageB.context_id;
+
+ // Open `urlA`.
+ window.open(urlA, '_blank', 'noopener');
+ await pageA.execute_script(waitForPageShow);
+
+ // Get Clients.matchAll() and check whether `pageA` is controlled.
+ // Actual `assert_*()` is called after `assert_bfcached()` below.
+ const clients1 = await (await fetch('/get-clients-matchall')).json();
+ const controlled1 = await pageA.execute_script(
+ () => (navigator.serviceWorker.controller !== null));
+
+ // Navigate to `urlB` and get Clients.matchAll() when `urlA` is in BFCache.
+ await pageA.execute_script(
+ (url) => prepareNavigation(() => {
+ location.href = url;
+ }),
+ [urlB]);
+ await pageB.execute_script(waitForPageShow);
+ const clients2 = await (await fetch('/get-clients-matchall')).json();
+
+ // Back navigate and check whether the page is restored from BFCache.
+ await pageB.execute_script(
+ () => {
+ prepareNavigation(() => { history.back(); });
+ }
+ );
+ await pageA.execute_script(waitForPageShow);
+ await assert_bfcached(pageA);
+
+ // Get Clients.matchAll() and check whether `pageA` is controlled.
+ const clients3 = await (await fetch('/get-clients-matchall')).json();
+ const controlled3 = await pageA.execute_script(
+ () => (navigator.serviceWorker.controller !== null));
+
+ // Clients.matchAll() should not list `urlA` when it is in BFCache.
+ assert_true(clients1.indexOf(urlA) >= 0,
+ '1: matchAll() before navigation');
+ assert_true(clients2.indexOf(urlA) < 0,
+ '2: matchAll() before back navigation');
+ assert_true(clients3.indexOf(urlA) >= 0,
+ '3: matchAll() after back navigation');
+
+ // `pageA` should be controlled before/after BFCached.
+ assert_true(controlled1,
+ 'pageA should be controlled before BFCached');
+ assert_true(controlled3,
+ 'pageA should be controlled after restored');
+}, 'Clients.matchAll() should not list pages in BFCache');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-controlled-after-restore.https.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-controlled-after-restore.https.html
new file mode 100644
index 0000000000..a937eb85ac
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-controlled-after-restore.https.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/helper.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+promise_test(async t => {
+ const pageA = new RemoteContext(token());
+ const pageB = new RemoteContext(token());
+
+ const urlA = location.origin + executorPath + pageA.context_id;
+ const urlB = originCrossSite + executorPath + pageB.context_id;
+
+ // Register a service worker.
+ const workerUrl =
+ 'resources/service-worker.js?pipe=header(Service-Worker-Allowed,../)';
+ const registration =
+ await service_worker_unregister_and_register(t, workerUrl, './');
+ t.add_cleanup(_ => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ window.open(urlA, '_blank', 'noopener');
+ await pageA.execute_script(waitForPageShow);
+
+ assert_true(
+ await pageA.execute_script(
+ () => (navigator.serviceWorker.controller !== null)),
+ 'pageA should be controlled before navigation');
+
+ await navigateAndThenBack(pageA, pageB, urlB);
+ await assert_bfcached(pageA);
+
+ assert_true(
+ await pageA.execute_script(
+ () => (navigator.serviceWorker.controller !== null)),
+ 'navigator.serviceWorker.controller should be non-null ' +
+ 'after restored from BFCache');
+
+ const isControlled = await pageA.execute_script(
+ () => fetch('/is-controlled').then(r => r.text()));
+
+ assert_true(
+ await pageA.execute_script(
+ () => (navigator.serviceWorker.controller !== null)),
+ 'navigator.serviceWorker.controller should be non-null ' +
+ 'after restored from BFCache and after fetch');
+
+ assert_equals(isControlled, 'controlled',
+ 'fetch should be intercepted after restored from BFCache');
+}, 'Pages should remain controlled after restored from BFCache');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-unregister.https.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-unregister.https.html
new file mode 100644
index 0000000000..1c3f81153c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-unregister.https.html
@@ -0,0 +1,67 @@
+<!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/helper.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+// When a service worker is unregistered when a controlled page is in BFCache,
+// the page can be still restored from BFCache and remain controlled by the
+// service worker.
+promise_test(async t => {
+ // Register a service worker and make this page controlled.
+ const workerUrl =
+ 'resources/service-worker.js?pipe=header(Service-Worker-Allowed,../)';
+ const registration =
+ await service_worker_unregister_and_register(t, workerUrl, './');
+ t.add_cleanup(_ => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const controllerChanged = new Promise(
+ resolve => navigator.serviceWorker.oncontrollerchange = resolve);
+ await claim(t, registration.active);
+ await controllerChanged;
+
+ const pageA = new RemoteContext(token());
+ const pageB = new RemoteContext(token());
+
+ const urlA = location.origin + executorPath + pageA.context_id;
+ const urlB = originCrossSite + executorPath + pageB.context_id;
+
+ // Open `urlA`.
+ window.open(urlA, '_blank', 'noopener');
+ await pageA.execute_script(waitForPageShow);
+
+ assert_true(
+ await pageA.execute_script(
+ () => (navigator.serviceWorker.controller !== null)),
+ 'pageA should be controlled before navigation');
+
+ // Navigate to `urlB`.
+ await pageA.execute_script(
+ (url) => prepareNavigation(() => {
+ location.href = url;
+ }),
+ [urlB]);
+ await pageB.execute_script(waitForPageShow);
+
+ // Unregister the service worker when the controlled `pageA` is in BFCache.
+ await registration.unregister();
+
+ // Back navigate and check whether the page is restored from BFCache.
+ await pageB.execute_script(
+ () => {
+ prepareNavigation(() => { history.back(); });
+ }
+ );
+ await pageA.execute_script(waitForPageShow);
+ await assert_not_bfcached(pageA);
+
+ assert_true(
+ await pageA.execute_script(
+ () => (navigator.serviceWorker.controller === null)),
+ 'pageA should not be controlled');
+
+}, 'Unregister service worker while a controlled page is in BFCache');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/storage-events.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/storage-events.html
new file mode 100644
index 0000000000..6957496c30
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/storage-events.html
@@ -0,0 +1,101 @@
+<!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/helper.sub.js"></script>
+<script>
+// When localStorage (`key1`) is modified when a page (`pageA`) is in BFCache,
+// storage events should not be fired for the page after becoming active.
+// https://github.com/whatwg/storage/issues/119#issuecomment-1115844532
+promise_test(async t => {
+ const pageA = new RemoteContext(token());
+ const pageB = new RemoteContext(token());
+ const pageC = new RemoteContext(token());
+
+ const urlA = executorPath + pageA.context_id + '&events=pagehide,pageshow,load';
+ const urlB = originCrossSite + executorPath + pageB.context_id;
+ const urlC = executorPath + pageC.context_id + '&events=pagehide,pageshow,load';
+
+ // localStorage key to set while pageA is in BFCache.
+ const key1 = token();
+ // localStorage key to set after pageA is restored from BFCache.
+ const key2 = token();
+
+ const startRecordingStorageEvent = (key1, key2) => {
+ window.key1EventFired = new Promise(resolve => {
+ window.addEventListener('storage', e => {
+ if (e.key === key1) {
+ recordEvent('storage1');
+ resolve();
+ }
+ });
+ });
+ window.key2EventFired = new Promise(resolve => {
+ window.addEventListener('storage', e => {
+ if (e.key === key2) {
+ recordEvent('storage2');
+ resolve();
+ }
+ });
+ });
+ };
+
+ window.open(urlA, '_blank', 'noopener');
+ await pageA.execute_script(waitForPageShow);
+ await pageA.execute_script(startRecordingStorageEvent, [key1, key2]);
+
+ // Window C is an unrelated window kept open without navigation, to confirm
+ // that storage events are fired as expected in non-BFCache-related scenario
+ // and not blocked due to non-BFCache-related reasons.
+ window.open(urlC, '_blank');
+ await pageC.execute_script(waitForPageShow);
+ await pageC.execute_script(startRecordingStorageEvent, [key1, key2]);
+
+ // Navigate A to B.
+ await pageA.execute_script((url) => {
+ prepareNavigation(() => {
+ location.href = url;
+ });
+ }, [urlB]);
+ await pageB.execute_script(waitForPageShow);
+
+ // Update `key1` while pageA is in BFCache.
+ localStorage.setItem(key1, 'value');
+
+ // Wait for a storage event is fired on PageC and a while,
+ // to prevent race conditions between event processing
+ // triggered by `setItem()` and the following operations.
+ await pageC.execute_script(() => window.key1EventFired);
+ await new Promise(resolve => t.step_timeout(resolve, 1000));
+
+ // Back navigate to pageA, to be restored from BFCache.
+ await pageB.execute_script(
+ () => {
+ prepareNavigation(() => { history.back(); });
+ }
+ );
+ await pageA.execute_script(waitForPageShow);
+ await assert_bfcached(pageA);
+
+ // Update `key2` after pageA is restored from BFCache.
+ localStorage.setItem(key2, 'value');
+
+ // Wait for a storage event for `key2` is fired on PageA.
+ await pageA.execute_script(() => window.key2EventFired);
+
+ // Confirm that a storage event for `key1` is not fired on PageA.
+ assert_array_equals(
+ await pageA.execute_script(() => getRecordedEvents()),
+ [
+ 'window.load',
+ 'window.pageshow',
+ 'window.pagehide.persisted',
+ 'window.pageshow.persisted',
+ 'storage2',
+ ],
+ 'pageA should not receive storage events for updates while in BFCache');
+
+}, 'Storage events should not be fired for BFCached pages after becoming active');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/timers.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/timers.html
new file mode 100644
index 0000000000..aab650f36e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/timers.html
@@ -0,0 +1,68 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers:fully-active">
+<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/helper.sub.js"></script>
+<script>
+// Timers should be paused when the Document is not fully active.
+// This test is checking this by measuring the actual elapsed time for a timer
+// started before a page is stored into BFCache, staying for a while in BFCache,
+// and fired after the page is restored from BFCache.
+
+const delayMain = 18000;
+const delayBeforeForwardNavigation = 6000;
+const delayBeforeBackNavigation = 5000;
+// `delayBeforeForwardNavigation` and `delayBeforeBackNavigation` are set
+// sufficiently large in order to distinguish the expected case from other
+// scenarios listed in `funcAfterAssertion()`, and to allow some delays outside
+// timers (e.g. due to communication between Windows). The additional delays
+// can be large (e.g. ~4 seconds), so the delays above should be sufficiently
+// large.
+
+const startTime = performance.now();
+
+runBfcacheTest({
+ funcBeforeNavigation: async (delayMain, delayBeforeForwardNavigation) => {
+ // Set `promiseMainTimer` that is resolved after a timeout of `delayMain`
+ // ms.
+ window.promiseMainTimer = new Promise(resolve => {
+ setTimeout(resolve, delayMain);
+ });
+ // Then navigate to another page after `delayBeforeForwardNavigation` ms.
+ await new Promise(resolve =>
+ setTimeout(resolve, delayBeforeForwardNavigation));
+ },
+ argsBeforeNavigation: [delayMain, delayBeforeForwardNavigation],
+ funcBeforeBackNavigation: async (delayBeforeBackNavigation) => {
+ // Back navigate after `delayBeforeBackNavigation` ms.
+ await new Promise(resolve =>
+ setTimeout(resolve, delayBeforeBackNavigation));
+ },
+ argsBeforeBackNavigation: [delayBeforeBackNavigation],
+ funcAfterAssertion: async (pageA) => {
+ // Wait for `promiseMainTimer` resolution and check its timing.
+ await pageA.execute_script(() => window.promiseMainTimer);
+ const actualDelay = performance.now() - startTime;
+
+ if (actualDelay >= delayMain + delayBeforeBackNavigation +
+ delayBeforeForwardNavigation) {
+ assert_unreached(
+ "The timer is fired too late. " +
+ "Maybe the timer is reset when restored from BFCache and " +
+ "waits from the beginning again");
+ } else if (actualDelay >= delayMain + delayBeforeBackNavigation) {
+ // Expected: The timer is paused when the page is in BFCache.
+ } else if (actualDelay >= delayMain) {
+ assert_unreached(
+ "The timer is fired too early. " +
+ "Maybe the time isn't paused when the page is in BFCache");
+ } else {
+ assert_unreached(
+ "The timer is fired too early, even earlier than delayMain.");
+ }
+ }
+}, 'Timers should be paused when the page is in BFCache');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/001-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/001-1.html
new file mode 100644
index 0000000000..cadcf126f5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/001-1.html
@@ -0,0 +1,11 @@
+<!doctype html>
+001-1
+<script>
+addEventListener("pageshow",
+ function(e) {
+ parent.events.push(e);
+ if (parent.events.length == 2) {
+ parent.do_test();
+ }
+ }, false);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/001-2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/001-2.html
new file mode 100644
index 0000000000..6387bc89c8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/001-2.html
@@ -0,0 +1,5 @@
+<!doctype html>
+001-2
+<script>
+onload = function() {setTimeout(function() {history.go(-1)}, 500)}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/001.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/001.html
new file mode 100644
index 0000000000..336ede4cb6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/001.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<title>pageshow event from traversal</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe src="001-1.html"></iframe>
+<script>
+var t = async_test();
+var events = [];
+var iframe = document.getElementsByTagName("iframe")[0];
+
+onload = t.step_func(function() {
+ setTimeout(t.step_func(
+ function() {
+ assert_equals(iframe.contentDocument.readyState, "complete")
+ iframe.src = "001-2.html";
+ }), 500);
+ onload = null;
+})
+
+do_test = t.step_func(function() {
+ assert_equals(events.length, 2);
+ events.forEach(function(e, i) {
+ phase = i ? "after" : "before";
+ assert_equals(e.type, "pageshow", "type " + phase + " navigation");
+
+ // https://github.com/whatwg/html/issues/6794
+ assert_equals(e.bubbles, true, "bubbles " + phase + " navigation");
+ assert_equals(e.cancelable, true, "cancelable " + phase + " navigation");
+
+ assert_equals(e.persisted, i == 0 ? false : true, "persisted " + phase + " navigation");
+ t.done();
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/PopStateEvent.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/PopStateEvent.html
new file mode 100644
index 0000000000..9ee7576249
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/PopStateEvent.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Synthetic popstate events</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(function() {
+ assert_throws_js(
+ TypeError,
+ () => PopStateEvent(''),
+ "Calling PopStateEvent constructor without 'new' must throw"
+ );
+}, "PopStateEvent constructor called as normal function");
+
+test(function () {
+ assert_false('initPopStateEvent' in PopStateEvent.prototype,
+ 'There should be no PopStateEvent#initPopStateEvent');
+}, 'initPopStateEvent');
+
+test(function () {
+ var popStateEvent = new PopStateEvent("popstate");
+ assert_equals(popStateEvent.state, null, "the PopStateEvent.state");
+}, "Initial value of PopStateEvent.state must be null");
+
+test(function () {
+ var popStateEvent = new PopStateEvent("popstate");
+ assert_false(popStateEvent.hasUAVisualTransition, "the PopStateEvent.hasUAVisualTransition");
+}, "Initial value of PopStateEvent.hasUAVisualTransition must be false");
+
+test(function () {
+ var state = history.state;
+ var data;
+ var hasUAVisualTransition = false;
+ window.addEventListener('popstate', function (e) {
+ data = e.state;
+ hasUAVisualTransition = e.hasUAVisualTransition;
+ });
+ window.dispatchEvent(new PopStateEvent('popstate', {
+ 'state': {testdata:true},
+ 'hasUAVisualTransition': true
+ }));
+ assert_true(data.testdata,'state data was corrupted');
+ assert_equals(history.state, state, "history.state was NOT set by dispatching the event");
+ assert_true(hasUAVisualTransition, 'hasUAVisualTransition not set correctly');
+}, 'Dispatching a synthetic PopStateEvent');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/api-availability.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/api-availability.html
new file mode 100644
index 0000000000..2f7d3fafdf
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/api-availability.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<title>API availability following history traversal</title>
+<meta charset=utf-8>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<p>Test requires popup blocker disabled</p>
+<div id=log></div>
+<script>
+var t = async_test();
+var hasNavigated = false;
+var child;
+t.step(function() {
+ child = window.open("resources/api-availability-1.html");
+ t.add_cleanup(function() {
+ child.close();
+ });
+});
+navigate = t.step_func(function() {
+ hasNavigated = true;
+ child.location = child.location.href.replace("api-availability-1.html", "api-availability-2.html");
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-0.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-0.html
new file mode 100644
index 0000000000..5cbab71a5e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-0.html
@@ -0,0 +1,35 @@
+<iframe id="test"></iframe>
+<script>
+var opener = window.opener;
+var t = opener.t;
+var f = document.getElementById("test");
+var l = opener.document.getElementById("step_log");
+
+log = function(t) {l.textContent += ("\n" + t)}
+var navigated = false;
+var steps = [
+ () => f.src = "browsing_context_name-1.html",
+ () => {
+ navigated = true;
+ opener.assert_equals(f.contentWindow.name, "test", "Initial load");
+ f.src = "browsing_context_name-2.html"
+ },
+ () => {
+ opener.assert_equals(f.contentWindow.name, "test1");
+ opener.assert_equals(history.length, 2);
+ history.back()
+ },
+ () => {
+ opener.assert_equals(f.contentWindow.name, "test1", "After navigation");
+ t.done();
+ }
+].map((x, i) => t.step_func(() => {log("Step " + (i+1)); x()}));
+
+next = () => steps.shift()();
+
+onload = () => {
+ log("page load");
+ f.onload = () => {log("iframe onload"); next()};
+ setTimeout(next, 0);
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-1.html
new file mode 100644
index 0000000000..85748a2ebc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-1.html
@@ -0,0 +1,6 @@
+document 1
+<script>
+if (!parent.navigated) {
+ window.name = "test";
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-2.html
new file mode 100644
index 0000000000..b0c869046b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-2.html
@@ -0,0 +1,4 @@
+document 2
+<script>
+window.name = "test1";
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-3.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-3.html
new file mode 100644
index 0000000000..e0c239744f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-3.html
@@ -0,0 +1,6 @@
+document 3
+<script>
+if (!parent.navigated) {
+ window.name = "test3";
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-4.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-4.html
new file mode 100644
index 0000000000..5d2dfa6bb8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name-4.html
@@ -0,0 +1,6 @@
+document 4
+<script>
+if (!parent.navigated) {
+ window.name = "test4";
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name.html
new file mode 100644
index 0000000000..60a8acb098
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<title>Retaining window.name on history traversal</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<pre id="step_log"></pre>
+
+<script>
+var t = async_test();
+t.step(() => {
+ win = window.open("browsing_context_name-0.html");
+ t.add_cleanup(() => win.close());
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name_cross_origin-0.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name_cross_origin-0.html
new file mode 100644
index 0000000000..9e91722714
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name_cross_origin-0.html
@@ -0,0 +1,34 @@
+<iframe id="test"></iframe>
+<script>
+var t = opener.t;
+var f = document.getElementById("test");
+var l = opener.document.getElementById("step_log");
+
+var log = function(t) {l.textContent += ("\n" + t)}
+var navigated = false;
+var steps = [
+ () => f.src = "browsing_context_name-1.html",
+ () => {
+ navigated = true;
+ opener.assert_equals(f.contentWindow.name, "test", "Initial load");
+ f.src = f.src.replace("http://", "http://www.").replace("browsing_context_name-1", "browsing_context_name-2");
+ },
+ () => {
+ // Can't test .name easily here because it's cross-origin
+ opener.assert_equals(history.length, 2);
+ history.back()
+ },
+ () => {
+ opener.assert_equals(f.contentWindow.name, "test", "After navigation");
+ t.done();
+ }
+].map((x, i) => t.step_func(() => {log("Step " + (i+1)); x()}));
+
+next = () => steps.shift()();
+
+onload = () => {
+ log("page load");
+ f.onload = () => {log("iframe onload"); next()};
+ setTimeout(next, 0);
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name_cross_origin.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name_cross_origin.html
new file mode 100644
index 0000000000..caa0bce3eb
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name_cross_origin.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<title>Restoring window.name on cross-origin history traversal</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<pre id="step_log"></pre>
+
+<script>
+var t = async_test();
+t.step(() => {
+ var win = window.open("browsing_context_name_cross_origin-0.html");
+ t.add_cleanup(() => win.close());
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name_cross_origin_2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name_cross_origin_2.html
new file mode 100644
index 0000000000..8202a892a3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name_cross_origin_2.html
@@ -0,0 +1,43 @@
+<!doctype html>
+<title>Restoring window.name on cross-origin history traversal</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<pre id="step_log"></pre>
+<iframe id="test"></iframe>
+<script>
+var t = async_test();
+var f = document.getElementById("test");
+var l = document.getElementById("step_log");
+
+log = function(t) {l.textContent += ("\n" + t)}
+
+var steps = [
+ function() {f.src = "browsing_context_name-1.html"},
+ function() {
+ assert_equals(f.contentWindow.name, "test", "Initial load");
+ setTimeout(next, 0);
+ },
+ function() {f.src = "browsing_context_name-3.html"},
+ function() {
+ assert_equals(f.contentWindow.name, "test3", "After navigation 1");
+ setTimeout(next, 0);
+ },
+ function() {f.src = f.src.replace("http://", "http://www.").replace("browsing_context_name-3", "browsing_context_name-2");},
+ function() {
+ setTimeout(next, 0);
+ },
+ function() {history.go(-2); setTimeout(next, 500)},
+ function() {
+ assert_equals(f.contentWindow.name, "test3", "After navigation 2");
+ t.done();
+ }
+].map(function(x) {return t.step_func(function() {log("Step " + step + " " + f.contentWindow.location); x()})});
+
+var step = 0;
+next = t.step_func(function() {steps[step++]()});
+
+f.onload=next;
+
+onload = function() { setTimeout(next, 0); };
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name_cross_origin_3.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name_cross_origin_3.html
new file mode 100644
index 0000000000..b6a35680dd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/browsing_context_name_cross_origin_3.html
@@ -0,0 +1,44 @@
+<!doctype html>
+<title>Restoring window.name on cross-origin history traversal</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<pre id="step_log"></pre>
+<iframe id="test"></iframe>
+<script>
+var t = async_test();
+var f = document.getElementById("test");
+var l = document.getElementById("step_log");
+
+log = function(t) {l.textContent += ("\n" + t)}
+
+var steps = [
+ function() {f.src = "browsing_context_name-1.html"},
+ function() {
+ assert_equals(f.contentWindow.name, "test", "Initial load");
+ setTimeout(next, 0);
+ },
+ function() {f.src = "browsing_context_name-3.html"},
+ function() {
+ assert_equals(f.contentWindow.name, "test3", "After navigation 1");
+ setTimeout(next, 0);
+ },
+ function() {f.src = f.src.replace("http://", "http://www.").replace("browsing_context_name-1", "browsing_context_name-2");},
+ function() {f.src = f.src.replace("http://www.", "http://").replace("browsing_context_name-2", "browsing_context_name-4");},
+ function() {
+ assert_equals(f.contentWindow.name, "test3", "After navigation 2");
+ history.go(-3); setTimeout(next, 500)
+ },
+ function() {
+ assert_equals(f.contentWindow.name, "test3", "After navigation 3");
+ t.done();
+ }
+].map(function(x) {return t.step_func(function() {log("Step " + step + " " + f.contentWindow.location); x()})});
+
+var step = 0;
+next = t.step_func(function() {steps[step++]()});
+
+f.onload=next;
+
+onload = function() { setTimeout(next, 0); };
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/document-state.https.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/document-state.https.html
new file mode 100644
index 0000000000..f03efc3b65
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/document-state.https.html
@@ -0,0 +1,133 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test the properties of a session history entry's document state</title>
+<link rel="help" href="https://html.spec.whatwg.org/#document-state">
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/get-host-info.sub.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/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+// In this test, we create an auxiliary window with a session history A -> B,
+// where the document on site B is the current active document. Bf-cache is
+// disabled via `Cache-Control: no-store` headers. We then `history.back()` to
+// site A, and perform `location.replace(B)`. This makes the first document in
+// the session history now same-origin/site with the URL in the subsequent
+// session history entry's document state.
+//
+// We then perform `history.forward()` in the first document, which loads the
+// second document (from network). We confirm that the resulting navigation
+// request was made with the expected state, stored on the history entry's
+// document state. The consequences of this are:
+// - The navigation is made with the `Sec-Fetch-Site: cross-site` header,
+// indicating that the *original* document state's initiator origin was
+// preserved
+// - The navigation is made with a cross-origin `Referer` header, indicating
+// that the *original* document state's referrer was preserved
+// - The resulting document has a cross-origin `document.referrer`, indicating
+// the same as above
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+ const A = await rcHelper.addWindow();
+
+ // Create B on a new origin (with bf-cache disabled).
+ const B = await A.navigateToNew({
+ origin: 'HTTPS_NOTSAMESITE_ORIGIN',
+ headers: [['Cache-Control', 'no-store']],
+ });
+
+ // This is the origin we're going to navigate A to, so that it becomes
+ // same-origin with B.
+ const originB = new URL(await B.executeScript(() => location.href)).origin;
+ await B.historyBack();
+
+ // Make A navigate to the same document but in origin B:
+ const urlA = await A.executeScript(() => location.href);
+ const originA = new URL(urlA).origin;
+ assert_not_equals(originA, originB, 'Contexts A and B are cross-origin');
+
+ // Load A's current document but on origin B.
+ const newUrlOnOriginB = urlA.replace(originA, originB);
+ await A.navigate((url) => {
+ location.replace(url);
+ }, [newUrlOnOriginB]);
+
+ // Assert that A and B are now same-origin:
+ const newUrlA = await A.executeScript(() => {
+ return location.href;
+ });
+
+ // Now the session history looks like:
+ // B -> B (initiator origin: A)
+ assert_equals(new URL(newUrlA).origin, originB);
+
+ // This means that when we navigate forward, we should request the second
+ // document with the history entry's document state, which mostly preserves
+ // parameters from the original initiator (a cross-site document), despite a
+ // now-same-origin document initiating this navigation via history.
+ await A.historyForward();
+
+ const requestHeaders = await B.getRequestHeaders();
+ const documentReferrer = await B.executeScript(() => document.referrer);
+
+ assert_equals(requestHeaders.get('sec-fetch-site'), 'cross-site',
+ 'Same-origin forward history navigation to a document whose original ' +
+ 'initiator was cross-site, ends up with Sec-Fetch-Dest: cross-site ' +
+ 'header');
+ assert_equals(requestHeaders.get('referer'), originA + '/',
+ 'Same-origin forward history navigation to a document whose original ' +
+ 'initiator was cross-site ends up with the Referer header that is the ' +
+ 'original cross-site initiator');
+ assert_equals(documentReferrer, originA + '/',
+ 'Same-origin forward history navigation to a document whose original ' +
+ 'initiator was cross-site ends up with document.referrer that is the ' +
+ 'original cross-site initiator');
+}, "A navigation's initiator origin and referrer are stored in the document " +
+ "state and used in the document repopulation case");
+
+// This test is similar to the above, but instead of testing for the true
+// history entry -> document state -> document repopulation case, we stay on [B]
+// (the document who was navigated to from [A]) and run `location.reload()` to
+// confirm that the initiator information from the [A] -> [B] navigation is used
+// when reloading [B], not [B]'s own same-origin information.
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+ const A = await rcHelper.addWindow();
+
+ const originA = new URL(await A.executeScript(() => location.href)).origin;
+
+ // Create B on a new origin.
+ const B = await A.navigateToNew({
+ origin: 'HTTPS_NOTSAMESITE_ORIGIN',
+ });
+
+ const originB = new URL(await B.executeScript(() => location.href)).origin;
+ assert_not_equals(originA, originB, 'Contexts A and B are cross-origin');
+
+ // Reload B.
+ await B.navigate(() => {
+ location.reload();
+ }, []);
+
+ const requestHeaders = await B.getRequestHeaders();
+ const documentReferrer = await B.executeScript(() => document.referrer);
+
+ assert_equals(requestHeaders.get('sec-fetch-site'), 'cross-site',
+ 'Same-origin forward history navigation to a document whose original ' +
+ 'initiator was cross-site, ends up with Sec-Fetch-Dest: cross-site ' +
+ 'header');
+ assert_equals(requestHeaders.get('referer'), originA + '/',
+ 'Same-origin forward history navigation to a document whose original ' +
+ 'initiator was cross-site ends up with the Referer header that is the ' +
+ 'original cross-site initiator');
+ assert_equals(documentReferrer, originA + '/',
+ 'Same-origin forward history navigation to a document whose original ' +
+ 'initiator was cross-site ends up with document.referrer that is the ' +
+ 'original cross-site initiator');
+}, "A navigation's initiator origin and referrer are stored in the document " +
+ "state and used on location.reload()");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/after-load-hash-twice.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/after-load-hash-twice.html
new file mode 100644
index 0000000000..75889ef517
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/after-load-hash-twice.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Popstate/hashchange/load event ordering</title>
+
+<script>
+// Set these up super-early before we hit the network for the test harness, just in case.
+window.eventOrder = [];
+window.onhashchange = () => window.eventOrder.push("hashchange");
+window.onpopstate = () => window.eventOrder.push("popstate");
+window.onload = () => window.eventOrder.push("load");
+</script>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+async_test(t => {
+ assert_array_equals(window.eventOrder, []);
+
+ // 0 timeout is necessary because if we do location.hash assignment before load is finished firing it counts as a replacement.
+ window.addEventListener("load", () => t.step_timeout(() => {
+ assert_array_equals(window.eventOrder, ["load"]);
+
+ window.addEventListener("hashchange", t.step_func(() => {
+ assert_array_equals(window.eventOrder, ["load", "popstate", "popstate", "hashchange"]);
+
+ window.addEventListener("hashchange", t.step_func_done(() => {
+ assert_array_equals(window.eventOrder, ["load", "popstate", "popstate", "hashchange", "hashchange"]);
+ }));
+ }), { once: true });
+
+ location.hash = "#1";
+ assert_array_equals(window.eventOrder, ["load", "popstate"]);
+ location.hash = "#2";
+ assert_array_equals(window.eventOrder, ["load", "popstate", "popstate"]);
+ }, 0));
+}, "when changing hash, after the load event");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/after-load-hash.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/after-load-hash.html
new file mode 100644
index 0000000000..f74d716d91
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/after-load-hash.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Popstate/hashchange/load event ordering</title>
+
+<script>
+// Set these up super-early before we hit the network for the test harness, just in case.
+window.eventOrder = [];
+window.onhashchange = () => window.eventOrder.push("hashchange");
+window.onpopstate = () => window.eventOrder.push("popstate");
+window.onload = () => window.eventOrder.push("load");
+</script>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+async_test(t => {
+ assert_array_equals(window.eventOrder, []);
+
+ // 0 timeout is necessary because if we do location.hash assignment before load is finished firing it counts as a replacement.
+ window.addEventListener("load", () => t.step_timeout(() => {
+ assert_array_equals(window.eventOrder, ["load"]);
+
+ window.addEventListener("hashchange", t.step_func_done(() => {
+ assert_array_equals(window.eventOrder, ["load", "popstate", "hashchange"]);
+ }));
+
+ location.hash = "#1";
+ assert_array_equals(window.eventOrder, ["load", "popstate"]);
+ }, 0));
+}, "when changing hash, after the load event");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/after-load-pushState.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/after-load-pushState.html
new file mode 100644
index 0000000000..4f9f3dad47
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/after-load-pushState.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Popstate/hashchange/load event ordering</title>
+
+<script>
+// Set these up super-early before we hit the network for the test harness, just in case.
+window.eventOrder = [];
+window.onhashchange = () => window.eventOrder.push("hashchange");
+window.onpopstate = () => window.eventOrder.push("popstate");
+window.onload = () => window.eventOrder.push("load");
+</script>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+async_test(t => {
+ assert_array_equals(window.eventOrder, []);
+
+ // 0 timeout is necessary because if we do pushState before load is finished firing it counts as a replacement.
+ window.addEventListener("load", () => t.step_timeout(() => {
+ assert_array_equals(window.eventOrder, ["load"]);
+
+ t.step_timeout(t.step_func_done(() => {
+ assert_array_equals(window.eventOrder, ["load"]);
+ }), 100);
+
+ history.pushState({ state: "new state" }, "");
+ }, 0));
+}, "when pushing state, after the load event");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/after-load-replaceState.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/after-load-replaceState.html
new file mode 100644
index 0000000000..28148ff7b2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/after-load-replaceState.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Popstate/hashchange/load event ordering</title>
+
+<script>
+// Set these up super-early before we hit the network for the test harness, just in case.
+window.eventOrder = [];
+window.onhashchange = () => window.eventOrder.push("hashchange");
+window.onpopstate = () => window.eventOrder.push("popstate");
+window.onload = () => window.eventOrder.push("load");
+</script>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+async_test(t => {
+ assert_array_equals(window.eventOrder, []);
+
+ window.addEventListener("load", t.step_func(() => {
+ assert_array_equals(window.eventOrder, ["load"]);
+
+ t.step_timeout(t.step_func_done(() => {
+ assert_array_equals(window.eventOrder, ["load"]);
+ }), 100);
+
+ history.replaceState({ state: "new state" }, "");
+ }));
+}, "when replacing state, after the load event");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/before-load-hash-twice.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/before-load-hash-twice.html
new file mode 100644
index 0000000000..7c8df11843
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/before-load-hash-twice.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Popstate/hashchange/load event ordering</title>
+
+<script>
+// Set these up super-early before we hit the network for the test harness, just in case.
+window.eventOrder = [];
+window.onhashchange = () => window.eventOrder.push("hashchange");
+window.onpopstate = () => window.eventOrder.push("popstate");
+window.onload = () => window.eventOrder.push("load");
+</script>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+async_test(t => {
+ assert_array_equals(window.eventOrder, []);
+
+ window.addEventListener("load", t.step_func_done(() => {
+ assert_array_equals(window.eventOrder, ["popstate", "popstate", "hashchange", "hashchange", "load"]);
+ }));
+
+ location.hash = "#1";
+ assert_array_equals(window.eventOrder, ["popstate"]);
+ location.hash = "#2";
+ assert_array_equals(window.eventOrder, ["popstate", "popstate"]);
+}, "when changing hash twice, before load");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/before-load-hash.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/before-load-hash.html
new file mode 100644
index 0000000000..97c4636fad
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/before-load-hash.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Popstate/hashchange/load event ordering</title>
+
+<script>
+// Set these up super-early before we hit the network for the test harness, just in case.
+window.eventOrder = [];
+window.onhashchange = () => window.eventOrder.push("hashchange");
+window.onpopstate = () => window.eventOrder.push("popstate");
+window.onload = () => window.eventOrder.push("load");
+</script>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+async_test(t => {
+ assert_array_equals(window.eventOrder, []);
+
+ window.addEventListener("load", t.step_func_done(() => {
+ assert_array_equals(window.eventOrder, ["popstate", "hashchange", "load"]);
+ }));
+
+ location.hash = "#1";
+ assert_array_equals(window.eventOrder, ["popstate"]);
+}, "when changing hash, before load");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/before-load-pushState.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/before-load-pushState.html
new file mode 100644
index 0000000000..a08afa474f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/before-load-pushState.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Popstate/hashchange/load event ordering</title>
+
+<script>
+// Set these up super-early before we hit the network for the test harness, just in case.
+window.eventOrder = [];
+window.onhashchange = () => window.eventOrder.push("hashchange");
+window.onpopstate = () => window.eventOrder.push("popstate");
+window.onload = () => window.eventOrder.push("load");
+</script>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+async_test(t => {
+ assert_array_equals(window.eventOrder, []);
+
+ window.addEventListener("load", t.step_func(() => {
+ t.step_timeout(t.step_func_done(() => {
+ assert_array_equals(window.eventOrder, ["load"]);
+ }), 100);
+ }));
+
+ history.pushState({ state: "new state" }, "");
+}, "when pushing state, before load");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/before-load-replaceState.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/before-load-replaceState.html
new file mode 100644
index 0000000000..10d30038fb
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/before-load-replaceState.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Popstate/hashchange/load event ordering</title>
+
+<script>
+// Set these up super-early before we hit the network for the test harness, just in case.
+window.eventOrder = [];
+window.onhashchange = () => window.eventOrder.push("hashchange");
+window.onpopstate = () => window.eventOrder.push("popstate");
+window.onload = () => window.eventOrder.push("load");
+</script>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+async_test(t => {
+ assert_array_equals(window.eventOrder, []);
+
+ t.step_timeout(t.step_func_done(() => {
+ assert_array_equals(window.eventOrder, ["load"]);
+ }), 100);
+
+ history.replaceState({ state: "new state" }, "");
+}, "when replacing state, before load");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/pushState-inside-popstate.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/pushState-inside-popstate.html
new file mode 100644
index 0000000000..35ada116ed
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/pushState-inside-popstate.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(t => {
+ let popstate_called = false;
+ window.onpopstate = t.step_func(e => {
+ popstate_called = true;
+ history.pushState(2, null, "#2");
+ assert_not_equals(history.state, e.state);
+ });
+ location.hash = "#1";
+ assert_true(popstate_called);
+}, "pushState inside popstate")
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/same-document-traverse-immediate.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/same-document-traverse-immediate.html
new file mode 100644
index 0000000000..51ea20b289
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/same-document-traverse-immediate.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Popstate/hashchange/load event ordering</title>
+
+<script>
+// Set these up super-early before we hit the network for the test harness, just in case.
+window.eventOrder = [];
+window.onhashchange = () => window.eventOrder.push("hashchange");
+window.onpopstate = () => window.eventOrder.push("popstate");
+window.onload = () => window.eventOrder.push("load");
+</script>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+async_test(t => {
+ assert_array_equals(window.eventOrder, []);
+
+ // 0 timeout is necessary because if we do location.hash assignment before load is finished firing it counts as a replacement.
+ window.addEventListener("load", () => t.step_timeout(() => {
+ assert_array_equals(window.eventOrder, ["load"]);
+
+ window.addEventListener("hashchange", t.step_func(() => {
+ assert_array_equals(window.eventOrder, ["load", "popstate", "hashchange"]);
+
+ window.addEventListener("hashchange", t.step_func_done(() => {
+ assert_array_equals(window.eventOrder, ["load", "popstate", "hashchange", "popstate", "hashchange"]);
+ }));
+ }), { once: true });
+
+ location.hash = "#1";
+ assert_array_equals(window.eventOrder, ["load", "popstate"]);
+ history.back();
+ assert_array_equals(window.eventOrder, ["load", "popstate"]);
+ }, 0));
+}, "when traversing back, before hashchange");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/same-document-traverse-wait.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/same-document-traverse-wait.html
new file mode 100644
index 0000000000..39bc760ff7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/event-order/same-document-traverse-wait.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Popstate/hashchange/load event ordering</title>
+
+<script>
+// Set these up super-early before we hit the network for the test harness, just in case.
+window.eventOrder = [];
+window.onhashchange = () => window.eventOrder.push("hashchange");
+window.onpopstate = () => window.eventOrder.push("popstate");
+window.onload = () => window.eventOrder.push("load");
+</script>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+async_test(t => {
+ assert_array_equals(window.eventOrder, []);
+
+ // 0 timeout is necessary because if we do location.hash assignment before load is finished firing it counts as a replacement.
+ window.addEventListener("load", () => t.step_timeout(() => {
+ assert_array_equals(window.eventOrder, ["load"]);
+
+ window.addEventListener("hashchange", t.step_func(() => {
+ assert_array_equals(window.eventOrder, ["load", "popstate", "hashchange"]);
+
+ window.addEventListener("hashchange", t.step_func_done(() => {
+ assert_array_equals(window.eventOrder, ["load", "popstate", "hashchange", "popstate", "hashchange"]);
+ }));
+
+ history.back();
+ assert_array_equals(window.eventOrder, ["load", "popstate", "hashchange"]);
+ }), { once: true });
+
+ location.hash = "#1";
+ assert_array_equals(window.eventOrder, ["load", "popstate"]);
+ }, 0));
+}, "when traversing back, after hashchange");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/events.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/events.html
new file mode 100644
index 0000000000..d5ff83fac0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/events.html
@@ -0,0 +1,151 @@
+<!doctype html>
+<title> PageTransitionEffect Event </title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(function() {
+ var e = new PageTransitionEvent("pageshow", {persisted:false, cancelable:false, bubbles:false});
+ assert_true(e instanceof PageTransitionEvent);
+ assert_equals(e.type, "pageshow");
+ assert_false(e.bubbles, "bubbles");
+ assert_false(e.cancelable, "cancelable");
+ assert_false(e.persisted, "persisted");
+}, "Constructing pageshow event");
+
+test(function() {
+ var e = new PageTransitionEvent("pagehide", {persisted:false, cancelable:false, bubbles:false});
+ assert_true(e instanceof PageTransitionEvent);
+ assert_equals(e.type, "pagehide");
+ assert_false(e.persisted, "persisted");
+ assert_false(e.bubbles, "bubbles");
+ assert_false(e.cancelable, "cancelable");
+}, "Constructing pagehide event");
+
+test(function() {
+ var e = new PageTransitionEvent("pageshow", {persisted:true});
+ assert_true(e instanceof PageTransitionEvent);
+ assert_equals(e.type, "pageshow");
+ assert_true(e.persisted, "persisted");
+ assert_false(e.bubbles, "bubbles");
+ assert_false(e.cancelable, "cancelable");
+}, "Constructing pageshow event, persisted true");
+
+test(function() {
+ var e = new PageTransitionEvent("pagehide", {persisted:true});
+ assert_true(e instanceof PageTransitionEvent);
+ assert_equals(e.type, "pagehide");
+ assert_true(e.persisted, "persisted");
+ assert_false(e.bubbles, "bubbles");
+ assert_false(e.cancelable, "cancelable");
+}, "Constructing pagehide event, persisted true");
+
+test(function() {
+ var e = new PageTransitionEvent("pageshow", {});
+ assert_true(e instanceof PageTransitionEvent);
+ assert_equals(e.type, "pageshow");
+ assert_false(e.persisted, "persisted");
+ assert_false(e.bubbles, "bubbles");
+ assert_false(e.cancelable, "cancelable");
+}, "Constructing pageshow event, empty options");
+
+test(function() {
+ var e = new PageTransitionEvent("pagehide", {});
+ assert_true(e instanceof PageTransitionEvent);
+ assert_equals(e.type, "pagehide");
+ assert_false(e.persisted, "persisted");
+ assert_false(e.bubbles, "bubbles");
+ assert_false(e.cancelable, "cancelable");
+}, "Constructing pagehide event, empty options");
+
+test(function() {
+ var e = new PageTransitionEvent("pageshow");
+ assert_true(e instanceof PageTransitionEvent);
+ assert_equals(e.type, "pageshow");
+ assert_false(e.persisted, "persisted");
+ assert_false(e.bubbles, "bubbles");
+ assert_false(e.cancelable, "cancelable");
+}, "Constructing pageshow event, missing options");
+
+test(function() {
+ var e = new PageTransitionEvent("pagehide");
+ assert_true(e instanceof PageTransitionEvent);
+ assert_equals(e.type, "pagehide");
+ assert_false(e.persisted, "persisted");
+ assert_false(e.bubbles, "bubbles");
+ assert_false(e.cancelable, "cancelable");
+}, "Constructing pagehide event, missing options");
+
+test(function() {
+ var e = new PageTransitionEvent("pageshow", {persisted:null});
+ assert_true(e instanceof PageTransitionEvent);
+ assert_equals(e.type, "pageshow");
+ assert_false(e.persisted, "persisted");
+ assert_false(e.bubbles, "bubbles");
+ assert_false(e.cancelable, "cancelable");
+}, "Constructing pageshow event, persisted:null");
+
+test(function() {
+ var e = new PageTransitionEvent("pagehide", {persisted:null});
+ assert_true(e instanceof PageTransitionEvent);
+ assert_equals(e.type, "pagehide");
+ assert_false(e.persisted, "persisted");
+ assert_false(e.bubbles, "bubbles");
+ assert_false(e.cancelable, "cancelable");
+}, "Constructing pagehide event, persisted:null");
+
+test(function() {
+ var e = new PageTransitionEvent("pageshow", {persisted:undefined});
+ assert_true(e instanceof PageTransitionEvent);
+ assert_equals(e.type, "pageshow");
+ assert_false(e.persisted, "persisted");
+ assert_false(e.bubbles, "bubbles");
+ assert_false(e.cancelable, "cancelable");
+}, "Constructing pageshow event, persisted:undefined");
+
+test(function() {
+ var e = new PageTransitionEvent("pagehide", {persisted:undefined});
+ assert_true(e instanceof PageTransitionEvent);
+ assert_equals(e.type, "pagehide");
+ assert_false(e.persisted, "persisted");
+ assert_false(e.bubbles, "bubbles");
+ assert_false(e.cancelable, "cancelable");
+}, "Constructing pagehide event, persisted:undefined");
+
+test(function() {
+ var e = new PageTransitionEvent("pageshow", {bubbles:true});
+ assert_true(e instanceof PageTransitionEvent);
+ assert_equals(e.type, "pageshow");
+ assert_false(e.persisted, "persisted");
+ assert_true(e.bubbles, "bubbles");
+ assert_false(e.cancelable, "cancelable");
+}, "Constructing pageshow event, bubbles:true");
+
+test(function() {
+ var e = new PageTransitionEvent("pagehide", {bubbles:true});
+ assert_true(e instanceof PageTransitionEvent);
+ assert_equals(e.type, "pagehide");
+ assert_false(e.persisted, "persisted");
+ assert_true(e.bubbles, "bubbles");
+ assert_false(e.cancelable, "cancelable");
+}, "Constructing pagehide event, bubbles:true");
+
+test(function() {
+ var e = new PageTransitionEvent("pageshow", {cancelable:true});
+ assert_true(e instanceof PageTransitionEvent);
+ assert_equals(e.type, "pageshow");
+ assert_false(e.persisted, "persisted");
+ assert_false(e.bubbles, "bubbles");
+ assert_true(e.cancelable, "cancelable");
+}, "Constructing pageshow event, cancelable:true");
+
+test(function() {
+ var e = new PageTransitionEvent("pagehide", {cancelable:true});
+ assert_true(e instanceof PageTransitionEvent);
+ assert_equals(e.type, "pagehide");
+ assert_false(e.persisted, "persisted");
+ assert_false(e.bubbles, "bubbles");
+ assert_true(e.cancelable, "cancelable");
+}, "Constructing pagehide event, cancelable:true");
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/hashchange_event.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/hashchange_event.html
new file mode 100644
index 0000000000..b7111255f8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/hashchange_event.html
@@ -0,0 +1,49 @@
+<!doctype html>
+<title>Queue a task to fire hashchange event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+t = async_test();
+window.onload = t.step_func(function () {
+ if (location.href.toString().indexOf("#") > -1) {
+ location.href = location.href.replace(/#.*$/,'');
+ return;
+ }
+ var root = location.href;
+ var oldURLs = [];
+ var newURLs = [];
+
+ var timer = null;
+
+ location.hash = 'foo';
+ window.onhashchange = t.step_func(function (e) {
+ assert_true(e.isTrusted, "isTrusted");
+ assert_equals(e.target, window, "target");
+ assert_equals(e.type, "hashchange", "type");
+ assert_true(e instanceof HashChangeEvent, "is HashChangeEvent");
+ assert_false(e.bubbles, "bubbles");
+ assert_false(e.cancelable, "cancelable");
+ oldURLs.push(e.oldURL);
+ newURLs.push(e.newURL);
+ if (newURLs.length === 2) {
+ check_result();
+ } else if (timer === null) {
+ timer = setTimeout(function() {check_result()}, 500);
+ }
+ })
+
+ check_result = t.step_func(function() {
+ clearTimeout(timer);
+ try {
+ assert_array_equals([root, root+"#foo"], oldURLs, "e.newURL");
+ assert_array_equals([root+"#foo", root+"#bar"], newURLs, "e.newURL");
+ t.done();
+ } finally {
+ location.hash = "";
+ }
+ });
+
+ location.hash = 'bar';
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/history-traversal-navigate-parent-while-child-loading.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/history-traversal-navigate-parent-while-child-loading.html
new file mode 100644
index 0000000000..2b70375a14
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/history-traversal-navigate-parent-while-child-loading.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i"></iframe>
+<body>
+<script>
+async_test(t => {
+ let starting_history_length = history.length;
+ let iframe_url = (new URL("/common/blank.html", location.href)).href;
+ i.src = iframe_url;
+
+ history.pushState("a", "", "#a");
+ assert_equals(history.length, starting_history_length + 1, "First history length");
+
+ i.onload = t.step_func(() => {
+ assert_equals(history.length, starting_history_length + 1, "Second history length");
+ assert_equals(i.contentWindow.location.href, iframe_url);
+ assert_equals(location.hash, "#a");
+ history.back();
+ // Wait a while for a back navigation. Since all of the possible outcomes
+ // are either same-document or navigating to about:blank, this doesn't need
+ // to wait terribly long.
+ t.step_timeout(t.step_func_done(() => {
+ assert_equals(location.hash, "", "top frame should have navigated back");
+ assert_equals(i.contentWindow.location.href, iframe_url, "iframe should not have navigated");
+ }), 100);
+ });
+}, "pushState() in parent while child is doing initial navigation, then go back");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/history-traversal-navigates-multiple-frames.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/history-traversal-navigates-multiple-frames.html
new file mode 100644
index 0000000000..4f2429fbfd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/history-traversal-navigates-multiple-frames.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+async_test(t => {
+ window.onload = () => t.step_timeout(t.step_func(() => {
+ let starting_history_length = history.length;
+ location.hash = "#a";
+ assert_equals(starting_history_length + 1, history.length);
+ i.contentWindow.location.hash = "#b";
+ assert_equals(starting_history_length + 2, history.length);
+
+ let popstateCount = 0;
+ const popstateCalled = t.step_func(() => {
+ popstateCount++;
+ if (popstateCount < 2)
+ return;
+ assert_equals(location.hash, "");
+ assert_equals(i.contentWindow.location.hash, "");
+ t.done();
+ });
+
+ window.onpopstate = popstateCalled;
+ i.contentWindow.onpopstate = popstateCalled;
+ history.go(-2);
+ }), 0);
+}, "A history traversal should be able to navigate a parent and child simultaneously");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/pagereveal/order-in-bfcache-restore.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/pagereveal/order-in-bfcache-restore.html
new file mode 100644
index 0000000000..f453c80a2a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/pagereveal/order-in-bfcache-restore.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<title>pagereveal event fires and in correct order on restoration from BFCache</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/browsing-the-web.html#updating-the-document">
+<link rel="author" href="mailto:bokan@chromium.org">
+<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/back-forward-cache/resources/helper.sub.js"></script>
+<script>
+// runBfcacheTest opens a popup to pageA which navigates to pageB and then
+// back, ensuring pageA is stored in the BFCache.
+runBfcacheTest({
+ funcBeforeNavigation: async () => {
+ // This function executes in pageA
+
+ // Wait for an animation frame to ensure the the initial-load
+ // `pagereveal` has already been fired so it doesn't get recorded
+ // below.
+ const raf = new Promise(resolve => requestAnimationFrame(resolve));
+ await raf;
+
+ window.event_log = [];
+ let restored = false;
+
+ function recordRafs() {
+ requestAnimationFrame( () => {
+ // Avoid recording animation frames until the page is restored from
+ // BFCache since it's currently uncached. This test is interested only
+ // in the behavior during restoration.
+ if (restored)
+ window.event_log.push('rAF');
+
+ recordRafs();
+ });
+ }
+
+ recordRafs();
+
+ addEventListener('pageshow', (e) => {
+ window.event_log.push('pageshow' + (e.persisted ? '.persisted' : ''));
+ if (e.persisted)
+ restored = true;
+ });
+
+ addEventListener('pagereveal', () => {
+ window.event_log.push('pagereveal');
+ });
+ },
+ funcAfterAssertion: async (pageA, pageB, t) => {
+ let event_log = await pageA.execute_script(async () => {
+ // Ensure at least one animation frame is produced to ensure
+ // pagereveal must have fired.
+ await new Promise(requestAnimationFrame);
+ return window.event_log;
+ });
+
+ // Expect that the events seen are:
+ // pageshow.persisted, pagereveal, rAF, rAF, rAF, ...
+ assert_equals(event_log.slice(0, 3).toString(),
+ 'pageshow.persisted,pagereveal,rAF');
+ for (let i = 3; i < event_log.length; ++i) {
+ assert_equals(event_log[i], 'rAF',
+ 'All events following pagereveal should be animation frames');
+ }
+ },
+ targetOrigin: originSameOrigin,
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/pagereveal/order-in-new-document-navigation.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/pagereveal/order-in-new-document-navigation.html
new file mode 100644
index 0000000000..d2c44511d3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/pagereveal/order-in-new-document-navigation.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<title>pagereveal event fires and in correct order on new-document navigation</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/webappapis.html#update-the-rendering">
+<link rel="author" href="mailto:bokan@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+const event_log = [];
+
+addEventListener('pageshow', () => event_log.push('pageshow'));
+addEventListener('pagereveal', () => event_log.push('pagereveal'));
+requestAnimationFrame(() => event_log.push('rAF'));
+
+promise_test(async () => {
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ assert_equals(event_log.toString(),'pageshow,pagereveal,rAF');
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/pagereveal/order-in-prerender-activation.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/pagereveal/order-in-prerender-activation.html
new file mode 100644
index 0000000000..b281b2b088
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/pagereveal/order-in-prerender-activation.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>pagereveal event fires and in correct order on prerender activation</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/webappapis.html#update-the-rendering">
+<link rel="author" href="mailto:bokan@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/speculation-rules/resources/utils.js"></script>
+<script src="/speculation-rules/prerender/resources/utils.js"></script>
+<script>
+setup(() => assertSpeculationRulesIsSupported());
+
+const uid = token();
+const initiator_url = `resources/order-in-prerender-activation-popup.html?uid=${uid}`;
+
+// This test opens a popup to an initiator page. That page then prerenders a
+// "prerendering" version of itself (by adding a `prerendering` query param)
+// and navigates itself to activate the prerender. The results are recorded and
+// sent back to this test harness.
+promise_test(async () => {
+ const channel = new PrerenderChannel('result', uid);
+ const test_done = new Promise(resolve => {
+ channel.addEventListener('message', e => resolve(e.data), { once: true });
+ });
+
+ window.open(initiator_url, '_blank', 'noopener');
+
+ const result = await test_done;
+
+ if (result.hasOwnProperty('fail')) {
+ assert_unreached(result.fail);
+ }
+
+ // The test records relevant event occurrences up to the second animation
+ // frame. Ensure their order and apparance is as expected.
+ const events_in_order = result.events.join(',');
+ assert_equals(events_in_order,
+ 'pageshow,prerenderingchange,pagereveal,raf');
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/pagereveal/resources/order-in-prerender-activation-popup.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/pagereveal/resources/order-in-prerender-activation-popup.html
new file mode 100644
index 0000000000..78989adc17
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/pagereveal/resources/order-in-prerender-activation-popup.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<title>pagereveal event fires and in correct order on prerender activation (popup)</title>
+<link rel="author" href="mailto:bokan@chromium.org">
+<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 is_prerender_step = params.has('prerendering');
+
+const ready_channel = new PrerenderChannel('ready-to-activate', uid);
+
+function finish(result) {
+ const result_channel = new PrerenderChannel('result', uid);
+ result_channel.postMessage(result);
+ result_channel.close();
+ window.close();
+}
+
+// testharness.js assertions don't work inside this popup so this small helper
+// sends a failure signal back to the test page which will cause test failure.
+function assert(cond, desc) {
+ if (!cond) {
+ finish({fail: desc});
+ }
+}
+
+// The first load of this page should be without 'prerendering' and is used
+// to setup the prerender and then activate it when it's ready.
+if (!is_prerender_step) {
+ assert(!document.prerendering, 'initiator page must not be prerendered');
+
+ const ready_to_activate = new Promise(resolve => {
+ ready_channel.addEventListener('message', resolve, {once: true});
+ });
+
+ const prerendering_url = location.href + '&prerendering';
+ startPrerendering(prerendering_url);
+
+ ready_to_activate.then(() => {
+ location.replace(prerendering_url);
+ });
+} else {
+ assert(document.prerendering, 'prerendering step must be initially prerendered');
+
+ const result = {
+ events: []
+ };
+
+ document.addEventListener('prerenderingchange', () => {
+ result.events.push('prerenderingchange');
+ });
+
+ addEventListener('pageshow', () => {
+ result.events.push('pageshow');
+ });
+
+ // A second rAF will end the test.
+ requestAnimationFrame(() => {
+ result.events.push('raf');
+ requestAnimationFrame(() => finish(result));
+ });
+
+ addEventListener('pagereveal', () => {
+ result.events.push('pagereveal');
+ });
+
+ addEventListener('load', () => {
+ ready_channel.postMessage('unused-readyToActivateMessage');
+ ready_channel.close();
+ });
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resources/blank1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resources/blank1.html
new file mode 100644
index 0000000000..6b4df1ef2f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resources/blank1.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<style>
+body {
+ height: 2000px;
+ width: 2000px;
+}
+</style>
+<body> Blank 1 </body> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resources/blank2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resources/blank2.html
new file mode 100644
index 0000000000..def2139667
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resources/blank2.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<style>
+body {
+ height: 2000px;
+ width: 2000px;
+}
+</style>
+<body> Blank 2 </body>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resources/page-with-fragment.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resources/page-with-fragment.html
new file mode 100644
index 0000000000..11737661d0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resources/page-with-fragment.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<style>
+body {
+ height: 2000px;
+ width: 2000px;
+}
+#fragment {
+ position: absolute;
+ top: 800px;
+ background-color: #faa;
+ display: block;
+ height: 100px;
+ width: 100px;
+}
+
+</style>
+<body>
+Page with fragment
+ <a id="fragment" name="fragment" class='box'></a>
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resources/post_name_on_load.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resources/post_name_on_load.html
new file mode 100644
index 0000000000..1e9b10d1ee
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resources/post_name_on_load.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<script>
+addEventListener('load', _ => {
+ let params = new URLSearchParams(window.location.search);
+ window.opener.postMessage(params.get('name'), '*');
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resume-timer-on-history-back.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resume-timer-on-history-back.html
new file mode 100644
index 0000000000..77602b2d42
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/resume-timer-on-history-back.html
@@ -0,0 +1,146 @@
+<!doctype html>
+<title>Verify history.back() on a persisted page resumes timers</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script type="text/javascript">
+
+function make_post_back_url(name) {
+ return new URL('resources/post_name_on_load.html?name=' + name,
+ window.location).href;
+}
+
+function wait_for_message(name) {
+ return new Promise(resolve => {
+ addEventListener('message', function onMsg(evt) {
+ if (evt.data !== name) {
+ return;
+ }
+ removeEventListener('message', onMsg);
+ resolve();
+ });
+ });
+}
+
+function with_window_by_name(name) {
+ let win = window.open(make_post_back_url(name));
+ return wait_for_message(name).then(_ => {
+ return win;
+ });
+}
+
+function with_nested_frame(win, url) {
+ return new Promise(resolve => {
+ let frame = win.document.createElement('iframe');
+ frame.addEventListener('load', function onLoad(evt) {
+ removeEventListener('load', onLoad);
+ resolve(frame);
+ });
+ frame.src = url;
+ win.document.body.appendChild(frame);
+ });
+}
+
+function delay(win, delay) {
+ return new Promise(resolve => {
+ win.setTimeout(_ => {
+ resolve(win);
+ }, delay);
+ });
+}
+
+function navigate_by_name(win, name) {
+ win.location = make_post_back_url(name);
+ return wait_for_message(name).then(_ => {
+ return win;
+ });
+}
+
+function go_back(win) {
+ return new Promise(resolve => {
+ win.onpagehide = e => resolve(win);
+ win.history.back();
+ });
+}
+
+let DELAY = 500;
+
+promise_test(t => {
+ // Create a new window so we can navigate it later.
+ return with_window_by_name('foo').then(win => {
+ // Schedule a timer within the new window. Our intent is
+ // to navigate the window before the timer fires.
+ let delayFired = false;
+ let innerDelay = delay(win, DELAY);
+ innerDelay.then(_ => {
+ delayFired = true;
+ });
+
+ return navigate_by_name(win, 'bar').then(_ => {
+ // Since the window has navigated the timer should not
+ // fire. We set a timer on our current test window
+ // to verify the other timer is not received.
+ assert_false(delayFired);
+ return delay(window, DELAY * 2);
+ }).then(_ => {
+ // The navigated window's timer should not have fired.
+ assert_false(delayFired);
+ // Now go back to the document that set the timer.
+ return go_back(win);
+ }).then(_ => {
+ // We wait for one of two conditions here. For browsers
+ // with a bfcache the original suspended timer will fire.
+ // Alternatively, if the browser reloads the page the original
+ // message will be sent again. Wait for either of these
+ // two events.
+ return Promise.race([wait_for_message('foo'), innerDelay]);
+ }).then(_ => {
+ win.close();
+ });
+ });
+}, 'history.back() handles top level page timer correctly');
+
+promise_test(t => {
+ let win;
+ // Create a new window so we can navigate it later.
+ return with_window_by_name('foo').then(w => {
+ win = w;
+
+ // Create a nested frame so we check if navigation and history.back()
+ // properly handle child window state.
+ return with_nested_frame(win, 'about:blank');
+
+ }).then(frame => {
+ // Schedule a timer within the nested frame contained by the new window.
+ // Our intent is to navigate the window before the timer fires.
+ let delayFired = false;
+ let innerDelay = delay(frame.contentWindow, DELAY);
+ innerDelay.then(_ => {
+ delayFired = true;
+ });
+
+ return navigate_by_name(win, 'bar').then(_ => {
+ // Since the window has navigated the timer should not
+ // fire. We set a timer on our current test window
+ // to verify the other timer is not received.
+ assert_false(delayFired);
+ return delay(window, DELAY * 2);
+ }).then(_ => {
+ // The navigated window's timer should not have fired.
+ assert_false(delayFired);
+ // Now go back to the document containing the frame that set the timer.
+ return go_back(win);
+ }).then(_ => {
+ // We wait for one of two conditions here. For browsers
+ // with a bfcache the original suspended timer will fire.
+ // Alternatively, if the browser reloads the page the original
+ // message will be sent again. Wait for either of these
+ // two events.
+ return Promise.race([wait_for_message('foo'), innerDelay]);
+ }).then(_ => {
+ win.close();
+ });
+ });
+}, 'history.back() handles nested iframe timer correctly');
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-basic.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-basic.html
new file mode 100644
index 0000000000..e47cd9c383
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-basic.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<title>Verify existence and basic read/write function of history.scrollRestoration</title>
+
+<style>
+ body {
+ height: 2000px;
+ width: 2000px;
+ }
+</style>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script type="text/javascript">
+ 'use strict';
+
+ test(function() {
+ assert_equals(history.scrollRestoration, 'auto');
+ }, 'Default value is "auto"');
+
+ test(function() {
+ history.scrollRestoration = 'manual';
+ assert_equals(history.scrollRestoration, 'manual', 'should be able to set "manual"');
+ history.scrollRestoration = 'auto';
+ assert_equals(history.scrollRestoration, 'auto', 'should be able to set "auto"');
+ }, 'It is writable');
+
+ test(function() {
+ history.scrollRestoration = 'auto';
+ for (var v of [3.1415, {}, 'bogus']) {
+ history.scrollRestoration = v;
+ assert_equals(history.scrollRestoration, 'auto', `setting to invalid value (${v}) should be ignored`);
+ }
+ }, 'Invalid values are ignored');
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-fragment-scrolling-cross-origin.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-fragment-scrolling-cross-origin.html
new file mode 100644
index 0000000000..fec801e94b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-fragment-scrolling-cross-origin.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<meta name=timeout content=long>
+<title>Precedence of scroll restoration mode over fragment scrolling in cross-origin history traversal</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<style>
+ iframe {
+ height: 300px;
+ width: 300px;
+ }
+</style>
+<div id="log"></div>
+<script>
+ 'use strict';
+
+ var next;
+ function frameOnload() {
+ if (next) {
+ next();
+ } else {
+ // The test does the following navigation steps for iframe
+ // 1. load page-with-fragment.html#fragment
+ // 2. load blank1
+ // 3. go back to page-with-fragment.html
+ async_test(function(t) {
+ var iframe = document.querySelector('iframe');
+ var hostInfo = get_host_info();
+ var basePath = location.pathname.substring(0, location.pathname.lastIndexOf('/'));
+ var localURL = hostInfo.HTTP_ORIGIN + basePath + '/resources/page-with-fragment.html#fragment';
+ var remoteURL = hostInfo.HTTP_REMOTE_ORIGIN + basePath + "/resources/blank1.html"
+
+ var steps = [
+ function() {
+ assert_equals(iframe.contentWindow.location.href, localURL, 'should be on page-with-fragment page');
+ // wait one animation frame to ensure layout is run and fragment scrolling is complete
+ iframe.contentWindow.requestAnimationFrame(function() {
+ assert_approx_equals(iframe.contentWindow.scrollY, 800, 5, 'should scroll to fragment');
+
+ iframe.contentWindow.history.scrollRestoration = 'manual';
+ assert_equals(iframe.contentWindow.history.scrollRestoration, 'manual');
+ setTimeout(next, 0);
+ });
+ }, function() {
+ // navigate to a new page from a different origin
+ iframe.src = remoteURL;
+ }, function() {
+ // going back causes the iframe to traverse back
+ history.back();
+ }, function() {
+ // coming back from history, scrollRestoration should be set to manual and respected
+ assert_equals(iframe.contentWindow.location.href, localURL, 'should be back on page-with-fragment page');
+ iframe.contentWindow.requestAnimationFrame(t.step_func_done(function() {
+ assert_equals(iframe.contentWindow.history.scrollRestoration, 'manual', 'navigating back should retain scrollRestoration value');
+ assert_equals(iframe.contentWindow.scrollX, 0, 'should not scroll to fragment');
+ assert_equals(iframe.contentWindow.scrollY, 0, 'should not scroll to fragment');
+ }));
+ }
+ ];
+
+ var stepCount = 0;
+ next = t.step_func(function() {
+ steps[stepCount++]();
+ });
+ next();
+ }, 'Manual scroll restoration should take precedent over scrolling to fragment in cross origin navigation');
+ }
+ }
+</script>
+<iframe src="resources/page-with-fragment.html#fragment" onload="frameOnload()"></iframe>
+
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-fragment-scrolling-samedoc.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-fragment-scrolling-samedoc.html
new file mode 100644
index 0000000000..073e0f6e06
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-fragment-scrolling-samedoc.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<style>
+ body {
+ height: 2000px;
+ width: 2000px;
+ }
+
+ #fragment {
+ position: absolute;
+ top: 800px;
+ background-color: #faa;
+ display: block;
+ height: 100px;
+ width: 100px;
+ }
+</style>
+
+<body>
+ <a id="fragment" name="fragment" class='box'></a>
+</body>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script type="text/javascript">
+ 'use strict';
+
+ async_test(function(t) {
+ history.scrollRestoration = 'manual';
+ assert_equals(history.scrollRestoration, 'manual');
+
+ location.hash = '#fragment';
+ assert_equals(window.scrollY, 800, 'new navigations should scroll to fragment');
+
+ // create a new entry and reset the scroll before verification
+ history.pushState(null, null, '#done');
+ window.scrollTo(0, 0);
+ assert_equals(window.scrollY, 0, 'should reset scroll before verification');
+
+ setTimeout(function() {
+ // setup verification
+ window.addEventListener('hashchange', t.step_func(function() {
+ assert_equals(location.hash, '#fragment');
+ assert_equals(history.scrollRestoration, 'manual');
+ // navigating back should give precedent to history restoration which is 'manual'
+ assert_equals(window.scrollX, 0, 'should not scroll to fragment');
+ assert_equals(window.scrollY, 0, 'should not scroll to fragment');
+ t.done();
+ }));
+ // kick off verification
+ window.history.back();
+ }, 0);
+
+ }, 'Manual scroll restoration should take precedent over scrolling to fragment in cross doc navigation');
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-navigation-cross-origin.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-navigation-cross-origin.html
new file mode 100644
index 0000000000..87a337b2da
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-navigation-cross-origin.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<meta name=timeout content=long>
+<title>Correct behaviour of scroll restoration mode is cross origin history traversal</title>
+
+<style>
+ iframe {
+ height: 300px;
+ width: 300px;
+ }
+</style>
+
+<body>
+ <iframe></iframe>
+</body>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script type="text/javascript">
+ 'use strict';
+
+ // The test does the following navigation steps for iframe
+ // 1. load blank1
+ // 2. load blank2
+ // 3. go back to blank1
+ async_test(function(t) {
+ var iframe = document.querySelector('iframe');
+ var baseURL = location.href.substring(0, location.href.lastIndexOf('/'));
+
+ var steps = [
+ function() {
+ iframe.src = 'resources/blank1.html';
+ },
+ function() {
+ assert_equals(iframe.contentWindow.location.href, baseURL + '/resources/blank1.html', 'should be on first blank page');
+ iframe.contentWindow.history.scrollRestoration = 'manual';
+ assert_equals(iframe.contentWindow.history.scrollRestoration, 'manual');
+ iframe.contentWindow.scrollTo(500, 500);
+ assert_equals(iframe.contentWindow.scrollX, 500, 'scripted scrolling should take effect');
+ assert_equals(iframe.contentWindow.scrollY, 500, 'scripted scrolling should take effect');
+ setTimeout(next, 0);
+ },
+ function() {
+ // navigate to new page
+ iframe.src = 'resources/blank2.html';
+ },
+ function() {
+ assert_equals(iframe.contentWindow.location.href, baseURL + '/resources/blank2.html', 'should be on second blank page');
+ assert_equals(iframe.contentWindow.history.scrollRestoration, 'auto', 'new page loads should set scrollRestoration to "auto"');
+ setTimeout(next, 0);
+ }, function() {
+ iframe.contentWindow.history.back();
+ }, function() {
+ // coming back scrollRestoration should be restored to 'manual' and respected
+ assert_equals(iframe.contentWindow.location.href, baseURL + '/resources/blank1.html', 'should be back on first blank page');
+ assert_equals(iframe.contentWindow.history.scrollRestoration, 'manual', 'navigating back should retain scrollRestoration value');
+ assert_equals(iframe.contentWindow.scrollX, 0, 'horizontal scroll offset should not be restored');
+ assert_equals(iframe.contentWindow.scrollY, 0, 'vertical scroll offset should not be restored');
+ t.done();
+ }
+ ];
+
+ var stepCount = 0;
+ var next = t.step_func(function() {
+ steps[stepCount++]();
+ });
+
+ iframe.onload = next;
+ next();
+ }, 'Navigating to new page should reset to "auto" and navigating back should restore and respect scroll restoration mode');
+
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-navigation-samedoc.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-navigation-samedoc.html
new file mode 100644
index 0000000000..46d40eedc6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-navigation-samedoc.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<title>Correct behaviour of scroll restoration mode in same document history traversals</title>
+
+<style>
+ body {
+ height: 10000px;
+ width: 10000px;
+ }
+</style>
+
+<body></body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script type="text/javascript">
+ 'use strict';
+
+ async_test(function(t) {
+ history.scrollRestoration = 'auto';
+ window.scrollTo(0, 0);
+
+ // create history entries and then verify the impact of scrollRestoration
+ // when they are popped
+ var entries = {
+ /* For scroll restoration mode 'auto', the spec does not require scroll
+ position to be restored at any particular value. */
+ '#1': {type: 'push', expectedScroll: null, scrollRestoration: 'auto'},
+ '#2': {type: 'replace', expectedScroll: null, scrollRestoration: 'auto'},
+ /* For scroll restoration mode 'manual', the spec requires scroll position
+ not to be restored. So we expect [555,555] which is the latest position
+ before navigation. */
+ '#3': {type: 'push', expectedScroll: [555, 555], scrollRestoration: 'manual'},
+ '#4': {type: 'replace', expectedScroll: [555, 555], scrollRestoration: 'manual'}
+ };
+
+ // setup entries
+ for (var key in entries) {
+ var entry = entries[key],
+ beforeValue = history.scrollRestoration,
+ newValue = entry.scrollRestoration;
+
+ var args = [{key: key}, '', key];
+ if (entry.type == 'push') {
+ history.pushState.apply(history, args);
+ } else {
+ history.pushState(null, '', key);
+ history.replaceState.apply(history, args);
+ }
+ assert_equals(history.scrollRestoration, beforeValue, `history.scrollRestoration value is retained after pushing new state`);
+ history.scrollRestoration = newValue;
+ assert_equals(history.scrollRestoration, newValue, `Setting scrollRestoration to ${newValue} works as expected`);
+ window.scrollBy(50, 100);
+ }
+
+ // setup verification
+ window.addEventListener('hashchange', t.step_func(function() {
+ var key = location.hash,
+ entry = entries[key];
+
+ if (key === '') {
+ t.done();
+ return;
+ }
+ assert_equals(history.state.key, key, `state should have key: ${key}`);
+ assert_equals(history.scrollRestoration, entry.scrollRestoration, 'scrollRestoration is updated correctly');
+ if (entry.expectedScroll) {
+ assert_equals(window.scrollX, entry.expectedScroll[0], `scrollX is correct for ${key}`);
+ assert_equals(window.scrollY, entry.expectedScroll[1], `scrollY is correct for ${key}`);
+ }
+
+ window.history.back();
+ }));
+
+ // reset the scroll and kick off the verification
+ setTimeout(function() {
+ history.pushState(null, null, '#done');
+ window.scrollTo(555, 555);
+ window.history.back();
+ }, 0);
+
+ }, 'history.{push,replace}State retain scroll restoration mode and navigation in the same document respects it');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/popstate_event.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/popstate_event.html
new file mode 100644
index 0000000000..05b1dbf4b7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/popstate_event.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<title>Queue a task to fire popstate event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+t = async_test();
+window.onload = t.step_func(function () {
+ var states = [];
+
+ var timer = null;
+
+ history.pushState("a", "State a", "/a");
+ history.pushState("b", "State b", "/b");
+
+ history.back();
+ window.onpopstate = t.step_func(function (e) {
+ assert_true(e.isTrusted, "isTrusted");
+ assert_equals(e.target, window, "target");
+ assert_equals(e.type, "popstate", "type");
+ assert_true(e instanceof PopStateEvent, "is PopStateEvent");
+ assert_false(e.bubbles, "bubbles");
+ assert_false(e.cancelable, "cancelable");
+ assert_not_equals(e.hasUAVisualTransition, undefined);
+
+ states.push(e.state);
+
+ if (states.length === 2) {
+ check_result();
+ } else if (timer === null) {
+ timer = setTimeout(function() {check_result()}, 500);
+ }
+ })
+
+ check_result = t.step_func(function() {
+ clearTimeout(timer);
+ try {
+ assert_array_equals(states, ["a", null]);
+ t.done();
+ } finally {
+ location.hash = "";
+ }
+ });
+
+ setTimeout(function() {history.back()}, 0);
+
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/a.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/a.html
new file mode 100644
index 0000000000..55b73e1153
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/a.html
@@ -0,0 +1 @@
+Welcome to A. \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/api-availability-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/api-availability-1.html
new file mode 100644
index 0000000000..2c31168750
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/api-availability-1.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<title>API availability following history traversal - 1</title>
+<script>
+var controller = opener;
+var t = controller.t;
+var assert_not_equals = controller.assert_not_equals;
+
+t.step(function() {
+ // If this document is discarded as a result of navigation, then this script
+ // will be executed a second time. The semantics this test intends to verify
+ // cannot be observed under these conditions, the discarding is not itself a
+ // violation. Silently pass the test in that case.
+ if (controller.hasNavigated) {
+ t.done();
+ return;
+ }
+
+ t.step_timeout(function() {
+ assert_not_equals(window.history, null, 'history');
+ assert_not_equals(window.localStorage, null, 'localStorage');
+ assert_not_equals(window.location, null, 'location');
+ assert_not_equals(window.navigator, null, 'navigator');
+ assert_not_equals(window.opener, null, 'opener');
+ assert_not_equals(window.sessionStorage, null, 'sessionStorage');
+
+ t.done();
+ }, 1000);
+
+ controller.navigate();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/api-availability-2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/api-availability-2.html
new file mode 100644
index 0000000000..420e5092bc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/api-availability-2.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<title>API availability following history traversal - 2</title>
+<body onload="history.back()"></body>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/b.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/b.html
new file mode 100644
index 0000000000..8f2fc900dd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/b.html
@@ -0,0 +1 @@
+Welcome to B. \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/c.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/c.html
new file mode 100644
index 0000000000..db494e878e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/c.html
@@ -0,0 +1 @@
+Welcome to C. \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/unset_context_name-1.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/unset_context_name-1.sub.html
new file mode 100644
index 0000000000..97918a1f99
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/unset_context_name-1.sub.html
@@ -0,0 +1,45 @@
+<!doctype html>
+<!-- test must be run in a top level browsing context -->
+<title>window.name test helper</title>
+<script>
+const search = window.location.search.replace("?", "");
+const steps = search.split("|");
+
+async function proceedTest() {
+ while (steps.length) {
+ const step = steps.shift();
+
+ if (step.startsWith("report=")) {
+ const id = step.split("=")[1];
+ const stashURL = new URL("unset_context_name_stash.py", location);
+ stashURL.searchParams.set('id', id);
+ stashURL.searchParams.set('value', window.name);
+
+ await fetch(stashURL, { method: "POST" });
+ continue;
+ }
+
+ if (step === "close") {
+ window.close();
+ break;
+ }
+
+ if (step === "navigate") {
+ const url = new URL(window.location);
+ url.host = "{{hosts[][www]}}:{{ports[http][0]}}";
+ url.search = "?" + steps.join("|");
+ window.location = url.href;
+ break;
+ }
+
+ if (step.startsWith("set=")) {
+ window.name = step.split("=")[1];
+ continue;
+ }
+
+ throw new Error("Unsupported step!");
+ }
+}
+
+proceedTest();
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/unset_context_name_stash.py b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/unset_context_name_stash.py
new file mode 100644
index 0000000000..411a4587bc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/resources/unset_context_name_stash.py
@@ -0,0 +1,13 @@
+def main(request, response):
+ key = request.GET.first(b"id")
+ if request.method == "POST":
+ value = request.GET.first(b"value")
+ request.server.stash.take(key)
+ request.server.stash.put(key, value)
+ return b"OK"
+ else:
+ value = request.server.stash.take(key)
+ if value is not None:
+ return value
+ else:
+ return b"NONE"
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/same-url.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/same-url.html
new file mode 100644
index 0000000000..bcca5ed90c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/same-url.html
@@ -0,0 +1,50 @@
+<title>Test same-URL navigation and its effects on history</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<iframe src=resources/a.html></iframe>
+<script>
+async_test((t) => {
+ let state = "begin"
+ self[0].frameElement.onload = t.step_func(() => {
+ if(state === "b first") {
+ assert_equals(history.length, 2)
+
+ state = "c first"
+ navigateFrameAfterDelay(t, "resources/c.html")
+ } else if (state === "c first") {
+ assert_equals(history.length, 3)
+
+ state = "a second"
+ history.back(2)
+ } else if (state === "a second") {
+ assert_equals(history.length, 3)
+
+ state = "a third"
+ navigateFrameAfterDelay(t, "resources/a.html")
+ } else if (state === "a third") {
+ assert_equals(history.length, 3)
+ t.done()
+ }
+ })
+ onload = t.step_func(() => {
+ assert_equals(state, "begin")
+ assert_equals(history.length, 1)
+
+ state = "b first"
+
+ navigateFrameAfterDelay(t, "resources/b.html")
+ })
+})
+
+function navigateFrameAfterDelay(t, url) {
+ // Delay to avoid triggering the "replace" behavior which occurs if
+ // the page isn't yet completely loaded, which only occurs after the
+ // load event handlers have finished:
+ // https://html.spec.whatwg.org/#location-object-navigate
+ // https://html.spec.whatwg.org/#the-end:completely-finish-loading
+ t.step_timeout(() => {
+ self[0].location = url
+ }, 0)
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/scroll-restoration-order.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/scroll-restoration-order.html
new file mode 100644
index 0000000000..8fe7d9f977
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/scroll-restoration-order.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>History restoration order test</title>
+<meta name="assert" content="https://html.spec.whatwg.org/multipage/browsing-the-web.html#history-traversal">
+<meta name="assert" content="Traversing history should restore scroll position after dispatching popstate and before dispatching hashchange">
+
+<style>
+ body {
+ height: 200vh;
+ width: 200vw;
+ }
+</style>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ 'use strict';
+ async_test(function(t) {
+ window.addEventListener('load', t.step_func(function() {
+ // Allow 1px epsilon for fractional scrolling.
+ assert_array_approx_equals(scrollPosition(), [0, 0], 1);
+
+ history.pushState('#1', '', '#1');
+ window.scrollTo(50, 100);
+ assert_array_approx_equals(scrollPosition(), [50, 100], 1);
+
+ history.pushState('#2', '', '#2');
+ window.scrollTo(100, 200);
+ assert_array_approx_equals(scrollPosition(), [100, 200], 1);
+
+ setTimeout(t.step_func(function(){
+ history.pushState(null, null, '#done');
+ window.scrollTo(555, 555);
+ assert_array_approx_equals(scrollPosition(), [555, 555], 1);
+ // Kick off the verification.
+ window.history.back();
+ }), 0);
+ }));
+
+ window.addEventListener('popstate', t.step_func(function() {
+ // Verify that scroll position is *not* restored before popstate.
+ const key = location.hash;
+ const expected_scroll_position = expectedScrollPositionForKey(key);
+ assert_not_equals(scrollPosition()[0], expected_scroll_position[0], `scroll is restored before popstate for ${key}`);
+ assert_not_equals(scrollPosition()[1], expected_scroll_position[1], `scroll is restored before popstate for ${key}`);
+
+ if (key == '')
+ t.done();
+ else
+ setTimeout(t.step_func(function(){ window.history.back(); }), 0);
+ }));
+
+ window.addEventListener('hashchange', t.step_func(function() {
+ // Verify that scroll position is restored before hashchange.
+ var key = location.hash;
+ const expected_scroll_position = expectedScrollPositionForKey(key);
+ assert_array_approx_equals(scrollPosition(), expected_scroll_position, 1, `scroll is restored before hashchange for ${key}`);
+ }));
+
+ function scrollPosition() {
+ return [window.pageXOffset, window.pageYOffset];
+ }
+
+ function expectedScrollPositionForKey(key) {
+ switch (key) {
+ case '#2': return [100, 200];
+ case '#1': return [50, 100];
+ case '' : return [0, 0];
+ default: assert_unreached();
+ }
+ }
+
+ }, 'Traversing history should restore scroll position after dispatching popstate and before dispatching hashchange');
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/srcdoc/consecutive-srcdoc.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/srcdoc/consecutive-srcdoc.html
new file mode 100644
index 0000000000..9aab36b986
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/srcdoc/consecutive-srcdoc.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>changing srcdoc to a different srcdoc</title>
+<link rel="help" href="https://github.com/whatwg/html/issues/6809#issuecomment-905677979">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/helpers.js"></script>
+
+<script>
+"use strict";
+
+// Note: bfcache won't mess with any windows with openers, so it doesn't
+// interfere with these tests.
+
+promise_test(async t => {
+ // Set up a window whose iframe contains [normal page, srcdoc] entries.
+ const w = await openWindow("../../resources/has-iframe.html", t);
+ const iframe = w.document.querySelector("iframe");
+
+ await waitToAvoidReplace(t);
+ iframe.srcdoc = srcdocThatPostsParentOpener("srcdoc1");
+ await waitForMessage(iframe.contentWindow);
+
+ assert_equals(w.history.length, 2);
+
+ // Now navigate to a different srcdoc
+ iframe.srcdoc = srcdocThatPostsParentOpener("srcdoc2");
+
+ // Test that it's a replace.
+ await waitForMessage(iframe.contentWindow);
+ assert_equals(w.history.length, 2,
+ "history.length must not change since it was a replace");
+ assert_equals(
+ iframe.contentDocument.querySelector("p").textContent,
+ "srcdoc2",
+ "Sanity check: the srcdoc document did indeed update"
+ );
+}, "changing srcdoc does a replace navigation since the URL is still " +
+ "about:srcdoc");
+
+promise_test(async t => {
+ // Set up a window whose iframe contains [normal page, srcdoc] entries.
+ const w = await openWindow("../../resources/has-iframe.html", t);
+ const iframe = w.document.querySelector("iframe");
+
+ await waitToAvoidReplace(t);
+ iframe.srcdoc = srcdocThatPostsParentOpener("srcdoc1");
+ await waitForMessage(iframe.contentWindow);
+
+ assert_equals(w.history.length, 2);
+
+ // Now navigate to about:srcdoc#yo
+ iframe.contentWindow.location.href = "about:srcdoc#yo";
+ assert_equals(iframe.contentWindow.location.href, "about:srcdoc#yo");
+ assert_equals(w.history.length, 3);
+
+ // Now navigate to a different srcdoc
+ iframe.srcdoc = srcdocThatPostsParentOpener("srcdoc2");
+
+ // Test that it's a push back to about:srcdoc.
+ await waitForMessage(iframe.contentWindow);
+ assert_equals(
+ w.history.length,
+ 4,
+ "history.length must increase since it was a push"
+ );
+ assert_equals(iframe.contentWindow.location.href, "about:srcdoc");
+ assert_equals(
+ iframe.contentDocument.querySelector("p").textContent,
+ "srcdoc2",
+ "Sanity check: the srcdoc document did indeed update"
+ );
+
+ // Test that we can go back to about:srcdoc#yo.
+ w.history.back();
+ await waitForMessage(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.href, "about:srcdoc#yo");
+ assert_equals(
+ iframe.contentDocument.querySelector("p").textContent,
+ "srcdoc1",
+ "srcdoc content must be restored from history"
+ );
+}, "changing srcdoc to about:srcdoc#yo then another srcdoc does two push " +
+ "navigations and we can navigate back");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/srcdoc/srcdoc-history-entries.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/srcdoc/srcdoc-history-entries.html
new file mode 100644
index 0000000000..09f4094c5f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/srcdoc/srcdoc-history-entries.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>srcdoc history entries</title>
+<link rel="help" href="https://github.com/whatwg/html/issues/6809">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/helpers.js"></script>
+
+<script>
+"use strict";
+
+// Note: bfcache won't mess with any windows with openers, so it doesn't
+// interfere with these tests.
+
+promise_test(async t => {
+ // Set up a window whose iframe contains
+ // [normal page, srcdoc, normal page, srcdoc] entries.
+ const w = await openWindow("/common/blank.html", t);
+ const iframe = await addIframe("/common/blank.html?iframe", w.document);
+
+ assert_equals(w.history.length, 1);
+
+ await waitToAvoidReplace(t);
+ iframe.srcdoc = srcdocThatPostsParentOpener("srcdoc1");
+ assert_equals(w.history.length, 1, "srcdoc navigation must not be sync");
+
+ await waitForMessage(iframe.contentWindow);
+ assert_equals(w.history.length, 2);
+
+ await waitToAvoidReplace(t);
+ const middleURL = (new URL(
+ "../../resources/post-top-opener-on-load.html", location.href)).href;
+ iframe.contentWindow.location.href = middleURL;
+
+ await waitForMessage(iframe.contentWindow);
+ assert_equals(w.history.length, 3);
+
+ await waitToAvoidReplace(t);
+ iframe.srcdoc = srcdocThatPostsParentOpener("srcdoc2");
+ assert_equals(w.history.length, 3, "srcdoc navigation must not be sync");
+
+ await waitForMessage(iframe.contentWindow);
+ assert_equals(w.history.length, 4);
+
+ // Now test traversal.
+ w.history.back();
+ await waitForMessage(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.href, middleURL);
+
+ await waitToAvoidReplace(t);
+
+ w.history.back();
+ await waitForMessage(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.href, "about:srcdoc");
+ assert_equals(
+ iframe.contentDocument.querySelector("p").textContent,
+ "srcdoc1",
+ "srcdoc contents must be restored from history, not from the current " +
+ "value ('srcdoc2') of the content attribute"
+ );
+}, "srcdoc history entries: the iframe itself navigates");
+
+promise_test(async t => {
+ // Set up a window whose iframe contains [normal page, srcdoc] entries.
+ const w = await openWindow("../../resources/has-iframe.html", t);
+ const iframe = w.document.querySelector("iframe");
+
+ assert_equals(w.history.length, 1);
+
+ await waitToAvoidReplace(t);
+ iframe.srcdoc = srcdocThatPostsParentOpener("srcdoc1");
+ assert_equals(w.history.length, 1, "srcdoc navigation must not be sync");
+
+ await waitForMessage(iframe.contentWindow);
+ assert_equals(w.history.length, 2);
+
+ // Now navigate the window itself.
+ w.location.href = "../../resources/post-top-opener-on-load.html";
+ await waitForMessage(w);
+ assert_equals(w.history.length, 3);
+
+ // Now test traversal.
+ w.history.back();
+ await waitForMessage(w);
+ const iframeAgain = w.document.querySelector("iframe");
+
+ assert_equals(iframeAgain.contentWindow.location.href, "about:srcdoc");
+ assert_equals(
+ iframeAgain.contentDocument?.querySelector("p")?.textContent,
+ "srcdoc1",
+ "srcdoc contents must be restored from history, not from the current " +
+ "value of the (not-existing) content attribute"
+ );
+}, "srcdoc history entries: the container window navigates");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/support/window-name-after-cross-origin-main-frame-navigation-popup.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/support/window-name-after-cross-origin-main-frame-navigation-popup.sub.html
new file mode 100644
index 0000000000..e13d191658
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/support/window-name-after-cross-origin-main-frame-navigation-popup.sub.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+</head>
+<body>
+ <script>
+ document.location = "window-name-navigation.sub.html?hostname={{domains[www1]}}&shouldhavename=false&sendmessage=true";
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/support/window-name-after-same-origin-main-frame-navigation-1.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/support/window-name-after-same-origin-main-frame-navigation-1.sub.html
new file mode 100644
index 0000000000..4b7824b488
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/support/window-name-after-same-origin-main-frame-navigation-1.sub.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<script>
+ window.location = "window-name-navigation.sub.html?hostname={{host}}&shouldhavename=true&sendmessage=true";
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/support/window-name-navigation.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/support/window-name-navigation.sub.html
new file mode 100644
index 0000000000..285469a148
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/support/window-name-navigation.sub.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <script>
+ var url = new URL(window.location.href);
+ url.hostname = "{{GET[hostname]}}";
+ url.pathname = "/html/browsers/browsing-the-web/history-traversal/support/window-name-test.sub.html";
+ url.search = "shouldhavename={{GET[shouldhavename]}}&sendmessage={{GET[sendmessage]}}";
+ window.name = "test";
+ window.location = url.href;
+ </script>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/support/window-name-test.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/support/window-name-test.sub.html
new file mode 100644
index 0000000000..460db2e979
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/support/window-name-test.sub.html
@@ -0,0 +1,23 @@
+<script>
+ function process_test_result(passed, test_name) {
+ if ({{GET[sendmessage]}}) {
+ if (window.opener) {
+ window.opener.postMessage(passed, "*");
+ } else {
+ parent.postMessage(passed, "*");
+ }
+ } else {
+ let host = window.opener || parent;
+ host.test(function(t) {
+ host.assert_equals(passed, true);
+ }, test_name);
+ host.done();
+ }
+ }
+
+ if ({{GET[shouldhavename]}}) {
+ process_test_result(window.name == "test", "Test that window name is present");
+ } else {
+ process_test_result(window.name == "", "Test that window name is not present");
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/unset_context_name.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/unset_context_name.html
new file mode 100644
index 0000000000..285f6d7428
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/unset_context_name.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<title>window.name is reset after navigating to a different origin</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>
+
+async function pollResultAndCheck(t, id, expected) {
+ const stashURL = new URL("resources/unset_context_name_stash.py", location);
+ stashURL.searchParams.set('id', id);
+
+ let res = "NONE";
+ while (res == "NONE") {
+ await new Promise(resolve => { t.step_timeout(resolve, 100); });
+
+ const response = await fetch(stashURL);
+ res = await response.text();
+ }
+ if (res !== expected) {
+ assert_unreached('Stash result does not equal expected result.')
+ }
+}
+
+promise_test(async t => {
+ const id = token();
+
+ window.open(`resources/unset_context_name-1.sub.html?set=${id}|navigate|report=${id}|close`, "_blank", "noopener");
+ await pollResultAndCheck(t, id, "");
+}, "Window.name is reset after navigating to a different origin");
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-cross-origin-aux-frame-navigation.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-cross-origin-aux-frame-navigation.sub.html
new file mode 100644
index 0000000000..6bc64c6373
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-cross-origin-aux-frame-navigation.sub.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <!-- window.name should equal "test" after a cross-origin auxiliary frame navigation. -->
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+</head>
+<body>
+ <script>
+ t = async_test("Test that the window name is correct");
+ window.addEventListener("message", t.step_func_done(function(e) {
+ assert_equals(e.data, true);
+ }));
+ window.open("support/window-name-navigation.sub.html?hostname={{domains[www1]}}&shouldhavename=true&sendmessage=true");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-cross-origin-main-frame-navigation.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-cross-origin-main-frame-navigation.sub.html
new file mode 100644
index 0000000000..fb0bb1883f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-cross-origin-main-frame-navigation.sub.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+</head>
+<body>
+ <button id="button" onclick="popup();">open popup</button>
+ <script>
+ function popup() {
+ window.popupWin = window.open('support/window-name-after-cross-origin-main-frame-navigation-popup.sub.html', '_blank');
+ }
+ async_test(t => {
+ t.add_cleanup(() => {
+ popupWin.close();
+ })
+ document.getElementById('button').click();
+ onmessage = t.step_func(e => {
+ assert_true(e.data);
+ });
+ }, 'window.name should equal "" after a cross-origin main frame navigation');
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-cross-origin-sub-frame-navigation.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-cross-origin-sub-frame-navigation.sub.html
new file mode 100644
index 0000000000..a309be6d80
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-cross-origin-sub-frame-navigation.sub.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <!-- window.name should equal "test" after a cross-origin sub frame navigation. -->
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+</head>
+<body>
+ <script>
+ t = async_test("Test that the window name is correct");
+ window.addEventListener("message", t.step_func_done(function(e) {
+ assert_equals(e.data, true);
+ }));
+ </script>
+ <iframe src="support/window-name-navigation.sub.html?hostname={{domains[www1]}}&shouldhavename=true&sendmessage=true";
+ </iframe>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-same-origin-aux-frame-navigation.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-same-origin-aux-frame-navigation.sub.html
new file mode 100644
index 0000000000..8e0a95d8c0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-same-origin-aux-frame-navigation.sub.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <!-- window.name should equal "test" after a same-origin auxiliary frame navigation. -->
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+</head>
+<body>
+ <script>
+ t = async_test("Test that the window name is correct");
+ window.addEventListener("message", t.step_func_done(function(e) {
+ assert_equals(e.data, true);
+ }));
+ window.open("support/window-name-navigation.sub.html?hostname={{host}}&shouldhavename=true&sendmessage=true");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-same-origin-main-frame-navigation.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-same-origin-main-frame-navigation.html
new file mode 100644
index 0000000000..ef11d9971a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-same-origin-main-frame-navigation.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<title>window.name after a same-origin main frame navigation</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<body>
+ <script>
+ var win;
+ async_test(function(t) {
+ win = window.open("support/window-name-after-same-origin-main-frame-navigation-1.sub.html")
+ addEventListener("message", t.step_func_done(e => assert_true(e.data)));
+ }).add_cleanup(() => {if (win) {win.close()}});
+ </script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-same-origin-sub-frame-navigation.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-same-origin-sub-frame-navigation.sub.html
new file mode 100644
index 0000000000..48a6e247b0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/history-traversal/window-name-after-same-origin-sub-frame-navigation.sub.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <!-- window.name should equal "test" after a same-origin sub frame navigation. -->
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+</head>
+<body>
+ <script>
+ t = async_test("Test that the window name is correct");
+ window.addEventListener("message", t.step_func_done(function(e) {
+ assert_equals(e.data, true);
+ }));
+ </script>
+ <iframe src="support/window-name-navigation.sub.html?hostname={{host}}&shouldhavename=true&sendmessage=true";
+ </iframe>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/003-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/003-1.html
new file mode 100644
index 0000000000..4d2229eb51
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/003-1.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<script>
+onload = function() {
+ parent.postMessage("003-1", "*");
+ setTimeout(function() {location = "003-2.html";}, 100);
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/003-2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/003-2.html
new file mode 100644
index 0000000000..827a069479
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/003-2.html
@@ -0,0 +1,9 @@
+<!doctype html>
+003-2
+<script>
+onload = function() {
+ parent.postMessage("003-2", "*")
+ setTimeout(function() {history.go(-1)})
+}
+onunload = function() {location = "003-3.html"}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/003-3.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/003-3.html
new file mode 100644
index 0000000000..8b26c896fd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/003-3.html
@@ -0,0 +1,4 @@
+<!doctype html>
+<script>
+parent.postMessage("003-3", "*");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/003.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/003.html
new file mode 100644
index 0000000000..f437150960
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/003.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<title>Navigation from unload whilst traversing history</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe src="003-1.html"></iframe>
+<script>
+var t = async_test();
+
+var pages = [];
+var iframe = document.getElementsByTagName("iframe")[0];
+
+
+onmessage = t.step_func(function(e) {
+ pages.push(e.data);
+ if(pages.length == 3) {
+ assert_array_equals(pages, ["003-1", "003-2", "003-1"]);
+ t.done();
+ iframe.parentNode.removeChild(iframe);
+ }
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/004-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/004-1.html
new file mode 100644
index 0000000000..02f916fd9c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/004-1.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<script>
+onload = function() {
+ parent.postMessage("004-1", "*");
+ setTimeout(function() {location = location.href.replace("http://", "http://www.").replace("004-1.html", "004-2.html");}, 100);
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/004-2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/004-2.html
new file mode 100644
index 0000000000..f2ef83ee1a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/004-2.html
@@ -0,0 +1,9 @@
+<!doctype html>
+003-2
+<script>
+onload = function() {
+ parent.postMessage("004-2", "*")
+ setTimeout(function() {history.go(-1)})
+}
+onunload = function() {location = "004-3.html"}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/004-3.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/004-3.html
new file mode 100644
index 0000000000..c98711ae98
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/004-3.html
@@ -0,0 +1,4 @@
+<!doctype html>
+<script>
+parent.postMessage("004-3", "*");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/004.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/004.html
new file mode 100644
index 0000000000..dddde4918b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/004.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<title>Navigation from unload whilst traversing cross-origin history</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe src="004-1.html"></iframe>
+<script>
+var t = async_test();
+
+var pages = [];
+var iframe = document.getElementsByTagName("iframe")[0];
+
+
+onmessage = t.step_func(function(e) {
+ pages.push(e.data);
+ if(pages.length == 3) {
+ assert_array_equals(pages, ["004-1", "004-2", "004-1"]);
+ t.done();
+ iframe.parentNode.removeChild(iframe);
+ }
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/005.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/005.html
new file mode 100644
index 0000000000..a87e6e9b3d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/005.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<title>Link with onclick navigation and href navigation </title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe id="test" name="test"></iframe>
+<a target="test" onclick="document.getElementById('test').contentWindow.location='resources/click.html'" href="resources/href.html">Test</a>
+<script>
+var t = async_test();
+t.step(function() {document.links[0].click()});
+onmessage = t.step_func(
+ function(e) {
+ assert_equals(e.data, "href");
+ t.done();
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/006.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/006.html
new file mode 100644
index 0000000000..17b3123faa
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/006.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<title>Link with onclick form submit and href navigation </title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe name="test"></iframe>
+<form target="test" action="resources/click.html"></form>
+<a target="test" onclick="document.forms[0].submit()" href="resources/href.html">Test</a>
+<script>
+var t = async_test();
+t.step(function() {document.links[0].click()});
+onmessage = t.step_func(
+ function(e) {
+ assert_equals(e.data, "href");
+ t.done();
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/007.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/007.html
new file mode 100644
index 0000000000..dbb23ee644
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/007.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<title>Link with onclick javascript url and href navigation </title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe id="test" name="test"></iframe>
+<a target="test" onclick="document.getElementById('test').contentWindow.location = 'javascript:\'abc<script>parent.postMessage(&quot;click&quot;, &quot;*&quot;)</script>\'';" href="resources/href.html">Test</a>
+<script>
+var t = async_test();
+t.step(function() {document.getElementsByTagName("a")[0].click()});
+onmessage = t.step_func(
+ function(e) {
+ assert_equals(e.data, "href");
+ t.done();
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/008.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/008.html
new file mode 100644
index 0000000000..3201d0679d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/008.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<title>Link with onclick form submit to javascript url and href navigation </title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe id="test" name="test"></iframe>
+<form target="test" action="javascript:'<script>parent.postMessage(&quot;click&quot;, &quot;*&quot;)</script>'"></form>
+<a target="test" onclick="document.forms[0].submit()" href="resources/href.html">Test</a>
+<script>
+var t = async_test();
+t.step(function() {document.getElementsByTagName("a")[0].click()});
+onmessage = t.step_func(
+ function(e) {
+ assert_equals(e.data, "href");
+ t.done();
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/009.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/009.html
new file mode 100644
index 0000000000..d963bed2bf
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/009.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<title>Link with onclick form submit to javascript url with document.write and href navigation </title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe id="test" name="test"></iframe>
+<form target="test" action="javascript:(function() {document.write('<script>parent.postMessage(&quot;write&quot;, &quot;*&quot;)</script>'); return '<script>parent.postMessage(&quot;click&quot;, &quot;*&quot;)</script>'})()"></form>
+<a target="test" onclick="document.forms[0].submit()" href="resources/href.html">Test</a>
+<script>
+var t = async_test();
+var events = [];
+t.step(function() {
+ document.getElementsByTagName("a")[0].click()});
+onmessage = t.step_func(
+ function(e) {
+ events.push(e.data);
+ if (events.length === 2) {
+ assert_array_equals(events, ["write", "href"]);
+ t.done();
+ }
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/010.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/010.html
new file mode 100644
index 0000000000..606ad82f48
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/010.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<title>Link with onclick form submit to javascript url with delayed document.write and href navigation </title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe id="test" name="test"></iframe>
+<form target="test" action="javascript:(function() {var x = new XMLHttpRequest(); x.open('GET', 'resources/blank.html?pipe=trickle(d2)', false); x.send(); document.write('<script>parent.postMessage(&quot;write&quot;, &quot;*&quot;)</script>'); return '<script>parent.postMessage(&quot;click&quot;, &quot;*&quot;)</script>'})()"></form>
+<a target="test" onclick="document.forms[0].submit()" href="resources/href.html">Test</a>
+<script>
+var t = async_test();
+onload = t.step_func(function() {document.getElementsByTagName("a")[0].click()});
+onmessage = t.step_func(
+ function(e) {
+ assert_equals(e.data, "href");
+ t.done();
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/011.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/011.html
new file mode 100644
index 0000000000..4d54267968
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/011.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<title>Link with onclick navigation to javascript url with document.write and href navigation </title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe id="test" name="test"></iframe>
+<a target="test" onclick="javascript:(function() {document.write('<script>parent.postMessage(&quot;write&quot;, &quot;*&quot;)</script>'); return '<script>parent.postMessage(&quot;click&quot;, &quot;*&quot;)</script>'})()" href="resources/href.html">Test</a>
+<script>
+var t = async_test();
+var events = [];
+t.step(function() {
+ document.getElementsByTagName("a")[0].click()});
+onmessage = t.step_func(
+ function(e) {
+ events.push(e.data);
+ if (events.length === 2) {
+ assert_array_equals(events, ["write", "href"]);
+ t.done();
+ }
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/012.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/012.html
new file mode 100644
index 0000000000..3795975d70
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/012.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>Link with onclick navigation to javascript url with delayed document.write and href navigation </title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe id="test" name="test"></iframe>
+<!-- XXX: What is this test trying to do? It's navigating the subframe, but
+ doing a write() to _this_ document, and the "javascript:" in there is
+ completely a red herring: it's a label, not a protocol. There is no
+ javascript url involved here, unlike what the title claims! -->
+<a target="test" onclick="javascript:(function() {var x = new XMLHttpRequest(); x.open('GET', 'blank.html?pipe=trickle(d2)', false); x.send(); document.write('write<script>parent.postMessage(&quot;write&quot;, &quot;*&quot;)</script>'); return '<script>parent.postMessage(&quot;click&quot;, &quot;*&quot;)</script>'})()" href="href.html">Test</a>
+<script>
+var t = async_test();
+t.step(function() {document.getElementsByTagName("a")[0].click()});
+onmessage = t.step_func(
+ function(e) {
+ assert_equals(e.data, "href");
+ t.done();
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/013.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/013.html
new file mode 100644
index 0000000000..36a4521843
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/013.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>Link with onclick navigation to javascript url with delayed document.write and href navigation </title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe id="test" name="test"></iframe>
+<a target="test" href="javascript:parent.events.push('javascript');">Test</a>
+<script>
+var t = async_test();
+var events = [];
+t.step(function() {
+ document.getElementsByTagName("a")[0].click();
+ events.push('after script');
+});
+onload = t.step_func(function() {
+ // javascript: executions are async.
+ assert_array_equals(events, ['after script', 'javascript']);
+ t.done();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/014.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/014.html
new file mode 100644
index 0000000000..b27ca992bd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/014.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<title> Link with javascript onclick form submission script order </title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe id="test" name="test"></iframe>
+<form target="test" action="javascript:parent.events.push('submit');"></form>
+<a target="test" onclick="document.forms[0].submit()">Test</a>
+<script>
+var t = async_test();
+var events = [];
+t.step(function() {
+ document.getElementsByTagName("a")[0].click();
+ events.push('after script');
+});
+onload = t.step_func(function() {
+ // javascript: executions are async.
+ assert_array_equals(events, ['after script', 'submit']);
+ t.done();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/015.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/015.html
new file mode 100644
index 0000000000..696aaec2c8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/015.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title> Link with javascript onclick and href script order </title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe id="test" name="test"></iframe>
+<a target="test" onclick="parent.events.push('click');" href="javascript:parent.events.push('href')">Test</a>
+<script>
+var t = async_test();
+var events = [];
+t.step(function() {
+ document.getElementsByTagName("a")[0].click();
+ events.push('after script');
+});
+onload = t.step_func(function() {
+ // javascript: executions are async.
+ assert_array_equals(events, ['click', 'after script', 'href']);
+ t.done();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/abort-document-load-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/abort-document-load-1.html
new file mode 100644
index 0000000000..50a9a50fa0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/abort-document-load-1.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<script>
+parent.postMessage(document.readyState, "*");
+let f = document.createElement("iframe");
+f.onload = function() {
+ parent.postMessage("stop", "*");
+ window.stop();
+};
+document.documentElement.appendChild(f);
+
+window.addEventListener("load", (event) => {
+ parent.postMessage("load", "*");
+});
+window.addEventListener("error", (event) => {
+ parent.postMessage("error", "*");
+});
+window.addEventListener("abort", (event) => {
+ parent.postMessage("abort", "*");
+});
+window.addEventListener("pageshow", (event) => {
+ parent.postMessage("pageshow", "*");
+});
+window.addEventListener("DOMContentLoaded", (event) => {
+ parent.postMessage("DOMContentLoaded", "*");
+});
+document.addEventListener("readystatechange", (event) => {
+ if (document.readyState === "complete") {
+ parent.postMessage("complete", "*");
+ }
+});
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/abort-document-load-2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/abort-document-load-2.html
new file mode 100644
index 0000000000..966b93c51a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/abort-document-load-2.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<script>
+parent.postMessage(document.readyState, "*");
+
+window.addEventListener("load", (event) => {
+ parent.postMessage("load", "*");
+});
+window.addEventListener("error", (event) => {
+ parent.postMessage("error", "*");
+});
+window.addEventListener("abort", (event) => {
+ parent.postMessage("abort", "*");
+});
+window.addEventListener("pageshow", (event) => {
+ parent.postMessage("pageshow", "*");
+});
+window.addEventListener("DOMContentLoaded", (event) => {
+ parent.postMessage("DOMContentLoaded", "*");
+});
+document.addEventListener("readystatechange", (event) => {
+ if (document.readyState === "complete") {
+ parent.postMessage("complete", "*");
+ }
+});
+
+window.setTimeout(function() {
+ parent.postMessage("stop", "*");
+ window.stop();
+}, 100);
+
+</script>
+<link rel="stylesheet" href="/common/slow.py"></link>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/abort-document-load.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/abort-document-load.html
new file mode 100644
index 0000000000..4a4c3df4e7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/abort-document-load.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<title>Aborting a Document load</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/browsing-the-web.html#aborting-a-document-load">
+<div id="log"></div>
+<script>
+var events = [];
+onmessage = function(e) {
+ events.push(e.data);
+};
+async_test(test => {
+ test.step_timeout(() => {
+ const frame = document.querySelector('iframe');
+ const child = frame.contentWindow;
+ assert_equals(child.document.readyState, 'complete', 'readyState is complete');
+ assert_array_equals(events, ["loading", "DOMContentLoaded", "stop", "complete"], 'no load event was fired');
+ events = [];
+ frame.src = "abort-document-load-2.html";
+
+ test.step_timeout(() => {
+ const child = frame.contentWindow;
+ assert_equals(child.document.readyState, 'complete', 'readyState is complete');
+ assert_array_equals(events, ["loading", "DOMContentLoaded", "stop", "complete"], 'no load event was fired');
+ test.done();
+ }, 1000);
+ }, 1000);
+});
+</script>
+<iframe src="abort-document-load-1.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/about-srcdoc-navigation-blocked.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/about-srcdoc-navigation-blocked.window.js
new file mode 100644
index 0000000000..58b73494e1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/about-srcdoc-navigation-blocked.window.js
@@ -0,0 +1,52 @@
+// META: title=Navigation to about:srcdoc, not via srcdoc="", must be blocked
+// META: script=../resources/helpers.js
+
+promise_test(async t => {
+ const iframe = await addSrcdocIframe();
+
+ iframe.contentWindow.location = "/common/blank.html";
+ await waitForIframeLoad(iframe);
+
+ iframe.contentWindow.location = "about:srcdoc";
+
+ // Fetching "about:srcdoc" should result in a network error, and navigating
+ // to a network error should produce an opaque-origin page. In particular,
+ // since the error page should end up being cross-origin to the parent
+ // frame, `contentDocument` should return `null`.
+ //
+ // If instead this results in a message because we re-loaded a srcdoc document
+ // from the contents of the srcdoc="" attribute, immediately fail.
+ await Promise.race([
+ t.step_wait(() => iframe.contentDocument === null),
+ failOnMessage(iframe.contentWindow)
+ ]);
+}, "Navigations to about:srcdoc via window.location must be blocked");
+
+promise_test(async t => {
+ const iframe = await addSrcdocIframe();
+
+ iframe.contentWindow.location = "about:srcdoc?query";
+
+ // See the documentation in the above test.
+ await Promise.race([
+ t.step_wait(() => iframe.contentDocument === null),
+ failOnMessage(iframe.contentWindow)
+ ]);
+}, "Navigations to about:srcdoc?query via window.location within an " +
+ "about:srcdoc document must be blocked");
+
+promise_test(async t => {
+ const iframe = await addSrcdocIframe();
+ iframe.contentWindow.name = "test_frame";
+
+ iframe.contentWindow.location = "/common/blank.html";
+ await waitForIframeLoad(iframe);
+
+ window.open("about:srcdoc", "test_frame");
+
+ // See the documentation in the above test.
+ await Promise.race([
+ t.step_wait(() => iframe.contentDocument === null),
+ failOnMessage(iframe.contentWindow)
+ ]);
+}, "Navigations to about:srcdoc via window.open() must be blocked");
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/anchor-fragment-form-submit-longfragment.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/anchor-fragment-form-submit-longfragment.html
new file mode 100644
index 0000000000..8b9802f589
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/anchor-fragment-form-submit-longfragment.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" title="Joey Arhar" href="mailto:jarhar@chromium.org">
+<link rel="help" href="https://html.spec.whatwg.org/C/#following-hyperlinks">
+<title>Anchor element with onclick form submission and href to fragment</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!-- When an anchor element has an onclick handler which submits a form,
+ the anchor's navigation should occur instead of the form's navigation.
+ However, if the anchor has an href which is just a fragment like "#",
+ then the form should be submitted. Many sites rely on this behavior. -->
+
+<iframe name="test"></iframe>
+<form target="test" action="resources/form.html"></form>
+<a id="anchor" target="test" onclick="document.forms[0].submit()" href="#fragment">Test</a>
+
+<script>
+async_test(t => {
+ const anchor = document.getElementById('anchor');
+ t.step(() => anchor.click());
+ window.onmessage = t.step_func(event => {
+ if (typeof event.data === 'string' && event.data.includes('navigation')) {
+ assert_equals(event.data, 'form navigation');
+ t.done();
+ }
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/anchor-fragment-form-submit-withpath.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/anchor-fragment-form-submit-withpath.html
new file mode 100644
index 0000000000..823174181e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/anchor-fragment-form-submit-withpath.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" title="Joey Arhar" href="mailto:jarhar@chromium.org">
+<link rel="help" href="https://html.spec.whatwg.org/C/#following-hyperlinks">
+<title>Anchor element with onclick form submission and href to fragment</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!-- When an anchor element has an onclick handler which submits a form,
+ the anchor's navigation should occur instead of the form's navigation.
+ However, if the anchor has an href which is just a fragment like "#",
+ then the form should be submitted. Many sites rely on this behavior. -->
+
+<iframe name="test"></iframe>
+<form target="test" action="resources/form.html"></form>
+<a id="anchor" target="test" onclick="document.forms[0].submit()">Test</a>
+
+<script>
+async_test(t => {
+ const iframe = document.querySelector('iframe');
+ iframe.onload = t.step_func(() => {
+ const anchor = document.getElementById('anchor');
+ anchor.href = '/#';
+ anchor.click();
+ window.onmessage = t.step_func(event => {
+ if (typeof event.data === 'string' && event.data.includes('navigation')) {
+ assert_equals(event.data, 'form navigation');
+ t.done();
+ }
+ });
+ });
+
+ iframe.src = '/';
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/anchor-fragment-form-submit.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/anchor-fragment-form-submit.html
new file mode 100644
index 0000000000..e0c0e6f82d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/anchor-fragment-form-submit.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" title="Joey Arhar" href="mailto:jarhar@chromium.org">
+<link rel="help" href="https://html.spec.whatwg.org/C/#following-hyperlinks">
+<title>Anchor element with onclick form submission and href to fragment</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!-- When an anchor element has an onclick handler which submits a form,
+ the anchor's navigation should occur instead of the form's navigation.
+ However, if the anchor has an href which is just a fragment like "#",
+ then the form should be submitted. Many sites rely on this behavior. -->
+
+<iframe name="test"></iframe>
+<form target="test" action="resources/form.html"></form>
+<a id="anchor" target="test" onclick="document.forms[0].submit()" href="#">Test</a>
+
+<script>
+async_test(t => {
+ const anchor = document.getElementById('anchor');
+ t.step(() => anchor.click());
+ window.onmessage = t.step_func(event => {
+ if (typeof event.data === 'string' && event.data.includes('navigation')) {
+ assert_equals(event.data, 'form navigation');
+ t.done();
+ }
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/anchor-jsurl-form-submit.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/anchor-jsurl-form-submit.html
new file mode 100644
index 0000000000..1bf5d3595a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/anchor-jsurl-form-submit.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" title="Joey Arhar" href="mailto:jarhar@chromium.org">
+<link rel="help" href="https://html.spec.whatwg.org/C/#following-hyperlinks">
+<title>Anchor element with onclick form submission and href to javascript: url</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!-- When an anchor element has an onclick handler which submits a form,
+ the anchor's navigation should occur instead of the form's navigation.
+ However, if the anchor has an href which returns undefined, then the form
+ should be submitted. -->
+
+<iframe name="test"></iframe>
+<form target="test" action="resources/form.html"></form>
+<a id="anchor" target="test" onclick="document.forms[0].submit()" href="javascript:void(0)">Test</a>
+
+<script>
+async_test(t => {
+ const anchor = document.getElementById('anchor');
+ t.step(() => anchor.click());
+ window.onmessage = t.step_func(event => {
+ if (typeof event.data === 'string' && event.data.includes('navigation')) {
+ assert_equals(event.data, 'form navigation');
+ t.done();
+ }
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/child-navigates-parent-cross-origin.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/child-navigates-parent-cross-origin.window.js
new file mode 100644
index 0000000000..c5bed0fd4c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/child-navigates-parent-cross-origin.window.js
@@ -0,0 +1,90 @@
+// META: script=/common/get-host-info.sub.js
+// META: script=resources/wait-for-messages.js
+
+function testNavigationFails(params) {
+ return async (t) => {
+ // Start waiting for messages before inserting the child frame, to avoid any
+ // race conditions. Note that this would be racy if we executed tests
+ // concurrently, thankfully `promise_test` executes sequentially. See also:
+ // https://github.com/web-platform-tests/rfcs/pull/75
+ const messagesPromise = waitForMessages(1);
+
+ // Execute the test in an iframe, so that the document executing the test
+ // is not navigated away mid-test in case of failure.
+ const child = document.createElement("iframe");
+ document.body.appendChild(child);
+ t.add_cleanup(() => { document.body.removeChild(child); });
+
+ const url = new URL(
+ "resources/child-navigates-parent-cross-origin-inner.html",
+ window.location);
+
+ // Load the grandchild iframe from a different origin.
+ url.host = get_host_info().REMOTE_HOST;
+
+ for (const key in params || {}) {
+ url.searchParams.set(key, params[key]);
+ }
+
+ const grandchild = child.contentDocument.createElement("iframe");
+ grandchild.src = url;
+ child.contentDocument.body.appendChild(grandchild);
+
+ const messages = await messagesPromise;
+ assert_array_equals(messages, ["error: SecurityError"]);
+ }
+}
+
+promise_test(
+ testNavigationFails(),
+ "Child document attempts to navigate cross-origin parent via location");
+
+promise_test(
+ testNavigationFails({ "property": "hash" }),
+ "Child document attempts to navigate cross-origin parent via "+
+ "location.hash");
+
+promise_test(
+ testNavigationFails({ "property": "host" }),
+ "Child document attempts to navigate cross-origin parent via "+
+ "location.host");
+
+promise_test(
+ testNavigationFails({ "property": "hostname" }),
+ "Child document attempts to navigate cross-origin parent via "+
+ "location.hostname");
+
+promise_test(
+ testNavigationFails({ "property": "href" }),
+ "Child document attempts to navigate cross-origin parent via "+
+ "location.href");
+
+promise_test(
+ testNavigationFails({ "property": "pathname" }),
+ "Child document attempts to navigate cross-origin parent via "+
+ "location.pathname");
+
+promise_test(
+ testNavigationFails({ "property": "protocol" }),
+ "Child document attempts to navigate cross-origin parent via "+
+ "location.protocol");
+
+promise_test(
+ testNavigationFails({ "property": "reload" }),
+ "Child document attempts to navigate cross-origin parent via "+
+ "location.reload()");
+
+promise_test(
+ testNavigationFails({ "property": "replace" }),
+ "Child document attempts to navigate cross-origin parent via "+
+ "location.replace()");
+
+promise_test(
+ testNavigationFails({ "property": "search" }),
+ "Child document attempts to navigate cross-origin parent via "+
+ "location.search");
+
+promise_test(
+ testNavigationFails({ "property": "xxxNonExistent" }),
+ "Child document attempts to navigate cross-origin parent via non-standard "+
+ "location property");
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/child-navigates-parent-same-origin.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/child-navigates-parent-same-origin.window.js
new file mode 100644
index 0000000000..a40c412029
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/child-navigates-parent-same-origin.window.js
@@ -0,0 +1,24 @@
+// META: script=resources/wait-for-messages.js
+
+function testNavigation(url) {
+ return async (t) => {
+ // Start waiting for messages before inserting the child frame, to avoid any
+ // race conditions.
+ const messagesPromise = waitForMessages(3);
+
+ const iframe = document.createElement("iframe");
+ iframe.src = url;
+ document.body.appendChild(iframe);
+
+ const messages = await messagesPromise;
+ assert_array_equals(messages, ["initial", "inner", "destination"]);
+ }
+}
+
+promise_test(
+ testNavigation("resources/child-navigates-parent-location-initial.html"),
+ "Child document navigates same-origin parent via document.location");
+
+promise_test(
+ testNavigation("resources/child-navigates-parent-submit-initial.html"),
+ "Child document navigates same-origin parent via form submission");
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/cross-origin-top-navigation-with-user-activation-in-parent.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/cross-origin-top-navigation-with-user-activation-in-parent.window.js
new file mode 100644
index 0000000000..42b4b208ee
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/cross-origin-top-navigation-with-user-activation-in-parent.window.js
@@ -0,0 +1,8 @@
+async_test(t => {
+ addEventListener('message', t.step_func_done(e => {
+ assert_equals(e.data, 'Denied');
+ }));
+ const w = open("resources/page-with-top-navigating-iframe.html?parent_user_gesture=true");
+ t.add_cleanup(() => {w.close()});
+
+}, "Cross-origin top navigation is blocked without user activation, even if the parent has user activation");
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/cross-origin-top-navigation-without-user-activation.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/cross-origin-top-navigation-without-user-activation.window.js
new file mode 100644
index 0000000000..57f0bce247
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/cross-origin-top-navigation-without-user-activation.window.js
@@ -0,0 +1,8 @@
+async_test(t => {
+ addEventListener('message', t.step_func_done(e => {
+ assert_equals(e.data, 'Denied');
+ }));
+ const w = open("resources/page-with-top-navigating-iframe.html");
+ t.add_cleanup(() => {w.close()});
+
+}, "Cross-origin top navigation is blocked without user activation");
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/empty-iframe-load-event.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/empty-iframe-load-event.html
new file mode 100644
index 0000000000..b9108f9937
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/empty-iframe-load-event.html
@@ -0,0 +1,39 @@
+<!doctype html>
+<title>load event for empty iframe in relation to the event loop</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+setup({explicit_done:true});
+let ran = false;
+
+onload = function() {
+ let iframe = document.createElement("iframe");
+ iframe.onload = function() {
+ test(function() {
+ assert_equals(ran, false, 'Expected onload to run first');
+ }, "Check execution order on load handler");
+ if (ran) {
+ done();
+ } else {
+ ran = true;
+ }
+ };
+ document.body.appendChild(iframe);
+
+ // Nested timeout to accommodate Gecko, because the it seems
+ // the outer setTimeout takes its slot in the event queue right away
+ // but the load event task takes its slot only at the end of this script.
+ setTimeout(function() {
+ setTimeout(function() {
+ test(function() {
+ assert_equals(ran, true, 'Expected nested setTimeout to run second');
+ }, "Check execution order from nested timeout");
+ if (ran) {
+ done();
+ } else {
+ ran = true;
+ }
+ });
+ });
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/empty_fragment.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/empty_fragment.html
new file mode 100644
index 0000000000..18a6f84c9f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/empty_fragment.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Navigating to the same URL with an empty fragment aborts the navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe src="empty_fragment_iframe.html"></iframe>
+<script>
+// If the navigation were not aborted, we would expect multiple load events
+// as the page continually reloads itself.
+async_test(function(t) {
+ var count = 0;
+ var iframe = document.querySelector('iframe');
+ iframe.onload = t.step_func(function() {
+ count++;
+ });
+ window.child_succeeded = t.step_func_done(function() {
+ assert_equals(count, 1);
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/empty_fragment_iframe.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/empty_fragment_iframe.html
new file mode 100644
index 0000000000..26b28a0d7d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/empty_fragment_iframe.html
@@ -0,0 +1,11 @@
+<script>
+var timeout;
+onload = function() {
+ location.hash = "";
+ timeout = setTimeout(function() { parent.child_succeeded() }, 2000);
+};
+
+onbeforeunload = function() {
+ clearTimeout(timeout);
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/failure-check-sequence.https.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/failure-check-sequence.https.html
new file mode 100644
index 0000000000..5d7aa2b9cb
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/failure-check-sequence.https.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Sequence of the checks performed against a navigation response</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<script>
+'use strict';
+const collect = (win) => {
+ const report = new Promise((resolve) => {
+ if (!win.ReportingObserver) {
+ return;
+ }
+
+ const observer = new win.ReportingObserver(resolve);
+ observer.observe();
+ }).then((reports) => reports[0].type);
+ // Although CSP also makes use of ReportingObserver, monitoring this event
+ // allows the test to provide value to implementations that have not yet
+ // integrated CSP and Reporting (as of the time of this writing, Firefox and
+ // Safari).
+ const cspViolation = new Promise((resolve) => {
+ win.document.addEventListener('securitypolicyviolation', () => resolve('csp-violation'));
+ });
+ const halfASecond = new Promise((resolve) => setTimeout(() => resolve(null), 500));
+
+ return Promise.race([report, cspViolation, halfASecond]);
+};
+
+const createWindow = (t, url) => {
+ const win = open(url);
+ t.add_cleanup(() => win.close());
+ return new Promise((resolve) => win.onload = () => resolve(win));
+};
+
+promise_test(async (t) => {
+ const win = await createWindow(t, '/common/blank.html?pipe=header(content-security-policy, frame-src none)');
+ const iframe = win.document.createElement('iframe');
+ iframe.src = '/common/blank.html?pipe=header(x-frame-options, deny)';
+ win.document.body.appendChild(iframe);
+
+ assert_equals(await collect(win), 'csp-violation');
+}, 'CSP check precedes X-Frame-Options check');
+
+promise_test(async (t) => {
+ const win = await createWindow(t, '/common/blank.html?pipe=header(content-security-policy, frame-src none)|header(cross-origin-embedder-policy, require-corp)');
+ const iframe = win.document.createElement('iframe');
+ iframe.src = '/common/blank.html';
+ win.document.body.appendChild(iframe);
+
+ assert_equals(await collect(win), 'csp-violation');
+}, 'CSP check precedes COEP check - CSP header first');
+
+promise_test(async (t) => {
+ const win = await createWindow(t, '/common/blank.html?pipe=header(cross-origin-embedder-policy, require-corp)|header(content-security-policy, frame-src none)');
+ const iframe = win.document.createElement('iframe');
+ iframe.src = '/common/blank.html';
+ win.document.body.appendChild(iframe);
+
+ assert_equals(await collect(win), 'csp-violation');
+}, 'CSP check precedes COEP check - COEP header first');
+
+promise_test(async (t) => {
+ const win = await createWindow(t, '/common/blank.html?pipe=header(cross-origin-embedder-policy, require-corp)');
+ const iframe = win.document.createElement('iframe');
+ iframe.src = '/common/blank.html?pipe=header(x-frame-options, deny)';
+ win.document.body.appendChild(iframe);
+
+ assert_equals(await collect(win), 'coep');
+}, 'COEP check precedes X-Frame-Options check');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-nosrc.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-nosrc.html
new file mode 100644
index 0000000000..b89d3bb54b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-nosrc.html
@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>Navigations on iframe without src attribute</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<body></body>
+<script>
+/*
+ When an iframe is created it will contain the initial empty document. If the
+ src and srcdoc attribute is not set, it will stay on the initial empty
+ document.
+ These tests verifies the behavior of navigations that happen on the initial
+ empty document in that situation. They should all be converted to do a
+ replacement.
+*/
+"use strict";
+const url1 = "/common/blank.html?1";
+const url2 = "/common/blank.html?2";
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src not set, which will stay on the initial empty
+ // document.
+ const iframe = insertIframe(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with no src must not change history.length");
+
+ // Navigate away from the initial empty document through iframe.src. This
+ // should do a replacement.
+ iframe.src = url1;
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on document loaded by iframe with no src");
+
+ // Navigate again using the same method, but this time it shouldn't do a
+ // replacement since it's no longer on the initial empty document.
+ iframe.src = url2;
+ await waitForLoad(t, iframe, url2);
+ assert_equals(history.length, startingHistoryLength + 1,
+ "history.length increases after normal navigation from non-initial empty document");
+}, "src");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src not set, which will stay on the initial empty
+ // document.
+ const iframe = insertIframe(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with no src must not change history.length");
+
+ // Navigate away from the initial empty document through setting
+ // location.href. This should do a replacement.
+ iframe.contentWindow.location.href = url1;
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on document loaded by iframe with no src");
+
+ // Navigate again using the same method, but this time it shouldn't do a
+ // replacement since it's no longer on the initial empty document.
+ iframe.contentWindow.location.href = url2;
+ await waitForLoad(t, iframe, url2);
+ assert_equals(history.length, startingHistoryLength + 1,
+ "history.length increases after normal navigation from non-initial empty document");
+}, "location.href");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src not set, which will stay on the initial empty
+ // document.
+ const iframe = insertIframe(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with no src must not change history.length");
+
+ // Navigate away from the initial empty document through location.assign().
+ // This should do a replacement.
+ iframe.contentWindow.location.assign(url1);
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on document loaded by iframe with no src");
+
+ // Navigate again using the same method, but this time it shouldn't do a
+ // replacement since it's no longer on the initial empty document.
+ iframe.contentWindow.location.assign(url2);
+ await waitForLoad(t, iframe, url2);
+ assert_equals(history.length, startingHistoryLength + 1,
+ "history.length increases after normal navigation from non-initial empty document");
+}, "location.assign");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src not set, which will stay on the initial empty
+ // document.
+ const iframe = insertIframe(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with no src must not change history.length");
+
+ // Navigate away from the initial empty document through window.open().
+ // This should do a replacement.
+ iframe.contentWindow.open(url1, "_self");
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on document loaded by iframe with no src");
+
+ // Navigate again using the same method, but this time it shouldn't do a
+ // replacement since it's no longer on the initial empty document.
+ iframe.contentWindow.open(url2, "_self");
+ await waitForLoad(t, iframe, url2);
+ assert_equals(history.length, startingHistoryLength + 1,
+ "history.length increases after normal navigation from non-initial empty document");
+}, "window.open");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src not set, which will stay on the initial empty
+ // document.
+ const iframe = insertIframe(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with no src must not change history.length");
+
+ // Navigate away from the initial empty document through clicking an <a>
+ // element. This should do a replacement.
+ const a1 = iframe.contentDocument.createElement("a");
+ a1.href = url1;
+ iframe.contentDocument.body.appendChild(a1);
+ a1.click();
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on document loaded by iframe with no src");
+
+ // Navigate again using the same method, but this time it shouldn't do a
+ // replacement since it's no longer on the initial empty document.
+ const a2 = iframe.contentDocument.createElement("a");
+ a2.href = url2;
+ iframe.contentDocument.body.appendChild(a2);
+ a2.click();
+ await waitForLoad(t, iframe, url2);
+ assert_equals(history.length, startingHistoryLength + 1,
+ "history.length increases after normal navigation from non-initial empty document");
+}, "link click");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src not set, which will stay on the initial empty
+ // document.
+ const iframe = insertIframe(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with no src must not change history.length");
+
+ // Navigate away from the initial empty document through form submission.
+ // This should do a replacement.
+ const form1 = iframe.contentDocument.createElement("form");
+ form1.action = "/common/blank.html";
+ iframe.contentDocument.body.appendChild(form1);
+ const input1 = iframe.contentDocument.createElement("input");
+ input1.type = "hidden";
+ input1.name = "1";
+ form1.append(input1);
+ form1.submit();
+ await waitForLoad(t, iframe, url1 + "=");
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on document loaded by iframe with no src");
+
+ // Navigate again using the same method, but this time it shouldn't do a
+ // replacement since it's no longer on the initial empty document.
+ const form2 = iframe.contentDocument.createElement("form");
+ form2.action = "/common/blank.html";
+ iframe.contentDocument.body.appendChild(form2);
+ const input2 = iframe.contentDocument.createElement("input");
+ input2.type = "hidden";
+ input2.name = "2";
+ form2.append(input2);
+ form2.submit();
+ await waitForLoad(t, iframe, url2 + "=");
+ assert_equals(history.length, startingHistoryLength + 1,
+ "history.length increases after normal navigation from non-initial empty document");
+}, "form submission");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-204-fragment.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-204-fragment.html
new file mode 100644
index 0000000000..2a472f6a6d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-204-fragment.html
@@ -0,0 +1,144 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>Fragment navigations on iframe with src set to URL that doesn't load a document (HTTP 204)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<body></body>
+<script>
+/*
+ When an iframe is created it will contain the initial empty document. If the
+ src is set to a URL that doesn't load a new document (e.g. it results in a
+ HTTP 204 response), it will stay on the initial empty document. After fragment
+ navigations happen on it, it should still stay on the initial empty document.
+ These tests verifies the behavior of navigations that happen on the initial
+ empty document in that situation. They should all be converted to do a
+ replacement.
+*/
+"use strict";
+const url1 = "about:blank#1";
+const url2 = "/common/blank.html?2";
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to URL that doesn't load a new document, so
+ // it will stay in the initial empty document.
+ const iframe = insertIframeWith204Src(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src set to URL that doesn't load a new document must not change history.length");
+
+ // Do a fragment navigation within the initial empty document through iframe.src.
+ iframe.src = url1;
+ await new Promise(resolve => t.step_timeout(resolve, 100));
+ assert_equals(iframe.contentWindow.location.href, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after fragment navigation on initial empty document");
+
+ // Navigate away from the initial empty document through iframe.src.
+ iframe.src = url2;
+ await waitForLoad(t, iframe, url2);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation from initial empty document");
+}, "src");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to URL that doesn't load a new document, so
+ // it will stay in the initial empty document.
+ const iframe = insertIframeWith204Src(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src set to URL that doesn't load a new document must not change history.length");
+
+ // Do a fragment navigation within the initial empty document through setting location.href.
+ // This should do a replacement.
+ iframe.contentWindow.location.href = url1;
+ await new Promise(resolve => t.step_timeout(resolve, 100));
+ assert_equals(iframe.contentWindow.location.href, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after fragment navigation on initial empty document");
+
+ // Navigate away from the initial empty document through setting location.href.
+ // This should do a replacement.
+ iframe.contentWindow.location.href = url2;
+ await waitForLoad(t, iframe, url2);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation from initial empty document");
+}, "location.href");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to URL that doesn't load a new document, so
+ // it will stay in the initial empty document.
+ const iframe = insertIframeWith204Src(t);
+ await new Promise(resolve => t.step_timeout(resolve, 100));
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src set to URL that doesn't load a new document must not change history.length");
+
+ // Do a fragment navigation within the initial empty document through location.assign().
+ // This should do a replacement.
+ iframe.contentWindow.location.assign(url1);
+ assert_equals(iframe.contentWindow.location.href, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after fragment navigation on initial empty document");
+
+ // Navigate away from the initial empty document through location.assign().
+ // This should do a replacement.
+ iframe.contentWindow.location.assign(url2);
+ await waitForLoad(t, iframe, url2);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation from initial empty document");
+}, "location.assign");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to URL that doesn't load a new document, so
+ // it will stay in the initial empty document.
+ const iframe = insertIframeWith204Src(t);
+ await new Promise(resolve => t.step_timeout(resolve, 100));
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src set to URL that doesn't load a new document must not change history.length");
+
+ // Do a fragment navigation within the initial empty document through window.open().
+ // This should do a replacement.
+ iframe.contentWindow.open(url1, "_self");
+ assert_equals(iframe.contentWindow.location.href, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after fragment navigation on initial empty document");
+
+ // Navigate away from the initial empty document through window.open().
+ // This should do a replacement.
+ iframe.contentWindow.open(url2, "_self");
+ await waitForLoad(t, iframe, url2);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation from initial empty document");
+}, "window.open");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to URL that doesn't load a new document, so it will stay in the initial empty document.
+ const iframe = insertIframeWith204Src(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src set to URL that doesn't load a new document must not change history.length");
+
+ // Do a fragment navigation within the initial empty document through clicking an <a> element.
+ // This should do a replacement.
+ const a1 = iframe.contentDocument.createElement("a");
+ a1.href = url1;
+ iframe.contentDocument.body.appendChild(a1);
+ a1.click();
+ await new Promise(resolve => t.step_timeout(resolve, 100));
+ assert_equals(iframe.contentWindow.location.href, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after fragment navigation on initial empty document");
+
+ // Navigate away from the initial empty document through clicking an <a> element.
+ // This should do a replacement.
+ const a2 = iframe.contentDocument.createElement("a");
+ a2.href = url2;
+ iframe.contentDocument.body.appendChild(a2);
+ a2.click();
+ await waitForLoad(t, iframe, url2);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation from initial empty document");
+}, "link click");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-204-pushState-replaceState.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-204-pushState-replaceState.html
new file mode 100644
index 0000000000..8a97fd36a4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-204-pushState-replaceState.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>pushState/replaceState on iframe with src set to URL that doesn't load a document (HTTP 204)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<body></body>
+<script>
+/*
+ When an iframe is created it will contain the initial empty document. If the
+ src is set to a URL that doesn't load a new document (e.g. it results in a
+ HTTP 204 response), it will stay on the initial empty document. If
+ history.pushState() or history.replaceState() is called on it, it should
+ still stay on the initial empty document.
+ These tests verifies the behavior of navigations that happen on the initial
+ empty document in that situation. They should all be converted to do a
+ replacement.
+*/
+"use strict";
+const crossDocumentURL = "/common/blank.html?2";
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to URL that doesn't load a new document, so
+ // it will stay on the initial empty document.
+ const iframe = insertIframeWith204Src(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src set to URL that doesn't load a new document must not change history.length");
+
+ // Do a history.pushState() to about:blank#foo.
+ let pushURL = "about:blank#foo";
+ iframe.contentWindow.history.pushState({}, "title", pushURL);
+ assert_equals(iframe.contentWindow.location.href, pushURL);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after history.pushState() on the initial empty document");
+
+ // Navigate away from the initial empty document. This should do replacement.
+ iframe.src = crossDocumentURL;
+ await waitForLoad(t, iframe, crossDocumentURL);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation from initial empty document");
+}, "history.pushState");
+
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to URL that doesn't load a new document, so
+ // it will stay on the initial empty document.
+ const iframe = insertIframeWith204Src(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src set to URL that doesn't load a new document must not change history.length");
+
+ // Do a history.replaceState() to about:blank#foo.
+ let replaceURL = "about:blank#foo";
+ iframe.contentWindow.history.replaceState({}, "title", replaceURL);
+ assert_equals(iframe.contentWindow.location.href, replaceURL);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after history.replaceState() on the initial empty document");
+
+ // Navigate away from the initial empty document. This should do replacement.
+ iframe.src = crossDocumentURL;
+ await waitForLoad(t, iframe, crossDocumentURL);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation from initial empty document");
+}, "history.replaceState");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-204.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-204.html
new file mode 100644
index 0000000000..f7bade68fb
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-204.html
@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>Navigations on iframe with src set to URL that doesn't load a new document</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<body></body>
+<script>
+/*
+ When an iframe is created it will contain the initial empty document. If the
+ src is set to a URL that doesn't load a new document (e.g. it results in a
+ HTTP 204 response), it will stay on the initial empty document.
+ These tests verify the behavior of navigations that happen on the initial
+ empty document in that situation. They should all be converted to do a
+ replacement.
+*/
+"use strict";
+const url1 = "/common/blank.html?1";
+const url2 = "/common/blank.html?2";
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to URL that doesn't load a new document, so
+ // it will stay on the initial empty document.
+ const iframe = insertIframeWith204Src(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src set to URL that doesn't load a new document must not change history.length");
+
+ // Navigate away from the initial empty document through iframe.src. This
+ // should do a replacement.
+ iframe.src = url1;
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on document loaded by iframe with no src");
+
+ // Navigate again using the same method, but this time it shouldn't do a
+ // replacement since it's no longer on the initial empty document.
+ iframe.src = url2;
+ await waitForLoad(t, iframe, url2);
+ assert_equals(history.length, startingHistoryLength + 1,
+ "history.length increases after normal navigation from non-initial empty document");
+}, "Navigating to a different document with src");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to URL that doesn't load a new document, so
+ // it will stay on the initial empty document.
+ const iframe = insertIframeWith204Src(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src set to URL that doesn't load a new document must not change history.length");
+
+ // Navigate away from the initial empty document through setting
+ // location.href. This should do a replacement.
+ iframe.contentWindow.location.href = url1;
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on document loaded by iframe with no src");
+
+ // Navigate again using the same method, but this time it shouldn't do a
+ // replacement since it's no longer on the initial empty document.
+ iframe.contentWindow.location.href = url2;
+ await waitForLoad(t, iframe, url2);
+ assert_equals(history.length, startingHistoryLength + 1,
+ "history.length increases after normal navigation from non-initial empty document");
+}, "Navigating to a different document with location.href");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to URL that doesn't load a new document, so
+ // it will stay on the initial empty document.
+ const iframe = insertIframeWith204Src(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src set to URL that doesn't load a new document must not change history.length");
+
+ // Navigate away from the initial empty document through location.assign().
+ // This should do a replacement.
+ iframe.contentWindow.location.assign(url1);
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on document loaded by iframe with no src");
+
+ // Navigate again using the same method, but this time it shouldn't do a
+ // replacement since it's no longer on the initial empty document.
+ iframe.contentWindow.location.assign(url2);
+ await waitForLoad(t, iframe, url2);
+ assert_equals(history.length, startingHistoryLength + 1,
+ "history.length increases after normal navigation from non-initial empty document");
+}, "Navigating to a different document with location.assign");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to URL that doesn't load a new document, so
+ // it will stay on the initial empty document.
+ const iframe = insertIframeWith204Src(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src set to URL that doesn't load a new document must not change history.length");
+
+ // Navigate away from the initial empty document through window.open().
+ // This should do a replacement.
+ iframe.contentWindow.open(url1, "_self");
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on document loaded by iframe with no src");
+
+ // Navigate again using the same method, but this time it shouldn't do a
+ // replacement since it's no longer on the initial empty document.
+ iframe.contentWindow.open(url2, "_self");
+ await waitForLoad(t, iframe, url2);
+ assert_equals(history.length, startingHistoryLength + 1,
+ "history.length increases after normal navigation from non-initial empty document");
+}, "Navigating to a different document with window.open");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to URL that doesn't load a new document, so
+ // it will stay on the initial empty document.
+ const iframe = insertIframeWith204Src(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src set to URL that doesn't load a new document must not change history.length");
+
+ // Navigate away from the initial empty document through clicking an <a>
+ // element. This should do a replacement.
+ const a1 = iframe.contentDocument.createElement("a");
+ a1.href = url1;
+ iframe.contentDocument.body.appendChild(a1);
+ a1.click();
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on document loaded by iframe with no src");
+
+ // Navigate again using the same method, but this time it shouldn't do a
+ // replacement since it's no longer on the initial empty document.
+ const a2 = iframe.contentDocument.createElement("a");
+ a2.href = url2;
+ iframe.contentDocument.body.appendChild(a2);
+ a2.click();
+ await waitForLoad(t, iframe, url2);
+ assert_equals(history.length, startingHistoryLength + 1,
+ "history.length increases after normal navigation from non-initial empty document");
+}, "Navigating to a different document with link click");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to URL that doesn't load a new document, so
+ // it will stay on the initial empty document.
+ const iframe = insertIframeWith204Src(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src set to URL that doesn't load a new document must not change history.length");
+
+ // Navigate away from the initial empty document through form submission.
+ // This should do a replacement.
+ const form1 = iframe.contentDocument.createElement("form");
+ form1.action = "/common/blank.html";
+ iframe.contentDocument.body.appendChild(form1);
+ const input1 = iframe.contentDocument.createElement("input");
+ input1.type = "hidden";
+ input1.name = "1";
+ form1.append(input1);
+ form1.submit();
+ await waitForLoad(t, iframe, url1 + "=");
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on document loaded by iframe with no src");
+
+ // Navigate again using the same method, but this time it shouldn't do a
+ // replacement since it's no longer on the initial empty document.
+ const form2 = iframe.contentDocument.createElement("form");
+ form2.action = "/common/blank.html";
+ iframe.contentDocument.body.appendChild(form2);
+ const input2 = iframe.contentDocument.createElement("input");
+ input2.type = "hidden";
+ input2.name = "2";
+ form2.append(input2);
+ form2.submit();
+ await waitForLoad(t, iframe, url2 + "=");
+ assert_equals(history.length, startingHistoryLength + 1,
+ "history.length increases after normal navigation from non-initial empty document");
+}, "Navigating to a different document with form submission");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-aboutblank-navigate-immediately.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-aboutblank-navigate-immediately.html
new file mode 100644
index 0000000000..a75257d91c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-aboutblank-navigate-immediately.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>Navigations immediately after appending iframe with src='about:blank'</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<body></body>
+<script>
+/*
+ When an iframe is created with src="about:blank", it will stay on the initial
+ empty document. These tests verify the behavior of navigations that happen
+ immediately after the iframe is created, which should result in replacement.
+*/
+"use strict";
+const url1 = "/common/blank.html?1";
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to about:blank. This would trigger a
+ // navigation to a non-initial about:blank document.
+ const iframe = insertIframeWithAboutBlankSrc(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src='about:blank' must not change history.length");
+
+ // Trigger a navigation to url1 through the iframe's src attribute.
+ // The iframe should still be on the initial empty document, and the
+ // navigation should do replacement.
+ iframe.src = url1;
+ // Wait for the latest navigation to finish.
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on initial empty document");
+}, "Navigating to a different document with src");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to about:blank but navigate away from it immediately below.
+ const iframe = insertIframeWithAboutBlankSrc(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src='about:blank' must not change history.length");
+
+ // Navigate away from the initial empty document through setting
+ // location.href. The iframe should still be on the initial empty document,
+ // and the navigation should do replacement.
+ iframe.contentWindow.location.href = url1;
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on initial empty document");
+}, "Navigating to a different document with location.href");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to about:blank but navigate away from it immediately below.
+ const iframe = insertIframeWithAboutBlankSrc(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src='about:blank' must not change history.length");
+
+ // Navigate away from the initial empty document through location.assign().
+ // The iframe should still be on the initial empty document, and the
+ // navigation should do replacement.
+ iframe.contentWindow.location.assign(url1);
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on initial empty document");
+}, "Navigating to a different document with location.assign");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to about:blank but navigate away from it immediately below.
+ const iframe = insertIframeWithAboutBlankSrc(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src='about:blank' must not change history.length");
+
+ // Navigate away from the initial empty document through window.open().
+ // The iframe should still be on the initial empty document, and the
+ // navigation should do replacement.
+ iframe.contentWindow.open(url1, "_self");
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on initial empty document");
+}, "Navigating to a different document with window.open");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to about:blank but navigate away from it immediately below.
+ const iframe = insertIframeWithAboutBlankSrc(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src='about:blank' must not change history.length");
+
+ // Navigate away from the initial empty document through clicking an <a>
+ // element. The iframe should still be on the initial empty document, and the
+ // navigation should do replacement.
+ const a = iframe.contentDocument.createElement("a");
+ a.href = url1;
+ iframe.contentDocument.body.appendChild(a);
+ a.click();
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on initial empty document");
+}, "Navigating to a different document with link click");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to about:blank but navigate away from it immediately below.
+ const iframe = insertIframeWithAboutBlankSrc(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src='about:blank' must not change history.length");
+
+ // Navigate away from the initial empty document through form submission.
+ // The iframe should still be on the initial empty document, and the
+ // navigation should do replacement.
+ const form = iframe.contentDocument.createElement("form");
+ form.action = "/common/blank.html";
+ iframe.contentDocument.body.appendChild(form);
+ const input = iframe.contentDocument.createElement("input");
+ input.type = "hidden";
+ input.name = "1";
+ form.append(input);
+ form.submit();
+ await waitForLoad(t, iframe, url1 + "=");
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on initial empty document");
+}, "Navigating to a different document with form submission");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-aboutblank-wait-for-load.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-aboutblank-wait-for-load.html
new file mode 100644
index 0000000000..b7066ce521
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-aboutblank-wait-for-load.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>Navigations after iframe with src='about:blank' finished loading</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<body></body>
+<script>
+ /*
+ When an iframe is created with src="about:blank", it will stay on the initial
+ empty document. These tests verify the behavior of navigations that happen
+ immediately after the load event is fired on the iframe element, which
+ should result in replacement.
+ */
+"use strict";
+const url1 = "/common/blank.html?1";
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to about:blank, and wait for it to finish
+ // loading. This would trigger and commit a navigation to a non-initial
+ // about:blank document.
+ const iframe = await insertIframeWithAboutBlankSrcWaitForLoad(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src='about:blank' must not change history.length");
+
+ // Trigger a navigation to url1 through the iframe's src attribute.
+ // The iframe should still be on the initial empty document, and the
+ // navigation should do replacement.
+ iframe.src = url1;
+ // Wait for the latest navigation to finish.
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on initial empty document");
+}, "Navigating to a different document with src");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to about:blank, and wait for it to finish
+ // loading. This would trigger and commit a navigation to a non-initial
+ // about:blank document.
+ const iframe = await insertIframeWithAboutBlankSrcWaitForLoad(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src='about:blank' must not change history.length");
+
+ // Navigate away from the initial empty document through setting
+ // location.href. The iframe should still be on the initial empty document,
+ // and the navigation should do replacement.
+ iframe.contentWindow.location.href = url1;
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on initial empty document");
+ }, "Navigating to a different document with location.href");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to about:blank, and wait for it to finish
+ // loading. This would trigger and commit a navigation to a non-initial
+ // about:blank document.
+ const iframe = await insertIframeWithAboutBlankSrcWaitForLoad(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src='about:blank' must not change history.length");
+
+ // Navigate away from the initial empty document through setting
+ // location.href. The iframe should still be on the initial empty document,
+ // and the navigation should do replacement.
+ iframe.contentWindow.location.href = url1;
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on initial empty document");
+}, "Navigating to a different document with location.assign");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to about:blank, and wait for it to finish
+ // loading. This would trigger and commit a navigation to a non-initial
+ // about:blank document.
+ const iframe = await insertIframeWithAboutBlankSrcWaitForLoad(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src='about:blank' must not change history.length");
+
+ // Navigate away from the initial empty document through window.open().
+ // The iframe should still be on the initial empty document, and the
+ // navigation should do replacement.
+ iframe.contentWindow.open(url1, "_self");
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on initial empty document");
+}, "Navigating to a different document with window.open");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to about:blank, and wait for it to finish
+ // loading. This would trigger and commit a navigation to a non-initial
+ // about:blank document.
+ const iframe = await insertIframeWithAboutBlankSrcWaitForLoad(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src='about:blank' must not change history.length");
+
+ // Navigate away from the initial empty document through clicking an <a>
+ // element. The iframe should still be on the initial empty document, and the
+ // navigation should do replacement.
+ const a = iframe.contentDocument.createElement("a");
+ a.href = url1;
+ iframe.contentDocument.body.appendChild(a);
+ a.click();
+ await waitForLoad(t, iframe, url1);
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on initial empty document");
+}, "Navigating to a different document with link click");
+
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+ // Create an iframe with src set to about:blank which will commit an about:blank document that is not the initial empty document, and wait for it to load.
+ const iframe = await insertIframeWithAboutBlankSrcWaitForLoad(t);
+ assert_equals(history.length, startingHistoryLength,
+ "Inserting iframe with src='about:blank' must not change history.length");
+
+ // Navigate away from the initial empty document through form submission.
+ // The iframe should still be on the initial empty document, and the
+ // navigation should do replacement.
+ const form = iframe.contentDocument.createElement("form");
+ form.action = "/common/blank.html";
+ iframe.contentDocument.body.appendChild(form);
+ const input = iframe.contentDocument.createElement("input");
+ input.type = "hidden";
+ input.name = "1";
+ form.append(input);
+ form.submit();
+ await waitForLoad(t, iframe, url1 + "=");
+ assert_equals(history.length, startingHistoryLength,
+ "history.length must not change after normal navigation on initial empty document");
+}, "Navigating to a different document with form submission");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/initial-content-replacement.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/initial-content-replacement.html
new file mode 100644
index 0000000000..44f890df6c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/initial-content-replacement.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>
+ Content synchronously added to iframe/opened window's document after creation
+ won't get replaced asynchronously when staying on the initial empty document
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<body></body>
+<script>
+"use strict";
+
+// Asserts the document on |w| stays the same after waiting 100ms.
+function assertDocumentStaysTheSame(t, w) {
+ const initialDocument = w.document;
+ initialDocument.body.innerHTML = "foo";
+ return new Promise((resolve) => {
+ t.step_timeout(() => {
+ assert_equals(w.document.body.innerHTML, "foo");
+ assert_equals(w.document, initialDocument);
+ resolve();
+ }, 100);
+ });
+}
+
+promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ await assertDocumentStaysTheSame(t, iframe.contentWindow);
+}, "Content synchronously added to <iframe> with no src won't get replaced");
+
+promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ iframe.src = "";
+ document.body.appendChild(iframe);
+ await assertDocumentStaysTheSame(t, iframe.contentWindow);
+}, "Content synchronously added to <iframe> with src='' won't get replaced");
+
+promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ iframe.src = "about:blank";
+ document.body.appendChild(iframe);
+ await assertDocumentStaysTheSame(t, iframe.contentWindow);
+}, "Content synchronously added to <iframe> with src='about:blank' won't get replaced");
+
+promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ iframe.src = "about:blank#foo";
+ document.body.appendChild(iframe);
+ await assertDocumentStaysTheSame(t, iframe.contentWindow);
+}, "Content synchronously added to <iframe> with src='about:blank#foo' won't get replaced");
+
+promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ iframe.src = "about:blank?foo";
+ document.body.appendChild(iframe);
+ await assertDocumentStaysTheSame(t, iframe.contentWindow);
+}, "Content synchronously added to <iframe> with src='about:blank?foo' won't get replaced");
+
+promise_test(async t => {
+ const w = window.open();
+ await assertDocumentStaysTheSame(t, w);
+}, "Content synchronously added to window.open()-ed document won't get replaced");
+
+promise_test(async t => {
+ const w = window.open("");
+ await assertDocumentStaysTheSame(t, w);
+}, "Content synchronously added to window.open('')-ed document won't get replaced");
+
+promise_test(async t => {
+ const w = window.open("about:blank");
+ await assertDocumentStaysTheSame(t, w);
+}, "Content synchronously added to window.open('about:blank')-ed document won't get replaced");
+
+promise_test(async t => {
+ const w = window.open("about:blank#foo");
+ await assertDocumentStaysTheSame(t, w);
+}, "Content synchronously added to window.open('about:blank#foo')-ed document won't get replaced");
+
+promise_test(async t => {
+ const w = window.open("about:blank?foo");
+ await assertDocumentStaysTheSame(t, w);
+}, "Content synchronously added to window.open('about:blank?foo')-ed document won't get replaced");
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-event-iframe-element.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-event-iframe-element.html
new file mode 100644
index 0000000000..0d19770cc1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-event-iframe-element.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>"load" event fires on the iframe element when loading the initial empty document</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<body></body>
+<script>
+"use strict";
+
+promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ let loadCount = 0;
+ iframe.addEventListener("load", () => {
+ loadCount++;
+ });
+ document.body.appendChild(iframe);
+ assert_equals(loadCount, 1);
+}, "load event fires synchronously on <iframe> element created with no src");
+
+promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ iframe.src = "";
+ let loadCount = 0;
+ iframe.addEventListener("load", () => {
+ loadCount++;
+ });
+ document.body.appendChild(iframe);
+ assert_equals(loadCount, 1);
+}, "load event fires synchronously on <iframe> element created with src=''");
+
+promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ iframe.src = "about:blank";
+ let loadCount = 0;
+ iframe.addEventListener("load", () => {
+ loadCount++;
+ });
+ document.body.appendChild(iframe);
+ assert_equals(loadCount, 1);
+}, "load event fires synchronously on <iframe> element created with src='about:blank'");
+
+promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ iframe.src = "about:blank#foo";
+ let loadCount = 0;
+ iframe.addEventListener("load", () => {
+ loadCount++;
+ });
+ document.body.appendChild(iframe);
+ assert_equals(loadCount, 1);
+}, "load event fires synchronously on <iframe> element created with src='about:blank#foo'");
+
+promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ iframe.src = "about:blank?foo";
+ let loadCount = 0;
+ iframe.addEventListener("load", () => {
+ loadCount++;
+ });
+ document.body.appendChild(iframe);
+ assert_equals(loadCount, 1);
+}, "load event fires synchronously on <iframe> element created with src='about:blank?foo'");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-iframe-contentWindow.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-iframe-contentWindow.html
new file mode 100644
index 0000000000..4aea4aac81
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-iframe-contentWindow.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>"load" & "pageshow" events do not fire on contentWindow of iframe that stays on the initial empty document</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<body></body>
+<script>
+"use strict";
+
+promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ return assertNoLoadAndPageshowEvent(t, iframe.contentWindow);
+}, "load & pageshow event do not fire on contentWindow of <iframe> element created with no src");
+
+promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ iframe.src = "";
+ document.body.appendChild(iframe);
+ return assertNoLoadAndPageshowEvent(t, iframe.contentWindow);
+}, "load & pageshow events do not fire on contentWindow of <iframe> element created with src=''");
+
+promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ iframe.src = "about:blank";
+ document.body.appendChild(iframe);
+ return assertNoLoadAndPageshowEvent(t, iframe.contentWindow);
+}, "load & pageshow events do not fire on contentWindow of <iframe> element created with src='about:blank'");
+
+promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ iframe.src = "about:blank#foo";
+ document.body.appendChild(iframe);
+ return assertNoLoadAndPageshowEvent(t, iframe.contentWindow);
+}, "load & pageshow events do not fire on contentWindow of <iframe> element created with src='about:blank#foo'");
+
+promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ iframe.src = "about:blank?foo";
+ document.body.appendChild(iframe);
+ return assertNoLoadAndPageshowEvent(t, iframe.contentWindow);
+}, "load & pageshow events do not fire on contentWindow of <iframe> element created with src='about:blank?foo'");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-window-open.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-window-open.html
new file mode 100644
index 0000000000..9703502f7f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-window-open.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>"load" and "pageshow" events don't fire on window.open() that stays on the initial empty document</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<body></body>
+<script>
+"use strict";
+
+promise_test(async t => {
+ const w = window.open();
+ return assertNoLoadAndPageshowEvent(t, w)
+}, "load event does not fire on window.open()");
+
+promise_test(async t => {
+ const w = window.open("");
+ return assertNoLoadAndPageshowEvent(t, w)
+}, "load event does not fire on window.open('')");
+
+promise_test(async t => {
+ const w = window.open("about:blank");
+ return assertNoLoadAndPageshowEvent(t, w)
+}, "load event does not fire on window.open('about:blank')");
+
+promise_test(async t => {
+ const w = window.open("about:blank#foo");
+ return assertNoLoadAndPageshowEvent(t, w)
+}, "load event does not fire on window.open('about:blank#foo')");
+
+promise_test(async t => {
+ const w = window.open("about:blank?foo");
+ return assertNoLoadAndPageshowEvent(t, w)
+}, "load event does not fire on window.open('about:blank?foo')");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/resources/code-injector.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/resources/code-injector.html
new file mode 100644
index 0000000000..631b95f9ed
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/resources/code-injector.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Subframe</title>
+
+<script>
+"use strict";
+{{GET[code]}}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/resources/helpers.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/resources/helpers.js
new file mode 100644
index 0000000000..8d9473a949
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/resources/helpers.js
@@ -0,0 +1,121 @@
+// Returns a promise that asserts the "load" and "pageshow" events are not
+// fired on |target|.
+function assertNoLoadAndPageshowEvent(t, target) {
+ target.addEventListener("load", t.unreached_func("load should not be fired"));
+ target.addEventListener("pageshow", t.unreached_func("pageshow should not be fired"));
+ return new Promise(resolve => {
+ // Wait 50ms to ensure events fired after asynchronous navigations are
+ // also captured.
+ setTimeout(resolve, 50);
+ });
+}
+
+const url204 = "/common/blank.html?pipe=status(204)";
+const postMessageToOpenerOnLoad = `
+ window.onload = () => {
+ window.opener.postMessage("loaded", "*")
+ }
+ `;
+
+// -- Start of helpers for iframe initial empty document tests.
+
+// Creates an iframe with an unset src and appends it to the document.
+window.insertIframe = (t) => {
+ const iframe = document.createElement("iframe");
+ t.add_cleanup(() => iframe.remove());
+ document.body.append(iframe);
+ return iframe;
+};
+
+// Creates an iframe with src set to a URL that doesn't commit a new document
+// (results in a HTTP 204 response) and appends it to the document.
+window.insertIframeWith204Src = (t) => {
+ const iframe = document.createElement("iframe");
+ iframe.src = url204;
+ t.add_cleanup(() => iframe.remove());
+ document.body.append(iframe);
+ return iframe;
+};
+
+// Creates an iframe with src="about:blank" and appends it to the document.
+window.insertIframeWithAboutBlankSrc = (t) => {
+ const iframe = document.createElement("iframe");
+ t.add_cleanup(() => iframe.remove());
+ iframe.src = "about:blank";
+ document.body.append(iframe);
+ return iframe;
+};
+
+// Creates an iframe with src="about:blank", appends it to the document, and
+// waits for the non-initial about:blank document finished loading.
+window.insertIframeWithAboutBlankSrcWaitForLoad = async (t) => {
+ const iframe = insertIframeWithAboutBlankSrc(t);
+ const aboutBlankLoad = new Promise(resolve => {
+ // In some browsers, the non-initial about:blank navigation commits
+ // asynchronously, while in other browsers, it would commit synchronously.
+ // This means we can't wait for the "load" event as it might have already
+ // ran. Instead, just wait for 100ms before resolving, as the non-initial
+ // about:blank navigation will most likely take less than 100 ms to commit.
+ t.step_timeout(resolve, 100);
+ });
+ await aboutBlankLoad;
+ return iframe;
+};
+
+// Waits for the "load" event for |urlRelativeToThisDocument| to run on
+// |iframe|.
+window.waitForLoad = (t, iframe, urlRelativeToThisDocument) => {
+ return new Promise(resolve => {
+ iframe.addEventListener("load", t.step_func(() => {
+ assert_equals(iframe.contentWindow.location.href, (new URL(urlRelativeToThisDocument, location.href)).href);
+
+ // Wait a bit longer to ensure all history stuff has settled, e.g. the document is "completely loaded"
+ // (which happens from a queued task).
+ setTimeout(resolve, 0);
+ }), { once: true });
+ });
+};
+
+// -- End of helpers for iframe initial empty document tests.
+
+// -- Start of helpers for opened windows' initial empty document tests.
+
+// window.open() to a URL that doesn't load a new document (results in a HTTP
+// 204 response). This should create a new window that stays on the initial
+// empty document.
+window.windowOpen204 = (t) => {
+ const openedWindow = window.open(url204);
+ t.add_cleanup(() => openedWindow.close());
+ return openedWindow;
+};
+
+// window.open() (no URL set). This should create a new window that stays on
+// the initial empty document as it won't trigger a non-initial about:blank
+// navigation.
+window.windowOpenNoURL = (t) => {
+ const openedWindow = window.open();
+ t.add_cleanup(() => openedWindow.close());
+ return openedWindow;
+};
+
+// window.open("about:blank"). This should create a new window that stays on
+// the initial empty document as it won't trigger a non-initial about:blank
+// navigation.
+window.windowOpenAboutBlank = (t) => {
+ const openedWindow = window.open("about:blank");
+ t.add_cleanup(() => openedWindow.close());
+ return openedWindow;
+};
+
+// Waits for a postMessage with data set to |message| is received on the current
+// window.
+window.waitForMessage = (t, message) => {
+ return new Promise(resolve => {
+ window.addEventListener("message", t.step_func((event) => {
+ if (event.data == message)
+ resolve();
+ }));
+ });
+};
+
+// -- End of helpers for opened windows' initial empty document tests.
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-204-fragment.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-204-fragment.html
new file mode 100644
index 0000000000..bb47cd3820
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-204-fragment.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>Fragment navigation on initial empty document created through window.open(url-with-204-response)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<body></body>
+<script>
+/*
+ When a new window is opened through window.open() it will contain the initial
+ empty document. If the URL parameter is set to the URL that doesn't load a
+ new document (e.g. it results in a HTTP 204 response), it will stay on the
+ initial empty document. If fragment navigations happen, it will still stay on
+ the initial empty document.
+ These tests verify the behavior of navigations that happen on the initial
+ empty document in that situation. They should all be converted to do a
+ replacement.
+*/
+"use strict";
+const url1 = "about:blank#foo";
+const url2 = "resources/code-injector.html?2&pipe=sub(none)&code=" +
+ encodeURIComponent(postMessageToOpenerOnLoad);
+
+promise_test(async t => {
+ // Open a new window with a URL that doesn't load a new document, so it will stay in the initial empty document.
+ const openedWindow = windowOpen204(t);
+
+ // Do a fragment navigation within the initial empty document through setting location.href.
+ // This should do a replacement.
+ openedWindow.location.href = url1;
+ await new Promise(resolve => t.step_timeout(resolve, 100));
+ assert_equals(openedWindow.location.hash, "#foo");
+ assert_equals(openedWindow.history.length, 1,
+ "history.length should not increase after fragment navigation on initial empty document");
+
+ // Navigate away from the initial empty document through setting location.href.
+ // This should do a replacement.
+ openedWindow.location.href = url2;
+ await waitForMessage(t, "loaded");
+ assert_equals(openedWindow.history.length, 1,
+ "history.length should not increase after normal navigation away from initial empty document");
+}, "location.href");
+
+promise_test(async t => {
+ // Open a new window with a URL that doesn't load a new document, so it will stay in the initial empty document.
+ const openedWindow = windowOpen204(t);
+
+ // Do a fragment navigation within the initial empty document through location.assign().
+ // This should do a replacement.
+ openedWindow.location.assign(url1);
+ await new Promise(resolve => t.step_timeout(resolve, 100));
+ assert_equals(openedWindow.location.hash, "#foo");
+ assert_equals(openedWindow.history.length, 1,
+ "history.length should not increase after fragment navigation on initial empty document");
+
+ // Navigate away from the initial empty document through location.assign().
+ // This should do a replacement.
+ openedWindow.location.assign(url2);
+ await waitForMessage(t, "loaded");
+ assert_equals(openedWindow.history.length, 1,
+ "history.length should not increase after normal navigation away from initial empty document");
+}, "location.assign");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-204-pushState-replaceState.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-204-pushState-replaceState.html
new file mode 100644
index 0000000000..b5382b189f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-204-pushState-replaceState.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>Fragment navigation on initial empty document created through window.open(url-with-204-response)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<body></body>
+<script>
+/*
+ When a new window is opened through window.open() it will contain the initial
+ empty document. If the URL parameter is set to the URL that doesn't load a
+ new document (e.g. it results in a HTTP 204 response), it will stay on the
+ initial empty document. If history.pushState() or history.replaceState() is
+ called on it, it should still stay on the initial empty document.
+ These tests verify the behavior of navigations that happen on the initial
+ empty document in that situation. They should all be converted to do a
+ replacement.
+*/
+"use strict";
+const url1 = "about:blank#foo";
+const url2 = "resources/code-injector.html?2&pipe=sub(none)&code=" +
+ encodeURIComponent(postMessageToOpenerOnLoad);
+
+promise_test(async t => {
+ // Open a new window with a URL that doesn't load a new document, so it will stay in the initial empty document.
+ const openedWindow = windowOpen204(t);
+
+ // Do a history.pushState() to about:blank#foo.
+ let pushURL = "about:blank#foo";
+ openedWindow.history.pushState({}, "title", pushURL);
+ assert_equals(openedWindow.location.href, pushURL);
+ assert_equals(history.length, 1,
+ "history.length must not change after history.pushState() on the initial empty document");
+
+ // Navigate away from the initial empty document through setting location.href.
+ // This should do a replacement.
+ openedWindow.location.href = url2;
+ await waitForMessage(t, "loaded");
+ assert_equals(openedWindow.history.length, 1,
+ "history.length should not increase after normal navigation away from initial empty document");
+}, "history.pushState");
+
+promise_test(async t => {
+ // Open a new window with a URL that doesn't load a new document, so it will stay in the initial empty document.
+ const openedWindow = windowOpen204(t);
+
+ // Do a history.pushState() to about:blank#foo.
+ let replaceURL = "about:blank#foo";
+ openedWindow.history.replaceState({}, "title", replaceURL);
+ assert_equals(openedWindow.location.href, replaceURL);
+ assert_equals(history.length, 1,
+ "history.length must not change after history.replaceState() on the initial empty document");
+
+ // Navigate away from the initial empty document through location.assign().
+ // This should do a replacement.
+ openedWindow.location.assign(url2);
+ await waitForMessage(t, "loaded");
+ assert_equals(openedWindow.history.length, 1,
+ "history.length should not increase after normal navigation away from initial empty document");
+}, "history.replaceState");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-204.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-204.html
new file mode 100644
index 0000000000..6461b3c8fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-204.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>Navigations on window.open() with URL that doesn't load a new document</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<body></body>
+<script>
+/*
+ When a new window is opened through window.open() it will contain the initial
+ empty document. If the URL parameter is set to the URL that doesn't load a
+ new document (e.g. it results in a HTTP 204 response), it will stay on the
+ initial empty document.
+ These tests verify the behavior of navigations that happen on the initial
+ empty document in that situation. They should all be converted to do a
+ replacement.
+*/
+"use strict";
+const url1 = "resources/code-injector.html?1&pipe=sub(none)&code=" +
+ encodeURIComponent(postMessageToOpenerOnLoad);
+const url2 = "resources/code-injector.html?2&pipe=sub(none)&code=" +
+ encodeURIComponent(postMessageToOpenerOnLoad);
+
+promise_test(async t => {
+ // Open a new window with a URL that doesn't load a new document, so it will stay in the initial empty document.
+ const openedWindow = windowOpen204(t);
+
+ // Navigate away from the initial empty document through setting
+ // location.href. This should do a replacement.
+ openedWindow.location.href = url1;
+ await waitForMessage(t, "loaded");
+ assert_equals(openedWindow.history.length, 1,
+ "history.length should not increase after normal navigation away from initial empty document");
+
+ // Navigate again using the same method, but this time it shouldn't do a
+ // replacement since it's no longer on the initial empty document.
+ openedWindow.location.href = url2;
+ await waitForMessage(t, "loaded");
+ assert_equals(openedWindow.history.length, 2,
+ "history.length should increase after normal navigation away from non-initial empty document");
+}, "location.href");
+
+promise_test(async t => {
+ // Open a new window with a URL that doesn't load a new document, so it will stay in the initial empty document.
+ const openedWindow = windowOpen204(t);
+
+ // Navigate away from the initial empty document through location.assign().
+ // This should do a replacement.
+ openedWindow.location.assign(url1);
+ await waitForMessage(t, "loaded");
+ assert_equals(openedWindow.history.length, 1,
+ "history.length should not increase after normal navigation away from initial empty document");
+
+ // Navigate again using the same method, but this time it shouldn't do a
+ // replacement since it's no longer on the initial empty document.
+ openedWindow.location.assign(url2);
+ await waitForMessage(t, "loaded");
+ assert_equals(openedWindow.history.length, 2,
+ "history.length should increase after normal navigation away from non-initial empty document");
+}, "location.assign");
+/*
+promise_test(async t => {
+ // Open a new window with a URL that doesn't load a new document, so it will stay in the initial empty document.
+ const openedWindow = windowOpen204(t);
+
+ // Navigate away from the initial empty document through setting
+ // window.open(). This should do a replacement.
+ openedWindow.open(url1, "_self");
+ await waitForMessage(t, "loaded");
+ assert_equals(openedWindow.history.length, 1,
+ "history.length should increase after normal navigation away from non-initial empty document");
+
+ // Navigate again using the same method, but this time it shouldn't do a
+ // replacement since it's no longer on the initial empty document.
+ openedWindow.open(url2, "_self");
+ await waitForMessage(t, "loaded");
+ assert_equals(openedWindow.history.length, 2,
+ "history.length should increase after normal navigation away from non-initial empty document");
+}, "window.open");
+*/
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-aboutblank.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-aboutblank.html
new file mode 100644
index 0000000000..f3033d6a21
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-aboutblank.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>Navigations on window.open(about:blank) after waiting for it to load</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<body></body>
+<script>
+/*
+ When a new window is opened through window.open() it will contain the initial
+ empty document. If the URL parameter is set to about:blank, it will stay on
+ the initial empty document (unlike iframes with src="about:blank", which will
+ start a navigation to a non-initial about:blank document).
+ These tests verify the behavior of navigations that happen on the initial
+ empty document in that situation. They should all be converted to do a
+ replacement.
+*/
+"use strict";
+const url1 = "resources/code-injector.html?1&pipe=sub(none)&code=" +
+ encodeURIComponent(postMessageToOpenerOnLoad);
+const url2 = "resources/code-injector.html?2&pipe=sub(none)&code=" +
+ encodeURIComponent(postMessageToOpenerOnLoad);
+
+promise_test(async t => {
+ // Open a window with URL about:blank, which will commit the
+ // initial empty document and stay on it.
+ const openedWindow = windowOpenAboutBlank(t);
+
+ // Unlike iframe with src="about:blank", window.open("about:blank") won't
+ // trigger a navigation to a non-initial about:blank document, so it should
+ // stay on the initial empty document. To verify, wait for 100ms before
+ // continuing.
+ await new Promise((resolve) => t.step_timeout(resolve, 100));
+
+ // Navigate away from the initial empty document through location.href.
+ // This should do a replacement.
+ openedWindow.location.href = url1;
+ await waitForMessage(t, "loaded");
+ assert_equals(openedWindow.history.length, 1,
+ "history.length should not increase after normal navigation away from initial empty document");
+}, "location.href");
+
+promise_test(async t => {
+ // Open a window with URL about:blank, which will commit the
+ // initial empty document and stay on it.
+ const openedWindow = windowOpenAboutBlank(t);
+
+ // Unlike iframe with src="about:blank", window.open("about:blank") won't
+ // trigger a navigation to a non-initial about:blank document, so it should
+ // stay on the initial empty document. To verify, wait for 100ms before
+ // continuing.
+ await new Promise((resolve) => t.step_timeout(resolve, 100));
+
+ // Navigate away from the initial empty document through location.assign().
+ // This should do a replacement.
+ openedWindow.location.assign(url1);
+ await waitForMessage(t, "loaded");
+ assert_equals(openedWindow.history.length, 1,
+ "history.length should not increase after normal navigation away from initial empty document");
+}, "location.assign");
+/*
+promise_test(async t => {
+ // Open a window with URL about:blank, which will commit the
+ // initial empty document and stay on it.
+ const openedWindow = windowOpenAboutBlank(t);
+
+ // Unlike iframe with src="about:blank", window.open("about:blank") won't
+ // trigger a navigation to a non-initial about:blank document, so it should
+ // stay on the initial empty document. To verify, wait for 100ms before
+ // continuing.
+ await new Promise((resolve) => t.step_timeout(resolve, 100));
+
+ // Navigate away from the initial empty document through window.open().
+ // This should do a replacement.
+ openedWindow.open(url1, "_self");
+ await waitForMessage(t, "loaded");
+ assert_equals(openedWindow.history.length, 1,
+ "history.length should not increase after normal navigation away from initial empty document");
+}, "window.open");
+*/
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-history-length.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-history-length.html
new file mode 100644
index 0000000000..ab89bc4098
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-history-length.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>history.length value on window.open()-ed window</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<body></body>
+<script>
+/*
+ When a new window is opened through window.open() it will contain the initial
+ empty document, and the history.length value should be 1.
+*/
+
+promise_test(async t => {
+ const openedWindow = windowOpenNoURL(t);
+ assert_equals(openedWindow.history.length, 1,
+ "history.length should start at 1 for newly opened window");
+}, "Starting history.length for window.open()");
+
+promise_test(async t => {
+ const openedWindow = windowOpenAboutBlank(t);
+ assert_equals(openedWindow.history.length, 1,
+ "history.length should start at 1 for newly opened window");
+}, "Starting history.length for window.open(about:blank)");
+
+promise_test(async t => {
+ const openedWindow = windowOpen204(t);
+ assert_equals(openedWindow.history.length, 1,
+ "history.length should start at 1 for newly opened window");
+}, "Starting history.length for window.open(url-with-204-response)");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-nourl.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-nourl.html
new file mode 100644
index 0000000000..801f77ad48
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/window-open-nourl.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>Navigations on window.open() to URL that doesn't load a new document</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<body></body>
+<script>
+/*
+ When a new window is opened through window.open() it will contain the initial
+ empty document. If the URL parameter is not set, it will stay on the initial
+ empty document.
+ These tests verify the behavior of navigations that happen on the initial
+ empty document in that situation. They should all be converted to do a
+ replacement.
+*/
+"use strict";
+const url1 = "resources/code-injector.html?1&pipe=sub(none)&code=" + encodeURIComponent(postMessageToOpenerOnLoad);
+const url2 = "resources/code-injector.html?2&pipe=sub(none)&code=" + encodeURIComponent(postMessageToOpenerOnLoad);
+
+promise_test(async t => {
+ // Open a new window with no URL, which will stay in the initial empty document until the navigation below.
+ const openedWindow = windowOpenNoURL(t);
+
+ // Navigate away from the initial empty document through setting
+ // location.href. This should do a replacement.
+ openedWindow.location.href = url1;
+ await waitForMessage(t, "loaded");
+ assert_equals(openedWindow.history.length, 1,
+ "history.length should not increase after normal navigation away from initial empty document");
+
+ // Navigate again using the same method, but this time it shouldn't do a
+ // replacement since it's no longer on the initial empty document.
+ openedWindow.location.href = url2;
+ await waitForMessage(t, "loaded");
+ assert_equals(openedWindow.history.length, 2,
+ "history.length should increase after normal navigation away from non-initial empty document");
+}, "location.href");
+
+promise_test(async t => {
+ // Open a new window with no URL, which will stay in the initial empty document until the navigation below.
+ const openedWindow = windowOpenNoURL(t);
+
+ // Navigate away from the initial empty document through location.assign().
+ // This should do a replacement.
+ openedWindow.location.assign(url1);
+ await waitForMessage(t, "loaded");
+ assert_equals(openedWindow.history.length, 1,
+ "history.length should not increase after normal navigation away from initial empty document");
+
+ // Navigate again using the same method, but this time it shouldn't do a
+ // replacement since it's no longer on the initial empty document.
+ openedWindow.location.assign(url2);
+ await waitForMessage(t, "loaded");
+ assert_equals(openedWindow.history.length, 2,
+ "history.length should increase after normal navigation away from non-initial empty document");
+}, "location.assign");
+/*
+promise_test(async t => {
+ // Open a new window with no URL, which will stay in the initial empty document until the navigation below.
+ const openedWindow = windowOpenNoURL(t);
+
+ // Navigate away from the initial empty document through setting
+ // window.open(). This should do a replacement.
+ openedWindow.open(url1, "_self");
+ await waitForMessage(t, "loaded");
+ assert_equals(openedWindow.history.length, 1,
+ "history.length should not increase after normal navigation away from initial empty document");
+
+ // Navigate again using the same method, but this time it shouldn't do a
+ // replacement since it's no longer on the initial empty document.
+ openedWindow.open(url2, "_self");
+ await waitForMessage(t, "loaded");
+ assert_equals(openedWindow.history.length, 2,
+ "history.length should increase after normal navigation away from non-initial empty document");
+}, "window.open");
+*/
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-abort/javascript-url-abort-return-value-string.tentative.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-abort/javascript-url-abort-return-value-string.tentative.html
new file mode 100644
index 0000000000..f626a79ae6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-abort/javascript-url-abort-return-value-string.tentative.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<title>Aborting fetch for javascript:string navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#navigate">
+<link rel="help" href="https://github.com/whatwg/html/issues/2590">
+<div id="log"></div>
+<iframe src="support/iframe-and-links.html"></iframe>
+<script>
+async_test(test => {
+ onload = () => {
+ const child = document.querySelector('iframe').contentWindow;
+ child.document.querySelector("#slowLink").click();
+ // The step below is in a timeout. The framed page can't communicate back mid-parse because that
+ // would involve running script, which makes that navigation "mature", and we need to do this
+ // before it matures.
+ test.step_timeout(() => {
+ child.document.querySelector("#javascriptStringLink").click();
+ child.document.querySelector("iframe").onload = test.step_func_done(() => {
+ assert_false(child.childLoaded, 'child.childLoaded');
+ });
+ }, 100);
+ };
+ window.javascriptStringDocLoaded = test.step_func(() => {
+ assert_unreached("javascript: URL doc replaced the document, should be targeted to child iframe.");
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-abort/javascript-url-abort-return-value-undefined.tentative.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-abort/javascript-url-abort-return-value-undefined.tentative.html
new file mode 100644
index 0000000000..80a0d27a7d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-abort/javascript-url-abort-return-value-undefined.tentative.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<title>Not aborting fetch for javascript:undefined navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#navigate">
+<link rel="help" href="https://github.com/whatwg/html/issues/2590">
+<div id="log"></div>
+<iframe src="support/iframe-and-links.html"></iframe>
+<script>
+async_test(test => {
+ onload = () => {
+ const child = document.querySelector('iframe').contentWindow;
+ child.document.querySelector("#slowLink").click();
+ // The step below is in a timeout. The framed page can't communicate back mid-parse because that
+ // would involve running script, which makes that navigation "mature", and we need to do this
+ // before it matures.
+ test.step_timeout(() => {
+ child.document.querySelector("#javascriptUndefinedLink").click();
+ child.document.querySelector("iframe").onload = test.step_func_done(() => {
+ assert_true(child.childLoaded, 'child.childLoaded');
+ });
+ }, 100);
+ };
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-abort/support/iframe-and-links.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-abort/support/iframe-and-links.html
new file mode 100644
index 0000000000..545b0988ba
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-abort/support/iframe-and-links.html
@@ -0,0 +1,18 @@
+<!doctype html>
+
+<iframe name="iframe"></iframe>
+
+<!-- slow link's response is delayed by 1 second -->
+<!-- https://wptserve.readthedocs.io/en/latest/pipes.html#trickle -->
+<a target="iframe" href="set-child-loaded.html?pipe=trickle(d1)" id="slowLink">slow link</a>
+<a target="iframe" href="javascript:'javascript:string <script> parent.javascriptStringDocLoaded(); </script>'" id="javascriptStringLink">javascript:string link</a>
+<a target="iframe" href="javascript:undefined" id="javascriptUndefinedLink">javascript:undefined link</a>
+
+<script>
+// set-child-loaded.html (the slow link) sets this to true.
+window.childLoaded = false;
+
+// Do nothing when the javascript:string doc has loaded, if it's correctly targeted to the above iframe.
+// However, if it replaces this document, it needs to fail the test (handled in the parent).
+function javascriptStringDocLoaded() {}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-abort/support/set-child-loaded.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-abort/support/set-child-loaded.html
new file mode 100644
index 0000000000..a4b34ad6e9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-abort/support/set-child-loaded.html
@@ -0,0 +1,5 @@
+<!doctype html>
+set-child-loaded.html
+<script>
+parent.childLoaded = true;
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-global-scope.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-global-scope.html
new file mode 100644
index 0000000000..d3dd38ebed
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-global-scope.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+
+<a id="javascript-link" href="javascript:changeStatus()">link</a>
+
+<script>
+function changeStatus() {
+ t.done();
+}
+
+var t = async_test(function(t) {
+ document.querySelector("#javascript-link").click();
+}, "javascript: scheme urls should be executed in current global scope");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-load-as-html.xhtml b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-load-as-html.xhtml
new file mode 100644
index 0000000000..b23bef5917
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-load-as-html.xhtml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="windows-1250"?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../resources/helpers.js"></script>
+ <meta charset="windows-1250"/>
+ <title>javascript: URL navigation to a string must create a HTML document using the correct properties</title>
+</head>
+<body>
+ <!--
+ This document is XHTML and windows-1250 so that we can test the resulting javascript: URL document is not.
+ The same for the window we open.
+ -->
+ <script><![CDATA[
+ promise_test(async (t) => {
+ const w = await openWindow("resources/xhtml-and-non-utf-8.xhtml", t);
+
+ w.location.href = `javascript:'a string<script>
+ opener.postMessage({
+ compatMode: document.compatMode,
+ contentType: document.contentType,
+ characterSet: document.characterSet,
+ doctypeIsNull: document.doctype === null
+ }, "*");
+ <` + `/script>'`;
+
+ const results = await waitForMessage(w);
+
+ assert_equals(results.compatMode, "BackCompat");
+ assert_equals(results.contentType, "text/html");
+ assert_equals(results.characterSet, "UTF-8");
+ assert_equals(results.doctypeIsNull, true);
+ });
+ ]]></script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-no-beforeunload.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-no-beforeunload.window.js
new file mode 100644
index 0000000000..47e8f11797
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-no-beforeunload.window.js
@@ -0,0 +1,80 @@
+// META: script=../resources/helpers.js
+
+for (const stringCompletion of [false, true]) {
+ const testNameSuffix = stringCompletion ? ": string completion" : ": undefined completion";
+
+ testNoBeforeunload(
+ { testRunnerWindow: "top", stringCompletion },
+ async (t, urlToSet) => {
+ const iframe = await addIframe();
+ iframe.contentWindow.location.href = urlToSet;
+
+ return iframe.contentWindow;
+ },
+ `Navigating an iframe via location.href to a javascript: URL must not fire beforeunload${testNameSuffix}`
+ );
+
+ testNoBeforeunload(
+ { testRunnerWindow: "top", stringCompletion },
+ async (t, urlToSet) => {
+ const iframe = await addIframe();
+ iframe.src = urlToSet;
+
+ return iframe.contentWindow;
+ },
+ `Navigating an iframe via src="" to a javascript: URL after insertion must not fire beforeunload${testNameSuffix}`
+ );
+
+ testNoBeforeunload(
+ { testRunnerWindow: "opener", stringCompletion },
+ async (t, urlToSet) => {
+ const w = await openWindow("/common/blank.html", t);
+ w.location.href = urlToSet;
+
+ return w;
+ },
+ `Navigating an opened window via location.href to a javascript: URL must not fire beforeunload${testNameSuffix}`
+ );
+
+
+ testNoBeforeunload(
+ { testRunnerWindow: "opener", stringCompletion },
+ async (t, urlToSet) => {
+ const w = await openWindow("../resources/has-iframe.html", t);
+ w.frames[0].onbeforeunload = t.unreached_func("beforeunload must not fire on the iframe");
+ w.location.href = urlToSet;
+
+ return w;
+ },
+ `Navigating an opened window with an iframe via location.href to a javascript: URL must not fire beforeunload on the iframe${testNameSuffix}`
+ );
+}
+
+function testNoBeforeunload({ testRunnerWindow, stringCompletion }, setupAndNavigateFunc, description) {
+ promise_test(async t => {
+ t.add_cleanup(() => {
+ delete window.resolveTestPromise;
+ });
+
+ const ranPromise = new Promise(resolve => {
+ window.resolveTestPromise = resolve;
+ });
+
+ const urlToSet = makeURL({ testRunnerWindow, stringCompletion });
+ const w = await setupAndNavigateFunc(t, urlToSet);
+ w.onbeforeunload = t.unreached_func("beforeunload must not fire");
+
+ await ranPromise;
+ if (stringCompletion) {
+ await waitForMessage(w);
+ }
+ }, description);
+}
+
+function makeURL({ testRunnerWindow, stringCompletion }) {
+ const completion = stringCompletion ?
+ `"a string<script>window.${testRunnerWindow}.postMessage('ready', '*');</script>";` :
+ `undefined;`;
+
+ return `javascript:window.${testRunnerWindow}.resolveTestPromise();${completion};`;
+}
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-query-fragment-components.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-query-fragment-components.html
new file mode 100644
index 0000000000..eced9646e5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-query-fragment-components.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<title> javascript url with query and fragment components </title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+var a = null;
+var b = null;
+var c = null;
+</script>
+
+<iframe id="a" src='javascript:"nope" ? "yep" : "what";'></iframe>
+<iframe id="b" src='javascript:"wrong"; // # %0a "ok";'></iframe>
+<iframe id="c" src='javascript:"%252525 ? %252525 # %252525"'></iframe>
+
+<script>
+var t = async_test("iframes with javascript src");
+function check(id, expected) {
+ assert_equals(
+ document.getElementById(id).contentDocument.body.textContent,
+ expected);
+}
+onload = t.step_func(function() {
+ check("a", "yep");
+ check("b", "ok");
+ check("c", "%2525 ? %2525 # %2525");
+ t.done();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-referrer.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-referrer.window.js
new file mode 100644
index 0000000000..1f11429c9e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-referrer.window.js
@@ -0,0 +1,38 @@
+// META: script=../resources/helpers.js
+// META: title=javascript: URL navigation to a string must create a document whose referrer is the navigation initiator
+
+const originalURL = location.href;
+
+const testCases = [
+ ["unsafe-url", location.href],
+ ["origin", self.origin + "/"],
+ ["no-referrer", ""]
+];
+
+for (const [referrerPolicyForStartingWindowCreation, expectedReferrer] of testCases) {
+ promise_test(async (t) => {
+ const meta = document.createElement("meta");
+ meta.name = "referrer";
+ meta.content = referrerPolicyForStartingWindowCreation;
+ t.add_cleanup(() => meta.remove());
+ document.head.append(meta);
+
+ const w = await openWindow("/common/blank.html", t);
+ const originalReferrer = w.document.referrer;
+ assert_equals(originalReferrer, expectedReferrer,
+ "Sanity check: opened window's referrer is set correctly");
+
+ // Mess with the current document's URL so that the initiator URL is different. Then, if that
+ // shows up as the javascript: URL document's referrer, we know the navigation initiator's URL is
+ // being used as the referrer, which is incorrect.
+ history.replaceState(undefined, "", "/incorrect-referrer.html");
+ t.add_cleanup(() => history.replaceState(undefined, "", originalURL));
+
+ w.location.href = `javascript:'a string<script>opener.postMessage(document.referrer, "*");</script>'`;
+
+ const referrer = await waitForMessage(w);
+
+ assert_equals(referrer, originalReferrer,
+ "javascript: URL-created document's referrer equals the previous document's referrer");
+ }, `${referrerPolicyForStartingWindowCreation} referrer policy used to create the starting page`);
+}
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-return-value-handling-dynamic.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-return-value-handling-dynamic.html
new file mode 100644
index 0000000000..3c08d29674
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-return-value-handling-dynamic.html
@@ -0,0 +1,58 @@
+<!doctype html>
+<meta charset=windows-1252> <!-- intentionally not UTF-8 to test that the javascript: frames are forced to UTF-8 -->
+<title>Test javascript URL string return values in direct and indirect (target) frame contexts.</title>
+<!-- Waiting on https://github.com/whatwg/html/pull/6781 to be non-tentative. -->
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+const testCases = [
+ [[0x41]],
+ [[0x80,0xFF]],
+ [[0x80,0xFF,0x100]],
+ [[0xD83D,0xDE0D]],
+ [[0xDE0D,0x41], [0xFFFD,0x41]]
+];
+
+function formatCharCodes(charCodes) {
+ return charCodes.map(code => code.toString(16).toUpperCase().padStart(4, '0')).join(" ");
+}
+
+for (const [input, expected = input] of testCases) {
+ const javascriptURL = `javascript:String.fromCharCode(${input})`;
+ const output = String.fromCharCode(...expected);
+
+ async_test(t => {
+ const frame = document.createElement("iframe");
+ t.add_cleanup(() => frame.remove());
+ frame.src = javascriptURL;
+
+ t.step_timeout(() => {
+ assert_equals(frame.contentDocument.body.textContent, output);
+ assert_equals(frame.contentDocument.charset, "UTF-8");
+ t.done();
+ }, 200);
+
+ document.body.appendChild(frame);
+ }, `${formatCharCodes(input)} set in src=""`);
+
+ async_test(t => {
+ const frame = document.createElement("iframe");
+ const href = document.createElement("a");
+ t.add_cleanup(() => { frame.remove(); href.remove(); });
+ frame.name = "hi" + input;
+ href.target = "hi" + input;
+ href.href = javascriptURL;
+
+ t.step_timeout(() => {
+ assert_equals(frame.contentDocument.body.textContent, output);
+ assert_equals(frame.contentDocument.charset, "UTF-8");
+ t.done();
+ }, 200)
+
+ document.body.appendChild(frame);
+ document.body.appendChild(href);
+ href.click();
+ }, `${formatCharCodes(input)} set in href="" targeting a frame and clicked`);
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-return-value-handling.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-return-value-handling.html
new file mode 100644
index 0000000000..621a8cbaec
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-return-value-handling.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Test that javascript: evaluation only performs a navigation to the
+ result when the result is a string value.</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<iframe src="javascript:'1'"></iframe>
+<iframe src="javascript:1"></iframe>
+<iframe src="javascript:({ toString: function() { return '1'; } })"></iframe>
+<iframe src="javascript:undefined"></iframe>
+<iframe src="javascript:null"></iframe>
+<iframe src="javascript:true"></iframe>
+<iframe src="javascript:new String('1')"></iframe>
+<script>
+ var t = async_test();
+ onload = t.step_func_done(function() {
+ assert_equals(frames[0].document.documentElement.textContent,
+ "1", "string return should cause navigation");
+ // The rest of the test is disabled for now, until
+ // https://github.com/whatwg/html/issues/1895 gets sorted out
+/*
+ assert_equals(frames[1].document.documentElement.textContent,
+ "", "number return should not cause navigation");
+ assert_equals(frames[2].document.documentElement.textContent,
+ "", "object return should not cause navigation");
+ assert_equals(frames[3].document.documentElement.textContent,
+ "", "undefined return should not cause navigation");
+ assert_equals(frames[4].document.documentElement.textContent,
+ "", "null return should not cause navigation");
+ assert_equals(frames[5].document.documentElement.textContent,
+ "", "null return should not cause navigation");
+ assert_equals(frames[6].document.documentElement.textContent,
+ "", "String object return should not cause navigation");
+*/
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-security-check-failure.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-security-check-failure.sub.html
new file mode 100644
index 0000000000..a153ad3e48
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-security-check-failure.sub.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>javascript: URL security check</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+"use strict";
+
+const cases = [
+ ["cross-origin", "http://{{hosts[][www]}}:{{ports[http][0]}}/common/blank.html"],
+ ["cross-origin-domain but same-origin", "/html/browsers/windows/resources/document-domain-setter.html"]
+];
+
+for (const [description, url] of cases) {
+ promise_test(async t => {
+ const iframe = await insertIframe(t, url);
+
+ const unreached = t.unreached_func("message event fired");
+ t.add_cleanup(() => window.removeEventListener("message", unreached));
+ window.addEventListener("message", unreached);
+
+ iframe.src = `javascript:parent.postMessage("boo", "*")`;
+
+ // If no message was received after this time, the test passes.
+ await new Promise(r => t.step_timeout(r, 50));
+ }, `${description}, setting src`);
+
+ promise_test(async t => {
+ const iframe = await insertIframe(t, url);
+
+ const unreached = t.unreached_func("message event fired");
+ t.add_cleanup(() => window.removeEventListener("message", unreached));
+ window.addEventListener("message", unreached);
+
+ iframe.contentWindow.location.href = `javascript:parent.postMessage("boo", "*")`;
+
+ // If no message was received after this time, the test passes.
+ await new Promise(r => t.step_timeout(r, 50));
+ }, `${description}, setting location.href`);
+}
+
+function insertIframe(t, url) {
+ return new Promise((resolve, reject) => {
+ const iframe = document.createElement("iframe");
+ iframe.src = url;
+ iframe.onload = () => resolve(iframe);
+ iframe.onerror = () => reject(new Error("Failed to load the outer iframe"));
+
+ t.add_cleanup(() => iframe.remove());
+
+ document.body.append(iframe);
+ });
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-security-check-multi-globals.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-security-check-multi-globals.sub.html
new file mode 100644
index 0000000000..4b9d3b7afa
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-security-check-multi-globals.sub.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Multi-globals: which one is the initiator for the javascript: URL security check?</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+"use strict";
+document.domain = "{{hosts[][]}}";
+
+// These tests would fail if a different pair of origins were compared (see, e.g., the discussion in
+// https://github.com/whatwg/html/issues/6514).
+
+promise_test(async t => {
+ const iframe = await insertIframe(t);
+ const innerIframe = iframe.contentDocument.querySelector("iframe");
+
+ // - incumbentNavigationOrigin = this page's origin, http://{{hosts[][]}}:{{ports[http][0]}}
+ // - iframe's current origin is this origin, http://{{hosts[][]}}:{{ports[http][0]}}.
+ // javascript:'s security check uses incumbentNavigationOrigin vs. the iframe's current origin
+ // so the check will pass and the result will get written.
+ innerIframe.src = "javascript:'test'";
+
+ await waitForLoad(innerIframe, "Failed to load the javascript: URL");
+
+ assert_equals(innerIframe.contentDocument.body.textContent, "test");
+}, "Using iframeEl.src");
+
+promise_test(async t => {
+ const iframe = await insertIframe(t);
+ const innerIframe = iframe.contentDocument.querySelector("iframe");
+
+ // Here, https://html.spec.whatwg.org/#location-object-navigate sets the source browsing context to the
+ // incumbent settings object's browsing context. So incumbentNavigationOrigin = this page's origin,
+ // http://{{hosts[][]}}:{{ports[http][0]}}.
+ //
+ // So again, the check will pass.
+
+ iframe.contentWindow.frames[0].location.href = "javascript:'test'";
+
+ await waitForLoad(innerIframe, "Failed to load the javascript: URL");
+
+ assert_equals(innerIframe.contentDocument.body.textContent, "test");
+}, "Using location.href");
+
+function insertIframe(t) {
+ return new Promise((resolve, reject) => {
+ const iframe = document.createElement("iframe");
+ iframe.src = "http://{{hosts[][www]}}:{{ports[http][0]}}/html/browsers/browsing-the-web/navigating-across-documents/resources/multi-globals-subframe-1.sub.html";
+ iframe.onload = () => resolve(iframe);
+ iframe.onerror = () => reject(new Error("Failed to load the outer iframe"));
+
+ t.add_cleanup(() => iframe.remove());
+
+ document.body.append(iframe);
+ });
+}
+
+function waitForLoad(iframe, errorMessage = "Failed to load iframe") {
+ return new Promise((resolve, reject) => {
+ iframe.onload = () => resolve(iframe);
+ iframe.onerror = () => reject(new Error(errorMessage));
+ });
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-security-check-same-origin-domain.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-security-check-same-origin-domain.sub.html
new file mode 100644
index 0000000000..a14a13cfd6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-security-check-same-origin-domain.sub.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>javascript: URL security check for same-origin-domain but not same-origin</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe src="http://{{hosts[][www]}}:{{ports[http][0]}}/html/browsers/browsing-the-web/navigating-across-documents/resources/document-domain-set-to-site.sub.html"></iframe>
+<script>
+"use strict";
+document.domain = "{{host}}";
+
+setup({ explicit_done: true });
+
+window.onload = () => {
+ async_test(t => {
+ assert_equals(frames[0].document.body.textContent, "", "before");
+
+ window.onmessage = t.step_func_done(() => {
+ assert_equals(frames[0].document.body.textContent, "new", "after");
+ });
+
+ frames[0].location.href = "javascript:parent.postMessage('done', '*'); 'new';";
+ });
+ done();
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-task-queuing.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-task-queuing.html
new file mode 100644
index 0000000000..1bb05bfb19
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-task-queuing.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>javascript: URL task queuing</title>
+<link rel="help" href="https://github.com/whatwg/html/issues/3730">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+"use strict";
+
+testIsAsync(() => {
+ const iframe = document.createElement("iframe");
+ document.body.append(iframe);
+ iframe.contentWindow.location.href = "javascript:window.top.javascriptURLRan = true; window.top.resolveTestPromise();";
+}, `Navigating an iframe via location.href to a javascript: URL must queue a task`);
+
+testIsAsync(() => {
+ const iframe = document.createElement("iframe");
+ iframe.src = "javascript:window.top.javascriptURLRan = true; window.top.resolveTestPromise();";
+ document.body.append(iframe);
+}, `Navigating an iframe via src="" to a javascript: URL before insertion must queue a task`);
+
+testIsAsync(() => {
+ const iframe = document.createElement("iframe");
+ document.body.append(iframe);
+ iframe.src = "javascript:window.top.javascriptURLRan = true; window.top.resolveTestPromise();";
+}, `Navigating an iframe via src="" to a javascript: URL after insertion must queue a task`);
+
+testIsAsync(() => {
+ const w = window.open();
+ w.location.href = "javascript:window.opener.javascriptURLRan = true; window.opener.resolveTestPromise();";
+}, `Navigating an opened window via location.href to a javascript: URL must queue a task`);
+
+testIsAsync(() => {
+ window.open("javascript:window.opener.javascriptURLRan = true; window.opener.resolveTestPromise();");
+}, `Navigating an opened window as part of creation to a javascript: URL must queue a task`);
+
+function testIsAsync(setupFunc, description) {
+ promise_test(async t => {
+ t.add_cleanup(() => {
+ delete window.resolveTestPromise;
+ delete window.javascriptURLRan;
+ });
+
+ const ranPromise = new Promise(resolve => {
+ window.resolveTestPromise = resolve;
+ });
+
+ setupFunc();
+
+ assert_equals(window.javascriptURLRan, undefined, "Must not run sync");
+
+ // Ensure that we do actually run the code, though.
+ await ranPromise;
+ }, description);
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/context-for-location-assign.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/context-for-location-assign.html
new file mode 100644
index 0000000000..cb2984d409
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/context-for-location-assign.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/context-helper.js"></script>
+<script>
+window.scriptToRun = 'relevantWindow.location.assign("target.html");';
+
+async_test(t => {
+ window.addEventListener("message", t.step_func_done(function(e) {
+ // Base URL used for parsing a relative URL to `target.html`
+ // should be the base URL of the entry settings object in
+ // https://html.spec.whatwg.org/C/#dom-location-assign
+ assert_equals(
+ e.data.location,
+ new URL('target.html', entryUrl).href,
+ 'Base URL should use the entry settings object');
+
+ // `document.referrer` should reflect the source browsing context,
+ // which is the incumbent in
+ // https://html.spec.whatwg.org/C/#location-object-navigate
+ assert_equals(
+ e.data.referrer, incumbentUrl,
+ 'Referrer should use the incumbent settings object');
+ }));
+}, 'Fetch client and URL resolution for location.assign()');
+</script>
+<iframe id="entry" src="entry/entry.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/context-for-location-href.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/context-for-location-href.html
new file mode 100644
index 0000000000..02ff214ed5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/context-for-location-href.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/context-helper.js"></script>
+<script>
+window.scriptToRun = 'relevantWindow.location.href = "target.html";';
+
+async_test(t => {
+ window.addEventListener("message", t.step_func_done(function(e) {
+ // Base URL used for parsing a relative URL to `target.html`
+ // should be the base URL of the entry settings object in
+ // https://html.spec.whatwg.org/C/#dom-location-href
+ assert_equals(
+ e.data.location,
+ new URL('target.html', entryUrl).href,
+ 'Base URL should use the entry settings object');
+
+ // `document.referrer` should reflect the source browsing context,
+ // which is the incumbent in
+ // https://html.spec.whatwg.org/C/#location-object-navigate
+ assert_equals(
+ e.data.referrer, incumbentUrl,
+ 'Referrer should use the incumbent settings object');
+ }));
+}, 'Fetch client and URL resolution for location.href setter');
+</script>
+<iframe id="entry" src="entry/entry.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/context-for-location.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/context-for-location.html
new file mode 100644
index 0000000000..fae17dd2ac
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/context-for-location.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/context-helper.js"></script>
+<script>
+window.scriptToRun = 'relevantWindow.location = "target.html";';
+
+async_test(t => {
+ window.addEventListener("message", t.step_func_done(function(e) {
+ // Base URL used for parsing a relative URL to `target.html`
+ // should be the base URL of the entry settings object in
+ // https://html.spec.whatwg.org/C/#dom-location-assign
+ assert_equals(
+ e.data.location,
+ new URL('target.html', entryUrl).href,
+ 'Base URL should use the entry settings object');
+
+ // `document.referrer` should reflect the source browsing context,
+ // which is the incumbent in
+ // https://html.spec.whatwg.org/C/#location-object-navigate
+ assert_equals(
+ e.data.referrer, incumbentUrl,
+ 'Referrer should use the incumbent settings object');
+ }));
+}, 'Fetch client and URL resolution for location setter');
+</script>
+<iframe id="entry" src="entry/entry.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/context-for-window-open.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/context-for-window-open.html
new file mode 100644
index 0000000000..0a391ef28e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/context-for-window-open.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/context-helper.js"></script>
+<script>
+window.scriptToRun =
+ 'relevantWindow.open("target.html", "target");';
+
+async_test(t => {
+ window.addEventListener("message", t.step_func_done(function(e) {
+ // Base URL used for parsing a relative URL to `target.html`
+ // should be the base URL of the entry settings object in
+ // https://html.spec.whatwg.org/C/#window-open-steps
+ assert_equals(
+ e.data.location,
+ new URL('target.html', entryUrl).href,
+ 'Base URL should use the entry settings object');
+
+ // `document.referrer` should reflect the source browsing context,
+ // which is the entry in
+ // https://html.spec.whatwg.org/C/#window-open-steps
+ assert_equals(
+ e.data.referrer, entryUrl,
+ 'Referrer should use the entry settings object');
+ }));
+}, 'Fetch client and URL resolution for window.open()');
+</script>
+<iframe id="entry" src="entry/entry.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/entry/entry.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/entry/entry.html
new file mode 100644
index 0000000000..82fecfdd38
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/entry/entry.html
@@ -0,0 +1,4 @@
+<body onload="top.go()">
+<iframe id="incumbent" src="../incumbent/empty.html"></iframe>
+<iframe id="relevant" src="../relevant/empty.html"></iframe>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/entry/target.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/entry/target.html
new file mode 100644
index 0000000000..5ceaeaf07e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/entry/target.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<script src="../resources/target.js"></script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/incumbent/empty.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/incumbent/empty.html
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/incumbent/empty.html
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/incumbent/target.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/incumbent/target.html
new file mode 100644
index 0000000000..5ceaeaf07e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/incumbent/target.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<script src="../resources/target.js"></script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/relevant/empty.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/relevant/empty.html
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/relevant/empty.html
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/relevant/target.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/relevant/target.html
new file mode 100644
index 0000000000..5ceaeaf07e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/relevant/target.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<script src="../resources/target.js"></script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/resources/context-helper.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/resources/context-helper.js
new file mode 100644
index 0000000000..dda338b4cc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/resources/context-helper.js
@@ -0,0 +1,34 @@
+// Usage: in the top-level Window, include:
+//
+// <script src="resources/context-helper.js"></script>
+// <script>
+// window.scriptToRun = '...';
+// </script>
+// <iframe id="entry" src="entry/entry.html"></iframe>
+//
+// Then `scriptToRun` is evaluated, with:
+// - The entry Realm is that of entry/entry.html
+// - The incumbent Realm is that of incumbent/empty.html
+// - The relevant Realm of `relevantWindow`, `relevantWindow.location` etc. is
+// that of relevant/empty.html
+
+window.scriptToRun = '';
+
+const entryUrl = new URL('entry/entry.html', location).href;
+const incumbentUrl = new URL('incumbent/empty.html', location).href;
+const relevantUrl = new URL('relevant/empty.html', location).href;
+
+function go() {
+ const entry = document.querySelector('#entry');
+ const incumbent = entry.contentDocument.querySelector('#incumbent');
+ const incumbentScript = incumbent.contentDocument.createElement('script');
+ incumbentScript.textContent = `
+ function go() {
+ const relevantWindow =
+ parent.document.querySelector('#relevant').contentWindow;
+ ${window.scriptToRun}
+ }
+ `;
+ incumbent.contentDocument.head.appendChild(incumbentScript);
+ incumbent.contentWindow.go();
+}
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/resources/target.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/resources/target.js
new file mode 100644
index 0000000000..e3a507b0d8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/multiple-globals/resources/target.js
@@ -0,0 +1,11 @@
+window.onload = function() {
+ let testWindow;
+ if (opener) {
+ testWindow = opener.top;
+ } else {
+ testWindow = top;
+ }
+ testWindow.postMessage(
+ {location: location.href, referrer: document.referrer},
+ "*");
+}
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigate-cross-origin-iframe-to-same-url-with-fragment-fire-load-event.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigate-cross-origin-iframe-to-same-url-with-fragment-fire-load-event.html
new file mode 100644
index 0000000000..f74bbfd7d3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigate-cross-origin-iframe-to-same-url-with-fragment-fire-load-event.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body>
+<script>
+async_test(t => {
+ const crossOriginUrl = new URL(get_host_info().HTTPS_REMOTE_ORIGIN);
+ crossOriginUrl.pathname = "/common/blank.html";
+ const i = document.createElement("iframe");
+ i.src = crossOriginUrl;
+ document.body.appendChild(i);
+
+ let wasLoadEventFired = false;
+ i.onload = t.step_func(() => {
+ // Though iframe is cross-origin and changing hash leads soft reload, the
+ // load event should be fired to protect sensitive information.
+ // See: https://crbug.com/1248444
+ crossOriginUrl.hash = "#foo";
+ i.onload = () => {
+ assert_false(wasLoadEventFired)
+ wasLoadEventFired = true;
+ // Wait for a while to ensure other onload events are never fired.
+ t.step_timeout(() => t.done(), 100);
+ };
+ i.src = crossOriginUrl;
+ });
+
+}, "Changing the URL hash of a cross-origin iframe should fire a load event");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigate-cross-origin-iframe-to-same-url-with-fragment.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigate-cross-origin-iframe-to-same-url-with-fragment.html
new file mode 100644
index 0000000000..ed228ad59b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigate-cross-origin-iframe-to-same-url-with-fragment.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body>
+<script>
+async_test(t => {
+ let starting_history_length = history.length;
+ let cross_origin_url = new URL(get_host_info().HTTPS_REMOTE_ORIGIN);
+ cross_origin_url.pathname = "/common/blank.html";
+ cross_origin_url.hash = "#foo";
+ let i = document.createElement("iframe");
+ i.src = cross_origin_url;
+ document.body.appendChild(i);
+
+ window.onload = () => t.step_timeout(() => {
+ assert_equals(starting_history_length, history.length);
+ i.src = cross_origin_url;
+ // Give the navigation time to happen - no events will fire.
+ t.step_timeout(() => {
+ assert_equals(starting_history_length + 1, history.length);
+ t.done();
+ }, 100);
+ }, 0);
+}, "Navigating a cross-origin iframe to its current url should not replace");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigate-cross-origin-iframe-to-same-url.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigate-cross-origin-iframe-to-same-url.html
new file mode 100644
index 0000000000..9996d58914
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigate-cross-origin-iframe-to-same-url.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body>
+<script>
+async_test(t => {
+ let starting_history_length = history.length;
+ let cross_origin_url = new URL(get_host_info().HTTPS_REMOTE_ORIGIN);
+ cross_origin_url.pathname = "/common/blank.html";
+ let i = document.createElement("iframe");
+ i.src = cross_origin_url;
+ document.body.appendChild(i);
+
+ window.onload = () => t.step_timeout(() => {
+ assert_equals(starting_history_length, history.length);
+ i.onload = t.step_func_done(() => assert_equals(starting_history_length + 1, history.length));
+ i.src = cross_origin_url;
+ }, 0);
+}, "Navigating a cross-origin iframe to its current url should not replace");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigate-to-unparseable-url.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigate-to-unparseable-url.html
new file mode 100644
index 0000000000..d20a440d03
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigate-to-unparseable-url.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>location.href unparseable URL throws a SyntaxError DOMException</title>
+<link rel="help" href="https://html.spec.whatwg.org/#the-location-interface:dom-location-href-2">
+<link rel="help" href="https://html.spec.whatwg.org/#following-hyperlinks-2">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+const kUnparseableURL = self.origin + ":notaport/common/blank.html";
+
+promise_test(async t => {
+ const win = window.open("/common/blank.html");
+ t.add_cleanup(() => {
+ win.close();
+ });
+
+ await new Promise(resolve => {
+ win.onload = resolve;
+ });
+
+ assert_throws_dom("SyntaxError", win.DOMException, () => {
+ win.location.href = kUnparseableURL;
+ }, "location.href setter throws a SyntaxError DOMException");
+}, "location.href setter throws a SyntaxError DOMException for unparseable " +
+ "URLs");
+
+promise_test(async t => {
+ const win = window.open("/common/blank.html");
+ t.add_cleanup(() => {
+ win.close();
+ });
+
+ await new Promise(resolve => {
+ win.onload = resolve;
+ });
+
+ // If the newly-opened window tries to navigate, fail the test.
+ const failPromise = new Promise((resolve, reject) => {
+ win.onpagehide = () =>
+ reject(new Error("Navigation was attempted to unparseable URL"));
+ });
+
+ // A promise to wait on to confirm the newly-opened window did not navigate.
+ const successPromise = new Promise(resolve => {
+ t.step_timeout(resolve, 2000);
+ });
+
+ const a = win.document.createElement('a');
+ a.href = kUnparseableURL;
+ win.document.body.append(a);
+ a.click();
+
+ return Promise.race([successPromise, failPromise]);
+}, "<a> tag navigate fails for unparseable URLs");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-cross-origin.sub.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-cross-origin.sub.window.js
new file mode 100644
index 0000000000..f23e2c440b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-cross-origin.sub.window.js
@@ -0,0 +1,15 @@
+// META: title=Cross-origin navigation started from unload handler must be ignored
+// META: script=../resources/helpers.js
+
+promise_test(async () => {
+ const iframe = await addIframe();
+
+ iframe.contentWindow.addEventListener("unload", () => {
+ iframe.contentWindow.location.href = "//{{hosts[][www]}}/common/blank.html?fail";
+ });
+
+ iframe.src = "/common/blank.html?pass";
+
+ await waitForIframeLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?pass");
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-data-url.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-data-url.window.js
new file mode 100644
index 0000000000..cf39b01107
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-data-url.window.js
@@ -0,0 +1,15 @@
+// META: title=data: URL navigation started from unload handler must be ignored
+// META: script=../resources/helpers.js
+
+promise_test(async () => {
+ const iframe = await addIframe();
+
+ iframe.contentWindow.addEventListener("unload", () => {
+ iframe.contentWindow.location.href =
+ `data:text/html,unload<script>parent.postMessage('fail', '*');</script>`;
+ });
+
+ iframe.src =
+ `data:text/html,load<script>parent.postMessage('pass', '*')</script>`;
+ assert_equals(await waitForMessage(iframe.contentWindow), "pass");
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-form-submit-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-form-submit-1.html
new file mode 100644
index 0000000000..e06def9b20
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-form-submit-1.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+
+<h1>navigation-unload-form-submit-1.html</h1>
+
+<script>
+let isSubmit = false;
+
+window.unload = function () {
+ window.location = 'is-Submit' + isSubmit;
+}
+
+function setIsSubmit() {
+ isSubmit = true;
+}
+</script>
+
+<form onsubmit="setIsSubmit" action="navigation-unload-form-submit-2.html">
+ <input type="submit">
+</form>
+
+<script>
+parent.finishedLoading();
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-form-submit-2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-form-submit-2.html
new file mode 100644
index 0000000000..43cd3c1b33
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-form-submit-2.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+
+<h1>navigation-unload-form-submit-2.html</h1>
+
+<script>
+parent.finishedLoading();
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-form-submit.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-form-submit.html
new file mode 100644
index 0000000000..029170d46a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-form-submit.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/browsers.html#navigating-across-documents">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+
+<iframe id="i" src="navigation-unload-form-submit-1.html"></iframe>
+
+<!-- derived from https://bugzilla.mozilla.org/show_bug.cgi?id=247660#c0 -->
+
+<script>
+var test = async_test('Tests that navigation during an unload caused by a form submit does nothing');
+window.onload = test.step_func(function() {
+ var i = document.querySelector('#i');
+
+ window.finishedLoading = test.step_func_done(function () {
+ assert_equals(i.contentWindow.location.pathname.split('/').pop(), 'navigation-unload-form-submit-2.html');
+ assert_equals(i.contentWindow.location.hash, '');
+ });
+
+ i.contentWindow.document.querySelector('input[type="submit"]').click();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-javascript-url.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-javascript-url.window.js
new file mode 100644
index 0000000000..abbcb888a0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-javascript-url.window.js
@@ -0,0 +1,15 @@
+// META: title=javascript: URL navigation started from unload handler must be ignored
+// META: script=../resources/helpers.js
+
+promise_test(async () => {
+ const iframe = await addIframe();
+
+ iframe.contentWindow.addEventListener("unload", () => {
+ iframe.contentWindow.location.href =
+ `javascript:"unload<script>parent.postMessage('fail', '*');</script>"`;
+ });
+
+ iframe.src =
+ `javascript:"load<script>parent.postMessage('pass', '*')</script>"`;
+ assert_equals(await waitForMessage(iframe.contentWindow), "pass");
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin-fragment-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin-fragment-1.html
new file mode 100644
index 0000000000..3b5d725fee
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin-fragment-1.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+
+<h1>navigation-unload-same-origin-fragment-1.html</h1>
+
+<script>
+if (parent.finishedLoading) {
+ parent.finishedLoading();
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin-fragment-2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin-fragment-2.html
new file mode 100644
index 0000000000..2fb7f50b43
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin-fragment-2.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+
+<h1>navigation-unload-same-origin-fragment-2.html</h1>
+
+<script>
+if (parent.finishedLoading) {
+ parent.finishedLoading();
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin-fragment.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin-fragment.html
new file mode 100644
index 0000000000..c4dceb9e04
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin-fragment.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/browsers.html#navigating-across-documents">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+
+<iframe id="i" src="navigation-unload-same-origin-fragment-1.html"></iframe>
+
+<!-- a timeout indicates that setting i.contentWindow.location.hash (a second navigation) aborted the first navigation,
+ and so it stayed on a.html and finishedLoading was never called -->
+
+<script>
+var test = async_test('Tests that a fragment navigation in the unload handler will not block the initial navigation');
+window.onload = test.step_func(function() {
+ var i = document.querySelector('#i');
+
+ i.contentWindow.onunload = test.step_func(function() {
+ i.contentWindow.location.hash = '#fragment';
+ });
+
+ window.finishedLoading = test.step_func_done(function () {
+ assert_equals(i.contentWindow.location.pathname.split('/').pop(), 'navigation-unload-same-origin-fragment-2.html');
+ assert_equals(i.contentWindow.location.hash, '');
+ });
+
+ i.contentWindow.location.href = 'navigation-unload-same-origin-fragment-2.html';
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin.window.js
new file mode 100644
index 0000000000..826af4b75b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin.window.js
@@ -0,0 +1,15 @@
+// META: title=Same-origin navigation started from unload handler must be ignored
+// META: script=../resources/helpers.js
+
+promise_test(async () => {
+ const iframe = await addIframe();
+
+ iframe.contentWindow.addEventListener("unload", () => {
+ iframe.contentWindow.location.href = "/common/blank.html?fail";
+ });
+
+ iframe.src = "/common/blank.html?pass";
+
+ await waitForIframeLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?pass");
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/plugin-document.historical.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/plugin-document.historical.html
new file mode 100644
index 0000000000..547917b795
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/plugin-document.historical.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Same-origin PDFs must not create accessible Document objects</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!-- https://github.com/whatwg/html/pull/6947 -->
+
+<iframe src="resources/portable-document-format-sample-valid.pdf"></iframe>
+
+<script>
+setup({ explicit_done: true });
+
+window.onload = () => {
+ test(() => {
+ assert_throws_dom("SecurityError", () => {
+ document.querySelector("iframe").contentWindow.document;
+ });
+ });
+ done();
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/redirect-to-about.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/redirect-to-about.window.js
new file mode 100644
index 0000000000..5480911895
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/redirect-to-about.window.js
@@ -0,0 +1,34 @@
+"use strict";
+
+["about:blank", "about:srcdoc", "about:nonstandard"].forEach(aboutURL => {
+ promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ iframe.src = `resources/redirect.py?location=${aboutURL}`;
+ document.body.append(iframe);
+
+ // Unfortunately Firefox does not fire a load event for network errors yet, but there is no
+ // other way I can see to test this. (Also applicable below.)
+ await new Promise(r => iframe.onload = r);
+
+ // Must throw since error pages are opaque origin.
+ assert_throws_dom("SecurityError", () => {
+ iframe.contentWindow.document;
+ });
+ }, `An iframe with src set to a redirect to ${aboutURL}`);
+
+ promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ iframe.src = "/common/blank.html";
+ document.body.append(iframe);
+
+ await new Promise(r => iframe.onload = r);
+
+ iframe.contentWindow.location.href = `resources/redirect.py?location=${aboutURL}`;
+ await new Promise(r => iframe.onload = r);
+
+ // Must throw since error pages are opaque origin.
+ assert_throws_dom("SecurityError", () => {
+ iframe.contentWindow.document;
+ });
+ }, `An iframe that is navigated to a redirect to ${aboutURL}`);
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/redirect-to-data.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/redirect-to-data.html
new file mode 100644
index 0000000000..f9e8021ddf
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/redirect-to-data.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Redirecting to data: URLs is disallowed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+"use strict";
+
+promise_test(async (t) => {
+ window.onmessage = t.unreached_func("must not be messaged");
+ t.add_cleanup(() => { window.onmessage = null; });
+
+ const iframe = document.createElement("iframe");
+ iframe.src = `resources/redirect.py?location=data:text/html,FAIL<script>parent.postMessage('FAIL', '*')</${'script'}>`;
+ document.body.append(iframe);
+
+ await new Promise(r => iframe.onload = r);
+
+ // Must throw since error pages are opaque origin.
+ assert_throws_dom("SecurityError", () => {
+ iframe.contentWindow.document;
+ });
+
+ // Test passes if after 100 ms we haven't gotten the message.
+ await new Promise(r => t.step_timeout(r, 100));
+}, "Loading an iframe with src=redirecting URL");
+
+promise_test(async (t) => {
+ window.onmessage = t.unreached_func("must not be messaged");
+ t.add_cleanup(() => { window.onmessage = null; });
+
+ const iframe = document.createElement("iframe");
+ iframe.src = "/common/blank.html";
+ document.body.append(iframe);
+
+ await new Promise(r => iframe.onload = r);
+
+ iframe.contentWindow.location.href = `resources/redirect.py?location=data:text/html,FAIL<script>parent.postMessage('FAIL', '*')</${'script'}>`;
+ await new Promise(r => iframe.onload = r);
+
+ // Must throw since error pages are opaque origin.
+ assert_throws_dom("SecurityError", () => {
+ iframe.contentWindow.document;
+ });
+
+ // Test passes if after 100 ms we haven't gotten the message.
+ await new Promise(r => t.step_timeout(r, 100));
+}, "Navigating an iframe to a redirecting URL");
+
+promise_test(async (t) => {
+ window.onmessage = t.unreached_func("must not be messaged");
+ t.add_cleanup(() => { window.onmessage = null; });
+
+ const w = window.open(`resources/redirect.py?location=data:text/html,FAIL<script>parent.postMessage('FAIL', '*')</${'script'}>`);
+
+ // Test passes if after 100 ms we haven't gotten the message.
+ await new Promise(r => t.step_timeout(r, 100));
+}, "Loading a popup directly to the redirecting URL");
+
+promise_test(async (t) => {
+ const w = window.open(`resources/message-opener.html`);
+ await new Promise(r => window.onmessage = r);
+
+ window.onmessage = t.unreached_func("must not be messaged");
+ t.add_cleanup(() => { window.onmessage = null; });
+
+ w.location.href = `resources/redirect.py?location=data:text/html,FAIL<script>parent.postMessage('FAIL', '*')</${'script'}>`;
+
+ // Test passes if after 100 ms we haven't gotten the message.
+ await new Promise(r => t.step_timeout(r, 100));
+}, "Loading a popup that eventually goes to the redirecting URL");
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/redirect-to-unparseable-url.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/redirect-to-unparseable-url.html
new file mode 100644
index 0000000000..b025f34478
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/redirect-to-unparseable-url.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Session history interaction with redirects to unparseable URLs</title>
+<link rel="help" href="https://html.spec.whatwg.org/#create-navigation-params-by-fetching">
+<link rel="help" href="https://html.spec.whatwg.org/#read-ua-inline">
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+const kUnparseableURL = self.origin + ":notaport/common/blank.html";
+
+promise_test(async t => {
+ const iframe = document.createElement('iframe');
+ t.add_cleanup(() => {
+ iframe.remove();
+ });
+
+ function getIframeLoadPromise() {
+ return new Promise(resolve => {
+ iframe.addEventListener('load', () => {
+ // Wait for the iframe to load + one task so that its navigations are
+ // not done in "replace" mode.
+ t.step_timeout(resolve, 0);
+ }, {once: true});
+ });
+ }
+
+ document.body.append(iframe);
+
+ assert_equals(history.length, 1, "Precondition: history.length is 1");
+
+ const first_load_promise = getIframeLoadPromise();
+ iframe.src = '/common/blank.html';
+ await first_load_promise;
+
+ // This navigation will fail, because it redirects to an unparseable URL.
+ const error_load_promise = getIframeLoadPromise();
+ const error_url = new URL('resources/no-cache-single-redirect.py', location.href);
+ error_url.searchParams.append('uuid', token());
+ error_url.searchParams.append('location', kUnparseableURL);
+ iframe.src = error_url;
+ await error_load_promise;
+
+ assert_equals(history.length, 2,
+ "history.length is 2 after two iframe navigations beyond the initial " +
+ "about:blank Document, the first of which 'replaced' the initial " +
+ "about:blank Document");
+
+ // Per https://html.spec.whatwg.org/#read-ua-inline, error Documents have
+ // opaque origins, so the `contentDocument` shouldn't be accessible.
+ assert_equals(iframe.contentDocument, null,
+ "Cannot reach iframe.contentDocument for error Documents");
+
+ const back_load_promise = getIframeLoadPromise();
+ history.back();
+ await back_load_promise;
+
+ const forward_load_promise = getIframeLoadPromise();
+ history.forward();
+ await forward_load_promise;
+
+ assert_not_equals(iframe.contentDocument, null, "iframe.contentDocument is accessible");
+ assert_equals(iframe.contentDocument.body.innerText, "No redirect",
+ "Traversal to history entry whose URL was once associated with an " +
+ "error Document correctly requests the same URL again");
+}, "Navigating to a url (A) that redirects to an unparseable URL (B), saves " +
+ "the URL (A) in the history entry, for later traversal");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/README.md b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/README.md
new file mode 100644
index 0000000000..52548db656
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/README.md
@@ -0,0 +1 @@
+See `/html/semantics/document-metadata/the-meta-element/pragma-directives/attr-meta-http-equiv-refresh/parsing.html` for more detailed parsing tests (shared with `<meta http-equiv=refresh>`).
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/navigate.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/navigate.window.js
new file mode 100644
index 0000000000..7d5a0fe21d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/navigate.window.js
@@ -0,0 +1,23 @@
+async_test(t => {
+ const frame = document.createElement("iframe");
+ frame.src = "resources/refresh.py"
+ frame.onload = t.step_func(() => {
+ // Could be better by verifying that resources/refresh.py loads too
+ if(frame.contentWindow.location.href === (new URL("resources/refreshed.txt?\u0080\u00FF", self.location)).href) { // Make sure bytes got mapped to code points of the same value
+ t.done();
+ }
+ });
+ document.body.appendChild(frame)
+}, "When navigating the Refresh header needs to be followed");
+
+async_test(t => {
+ const frame = document.createElement("iframe");
+ frame.src = "resources/multiple.asis"
+ frame.onload = t.step_func(() => {
+ // Could be better by verifying that resources/refresh.py loads too
+ if(frame.contentWindow.location.href === (new URL("resources/refreshed.txt", self.location)).href) {
+ t.done();
+ }
+ });
+ document.body.appendChild(frame)
+}, "When there's both a Refresh header and <meta> the Refresh header wins")
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/multiple.asis b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/multiple.asis
new file mode 100644
index 0000000000..3026d8297b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/multiple.asis
@@ -0,0 +1,6 @@
+HTTP/1.1 200 OK
+Refresh: 0,./refreshed.txt
+Content-Type:text/html
+
+I don't understand.
+<meta http-equiv=refresh content=1;./>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/refresh.py b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/refresh.py
new file mode 100644
index 0000000000..ecdd24f268
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/refresh.py
@@ -0,0 +1,4 @@
+def main(request, response):
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.headers.set(b"Refresh", b"0;./refreshed.txt?\x80\xFF") # Test byte to Unicode conversion
+ response.content = u"Not refreshed.\n"
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/refreshed.txt b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/refreshed.txt
new file mode 100644
index 0000000000..5df065b456
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/refreshed.txt
@@ -0,0 +1 @@
+Have another.
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/subresource.any.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/subresource.any.js
new file mode 100644
index 0000000000..930dd34ad5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/refresh/subresource.any.js
@@ -0,0 +1,6 @@
+promise_test(() => {
+ return fetch("resources/refresh.py").then(response => {
+ assert_equals(response.headers.get("refresh"), "0;./refreshed.txt?\u0080\u00FF"); // Make sure bytes got mapped to code points of the same value
+ assert_equals(response.url, (new URL("resources/refresh.py", self.location)).href);
+ });
+}, "Refresh does not affect subresources.");
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click-during-load.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click-during-load.html
new file mode 100644
index 0000000000..e035b1f517
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click-during-load.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onload = () => {
+ const a = document.createElement("a");
+ a.href = "/common/blank.html?thereplacement";
+ document.body.append(a);
+ a.click();
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength + 1, "history.length must change after waiting for the load");
+}, "aElement.click() during the load event must NOT replace");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click-during-pageshow.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click-during-pageshow.html
new file mode 100644
index 0000000000..006ce531e0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click-during-pageshow.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onpageshow = () => {
+ const a = document.createElement("a");
+ a.href = "/common/blank.html?thereplacement";
+ document.body.append(a);
+ a.click();
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength + 1, "history.length must change after waiting for the load");
+}, "aElement.click() during the pageshow event must NOT replace");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click.html
new file mode 100644
index 0000000000..be7d1a9849
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ const a = document.createElement("a");
+ a.href = "/common/blank.html?thereplacement";
+ document.currentScript.before(a);
+ a.click();
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoad(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength + 1, "history.length must change after waiting for the load");
+}, "aElement.click() before the load event must NOT replace");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-user-click-during-load.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-user-click-during-load.html
new file mode 100644
index 0000000000..811c828331
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-user-click-during-load.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onload = () => {
+ const a = document.createElement("a");
+ a.href = "/common/blank.html?thereplacement";
+ a.id = "the-anchor";
+ a.textContent = "needs to have content to be clickable";
+ document.body.append(a);
+ parent.test_driver.click(a);
+ };
+ `;
+
+ const startURL = "resources/slow-code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength + 1, "history.length must change after waiting for the load");
+}, "User click on <a> during the load event must NOT replace");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-user-click-during-pageshow.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-user-click-during-pageshow.html
new file mode 100644
index 0000000000..6621b081e1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-user-click-during-pageshow.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onpageshow = () => {
+ const a = document.createElement("a");
+ a.href = "/common/blank.html?thereplacement";
+ a.id = "the-anchor";
+ a.textContent = "needs to have content to be clickable";
+ document.body.append(a);
+ parent.test_driver.click(a);
+ };
+ `;
+
+ const startURL = "resources/slow-code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength + 1, "history.length must change after waiting for the load");
+}, "User click on <a> during the pageshow event must NOT replace");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-user-click.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-user-click.html
new file mode 100644
index 0000000000..c9034f3573
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-user-click.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ const a = document.createElement("a");
+ a.href = "/common/blank.html?thereplacement";
+ a.id = "the-anchor";
+ a.textContent = "needs to have content to be clickable";
+ document.currentScript.before(a);
+ parent.test_driver.click(a);
+ `;
+
+ const startURL = "resources/slow-code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoad(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength + 1, "history.length must change after waiting for the load");
+}, "User click on <a> before the load event must NOT replace");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-requestsubmit-during-load.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-requestsubmit-during-load.html
new file mode 100644
index 0000000000..0dee49edb3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-requestsubmit-during-load.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onload = () => {
+ const form = document.createElement("form");
+ form.action = "/common/blank.html";
+
+ const input = document.createElement("input");
+ input.type = "hidden";
+ input.name = "thereplacement";
+ form.append(input);
+
+ document.body.append(form);
+ form.requestSubmit();
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement=";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "Replace during the load event, triggered by formElement.submit()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-requestsubmit-during-pageshow.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-requestsubmit-during-pageshow.html
new file mode 100644
index 0000000000..0cf0838496
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-requestsubmit-during-pageshow.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onpageshow = () => {
+ const form = document.createElement("form");
+ form.action = "/common/blank.html";
+
+ const input = document.createElement("input");
+ input.type = "hidden";
+ input.name = "thereplacement";
+ form.append(input);
+
+ document.body.append(form);
+ form.requestSubmit();
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement=";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "Replace during the pageshow event, triggered by formElement.submit()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-requestsubmit.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-requestsubmit.html
new file mode 100644
index 0000000000..80beb3718b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-requestsubmit.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ const form = document.createElement("form");
+ form.action = "/common/blank.html";
+
+ const input = document.createElement("input");
+ input.type = "hidden";
+ input.name = "thereplacement";
+ form.append(input);
+
+ document.currentScript.before(form);
+ form.requestSubmit();
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement=";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoad(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "Replace before load, triggered by formElement.requestSubmit()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-button-click-during-load.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-button-click-during-load.html
new file mode 100644
index 0000000000..1b3a163176
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-button-click-during-load.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onload = () => {
+ const form = document.createElement("form");
+ form.action = "/common/blank.html";
+
+ const input = document.createElement("input");
+ input.type = "hidden";
+ input.name = "thereplacement";
+ form.append(input);
+
+ const button = document.createElement("button");
+ button.type = "submit";
+ form.append(button);
+
+ document.body.append(form);
+ button.click();
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement=";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "Replace during load, triggered by submitButton.click()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-button-click-during-pageshow.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-button-click-during-pageshow.html
new file mode 100644
index 0000000000..c0022fd0f2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-button-click-during-pageshow.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onpageshow = () => {
+ const form = document.createElement("form");
+ form.action = "/common/blank.html";
+
+ const input = document.createElement("input");
+ input.type = "hidden";
+ input.name = "thereplacement";
+ form.append(input);
+
+ const button = document.createElement("button");
+ button.type = "submit";
+ form.append(button);
+
+ document.body.append(form);
+ button.click();
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement=";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "Replace during pageshow, triggered by submitButton.click()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-button-click.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-button-click.html
new file mode 100644
index 0000000000..873c49b5be
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-button-click.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ const form = document.createElement("form");
+ form.action = "/common/blank.html";
+
+ const input = document.createElement("input");
+ input.type = "hidden";
+ input.name = "thereplacement";
+ form.append(input);
+
+ const button = document.createElement("button");
+ button.type = "submit";
+ form.append(button);
+
+ document.currentScript.before(form);
+ button.click();
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement=";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoad(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "Replace before load, triggered by submitButton.click()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-cross-frame-crossorigin.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-cross-frame-crossorigin.sub.html
new file mode 100644
index 0000000000..a2ea20bf7f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-cross-frame-crossorigin.sub.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<form target="the-frame">
+ <input type="hidden" name="pushed">
+</form>
+
+<script>
+"use strict";
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+
+ const form = document.querySelector("form");
+ const frameEndingURL = changeURLHost(
+ absoluteURL("resources/slow-message-source-with-history-and-location.html?pushed="),
+ "{{hosts[][www]}}"
+ );
+ form.action = frameEndingURL;
+
+ const frameStartingCode = `
+ parent.postMessage({ historyLength: history.length, locationHref: location.href }, "*");
+ `;
+
+ const frameStartingURL = codeInjectorURL(frameStartingCode);
+ const frame = insertIframe(t, frameStartingURL, "the-frame");
+ t.add_cleanup(() => frame.remove()); // helps avoid waiting for the slow load to finish the tests
+ assert_equals(history.length, startingHistoryLength, "Inserting frame must not change history.length");
+
+ const frameBeforeLoadedMessage = await waitForMessage();
+ assert_equals(frameBeforeLoadedMessage.historyLength, startingHistoryLength, "frame's starting history.length");
+ assert_equals(frameBeforeLoadedMessage.locationHref, frame.src, "frame's starting location.href");
+
+ form.submit();
+
+ const frameAfterFormSubmitMessage = await waitForMessage();
+ assert_equals(frameAfterFormSubmitMessage.historyLength, startingHistoryLength + 1, "frame's after-submit history.length");
+ assert_equals(frameAfterFormSubmitMessage.locationHref, frameEndingURL, "frame's after-submit location.href");
+}, "No replace before load, triggered by cross-iframe formElement.submit() [iframe is cross-origin]");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-cross-frame.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-cross-frame.html
new file mode 100644
index 0000000000..d647e6ad06
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-cross-frame.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<form target="the-frame">
+ <input type="hidden" name="pushed">
+</form>
+
+<script>
+"use strict";
+promise_test(async t => {
+ const startingHistoryLength = history.length;
+
+ const form = document.querySelector("form");
+ const frameEndingURL = absoluteURL("resources/slow-message-source-with-history-and-location.html?pushed=");
+ form.action = frameEndingURL;
+
+ const frameStartingCode = `
+ window.onload = () => { window.onloadFired = true; };
+ parent.postMessage({ historyLength: history.length, locationHref: location.href }, "*");
+ parent.document.querySelector("form").submit();
+ `;
+
+ const frameStartingURL = codeInjectorURL(frameStartingCode);
+ const frame = insertIframe(t, frameStartingURL, "the-frame");
+ t.add_cleanup(() => frame.remove()); // helps avoid waiting for the slow load to finish the tests
+ assert_equals(history.length, startingHistoryLength, "Inserting frame must not change history.length");
+
+ const frameBeforeLoadedMessage = await waitForMessage();
+ assert_equals(frameBeforeLoadedMessage.historyLength, startingHistoryLength, "frame's starting history.length");
+ assert_equals(frameBeforeLoadedMessage.locationHref, frame.src, "frame's starting location.href");
+ assert_equals(frame.contentWindow.onloadFired, undefined, "frame's onload not fired yet");
+
+ const frameAfterFormSubmitMessage = await waitForMessage();
+ assert_equals(frameAfterFormSubmitMessage.historyLength, startingHistoryLength + 1, "frame's after-submit history.length");
+ assert_equals(frameAfterFormSubmitMessage.locationHref, frameEndingURL, "frame's after-submit location.href");
+}, "No replace before load, triggered by cross-iframe formElement.submit()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-during-load.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-during-load.html
new file mode 100644
index 0000000000..2a60f44fc6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-during-load.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onload = () => {
+ const form = document.createElement("form");
+ form.action = "/common/blank.html";
+
+ const input = document.createElement("input");
+ input.type = "hidden";
+ input.name = "thereplacement";
+ form.append(input);
+
+ document.body.append(form);
+ form.submit();
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement=";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "Replace during the load event, triggered by formElement.submit()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-during-pageshow.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-during-pageshow.html
new file mode 100644
index 0000000000..886bf469a2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-during-pageshow.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onpageshow = () => {
+ const form = document.createElement("form");
+ form.action = "/common/blank.html";
+
+ const input = document.createElement("input");
+ input.type = "hidden";
+ input.name = "thereplacement";
+ form.append(input);
+
+ document.body.append(form);
+ form.submit();
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement=";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "Replace during the pageshow event, triggered by formElement.submit()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-popup-crossorigin.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-popup-crossorigin.sub.html
new file mode 100644
index 0000000000..d54cc81477
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-popup-crossorigin.sub.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<form target="the-window">
+ <input type="hidden" name="pushed">
+</form>
+
+<script>
+"use strict";
+promise_test(async t => {
+ const form = document.querySelector("form");
+ const wEndingURL = changeURLHost(
+ absoluteURL("resources/slow-message-source-with-history-and-location.html?pushed="),
+ "{{hosts[][www]}}"
+ );
+ form.action = wEndingURL;
+
+ const wStartingCode = `
+ opener.postMessage({ historyLength: history.length, locationHref: location.href }, "*");
+ `;
+
+ const wStartingURL = codeInjectorURL(wStartingCode);
+ const w = window.open(wStartingURL, "the-window");
+ t.add_cleanup(() => w.close());
+
+ const wBeforeLoadedMessage = await waitForMessage();
+ assert_equals(wBeforeLoadedMessage.historyLength, 1, "window's starting history.length");
+ assert_equals(wBeforeLoadedMessage.locationHref, wStartingURL, "window's starting location.href");
+
+ form.submit();
+
+ const wAfterFormSubmitMessage = await waitForMessage();
+ assert_equals(wAfterFormSubmitMessage.historyLength, 2, "window's after-submit history.length");
+ assert_equals(wAfterFormSubmitMessage.locationHref, wEndingURL, "window's after-submit location.href");
+}, "No replace before load, triggered by formElement.submit() in the opener window, after the opener has loaded [window is cross-origin]");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-popup.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-popup.html
new file mode 100644
index 0000000000..b27d54675d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-popup.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<form target="the-window">
+ <input type="hidden" name="pushed">
+</form>
+
+<script>
+"use strict";
+promise_test(async t => {
+ const form = document.querySelector("form");
+ const wEndingURL = absoluteURL("resources/slow-message-source-with-history-and-location.html?pushed=");
+ form.action = wEndingURL;
+
+ const wStartingCode = `
+ window.onload = () => { window.onloadFired = true; };
+ opener.postMessage({ historyLength: history.length, locationHref: location.href }, "*");
+ opener.document.querySelector("form").submit();
+ `;
+
+ const wStartingURL = codeInjectorURL(wStartingCode);
+ const w = window.open(wStartingURL, "the-window");
+ t.add_cleanup(() => w.close());
+
+ const wBeforeLoadedMessage = await waitForMessage();
+ assert_equals(wBeforeLoadedMessage.historyLength, 1, "window's starting history.length");
+ assert_equals(wBeforeLoadedMessage.locationHref, wStartingURL, "window's starting location.href");
+ assert_equals(w.onloadFired, undefined, "window's onload not fired yet");
+
+ const wAfterFormSubmitMessage = await waitForMessage();
+ assert_equals(wAfterFormSubmitMessage.historyLength, 2, "window's after-submit history.length");
+ assert_equals(wAfterFormSubmitMessage.locationHref, wEndingURL, "window's after-submit location.href");
+}, "No replace before load, triggered by formElement.submit() in the opener window, after the opener has loaded");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit.html
new file mode 100644
index 0000000000..eace0b0a69
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ const form = document.createElement("form");
+ form.action = "/common/blank.html";
+
+ const input = document.createElement("input");
+ input.type = "hidden";
+ input.name = "thereplacement";
+ form.append(input);
+
+ document.currentScript.before(form);
+ form.submit();
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement=";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoad(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "Replace before load, triggered by same-document formElement.submit()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/history-pushstate-during-load.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/history-pushstate-during-load.html
new file mode 100644
index 0000000000..233d80d8d7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/history-pushstate-during-load.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onload = () => {
+ history.pushState(null, null, "/common/blank.html?thereplacement");
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength + 1, "history.length must change after waiting for the replacement");
+}, "history.pushState() during the load event must NOT replace");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/history-pushstate-during-pageshow.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/history-pushstate-during-pageshow.html
new file mode 100644
index 0000000000..a4e83baf1d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/history-pushstate-during-pageshow.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onpageshow = () => {
+ history.pushState(null, null, "/common/blank.html?thereplacement");
+ parent.postMessage("done", "*");
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ assert_equals(await waitForMessage(), "done");
+ assert_equals(history.length, startingHistoryLength + 1, "history.length must change after waiting for the replacement");
+}, "history.pushState() during the pageshow event must NOT replace");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/history-pushstate.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/history-pushstate.html
new file mode 100644
index 0000000000..2faef7c319
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/history-pushstate.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent("history.pushState(null, null, `/common/blank.html?thereplacement`);");
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoad(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength + 1, "history.length must change after waiting for the replacement");
+}, "history.pushState() before the load event must NOT replace");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/iframe-src-during-load.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/iframe-src-during-load.html
new file mode 100644
index 0000000000..83f26b2aa9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/iframe-src-during-load.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onload = () => {
+ parent.document.querySelectorAll("iframe")[1].src = "/common/blank.html?thereplacement";
+ };
+ `;
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "Replace during the load event, triggered by setting iframeElement.src");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/iframe-src-during-pageshow.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/iframe-src-during-pageshow.html
new file mode 100644
index 0000000000..61345cf7f9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/iframe-src-during-pageshow.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onpageshow = () => {
+ parent.document.querySelectorAll("iframe")[1].src = "/common/blank.html?thereplacement";
+ };
+ `;
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "Replace during the pageshow event, triggered by setting iframeElement.src");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/iframe-src.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/iframe-src.html
new file mode 100644
index 0000000000..47407c106e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/iframe-src.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onload = () => { window.onloadFired = true; };
+ `;
+ const startURL = "resources/slow-code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ const absoluteStartURL = (new URL(startURL, location.href)).href;
+ while (true) {
+ if (iframe.contentWindow.location.href === absoluteStartURL) {
+ break;
+ }
+ await new Promise(r => setTimeout(r, 0));
+ }
+
+ assert_equals(iframe.contentWindow.location.href, (new URL(startURL, location.href)).href, "Iframe must be navigated away from the initial about:blank document");
+ assert_equals(iframe.contentWindow.onloadFired, undefined, "onload must not yet have fired");
+
+ iframe.src = afterReplacementURL;
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "Replace before load, triggered by setting iframeElement.src");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-assign-during-load.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-assign-during-load.html
new file mode 100644
index 0000000000..2b4ac49533
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-assign-during-load.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onload = () => {
+ location.assign("/common/blank.html?thereplacement");
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "Replace during the load event, triggered by location.assign()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-assign-during-pageshow.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-assign-during-pageshow.html
new file mode 100644
index 0000000000..0f84cbe246
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-assign-during-pageshow.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onpageshow = () => {
+ location.assign("/common/blank.html?thereplacement");
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "Replace during the pageshow event, triggered by location.assign()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-assign-user-click.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-assign-user-click.html
new file mode 100644
index 0000000000..785ec024e0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-assign-user-click.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ const button = document.createElement("button");
+ button.id = "the-button";
+ button.textContent = "needs to have content to be clickable";
+ button.onclick = () => { location.assign("/common/blank.html?thereplacement"); };
+ document.currentScript.before(button);
+ parent.test_driver.click(button);
+ `;
+
+ const startURL = "resources/slow-code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoad(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength + 1, "history.length must change after waiting for the load");
+}, "NO replace before load, triggered by location.assign()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-assign.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-assign.html
new file mode 100644
index 0000000000..53ea96ffea
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-assign.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent("location.assign(`/common/blank.html?thereplacement`);");
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoad(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "Replace before load, triggered by location.assign()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter-during-load.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter-during-load.html
new file mode 100644
index 0000000000..e4c8ca80a1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter-during-load.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Replace during the load event, triggered by location setters</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onload = () => {
+ location.href = "/common/blank.html?thereplacement";
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "href");
+
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onload = () => {
+ location.search = "thereplacement";
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "resources/code-injector.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+}, "search");
+
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onload = () => {
+ location.hash = "thereplacement";
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = startURL + "#thereplacement";
+ const iframe = insertIframe(t, startURL);
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "hash");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter-during-pageshow.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter-during-pageshow.html
new file mode 100644
index 0000000000..9997783ff5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter-during-pageshow.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Replace during the pageshow event, triggered by location setters</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onpageshow = () => {
+ location.href = "/common/blank.html?thereplacement";
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "href");
+
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onpageshow = () => {
+ location.search = "thereplacement";
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "resources/code-injector.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+}, "search");
+
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onpageshow = () => {
+ location.hash = "thereplacement";
+ parent.postMessage("done", "*");
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = startURL + "#thereplacement";
+ const iframe = insertIframe(t, startURL);
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ assert_equals(await waitForMessage(), "done");
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "hash");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter-user-click.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter-user-click.html
new file mode 100644
index 0000000000..9276327f1a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter-user-click.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>No replace before load, triggered by location setters called as part of user-initiated clicks</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<body>
+<script>
+"use strict";
+
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ const button = document.createElement("button");
+ button.id = "the-button";
+ button.textContent = "needs to have content to be clickable";
+ button.onclick = () => { location.href = "/common/blank.html?thereplacement"; };
+ document.currentScript.before(button);
+ parent.test_driver.click(button);
+ `;
+
+ const startURL = "resources/slow-code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoad(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength + 1, "history.length must change after waiting for the load");
+}, "href");
+
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ const button = document.createElement("button");
+ button.id = "the-button";
+ button.textContent = "needs to have content to be clickable";
+ button.onclick = () => { location.search = "thereplacement"; };
+ document.currentScript.before(button);
+ parent.test_driver.click(button);
+ `;
+
+ const startURL = "resources/slow-code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "resources/slow-code-injector.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoad(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength + 1, "history.length must change after waiting for the load");
+}, "search");
+
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ const button = document.createElement("button");
+ button.id = "the-button";
+ button.textContent = "needs to have content to be clickable";
+ button.onclick = () => { location.hash = "thereplacement"; };
+ document.currentScript.before(button);
+ parent.test_driver.click(button);
+ `;
+
+ const startURL = "resources/slow-code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = startURL + "#thereplacement";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoad(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength + 1, "history.length must change after waiting for the load");
+}, "hash");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter-user-mouseup.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter-user-mouseup.html
new file mode 100644
index 0000000000..9068e5a116
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter-user-mouseup.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>No replace before load, triggered by location setters called as part of user-initiated mouseups</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<!--
+ We test this separate from click because the spec as of
+ https://html.spec.whatwg.org/commit-snapshots/4ba46b025ec806ded7b4911bf8f9dd7bf9ff365e/#location-object-setter-navigate
+ referenced click handlers specifically, instead of using the general user activation concept which
+ includes other events like mouseup.
+-->
+
+<body>
+<script>
+"use strict";
+
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ const button = document.createElement("button");
+ button.id = "the-button";
+ button.textContent = "needs to have content to be clickable";
+ button.onmouseup = () => { location.href = "/common/blank.html?thereplacement"; };
+ document.currentScript.before(button);
+ parent.test_driver.click(button);
+ `;
+
+ const startURL = "resources/slow-code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoad(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength + 1, "history.length must change after waiting for the load");
+}, "href");
+
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ const button = document.createElement("button");
+ button.id = "the-button";
+ button.textContent = "needs to have content to be clickable";
+ button.onmouseup = () => { location.search = "thereplacement"; };
+ document.currentScript.before(button);
+ parent.test_driver.click(button);
+ `;
+
+ const startURL = "resources/slow-code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "resources/slow-code-injector.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoad(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength + 1, "history.length must change after waiting for the load");
+}, "search");
+
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ const button = document.createElement("button");
+ button.id = "the-button";
+ button.textContent = "needs to have content to be clickable";
+ button.onmouseup = () => { location.hash = "thereplacement"; };
+ document.currentScript.before(button);
+ parent.test_driver.click(button);
+ `;
+
+ const startURL = "resources/slow-code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = startURL + "#thereplacement";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoad(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength + 1, "history.length must change after waiting for the load");
+}, "hash");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter.html
new file mode 100644
index 0000000000..b8049f084b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Replace before load, triggered by location setters</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent("location.href = `/common/blank.html?thereplacement`;");
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoad(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "href");
+
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent("location.search = `thereplacement`;");
+ const afterReplacementURL = "resources/code-injector.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoad(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+}, "search");
+
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent("location.hash = `thereplacement`;");
+ const afterReplacementURL = startURL + "#thereplacement";
+ const iframe = insertIframe(t, startURL);
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoad(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after waiting for the replacement");
+
+ await checkSentinelIframe(t, sentinelIframe);
+ assert_equals(history.length, startingHistoryLength, "history.length must not change after checking the sentinel iframe");
+}, "hash");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/code-injector.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/code-injector.html
new file mode 100644
index 0000000000..73dacf0d76
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/code-injector.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Subframe</title>
+
+<body>
+<script>
+"use strict";
+{{GET[code]}}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/helpers.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/helpers.js
new file mode 100644
index 0000000000..58102aa925
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/helpers.js
@@ -0,0 +1,89 @@
+window.waitForLoad = (t, iframe, urlRelativeToThisDocument) => {
+ return new Promise(resolve => {
+ iframe.addEventListener("load", t.step_func(() => {
+ assert_equals(iframe.contentWindow.location.href, (new URL(urlRelativeToThisDocument, location.href)).href);
+
+ // Wait a bit longer to ensure all history stuff has settled, e.g. the document is "completely loaded"
+ // (which happens from a queued task).
+ setTimeout(resolve, 0);
+ }), { once: true });
+ });
+};
+
+window.waitForLoadAllowingIntermediateLoads = (t, iframe, urlRelativeToThisDocument) => {
+ return new Promise(resolve => {
+ const handler = t.step_func(() => {
+ if (iframe.contentWindow.location.href === (new URL(urlRelativeToThisDocument, location.href)).href) {
+ // Wait a bit longer to ensure all history stuff has settled, e.g. the document is "completely loaded"
+ // (which happens from a queued task).
+ setTimeout(resolve, 0);
+ iframe.removeEventListener("load", handler);
+ }
+ });
+
+ iframe.addEventListener("load", handler);
+ });
+};
+
+window.waitForMessage = () => {
+ return new Promise(resolve => {
+ window.addEventListener("message", e => {
+ resolve(e.data);
+ }, { once: true });
+ });
+};
+
+window.setupSentinelIframe = async (t) => {
+ // If this iframe gets navigated by history.back(), then the iframe under test did not, so we did a replace.
+ const sentinelIframe = document.createElement("iframe");
+ sentinelIframe.src = "/common/blank.html?sentinelstart";
+ document.body.append(sentinelIframe);
+ t.add_cleanup(() => sentinelIframe.remove());
+
+ await waitForLoad(t, sentinelIframe, "/common/blank.html?sentinelstart");
+
+ sentinelIframe.src = "/common/blank.html?sentinelend";
+ await waitForLoad(t, sentinelIframe, "/common/blank.html?sentinelend");
+
+ return sentinelIframe;
+};
+
+window.checkSentinelIframe = async (t, sentinelIframe) => {
+ // Go back. Since iframe should have done a replace, this should move sentinelIframe back, not iframe.
+ history.back();
+ await waitForLoad(t, sentinelIframe, "/common/blank.html?sentinelstart");
+};
+
+window.insertIframe = (t, url, name) => {
+ const iframe = document.createElement("iframe");
+ iframe.src = url;
+
+ // In at least Chromium, window name targeting for form submission doesn't work if the name is set
+ // after the iframe is inserted into the DOM. So we can't just have callers do this themselves.
+ if (name) {
+ iframe.name = name;
+ }
+
+ document.body.append(iframe);
+
+ // Intentionally not including the following:
+ // t.add_cleanup(() => iframe.remove());
+ // Doing so breaks some of the testdriver.js tests with "cannot find window" errors.
+ return iframe;
+};
+
+// TODO(domenic): clean up other tests in the parent directory to use this.
+window.absoluteURL = relativeURL => {
+ return (new URL(relativeURL, location.href)).href;
+};
+
+// TODO(domenic): clean up other tests in the parent directory to use this.
+window.codeInjectorURL = code => {
+ return absoluteURL("resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code));
+};
+
+window.changeURLHost = (url, newHost) => {
+ const urlObj = new URL(url);
+ urlObj.host = newHost;
+ return urlObj.href;
+};
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/message-opener.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/message-opener.html
new file mode 100644
index 0000000000..b6f9d31358
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/message-opener.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Popup</title>
+
+<script>
+"use strict";
+opener.postMessage("ready", "*");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/slow-code-injector.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/slow-code-injector.html
new file mode 100644
index 0000000000..b7e99dcbfa
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/slow-code-injector.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Subframe</title>
+
+<body>
+<script>
+"use strict";
+{{GET[code]}}
+</script>
+
+<!--
+ This is necessary in cases involving user interaction because those happen async through the
+ webdriver infrastructure. Without this, the load event might happen before the click ever goes
+ through.
+-->
+<img src="/common/slow.py">
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/slow-message-source-with-history-and-location.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/slow-message-source-with-history-and-location.html
new file mode 100644
index 0000000000..da279ebf82
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/slow-message-source-with-history-and-location.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Subframe or popup</title>
+
+<script>
+"use strict";
+(window.opener || window.parent).postMessage(
+ { historyLength: history.length, locationHref: location.href },
+ "*"
+);
+</script>
+
+<!--
+ This delays the load event, hopefully long enough that we can do whatever before-load action we're aiming for.
+-->
+<img src="/common/slow.py">
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-popup-during-load.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-popup-during-load.html
new file mode 100644
index 0000000000..5308c01c39
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-popup-during-load.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ // Generating a new one instead of hard-coding makes running tests manually a bit easier.
+ const windowName = token();
+
+ const code = `
+ window.onload = () => opener.navigateMe();
+ opener.postMessage("arrived at start URL", "*");
+ `;
+
+ const startURL = "resources/slow-code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const absoluteStartURL = (new URL(startURL, location.href)).href;
+
+ const afterReplacementURL = "resources/message-opener.html";
+ const absoluteAfterReplacementURL = (new URL(afterReplacementURL, location.href)).href;
+
+ window.navigateMe = () => {
+ window.open(absoluteAfterReplacementURL, windowName);
+ };
+
+ // First message sent is ignored; we only check it after navigating back.
+ const w = window.open(startURL, windowName);
+ t.add_cleanup(() => w.close());
+
+ // Wait to get past any initial about:blank
+ while (true) {
+ if (w.location.href === absoluteStartURL) {
+ break;
+ }
+ await new Promise(r => t.step_timeout(r, 0));
+ }
+
+ assert_equals(w.onloadFired, undefined, "onload must not yet have fired");
+ assert_equals(w.history.length, 1, "history.length for the opened window must start at 1");
+
+ await new Promise(r => {
+ window.addEventListener("message", t.step_func(e => {
+ if (e.data === "ready") {
+ resolve();
+ }
+ }));
+ });
+
+ assert_equals(w.history.length, 2, "history.length must increase");
+ assert_equals(w.location.href, absoluteAfterReplacementURL);
+
+ const promise = new Promise(resolve => {
+ window.addEventListener("message", t.step_func(e => {
+ assert_equals(e.data, "arrived at start URL");
+ resolve();
+ }));
+ });
+
+ w.history.back();
+
+ await promise;
+ assert_equals(w.location.href, absoluteStartURL, "1 second after attempting to go back, it indeed went back");
+}, "No replace before load, triggered by window.open() on a non-_self window");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-popup-during-pageshow.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-popup-during-pageshow.html
new file mode 100644
index 0000000000..065d933225
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-popup-during-pageshow.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ // Generating a new one instead of hard-coding makes running tests manually a bit easier.
+ const windowName = token();
+
+ const code = `
+ window.onpageshow = () => opener.navigateMe();
+ opener.postMessage("arrived at start URL", "*");
+ `;
+
+ const startURL = "resources/slow-code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const absoluteStartURL = (new URL(startURL, location.href)).href;
+
+ const afterReplacementURL = "resources/message-opener.html";
+ const absoluteAfterReplacementURL = (new URL(afterReplacementURL, location.href)).href;
+
+ window.navigateMe = () => {
+ window.open(absoluteAfterReplacementURL, windowName);
+ };
+
+ // First message sent is ignored; we only check it after navigating back.
+ const w = window.open(startURL, windowName);
+ t.add_cleanup(() => w.close());
+
+ // Wait to get past any initial about:blank
+ while (true) {
+ if (w.location.href === absoluteStartURL) {
+ break;
+ }
+ await new Promise(r => t.step_timeout(r, 0));
+ }
+
+ assert_equals(w.onloadFired, undefined, "onload must not yet have fired");
+ assert_equals(w.history.length, 1, "history.length for the opened window must start at 1");
+
+ await new Promise(r => {
+ window.addEventListener("message", t.step_func(e => {
+ if (e.data === "ready") {
+ resolve();
+ }
+ }));
+ });
+
+ assert_equals(w.history.length, 2, "history.length must increase");
+ assert_equals(w.location.href, absoluteAfterReplacementURL);
+
+ const promise = new Promise(resolve => {
+ window.addEventListener("message", t.step_func(e => {
+ assert_equals(e.data, "arrived at start URL");
+ resolve();
+ }));
+ });
+
+ w.history.back();
+
+ await promise;
+ assert_equals(w.location.href, absoluteStartURL, "1 second after attempting to go back, it indeed went back");
+}, "No replace before load, triggered by window.open() on a non-_self window");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-popup.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-popup.html
new file mode 100644
index 0000000000..7b3e05f8f6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-popup.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ // Generating a new one instead of hard-coding makes running manual tests a bit easier.
+ const windowName = token();
+
+ const code = `
+ window.onload = () => { window.onloadFired = true; };
+ `;
+
+ const startURL = "resources/slow-code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const absoluteStartURL = (new URL(startURL, location.href)).href;
+
+ const afterReplacementURL = "resources/message-opener.html";
+ const absoluteAfterReplacementURL = (new URL(afterReplacementURL, location.href)).href;
+
+ const w = window.open(startURL, windowName);
+ t.add_cleanup(() => w.close());
+
+ // Wait to get past any initial about:blank
+ while (true) {
+ if (w.location.href === absoluteStartURL) {
+ break;
+ }
+ await new Promise(r => setTimeout(r, 0));
+ }
+
+ assert_equals(w.onloadFired, undefined, "onload must not yet have fired");
+ assert_equals(w.history.length, 1, "history.length for the opened window must start at 1");
+
+ window.open(afterReplacementURL, windowName);
+ await new Promise(r => { window.onmessage = r; });
+
+ assert_equals(w.history.length, 2, "history.length must increase");
+ assert_equals(w.location.href, absoluteAfterReplacementURL);
+
+ w.history.back();
+
+ await new Promise(r => t.step_timeout(r, 1000));
+ assert_equals(w.location.href, absoluteStartURL, "1 second after attempting to go back, it indeed went back");
+}, "No replace before load, triggered by window.open() on a non-_self window");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-self-during-load.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-self-during-load.html
new file mode 100644
index 0000000000..255601ba8d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-self-during-load.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onload = () => {
+ window.open("/common/blank.html?thereplacement", "_self");
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength + 1, "history.length must increase");
+}, "No replace during load, triggered by window.open(_self) on an iframe");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-self-during-pageshow.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-self-during-pageshow.html
new file mode 100644
index 0000000000..a8327cfac3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-self-during-pageshow.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const code = `
+ window.onpageshow = () => {
+ window.open("/common/blank.html?thereplacement", "_self");
+ };
+ `;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent(code);
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoadAllowingIntermediateLoads(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength + 1, "history.length must increase");
+}, "No replace during pageshow, triggered by window.open(_self) on an iframe");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-self.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-self.html
new file mode 100644
index 0000000000..10e8f38001
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/window-open-self.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+"use strict";
+promise_test(async t => {
+ const sentinelIframe = await setupSentinelIframe(t);
+ const startingHistoryLength = history.length;
+
+ const startURL = "resources/code-injector.html?pipe=sub(none)&code=" + encodeURIComponent("window.open(`/common/blank.html?thereplacement`, `_self`);");
+ const afterReplacementURL = "/common/blank.html?thereplacement";
+ const iframe = insertIframe(t, startURL);
+
+ assert_equals(history.length, startingHistoryLength, "Inserting the under-test iframe must not change history.length");
+
+ await waitForLoad(t, iframe, afterReplacementURL);
+ assert_equals(history.length, startingHistoryLength + 1, "history.length must increase");
+}, "No replace before load, triggered by window.open(_self) on an iframe");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/blank.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/blank.html
new file mode 100644
index 0000000000..c50eddd41f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/blank.html
@@ -0,0 +1 @@
+<!doctype html>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-cross-origin-inner.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-cross-origin-inner.html
new file mode 100644
index 0000000000..72b92c8061
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-cross-origin-inner.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<script>
+const params = new URL(window.location).searchParams;
+const property = params.get("property");
+
+try {
+ if (property === null) {
+ parent.location = "foo";
+ } else if (property === "reload") {
+ parent.location.reload();
+ } else if (property === "replace") {
+ parent.location.replace("foo");
+ } else {
+ parent.location[property] = "foo";
+ }
+ parent.parent.postMessage("success", "*");
+} catch (e) {
+ parent.parent.postMessage(`error: ${e.name}`, "*");
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-destination.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-destination.html
new file mode 100644
index 0000000000..bb8ba4e698
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-destination.html
@@ -0,0 +1,4 @@
+<!doctype html>
+<script>
+parent.postMessage("destination", "*");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-location-initial.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-location-initial.html
new file mode 100644
index 0000000000..a4a1713a27
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-location-initial.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<script>parent.postMessage("initial", "*")</script>
+<iframe src="child-navigates-parent-location-inner.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-location-inner.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-location-inner.html
new file mode 100644
index 0000000000..4d7d24730f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-location-inner.html
@@ -0,0 +1,5 @@
+<!doctype html>
+<script>
+parent.parent.postMessage("inner", "*");
+parent.location = "child-navigates-parent-destination.html"
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-submit-initial.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-submit-initial.html
new file mode 100644
index 0000000000..943708e0cd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-submit-initial.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<script>parent.postMessage("initial", "*")</script>
+<iframe src="child-navigates-parent-submit-inner.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-submit-inner.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-submit-inner.html
new file mode 100644
index 0000000000..0b49e13d65
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/child-navigates-parent-submit-inner.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<form action="child-navigates-parent-destination.html" target="_parent"></form>
+<script>
+parent.parent.postMessage("inner", "*");
+document.forms[0].submit();
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/click.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/click.html
new file mode 100644
index 0000000000..8cb03b74d5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/click.html
@@ -0,0 +1,4 @@
+<!doctype html>
+<script>
+parent.postMessage("click", "*");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/document-domain-set-to-site.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/document-domain-set-to-site.sub.html
new file mode 100644
index 0000000000..3c4355c452
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/document-domain-set-to-site.sub.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+
+<script>
+"use strict";
+document.domain = "{{host}}";
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/form.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/form.html
new file mode 100644
index 0000000000..6523a82b39
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/form.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<script>
+parent.postMessage("form navigation", "*");
+</script>
+form navigation
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/href.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/href.html
new file mode 100644
index 0000000000..eccadadf41
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/href.html
@@ -0,0 +1,5 @@
+<!doctype html>
+<script>
+parent.postMessage("href", "*");
+</script>
+href
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/message-opener.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/message-opener.html
new file mode 100644
index 0000000000..b6f9d31358
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/message-opener.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Popup</title>
+
+<script>
+"use strict";
+opener.postMessage("ready", "*");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/message-parent.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/message-parent.html
new file mode 100644
index 0000000000..3656358f2d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/message-parent.html
@@ -0,0 +1,3 @@
+<script>
+ window.parent.postMessage("ready", "*");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/multi-globals-subframe-1.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/multi-globals-subframe-1.sub.html
new file mode 100644
index 0000000000..a87b2fd2be
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/multi-globals-subframe-1.sub.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Multi-globals test outer subframe</title>
+
+<script>
+"use strict";
+document.domain = "{{hosts[][]}}";
+</script>
+
+<iframe src="http://{{hosts[][]}}:{{ports[http][0]}}/html/browsers/browsing-the-web/navigating-across-documents/resources/multi-globals-subframe-2.sub.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/multi-globals-subframe-2.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/multi-globals-subframe-2.sub.html
new file mode 100644
index 0000000000..593c428a67
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/multi-globals-subframe-2.sub.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Multi-globals test inner subframe</title>
+
+<script>
+"use strict";
+document.domain = "{{hosts[][]}}";
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/no-cache-single-redirect.py b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/no-cache-single-redirect.py
new file mode 100644
index 0000000000..9d3aff817d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/no-cache-single-redirect.py
@@ -0,0 +1,21 @@
+# This handler receives requests identified by UUIDs that have mandatory
+# `location` query parameters. Every other request for the same URL will result
+# in a redirect to the URL described by `location`. When we don't redirect, we
+# simply return the HTML document "No redirect".
+def main(request, response):
+ response.headers.set(b"Cache-Control", b"no-store")
+
+ uuid = request.GET.first(b"uuid")
+ value = request.server.stash.take(uuid)
+
+ if value is None:
+ response.status = 302
+ location = request.GET.first(b"location")
+ response.headers.set(b"Location", location)
+ # Ensure that the next time this uuid is request, we don't redirect.
+ request.server.stash.put(uuid, "sentinel value")
+ else:
+ # If we're in this branch, then `value` is not none, but the stash now
+ # has `None` associated with `uuid`, which means on the next request for
+ # this `uuid` we'll end up in the above branch instead.
+ return ([(b"Content-Type", b"text/html")], b"No redirect")
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/page-that-post-message-to-opener.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/page-that-post-message-to-opener.html
new file mode 100644
index 0000000000..65a0c82149
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/page-that-post-message-to-opener.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Page that postMessage to its opener</title>
+<script>
+ opener.postMessage('Allowed', '*');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/page-with-top-navigating-iframe.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/page-with-top-navigating-iframe.html
new file mode 100644
index 0000000000..568e44296f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/page-with-top-navigating-iframe.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src=/common/get-host-info.sub.js></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<title>Page that embeds an iframe that navigates its top</title>
+<script>
+function addIframe() {
+ const iframe = document.createElement('iframe');
+ const path = new URL("top-navigating-page.html", window.location).pathname;
+ iframe.src = get_host_info().HTTP_NOTSAMESITE_ORIGIN + path;
+ document.body.appendChild(iframe);
+}
+
+addEventListener('load', () => {
+ const urlParams = new URLSearchParams(location.search);
+ const parentUserGesture = urlParams.get('parent_user_gesture') === 'true';
+ if (parentUserGesture)
+ test_driver.bless("Giving parent frame user activation").then(addIframe);
+ else
+ addIframe();
+});
+</script>
+
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/portable-document-format-sample-valid.pdf b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/portable-document-format-sample-valid.pdf
new file mode 100644
index 0000000000..d008b1f23a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/portable-document-format-sample-valid.pdf
Binary files differ
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/portable-document-format-sample-valid.pdf.headers b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/portable-document-format-sample-valid.pdf.headers
new file mode 100644
index 0000000000..5a8e57e482
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/portable-document-format-sample-valid.pdf.headers
@@ -0,0 +1 @@
+Content-Type: application/pdf
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/redirect.py b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/redirect.py
new file mode 100644
index 0000000000..3c78c256b0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/redirect.py
@@ -0,0 +1,4 @@
+def main(request, response):
+ location = request.GET.first(b"location")
+ response.status = 302
+ response.headers.set(b"Location", location)
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/top-navigating-page.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/top-navigating-page.html
new file mode 100644
index 0000000000..557f408fba
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/top-navigating-page.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Page that navigates its top</title>
+<script src=/common/get-host-info.sub.js></script>
+<script>
+
+let path = new URL("page-that-post-message-to-opener.html", window.location).pathname;
+let fullUrl = get_host_info().HTTP_NOTSAMESITE_ORIGIN + path;
+try {
+ top.location = fullUrl;
+} catch {
+ top.opener.postMessage('Denied', '*');
+}
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/wait-for-messages.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/wait-for-messages.js
new file mode 100644
index 0000000000..62ddec49f1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/wait-for-messages.js
@@ -0,0 +1,15 @@
+// Asynchronous function that waits for the given number of messages to be
+// received by `window`, then returns those messages.
+function waitForMessages(numMessages) {
+ return new Promise((resolve) => {
+ const messages = [];
+
+ window.addEventListener("message", function handler(evt) {
+ messages.push(evt.data);
+ if (messages.length == numMessages) {
+ window.removeEventListener("message", handler);
+ resolve(messages);
+ }
+ });
+ });
+}
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/xhtml-and-non-utf-8.xhtml b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/xhtml-and-non-utf-8.xhtml
new file mode 100644
index 0000000000..3aacf33f1c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/resources/xhtml-and-non-utf-8.xhtml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="windows-1250"?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <meta charset="windows-1250"/>
+ <title>A test document used when you need something very non-default</title>
+</head>
+<body>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-function-parent-then-fragment.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-function-parent-then-fragment.html
new file mode 100644
index 0000000000..d01ef4c77a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-function-parent-then-fragment.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>
+ Set location from a parent, then do a fragment navigation from within the
+ frame.
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe></iframe>
+<script>
+ promise_test(async test => {
+ // Wait for the DOM to be ready before inserting an <iframe> into it.
+ await new Promise(resolve => { onload = resolve });
+ // Insert an <iframe> and wait for a dummy document to be loaded into it.
+ let iframe = document.createElement("iframe");
+ iframe.src = "support/dummy.html";
+ let iframe_loaded = new Promise(resolve => { iframe.onload = resolve });
+ document.body.appendChild(iframe);
+ await iframe_loaded;
+ // The referrer is the main frame's URL since it initiated the iframe
+ // creation.
+ assert_equals(iframe.contentDocument.referrer, document.URL);
+ // Do a fragment navigation from the frame, which will fire the
+ // 'hashchange' function.
+ let hash_changed = new Promise(resolve => {
+ iframe.contentWindow.onhashchange = resolve
+ });
+ let navigateScript = iframe.contentDocument.createElement("script");
+ navigateScript.innerHTML = "location.href = '#foo'";
+ iframe.contentDocument.body.appendChild(navigateScript);
+ await hash_changed;
+ // The referrer stays the same, even when the last navigation was
+ // initiated by the iframe (instead of the main frame document).
+ assert_equals(iframe.contentDocument.referrer, document.URL);
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-function-parent.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-function-parent.html
new file mode 100644
index 0000000000..2efc3a6b4a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-function-parent.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Set location from a parent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<iframe></iframe>
+<script>
+ setup({ single_test: true });
+ onload = function() {
+ var fr = document.querySelector("iframe")
+ fr.contentWindow.location = "support/dummy.html"
+ fr.onload = function() {
+ assert_equals(fr.contentDocument.referrer, document.URL)
+ done()
+ }
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-function-src.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-function-src.html
new file mode 100644
index 0000000000..e21260cd35
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-function-src.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Set src from a function called from a parent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<iframe src="support/set-parent-src.html"></iframe>
+<script>
+async_test(function() {
+ onload = this.step_func(function() {
+ var fr = document.querySelector("iframe")
+ fr.contentWindow.go()
+ fr.onload = this.step_func_done(function() {
+ assert_equals(fr.contentDocument.referrer, document.URL)
+ })
+ })
+})
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-function.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-function.html
new file mode 100644
index 0000000000..c6fa765b89
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-function.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Set location from a function called from a parent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<iframe src="support/location-set.html"></iframe>
+<script>
+async_test(function() {
+ onload = this.step_func(function() {
+ var fr = document.querySelector("iframe")
+ var url = fr.contentDocument.URL
+ fr.contentWindow.go()
+ fr.onload = this.step_func_done(function() {
+ assert_equals(fr.contentDocument.referrer, url)
+ })
+ })
+})
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-src-about-blank.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-src-about-blank.html
new file mode 100644
index 0000000000..479019d847
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-src-about-blank.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Set the src attribute to about:blank and check referrer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<iframe></iframe>
+<script>
+ setup({
+ single_test: true
+ });
+ onload = function() {
+ var fr = document.querySelector("iframe")
+ fr.src = "about:blank"
+ fr.onload = function() {
+ assert_equals(fr.contentDocument.referrer, document.location.origin + '/')
+ done()
+ }
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/support/dummy.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/support/dummy.html
new file mode 100644
index 0000000000..0638657093
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/support/dummy.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<meta charset=utf-8>
+<p>Hello. \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/support/location-set.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/support/location-set.html
new file mode 100644
index 0000000000..ad733afac3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/support/location-set.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<meta charset=utf-8>
+<script>
+ function go() {
+ location.href = "support/dummy.html"
+ }
+</script>
+<p>Hello. Go. \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/support/set-parent-src.html b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/support/set-parent-src.html
new file mode 100644
index 0000000000..9d45be8c8d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/source/support/set-parent-src.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<meta charset=utf-8>
+<script>
+ function go() {
+ frameElement.src = "support/dummy.html"
+ }
+</script>
+<p>Hello. Go. \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/top-level-data-url.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/top-level-data-url.window.js
new file mode 100644
index 0000000000..ca321f106a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/navigating-across-documents/top-level-data-url.window.js
@@ -0,0 +1,20 @@
+// META: timeout=long
+
+const dataURL = `data:text/html,...`;
+const encodedDataURL = encodeURIComponent(dataURL);
+
+[dataURL, `resources/redirect.py?location=${encodedDataURL}`].forEach(url => {
+ [undefined, "opener", "noopener", "noreferrer"].forEach(opener => {
+ async_test(t => {
+ const popup = window.open(url, "", opener);
+ t.step_timeout(() => {
+ if (opener === "noopener" || opener == "noreferrer") {
+ assert_equals(popup, null);
+ } else {
+ assert_true(popup.closed);
+ }
+ t.done();
+ }, 1500);
+ }, `Navigating a popup using window.open("${url}", "", "${opener}")`);
+ });
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/README.md b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/README.md
new file mode 100644
index 0000000000..cc313a155a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/README.md
@@ -0,0 +1,11 @@
+# Overlapping navigation and traversal tests
+
+These tests follow the behavior outlined in the
+[session history rewrite](https://github.com/whatwg/html/pull/6315).
+
+<https://github.com/whatwg/html/issues/6927> discusses these results.
+
+We are not yet 100% sure on this behavior, especially for overlapping
+traversal cases where the spec is complex and some of the tests don't
+seem to match any browser. Please feel free to discuss on the spec
+issue.
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/anchor-fragment-history-back-on-click.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/anchor-fragment-history-back-on-click.html
new file mode 100644
index 0000000000..a081bec514
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/anchor-fragment-history-back-on-click.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(async t => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ await new Promise(resolve => window.onload = () => t.step_timeout(resolve, 0));
+
+ location.hash = "#1";
+ assert_equals(location.hash, "#1");
+ location.hash = "#2";
+ assert_equals(location.hash, "#2");
+
+ let anchor = document.createElement("a");
+ anchor.href = "#3";
+ anchor.onclick = () => {
+ history.back();
+ };
+
+ let navigations = [];
+ let navigationsPromise = new Promise(resolve => {
+ onpopstate = () => {
+ navigations.push(location.hash);
+ if (navigations.length === 2) {
+ resolve();
+ }
+ }
+ });
+
+ anchor.click();
+ await navigationsPromise;
+
+ // We were on #2 when history.back() was called so we should be on #1 now.
+ assert_equals(location.hash, "#1");
+
+ // While the history navigation back to "#1" was pending, we should have navigated to "#3".
+ assert_array_equals(navigations, ["#3", "#1"]);
+}, "Anchor with a fragment href and a click handler that navigates back");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-nav.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-nav.html
new file mode 100644
index 0000000000..99d9a8fbb1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-nav.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cross-document navigation after a cross-document navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ According to the spec, the navigate algorithm synchronously cancels ongoing
+ non-mature navigations.
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad, waitForPotentialNetworkLoads } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.location.search = "?1";
+ iframe.contentWindow.location.search = "?2";
+ assert_equals(iframe.contentWindow.location.search, "");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?2");
+
+ iframe.onload = t.unreached_func("second load event");
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.search, "?2");
+}, "cross-document navigation then cross-document navigation");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-traversal.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-traversal.html
new file mode 100644
index 0000000000..341f66a996
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-traversal.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cross-document traversal during cross-document navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ According to the spec, "apply the history step" will set the ongoing
+ navigation to "traversal", canceling any non-mature navigations.
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad, delay } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.location.search = "?3";
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1", "must go back one step eventually");
+}, "cross-document navigations are stopped by cross-document back()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-nav.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-nav.html
new file mode 100644
index 0000000000..99525cb3ed
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-nav.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cross-document navigation after a same-document navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ According to the spec, the "URL and history update steps" (used by
+ pushState()) and the fragment navigation steps, do *not* modify the ongoing
+ navigation, i.e. do not cancel any navigations.
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.location.search = "?1";
+ iframe.contentWindow.location.hash = "#2";
+
+ assert_equals(iframe.contentWindow.location.search, "");
+ assert_equals(iframe.contentWindow.location.hash, "#2");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1");
+ assert_equals(iframe.contentWindow.location.hash, "");
+}, "cross-document navigation then fragment navigation");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.location.search = "?1";
+ iframe.contentWindow.history.pushState(null, "", "/2");
+
+ assert_equals(iframe.contentWindow.location.search, "");
+ assert_equals(iframe.contentWindow.location.pathname, "/2");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1");
+ assert_equals(iframe.contentWindow.location.pathname, "/common/blank.html");
+}, "cross-document navigation then pushState()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-traversal.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-traversal.html
new file mode 100644
index 0000000000..2ff91be7e1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-traversal.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Same-document traversal during cross-document navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ According to the spec, "apply the history step" will set the ongoing
+ navigation to "traversal", canceling any navigation that is still processing
+ in parallel and hasn't yet reached "apply the history step".
+-->
+
+<body>
+<script type="module">
+import { createIframe, delay } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.location.hash = "#1";
+ await delay(t, 0);
+ iframe.contentWindow.location.hash = "#2";
+ await delay(t, 0);
+
+ iframe.contentWindow.location.search = "?1";
+ iframe.contentWindow.onload = t.unreached_func("load event fired");
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.search, "", "must not go back synchronously (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go back synchronously (hash)");
+
+ // Does go back eventually, and only one step
+ await t.step_wait(() => iframe.contentWindow.location.hash === "#1" && iframe.contentWindow.location.search === "");
+}, "cross-document navigations are stopped by same-document back()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-stop.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-stop.html
new file mode 100644
index 0000000000..0803d6c8d1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-stop.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Stop during cross-document navigations</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script type="module">
+import { createIframe, waitForPotentialNetworkLoads } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.location.search = "?1";
+ iframe.contentWindow.onload = t.unreached_func("load event fired");
+ iframe.contentWindow.stop();
+
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.search, "");
+}, "cross-document navigations are stopped by stop()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-nav.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-nav.html
new file mode 100644
index 0000000000..5141259d08
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-nav.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cross-document navigations during cross-document traversals</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ According to the spec, if ongoing navigation is "traversal", the navigation
+ fails and nothing happens.
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad, delay, waitForPotentialNetworkLoads } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+ const slowURL = (new URL("resources/slow.py", location.href)).href;
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.href = slowURL;
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.href = "/common/blank.html?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously");
+
+ iframe.contentWindow.location.href = "/common/blank.html?3";
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not navigate synchronously");
+
+ // We end up at slow.py and never at /common/blank.html?3
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.href, slowURL, "first load after the nav");
+
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.href, slowURL, "must stay on slow.py");
+}, "slow cross-document traversal and then fast cross-document navigation: traversal wins and nav is ignored");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+ const slowURL = (new URL("resources/slow.py", location.href)).href;
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously");
+
+ iframe.contentWindow.location.href = slowURL;
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not navigate synchronously");
+
+ // We end up at ?1 and never at slowURL
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1", "first load after the nav");
+
+ // The long timeout is because slow.py would take 2 seconds, if it did load.
+ await delay(t, 3000);
+ assert_equals(iframe.contentWindow.location.search, "?1", "must stay on ?1");
+}, "fast cross-document traversal and then slow cross-document navigation: traversal wins and nav is ignored");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-traversal.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-traversal.html
new file mode 100644
index 0000000000..97907df23d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-traversal.html
@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cross-document traversals during cross-document traversals</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ In the spec, all traversals are queued, and that includes computing what
+ "back" and "forward" mean, based on the "current session history step". The
+ "current session history step" is updated at the end of "apply the history
+ step", at which point the queued steps in "traverse history by a delta" get to
+ run and compute what is back/forward. So the basic structure is:
+
+ - back(), back(): go back once, then again.
+ - back(), forward(): go back once, then go forward.
+
+ However, note that these observable effects (e.g., actually loading an
+ intermediate document) are done via queued tasks. Those tasks will end up not
+ running, once we switch the active document due to the second traversal. So
+ the end observable result looks like:
+
+ - back(), back(): go back -2.
+ - back(), forward(): go nowhere.
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad, delay, waitForPotentialNetworkLoads } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?3";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.history.back();
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ assert_equals(iframe.contentWindow.location.search, "?2", "we made our way to ?2 for setup");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go forward synchronously");
+
+ iframe.onload = t.unreached_func("second load event");
+
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.search, "?2", "must stay on ?2");
+}, "cross-document traversals in opposite directions: the result is going nowhere");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go forward synchronously");
+
+ iframe.onload = t.unreached_func("second load event");
+
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.search, "?2", "must stay on ?2");
+}, "cross-document traversals in opposite directions, second traversal invalid at queuing time but valid at the time it is run: the result is going nowhere");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?3";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.search, "?3", "must not go back synchronously (1)");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.search, "?3", "must not go back synchronously (2)");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1", "first load event must be going back");
+
+ iframe.onload = t.unreached_func("second load event");
+
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.search, "?1", "must stay on ?1");
+}, "cross-document traversals in the same (back) direction: the result is going -2 with only one load event");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?3";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.history.back();
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ assert_equals(iframe.contentWindow.location.search, "?2", "we made our way to ?2 for setup");
+ iframe.contentWindow.history.back();
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ assert_equals(iframe.contentWindow.location.search, "?1", "we made our way to ?1 for setup");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.search, "?1", "must not go forward synchronously (1)");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.search, "?1", "must not go forward synchronously (2)");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?3", "first load event must be going forward");
+
+ iframe.onload = t.unreached_func("second load event");
+
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.search, "?3", "must stay on ?3");
+}, "cross-document traversals in the same (forward) direction: the result is going +2 with only one load event");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-nav.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-nav.html
new file mode 100644
index 0000000000..df6258f9b3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-nav.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Same-document navigations during cross-document traversals</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ The spec explicitly covers this case, with a Jake diagram:
+ https://whatpr.org/html/6315/browsing-the-web.html#example-sync-navigation-steps-queue-jumping-basic
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad, delay } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously");
+
+ iframe.contentWindow.location.hash = "#3";
+ assert_equals(iframe.contentWindow.location.search, "?2");
+ assert_equals(iframe.contentWindow.location.hash, "#3");
+
+ // Eventually ends up on ?1
+ await t.step_wait(() => iframe.contentWindow.location.search === "?1" && iframe.contentWindow.location.hash === "");
+}, "same-document traversals + fragment navigations");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously");
+
+ iframe.contentWindow.history.pushState(null, "", "?3");
+ assert_equals(iframe.contentWindow.location.search, "?3");
+
+ // Eventually ends up on ?1
+ await t.step_wait(() => iframe.contentWindow.location.search === "?1");
+}, "same-document traversals + pushState()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-traversal.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-traversal.html
new file mode 100644
index 0000000000..3c37c46b64
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-traversal.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Same-document traversals during cross-document traversals</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ This case is not significantly different from
+ cross-document-traversal-cross-document-traversal.html.
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad, waitForHashchange, delay, waitForPotentialNetworkLoads } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.hash = "#2";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.location.search = "?3";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.search, "?3", "must not go back synchronously 1 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go back synchronously 1 (hash)");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.search, "?3", "must not go back synchronously 2 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go back synchronously 2 (hash)");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1", "first load event must be going back (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "first load event must be going back (hash)");
+
+ iframe.contentWindow.onhashchange = t.unreached_func("hashchange event");
+ iframe.onload = t.unreached_func("second load event");
+
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.search, "?1", "must stay on ?1 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "must stay on ?1 (hash)");
+}, "traversals in the same (back) direction: coalesced");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.hash = "#3";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.history.back();
+ await waitForHashchange(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.hash, "", "we made our way to ?2 for setup");
+ iframe.contentWindow.history.back();
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ assert_equals(iframe.contentWindow.location.search, "?1", "we made our way to ?1 for setup");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.search, "?1", "must not go forward synchronously 1 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "must not go forward synchronously 1 (hash)");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.search, "?1", "must not go forward synchronously 2 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "must not go forward synchronously 2 (hash)");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?2", "first load event must be going forward (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#3", "first load event must be going forward (hash)");
+
+ iframe.contentWindow.onhashchange = t.unreached_func("hashchange event");
+ iframe.onload = t.unreached_func("second load event");
+
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.search, "?2", "must stay on ?2");
+ assert_equals(iframe.contentWindow.location.hash, "#3", "must stay on ?2");
+}, "traversals in the same (forward) direction: coalesced");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-stop.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-stop.html
new file mode 100644
index 0000000000..6202eb9226
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-stop.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Stop during cross-document traversals</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ The spec says that stop() must not stop traverals.
+
+ (Note: the spec also says the UI "stop" button must not stop traversals, but
+ that does not match browsers. See https://github.com/whatwg/html/issues/6905.
+ But that is not what's under test here.)
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad, delay } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously");
+
+ window.stop();
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1", "must go back eventually");
+}, "cross-document traversals are not stopped by stop()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/forward-to-pruned-entry.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/forward-to-pruned-entry.html
new file mode 100644
index 0000000000..8e1c349e21
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/forward-to-pruned-entry.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(async t => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ await new Promise(resolve => window.onload = () => t.step_timeout(resolve, 0));
+ location.hash = "#1";
+ location.hash = "#2";
+ history.go(-2);
+ await new Promise(r => window.onpopstate = r);
+
+ // Traverse forward then immediately do a same-document push. This will
+ // truncate the back forward list.
+ history.forward();
+ location.hash = "#clobber";
+
+ // history.forward() should be aborted.
+ window.onpopstate = t.unreached_func("history.forward() should have been cancelled");
+ await new Promise(r => t.step_timeout(r, 20));
+ assert_equals(location.hash, "#clobber");
+}, "If forward pruning clobbers the target of a traverse, abort");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/nav-cancelation-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/nav-cancelation-1.html
new file mode 100644
index 0000000000..b52fa04977
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/nav-cancelation-1.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent main frame cancels a same-origin child whose navigation is pending</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ This test asserts that a parent canceling a same-origin child's cross-origin
+ navigation does not result in load events firing synchronously in the parent
+-->
+
+<body>
+
+<iframe src=resources/slow.py></iframe>
+
+<script>
+promise_test(async t => {
+ let window_load_fired = false;
+ let iframe_load_fired = false;
+ const iframe = document.querySelector('iframe');
+
+ const window_load_promise = new Promise(resolve => {
+ window.onload = () => {
+ window_load_fired = true;
+ resolve();
+ }
+ });
+
+ const iframe_onload_promise = new Promise(resolve => {
+ iframe.onload = () => {
+ iframe_load_fired = true;
+ resolve();
+ }
+ });
+
+ // While the child navigation is in-flight, cancel it and record when the
+ // parent `load` event fires.
+ window.frames[0].location.href = "resources/slow.py?different";
+
+ // Synchronously after cancelation, no load events should have been fired.
+ assert_false(window_load_fired,
+ "Parent's load event does not synchronously fire after cancelation");
+ assert_false(iframe_load_fired,
+ "<iframe> load event does not synchronously fire after cancelation");
+
+ // Load events did not fire in a microtask after cancelation.
+ await Promise.resolve();
+ assert_false(window_load_fired,
+ "Parent's load event does not fire in the microtask after cancelation");
+ assert_false(iframe_load_fired,
+ "<iframe> load event does not fire in the microtask after cancelation");
+
+ // Canceling the navigation should unblock the parent's load event, but the
+ // new iframe navigation should still be pending, and the iframe load event
+ // shouldn't fire until *that one* is complete.
+ await window_load_promise;
+ assert_true(window_load_fired,
+ "Parent's load event fires asynchronously after child navigation cancelation");
+ assert_false(iframe_load_fired,
+ "<iframe> load event does not fire until subsequent navigation is complete");
+}, "parent cancels a pending navigation in a same-origin child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/nav-cancelation-2.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/nav-cancelation-2.sub.html
new file mode 100644
index 0000000000..c081513b7c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/nav-cancelation-2.sub.html
@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Grandparent main frame cancels a navigation in a cross-origin grandchild</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ This test asserts that an ancestor canceling a cross-origin descendant's
+ ongoing navigation does not result in load events firing in the ancestor
+ synchronously.
+
+ The reason this test uses a grandparent/grandchild pair to represent the
+ ancestor/descendant, instead of a parent/child pair, is because if a child
+ frame is blocking its parent window's load event, that means the child frame
+ navigation is being made from the initial about:blank Document to some
+ resource, and the initial about:blank child is synchronously scriptable from
+ the parent since they share the same window agent. This test is trying to
+ capture the scenario where the descendant document (that owns the ongoing
+ navigation) is hosted/scheduled on a different agent than the ancestor
+ document that cancels the descendant's ongoing navigation. The only way to do
+ this is to have a grandparent frame load a cross-origin child, whose document
+ itself loads a child frame that has a very slow ongoing navigation. That way
+ the grandparent can reach the grandchild via `window.frames[0].frames[0]`,
+ which is a proxy to the document living in a different agent.
+-->
+
+<body>
+
+<iframe src="http://{{domains[www1]}}:{{ports[http][0]}}/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/nav-cancelation-2-helper.html"></iframe>
+
+<script>
+promise_test(async t => {
+ let window_load_fired = false;
+ let iframe_load_fired = false;
+ let grandchild_iframe_load_fired = false;
+ const iframe = document.querySelector('iframe');
+
+ const window_load_promise = new Promise(resolve => {
+ window.onload = () => {
+ window_load_fired = true;
+ resolve();
+ }
+ });
+
+ const iframe_onload_promise = new Promise(resolve => {
+ iframe.onload = () => {
+ iframe_load_fired = true;
+ resolve();
+ }
+ });
+
+ // Let the grandchild frame get registered in window.frames.
+ await new Promise((resolve, reject) => {
+ window.addEventListener('message', e => {
+ if (e.data != "grandchild frame created") {
+ reject(Error(`Expected 'grandchild frame created', but got ${e.data}`));
+ }
+
+ resolve();
+ }, {once: true});
+ });
+
+ // Set up a message handler to listen to the grandchild's iframe element's
+ // load event being fired. We should never get this message, and we assert
+ // this below. If we ever get this message, that means one of two things:
+ // 1.) The grandparent (this document)'s load event was blocked on the
+ // completion of its grandchild's subsequent navigation (after
+ // cancelation)
+ // 2.) After the grandchild's navigation was canceled, its <iframe>'s load
+ // event was fired before its subsequent navigation completed
+ // Both of these are wrong.
+ addEventListener('message', e => {
+ assert_equals(e.data, "grandchild frame loaded",
+ `Expected 'grandchild frame loaded', but got ${e.data}`);
+ grandchild_iframe_load_fired = true;
+ });
+
+ // While the grandchild navigation is in-flight, cancel it and record when the
+ // our `load` event fires. The second navigation is a slow resource so that
+ // the speed of the network doesn't cause the grandchild load event to fire
+ // early and confuse the grandparent when running the assertions below. We're
+ // trying to clearly separate out when the grandparent load event fires vs
+ // when the grandchild load event fires.
+ window.frames[0].frames[0].location.href = "resources/slow.py?different";
+
+ // Synchronously after cancelation, no load events should have been fired.
+ assert_false(window_load_fired,
+ "Grandparent's load event does not synchronously fire after grandchild " +
+ "navigation cancelation");
+ assert_false(iframe_load_fired,
+ "<iframe> load event does not synchronously fire after grandchild " +
+ "navigation cancelation");
+ assert_false(grandchild_iframe_load_fired,
+ "Grandchild <iframe>'s load event does not synchronously fire upon " +
+ "navigation cancelation");
+
+ // Load events did not fire in a microtask after cancelation.
+ await Promise.resolve();
+ assert_false(window_load_fired,
+ "Grandparent's load event does not fire in the microtask after " +
+ "navigation canceled");
+ assert_false(iframe_load_fired,
+ "<iframe> load event does not fire in the microtask after navigation " +
+ "canceled");
+ assert_false(grandchild_iframe_load_fired,
+ "Grandchild <iframe> load event does not fire in the microtask after " +
+ "navigation canceled");
+
+ // Canceling the navigation should however, asynchronously unblock, in this
+ // order:
+ // 1.) Our child window's load event, captured by our `iframe`'s load event
+ // 2.) Our window load event
+ // On the other hand, the grandchild navigation should still be ongoing, so
+ // inside our child's document, the nested <iframe> representing our
+ // grandchild should not have had its load event fired yet.
+ await iframe_onload_promise;
+ assert_true(iframe_load_fired);
+ assert_false(window_load_fired,
+ "Grandparent's load event does not fire before its child iframe's load " +
+ "event");
+ assert_false(grandchild_iframe_load_fired,
+ "Grandchild <iframe>'s load event does not fire before its parent's load " +
+ "event and grandparent's load event");
+
+ // We want to assert that the grandparent is not (incorrectly) blocked on its
+ // grandchild's second navigation from completing. One sign that it was
+ // incorrectly blocked on its grandchild's second navigation is if the
+ // grandparent receives a message (saying that the grandchild <iframe>
+ // element's load event fired) before the grandparent's load event fires.
+ //
+ // This indicates a weird state where the grandparent's immediate child fired
+ // its load event in response to navigation cancelation (see the assertions
+ // above), but the grandparent itself is still blocked on the grandchild
+ // loading. If this is the case, the the postMessage() (that sets
+ // `grandchild_iframe_load_fired = true`) is received by the grandparent just
+ // before the grandparent's load event is unblocked and fired. Therefore we
+ // can detect this situation by checking `grandchild_iframe_load_fired`.
+ await window_load_promise;
+ assert_true(iframe_load_fired);
+ assert_true(window_load_fired,
+ "Grandparent's load event fires asynchronously after grandchild " +
+ "navigation cancelation");
+ assert_false(grandchild_iframe_load_fired,
+ "Grandchild <iframe> load event doesn't fire before grandparent's " +
+ "load event");
+
+ // Verify that the grandchild <iframe>'s load event does not fire within one
+ // task of the grandchild's load event from being fired. This is to further
+ // verify that the grandparent's load event is not tied to its grandchild's
+ // second navigation.
+ //
+ // If for example, the grandparent's load event *is* blocked on the
+ // grandchild's second navigation from finishing, it is still possible for the
+ // grandparent's load event to fire. For example, Chromium has a bug where if
+ // both are true:
+ // 1.) The grandparent frame is in the same process as the grandchild frame
+ // 2.) The grandparent frame's load event is blocked on its grandchild's
+ // second navigation
+ //
+ // ...then the following will happen:
+ // 1.) The grandchild's load event will fire, triggering a postMessage() to
+ // the grandparent frame. This queues a task to run the grandparent's
+ // message handler.
+ // 2.) The grandparent's load event will *immediately* fire, and the
+ // postMessage() will fire a single task later since it is queued.
+ //
+ // Therefore, we assert that `grandchild_iframe_load_fired` is not true up to
+ // a single task after the grandparent's load event fires.
+ await new Promise(resolve => {
+ t.step_timeout(resolve, 0);
+ });
+
+ assert_false(grandchild_iframe_load_fired,
+ "Grandchild <iframe>'s load event does not fire at least one task " +
+ "after the grandparent's window load event fires. It should only fire " +
+ "when its subsequent navigation is complete");
+}, "grandparent cancels a pending navigation in a cross-origin grandchild");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/helpers.mjs b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/helpers.mjs
new file mode 100644
index 0000000000..7938497920
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/helpers.mjs
@@ -0,0 +1,52 @@
+export function createIframe(t) {
+ return new Promise((resolve, reject) => {
+ const iframe = document.createElement("iframe");
+ iframe.onload = () => resolve(iframe);
+ iframe.onerror = () => reject(new Error("Could not load iframe"));
+ iframe.src = "/common/blank.html";
+
+ t.add_cleanup(() => iframe.remove());
+ document.body.append(iframe);
+ });
+}
+
+export function delay(t, ms) {
+ return new Promise(resolve => t.step_timeout(resolve, ms));
+}
+
+export function waitForLoad(obj) {
+ return new Promise(resolve => {
+ obj.addEventListener("load", resolve, { once: true });
+ });
+}
+
+export function waitForHashchange(obj) {
+ return new Promise(resolve => {
+ obj.addEventListener("hashchange", resolve, { once: true });
+ });
+}
+
+export function waitForPopstate(obj) {
+ return new Promise(resolve => {
+ obj.addEventListener("popstate", resolve, { once: true });
+ });
+}
+
+// This is used when we want to end the test by asserting some load doesn't
+// happen, but we're not sure how long to wait. We could just wait a long-ish
+// time (e.g. a second), but that makes the tests slow. Instead, assume that
+// network loads take roughly the same time. Then, you can use this function to
+// wait a small multiple of the duration of a separate iframe load; this should
+// be long enough to catch any problems.
+export async function waitForPotentialNetworkLoads(t) {
+ const before = performance.now();
+
+ // Sometimes we're doing something, like a traversal, which cancels our first
+ // attempt at iframe loading. In that case we bail out after 100 ms and try
+ // again. (Better ideas welcome...)
+ await Promise.race([createIframe(t), delay(t, 100)]);
+ await createIframe(t);
+
+ const after = performance.now();
+ await delay(t, after - before);
+}
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/nav-cancelation-2-helper.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/nav-cancelation-2-helper.html
new file mode 100644
index 0000000000..a0b4acda2e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/nav-cancelation-2-helper.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Page with child frame that navigates slowly</title>
+
+<!--
+ This file is used by `../nav-cancelation-2.sub.html`. The iframe below is its
+ grandchild iframe, and whenever its load event fires we report this up to our
+ parent Document.
+-->
+<iframe src="slow.py"></iframe>
+
+<script>
+ window.parent.postMessage("grandchild frame created", "*");
+ const iframe = document.querySelector('iframe');
+ iframe.onload = e => {
+ window.parent.postMessage("grandchild frame loaded", "*");
+ };
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/slow.py b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/slow.py
new file mode 100644
index 0000000000..5ee32a60ba
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/slow.py
@@ -0,0 +1,7 @@
+# Like /common/slow.py except with text/html content-type so that it won't
+# trigger strange parts of the <iframe> navigate algorithm.
+import time
+
+def main(request, response):
+ time.sleep(2)
+ return 200, [["Content-Type", "text/html"]], b''
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-cross-document-nav.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-cross-document-nav.html
new file mode 100644
index 0000000000..8082e9bbe0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-cross-document-nav.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cross-document navigation after a same-document navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ These tests are kind of silly since it's hard to imagine any other result:
+ same-document navigations are always synchronous so of course the
+ same-document navigation will succeed, followed by the cross-document one.
+
+ Nevertheless they're nice as a basis from which to write corresponding app
+ history tests, where the consequences aren't as obvious.
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.location.hash = "#1";
+ assert_equals(iframe.contentWindow.location.hash, "#1");
+
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?2");
+}, "fragment navigation then cross-document navigation");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.history.pushState(null, "", "?1");
+ assert_equals(iframe.contentWindow.location.search, "?1");
+
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?2");
+}, "pushState() then cross-document navigation");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-cross-document-traversal.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-cross-document-traversal.html
new file mode 100644
index 0000000000..fc6f92e819
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-cross-document-traversal.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Traversal after a same-document navigations</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ These tests are kind of silly since it's hard to imagine any other result:
+ same-document navigations are always synchronous so of course back() won't
+ cancel them.
+
+ Nevertheless they're nice as a basis from which to write corresponding app
+ history tests, where the consequences aren't as obvious.
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad, delay } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.location.hash = "#3";
+ iframe.contentWindow.history.go(-2);
+
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#3", "must not go back synchronously (hash)");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1", "must go back eventually (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "must go back eventually (hash)");
+}, "fragment navigation then go(-2)");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.history.pushState(null, "", "/3");
+ iframe.contentWindow.history.go(-2);
+
+ assert_equals(iframe.contentWindow.location.search, "", "must not go back synchronously (search)");
+ assert_equals(iframe.contentWindow.location.pathname, "/3", "must not go back synchronously (pathname)");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1", "must go back eventually (search)");
+ assert_equals(iframe.contentWindow.location.pathname, "/common/blank.html", "must go back eventually (pathname)");
+
+}, "pushState then go(-2)");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-same-document-nav.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-same-document-nav.html
new file mode 100644
index 0000000000..2d8961d6e4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-same-document-nav.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Same-document navigation after a same-document navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ These tests are kind of silly since it's hard to imagine any other result:
+ same-document navigations are always synchronous so of course two in a row
+ will succeed.
+
+ Nevertheless they're nice as a basis from which to write corresponding app
+ history tests, where the consequences aren't as obvious.
+-->
+
+<body>
+<script type="module">
+import { createIframe } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.location.hash = "#1";
+ assert_equals(iframe.contentWindow.location.hash, "#1");
+
+ iframe.contentWindow.location.hash = "#2";
+ assert_equals(iframe.contentWindow.location.hash, "#2");
+}, "fragment navigation then fragment navigation");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.history.pushState(null, "", "?1");
+ assert_equals(iframe.contentWindow.location.search, "?1");
+
+ iframe.contentWindow.history.pushState(null, "", "?2");
+ assert_equals(iframe.contentWindow.location.search, "?2");
+}, "pushState() then pushState()");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.history.pushState(null, "", "?1");
+ assert_equals(iframe.contentWindow.location.search, "?1");
+
+ iframe.contentWindow.location.hash = "#2";
+ assert_equals(iframe.contentWindow.location.search, "?1");
+ assert_equals(iframe.contentWindow.location.hash, "#2");
+}, "pushState() then fragment navigation");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.location.hash = "#1";
+ assert_equals(iframe.contentWindow.location.hash, "#1");
+
+ iframe.contentWindow.history.pushState(null, "", "?2");
+ assert_equals(iframe.contentWindow.location.search, "?2");
+ assert_equals(iframe.contentWindow.location.hash, "");
+}, "fragment navigation then pushState()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-same-document-traversal.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-same-document-traversal.html
new file mode 100644
index 0000000000..a112143837
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-same-document-traversal.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Same-document traversal after a same-document navigations</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ These tests are kind of silly since it's hard to imagine any other result:
+ same-document navigations are always synchronous so of course back() won't
+ cancel them.
+
+ Nevertheless they're nice as a basis from which to write corresponding app
+ history tests, where the consequences aren't as obvious.
+-->
+
+<body>
+<script type="module">
+import { createIframe, delay } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.location.hash = "#1";
+ await delay(t, 0);
+ iframe.contentWindow.location.hash = "#2";
+ await delay(t, 0);
+
+ iframe.contentWindow.location.hash = "#3";
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.hash, "#3", "must not go back synchronously");
+
+ // Does go back eventually, and only one step
+ await t.step_wait(() => iframe.contentWindow.location.hash === "#2");
+}, "fragment navigation then back()");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.history.pushState(null, "", "?1");
+ await delay(t, 0);
+ iframe.contentWindow.history.pushState(null, "", "?2");
+ await delay(t, 0);
+
+ iframe.contentWindow.history.pushState(null, "", "?3");
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.search, "?3", "must not go back synchronously");
+
+ // Does go back eventually, and only one step
+ await t.step_wait(() => iframe.contentWindow.location.search === "?2");
+}, "pushState then back()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-stop.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-stop.html
new file mode 100644
index 0000000000..a9036209a5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-stop.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Stop after a same-document navigations</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script type="module">
+import { createIframe } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.location.hash = "#1";
+ iframe.contentWindow.stop();
+
+ assert_equals(iframe.contentWindow.location.hash, "#1");
+}, "fragment navigations are not stopped by stop()");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.history.pushState(null, "", "?1");
+ iframe.contentWindow.stop();
+
+ assert_equals(iframe.contentWindow.location.search, "?1");
+}, "pushState() navigations are not stopped by stop()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-cross-document-nav.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-cross-document-nav.html
new file mode 100644
index 0000000000..37960c3c54
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-cross-document-nav.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cross-document navigations during same-document traversals</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ The spec says that navigations are ignored if there is an ongoing traversal.
+-->
+
+<body>
+<script type="module">
+import { createIframe, delay, waitForPotentialNetworkLoads } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.location.hash = "#1";
+ await delay(t, 0);
+ iframe.contentWindow.location.hash = "#2";
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go back synchronously");
+
+ iframe.contentWindow.location.search = "?1";
+ assert_equals(iframe.contentWindow.location.search, "", "must not navigate synchronously (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not navigate synchronously (hash)");
+
+ // Eventually ends up on #1.
+ await t.step_wait(() => iframe.contentWindow.location.hash === "#1", "traversal");
+
+ // Never loads a different document.
+ iframe.onload = t.unreached_func("load event");
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.search, "", "must stay on #2 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must stay on #2 (hash)");
+}, "same-document traversals are not canceled by cross-document navigations");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-cross-document-traversal.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-cross-document-traversal.html
new file mode 100644
index 0000000000..a48f4d484f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-cross-document-traversal.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cross-document traversals during same-document traversals</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ Compare this to cross-document-traversal-cross-document-traversal.html. Since
+ there are no network loads for the first traversal here, it does observably go
+ through. So we end up with both traversals before observable in sequence.
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad, waitForHashchange, delay } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.hash = "#3";
+ await waitForHashchange(iframe.contentWindow);
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously 1 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#3", "must not go back synchronously 1 (hash)");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously 1 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#3", "must not go back synchronously 1 (hash)");
+
+ await waitForHashchange(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.search, "?2", "first hashchange event must be going back (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "first hashchange event must be going back (hash)");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1", "first load event must be going back (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "first load event must be going back (hash)");
+}, "traversals in the same (back) direction: queued up");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.hash = "#2";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.location.search = "?3";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.history.back();
+ await waitForLoad(iframe);
+ iframe.contentWindow.history.back();
+ await waitForHashchange(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.search, "?1", "we made our way to ?1 for setup (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "we made our way to ?1 for setup (search)");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.search, "?1", "must not go forward synchronously 1 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "must not go forward synchronously 1 (hash)");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.search, "?1", "must not go forward synchronously 2 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "must not go forward synchronously 2 (hash)");
+
+ await waitForHashchange(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.search, "?1", "first hashchange event must be going forward (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#2", "first hashchange event must be going forward (hash)");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?3", "first load event must be going forward (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#2", "first load event must be going forward (hash)");
+}, "traversals in the same (forward) direction: queued up");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-nav.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-nav.html
new file mode 100644
index 0000000000..5094651ab5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-nav.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Same-document navigations during same-document traversals</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ Per spec, same-document navigations ignore the "ongoing navigation" flag and
+ just happen synchronously, then queue onto the session history traversal queue
+ to update the source of truth. However, the traversal was queued first, so it
+ will ignore that update when calculating its endpoint.
+-->
+
+<body>
+<script type="module">
+import { createIframe, delay } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.location.hash = "#1";
+ await delay(t, 0);
+ iframe.contentWindow.location.hash = "#2";
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go back synchronously");
+
+ iframe.contentWindow.location.hash = "#3";
+ assert_equals(iframe.contentWindow.location.hash, "#3");
+
+ // Eventually ends up on #1
+ await t.step_wait(() => iframe.contentWindow.location.hash === "#1");
+}, "same-document traversals are not canceled by fragment navigations and calculate their endpoint based on the original placement");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.history.pushState(null, "", "/1");
+ await delay(t, 0);
+ iframe.contentWindow.history.pushState(null, "", "/2");
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "must not go back synchronously");
+
+ iframe.contentWindow.history.pushState(null, "", "/3");
+ assert_equals(iframe.contentWindow.location.pathname, "/3");
+
+ // Eventually ends up on /1
+ await t.step_wait(() => iframe.contentWindow.location.pathname === "/1");
+}, "same-document traversals are not canceled by pushState() and calculate their endpoint based on the original placement");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-hashchange.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-hashchange.html
new file mode 100644
index 0000000000..df5ea5caab
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-hashchange.html
@@ -0,0 +1,148 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Same-document traversals during same-document traversals (using fragment navigations)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ Compare this to cross-document-traversal-cross-document-traversal.html. Since
+ there are no network loads or document unloads to cancel tasks, both
+ traversals should observably go through. Target step calculation for the
+ second traversal should take place after the first traversal is finished. So
+ we end up with both traversals observable in sequence.
+-->
+
+<body>
+<script type="module">
+import { createIframe, delay, waitForHashchange } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+ const baseURL = iframe.contentWindow.location.href;
+
+ // Setup
+ iframe.contentWindow.location.hash = "#1";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.location.hash = "#2";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.location.hash = "#3";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.history.back();
+ await waitForHashchange(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.hash, "#2", "we made our way to #2 for setup");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go back synchronously");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go forward synchronously");
+
+ const event1 = await waitForHashchange(iframe.contentWindow);
+ assert_equals(event1.oldURL, baseURL + "#2", "oldURL 1");
+ assert_equals(event1.newURL, baseURL + "#1", "newURL 1");
+ // Cannot test iframe.contentWindow.location.hash since the second history
+ // traversal task is racing with the fire an event task, so we don't know
+ // which will happen first.
+
+ const event2 = await waitForHashchange(iframe.contentWindow);
+ assert_equals(event2.oldURL, baseURL + "#1", "oldURL 2");
+ assert_equals(event2.newURL, baseURL + "#2", "newURL 2");
+ assert_equals(iframe.contentWindow.location.hash, "#2");
+}, "same-document traversals in opposite directions: queued up");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+ const baseURL = iframe.contentWindow.location.href;
+
+ // Setup
+ iframe.contentWindow.location.hash = "#1";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.location.hash = "#2";
+ await waitForHashchange(iframe.contentWindow);
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go back synchronously");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go forward synchronously");
+
+ const event1 = await waitForHashchange(iframe.contentWindow);
+ assert_equals(event1.oldURL, baseURL + "#2", "oldURL 1");
+ assert_equals(event1.newURL, baseURL + "#1", "newURL 1");
+ // Cannot test iframe.contentWindow.location.hash since the second history
+ // traversal task is racing with the fire an event task, so we don't know
+ // which will happen first.
+
+ const event2 = await waitForHashchange(iframe.contentWindow);
+ assert_equals(event2.oldURL, baseURL + "#1", "oldURL 2");
+ assert_equals(event2.newURL, baseURL + "#2", "newURL 2");
+ assert_equals(iframe.contentWindow.location.hash, "#2");
+}, "same-document traversals in opposite directions, second traversal invalid at queuing time: queued up");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+ const baseURL = iframe.contentWindow.location.href;
+
+ // Setup
+ iframe.contentWindow.location.hash = "#1";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.location.hash = "#2";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.location.hash = "#3";
+ await waitForHashchange(iframe.contentWindow);
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.hash, "#3", "must not go back synchronously (1)");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.hash, "#3", "must not go back synchronously (2)");
+
+ const event1 = await waitForHashchange(iframe.contentWindow);
+ assert_equals(event1.oldURL, baseURL + "#3", "oldURL 1");
+ assert_equals(event1.newURL, baseURL + "#2", "newURL 1");
+ // Cannot test iframe.contentWindow.location.hash since the second history
+ // traversal task is racing with the fire an event task, so we don't know
+ // which will happen first.
+
+ const event2 = await waitForHashchange(iframe.contentWindow);
+ assert_equals(event2.oldURL, baseURL + "#2", "oldURL 2");
+ assert_equals(event2.newURL, baseURL + "#1", "newURL 2");
+ assert_equals(iframe.contentWindow.location.hash, "#1");
+}, "same-document traversals in the same (back) direction: queue up");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+ const baseURL = iframe.contentWindow.location.href;
+
+ // Setup
+ iframe.contentWindow.location.hash = "#1";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.location.hash = "#2";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.location.hash = "#3";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.history.back();
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.history.back();
+ await waitForHashchange(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.hash, "#1", "we made our way to #1 for setup");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.hash, "#1", "must not go forward synchronously (1)");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.hash, "#1", "must not go forward synchronously (2)");
+
+ const event1 = await waitForHashchange(iframe.contentWindow);
+ assert_equals(event1.oldURL, baseURL + "#1", "oldURL 1");
+ assert_equals(event1.newURL, baseURL + "#2", "newURL 1");
+ // Cannot test iframe.contentWindow.location.hash since the second history
+ // traversal task is racing with the fire an event task, so we don't know
+ // which will happen first.
+
+ const event2 = await waitForHashchange(iframe.contentWindow);
+ assert_equals(event2.oldURL, baseURL + "#2", "oldURL 2");
+ assert_equals(event2.newURL, baseURL + "#3", "newURL 2");
+ assert_equals(iframe.contentWindow.location.hash, "#3");
+}, "same-document traversals in the same (forward) direction: queue up");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-pushstate.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-pushstate.html
new file mode 100644
index 0000000000..47c7d6e3dc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-pushstate.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Same-document traversals during same-document traversals (using pushState())</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ Compare this to cross-document-traversal-cross-document-traversal.html. Since
+ there are no network loads or document unloads to cancel tasks, both
+ traversals should observably go through. Target step calculation for the
+ second traversal should take place after the first traversal is finished. So
+ we end up with both traversals observable in sequence.
+-->
+
+<body>
+<script type="module">
+import { createIframe, delay, waitForPopstate } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.history.pushState(1, "", "/1");
+ assert_equals(iframe.contentWindow.location.pathname, "/1", "setup /1");
+ iframe.contentWindow.history.pushState(2, "", "/2");
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "setup /2");
+ iframe.contentWindow.history.pushState(3, "", "/3");
+ assert_equals(iframe.contentWindow.location.pathname, "/3", "setup /3");
+ iframe.contentWindow.history.back();
+ await waitForPopstate(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "we made our way to /2 for setup");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "must not go back synchronously");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "must not go forward synchronously");
+
+ const event1 = await waitForPopstate(iframe.contentWindow);
+ assert_equals(event1.state, 1, "state 1");
+ // Cannot test iframe.contentWindow.location.pathname since the second history
+ // traversal task is racing with the fire an event task, so we don't know
+ // which will happen first.
+
+ const event2 = await waitForPopstate(iframe.contentWindow);
+ assert_equals(event2.state, 2, "state 2");
+ assert_equals(iframe.contentWindow.location.pathname, "/2");
+}, "same-document traversals in opposite directions: queued up");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.history.pushState(1, "", "/1");
+ assert_equals(iframe.contentWindow.location.pathname, "/1", "setup /1");
+ iframe.contentWindow.history.pushState(2, "", "/2");
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "we made our way to /2 for setup");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "must not go back synchronously");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "must not go forward synchronously");
+
+ const event1 = await waitForPopstate(iframe.contentWindow);
+ assert_equals(event1.state, 1, "state 1");
+ // Cannot test iframe.contentWindow.location.pathname since the second history
+ // traversal task is racing with the fire an event task, so we don't know
+ // which will happen first.
+
+ const event2 = await waitForPopstate(iframe.contentWindow);
+ assert_equals(event2.state, 2, "state 2");
+ assert_equals(iframe.contentWindow.location.pathname, "/2");
+}, "same-document traversals in opposite directions, second traversal invalid at queuing time: queued up");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.history.pushState(1, "", "/1");
+ assert_equals(iframe.contentWindow.location.pathname, "/1", "setup /1");
+ iframe.contentWindow.history.pushState(2, "", "/2");
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "setup /2");
+ iframe.contentWindow.history.pushState(3, "", "/3");
+ assert_equals(iframe.contentWindow.location.pathname, "/3", "we made our way to /3 for setup");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.pathname, "/3", "must not go back synchronously (1)");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.pathname, "/3", "must not go back synchronously (2)");
+
+ const event1 = await waitForPopstate(iframe.contentWindow);
+ assert_equals(event1.state, 2, "state 1");
+ // Cannot test iframe.contentWindow.location.pathname since the second history
+ // traversal task is racing with the fire an event task, so we don't know
+ // which will happen first.
+
+ const event2 = await waitForPopstate(iframe.contentWindow);
+ assert_equals(event2.state, 1, "state 2");
+ assert_equals(iframe.contentWindow.location.pathname, "/1");
+}, "same-document traversals in the same (back) direction: queue up");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.history.pushState(1, "", "/1");
+ assert_equals(iframe.contentWindow.location.pathname, "/1", "setup /1");
+ iframe.contentWindow.history.pushState(2, "", "/2");
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "setup /2");
+ iframe.contentWindow.history.pushState(3, "", "/3");
+ assert_equals(iframe.contentWindow.location.pathname, "/3", "setup /3");
+ iframe.contentWindow.history.back();
+ await waitForPopstate(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "setup /2 again");
+ iframe.contentWindow.history.back();
+ await waitForPopstate(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.pathname, "/1", "we made our way to /1 for setup");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.pathname, "/1", "must not go forward synchronously (1)");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.pathname, "/1", "must not go forward synchronously (2)");
+
+ const event1 = await waitForPopstate(iframe.contentWindow);
+ assert_equals(event1.state, 2, "state 1");
+ // Cannot test iframe.contentWindow.location.pathname since the second history
+ // traversal task is racing with the fire an event task, so we don't know
+ // which will happen first.
+
+ const event2 = await waitForPopstate(iframe.contentWindow);
+ assert_equals(event2.state, 3, "state 2");
+ assert_equals(iframe.contentWindow.location.pathname, "/3");
+}, "same-document traversals in the same (forward) direction: queue up");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-stop.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-stop.html
new file mode 100644
index 0000000000..2f0570380a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-stop.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Stop during same-document traversals</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ The spec says that stop() must not stop traverals.
+
+ (Note: the spec also says the UI "stop" button must not stop traversals, but
+ that does not match browsers. See https://github.com/whatwg/html/issues/6905.
+ But that is not what's under test here.)
+-->
+
+<body>
+<script type="module">
+import { createIframe, delay } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.location.hash = "#1";
+ await delay(t, 0);
+ iframe.contentWindow.location.hash = "#2";
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go back synchronously");
+
+ window.stop();
+
+ // Does go back eventually
+ await t.step_wait(() => iframe.contentWindow.location.hash === "#1");
+}, "same-document traversals are not stopped by stop()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/read-media/cross-origin-video.html b/testing/web-platform/tests/html/browsers/browsing-the-web/read-media/cross-origin-video.html
new file mode 100644
index 0000000000..5f3e95cce4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/read-media/cross-origin-video.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<title>Test cross origin load of media document in parts</title>
+<link rel="motivation" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1781759">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body></body>
+<script>
+promise_test(async () => {
+ const frame = document.createElement('iframe');
+ const dir = location.pathname.replace(/\/[^\/]*$/, '/');
+ frame.src =
+ // remote origin intermediate document
+ get_host_info().HTTP_NOTSAMESITE_ORIGIN + dir
+ // iframe-document.sub.html has an iframe with src=childsrc.
+ + 'resources/iframe-document.sub.html?childsrc='
+ // same origin video document, so that we can play().
+ + get_host_info().ORIGIN
+ // 'PartialContent' ensures that the entire video resource does not load
+ // in one fetch.
+ + '/service-workers/service-worker/resources/fetch-access-control.py?'
+ + 'VIDEO%26PartialContent';
+
+ let v = document.createElement("video");
+ if (v.canPlayType("video/ogv") == "")
+ frame.src += "%26mp4";
+
+ document.body.appendChild(frame);
+ await new Promise(resolve => frame.onload = resolve);
+
+ const inner = frame.contentWindow.frames[0];
+ const video = inner.document.body.childNodes[0];
+ video.muted = true; // to allow playback
+ return video.play();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/read-media/pageload-image-in-popup.html b/testing/web-platform/tests/html/browsers/browsing-the-web/read-media/pageload-image-in-popup.html
new file mode 100644
index 0000000000..e9284824f4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/read-media/pageload-image-in-popup.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media documents: image</title>
+ <link rel="author" title="Takayoshi Kochi" href="mailto:kochi@chromium.org">
+ <link rel="author" title="Michael Ventnor" href="mailto:mventnor@mozilla.com">
+ <link rel="author" title="Ms2ger" href="mailto:ms2ger@gmail.com">
+ <link rel="help" href="https://html.spec.whatwg.org/multipage/#read-media">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+
+<script>
+ var t = async_test("The document for a standalone media file should have one child in the body.");
+
+ var imgwin = window.open('/images/blue.png');
+ imgwin.onload = t.step_func_done(function() {
+ assert_equals(imgwin.opener, window);
+ assert_equals(imgwin.document.contentType, "image/png");
+ var imgwinChildren = imgwin.document.body.childNodes;
+ assert_equals(imgwinChildren.length, 1, "Body of image document has 1 child");
+ assert_equals(imgwinChildren[0].nodeName, "IMG", "Only child of body must be an <img> element");
+ assert_equals(imgwinChildren[0].namespaceURI, "http://www.w3.org/1999/xhtml",
+ "Only child of body must be an HTML element");
+ imgwin.close();
+ });
+</script>
+</head>
+<body>
+ <div id="log"></div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/read-media/pageload-image.html b/testing/web-platform/tests/html/browsers/browsing-the-web/read-media/pageload-image.html
new file mode 100644
index 0000000000..67c97bafdd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/read-media/pageload-image.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media documents: image</title>
+ <link rel="author" title="Michael Ventnor" href="mailto:mventnor@mozilla.com">
+ <link rel="author" title="Ms2ger" href="mailto:ms2ger@gmail.com">
+ <link rel="help" href="https://html.spec.whatwg.org/multipage/#read-media">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+
+<script>
+ var t = async_test("The document for a standalone media file should have one child in the body.");
+
+ function frameLoaded() {
+ var testframe = document.getElementById('testframe');
+ assert_equals(testframe.contentDocument.contentType, "image/png");
+ assert_equals(testframe.contentDocument.compatMode, "CSS1Compat", "Media documents should be in standards mode");
+ var testframeChildren = testframe.contentDocument.body.childNodes;
+ assert_equals(testframeChildren.length, 1, "Body of image document has 1 child");
+ assert_equals(testframeChildren[0].nodeName, "IMG", "Only child of body must be an <img> element");
+ assert_equals(testframeChildren[0].namespaceURI, "http://www.w3.org/1999/xhtml",
+ "Only child of body must be an HTML element");
+ t.done();
+ }
+</script>
+</head>
+<body>
+ <div id="log"></div>
+ <iframe id="testframe" onload="t.step(frameLoaded)" src="/images/blue.png"></iframe>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/read-media/pageload-video.html b/testing/web-platform/tests/html/browsers/browsing-the-web/read-media/pageload-video.html
new file mode 100644
index 0000000000..9bce1f0c36
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/read-media/pageload-video.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+<title>Media documents: video</title>
+<link rel="author" title="Michael Ventnor" href="mailto:mventnor@mozilla.com">
+<link rel="author" title="Ms2ger" href="mailto:ms2ger@gmail.com">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#read-media">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/media.js"></script>
+<div id="log"></div>
+<script>
+async_test(function() {
+ var testframe = document.createElement('iframe');
+ var url = getVideoURI("/media/A4");
+ var contentType = getMediaContentType(url);
+ testframe.onload = this.step_func_done(function() {
+ assert_equals(testframe.contentDocument.contentType, contentType);
+ assert_equals(testframe.contentDocument.compatMode, "CSS1Compat", "Media documents should be in standards mode");
+ var testframeChildren = testframe.contentDocument.body.childNodes;
+ assert_equals(testframeChildren.length, 1, "Body of image document has 1 child");
+ assert_equals(testframeChildren[0].nodeName, "VIDEO", "Only child of body must be an <video> element");
+ assert_equals(testframeChildren[0].namespaceURI, "http://www.w3.org/1999/xhtml",
+ "Only child of body must be an HTML element");
+ });
+ testframe.src = url;
+ document.body.appendChild(testframe);
+}, "The document for a standalone media file should have one child in the body.");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/read-media/resources/iframe-document.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/read-media/resources/iframe-document.sub.html
new file mode 100644
index 0000000000..69523efa7a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/read-media/resources/iframe-document.sub.html
@@ -0,0 +1,5 @@
+<!doctype html>
+<html>
+<iframe src="{{GET[childsrc]}}">
+</iframe>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/read-text/load-text-plain.html b/testing/web-platform/tests/html/browsers/browsing-the-web/read-text/load-text-plain.html
new file mode 100644
index 0000000000..ad75631533
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/read-text/load-text-plain.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Page load processing model for text files</title>
+<link rel="author" title="Ms2ger" href="ms2ger@gmail.com">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#read-text">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test("Checking document metadata for text file");
+var tD = async_test("Checking DOM for text file");
+var tC = async_test("Checking contents for text file");
+var iframe = document.body.appendChild(document.createElement("iframe"));
+iframe.onload = function(e) {
+ var doc = iframe.contentDocument;
+ t.step(function() {
+ assert_equals(doc.compatMode, "CSS1Compat");
+ assert_equals(doc.contentType, "text/plain");
+ assert_equals(doc.doctype, null);
+ t.done();
+ })
+ tD.step(function() {
+ assert_equals(doc.childNodes.length, 1, "Document should have 1 child")
+ assert_equals(doc.documentElement.tagName, "HTML");
+ assert_equals(doc.documentElement.childNodes.length, 2,
+ "Root element should have 2 children")
+ assert_equals(doc.documentElement.firstChild.tagName, "HEAD");
+ assert_equals(doc.documentElement.lastChild.tagName, "BODY");
+ assert_equals(doc.documentElement.lastChild.childNodes.length, 1,
+ "Body element should have 1 child")
+ assert_equals(doc.documentElement.lastChild.firstChild.tagName, "PRE");
+ tD.done();
+ })
+ tC.step(function() {
+ assert_equals(doc.documentElement.lastChild.firstChild.firstChild.data,
+ "This is a sample text/plain document.\n\nThis is not an HTML document.\n\n");
+ tC.done();
+ })
+};
+iframe.src = "../../../../common/text-plain.txt";
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addHTML.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addHTML.window.js
new file mode 100644
index 0000000000..0c093cc462
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addHTML.window.js
@@ -0,0 +1,20 @@
+// META: title=RemoteContextWrapper addHtml
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=./resources/test-helper.js
+
+'use strict';
+
+// This tests that arguments passed to the constructor are respected.
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+
+ const main = await rcHelper.addWindow();
+ await assertSimplestScriptRuns(main);
+
+ await main.addHTML('<div id=div-id>div-content</div>');
+ await assertFunctionRuns(
+ main, () => document.getElementById('div-id').textContent, 'div-content');
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addIframe-srcdoc-startOn.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addIframe-srcdoc-startOn.window.js
new file mode 100644
index 0000000000..2ef047c212
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addIframe-srcdoc-startOn.window.js
@@ -0,0 +1,24 @@
+// META: title=RemoteContextWrapper addIframe with srcdoc and startOn
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=./resources/test-helper.js
+
+'use strict';
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+
+ const main = await rcHelper.addWindow();
+
+ const iframe = await main.addIframeSrcdoc(
+ /*extraConfig=*/ {startOn: 'pageshow'});
+
+ await assertSimplestScriptRuns(iframe);
+ await assert_equals(
+ await iframe.executeScript(() => {
+ return executorStartEvent.type;
+ }),
+ 'pageshow');
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addIframe-srcdoc.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addIframe-srcdoc.window.js
new file mode 100644
index 0000000000..1a959cbaf0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addIframe-srcdoc.window.js
@@ -0,0 +1,27 @@
+// META: title=RemoteContextWrapper addIframe with srcdoc
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=./resources/test-helper.js
+
+'use strict';
+
+// This tests that arguments passed to the constructor are respected.
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+
+ const main = await rcHelper.addWindow();
+
+ const iframe = await main.addIframeSrcdoc(
+ /*extraConfig=*/ {scripts: ['./resources/test-script.js']},
+ /*attributes=*/ {id: 'test-id'},
+ );
+
+ await assertSimplestScriptRuns(iframe);
+ await assertFunctionRuns(iframe, () => testFunction(), 'testFunction exists');
+
+ assert_equals(
+ await main.executeScript(() => document.getElementById('test-id').id),
+ 'test-id', 'verify id');
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addIframe.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addIframe.window.js
new file mode 100644
index 0000000000..c1630e4680
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addIframe.window.js
@@ -0,0 +1,40 @@
+// META: title=RemoteContextWrapper addIframe
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=./resources/test-helper.js
+
+'use strict';
+
+// This tests that arguments passed to the constructor are respected.
+promise_test(async t => {
+ // Precondition: Test was loaded from the HTTP_ORIGIN.
+ assert_equals(
+ location.origin, get_host_info()['HTTP_ORIGIN'],
+ 'test window was loaded on HTTP_ORIGIN');
+
+ const rcHelper = new RemoteContextHelper();
+
+ const main = await rcHelper.addWindow();
+
+ const headerName = 'x-wpt-test-header';
+ const headerValue = 'test-escaping()';
+ const iframe = await main.addIframe(
+ /*extraConfig=*/ {
+ origin: 'HTTP_REMOTE_ORIGIN',
+ scripts: ['/common/get-host-info.sub.js', './resources/test-script.js'],
+ headers: [[headerName, headerValue]],
+ },
+ /*attributes=*/ {id: 'test-id'},
+ );
+
+ await assertSimplestScriptRuns(iframe);
+ await assertFunctionRuns(iframe, () => testFunction(), 'testFunction exists');
+ await assertOriginIsAsExpected(iframe, get_host_info()['HTTP_REMOTE_ORIGIN']);
+ await assertHeaderIsAsExpected(iframe, headerName, headerValue);
+
+ assert_equals(
+ await main.executeScript(() => document.getElementById('test-id').id),
+ 'test-id', 'verify id');
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addScripts.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addScripts.window.js
new file mode 100644
index 0000000000..01cf06c65d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addScripts.window.js
@@ -0,0 +1,19 @@
+// META: title=RemoteContextWrapper addScripts
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=./resources/test-helper.js
+
+'use strict';
+
+// This tests that arguments passed to the constructor are respected.
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+
+ const main = await rcHelper.addWindow();
+ await assertSimplestScriptRuns(main);
+
+ await main.addScripts(['./resources/test-script.js']);
+ await assertFunctionRuns(main, () => testFunction(), 'testFunction exists');
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-defaults.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-defaults.window.js
new file mode 100644
index 0000000000..34fa9cd39a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-defaults.window.js
@@ -0,0 +1,15 @@
+// META: title=RemoteContextHelper with defaults
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=./resources/test-helper.js
+
+'use strict';
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+ const main = await rcHelper.addWindow();
+ await assertSimplestScriptRuns(main);
+ await assertOriginIsAsExpected(main, location.origin);
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-extra-config.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-extra-config.window.js
new file mode 100644
index 0000000000..112d2d726e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-extra-config.window.js
@@ -0,0 +1,36 @@
+// META: title=RemoteContextHelper addWindow with extra config
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=./resources/test-helper.js
+
+'use strict';
+
+// This tests that arguments passed to the constructor are respected.
+promise_test(async t => {
+ const header1Name = 'x-wpt-test-header1';
+ const header1Value = 'test-escaping1()';
+ const rcHelper = new RemoteContextHelper({
+ origin: 'HTTP_REMOTE_ORIGIN',
+ scripts: ['/common/get-host-info.sub.js', './resources/test-script.js'],
+ headers: [[header1Name, header1Value]],
+ });
+
+ const header2Name = 'x-wpt-test-header2';
+ const header2Value = 'test-escaping2()';
+ const main = await rcHelper.addWindow(
+ {
+ origin: location.origin,
+ scripts: [new URL('./resources/test-script2.js', location).toString()],
+ headers: [[header2Name, header2Value]],
+ },
+ );
+
+ await assertSimplestScriptRuns(main);
+ await assertFunctionRuns(main, () => testFunction(), 'testFunction exists');
+ await assertFunctionRuns(main, () => testFunction2(), 'testFunction2 exists');
+ await assertOriginIsAsExpected(main, location.origin);
+ await assertHeaderIsAsExpected(main, header1Name, header1Value);
+ await assertHeaderIsAsExpected(main, header2Name, header2Value);
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-features.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-features.window.js
new file mode 100644
index 0000000000..329c55e626
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-features.window.js
@@ -0,0 +1,23 @@
+// META: title=RemoteContextHelper addWindow features
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=./resources/test-helper.js
+
+'use strict';
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+ {
+ const main = await rcHelper.addWindow();
+ await assertSimplestScriptRuns(main);
+ await assertWindowHasOpenerEquals(main, true);
+ }
+ {
+ const main = await rcHelper.addWindow(
+ /*extraConfig=*/ null, /*options=*/ {features: 'noopener'});
+ await assertSimplestScriptRuns(main);
+ await assertWindowHasOpenerEquals(main, false);
+ }
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-invalid-origin.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-invalid-origin.window.js
new file mode 100644
index 0000000000..58aee312cc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-invalid-origin.window.js
@@ -0,0 +1,21 @@
+// META: title=RemoteContextHelper addWindow with extra config
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=./resources/test-helper.js
+
+'use strict';
+
+// This tests that arguments passed to the constructor are respected.
+promise_test(async t => {
+ const header1Name = 'x-wpt-test-header1';
+ const header1Value = 'test-escaping1()';
+ const rcHelper = new RemoteContextHelper({
+ origin: 'INVALID',
+ });
+
+ promise_rejects_js(
+ t, RangeError, rcHelper.addWindow(),
+ 'Exception should be thrown for invalid origin.');
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-startOn.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-startOn.window.js
new file mode 100644
index 0000000000..0f57a87638
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-startOn.window.js
@@ -0,0 +1,19 @@
+// META: title=RemoteContextHelper addWindow target
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=./resources/test-helper.js
+
+'use strict';
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+ const main = await rcHelper.addWindow({startOn: 'pageshow'});
+ await assertSimplestScriptRuns(main);
+ await assert_equals(
+ await main.executeScript(() => {
+ return executorStartEvent.type;
+ }),
+ 'pageshow');
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-target.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-target.window.js
new file mode 100644
index 0000000000..3f742048b4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWindow-target.window.js
@@ -0,0 +1,17 @@
+// META: title=RemoteContextHelper addWindow target
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=./resources/test-helper.js
+
+'use strict';
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+ const name = 'a name';
+ const main = await rcHelper.addWindow(
+ /*extraConfig=*/ null, /*options=*/ {target: name});
+ await assertSimplestScriptRuns(main);
+ await assertWindowNameEquals(main, name);
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWorker.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWorker.window.js
new file mode 100644
index 0000000000..d4ba8b0312
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/addWorker.window.js
@@ -0,0 +1,29 @@
+// META: title=RemoteContextWrapper addWorker
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=./resources/test-helper.js
+
+'use strict';
+
+// This tests that arguments passed to the constructor are respected.
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+
+ const main = await rcHelper.addWindow();
+
+ const headerName = 'x-wpt-test-header';
+ const headerValue = 'test-escaping()';
+ const worker = await main.addWorker(
+ {
+ scripts: ['/common/get-host-info.sub.js', './resources/test-script.js'],
+ headers: [[headerName, headerValue]],
+ },
+ );
+
+ await assertSimplestScriptRuns(worker);
+ await assertFunctionRuns(worker, () => testFunction(), 'testFunction exists');
+ await assertOriginIsAsExpected(worker, location.origin);
+ await assertHeaderIsAsExpected(worker, headerName, headerValue);
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/constructor.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/constructor.window.js
new file mode 100644
index 0000000000..dcbb3dabbc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/constructor.window.js
@@ -0,0 +1,38 @@
+// META: title=RemoteContextHelper constructor
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=./resources/test-helper.js
+
+'use strict';
+
+// This tests that arguments passed to the constructor are respected.
+promise_test(async t => {
+ // Precondition: Test was loaded from the HTTP_ORIGIN.
+ assert_equals(
+ location.origin, get_host_info()['HTTP_ORIGIN'],
+ 'test window was loaded on HTTP_ORIGIN');
+
+ const headerName = 'x-wpt-test-header';
+ const headerValue = 'test-escaping()';
+ const rcHelper = new RemoteContextHelper({
+ origin: 'HTTP_REMOTE_ORIGIN',
+ scripts: [
+ '/common/get-host-info.sub.js',
+ './resources/test-script.js',
+ ],
+ headers: [[headerName, headerValue]],
+ });
+
+
+ const main = await rcHelper.addWindow();
+
+ await assertSimplestScriptRuns(main);
+ await assertFunctionRuns(main, () => testFunction(), 'testFunction exists');
+
+ // Verify that the origin is different.
+ await assertOriginIsAsExpected(main, get_host_info()['HTTP_REMOTE_ORIGIN']);
+
+ await assertHeaderIsAsExpected(main, headerName, headerValue);
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/createContext-bad-executorCreator.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/createContext-bad-executorCreator.window.js
new file mode 100644
index 0000000000..bf24581173
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/createContext-bad-executorCreator.window.js
@@ -0,0 +1,20 @@
+// META: title=RemoteContextHelper createContext with throwing/rejecting executorCreators.
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+
+'use strict';
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+
+ const err = new Error('something bad!');
+ promise_rejects_exactly(
+ t, err, rcHelper.createContext({ executorCreator() { throw err; } }),
+ 'Sync exception must be rethrown');
+
+ promise_rejects_exactly(
+ t, err, rcHelper.createContext({ executorCreator() { return Promise.reject(err); } }),
+ 'Async rejection must be rethrown');
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/getRequestHeaders.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/getRequestHeaders.window.js
new file mode 100644
index 0000000000..90610e27fb
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/getRequestHeaders.window.js
@@ -0,0 +1,18 @@
+// META: title=RemoteContextHelper with defaults
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=./resources/test-helper.js
+
+'use strict';
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+ const main = await rcHelper.addWindow();
+
+ const headers = await main.getRequestHeaders();
+
+ assert_equals(headers.constructor, Headers);
+ assert_true(headers.has("user-agent"), "user-agent is present");
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/navigateToNew.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/navigateToNew.window.js
new file mode 100644
index 0000000000..f7dd3f8325
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/navigateToNew.window.js
@@ -0,0 +1,37 @@
+// META: title=RemoteContextWrapper navigateToNew
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=./resources/test-helper.js
+
+'use strict';
+
+promise_test(async t => {
+ // Precondition: Test was loaded from the HTTP_ORIGIN.
+ assert_equals(
+ location.origin, get_host_info()['HTTP_ORIGIN'],
+ 'test window was loaded on HTTP_ORIGIN');
+
+ const rcHelper = new RemoteContextHelper();
+
+ const main = await rcHelper.addWindow();
+
+ const headerName = 'x-wpt-test-header';
+ const headerValue = 'test-escaping()';
+ const newMain = await main.navigateToNew(
+ {
+ origin: 'HTTP_REMOTE_ORIGIN',
+ scripts: ['/common/get-host-info.sub.js', './resources/test-script.js'],
+ headers: [[headerName, headerValue]],
+ },
+ );
+
+ await assertSimplestScriptRuns(newMain);
+ await assertFunctionRuns(
+ newMain, () => testFunction(), 'testFunction exists');
+
+ const remoteOrigin = get_host_info()['HTTP_REMOTE_ORIGIN'];
+ await assertOriginIsAsExpected(newMain, remoteOrigin);
+ await assertHeaderIsAsExpected(newMain, headerName, headerValue);
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/navigation-bfcache.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/navigation-bfcache.window.js
new file mode 100644
index 0000000000..1fa90a9064
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/navigation-bfcache.window.js
@@ -0,0 +1,35 @@
+// META: title=RemoteContextHelper navigation using BFCache
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=./resources/test-helper.js
+
+'use strict';
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+
+ // Open a window with noopener so that BFCache will work.
+ const rc1 = await rcHelper.addWindow(
+ /*config=*/ null, /*options=*/ {features: 'noopener'});
+
+ // Add a pageshow listener to stash the event.
+ await rc1.executeScript(() => {
+ window.addEventListener('pageshow', (event) => {
+ window.pageshowEvent = event;
+ });
+ });
+
+ // Navigate away.
+ const rc2 = await rc1.navigateToNew();
+ await assertSimplestScriptRuns(rc2);
+
+ // Navigate back.
+ await rc2.historyBack();
+
+ // Verify that the document was BFCached.
+ assert_true(await rc1.executeScript(() => {
+ return window.pageshowEvent.persisted;
+ }));
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/navigation-helpers.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/navigation-helpers.window.js
new file mode 100644
index 0000000000..079e20661e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/navigation-helpers.window.js
@@ -0,0 +1,28 @@
+// META: title=RemoteContextHelper navigation helpers
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=./resources/test-helper.js
+// META: timeout=long
+
+'use strict';
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+ const rc1 = await rcHelper.addWindow();
+ await assertSimplestScriptRuns(rc1);
+
+ const rc2 = await rc1.navigateToNew();
+ await assertSimplestScriptRuns(rc2);
+
+ await rc2.historyBack();
+ await assertSimplestScriptRuns(rc1);
+
+ await rc1.historyForward();
+ await assertSimplestScriptRuns(rc2);
+
+ const rc3 = await rc2.navigateToNew();
+ await rc3.historyGo(-2);
+ await assertSimplestScriptRuns(rc1);
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/navigation-same-document.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/navigation-same-document.window.js
new file mode 100644
index 0000000000..6f637e2b90
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/navigation-same-document.window.js
@@ -0,0 +1,39 @@
+// META: title=RemoteContextHelper navigation using BFCache
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=./resources/test-helper.js
+
+'use strict';
+
+async function assertLocationIs(remoteContextWrapper, expectedLocation) {
+ assert_equals(
+ await remoteContextWrapper.executeScript(() => {
+ return location.toString();
+ }),
+ expectedLocation, 'verify location');
+}
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+
+ const rc = await rcHelper.addWindow();
+
+ const oldLocation = await rc.executeScript(() => {
+ return location.toString();
+ });
+ const newLocation = oldLocation + '#fragment';
+
+ // Navigate to same document.
+ await rc.navigateTo(newLocation);
+
+ // Verify that the window navigated.
+ await assertLocationIs(rc, newLocation);
+
+ // Navigate back.
+ await rc.historyBack(oldLocation);
+
+ // Verify that the window navigated back and the executor is running.
+ await assertLocationIs(rc, oldLocation);
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/resources/test-helper.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/resources/test-helper.js
new file mode 100644
index 0000000000..71cd87e553
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/resources/test-helper.js
@@ -0,0 +1,48 @@
+async function assertSimplestScriptRuns(remoteContextWrapper) {
+ assert_equals(
+ await remoteContextWrapper.executeScript(() => {
+ return 1;
+ }),
+ 1, 'simplest script runs');
+}
+
+async function assertFunctionRuns(
+ remoteContextWrapper, functionToRun, expectedReturn) {
+ assert_equals(
+ await remoteContextWrapper.executeScript(functionToRun), expectedReturn,
+ 'function runs');
+}
+
+async function assertOriginIsAsExpected(remoteContextWrapper, expectedOrigin) {
+ assert_equals(
+ await remoteContextWrapper.executeScript(() => {
+ return location.origin;
+ }),
+ expectedOrigin, 'verify origin');
+}
+
+async function assertWindowNameEquals(remoteContextWrapper, expectedName) {
+ assert_equals(
+ await remoteContextWrapper.executeScript(() => {
+ return window.name;
+ }),
+ expectedName, 'verify name');
+}
+
+async function assertWindowHasOpenerEquals(remoteContextWrapper, hasParent) {
+ assert_equals(
+ await remoteContextWrapper.executeScript(() => {
+ return !!window.opener;
+ }),
+ hasParent, 'verify opener');
+}
+
+async function assertHeaderIsAsExpected(
+ remoteContextWrapper, headerName, headerValue) {
+ assert_equals(
+ headerValue,
+ await remoteContextWrapper.executeScript(async (headerName) => {
+ const res = await fetch(location);
+ return res.headers.get(headerName);
+ }, [headerName]), 'header is set');
+} \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/resources/test-script.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/resources/test-script.js
new file mode 100644
index 0000000000..d1c02cab29
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/resources/test-script.js
@@ -0,0 +1,3 @@
+function testFunction() {
+ return 'testFunction exists';
+}
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/resources/test-script2.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/resources/test-script2.js
new file mode 100644
index 0000000000..f9e72c442b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper-tests/resources/test-script2.js
@@ -0,0 +1,3 @@
+function testFunction2() {
+ return 'testFunction2 exists';
+}
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/executor-common.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/executor-common.js
new file mode 100644
index 0000000000..ce9b5f9116
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/executor-common.js
@@ -0,0 +1,14 @@
+// Functions available by default in the executor.
+
+'use strict';
+
+let executor;
+
+// Expects addScript to be present (window or worker version).
+function addScripts(urls) {
+ return Promise.all(urls.map(addScript));
+}
+
+function startExecutor(uuid) {
+ executor = new Executor(uuid);
+}
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/executor-window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/executor-window.js
new file mode 100644
index 0000000000..54df6735c9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/executor-window.js
@@ -0,0 +1,67 @@
+// Functions available by default in the executor.
+
+'use strict';
+
+let executorStartEvent = null;
+
+function requestExecutor(uuid, startOn) {
+ if (startOn) {
+ addEventListener(startOn, (e) => {
+ executorStartEvent = e;
+ startExecutor(uuid);
+ });
+ } else {
+ startExecutor(uuid);
+ }
+}
+
+function addScript(url) {
+ const script = document.createElement('script');
+ script.src = url;
+ const promise = new Promise((resolve, reject) => {
+ script.onload = () => resolve(url);
+ script.onerror = (e) => reject(e);
+ });
+ document.body.appendChild(script);
+ return promise;
+}
+
+/**
+ * Suspends the executor and executes the function in `fnString` when it has
+ * suspended. Installs a pageshow handler to resume the executor if the
+ * document is BFCached. Installs a hashchange handler to detect when the
+ * navigation did not change documents.
+ *
+ * This returns nothing because fn is invoke after waiting for the document to
+ * be suspended. If we were to return a promise, the executor could not suspend
+ * until that promise resolved but the promise cannot resolve until the executor
+ * is suspended. This could be avoided by adding support
+ * directly in the dispatcher for tasks suspend immediately after execution.
+ *
+ * @param {string} fnString A stringified function to be executed.
+ * @param {any[]} args The arguments to pass to the function.
+ */
+function executeScriptToNavigate(fnString, args) {
+ // Only one of these listeners should run.
+ const controller = new AbortController();
+ window.addEventListener('pageshow', (event) => {
+ controller.abort();
+ executor.resume();
+ }, {signal: controller.signal, once: true});
+ window.addEventListener('hashchange', (event) => {
+ controller.abort();
+ const oldURLObject = new URL(event.oldURL);
+ const newURLObject = new URL(event.newURL);
+ oldURLObject.hash = '';
+ newURLObject.hash = '';
+ // If only the hash-fragment changed then the navigation was
+ // same-document and we should resume the executor.
+ if (oldURLObject.toString() == newURLObject.toString()) {
+ executor.resume();
+ }
+ }, {signal: controller.signal, once: true});
+
+ executor.suspend(() => {
+ eval(fnString).apply(null, args);
+ });
+}
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/executor-window.py b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/executor-window.py
new file mode 100644
index 0000000000..1a660100b9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/executor-window.py
@@ -0,0 +1,40 @@
+import html
+import json
+from urllib import parse
+
+def main(request, response):
+ initRequestHeaders = ""
+ for header_name in request.headers.keys():
+ for header_value in request.headers.get_list(header_name):
+ js_name = json.dumps(header_name.lower().decode("utf-8"))
+ js_value = json.dumps(header_value.decode("utf-8"))
+ initRequestHeaders += f"window.__requestHeaders.append({js_name}, {js_value});\n"
+ if (b"status" in request.GET):
+ status = int(request.GET.first(b"status"))
+ else:
+ status = 200
+ query = parse.parse_qs(request.url_parts.query)
+ scripts = []
+ for script in query.get("script", []):
+ scripts.append(f"<script src='{html.escape(script)}'></script>")
+ scripts_s = "\n".join(scripts)
+
+ uuid = query.get("uuid")[0]
+
+ start_on = query.get("startOn")
+ start_on_s = f"'{start_on[0]}'" if start_on else "null"
+
+ return (status, [("Content-Type", "text/html")], f"""
+<!DOCTYPE HTML>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="./executor-common.js"></script>
+<script src="./executor-window.js"></script>
+
+{scripts_s}
+<body>
+<script>
+window.__requestHeaders = new Headers();
+{initRequestHeaders}
+requestExecutor("{uuid}", {start_on_s});
+</script>
+""")
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/executor-worker.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/executor-worker.js
new file mode 100644
index 0000000000..0ca136191e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/executor-worker.js
@@ -0,0 +1,12 @@
+'use strict';
+
+importScripts('/common/dispatcher/dispatcher.js', './executor-common.js');
+
+function addScript(url) {
+ importScripts(url);
+}
+
+const params = new URLSearchParams(location.search);
+addScripts(params.getAll('script'));
+
+startExecutor(params.get('uuid'));
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
new file mode 100644
index 0000000000..aa24b36e8b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
@@ -0,0 +1,592 @@
+'use strict';
+
+// Requires:
+// - /common/dispatcher/dispatcher.js
+// - /common/utils.js
+// - /common/get-host-info.sub.js if automagic conversion of origin names to
+// URLs is used.
+
+/**
+ * This provides a more friendly interface to remote contexts in dispatches.js.
+ * The goal is to make it easy to write multi-window/-frame/-worker tests where
+ * the logic is entirely in 1 test file and there is no need to check in any
+ * other file (although it is often helpful to check in files of JS helper
+ * functions that are shared across remote context).
+ *
+ * So for example, to test that history traversal works, we create a new window,
+ * navigate it to a new document, go back and then go forward.
+ *
+ * @example
+ * promise_test(async t => {
+ * const rcHelper = new RemoteContextHelper();
+ * const rc1 = await rcHelper.addWindow();
+ * const rc2 = await rc1.navigateToNew();
+ * assert_equals(await rc2.executeScript(() => 'here'), 'here', 'rc2 is live');
+ * rc2.historyBack();
+ * assert_equals(await rc1.executeScript(() => 'here'), 'here', 'rc1 is live');
+ * rc1.historyForward();
+ * assert_equals(await rc2.executeScript(() => 'here'), 'here', 'rc2 is live');
+ * });
+ *
+ * Note on the correspondence between remote contexts and
+ * `RemoteContextWrapper`s. A remote context is entirely determined by its URL.
+ * So navigating away from one and then back again will result in a remote
+ * context that can be controlled by the same `RemoteContextWrapper` instance
+ * before and after navigation. Messages sent to a remote context while it is
+ * destroyed or in BFCache will be queued and processed if that that URL is
+ * navigated back to.
+ *
+ * Navigation:
+ * This framework does not keep track of the history of the frame tree and so it
+ * is up to the test script to keep track of what remote contexts are currently
+ * active and to keep references to the corresponding `RemoteContextWrapper`s.
+ *
+ * Any action that leads to navigation in the remote context must be executed
+ * using
+ * @see RemoteContextWrapper.navigate.
+ */
+
+{
+ const RESOURCES_PATH =
+ '/html/browsers/browsing-the-web/remote-context-helper/resources';
+ const WINDOW_EXECUTOR_PATH = `${RESOURCES_PATH}/executor-window.py`;
+ const WORKER_EXECUTOR_PATH = `${RESOURCES_PATH}/executor-worker.js`;
+
+ /**
+ * Turns a string into an origin. If `origin` is null this will return the
+ * current document's origin. If `origin` contains not '/', this will attempt
+ * to use it as an index in `get_host_info()`. Otherwise returns the input
+ * origin.
+ * @private
+ * @param {string|null} origin The input origin.
+ * @return {string|null} The output origin.
+ * @throws {RangeError} is `origin` cannot be found in
+ * `get_host_info()`.
+ */
+ function finalizeOrigin(origin) {
+ if (!origin) {
+ return location.origin;
+ }
+ if (!origin.includes('/')) {
+ const origins = get_host_info();
+ if (origin in origins) {
+ return origins[origin];
+ } else {
+ throw new RangeError(
+ `${origin} is not a key in the get_host_info() object`);
+ }
+ }
+ return origin;
+ }
+
+ /**
+ * @private
+ * @param {string} url
+ * @returns {string} Absolute url using `location` as the base.
+ */
+ function makeAbsolute(url) {
+ return new URL(url, location).toString();
+ }
+
+ /**
+ * Represents a configuration for a remote context executor.
+ */
+ class RemoteContextConfig {
+ /**
+ * @param {Object} [options]
+ * @param {string} [options.origin] A URL or a key in `get_host_info()`.
+ * @see finalizeOrigin for how origins are handled.
+ * @param {string[]} [options.scripts] A list of script URLs. The current
+ * document will be used as the base for relative URLs.
+ * @param {[string, string][]} [options.headers] A list of pairs of name
+ * and value. The executor will be served with these headers set.
+ * @param {string} [options.startOn] If supplied, the executor will start
+ * when this event occurs, e.g. "pageshow",
+ * (@see window.addEventListener). This only makes sense for
+ * window-based executors, not worker-based.
+ * @param {string} [options.status] If supplied, the executor will pass
+ * this value in the "status" parameter to the executor. The default
+ * executor will default to a status code of 200, if the parameter is
+ * not supplied.
+ */
+ constructor(
+ {origin, scripts = [], headers = [], startOn, status} = {}) {
+ this.origin = origin;
+ this.scripts = scripts;
+ this.headers = headers;
+ this.startOn = startOn;
+ this.status = status;
+ }
+
+ /**
+ * If `config` is not already a `RemoteContextConfig`, one is constructed
+ * using `config`.
+ * @private
+ * @param {object} [config]
+ * @returns
+ */
+ static ensure(config) {
+ if (!config) {
+ return DEFAULT_CONTEXT_CONFIG;
+ }
+ return new RemoteContextConfig(config);
+ }
+
+ /**
+ * Merges `this` with another `RemoteContextConfig` and to give a new
+ * `RemoteContextConfig`. `origin` is replaced by the other if present,
+ * `headers` and `scripts` are concatenated with `this`'s coming first.
+ * @param {RemoteContextConfig} extraConfig
+ * @returns {RemoteContextConfig}
+ */
+ merged(extraConfig) {
+ let origin = this.origin;
+ if (extraConfig.origin) {
+ origin = extraConfig.origin;
+ }
+ let startOn = this.startOn;
+ if (extraConfig.startOn) {
+ startOn = extraConfig.startOn;
+ }
+ let status = this.status;
+ if (extraConfig.status) {
+ status = extraConfig.status;
+ }
+ const headers = this.headers.concat(extraConfig.headers);
+ const scripts = this.scripts.concat(extraConfig.scripts);
+ return new RemoteContextConfig({
+ origin,
+ headers,
+ scripts,
+ startOn,
+ status
+ });
+ }
+ }
+
+ /**
+ * The default `RemoteContextConfig` to use if none is supplied. It has no
+ * origin, headers or scripts.
+ * @constant {RemoteContextConfig}
+ */
+ const DEFAULT_CONTEXT_CONFIG = new RemoteContextConfig();
+
+ /**
+ * This class represents a configuration for creating remote contexts. This is
+ * the entry-point
+ * for creating remote contexts, providing @see addWindow .
+ */
+ class RemoteContextHelper {
+ /**
+ * @param {RemoteContextConfig|object} config The configuration
+ * for this remote context.
+ */
+ constructor(config) {
+ this.config = RemoteContextConfig.ensure(config);
+ }
+
+ /**
+ * Creates a new remote context and returns a `RemoteContextWrapper` giving
+ * access to it.
+ * @private
+ * @param {Object} options
+ * @param {(url: string) => Promise<undefined>} [options.executorCreator] A
+ * function that takes a URL and causes the browser to navigate some
+ * window to that URL, e.g. via an iframe or a new window. If this is
+ * not supplied, then the returned RemoteContextWrapper won't actually
+ * be communicating with something yet, and something will need to
+ * navigate to it using its `url` property, before communication can be
+ * established.
+ * @param {RemoteContextConfig|object} [options.extraConfig] If supplied,
+ * extra configuration for this remote context to be merged with
+ * `this`'s existing config. If it's not a `RemoteContextConfig`, it
+ * will be used to construct a new one.
+ * @returns {Promise<RemoteContextWrapper>}
+ */
+ async createContext({
+ executorCreator,
+ extraConfig,
+ isWorker = false,
+ }) {
+ const config =
+ this.config.merged(RemoteContextConfig.ensure(extraConfig));
+
+ const origin = finalizeOrigin(config.origin);
+ const url = new URL(
+ isWorker ? WORKER_EXECUTOR_PATH : WINDOW_EXECUTOR_PATH, origin);
+
+ // UUID is needed for executor.
+ const uuid = token();
+ url.searchParams.append('uuid', uuid);
+
+ if (config.headers) {
+ addHeaders(url, config.headers);
+ }
+ for (const script of config.scripts) {
+ url.searchParams.append('script', makeAbsolute(script));
+ }
+
+ if (config.startOn) {
+ url.searchParams.append('startOn', config.startOn);
+ }
+
+ if (config.status) {
+ url.searchParams.append('status', config.status);
+ }
+
+ if (executorCreator) {
+ await executorCreator(url.href);
+ }
+
+ return new RemoteContextWrapper(new RemoteContext(uuid), this, url.href);
+ }
+
+ /**
+ * Creates a window with a remote context. @see createContext for
+ * @param {RemoteContextConfig|object} [extraConfig] Will be
+ * merged with `this`'s config.
+ * @param {Object} [options]
+ * @param {string} [options.target] Passed to `window.open` as the
+ * 2nd argument
+ * @param {string} [options.features] Passed to `window.open` as the
+ * 3rd argument
+ * @returns {Promise<RemoteContextWrapper>}
+ */
+ addWindow(extraConfig, options) {
+ return this.createContext({
+ executorCreator: windowExecutorCreator(options),
+ extraConfig,
+ });
+ }
+ }
+ // Export this class.
+ self.RemoteContextHelper = RemoteContextHelper;
+
+ /**
+ * Attaches header to the URL. See
+ * https://web-platform-tests.org/writing-tests/server-pipes.html#headers
+ * @param {string} url the URL to which headers should be attached.
+ * @param {[[string, string]]} headers a list of pairs of head-name,
+ * header-value.
+ */
+ function addHeaders(url, headers) {
+ function escape(s) {
+ return s.replace('(', '\\(').replace(')', '\\)');
+ }
+ const formattedHeaders = headers.map((header) => {
+ return `header(${escape(header[0])}, ${escape(header[1])})`;
+ });
+ url.searchParams.append('pipe', formattedHeaders.join('|'));
+ }
+
+ function windowExecutorCreator({target = '_blank', features} = {}) {
+ return url => {
+ window.open(url, target, features);
+ };
+ }
+
+ function elementExecutorCreator(
+ remoteContextWrapper, elementName, attributes) {
+ return url => {
+ return remoteContextWrapper.executeScript((url, elementName, attributes) => {
+ const el = document.createElement(elementName);
+ for (const attribute in attributes) {
+ el.setAttribute(attribute, attributes[attribute]);
+ }
+ el.src = url;
+ document.body.appendChild(el);
+ }, [url, elementName, attributes]);
+ };
+ }
+
+ function iframeSrcdocExecutorCreator(remoteContextWrapper, attributes) {
+ return async (url) => {
+ // `url` points to the content needed to run an `Executor` in the frame.
+ // So we download the content and pass it via the `srcdoc` attribute,
+ // setting the iframe's `src` to `undefined`.
+ attributes['srcdoc'] = await fetch(url).then(r => r.text());
+ elementExecutorCreator(
+ remoteContextWrapper, 'iframe', attributes)(undefined);
+ };
+ }
+
+ function workerExecutorCreator() {
+ return url => {
+ new Worker(url);
+ };
+ }
+
+ function navigateExecutorCreator(remoteContextWrapper) {
+ return url => {
+ return remoteContextWrapper.navigate((url) => {
+ window.location = url;
+ }, [url]);
+ };
+ }
+
+ /**
+ * This class represents a remote context running an executor (a
+ * window/frame/worker that can receive commands). It is the interface for
+ * scripts to control remote contexts.
+ *
+ * Instances are returned when new remote contexts are created (e.g.
+ * `addFrame` or `navigateToNew`).
+ */
+ class RemoteContextWrapper {
+ /**
+ * This should only be constructed by `RemoteContextHelper`.
+ * @private
+ */
+ constructor(context, helper, url) {
+ this.context = context;
+ this.helper = helper;
+ this.url = url;
+ }
+
+ /**
+ * Executes a script in the remote context.
+ * @param {function} fn The script to execute.
+ * @param {any[]} args An array of arguments to pass to the script.
+ * @returns {Promise<any>} The return value of the script (after
+ * being serialized and deserialized).
+ */
+ async executeScript(fn, args) {
+ return this.context.execute_script(fn, args);
+ }
+
+ /**
+ * Adds a string of HTML to the executor's document.
+ * @param {string} html
+ * @returns {Promise<undefined>}
+ */
+ async addHTML(html) {
+ return this.executeScript((htmlSource) => {
+ document.body.insertAdjacentHTML('beforebegin', htmlSource);
+ }, [html]);
+ }
+
+ /**
+ * Adds scripts to the executor's document.
+ * @param {string[]} urls A list of URLs. URLs are relative to the current
+ * document.
+ * @returns {Promise<undefined>}
+ */
+ async addScripts(urls) {
+ if (!urls) {
+ return [];
+ }
+ return this.executeScript(urls => {
+ return addScripts(urls);
+ }, [urls.map(makeAbsolute)]);
+ }
+
+ /**
+ * Adds an iframe with `src` attribute to the current document.
+ * @param {RemoteContextConfig} [extraConfig]
+ * @param {[string, string][]} [attributes] A list of pairs of strings
+ * of attribute name and value these will be set on the iframe element
+ * when added to the document.
+ * @returns {Promise<RemoteContextWrapper>} The remote context.
+ */
+ addIframe(extraConfig, attributes = {}) {
+ return this.helper.createContext({
+ executorCreator: elementExecutorCreator(this, 'iframe', attributes),
+ extraConfig,
+ });
+ }
+
+ /**
+ * Adds an iframe with `srcdoc` attribute to the current document
+ * @param {RemoteContextConfig} [extraConfig]
+ * @param {[string, string][]} [attributes] A list of pairs of strings
+ * of attribute name and value these will be set on the iframe element
+ * when added to the document.
+ * @returns {Promise<RemoteContextWrapper>} The remote context.
+ */
+ addIframeSrcdoc(extraConfig, attributes = {}) {
+ return this.helper.createContext({
+ executorCreator: iframeSrcdocExecutorCreator(this, attributes),
+ extraConfig,
+ });
+ }
+
+ /**
+ * Adds a dedicated worker to the current document.
+ * @param {RemoteContextConfig} [extraConfig]
+ * @returns {Promise<RemoteContextWrapper>} The remote context.
+ */
+ addWorker(extraConfig) {
+ return this.helper.createContext({
+ executorCreator: workerExecutorCreator(),
+ extraConfig,
+ isWorker: true,
+ });
+ }
+
+ /**
+ * Gets a `Headers` object containing the request headers that were used
+ * when the browser requested this document.
+ *
+ * Currently, this only works for `RemoteContextHelper`s representing
+ * windows, not workers.
+ * @returns {Promise<Headers>}
+ */
+ async getRequestHeaders() {
+ // This only works in window environments for now. We could make it work
+ // for workers too; if you have a need, just share or duplicate the code
+ // that's in executor-window.py. Anyway, we explicitly use `window` in
+ // the script so that we get a clear error if you try using it on a
+ // worker.
+
+ // We need to serialize and deserialize the `Headers` object manually.
+ const asNestedArrays = await this.executeScript(() => [...window.__requestHeaders]);
+ return new Headers(asNestedArrays);
+ }
+
+ /**
+ * Executes a script in the remote context that will perform a navigation.
+ * To do this safely, we must suspend the executor and wait for that to
+ * complete before executing. This ensures that all outstanding requests are
+ * completed and no more can start. It also ensures that the executor will
+ * restart if the page goes into BFCache or it was a same-document
+ * navigation. It does not return a value.
+ *
+ * NOTE: We cannot monitor whether and what navigations are happening. The
+ * logic has been made as robust as possible but is not fool-proof.
+ *
+ * Foolproof rule:
+ * - The script must perform exactly one navigation.
+ * - If that navigation is a same-document history traversal, you must
+ * `await` the result of `waitUntilLocationIs`. (Same-document non-traversal
+ * navigations do not need this extra step.)
+ *
+ * More complex rules:
+ * - The script must perform a navigation. If it performs no navigation,
+ * the remote context will be left in the suspended state.
+ * - If the script performs a direct same-document navigation, it is not
+ * necessary to use this function but it will work as long as it is the only
+ * navigation performed.
+ * - If the script performs a same-document history navigation, you must
+ * `await` the result of `waitUntilLocationIs`.
+ *
+ * @param {function} fn The script to execute.
+ * @param {any[]} args An array of arguments to pass to the script.
+ * @returns {Promise<undefined>}
+ */
+ navigate(fn, args) {
+ return this.executeScript((fnText, args) => {
+ executeScriptToNavigate(fnText, args);
+ }, [fn.toString(), args]);
+ }
+
+ /**
+ * Navigates to the given URL, by executing a script in the remote
+ * context that will perform navigation with the `location.href`
+ * setter.
+ *
+ * Be aware that performing a cross-document navigation using this
+ * method will cause this `RemoteContextWrapper` to become dormant,
+ * since the remote context it points to is no longer active and
+ * able to receive messages. You also won't be able to reliably
+ * tell when the navigation finishes; the returned promise will
+ * fulfill when the script finishes running, not when the navigation
+ * is done. As such, this is most useful for testing things like
+ * unload behavior (where it doesn't matter) or prerendering (where
+ * there is already a `RemoteContextWrapper` for the destination).
+ * For other cases, using `navigateToNew()` will likely be better.
+ *
+ * @param {string|URL} url The URL to navigate to.
+ * @returns {Promise<undefined>}
+ */
+ navigateTo(url) {
+ return this.navigate(url => {
+ location.href = url;
+ }, [url.toString()]);
+ }
+
+ /**
+ * Navigates the context to a new document running an executor.
+ * @param {RemoteContextConfig} [extraConfig]
+ * @returns {Promise<RemoteContextWrapper>} The remote context.
+ */
+ async navigateToNew(extraConfig) {
+ return this.helper.createContext({
+ executorCreator: navigateExecutorCreator(this),
+ extraConfig,
+ });
+ }
+
+ //////////////////////////////////////
+ // Navigation Helpers.
+ //
+ // It is up to the test script to know which remote context will be
+ // navigated to and which `RemoteContextWrapper` should be used after
+ // navigation.
+ //
+ // NOTE: For a same-document history navigation, the caller use `await` a
+ // call to `waitUntilLocationIs` in order to know that the navigation has
+ // completed. For convenience the method below can return the promise to
+ // wait on, if passed the expected location.
+
+ async waitUntilLocationIs(expectedLocation) {
+ return this.executeScript(async (expectedLocation) => {
+ if (location.href === expectedLocation) {
+ return;
+ }
+
+ // Wait until the location updates to the expected one.
+ await new Promise(resolve => {
+ const listener = addEventListener('hashchange', (event) => {
+ if (event.newURL === expectedLocation) {
+ removeEventListener(listener);
+ resolve();
+ }
+ });
+ });
+ }, [expectedLocation]);
+ }
+
+ /**
+ * Performs a history traversal.
+ * @param {integer} n How many steps to traverse. @see history.go
+ * @param {string} [expectedLocation] If supplied will be passed to @see waitUntilLocationIs.
+ * @returns {Promise<undefined>}
+ */
+ async historyGo(n, expectedLocation) {
+ await this.navigate((n) => {
+ history.go(n);
+ }, [n]);
+ if (expectedLocation) {
+ await this.waitUntilLocationIs(expectedLocation);
+ }
+ }
+
+ /**
+ * Performs a history traversal back.
+ * @param {string} [expectedLocation] If supplied will be passed to @see waitUntilLocationIs.
+ * @returns {Promise<undefined>}
+ */
+ async historyBack(expectedLocation) {
+ await this.navigate(() => {
+ history.back();
+ });
+ if (expectedLocation) {
+ await this.waitUntilLocationIs(expectedLocation);
+ }
+ }
+
+ /**
+ * Performs a history traversal back.
+ * @param {string} [expectedLocation] If supplied will be passed to @see waitUntilLocationIs.
+ * @returns {Promise<undefined>}
+ */
+ async historyForward(expectedLocation) {
+ await this.navigate(() => {
+ history.forward();
+ });
+ if (expectedLocation) {
+ await this.waitUntilLocationIs(expectedLocation);
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/resources/has-iframe.html b/testing/web-platform/tests/html/browsers/browsing-the-web/resources/has-iframe.html
new file mode 100644
index 0000000000..e6dfba1011
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/resources/has-iframe.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<iframe src="/common/blank.html"></iframe>
+<script>
+window.onload = () => {
+ window.opener.postMessage("top ready", "*");
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/resources/helpers.js b/testing/web-platform/tests/html/browsers/browsing-the-web/resources/helpers.js
new file mode 100644
index 0000000000..b19addfadf
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/resources/helpers.js
@@ -0,0 +1,84 @@
+// This file contains general helpers for navigation/history tests. The goal is
+// to make tests more imperative and ordered, instead of requiring lots of
+// nested callbacks and jumping back and forth. However,
+// html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// might be even better at that, so prefer that when you can.
+//
+// TODO(domenic): consider unifying with
+// overlapping-navigations-and-traversals/resources/helpers.mjs.
+
+window.openWindow = (url, t) => {
+ const w = window.open(url);
+ t?.add_cleanup(() => w.close());
+
+ return new Promise(resolve => {
+ w.addEventListener("load", () => resolve(w), { once: true });
+ });
+};
+
+window.addIframe = (url = "/common/blank.html", doc = document) => {
+ const iframe = doc.createElement("iframe");
+ iframe.src = url;
+ doc.body.append(iframe);
+
+ return new Promise(resolve => {
+ iframe.addEventListener("load", () => resolve(iframe), { once: true });
+ });
+};
+
+window.addSrcdocIframe = async () => {
+ const iframe = document.createElement("iframe");
+ iframe.srcdoc = `<script>window.parent.postMessage("srcdoc ready", "*")</scr` + `ipt>`;
+ document.body.append(iframe);
+
+ assert_equals(await waitForMessage(iframe.contentWindow), "srcdoc ready");
+
+ return iframe;
+};
+
+window.waitToAvoidReplace = t => {
+ return new Promise(resolve => t.step_timeout(resolve, 0));
+};
+
+window.waitForIframeLoad = iframe => {
+ return new Promise(resolve => {
+ iframe.addEventListener("load", () => resolve(), { once: true });
+ });
+};
+
+window.waitForMessage = expectedSource => {
+ return new Promise(resolve => {
+ window.addEventListener("message", ({ source, data }) => {
+ if (source === expectedSource) {
+ resolve(data);
+ }
+ });
+ });
+};
+
+window.waitForHashchange = w => {
+ return new Promise(resolve => {
+ w.addEventListener("hashchange", () => resolve(), { once: true });
+ });
+};
+
+window.srcdocThatPostsParentOpener = text => {
+ return `
+ <p>${text}</p>
+ <script>
+ window.onload = () => {
+ window.top.opener.postMessage('ready', '*');
+ };
+ <\/script>
+ `;
+};
+
+window.failOnMessage = expectedSource => {
+ return new Promise((_, reject) => {
+ window.addEventListener("message", ({ source, data }) => {
+ if (source === expectedSource) {
+ reject(new Error(`Received message "${data}" but expected to receive no message`));
+ }
+ });
+ });
+};
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/resources/post-top-opener-on-load.html b/testing/web-platform/tests/html/browsers/browsing-the-web/resources/post-top-opener-on-load.html
new file mode 100644
index 0000000000..47cb6c6150
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/resources/post-top-opener-on-load.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<script>
+window.onload = () => {
+ window.top.opener.postMessage("ready", "*");
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/001.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/001.html
new file mode 100644
index 0000000000..32599bbc50
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/001.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<!-- this tests the spec as it hopefully will be once bug https://www.w3.org/Bugs/Public/show_bug.cgi?id=17155 is fixed -->
+<title>Fragment Navigation: Updating document address</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(function() {
+ assert_equals(location.hash, "", "Page must be loaded with no hash")
+ var original_location = location.href;
+ location.hash = "test";
+ assert_equals(location.hash, "#test");
+ assert_equals(location.href, original_location + "#test");
+ location.hash = ""
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/002.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/002.html
new file mode 100644
index 0000000000..92bfd63415
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/002.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<!-- this tests the spec as it hopefully will be once bug https://www.w3.org/Bugs/Public/show_bug.cgi?id=17155 is fixed -->
+<title>Fragment Navigation: Updating document address twice</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(function() {
+ assert_equals(location.hash, "", "Page must be loaded with no hash")
+ var original_location = location.href;
+ location.hash = "test";
+ assert_equals(location.hash, "#test");
+ assert_equals(location.href, original_location + "#test");
+
+ location.hash = "test1";
+ assert_equals(location.hash, "#test1");
+ assert_equals(location.href, original_location + "#test1");
+
+ location.hash = "";
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/003.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/003.html
new file mode 100644
index 0000000000..86d85b9ca6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/003.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<!-- this tests the spec as it hopefully will be once bug https://www.w3.org/Bugs/Public/show_bug.cgi?id=17155 is fixed -->
+<title>Fragment Navigation: Updating scroll position</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div id="log"></div>
+<div id="test">scroll 1</div>
+<div style="height:10000px">Filler</div>
+<div id="test1">scroll 2</div>
+<script>
+test(function() {
+ assert_equals(document.scrollingElement.scrollTop, 0);
+ location.hash = "test";
+
+ var scroll1 = document.scrollingElement.scrollTop;
+ assert_true(scroll1 > 0);
+
+ location.hash = "test1";
+ var scroll2 = document.scrollingElement.scrollTop;
+ assert_true(scroll2 > scroll1);
+
+ location.hash = ""
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/004.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/004.html
new file mode 100644
index 0000000000..c365c6fd29
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/004.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<!-- this tests the spec as it hopefully will be once bug https://www.w3.org/Bugs/Public/show_bug.cgi?id=17155 is fixed -->
+<title>Fragment Navigation: hashchange event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div id="log"></div>
+<script>
+var t = async_test();
+t.step(function() {
+ assert_equals(location.hash, "", "Page must be loaded with no hash");
+ location.hash = "test";
+
+ addEventListener("hashchange",
+ t.step_func(function(e) {
+ assert_equals(e.target, window);
+ assert_equals(e.type, "hashchange");
+ assert_false(e.bubbles, "bubbles");
+ assert_false(e.cancelable, "cancelable");
+ t.done();
+ }), true)
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/005.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/005.html
new file mode 100644
index 0000000000..f0761a64f6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/005.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<!-- this tests the spec as it hopefully will be once bug https://www.w3.org/Bugs/Public/show_bug.cgi?id=17155 is fixed -->
+<title>Fragment Navigation: hashchange event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div id="log"></div>
+<script>
+var t = async_test();
+t.step(function() {
+ var original_url = location.href;
+ assert_equals(location.hash, "", "Page must be loaded with no hash");
+ location.hash = "test";
+
+ addEventListener("hashchange",
+ t.step_func(function(e) {
+ assert_equals(e.oldURL, original_url, "oldURL property");
+ assert_equals(e.newURL, location.href, "newURL property");
+ location.hash = "";
+ t.done();
+ }), true);
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/006.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/006.html
new file mode 100644
index 0000000000..a65b9eb4a2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/006.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<!-- this tests the spec as it hopefully will be once bug https://www.w3.org/Bugs/Public/show_bug.cgi?id=17155 is fixed -->
+<title>Fragment Navigation: hashchange event multiple changes old/newURL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div id="log"></div>
+<script>
+var t = async_test();
+t.step(function() {
+ var original_url = location.href;
+ assert_equals(location.hash, "", "Page must be loaded with no hash");
+ location.hash = "test";
+
+ var count = 0;
+ var mid_url = location.href;
+
+ addEventListener("hashchange",
+ t.step_func(function(e) {
+ if (count === 0) {
+ assert_equals(e.oldURL, original_url, "oldURL property first update");
+ assert_equals(e.newURL, mid_url, "newURL property first update");
+ count = 1;
+ } else if (count === 1) {
+ assert_equals(e.oldURL, mid_url, "oldURL property second update");
+ assert_equals(e.newURL, location.href, "newURL property second update");
+ location.hash = "";
+ t.done();
+ }
+ }), true);
+
+ location.hash = "test1";
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/007.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/007.html
new file mode 100644
index 0000000000..0b6fe813ba
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/007.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<!-- this tests the spec as it hopefully will be once bug https://www.w3.org/Bugs/Public/show_bug.cgi?id=17155 is fixed -->
+<title>Fragment Navigation: hashchange event multiple changes old/newURL</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div id="log"></div>
+<script>
+var t = async_test();
+t.step(function() {
+ var original_url = location.href;
+ assert_equals(location.hash, "", "Page must be loaded with no hash");
+
+ var count = 0;
+
+ location.hash = "test";
+
+ hashes = [];
+
+ addEventListener("hashchange",
+ t.step_func(function(e) {
+ if (count < 100) {
+ location.hash = "test" + count++;
+ hashes.push(location.hash);
+ } else if (count === 100) {
+ expected = [];
+ for (var i=0; i<100; i++) {
+ expected.push("#test" + i);
+ }
+ assert_array_equals(hashes, expected);
+ location.hash = "";
+ t.done();
+ }
+ }), true);
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/forward-triggers-hashchange.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/forward-triggers-hashchange.html
new file mode 100644
index 0000000000..45f7e38ba3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/forward-triggers-hashchange.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigating forward after replace() should still trigger hashchange</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#history-traversal">
+<link rel="author" href="mailto:d@domenic.me" title="Domenic Denicola">
+
+<!-- While writing ./replacement-enabled.html, a bug was discovered in Firefox where it does not
+fire hashchange when using history.forward(), at least under certain conditions. So, this test
+exercises that specifically. -->
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="navigate-helpers.js"></script>
+
+<body>
+
+<script>
+"use strict";
+let resolve, iframe;
+promise_test(() => {
+ iframe = document.createElement("iframe");
+ iframe.src = "/common/blank.html";
+ iframe.addEventListener("load", runTest);
+ document.body.appendChild(iframe);
+
+ return new Promise(r => resolve = r);
+});
+
+function runTest() {
+ iframe.removeEventListener("load", runTest);
+ const frameWindow = iframe.contentWindow;
+
+ resolve((async () => {
+ await navigateAndWaitForChange(frameWindow, w => w.location.href = "/common/blank.html#apple");
+ await navigateAndWaitForChange(frameWindow, w => w.location.href = "/common/blank.html#banana");
+ await navigateAndWaitForChange(frameWindow, w => w.location.href = "/common/blank.html#cat");
+
+ await navigateAndWaitForChange(frameWindow, w => w.history.back());
+ await navigateAndWaitForChange(frameWindow,
+ w => w.location.replace("/common/blank.html#zebra"));
+ await navigateAndWaitForChange(frameWindow, w => w.history.forward());
+ })());
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/fragment-and-encoding-2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/fragment-and-encoding-2.html
new file mode 100644
index 0000000000..cd2502ab1c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/fragment-and-encoding-2.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<meta charset=windows-1252>
+<title>Fragment navigation: encoding</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<div style=height:10000px></div>
+<div id=&#xFFFD;></div>
+<div id=&#xFEFF;&#xFFFD;></div>
+<script>
+function goToTop() {
+ location.hash = "top";
+ assert_equals(self.scrollY, 0, "#top");
+}
+
+test(() => {
+ assert_equals(location.hash, "", "Page must be loaded with no hash");
+
+ location.hash = "%C2";
+ assert_equals(location.hash, "#%C2");
+ assert_greater_than(self.scrollY, 1000, "#%C2");
+}, "Invalid percent-encoded UTF-8 byte should decode as U+FFFD");
+
+test(() => {
+ goToTop();
+
+ location.hash = "%EF%BB%BF%C2";
+ assert_equals(location.hash, "#%EF%BB%BF%C2");
+ assert_greater_than(self.scrollY, 1000, "#%EF%BB%BF%C2");
+}, "Percent-encoded UTF-8 BOM followed by invalid UTF-8 byte should decode as U+FEFF U+FFFD");
+
+test(() => {
+ goToTop();
+
+ location.hash = "%EF%BF%BD";
+ assert_equals(location.hash, "#%EF%BF%BD");
+ assert_greater_than(self.scrollY, 1000, "#%EF%BF%BD");
+
+ goToTop();
+}, "Percent-encoded UTF-8 byte sequence for U+FFFD should decode as U+FFFD");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/fragment-and-encoding.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/fragment-and-encoding.html
new file mode 100644
index 0000000000..21fbd4618d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/fragment-and-encoding.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<meta charset=windows-1252>
+<title>Fragment navigation: encoding</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<div style=height:10000px></div>
+<div id=&#xFF;></div>
+<div id=&#xFEFF;></div>
+<div id=&#x2661;&#x00FF;><div>
+<script>
+function goToTop() {
+ location.hash = "top";
+ assert_equals(self.scrollY, 0, "#top");
+}
+
+test(() => {
+ assert_equals(location.hash, "", "Page must be loaded with no hash");
+
+ location.hash = "\u00FF";
+ assert_equals(location.hash, "#%C3%BF");
+ assert_greater_than(self.scrollY, 1000, "#%C3%BF");
+}, "U+00FF should find U+00FF");
+
+test(() => {
+ goToTop();
+
+ location.hash = "%EF%BB%BF";
+ assert_greater_than(self.scrollY, 1000, "#%EF%BB%BF");
+}, "Percent-encoded UTF-8 BOM should find U+FEFF as BOM is not stripped when decoding");
+
+test(() => {
+ goToTop();
+
+ location.hash = "%FF";
+ assert_equals(self.scrollY, 0, "#%FF");
+}, "%FF should not find U+00FF as decoding it gives U+FFFD");
+
+test(() => {
+ goToTop();
+
+ // U+2661 in UTF-8 + %FF.
+ // Chrome had an issue that the following fragment was decoded as U+2661 U+00FF.
+ // https://github.com/whatwg/html/pull/3111
+ location.hash = "%E2%99%A1%FF";
+ assert_equals(self.scrollY, 0, "%E2%99%A1%FF");
+
+ goToTop();
+}, "Valid UTF-8 + invalid UTF-8 should not be matched to the utf8-decoded former + the isomorphic-decoded latter");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/navigate-helpers.js b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/navigate-helpers.js
new file mode 100644
index 0000000000..7a9adeb3d2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/navigate-helpers.js
@@ -0,0 +1,23 @@
+"use strict";
+
+// Usage examples:
+// navigateAndWaitForChange(frameWindow, w => w.location.href = "...");
+// navigateAndWaitForChange(frameWindow, w => w.history.back());
+// navigateAndWaitForChange(frameWindow, w => w.history.back(), { assumeSuccessAfter: 100 });
+
+window.navigateAndWaitForChange = (w, navigationAction, { assumeSuccessAfter } = {}) => {
+ return new Promise(resolve => {
+ w.addEventListener("hashchange", listener);
+
+ function listener() {
+ w.removeEventListener("hashchange", listener);
+ resolve();
+ }
+
+ if (assumeSuccessAfter !== undefined) {
+ step_timeout(resolve, assumeSuccessAfter);
+ }
+
+ navigationAction(w);
+ });
+};
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/replacement-enabled.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/replacement-enabled.html
new file mode 100644
index 0000000000..b22fbed80f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/replacement-enabled.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigating to a fragment should not clear forward history</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#scroll-to-fragid">
+<link rel="help" href="https://github.com/whatwg/html/issues/2796">
+<link rel="help" href="https://github.com/whatwg/html/pull/2869">
+<link rel="author" href="mailto:d@domenic.me" title="Domenic Denicola">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="navigate-helpers.js"></script>
+
+<body>
+
+<script>
+"use strict";
+let resolve, iframe;
+promise_test(() => {
+ iframe = document.createElement("iframe");
+ iframe.src = "/common/blank.html";
+ iframe.addEventListener("load", runTest);
+ document.body.appendChild(iframe);
+
+ return new Promise(r => resolve = r);
+});
+
+function runTest() {
+ iframe.removeEventListener("load", runTest);
+ const frameWindow = iframe.contentWindow;
+
+ resolve((async () => {
+ await navigateAndWaitForChange(frameWindow, w => w.location.href = "/common/blank.html#apple");
+ await navigateAndWaitForChange(frameWindow, w => w.location.href = "/common/blank.html#banana");
+ await navigateAndWaitForChange(frameWindow, w => w.location.href = "/common/blank.html#cat");
+
+ assert_equals(frameWindow.location.hash, "#cat");
+
+ // Might not be 4 (= 3 for iframe + 1 initial) due to cross-browser differences or if people are
+ // running this test in a window that has previously been places. The important thing for this
+ // test is the delta from this value.
+ const afterThreeNavigations = frameWindow.history.length;
+
+ await navigateAndWaitForChange(frameWindow, w => w.history.back());
+
+ assert_equals(frameWindow.location.hash, "#banana");
+ assert_equals(frameWindow.history.length, afterThreeNavigations,
+ "back() must not change the history length");
+
+ await navigateAndWaitForChange(frameWindow,
+ w => w.location.replace("/common/blank.html#zebra"));
+
+ assert_equals(frameWindow.location.hash, "#zebra");
+ assert_equals(frameWindow.history.length, afterThreeNavigations,
+ "replace() must not change the history length");
+
+ // As of the time of this writing (2017-08-14), Firefox is not firing hashchange on forward, so
+ // we automatically assume navigation succeeded after 100 ms. A sibling test will test this
+ // particular Firefox bug.
+ await navigateAndWaitForChange(frameWindow, w => w.history.forward(),
+ { assumeSuccessAfter: 500 });
+
+ assert_equals(frameWindow.location.hash, "#cat");
+ assert_equals(frameWindow.history.length, afterThreeNavigations,
+ "forward() must not change the history length");
+
+ })());
+}
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-frag-non-utf8-encoded-document.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-frag-non-utf8-encoded-document.html
new file mode 100644
index 0000000000..7d4e994f0a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-frag-non-utf8-encoded-document.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<title>Fragment Navigation: fragment id should not be found in non UTF8 document</title>
+<meta name=timeout content=long>
+<meta http-equiv="Content-Type" content="text/html; charset=gbk"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div></div>
+<div id="&#x586f" style="position:absolute; top:100px;"></div>
+<div style="height:200vh;"></div>
+<script>
+async_test(test => {
+ assert_equals(document.characterSet, "GBK", "Document should be GBK encoded");
+ assert_equals(location.hash, "", "Page must be loaded with no hash");
+ location.hash = '%89g';
+ test.step_timeout(() => {
+ assert_equals( document.scrollingElement.scrollTop, 0 );
+ test.done();
+ }, 1);
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-frag-percent-encoded.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-frag-percent-encoded.html
new file mode 100644
index 0000000000..aa179425c5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-frag-percent-encoded.html
@@ -0,0 +1,62 @@
+<!doctype html>
+<title>Fragment Navigation: fragment id should be percent-decoded</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div></div>
+<div id="has two spaces" style="position:absolute; top:100px;"></div>
+<div id="escape%20collision" style="position:absolute; top:200px;"></div>
+<div id="%20has%20two%20spaces" style="position:absolute; top:300px;"></div>
+<div id="escape collision" style="position:absolute; top:400px;"></div>
+<div id="do%20not%20go%20here" style="position:absolute; top:400px;"></div>
+<div style="height:200em;"></div>
+<script>
+var steps = [{
+ fragid:'has%20two%20spaces',
+ handler: function(){
+ assert_equals( document.scrollingElement.scrollTop, 100 );
+ }
+ },{
+ fragid:'escape%20collision',
+ handler: function(){
+ assert_equals( document.scrollingElement.scrollTop, 200 );
+ document.getElementById("%20has%20two%20spaces").setAttribute("id", "has%20two%20spaces");
+ }
+ },{
+ fragid:'has%20two%20spaces',
+ handler: function(){
+ assert_equals( document.scrollingElement.scrollTop, 300 );
+ }
+ },{
+ fragid:'do%20not%20go%20here',
+ handler: function(){
+ // don't move
+ assert_equals( document.scrollingElement.scrollTop, 400 );
+ }
+ }];
+
+function runNextStep(){
+ if( steps.length > 0 ) {
+ var step = steps.shift();
+ var listener = t.step_func( function(){
+ step.handler();
+ runNextStep();
+ });
+ scrollToFragmentThenDo( step.fragid, listener );
+ } else {
+ t.done();
+ }
+}
+
+function scrollToFragmentThenDo( fragid, then ){
+ location.hash = fragid;
+ setTimeout( then, 1 );
+}
+
+var t = async_test();
+t.step( function(){
+ assert_equals(location.hash, "", "Page must be loaded with no hash");
+ runNextStep();
+})
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-position-vertical-lr.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-position-vertical-lr.html
new file mode 100644
index 0000000000..57d99440e1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-position-vertical-lr.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html style="writing-mode: vertical-lr;">
+<head>
+<meta charset="UTF-8">
+<title>Fragment Navigation: Scroll to block start position in vertical-lr writing mode</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="test" style="position: absolute; top: 5px; left: 7px; margin: 5px 7px; border-style: solid; border-width: 5px 7px; padding: 5px 7px; height: 5px; width: 7px;"></div>
+<div style="width: 200vw;"></div>
+<script>
+async_test(function (t) {
+ on_event(window, 'load', function () {
+ t.step(function () {
+ window.scrollTo(0, 0);
+ location.hash = 'test';
+ assert_equals(window.scrollX, 14, 'Scroll to the left border edge of #test');
+ });
+ t.done();
+ });
+}, '');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-position-vertical-rl.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-position-vertical-rl.html
new file mode 100644
index 0000000000..60a902199a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-position-vertical-rl.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html style="writing-mode: vertical-rl;">
+<head>
+<meta charset="UTF-8">
+<title>Fragment Navigation: Scroll to block start position in vertical-rl writing mode</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="test" style="position: absolute; top: 5px; right: 7px; margin: 5px 7px; border-style: solid; border-width: 5px 7px; padding: 5px 7px; height: 5px; width: 7px;"></div>
+<div style="width: 200vw;"></div>
+<script>
+async_test(function (t) {
+ on_event(window, 'load', function () {
+ t.step(function () {
+ window.scrollTo(0, 0);
+ location.hash = 'test';
+ assert_equals(window.scrollX, -14, 'Scroll to the right border edge of #test');
+ });
+ t.done();
+ });
+}, '');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-position.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-position.html
new file mode 100644
index 0000000000..c38d2de83b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-position.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>Fragment Navigation: Scroll to block start position</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="test" style="position: absolute; top: 5px; left: 7px; margin: 5px 7px; border-style: solid; border-width: 5px 7px; padding: 5px 7px; height: 5px; width: 7px;"></div>
+<div style="height: 200vh;"></div>
+<script>
+async_test(function (t) {
+ on_event(window, 'load', function () {
+ t.step(function () {
+ window.scrollTo(0, 0);
+ location.hash = 'test';
+ assert_equals(window.scrollY, 10, 'Scroll to the top border edge of #test');
+ });
+ t.done();
+ });
+}, '');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-to-anchor-name.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-to-anchor-name.html
new file mode 100644
index 0000000000..060aed11e2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-to-anchor-name.html
@@ -0,0 +1,59 @@
+<!doctype html>
+<title>Fragment Navigation: scroll to anchor name is lower priority than equal id</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div></div>
+<a name="anchor1" style="position:absolute; top:200px;"></a>
+<div id="id-equals-anchor" style="position:absolute; top:300px;"></div>
+<a name="id-equals-anchor" style="position:absolute; top:400px;"></a>
+<a name="§1" style="position:absolute; top:400px;"></a>
+<div style="height:200em;"></div>
+<script>
+var steps = [{
+ fragid:'anchor1',
+ handler: function(){
+ assert_equals( scrollPosition(), 200 );
+ }
+ },{
+ fragid:'id-equals-anchor',
+ handler: function(){
+ // id still takes precedence over anchor name
+ assert_equals( scrollPosition(), 300 );
+ }
+ },{
+ fragid:'§1',
+ handler: function(){
+ assert_equals( scrollPosition(), 400 );
+ }
+ }];
+
+function scrollPosition(){
+ return document.documentElement.scrollTop || document.body.scrollTop;
+}
+
+function runNextStep(){
+ if( steps.length > 0 ) {
+ var step = steps.shift();
+ var listener = t.step_func( function(){
+ step.handler();
+ runNextStep();
+ });
+ scrollToFragmentThenDo( step.fragid, listener );
+ } else {
+ t.done();
+ }
+}
+
+function scrollToFragmentThenDo( fragid, then ){
+ location.hash = fragid;
+ setTimeout( then, 1 );
+}
+
+var t = async_test();
+t.step( function(){
+ assert_equals(location.hash, "", "Page must be loaded with no hash");
+ runNextStep();
+})
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-to-id-top.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-to-id-top.html
new file mode 100644
index 0000000000..601d40a2a1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-to-id-top.html
@@ -0,0 +1,51 @@
+<!doctype html>
+<title>Fragment Navigation: TOP is a valid element id, which overrides navigating to top of the document</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div></div>
+<div id="Top" style="position:absolute; top:200px;"></div>
+<div style="height:200em; position:relative;"></div>
+<script>
+var steps = [{
+ fragid:'Top',
+ handler: function(){
+ assert_equals( scrollPosition(), 200 );
+ }
+ },{
+ // scroling to top should work when fragid differs from id by case.
+ fragid:'top',
+ handler: function(){
+ assert_equals( scrollPosition(), 0 );
+ }
+ }];
+
+function scrollPosition(){
+ return document.documentElement.scrollTop || document.body.scrollTop;
+}
+
+function runNextStep(){
+ if( steps.length > 0 ) {
+ var step = steps.shift();
+ var listener = t.step_func( function(){
+ step.handler();
+ runNextStep();
+ });
+ scrollToFragmentThenDo( step.fragid, listener );
+ } else {
+ t.done();
+ }
+}
+
+function scrollToFragmentThenDo( fragid, then ){
+ location.hash = fragid;
+ setTimeout( then, 1 );
+}
+
+var t = async_test();
+t.step( function(){
+ assert_equals(location.hash, "", "Page must be loaded with no hash");
+ runNextStep();
+})
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-to-top.html b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-to-top.html
new file mode 100644
index 0000000000..bf62e4cd55
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/scroll-to-fragid/scroll-to-top.html
@@ -0,0 +1,56 @@
+<!doctype html>
+<title>Fragment Navigation: When fragid is TOP scroll to the top of the document</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div></div>
+<div id="not-the-top"></div>
+<div style="height:200em"></div>
+<script>
+var steps = [{
+ fragid:'not-the-top',
+ handler: function(){
+ assert_not_equals( document.scrollingElement.scrollTop, 0 );
+ }
+ },{
+ fragid:'top',
+ handler: function(){
+ assert_equals( document.scrollingElement.scrollTop, 0 );
+ }
+ },{
+ fragid:'not-the-top',
+ handler: function(){
+ assert_not_equals( document.scrollingElement.scrollTop, 0 );
+ }
+ },{
+ fragid:'TOP',
+ handler: function(){
+ assert_equals( document.scrollingElement.scrollTop, 0 );
+ }
+ }];
+
+function runNextStep(){
+ if( steps.length > 0 ) {
+ var step = steps.shift();
+ var listener = t.step_func( function(){
+ step.handler();
+ runNextStep();
+ });
+ scrollToFragmentThenDo( step.fragid, listener );
+ } else {
+ t.done();
+ }
+}
+
+function scrollToFragmentThenDo( fragid, then ){
+ location.hash = fragid;
+ setTimeout( then, 1 );
+}
+
+var t = async_test();
+t.step( function(){
+ assert_equals(location.hash, "", "Page must be loaded with no hash");
+ runNextStep();
+})
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/001.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/001.html
new file mode 100644
index 0000000000..1ef88d3cc1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/001.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<title>document.open in unload</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+var win;
+
+t.step(function() {
+ win = window.open("support/001-1.html");
+});
+
+add_completion_callback(function() {win.close()});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/002.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/002.html
new file mode 100644
index 0000000000..a4e0b243e2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/002.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<title>document.open in unload</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+var win;
+
+t.step(function() {
+ win = window.open("support/002-1.html");
+});
+
+add_completion_callback(function() {win.close()});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/003.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/003.html
new file mode 100644
index 0000000000..d0a19e0ddc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/003.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<title>document.open in beforeunload with link</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+var win;
+
+t.step(function() {
+ win = window.open("support/003-1.html");
+});
+
+add_completion_callback(function() {win.close()});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/004.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/004.html
new file mode 100644
index 0000000000..fca926f652
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/004.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<title>document.open in beforeunload with button</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+var win;
+
+t.step(function() {
+ win = window.open("support/004-1.html");
+});
+
+add_completion_callback(function() {win.close()});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/005.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/005.html
new file mode 100644
index 0000000000..c215fb88e7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/005.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<title>document.open in pagehide in iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+var win;
+
+t.step(function() {
+ win = window.open("support/005-1.html");
+});
+
+add_completion_callback(function() {win.close()});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/base.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/base.html
new file mode 100644
index 0000000000..70c07cba4c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/base.html
@@ -0,0 +1,14 @@
+<!doctype html>
+Base
+<script>
+onpagehide = function() {
+ if(top.base_hide) {
+ top.base_hide();
+ }
+}
+onpageshow = function() {
+if (top.base_show) {
+ top.base_show();
+}
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-canceling-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-canceling-1.html
new file mode 100644
index 0000000000..6ba1e65740
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-canceling-1.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Support page for beforeunload-canceling.html</title>
+
+<h1>If this goes away, it navigated</h1>
+
+<script>
+"use strict";
+
+window.runTest = (t, { valueToReturn, expectCancelation, setReturnValue, expectedReturnValue, cancel }) => {
+ window.onbeforeunload = t.step_func(e => {
+ if (cancel) {
+ e.preventDefault();
+ }
+
+ if (setReturnValue !== undefined) {
+ e.returnValue = setReturnValue;
+ }
+
+ return valueToReturn;
+ });
+
+ const listener = t.step_func(e => {
+ top.assert_equals(e.defaultPrevented, expectCancelation, "canceled");
+ top.assert_equals(e.returnValue, expectedReturnValue, "returnValue");
+ window.onbeforeunload = null;
+
+ t.done();
+ });
+
+ window.addEventListener("beforeunload", listener);
+
+ window.location.href = "about:blank";
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-canceling.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-canceling.html
new file mode 100644
index 0000000000..29a685fa59
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-canceling.html
@@ -0,0 +1,222 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>beforeunload return value cancelation behavior</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/webappapis.html#the-event-handler-processing-algorithm">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script>
+"use strict";
+
+promise_test(t => {
+ let onbeforeunloadHappened = false;
+ window.onbeforeunload = t.step_func(() => {
+ onbeforeunloadHappened = true;
+ return "cancel me";
+ });
+
+ const eventWatcher = new EventWatcher(t, window, "beforeunload");
+ const promise = eventWatcher.wait_for("beforeunload").then(e => {
+ assert_true(onbeforeunloadHappened, "CustomEvent must be able to trigger the event handler");
+ assert_false(e.defaultPrevented, "The event must not have been canceled");
+ window.onbeforeunload = null;
+ });
+
+ window.dispatchEvent(new CustomEvent("beforeunload"));
+
+ return promise;
+}, "Returning a string must not cancel the event: CustomEvent, non-cancelable");
+
+promise_test(t => {
+ let onbeforeunloadHappened = false;
+ window.onbeforeunload = t.step_func(() => {
+ onbeforeunloadHappened = true;
+ return "cancel me";
+ });
+
+ const eventWatcher = new EventWatcher(t, window, "beforeunload");
+ const promise = eventWatcher.wait_for("beforeunload").then(e => {
+ assert_true(onbeforeunloadHappened, "CustomEvent must be able to trigger the event handler");
+ assert_false(e.defaultPrevented, "The event must not have been canceled");
+ window.onbeforeunload = null;
+ t.done();
+ });
+
+ window.dispatchEvent(new CustomEvent("beforeunload", { cancelable: true }));
+
+ return promise;
+}, "Returning a string must not cancel the event: CustomEvent, cancelable");
+
+promise_test(t => {
+ let onbeforeunloadHappened = false;
+ window.onbeforeunload = t.step_func(() => {
+ onbeforeunloadHappened = true;
+ return false;
+ });
+
+ const eventWatcher = new EventWatcher(t, window, "beforeunload");
+ const promise = eventWatcher.wait_for("beforeunload").then(e => {
+ assert_true(onbeforeunloadHappened, "CustomEvent must be able to trigger the event handler");
+ assert_false(e.defaultPrevented, "The event must not have been canceled");
+ window.onbeforeunload = null;
+ t.done();
+ });
+
+ window.dispatchEvent(new CustomEvent("beforeunload", { cancelable: true }));
+
+ return promise;
+}, "Returning false must not cancel the event, because it's coerced to the DOMString \"false\" which does not cancel " +
+ "CustomEvents: CustomEvent, cancelable");
+
+// This test can be removed if we update the DOM Standard to disallow createEvent("BeforeUnloadEvent"). Browser support
+// is inconsistent. https://github.com/whatwg/dom/issues/362
+promise_test(t => {
+ const eventWatcher = new EventWatcher(t, window, "click");
+ const promise = eventWatcher.wait_for("click").then(e => {
+ assert_false(e.defaultPrevented, "The event must not have been canceled");
+ window.onbeforeunload = null;
+ t.done();
+ });
+
+ const ev = document.createEvent("BeforeUnloadEvent");
+ ev.initEvent("click", false, true);
+ window.dispatchEvent(ev);
+
+ return promise;
+}, "Returning a string must not cancel the event: BeforeUnloadEvent with type \"click\", cancelable");
+
+const testCases = [
+ {
+ valueToReturn: null,
+ expectCancelation: false,
+ expectedReturnValue: ""
+ },
+ {
+ valueToReturn: undefined,
+ expectCancelation: false,
+ expectedReturnValue: ""
+ },
+ {
+ valueToReturn: "",
+ expectCancelation: true,
+ expectedReturnValue: ""
+ },
+ {
+ valueToReturn: false,
+ expectCancelation: true,
+ expectedReturnValue: "false"
+ },
+ {
+ valueToReturn: true,
+ expectCancelation: true,
+ expectedReturnValue: "true"
+ },
+ {
+ valueToReturn: 0,
+ expectCancelation: true,
+ expectedReturnValue: "0"
+ },
+ {
+ valueToReturn: null,
+ expectCancelation: false,
+ setReturnValue: "foo",
+ expectedReturnValue: "foo"
+ },
+ {
+ valueToReturn: undefined,
+ expectCancelation: false,
+ setReturnValue: "foo",
+ expectedReturnValue: "foo"
+ },
+ {
+ valueToReturn: "",
+ expectCancelation: true,
+ setReturnValue: "foo",
+ expectedReturnValue: "foo"
+ },
+ {
+ valueToReturn: false,
+ expectCancelation: true,
+ setReturnValue: "foo",
+ expectedReturnValue: "foo"
+ },
+ {
+ valueToReturn: true,
+ expectCancelation: true,
+ setReturnValue: "foo",
+ expectedReturnValue: "foo"
+ },
+ {
+ valueToReturn: 0,
+ expectCancelation: true,
+ setReturnValue: "foo",
+ expectedReturnValue: "foo"
+ },
+ {
+ setReturnValue: "",
+ expectedReturnValue: "",
+ expectCancelation: false,
+ },
+ {
+ expectCancelation: true,
+ expectedReturnValue: "",
+ cancel: true
+ },
+ {
+ setReturnValue: "foo",
+ expectCancelation: true,
+ expectedReturnValue: "foo",
+ cancel: true
+ },
+ {
+ valueToReturn: "foo",
+ expectedReturnValue: "foo",
+ expectCancelation: true,
+ cancel: true
+ },
+ {
+ valueToReturn: "foo",
+ setReturnValue: "foo",
+ expectedReturnValue: "foo",
+ expectCancelation: true,
+ cancel: true
+ },
+ {
+ valueToReturn: true,
+ setReturnValue: "",
+ expectedReturnValue: "true",
+ expectCancelation: true,
+ cancel: true
+ }
+];
+
+var testCaseIndex = 0;
+function runNextTest() {
+ const testCase = testCases[testCaseIndex];
+
+ const labelAboutReturnValue = testCase.setReturnValue === undefined ? "" :
+ `; setting returnValue to ${testCase.setReturnValue}`;
+
+ const labelAboutCancel = testCase.cancel === undefined ? "" :
+ "; calling preventDefault()";
+
+ const suffixLabels = labelAboutReturnValue + labelAboutCancel;
+
+ async_test(t => {
+ const iframe = document.createElement("iframe");
+ iframe.onload = t.step_func(() => {
+ iframe.contentWindow.runTest(t, testCase);
+ if (++testCaseIndex < testCases.length)
+ runNextTest();
+ });
+
+ iframe.src = "beforeunload-canceling-1.html";
+ document.body.appendChild(iframe);
+ }, `Returning ${testCase.valueToReturn} with a real iframe unloading${suffixLabels}`);
+}
+
+runNextTest();
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-history-back-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-history-back-1.html
new file mode 100644
index 0000000000..4403cfa8e9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-history-back-1.html
@@ -0,0 +1,5 @@
+<!doctype html>
+001-1
+<script>
+addEventListener("beforeunload", function() {top.t.step(function() {top.beforeunload_fired = true})}, false);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-history-back.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-history-back.html
new file mode 100644
index 0000000000..5b0415c422
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-history-back.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<title>beforeunload event fires on history navigation back</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+beforeunload_fired = false;
+var t = async_test();
+
+var base_count = 0;
+
+onload = function() {setTimeout(t.step_func(function() {
+ var iframe = document.getElementsByTagName("iframe")[0]
+ iframe.onload = t.step_func(function() {
+ iframe.onload = null;
+ history.go(-1);
+ });
+
+ iframe.src = "beforeunload-on-history-back-1.html";
+}), 100)};
+
+base_show = t.step_func(function() {
+ base_count++;
+ if (base_count > 1) {
+ assert_true(beforeunload_fired);
+ t.done();
+ }
+});
+
+</script>
+<iframe src="base.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-navigation-of-parent-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-navigation-of-parent-1.html
new file mode 100644
index 0000000000..4f239dad1e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-navigation-of-parent-1.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<iframe src="beforeunload-on-navigation-of-parent-2.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-navigation-of-parent-2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-navigation-of-parent-2.html
new file mode 100644
index 0000000000..a34b182e70
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-navigation-of-parent-2.html
@@ -0,0 +1,4 @@
+<!doctype html>
+<script>
+addEventListener("beforeunload", function() {parent.parent.beforeunload_fired=true}, false)
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-navigation-of-parent.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-navigation-of-parent.html
new file mode 100644
index 0000000000..96d49567f3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-on-navigation-of-parent.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<title>beforeunload in iframe on navigation of parent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+beforeunload_fired = false;
+var t = async_test();
+
+var base_count = 0;
+
+onload = function() {setTimeout(t.step_func(function() {
+ var iframe = document.getElementsByTagName("iframe")[0]
+ iframe.onload = t.step_func(function() {
+ iframe.onload = null;
+ history.go(-1);
+ });
+
+ iframe.src = "beforeunload-on-navigation-of-parent-1.html";
+}), 100)};
+
+base_show = t.step_func(function() {
+ base_count++;
+ if (base_count > 1) {
+ assert_true(beforeunload_fired);
+ t.done();
+ }
+});
+
+</script>
+<iframe src="base.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-sticky-activation-iframe.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-sticky-activation-iframe.html
new file mode 100644
index 0000000000..212a10c005
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-sticky-activation-iframe.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Beforeunload must be gated behind sticky activation: nested browsing context</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<p>If you happen to be running this test as a human, then be sure not to interact with any part of the page; that would invalidate the results!
+
+<script>
+setup({ single_test: true });
+
+const iframe = document.createElement('iframe');
+iframe.src = 'support/beforeunload-sticky-start.html';
+
+window.onmessage = e => {
+ assert_equals(e.data, 'navigated successfully');
+
+ const desiredURL = (new URL('support/beforeunload-sticky-destination.html', location.href)).href;
+ assert_equals(iframe.contentWindow.location.href, desiredURL);
+
+ done();
+};
+
+document.body.append(iframe);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-sticky-activation-manual.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-sticky-activation-manual.html
new file mode 100644
index 0000000000..55612bbfc4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-sticky-activation-manual.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Beforeunload must be gated behind sticky activation: normal top-level browsing context</title>
+
+<p>This test is manual because we want to test non-popup, non-iframe situations. Sibling files contain automated tests for those situations.
+
+<p>In three seconds, this document will redirect itself to a new page. The test passes if the redirect succeeds. The test fails if a beforeunload dialog pops up asking for confirmation.
+
+<p>Be sure not to interact with any part of the page in the meantime. That would invalidate the results.
+
+<script>
+window.onbeforeunload = e => e.preventDefault();
+
+setTimeout(() => {
+ location.href = 'support/beforeunload-sticky-destination.html';
+}, 3000);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-sticky-activation-popup.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-sticky-activation-popup.html
new file mode 100644
index 0000000000..23bf8a440d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-sticky-activation-popup.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Beforeunload must be gated behind sticky activation: auxiliary browsing context</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<p>If you happen to be running this test as a human, then be sure not to interact with any part of the page; that would invalidate the results!
+
+<script>
+setup({ single_test: true });
+
+const w = window.open('support/beforeunload-sticky-start.html');
+
+window.onmessage = e => {
+ assert_equals(e.data, 'navigated successfully');
+
+ const desiredURL = (new URL('support/beforeunload-sticky-destination.html', location.href)).href;
+ assert_equals(w.location.href, desiredURL);
+
+ w.close();
+
+ done();
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-synchronous.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-synchronous.html
new file mode 100644
index 0000000000..6806eaf7a3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/beforeunload-synchronous.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>beforeunload event is emitted synchronously</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/webappapis.html#the-event-handler-processing-algorithm">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+'use strict';
+// "navigate a browsing context" synchronously calls "prompt to unload", which
+// synchronously calls "dispatch an event".
+
+async_test(function(t) {
+ var iframe = document.createElement('iframe');
+
+ iframe.onload = t.step_func(function() {
+ var callCount = 0;
+
+ iframe.contentWindow.onbeforeunload = function() {
+ callCount += 1;
+ };
+
+ iframe.contentWindow.location.href = '/common/blank.html';
+
+ assert_equals(callCount, 1, 'invoked synchronously exactly once');
+
+ t.done();
+ });
+
+ document.body.appendChild(iframe);
+});
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/navigation-within-beforeunload-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/navigation-within-beforeunload-1.html
new file mode 100644
index 0000000000..b96234fba2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/navigation-within-beforeunload-1.html
@@ -0,0 +1,10 @@
+<!doctype html>
+004-1
+<script>
+addEventListener("beforeunload",
+function() {
+if (top.counter++ < 999) {
+ location = "navigation-within-beforeunload-2.html?" + top.counter;
+}
+}, false);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/navigation-within-beforeunload-2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/navigation-within-beforeunload-2.html
new file mode 100644
index 0000000000..2dceaa6d6a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/navigation-within-beforeunload-2.html
@@ -0,0 +1,4 @@
+<!doctype html>
+<script>
+document.write(location)
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/navigation-within-beforeunload.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/navigation-within-beforeunload.html
new file mode 100644
index 0000000000..d6ecf5d52f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/navigation-within-beforeunload.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<title>Triggering navigation from within beforeunload event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+beforeunload_fired = false;
+var t = async_test();
+
+var base_count = 0;
+var counter = 0;
+
+onload = function() {setTimeout(function() {
+ var iframe = document.getElementsByTagName("iframe")[0]
+
+ iframe.onload = function() {
+ setTimeout(function() {iframe.contentWindow.location="navigation-within-beforeunload-2.html";}, 100);
+ // Step 4 of https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigating-across-documents
+ // doesn't seem to allow navigation within a beforeunload handler,
+ // so the counter should not go beyond 1.
+ iframe.onload = t.step_func(function() {assert_equals(counter, 1); t.done()});
+ };
+
+ iframe.src = "navigation-within-beforeunload-1.html?" + Math.random();
+
+}, 100)};
+
+</script>
+<iframe src="base.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/pagehide-on-history-forward-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/pagehide-on-history-forward-1.html
new file mode 100644
index 0000000000..a60c20ed80
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/pagehide-on-history-forward-1.html
@@ -0,0 +1,2 @@
+<!doctype html>
+filler text
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/pagehide-on-history-forward.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/pagehide-on-history-forward.html
new file mode 100644
index 0000000000..5e64b5ec66
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/pagehide-on-history-forward.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<title>pagehide event fires on history navigation forward</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+
+onload = function() {setTimeout(t.step_func(function() {
+ var iframe = document.getElementsByTagName("iframe")[0]
+
+ iframe.src = "pagehide-on-history-forward-1.html";
+}), 100)};
+
+base_hide = t.step_func(function() {
+ t.done()
+});
+</script>
+<iframe src="base.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt-and-unload-script-closeable.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt-and-unload-script-closeable.html
new file mode 100644
index 0000000000..b94789c40f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt-and-unload-script-closeable.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<title>beforeunload and unload events fire after window.close() in script-closeable browsing context</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+beforeunload_fired = false;
+var t = async_test();
+
+onload = t.step_func(function() {
+ window.close();
+});
+
+onbeforeunload = t.step_func(function() {
+ beforeunload_fired = true;
+});
+
+onunload = t.step_func(function() {
+ assert_true(beforeunload_fired);
+ t.done()
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt-and-unload-script-uncloseable-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt-and-unload-script-uncloseable-1.html
new file mode 100644
index 0000000000..3a557ce34e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt-and-unload-script-uncloseable-1.html
@@ -0,0 +1,10 @@
+<!doctype html>
+script-uncloseable-1
+<script>
+onbeforeunload = function() {
+ parent.beforeunload_fired = true;
+};
+onunload = function() {
+ parent.unload_fired = true;
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt-and-unload-script-uncloseable.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt-and-unload-script-uncloseable.html
new file mode 100644
index 0000000000..f6a17d740b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt-and-unload-script-uncloseable.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<title>beforeunload and unload events do not fire after window.close() in script-uncloseable browsing context</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var beforeunload_fired = false;
+var unload_fired = false;
+var t = async_test();
+
+onload = t.step_func(function() {
+ var iframe = document.getElementsByTagName("iframe")[0]
+ iframe.onload = t.step_func(function() {
+ iframe.contentWindow.close()
+ t.step_timeout(function() {
+ assert_false(beforeunload_fired);
+ assert_false(unload_fired);
+ t.done();
+ }, 1000);
+ });
+ iframe.src = "prompt-and-unload-script-uncloseable-1.html";
+});
+</script>
+<iframe></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/001-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/001-1.html
new file mode 100644
index 0000000000..b68afc49ec
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/001-1.html
@@ -0,0 +1,10 @@
+<script>
+addEventListener("beforeunload",
+function() {
+ parent.events.push("beforeunload");
+}, false);
+parent.events.push("before src change");
+
+location.href = "001-2.html&pipe=trickle(d2)";
+parent.events.push("after src change");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/001-2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/001-2.html
new file mode 100644
index 0000000000..9da0f9395c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/001-2.html
@@ -0,0 +1 @@
+001-2
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/001.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/001.html
new file mode 100644
index 0000000000..109dcc1393
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/001.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>beforeunload event order</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+events = [];
+onload = t.step_func(function() {
+ assert_array_equals(events, ["before src change", "beforeunload", "after src change"]);
+ t.done();
+})
+</script>
+<iframe src="001-1.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/002-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/002-1.html
new file mode 100644
index 0000000000..c5f57375da
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/002-1.html
@@ -0,0 +1,7 @@
+<script>
+addEventListsner("beforeunload", parent.t.step_func(
+function(e) {
+ parent.do_test(e);
+}, false);
+location.href = "001-2.html&pipe=trickle(d2)";
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/002.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/002.html
new file mode 100644
index 0000000000..d8f4fc60a9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/002.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>beforeunload event properties</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+
+function do_test(e) {
+ assert_equals(e.type, "beforeunload");
+ assert_false(e.bubbles, "bubbles");
+ assert_true(e.cancelable, "bubbles");
+ assert_equals(e.returnValue, "");
+}
+
+onload = t.step_func(function() {
+ t.done();
+})
+</script>
+<iframe src="001-1.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/003.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/003.html
new file mode 100644
index 0000000000..5683f1b120
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/003.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>beforeunload event in child frame for parent navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+
+function do_test(e) {
+ assert_equals(e.type, "beforeunload");
+ assert_false(e.bubbles, "bubbles");
+ assert_true(e.cancelable, "bubbles");
+ assert_equals(e.returnValue, "");
+}
+
+onload = t.step_func(function() {
+ t.done();
+})
+</script>
+<iframe src="001-1.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/004-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/004-1.html
new file mode 100644
index 0000000000..a3ca82f520
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/004-1.html
@@ -0,0 +1,28 @@
+<!doctype html>
+004-1
+<script>
+var handleBeforeUnload = function() {
+ parent.beforeunload_fired = true;
+ removeListener();
+ setTimeout(function() {
+ parent.timeout_fired = true;
+ }, 1000);
+}
+
+var removeListener = function() {
+ assert_true(window.removeEventListener('beforeunload', handleBeforeUnload, false));
+}
+
+window.addEventListener('beforeunload', handleBeforeUnload, false);
+
+onload = function() {
+ if (!parent.loaded) {
+ parent.loaded = true;
+ location="004-2.html?" + Math.random();
+ }
+}
+</script>
+// child frame with no onbeforeunload listener. Should leave the parent as unsalvageable.
+// Adding the iframe prevents potential implementation bugs where the the recursive steps of #prompt-to-unload-a-document
+// would overwrite the salvageable state of the parent.
+<iframe></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/004-2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/004-2.html
new file mode 100644
index 0000000000..1a605b1b3d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/004-2.html
@@ -0,0 +1,5 @@
+<!doctype html>
+004-2
+<script>
+onload = function() {setTimeout(parent.t.step_func(function() {parent.start_test(); history.go(-1)}), 100)}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/004.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/004.html
new file mode 100644
index 0000000000..7076a4dd18
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/004.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<title>salvagable state of document after setting beforeunload listener</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+
+var loaded = false;
+var beforeunload_fired = false;
+var timeout_fired = false;
+
+function start_test() {
+ step_timeout(
+ t.step_func(function() {
+ assert_true(beforeunload_fired);
+ assert_false(timeout_fired);
+ t.done()
+ }), 1000);
+}
+
+onload = function() {
+ var iframe = document.getElementsByTagName("iframe")[0]
+ onload = null;
+ iframe.src="004-1.html?" + Math.random();
+};
+
+</script>
+<iframe></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-001.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-001.html
new file mode 100644
index 0000000000..3b7ef74b71
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-001.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<title>Prompt when beforeunload is canceled</title>
+<script>
+addEventListener("beforeunload",
+function(e) {e.preventDefault()},
+false);
+</script>
+<p>When clicking the link below, you should get a prompt asking if you want to unload the document</p>
+<a href="next.html">Click here</a>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-002.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-002.html
new file mode 100644
index 0000000000..7be8a3301f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-002.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<title>Prompt when beforeunload has returnValue set</title>
+<script>
+addEventListener("beforeunload",
+function(e) {e.returnValue = "PASS if you see this"},
+false);
+</script>
+<p>When clicking the link below, you should get a prompt asking if you want to unload the document</p>
+<a href="next.html">Click here</a>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-003.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-003.html
new file mode 100644
index 0000000000..ff72b67055
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-003.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<title>Prompt when beforeunload is canceled</title>
+<script>
+addEventListener("beforeunload",
+function(e) {e.preventDefault()},
+false);
+</script>
+<p>When clicking the button below, you should get a prompt asking if you want to unload the document</p>
+<form method="get" action="next.html">
+<input type="submit" value="Click here">
+</form>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-004.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-004.html
new file mode 100644
index 0000000000..a4d2968922
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-004.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<title>Prompt on form submit</title>
+<script>
+addEventListener("beforeunload",
+function(e) {e.preventDefault()},
+false);
+</script>
+<p>When clicking the button below, you should get a prompt asking if you want to unload the document</p>
+<form method="get" action="next.html">
+<input type="submit" value="Click here">
+</form>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-005.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-005.html
new file mode 100644
index 0000000000..71ff0a241d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-005.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<title>Event loop pause for beforeunload</title>
+<script>
+var counter = 0;
+
+onload = function count() {
+ document.getElementById("log").textContent = counter++
+ setTimeout(count, 200);
+}
+
+addEventListener("beforeunload",
+function(e) {
+ e.preventDefault()
+},
+false);
+</script>
+<ul>
+<li>Click on the link below. When the prompt appears the counter at the bottom must stop incrementing.
+<li>Opt not to leave the page. The counter must start incrementing again
+</ul>
+<p><a href="">Click here</a>
+<div id="log"></div>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-006.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-006.html
new file mode 100644
index 0000000000..dae0340ad9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/manual-006.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<title>Prompt when beforeunload returns string value</title>
+<script>
+addEventListener("beforeunload",
+function(e) {return "PASS if you see this"},
+false);
+</script>
+<p>When clicking the link below, you should get a prompt asking if you want to unload the document</p>
+<a href="next.html">Click here</a>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/next.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/next.html
new file mode 100644
index 0000000000..38e7cdd5e0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/prompt/next.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<p>You should have seen a prompt asking you to unload the previous document
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/001-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/001-1.html
new file mode 100644
index 0000000000..2a9cab4aa4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/001-1.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<script>
+ t = opener.t;
+ do_test = t.step_func(function () {
+ localStorage.test6564729 += '4';
+ var d = document;
+ var e = document.open(); // has no effect (ignore-opens-during-unload > 0)
+ localStorage.test6564729 += (e == d) ? '5' : 'A [' + e + '] ';
+ document.write('FAIL - document.write executed and blocked navigation!'); // has no effect (ignore-opens-during-unload > 0)
+ localStorage.test6564729 += document.body.textContent.match('FAIL') ? 'B' : '6';
+ document.close(); // has no effect (no script-created parser)
+ localStorage.test6564729 += '7';
+ })
+onload = t.step_func(function() {
+ localStorage.test6564729 = '0';
+ setTimeout(t.step_func(function() {document.links[0].click()}));
+});
+</script>
+<body onbeforeunload="localStorage.test6564729 += '1'"
+ onpagehide="localStorage.test6564729 += '3'"
+ onunload="do_test()">
+<p><a href="001a.html">Follow this link to run the test.</a>
+<p><iframe src="001b.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/001a.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/001a.html
new file mode 100644
index 0000000000..36d4188b9e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/001a.html
@@ -0,0 +1,7 @@
+<!DOCTYPE HTML>
+<script>
+opener.t.step(function() {
+ opener.assert_equals(localStorage.test6564729, '0123456789');
+ opener.t.done();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/001b.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/001b.html
new file mode 100644
index 0000000000..eaafc371a1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/001b.html
@@ -0,0 +1,5 @@
+<!DOCTYPE HTML>
+<body onbeforeunload="localStorage.test6564729 += '2'"
+ onpagehide="localStorage.test6564729 += '8'"
+ onunload="localStorage.test6564729 += '9'">
+<p>Inner frame \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/002-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/002-1.html
new file mode 100644
index 0000000000..017fbb964c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/002-1.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<script>
+ var t = opener.t;
+
+ var do_test = t.step_func(function() {
+ localStorage.test6564729 += '1';
+ var d = document;
+ // This document's unload not triggered here because `document.open` erases
+ // all of the document's handlers. However the iframe's event handlers (002b)
+ // will fire. The `beforeunload` event handler will not fire because this is
+ // not a navigation resulting from a user interaction.
+ var e = document.open();
+ localStorage.test6564729 += (e == d) ? '8' : 'X';
+ var s = 'FAIL if you see this | ' + localStorage.test6564729;
+ document.write(s);
+ localStorage.test6564729 += document.body.textContent == s ? '9' : 'x';
+ document.close();
+ localStorage.test6564729 += 'Z';
+ document.body.textContent += ' // ' + localStorage.test6564729;
+ location = '002a.html'; // unload triggers again here, but they're not registered event listeners any more
+ });
+
+onload = t.step_func(function() {
+ localStorage.test6564729 = '0';
+ setTimeout(function() {document.getElementsByTagName("input")[0].click()}, 100);
+});
+</script>
+<body onbeforeunload="localStorage.test6564729 += '2'"
+ onpagehide="localStorage.test6564729 += '4'"
+ onunload="localStorage.test6564729 += '5'">
+<input type=button value="Activate this button to run the test" onclick="do_test()">
+<p><iframe src="002b.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/002a.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/002a.html
new file mode 100644
index 0000000000..51ff3e37b6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/002a.html
@@ -0,0 +1,7 @@
+<!DOCTYPE HTML>
+<script>
+ opener.t.step(function() {
+ opener.assert_equals(localStorage.test6564729, '016789Z');
+ opener.t.done();
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/002b.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/002b.html
new file mode 100644
index 0000000000..d08a7a8add
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/002b.html
@@ -0,0 +1,5 @@
+<!DOCTYPE HTML>
+<body onbeforeunload="localStorage.test6564729 += '3'"
+ onpagehide="localStorage.test6564729 += '6'"
+ onunload="localStorage.test6564729 += '7'">
+<p>Inner frame \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/003-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/003-1.html
new file mode 100644
index 0000000000..b3a4754b85
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/003-1.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<script>
+ var t = opener.t;
+ var do_test = t.step_func(function() {
+ localStorage.test6564729 += '1';
+ var d = document;
+ var e = document.open(); // has no effect (ignore-opens-during-unload > 0 because we're in beforeunload)
+ localStorage.test6564729 += (e == d) ? '2' : 'A [' + e + '] ';
+ document.write('FAIL - document.write executed and blocked navigation!'); // has no effect (ignore-opens-during-unload > 0)
+ localStorage.test6564729 += document.body.textContent.match('FAIL') ? 'B' : '3';
+ document.close(); // has no effect (no script-created parser)
+ localStorage.test6564729 += '4';
+ })
+
+ onload=t.step_func(function() {localStorage.test6564729 = '0'; setTimeout(t.step_func(function() {document.links[0].click()}), 100)})
+
+</script>
+<body
+ onbeforeunload="do_test()"
+ onpagehide="localStorage.test6564729 += '6'"
+ onunload="localStorage.test6564729 += '7'">
+<p><a href="003a.html">Follow this link to run the test.</a>
+<p><iframe src="003b.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/003a.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/003a.html
new file mode 100644
index 0000000000..5393fa221e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/003a.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<p>FAIL</p>
+<script>
+opener.t.step(function() {
+ opener.assert_equals(localStorage.test6564729, '0123456789')
+ opener.t.done();
+})
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/003b.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/003b.html
new file mode 100644
index 0000000000..c8f1917b85
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/003b.html
@@ -0,0 +1,5 @@
+<!DOCTYPE HTML>
+<body onbeforeunload="localStorage.test6564729 += '5'"
+ onpagehide="localStorage.test6564729 += '8'"
+ onunload="localStorage.test6564729 += '9'">
+<p>Inner frame \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/004-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/004-1.html
new file mode 100644
index 0000000000..06aba08af6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/004-1.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<script>
+ var t = opener.t;
+ var do_test = t.step_func(function() {
+ localStorage.test6564729 += 'B';
+ var d = document;
+ var e = document.open(); // unload triggered here - beforeunload C, D in 004b; pagehide E, unload F, pagehide G in 004b, unload HIJK in 004b
+ localStorage.test6564729 += (e == d) ? 'L' : 'Y';
+ var s = 'FAIL if you see this | ' + localStorage.test6564729;
+ document.write(s);
+ localStorage.test6564729 += document.body.textContent == s ? 'M' : 'y';
+ document.close();
+ localStorage.test6564729 += 'N';
+ location = '004a.html'; // unload triggers again here, but they're not registered event listeners any more
+ })
+onload = t.step_func(function() {
+ localStorage.test6564729 = 'A';
+ setTimeout(t.step_func(function() {document.getElementsByTagName("input")[0].click()}), 100);
+})
+</script>
+<body onbeforeunload="localStorage.test6564729 += 'C'"
+ onpagehide="localStorage.test6564729 += 'E'"
+ onunload="localStorage.test6564729 += 'F'">
+<input type=button value="Activate this button to run the test" onclick="do_test()">
+<p><iframe src="004b.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/004a.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/004a.html
new file mode 100644
index 0000000000..117e2b94ae
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/004a.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<p>FAIL</p>
+<script>
+opener.t.step(function() {
+ opener.assert_equals(localStorage.test6564729, 'ABCDEFGHIJKLMN');
+ opener.t.done();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/004b.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/004b.html
new file mode 100644
index 0000000000..788937a0b0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/004b.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<script>
+ function testRun() {
+ localStorage.test6564729 += 'H';
+ var d = parent.document;
+ var e = parent.document.open(); // no effect, since that document is already in unload
+ localStorage.test6564729 += (e == d) ? 'I' : 'X';
+ var s = 'FAIL';
+ document.write(s);
+ localStorage.test6564729 += document.body.textContent == s ? 'x' : 'J';
+ document.close();
+ localStorage.test6564729 += 'K';
+ }
+</script>
+<body onbeforeunload="localStorage.test6564729 += 'D'"
+ onpagehide="localStorage.test6564729 += 'G'"
+ onunload="testRun()">
+<p>Inner frame \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/005-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/005-1.html
new file mode 100644
index 0000000000..7b81a9f115
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/005-1.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<script>
+onload = opener.t.step_func(function() {
+ localStorage.test6564729 = '0'
+ setTimeout(opener.t.step_func(function() {document.links[0].click()}), 100);
+});
+</script>
+<body
+ onbeforeunload="localStorage.test6564729 += '1'"
+ onpagehide="localStorage.test6564729 += '3'"
+ onunload="localStorage.test6564729 += '4'">
+<p><a href="005a.html">Follow this link to run the test.</a>
+<p><iframe src="005b.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/005a.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/005a.html
new file mode 100644
index 0000000000..5185d3b921
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/005a.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<p>FAIL</p>
+<script>
+opener.t.step(function() {
+ opener.assert_equals(localStorage.test6564729, '012345678')
+ opener.t.done();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/005b.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/005b.html
new file mode 100644
index 0000000000..476e8e38c4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/005b.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<script>
+ var t = parent.opener.t;
+ var do_test = t.step_func(function () {
+ localStorage.test6564729 += '5';
+ var s = 'FAIL: document.open() has canceled the navigation (' + localStorage.test6564729 + ')';
+ parent.document.open();
+ parent.document.write(s);
+ parent.document.close();
+ localStorage.test6564729 += parent.document.body.textContent.match('FAIL') == s ? 'X' : '6';
+ localStorage.test6564729 += '7';
+ });
+</script>
+<body onbeforeunload="localStorage.test6564729 += '2'"
+ onpagehide="do_test()"
+ onunload="localStorage.test6564729 += '8'">
+<p>Inner frame
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/beforeunload-sticky-destination.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/beforeunload-sticky-destination.html
new file mode 100644
index 0000000000..2edcf1a43b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/beforeunload-sticky-destination.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Beforeunload must be gated behind sticky activation: destination page</title>
+
+<p>If you reached this page without clicking through a confirmation dialog, then the test has passed!
+
+<script>
+if (window.opener) {
+ window.opener.postMessage('navigated successfully');
+} else if (window.parent) {
+ window.parent.postMessage('navigated successfully');
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/beforeunload-sticky-start.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/beforeunload-sticky-start.html
new file mode 100644
index 0000000000..37109feafe
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/support/beforeunload-sticky-start.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Beforeunload must be gated behind sticky activation: start page</title>
+
+<p>This page will immediately navigate. If a beforeunload dialog pops up, the test fails.</p>
+
+<script>
+window.onbeforeunload = e => e.preventDefault();
+location.href = 'beforeunload-sticky-destination.html';
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/001-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/001-1.html
new file mode 100644
index 0000000000..74ba43954b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/001-1.html
@@ -0,0 +1,4 @@
+<!doctype html>
+<script>
+addEventListener("pagehide", parent.t.step_func(function() {parent.pagehide_fired = true}), false);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/001-2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/001-2.html
new file mode 100644
index 0000000000..90e28ab7fb
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/001-2.html
@@ -0,0 +1,2 @@
+<!doctype html>
+Filler
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/001.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/001.html
new file mode 100644
index 0000000000..444a2770c7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/001.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<title>pagehide event on unload</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+pagehide_fired = false;
+var t = async_test();
+
+onload = function() {setTimeout(function() {
+ var iframe = document.getElementsByTagName("iframe")[0]
+
+ iframe.onload = function() {
+ setTimeout(function() {
+ iframe.contentWindow.location="001-2.html";
+ }, 100);
+ iframe.onload = t.step_func(function() {assert_true(pagehide_fired); t.done()});
+ };
+
+ iframe.src = "001-1.html?" + Math.random();
+
+}, 100)};
+
+</script>
+<iframe></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/002-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/002-1.html
new file mode 100644
index 0000000000..fd8e2b7262
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/002-1.html
@@ -0,0 +1,4 @@
+<!doctype html>
+<script>
+addEventListener("pagehide", parent.t.step_func(parent.do_test()), false);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/002.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/002.html
new file mode 100644
index 0000000000..d36011286c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/002.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<title>pagehide event properties</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+
+onload = function() {setTimeout(function() {
+ var iframe = document.getElementsByTagName("iframe")[0]
+
+ iframe.onload = function() {
+ setTimeout(function() {
+ iframe.contentWindow.location="001-2.html";
+ }, 100);
+ iframe.onload = t.step_func(function() {t.done()});
+ };
+
+ function do_test(e) {
+ assert_equals(e.type, "pagehide");
+ assert_equals(e.target, iframe.contentDocument);
+ assert_equals(e.currentTarget, iframe.contentWindow);
+
+ // https://github.com/whatwg/html/issues/6794
+ assert_true(e.bubbles, "bubbles");
+ assert_true(e.cancelable, "cancelable");
+
+ assert_true(e.persisted, "persisted");
+ }
+
+ iframe.src = "002-1.html?" + Math.random();
+
+}, 100)};
+
+</script>
+<iframe></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/003-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/003-1.html
new file mode 100644
index 0000000000..9838c79456
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/003-1.html
@@ -0,0 +1,4 @@
+<!doctype html>
+<script>
+addEventListener("unload", parent.t.step_func(function(e) {parent.do_test(e)}), false);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/003.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/003.html
new file mode 100644
index 0000000000..97821be484
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/003.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<title>unload event properties</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+var do_test;
+
+onload = function() {setTimeout(function() {
+ var iframe = document.getElementsByTagName("iframe")[0]
+
+ iframe.onload = function() {
+ setTimeout(function() {
+ iframe.contentWindow.location="002-2.html";
+ }, 100);
+ iframe.onload = t.step_func(function() {t.done()});
+ };
+
+ do_test = function(e) {
+ assert_equals(e.type, "unload");
+ assert_equals(e.target, iframe.contentDocument);
+ assert_equals(e.currentTarget, iframe.contentWindow);
+ assert_false(e.bubbles, "bubbles");
+ assert_false(e.cancelable, "cancelable");
+ }
+
+ iframe.src = "003-1.html?" + Math.random();
+
+}, 100)};
+
+</script>
+<iframe></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/004-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/004-1.html
new file mode 100644
index 0000000000..5d0497556b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/004-1.html
@@ -0,0 +1,5 @@
+<!doctype html>
+<script>
+addEventListener("pagehide", function() {parent.events.push("pagehide"); setTimeout(function() {parent.events.push("timeout")}, 0)}, false);
+addEventListener("unload", function() {parent.events.push("unload")}, false);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/004.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/004.html
new file mode 100644
index 0000000000..301baa3b8e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/004.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<title>pagehide / unload event order</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+
+var events = [];
+
+onload = function() {setTimeout(function() {
+ var iframe = document.getElementsByTagName("iframe")[0]
+
+ iframe.onload = function() {
+ setTimeout(function() {
+ iframe.contentWindow.location="001-2.html";
+ }, 100);
+ iframe.onload = t.step_func(function() {
+ assert_array_equals(events, ["pagehide", "unload"])
+ t.done()});
+ };
+
+ iframe.src = "004-1.html?" + Math.random();
+
+}, 100)};
+
+</script>
+<iframe></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/006-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/006-1.html
new file mode 100644
index 0000000000..bc2e10bdc3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/006-1.html
@@ -0,0 +1,23 @@
+<!doctype html>
+006-1
+<script>
+onpagehide = function() {
+ onpagehide = null;
+ setTimeout(function() {
+ parent.t.unreached_func('setTimeout survived navigatoin');
+ }, 1000);
+}
+if (parent.loaded) {
+ setTimeout(function() { parent.t.done(); }, 2000);
+}
+onload = function() {
+ if (!parent.loaded) {
+ parent.loaded = true;
+ setTimeout(parent.t.step_func(
+ function() {
+ location="006-2.html?" + Math.random();
+ }
+ ), 100);
+ }
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/006-2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/006-2.html
new file mode 100644
index 0000000000..52365e55d8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/006-2.html
@@ -0,0 +1,5 @@
+<!doctype html>
+006-2
+<script>
+onload = function() {setTimeout(parent.t.step_func(function() {history.go(-1)}), 100)}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/006.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/006.html
new file mode 100644
index 0000000000..c9e4d68a10
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/006.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<title>salvagable state of document after setting pagehide listener</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+
+var loaded = false;
+
+onload = function() {setTimeout(function() {
+ var iframe = document.getElementsByTagName("iframe")[0]
+ onload = null;
+ iframe.src="006-1.html?" + Math.random();
+}, 100)};
+
+</script>
+<iframe></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/007-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/007-1.html
new file mode 100644
index 0000000000..ed19f4498a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/007-1.html
@@ -0,0 +1,21 @@
+<!doctype html>
+007-1
+<script>
+onunload = function() {
+ onunload = null;
+ parent.unload_fired = true;
+ setTimeout(function() {
+ parent.timeout_fired = true;
+ }, 100);
+}
+onload = function() {
+ if (!parent.loaded) {
+ parent.loaded = true;
+ setTimeout(parent.t.step_func(
+ function() {
+ location="007-2.html?" + Math.random();
+ }
+ ), 100);
+ }
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/007-2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/007-2.html
new file mode 100644
index 0000000000..f74cd1e67e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/007-2.html
@@ -0,0 +1,5 @@
+<!doctype html>
+007-2
+<script>
+onload = function() {setTimeout(parent.t.step_func(function() {parent.start_test(); history.go(-1)}), 100)}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/007.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/007.html
new file mode 100644
index 0000000000..4a2fed5fac
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/007.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<title>salvagable state of document after setting unload listener</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+
+var loaded = false;
+var unload_fired = false;
+var timeout_fired = false;
+
+function start_test() {
+ step_timeout(t.step_func(function() {
+ assert_true(unload_fired);
+ assert_false(timeout_fired);
+ t.done()
+ }), 1000);
+}
+
+onload = function() {setTimeout(function() {
+ var iframe = document.getElementsByTagName("iframe")[0]
+ onload = null;
+ iframe.src="007-1.html?" + Math.random();
+}, 100)};
+
+</script>
+<iframe></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/008-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/008-1.html
new file mode 100644
index 0000000000..29de29c911
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/008-1.html
@@ -0,0 +1,4 @@
+<!doctype html>
+<script>
+onpagehide = parent.t.step_func(function() {parent.t.done()});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/008.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/008.html
new file mode 100644
index 0000000000..015507d817
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/008.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<title>pagehide IDL attribute</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var iframe;
+var t = async_test();
+onload = function() {
+ setTimeout(function() {
+ var iframe = document.getElementsByTagName("iframe")[0];
+ iframe.src="about:blank";
+ }, 100)
+};
+</script>
+<iframe src="008-1.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/009-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/009-1.html
new file mode 100644
index 0000000000..d69a05914a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/009-1.html
@@ -0,0 +1,4 @@
+<!doctype html>
+<script>
+onunload = parent.t.step_func(function() {parent.t.done()});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/009.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/009.html
new file mode 100644
index 0000000000..0e93e04701
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/009.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<title>unload IDL attribute</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var iframe;
+var t = async_test();
+onload = function() {
+ setTimeout(function() {
+ var iframe = document.getElementsByTagName("iframe")[0];
+ iframe.src="about:blank";
+ }, 100)
+}
+</script>
+<iframe src="009-1.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/pagehide-manual-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/pagehide-manual-1.html
new file mode 100644
index 0000000000..3da0a0de3e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/pagehide-manual-1.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<p>Now go back. PASS should be displayed after a short pause
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/pagehide-manual.html b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/pagehide-manual.html
new file mode 100644
index 0000000000..ba34c3087f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/pagehide-manual.html
@@ -0,0 +1,5 @@
+<!doctype html>
+<title>Document salvagable state after setting pagehide handler</title>
+<script>onpagehide = function() {setTimeout(function(){document.body.innerHTML = "PASS"}, 100)}</script>
+<p>Click the link below then navigate back to this page. Shortly after returning you should see the text "PASS"</p>
+<p><a href="pagehide-manual-1.html">Click here</a>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/unload-main-frame-cross-origin.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/unload-main-frame-cross-origin.window.js
new file mode 100644
index 0000000000..65b4e533f6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/unload-main-frame-cross-origin.window.js
@@ -0,0 +1,34 @@
+// META: title=Unload runs in main frame when navigating cross-origin.
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+
+ const rc1 = await rcHelper.addWindow();
+
+ t.add_cleanup(() => localStorage.removeItem('unload'));
+
+ // Initialize storage and add "unload" event handler.
+ await rc1.executeScript(() => {
+ localStorage.setItem('unload', 'not yet');
+ addEventListener('unload', () => {
+ localStorage.setItem('unload', 'ran');
+ });
+ });
+
+ // Navigate away.
+ const rc2 = await rc1.navigateToNew(
+ {extraRemoteContextConfig: {origin: 'HTTP_REMOTE_ORIGIN'}});
+
+ // Navigate back.
+ await rc2.historyBack();
+
+ // Test that the unload handler wrote to storage.
+ // Running it in the remote context after going back should ensure that the
+ // navigation (and therefore the unload handler) has completed.
+ assert_equals(
+ await rc1.executeScript(() => localStorage.getItem('unload')), 'ran');
+});
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/unload-main-frame-same-origin.window.js b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/unload-main-frame-same-origin.window.js
new file mode 100644
index 0000000000..5a95455c4c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/unloading-documents/unload/unload-main-frame-same-origin.window.js
@@ -0,0 +1,33 @@
+// META: title=Unload runs in main frame when navigating same-origin.
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+
+ const rc1 = await rcHelper.addWindow();
+
+ t.add_cleanup(() => localStorage.removeItem('unload'));
+
+ // Initialize storage and add "unload" event handler.
+ await rc1.executeScript(() => {
+ localStorage.setItem('unload', 'not yet');
+ addEventListener('unload', () => {
+ localStorage.setItem('unload', 'ran');
+ });
+ });
+
+ // Navigate away.
+ const rc2 = await rc1.navigateToNew();
+
+ // Navigate back.
+ await rc2.historyBack();
+
+ // Test that the unload handler wrote to storage.
+ // Running it in the remote context after going back should ensure that the
+ // navigation (and therefore the unload handler) has completed.
+ assert_equals(
+ await rc1.executeScript(() => localStorage.getItem('unload')), 'ran');
+});
diff --git a/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-child1.html b/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-child1.html
new file mode 100644
index 0000000000..22bb0b2980
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-child1.html
@@ -0,0 +1,18 @@
+<body>
+ <a id="link" href="joint-session-history-child2.html">Child1</a>.
+ <iframe id="grandchild"></iframe>
+</body>
+<script>
+ window.onload = function() {
+ var link = document.getElementById("link");
+ var grandchild = document.getElementById("grandchild");
+ var timer = window.setInterval(poll, 100);
+ function poll() {
+ if (grandchild.getAttribute("data-grandchild-loaded")) {
+ window.clearInterval(timer);
+ link.click();
+ }
+ }
+ grandchild.src="joint-session-history-grandchild1.html";
+ };
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-child2.html b/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-child2.html
new file mode 100644
index 0000000000..24b4695166
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-child2.html
@@ -0,0 +1,5 @@
+<body>Child 2.</body>
+<script>
+ // Servo doesn't support postMessage yet, so we poll on attributes.
+ window.frameElement.setAttribute("data-child-loaded", true);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-filler.html b/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-filler.html
new file mode 100644
index 0000000000..b6d47c310b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-filler.html
@@ -0,0 +1 @@
+<body>Filler</body>
diff --git a/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-grandchild1.html b/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-grandchild1.html
new file mode 100644
index 0000000000..d05e152425
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-grandchild1.html
@@ -0,0 +1,8 @@
+<body>
+ <a id="link" href="joint-session-history-grandchild2.html">Grandchild1</a>.
+</body>
+<script>
+ window.onload = function() {
+ document.getElementById("link").click();
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-grandchild2.html b/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-grandchild2.html
new file mode 100644
index 0000000000..b5c81e1fca
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-grandchild2.html
@@ -0,0 +1,5 @@
+<body>Grandchild2.</body>
+<script>
+ // Servo doesn't support postMessage yet, so we poll on attributes.
+ window.frameElement.setAttribute("data-grandchild-loaded", true);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-iframe-state.html b/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-iframe-state.html
new file mode 100644
index 0000000000..ffa64c0b35
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-iframe-state.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Joint session history should not override parent's state.</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<iframe id="frame" src="about:blank"></iframe>
+<script>
+async_test(function(t) {
+ // Setup.
+ var initialState = {foo: 'bar'};
+ var newStateFromChild = {foo: 'baz'};
+ var child = document.getElementById("frame");
+
+ // Child's initial state should be a default empty state.
+ assert_equals(child.contentWindow.history.state, null);
+
+ // Perform navigation in the top-level container to set the state.
+ window.history.pushState(initialState, 'title');
+
+ // Validate initial state was properly set.
+ assert_object_equals(window.history.state, initialState);
+
+ child.onload = t.step_func_done(() => {
+ // Child's initial state should be `null`.
+ assert_equals(child.contentWindow.history.state, null);
+
+ // Navigate in the child.
+ child.contentWindow.history.pushState(newStateFromChild, 'title');
+
+ // Child's state should now equal the new state.
+ assert_object_equals(child.contentWindow.history.state, newStateFromChild);
+ // Parent's state should still be preserved, having exactly the same state as it
+ // had before parent navigated.
+ assert_object_equals(window.history.state, initialState);
+ })
+ child.src = "joint-session-history-filler.html";
+});
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-only-fully-active.html b/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-only-fully-active.html
new file mode 100644
index 0000000000..c42d160a21
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-only-fully-active.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Do only fully active documents count for session history?</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+ <iframe id="child"></iframe>
+</body>
+<script>
+ async_test(function(t) {
+ var old_history_len = window.history.length;
+ var child = document.getElementById("child");
+ var timer = window.setInterval(t.step_func(poll), 100);
+ function poll() {
+ if (child.getAttribute("data-child-loaded")) {
+ // Check to see how many entries have been added to the session history.
+ // The spec https://html.spec.whatwg.org/multipage/#joint-session-history
+ // says that only fully active documents are included in the joint session history.
+ // If only fully active documents count, then the only fully active document
+ // is the child, with session length 1, so the joint session length change will be 1.
+ // If all documents count, then the grandchild is reachable via the session history,
+ // and it has session length 1, so the joint session length change will be 2.
+ assert_equals(2, window.history.length - old_history_len);
+ window.clearInterval(timer);
+ t.done();
+ }
+ }
+ child.src = "joint-session-history-child1.html";
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-remove-iframe.html b/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-remove-iframe.html
new file mode 100644
index 0000000000..ee7aa368af
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/joint-session-history/joint-session-history-remove-iframe.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Joint session history length does not include entries from a removed iframe.</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<iframe id="frame" src="about:blank"></iframe>
+<script>
+async_test(function(t) {
+ t.step_timeout(() => {
+ var child = document.getElementById("frame");
+ var old_history_len = history.length;
+ child.onload = () => {
+ assert_equals(old_history_len + 1, history.length);
+ document.body.removeChild(document.getElementById("frame"));
+ assert_equals(old_history_len, history.length);
+ t.done();
+ }
+ child.src = "joint-session-history-filler.html";
+ }, 1000);
+});
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/001.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/001.html
new file mode 100644
index 0000000000..16cb834c78
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/001.html
@@ -0,0 +1,339 @@
+<!doctype html>
+<html>
+ <head>
+ <title>history.pushState tests</title>
+ <script type="text/javascript" src="/resources/testharness.js"></script>
+ <script type="text/javascript" src="/resources/testharnessreport.js"></script>
+ <script type="text/javascript">
+//does not test for firing of popstate onload, because this was dropped from the specification on 25 March 2011
+//covers history.state after load, in accordance with the specification draft from 25 March 2011
+//history.state before load is tested in 006 and 007
+//does not test for structured cloning of FileList, File or Blob interfaces, as these require manual file selection
+
+//**This test assumes that assignments to location.hash will be synchronous - this is how all browsers implement it.
+//The spec (as of 25 March 2011) disagrees.**//
+
+var histlength, atstep = 0, lasttimer;
+setup({explicit_done:true}); //tests should take under 6 seconds + execution time
+
+window.onload = function () {
+ if( location.protocol == 'file:' ) {
+ document.getElementsByTagName('p')[0].innerHTML = 'ERROR: This test cannot be run from file: (URL resolving will not work). It must be loaded over HTTP.';
+ return;
+ } else if( location.protocol == 'https:' ) {
+ document.getElementsByTagName('p')[0].innerHTML += '<br>WARNING: Browsers may intentionally fail to update history.length when pages are loaded over HTTPS, as a privacy restriction. If possible, load this page over HTTP.';
+ }
+ //use a timeout, because some browsers intentionally do not add history entries for URL changes in the onload thread
+ setTimeout(testinit,100);
+};
+function testinit() {
+ atstep = 1;
+ histlength = history.length;
+ iframe = document.getElementsByTagName('iframe')[0].src = 'blank2.html';
+ //reportload will now be called by the onload handler for the iframe
+}
+function reportload() {
+ var iframe = document.getElementsByTagName('iframe')[0], hashchng = false;
+ var canvassup = false, cloneobj;
+
+ function tests1() {
+ //Firefox may fail when reloading, because it recovers iframe state, and therefore does not see the need to alter history length
+ test(function () { assert_equals( history.length, histlength + 1, 'make sure that you loaded the test in a new tab/window' ); }, 'history.length should update when loading pages in an iframe');
+ histlength = history.length;
+ iframe.contentWindow.location.hash = 'test'; //should be synchronous **SEE COMMENT AT TOP OF FILE
+ test(function () {
+ assert_equals( history.length, histlength + 1, 'make sure that you loaded the test in a new tab/window' );
+ }, 'history.length should update when setting location.hash');
+ test(function () { assert_true( !!history.pushState, 'critical test; ignore any failures after this' ); }, 'history.pushState must exist'); //assert_own_property does not allow prototype inheritance
+ test(function () { assert_true( !!iframe.contentWindow.history.pushState, 'critical test; ignore any failures after this' ); }, 'history.pushState must exist within iframes');
+ test(function () {
+ assert_equals( iframe.contentWindow.history.state, null );
+ }, 'initial history.state should be null');
+ test(function () {
+ histlength = history.length;
+ iframe.contentWindow.history.pushState('','');
+ assert_equals( history.length, histlength + 1 );
+ }, 'history.length should update when pushing a state');
+ test(function () {
+ assert_equals( iframe.contentWindow.history.state, '' );
+ }, 'history.state should update after a state is pushed');
+ histlength = history.length;
+ iframe.contentWindow.addEventListener("popstate", tests2, {once: true});
+ history.back();
+ }
+ function tests2() {
+ test(function () {
+ assert_equals( history.length, histlength );
+ }, 'history.length should not decrease after going back');
+ test(function () {
+ assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test' );
+ }, 'traversing history must traverse pushed states');
+ iframe.contentWindow.addEventListener("hashchange", tests3, {once: true});
+ history.go(-1);
+ }
+ function tests3() {
+ test(function () {
+ assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), '', '(this could cause other failures later on)' );
+ }, 'traversing history must also traverse hash changes');
+
+ iframe.contentWindow.addEventListener("hashchange", tests4, {once: true});
+ //Safari 5.0.3 fails here - it navigates *this* document to the *iframe's* location, instead of just navigating the iframe
+ history.go(2);
+ }
+ async function tests4() {
+ test(function () {
+ //Firefox 4 beta 11 has a messed up error object, which does not have the right error type or .SECURITY_ERR property
+ assert_throws_dom('SECURITY_ERR',function () { history.pushState('','','//exa mple'); });
+ }, 'pushState must not be allowed to create invalid URLs');
+ test(function () {
+ assert_throws_dom('SECURITY_ERR',function () { history.pushState('','','http://www.example.com/'); });
+ }, 'pushState must not be allowed to create cross-origin URLs');
+ test(function () {
+ assert_throws_dom('SECURITY_ERR',function () { history.pushState('','','about:blank'); });
+ }, 'pushState must not be allowed to create cross-origin URLs (about:blank)');
+ test(function () {
+ assert_throws_dom('SECURITY_ERR',function () { history.pushState('','','data:text/html,'); });
+ }, 'pushState must not be allowed to create cross-origin URLs (data:URI)');
+ test(function () {
+ assert_throws_dom('SECURITY_ERR', iframe.contentWindow.DOMException, function () { iframe.contentWindow.history.pushState('','','http://www.example.com/'); });
+ }, 'security errors are expected to be thrown in the context of the document that owns the history object');
+ let hashchange =
+ new Promise(function (resolve) {
+ iframe.contentWindow.addEventListener("hashchange", resolve, {once: true});
+ });
+
+ test(function () {
+ iframe.contentWindow.location.hash = 'test2';
+ assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test2', 'location.hash did not change when told to' );
+ }, 'location.hash must be allowed to change (part 1)');
+ await hashchange;
+ history.go(-1);
+ iframe.contentWindow.addEventListener("hashchange", tests5, {once: true});
+ }
+ function tests5() {
+ test(function () {
+ assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test', 'location.hash did not change when going back' );
+ }, 'location.hash must be allowed to change (part 2)');
+ test(function () {
+ iframe.contentWindow.history.pushState('','');
+ assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test', 'location.hash changed when an unrelated state was pushed' );
+ }, 'pushState must not alter location.hash when no URL is provided');
+ history.go(1); //should do nothing, since the pushState should have removed the forward history
+ setTimeout(tests6,50); //.go is queued to end of thread
+ }
+ function tests6() {
+ test(function () {
+ assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test' );
+ }, 'pushState must remove all history after the current state');
+ test(function () {
+ iframe.contentWindow.history.pushState('','','#test3');
+ assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test3' );
+ }, 'pushState must be able to set location.hash');
+ //begin setup for "remove any tasks queued by the history traversal task source"
+ iframe.contentWindow.location.hash = '#test4';
+ iframe.contentWindow.history.go(-1); //must be queued
+ try {
+ //must remove the queued navigation in the same browsing context
+ iframe.contentWindow.history.pushState('','');
+ } catch(unsuperr) {}
+ //allow the browser to mistakenly run the .go if it is going to
+ //do not put two .go commands in the same thread, in case the browser mistakenly calculates the history position when
+ //calling .go instead of when executing the traversal task - that could give a false PASS in the next test otherwise
+ setTimeout(tests7,50);
+ }
+ function tests7() {
+ iframe.contentWindow.history.go(-1); //must be queued, but should not be removed this time
+ iframe.contentWindow.addEventListener("popstate", tests8, {once: true});
+ }
+ function tests8() {
+ test(function () {
+ assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test4' );
+ }, 'pushState must remove any tasks queued by the history traversal task source');
+ //end "remove any tasks queued by the history traversal task source"
+ window.addEventListener('hashchange',function () { hashchng = true; },false);
+ try {
+ //push a state that changes the hash
+ iframe.contentWindow.history.pushState('','',iframe.contentWindow.location.pathname+'#test5');
+ } catch(unsuperr) {}
+ setTimeout(tests9,50); //allow the hashchange event to process, if the browser has mistakenly fired it
+ }
+ function tests9() {
+ test(function () {
+ assert_false( hashchng );
+ }, 'pushState must not fire hashchange events');
+ test(function () {
+ iframe.contentWindow.history.pushState('','','/testing_ignore_me_404');
+ assert_equals( iframe.contentWindow.location.pathname, '/testing_ignore_me_404' );
+ }, 'pushState must be able to set location.pathname');
+ test(function () {
+ var newURL = location.href.replace(/\/[^\/]*$/)+'/testing_ignore_me_404/';
+ iframe.contentWindow.history.pushState('','',newURL);
+ assert_equals( iframe.contentWindow.location.href, newURL );
+ }, 'pushState must be able to set absolute URLs to the same host');
+ test(function () {
+ assert_throws_dom( 'DATA_CLONE_ERR', function () {
+ history.pushState({dummy:function () {}},'');
+ } );
+ }, 'pushState must not be able to use a function as data');
+ test(function () {
+ assert_throws_dom( 'DATA_CLONE_ERR', function () {
+ history.pushState({dummy:window},'');
+ } );
+ }, 'pushState must not be able to use a DOM node as data');
+ test(function () {
+ try { a.b = c; } catch(errdata) {
+ history.pushState({dummy:errdata},'');
+ assert_equals(ReferenceError.prototype, Object.getPrototypeOf(history.state.dummy));
+ }
+ }, 'pushState must be able to use an error object as data');
+ test(function () {
+ assert_throws_dom('DATA_CLONE_ERR', iframe.contentWindow.DOMException, function () {
+ iframe.contentWindow.history.pushState(document,'');
+ });
+ }, 'security errors are expected to be thrown in the context of the document that owns the history object (2)');
+ cloneobj = {
+ nulldata: null,
+ udefdata: window.undefined,
+ booldata: true,
+ numdata: 1,
+ strdata: 'string data',
+ boolobj: new Boolean(true),
+ numobj: new Number(1),
+ strobj: new String('string data'),
+ datedata: new Date(),
+ regdata: /a/g,
+ arrdata: [1]
+ };
+ cloneobj.regdata.lastIndex = 1;
+ cloneobj.looped = cloneobj;
+ //test the ImageData type, if the browser supports it
+ var canvas = document.createElement('canvas');
+ if( canvas.getContext && ( canvas = canvas.getContext('2d') ) && canvas.createImageData ) {
+ canvassup = true;
+ cloneobj.imgdata = canvas.createImageData(1,1);
+ }
+ test(function () {
+ try {
+ iframe.contentWindow.history.pushState(cloneobj,'new title');
+ } catch(e) {
+ cloneobj.looped = null;
+ //try again because this object is needed for future tests
+ iframe.contentWindow.history.pushState(cloneobj,'new title');
+ //rethrow so the browser gets a FAIL for not coping with the circular reference; "internal structured cloning algorithm" step 1
+ throw(e);
+ }
+ }, 'pushState must be able to make structured clones of complex objects');
+ test(function () {
+ assert_equals( iframe.contentWindow.history.state && iframe.contentWindow.history.state.strdata, 'string data' );
+ }, 'history.state should also reference a clone of the original object');
+ test(function () {
+ assert_not_equals( cloneobj, iframe.contentWindow.history.state );
+ }, 'history.state should be a clone of the original object, not a reference to it');
+ /*
+ behaviour is not defined per spec, and no known implementations do this
+ test(function () {
+ assert_equals( iframe.contentDocument.title, 'new title', 'not required for specification conformance' );
+ }, 'pushState MIGHT set the document title');
+ */
+ history.go(-1);
+ iframe.contentWindow.addEventListener("popstate", tests10, {once: true});
+ }
+ function tests10() {
+ var eventtime = setTimeout(function () { tests11(false); },500); //should be cleared by the event handler long before it has a chance to fire
+ iframe.contentWindow.addEventListener('popstate',function (e) { clearTimeout(eventtime); tests11(true,e); },false);
+ history.forward();
+ }
+ function tests11(hasFired,ev) {
+ test(function () {
+ assert_true( hasFired );
+ }, 'popstate event should fire when navigation occurs');
+ test(function () {
+ assert_true( !!ev && typeof(ev.state) != 'undefined', 'state information was not passed' );
+ assert_true( !!ev.state, 'state information does not contain the expected value - browser is probably stuck in the wrong history position' );
+ assert_equals( ev.state.nulldata, null, 'state null data was not correct' );
+ assert_equals( ev.state.udefdata, window.undefined, 'state undefined data was not correct' );
+ assert_true( ev.state.booldata, 'state boolean data was not correct' );
+ assert_equals( ev.state.numdata, 1, 'state numeric data was not correct' );
+ assert_equals( ev.state.strdata, 'string data', 'state string data was not correct' );
+ assert_true( !!ev.state.datedata.getTime, 'state date data was not correct' );
+ assert_own_property( ev.state, 'regdata', 'state regex data was not correct' );
+ assert_equals( ev.state.regdata.source, 'a', 'state regex pattern data was not correct' );
+ assert_true( ev.state.regdata.global, 'state regex flag data was not correct' );
+ assert_equals( ev.state.regdata.lastIndex, 0, 'state regex lastIndex data was not correct' );
+ assert_equals( ev.state.arrdata.length, 1, 'state array data was not correct' );
+ assert_true( ev.state.boolobj.valueOf(), 'state boolean data was not correct' );
+ assert_equals( ev.state.numobj.valueOf(), 1, 'state numeric data was not correct' );
+ assert_equals( ev.state.strobj.valueOf(), 'string data', 'state string data was not correct' );
+ if( canvassup ) {
+ assert_equals( ev.state.imgdata.width, 1, 'state ImageData was not correct' );
+ }
+ }, 'popstate event should pass the state data');
+ test(function () {
+ assert_equals( ev.state.looped, ev.state );
+ }, 'state data should cope with circular object references');
+ test(function () {
+ assert_not_equals( cloneobj, ev.state );
+ }, 'state data should be a clone of the original object, not a reference to it');
+ test(function () {
+ assert_equals( iframe.contentWindow.history.state && iframe.contentWindow.history.state.strdata, 'string data' );
+ }, 'history.state should also reference a clone of the original object (2)');
+ test(function () {
+ assert_not_equals( cloneobj, iframe.contentWindow.history.state );
+ }, 'history.state should be a clone of the original object, not a reference to it (2)');
+ test(function () {
+ assert_equals( iframe.contentWindow.history.state, ev.state );
+ }, 'history.state should be identical to the object passed to the event handler unless history.state is updated');
+ try {
+ iframe.contentWindow.persistval = true;
+ iframe.contentWindow.history.pushState('','', location.href.replace(/\/[^\/]*$/,'/blank3.html') );
+ } catch(unsuperr) {}
+ //it's already cached, so this should be very fast if the browser mistakenly loads it
+ //it should not need to load at all, since it's just a pushed state
+ setTimeout(tests12,1000);
+ }
+ function tests12() {
+ test(function () {
+ assert_true( iframe.contentWindow.persistval && !iframe.contentWindow.forreal );
+ }, 'pushState should not actually load the new URL');
+ atstep = 3;
+ iframe.contentWindow.location.reload(); //load the real URL
+ lasttimer = setTimeout(function () { tests13(false); },3000); //should be cleared by the onload handler long before it has a chance to fire
+ }
+ function tests13(passed) {
+ test(function () {
+ assert_true( passed, 'expected a load event to fire when reloading the URL from cache, gave up waiting after 3 seconds' );
+ }, 'reloading a pushed state should actually load the new URL');
+ //try to make browsers behave when reloading so that the correct URL is recovered - does not always work
+ iframe.contentWindow.location.href = location.href.replace(/\/[^\/]*$/,'/blank.html');
+ done();
+ }
+
+ if( atstep == 1 ) {
+ //blank2 has loaded
+ atstep = 2;
+ //use a timeout, because some browsers intentionally do not add history entries for URL changes in an onload thread
+ setTimeout(tests1,100);
+ } else if( atstep == 3 ) {
+ //blank3 should now have loaded after the .reload() command
+ atstep = 4;
+ clearTimeout(lasttimer);
+ tests13(true);
+ }
+}
+
+
+
+ </script>
+ </head>
+ <body>
+
+ <noscript><p>Enable JavaScript and reload</p></noscript>
+ <p>WARNING: This test should always be loaded in a new tab/window, to avoid browsers attempting to recover the state of frames, and history length. Do not reload the test.</p>
+ <div id="log">Running test...</div>
+ <p><iframe onload="reportload();" src="blank.html"></iframe></p>
+ <p><iframe src="blank.html"></iframe></p>
+ <p><iframe src="blank2.html"></iframe></p>
+ <p><iframe src="blank3.html"></iframe></p>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/002.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/002.html
new file mode 100644
index 0000000000..8bce7e72ff
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/002.html
@@ -0,0 +1,318 @@
+<!doctype html>
+<html>
+ <head>
+ <title>history.replaceState tests</title>
+ <script type="text/javascript" src="/resources/testharness.js"></script>
+ <script type="text/javascript" src="/resources/testharnessreport.js"></script>
+ <script type="text/javascript">
+//does not test for firing of popstate onload, because this was dropped from the specification on 25 March 2011
+//covers history.state after load, in accordance with the specification draft from 25 March 2011
+//history.state before load is tested in 006 and 007
+//does not test for structured cloning of FileList, File or Blob interfaces, as these require manual file selection
+
+//**This test assumes that assignments to location.hash will be synchronous - this is how all browsers implement it.
+//The spec (as of 25 March 2011) disagrees.
+
+var histlength, atstep = 0, lasttimer;
+setup({explicit_done:true}); //tests should take under 6 seconds + execution time
+
+window.onload = function () {
+ if( location.protocol == 'file:' ) {
+ document.getElementsByTagName('p')[0].innerHTML = 'ERROR: This test cannot be run from file: (URL resolving will not work). It must be loaded over HTTP.';
+ return;
+ } else if( location.protocol == 'https:' ) {
+ document.getElementsByTagName('p')[0].innerHTML += '<br>WARNING: Browsers may intentionally fail to update history.length when pages are loaded over HTTPS, as a privacy restriction. If possible, load this page over HTTP.';
+ }
+ //use a timeout, because some browsers intentionally do not add history entries for URL changes in the onload thread
+ setTimeout(testinit,100);
+};
+function testinit() {
+ atstep = 1;
+ histlength = history.length;
+ iframe = document.getElementsByTagName('iframe')[0].src = 'blank2.html';
+ //reportload will now be called by the onload handler for the iframe
+}
+function reportload() {
+ var iframe = document.getElementsByTagName('iframe')[0], hashchng = false;
+ var canvassup = false, cloneobj;
+
+ async function tests1() {
+ test(function () { assert_equals( history.length, histlength + 1, 'make sure that you loaded the test in a new tab/window' ); }, 'history.length should update when loading pages in an iframe');
+ histlength = history.length;
+ let hashchange = new Promise(function(resolve) {
+ iframe.contentWindow.addEventListener("hashchange", resolve, {once: true});
+ });
+ iframe.contentWindow.location.hash = 'test'; //should be synchronous **SEE COMMENT AT TOP OF FILE
+ test(function () {
+ assert_equals( history.length, histlength + 1, 'make sure that you loaded the test in a new tab/window' );
+ }, 'history.length should update when setting location.hash');
+ test(function () { assert_true( !!history.replaceState, 'critical test; ignore any failures after this' ); }, 'history.replaceState must exist'); //assert_own_property does not allow prototype inheritance
+ test(function () { assert_true( !!iframe.contentWindow.history.replaceState, 'critical test; ignore any failures after this' ); }, 'history.replaceState must exist within iframes');
+ test(function () {
+ assert_equals( iframe.contentWindow.history.state, null );
+ }, 'initial history.state should be null');
+
+ await hashchange;
+ hashchange = new Promise(function(resolve) {
+ iframe.contentWindow.addEventListener("hashchange", resolve, {once: true});
+ });
+ iframe.contentWindow.location.hash = 'test2';
+ await hashchange;
+
+ iframe.contentWindow.addEventListener("hashchange", tests2, {once: true});
+ history.back();
+ }
+ function tests2() {
+ test(function () {
+ histlength = history.length;
+ iframe.contentWindow.history.replaceState('','');
+ assert_equals( history.length, histlength );
+ }, 'history.length should not update when replacing a state with no URL');
+ test(function () {
+ assert_equals( iframe.contentWindow.history.state, '' );
+ }, 'history.state should update after a state is pushed');
+ test(function () {
+ assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test' );
+ }, 'hash should not change when replaceState is called without a URL');
+ test(function () {
+ histlength = history.length;
+ iframe.contentWindow.history.replaceState('','','#test3');
+ assert_equals( history.length, histlength );
+ }, 'history.length should not update when replacing a state with a URL');
+ test(function () {
+ assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test3' );
+ }, 'hash should change when replaceState is called with a URL');
+ iframe.contentWindow.addEventListener("hashchange", tests3, {once: true});
+ history.go(-1);
+ }
+ function tests3() {
+ test(function () {
+ assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), '' );
+ }, 'replaceState must replace the existing state and not add an extra one');
+ iframe.contentWindow.addEventListener("hashchange", tests4, {once: true});
+ history.go(2);
+ }
+ function tests4() {
+ test(function () {
+ assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test2' );
+ }, 'replaceState must replace the existing state without altering the forward history');
+ test(function () {
+ assert_throws_dom('SECURITY_ERR',function () { history.replaceState('','','//exa mple'); });
+ }, 'replaceState must not be allowed to create invalid URLs');
+ test(function () {
+ assert_throws_dom('SECURITY_ERR',function () { history.replaceState('','','http://www.example.com/'); });
+ }, 'replaceState must not be allowed to create cross-origin URLs');
+ test(function () {
+ assert_throws_dom('SECURITY_ERR',function () { history.replaceState('','','about:blank'); });
+ }, 'replaceState must not be allowed to create cross-origin URLs (about:blank)');
+ test(function () {
+ assert_throws_dom('SECURITY_ERR',function () { history.replaceState('','','data:text/html,'); });
+ }, 'replaceState must not be allowed to create cross-origin URLs (data:URI)');
+ test(function () {
+ assert_throws_dom('SECURITY_ERR',iframe.contentWindow.DOMException,function () { iframe.contentWindow.history.replaceState('','','http://www.example.com/'); });
+ }, 'security errors are expected to be thrown in the context of the document that owns the history object');
+ test(function () {
+ //avoids browsers running .go synchronously when only a hash change is involved
+ iframe.contentWindow.history.replaceState('','','/testing_ignore_me_404#test4');
+ assert_equals( iframe.contentWindow.location.pathname, '/testing_ignore_me_404' );
+ }, 'replaceState must be able to set location.pathname');
+ test(function () {
+ var newURL = location.href.replace(/\/[^\/]*$/)+'/testing_ignore_me_404/';
+ iframe.contentWindow.history.replaceState('','',newURL);
+ assert_equals( iframe.contentWindow.location.href, newURL );
+ }, 'replaceState must be able to set absolute URLs to the same host');
+
+ //allow the browser to run the .go
+ iframe.contentWindow.addEventListener("popstate", tests5, {once: true});
+ //begin setup for "[must not] remove any tasks queued by the history traversal task source"
+ iframe.contentWindow.history.go(-1); //must be queued so the next command takes place *beforehand*
+ try {
+ //must not remove the queued navigation in the same browsing context
+ iframe.contentWindow.history.replaceState('','',iframe.contentWindow.location.pathname+'#test5');
+ } catch(unsuperr2) {}
+ }
+ function tests5() {
+ test(function () {
+ assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test3' );
+ }, 'replaceState must not remove any tasks queued by the history traversal task source');
+ iframe.contentWindow.addEventListener("popstate", tests6, {once: true});
+ //Safari 5.0.3 fails here - it navigates *this* document to the *iframe's* location, instead of just navigating the iframe
+ history.go(1);
+ }
+ function tests6() {
+ test(function () {
+ assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test5' );
+ }, '.go must queue a task with the history traversal task source (run asynchronously)');
+ //end "[must not] remove any tasks queued by the history traversal task source"
+ window.addEventListener('hashchange',function () { hashchng = true; },false);
+ try {
+ //push a state that changes the hash
+ iframe.contentWindow.history.replaceState('','',iframe.contentWindow.location.pathname+'#test6');
+ } catch(unsuperr) {}
+ setTimeout(tests7,50); //allow the hashchange event to process, if the browser has mistakenly fired it
+ }
+ function tests7() {
+ test(function () {
+ assert_false( hashchng );
+ }, 'replaceState must not fire hashchange events');
+ test(function () {
+ assert_throws_dom( 'DATA_CLONE_ERR', function () {
+ history.replaceState({dummy:function () {}},'');
+ } );
+ }, 'replaceState must not be able to use a function as data');
+ test(function () {
+ assert_throws_dom( 'DATA_CLONE_ERR', function () {
+ history.replaceState({dummy:window},'');
+ } );
+ }, 'replaceState must not be able to use a DOM node as data');
+ test(function () {
+ try { a.b = c; } catch(errdata) {
+ history.replaceState({dummy:errdata},'');
+ assert_equals(ReferenceError.prototype, Object.getPrototypeOf(history.state.dummy));
+ }
+ }, 'replaceState must be able to use an error object as data');
+ test(function () {
+ assert_throws_dom('DATA_CLONE_ERR', iframe.contentWindow.DOMException, function () {
+ iframe.contentWindow.history.replaceState(document,'');
+ });
+ }, 'security errors are expected to be thrown in the context of the document that owns the history object (2)');
+ cloneobj = {
+ nulldata: null,
+ udefdata: window.undefined,
+ booldata: true,
+ numdata: 1,
+ strdata: 'string data',
+ boolobj: new Boolean(true),
+ numobj: new Number(1),
+ strobj: new String('string data'),
+ datedata: new Date(),
+ regdata: /a/g,
+ arrdata: [1]
+ };
+ cloneobj.regdata.lastIndex = 1;
+ cloneobj.looped = cloneobj;
+ //test the ImageData type, if the browser supports it
+ var canvas = document.createElement('canvas');
+ if( canvas.getContext && ( canvas = canvas.getContext('2d') ) && canvas.createImageData ) {
+ canvassup = true;
+ cloneobj.imgdata = canvas.createImageData(1,1);
+ }
+ test(function () {
+ try {
+ iframe.contentWindow.history.replaceState(cloneobj,'new title');
+ } catch(e) {
+ cloneobj.looped = null;
+ //try again because this object is needed for future tests
+ iframe.contentWindow.history.replaceState(cloneobj,'new title');
+ //rethrow so the browser gets a FAIL for not coping with the circular reference; "internal structured cloning algorithm" step 1
+ throw(e);
+ }
+ }, 'replaceState must be able to make structured clones of complex objects');
+ test(function () {
+ assert_equals( iframe.contentWindow.history.state && iframe.contentWindow.history.state.strdata, 'string data' );
+ }, 'history.state should also reference a clone of the original object');
+ test(function () {
+ assert_not_equals( cloneobj, iframe.contentWindow.history.state );
+ }, 'history.state should be a clone of the original object, not a reference to it');
+ iframe.contentWindow.addEventListener("popstate", tests8, {once: true});
+ history.go(-1);
+ }
+ function tests8() {
+ var eventtime = setTimeout(function () { tests9(false); },500); //should be cleared by the event handler long before it has a chance to fire
+ iframe.contentWindow.addEventListener('popstate',function (e) { clearTimeout(eventtime); tests9(true,e); },false);
+ history.forward();
+ }
+ function tests9(hasFired,ev) {
+ test(function () {
+ assert_true( hasFired );
+ }, 'popstate event should fire when navigation occurs');
+ test(function () {
+ assert_true( !!ev && typeof(ev.state) != 'undefined', 'state information was not passed' );
+ assert_true( !!ev.state, 'state information does not contain the expected value - browser is probably stuck in the wrong history position' );
+ assert_equals( ev.state.nulldata, null, 'state null data was not correct' );
+ assert_equals( ev.state.udefdata, window.undefined, 'state undefined data was not correct' );
+ assert_true( ev.state.booldata, 'state boolean data was not correct' );
+ assert_equals( ev.state.numdata, 1, 'state numeric data was not correct' );
+ assert_equals( ev.state.strdata, 'string data', 'state string data was not correct' );
+ assert_true( !!ev.state.datedata.getTime, 'state date data was not correct' );
+ assert_own_property( ev.state, 'regdata', 'state regex data was not correct' );
+ assert_equals( ev.state.regdata.source, 'a', 'state regex pattern data was not correct' );
+ assert_true( ev.state.regdata.global, 'state regex flag data was not correct' );
+ assert_equals( ev.state.regdata.lastIndex, 0, 'state regex lastIndex data was not correct' );
+ assert_equals( ev.state.arrdata.length, 1, 'state array data was not correct' );
+ assert_true( ev.state.boolobj.valueOf(), 'state boolean data was not correct' );
+ assert_equals( ev.state.numobj.valueOf(), 1, 'state numeric data was not correct' );
+ assert_equals( ev.state.strobj.valueOf(), 'string data', 'state string data was not correct' );
+ if( canvassup ) {
+ assert_equals( ev.state.imgdata.width, 1, 'state ImageData was not correct' );
+ }
+ }, 'popstate event should pass the state data');
+ test(function () {
+ assert_equals( ev.state.looped, ev.state );
+ }, 'state data should cope with circular object references');
+ test(function () {
+ assert_not_equals( cloneobj, ev.state );
+ }, 'state data should be a clone of the original object, not a reference to it');
+ test(function () {
+ assert_equals( iframe.contentWindow.history.state && iframe.contentWindow.history.state.strdata, 'string data' );
+ }, 'history.state should also reference a clone of the original object (2)');
+ test(function () {
+ assert_not_equals( cloneobj, iframe.contentWindow.history.state );
+ }, 'history.state should be a clone of the original object, not a reference to it (2)');
+ test(function () {
+ assert_equals( iframe.contentWindow.history.state, ev.state );
+ }, 'history.state should be identical to the object passed to the event handler unless history.state is updated');
+ try {
+ iframe.contentWindow.persistval = true;
+ iframe.contentWindow.history.replaceState('','', location.href.replace(/\/[^\/]*$/,'/blank3.html') );
+ } catch(unsuperr) {}
+ //it's already cached, so this should be very fast if the browser mistakenly loads it
+ //it should not need to load at all, since it's just a pushed state
+ setTimeout(tests10,1000);
+ }
+ function tests10() {
+ test(function () {
+ assert_true( iframe.contentWindow.persistval && !iframe.contentWindow.forreal );
+ }, 'replaceState should not actually load the new URL');
+ atstep = 3;
+ iframe.contentWindow.location.reload(); //load the real URL
+ lasttimer = setTimeout(function () { tests11(false); },3000); //should be cleared by the onload handler long before it has a chance to fire
+ }
+ function tests11(passed) {
+ test(function () {
+ assert_true( passed, 'expected a load event to fire when reloading the URL from cache, gave up waiting after 3 seconds' );
+ }, 'reloading a replaced state should actually load the new URL');
+ //try to make browsers behave when reloading so that the correct URL is recovered - does not always work
+ iframe.contentWindow.location.href = location.href.replace(/\/[^\/]*$/,'/blank.html');
+ done();
+ }
+
+ if( atstep == 1 ) {
+ //blank2 has loaded
+ atstep = 2;
+ //use a timeout, because some browsers intentionally do not add history entries for URL changes in an onload thread
+ setTimeout(tests1,100);
+ } else if( atstep == 3 ) {
+ //blank3 should now have loaded after the .reload() command
+ atstep = 4;
+ clearTimeout(lasttimer);
+ tests11(true);
+ }
+}
+
+
+
+ </script>
+ </head>
+ <body>
+
+ <noscript><p>Enable JavaScript and reload</p></noscript>
+ <p>WARNING: This test should always be loaded in a new tab/window, to avoid browsers attempting to recover the state of frames, and history length. Do not reload the test.</p>
+ <div id="log">Running test...</div>
+ <p><iframe onload="reportload();" src="blank.html"></iframe></p>
+ <p><iframe src="blank.html"></iframe></p>
+ <p><iframe src="blank2.html"></iframe></p>
+ <p><iframe src="blank3.html"></iframe></p>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/004.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/004.html
new file mode 100644
index 0000000000..e69889724f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/004.html
@@ -0,0 +1,62 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Final history position for history.go should be calculated when executing the task</title>
+ <script type="text/javascript" src="/resources/testharness.js"></script>
+ <script type="text/javascript" src="/resources/testharnessreport.js"></script>
+ <script type="text/javascript">
+setup({explicit_done:true});
+window.onload = function () {
+ var hashcount = 0;
+ if( location.hash && location.hash != '#' ) {
+ location.href = location.href.replace(/#.*$/,'');
+ return;
+ }
+ setTimeout(add1,100);
+ function add1() {
+ location.hash = '#foo';
+ setTimeout(add2,100);
+ }
+ function add2() {
+ location.hash = '#bar';
+ setTimeout(add3,100);
+ }
+ function add3() {
+ location.hash = '#baz';
+ setTimeout(dojumps,100);
+ }
+ function dojumps() {
+ window.onhashchange = function () {
+ hashcount++;
+ };
+ history.go(-2);
+ test(function () {
+ //many browsers special-case jumps that only imply hash changes and will do them synchronously - the spec does allow this
+ assert_equals( hashcount, 0, 'hashchange fired even though the location should not have changed' );
+ assert_equals( location.hash.replace(/^#/,''), 'baz', 'the browser navigated synchronously' );
+ }, '.go commands should be queued until the thread has ended');
+ history.go(-1);
+ setTimeout(checkjumps,100);
+ }
+ function checkjumps() {
+ test(function () {
+ assert_true( !!hashcount, 'this testcase requires haschange support; the test cannot be used in this browser' );
+ }, 'browser needs to support hashchange events for this testcase');
+ test(function () {
+ assert_equals( hashcount, 2, 'the wrong number of queued commands were executed' );
+ }, 'queued .go commands should all be executed when the queue is processed');
+ test(function () {
+ assert_equals( location.hash.replace(/^#/,''), '' );
+ }, 'history position should be calculated when executing, not when calling the .go command');
+ done();
+ }
+};
+ </script>
+ </head>
+ <body>
+
+ <noscript><p>Enable JavaScript and reload</p></noscript>
+ <div id="log"></div>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/005.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/005.html
new file mode 100644
index 0000000000..2152e85a3e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/005.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Popstate event listener registration</title>
+ <script type="text/javascript" src="/resources/testharness.js"></script>
+ <script type="text/javascript" src="/resources/testharnessreport.js"></script>
+ <script type="text/javascript">
+
+//this test checks that onpopstate works on the body element
+
+var readyForPop = false, bodypop = false, inlinepop = false;
+setup({explicit_done:true});
+
+//use a timeout to avoid "popstate fires onload" from setting the variables too early
+setTimeout(step1,1000);
+function step1() {
+ readyForPop = true;
+ test(function () {
+ history.pushState('','');
+ history.pushState('','');
+ }, 'history.pushState support is needed for this testcase');
+ history.go(-1);
+ setTimeout(step2,50); //.go is queued to end of thread
+}
+function step2() {
+ test(function () {
+ assert_true( bodypop );
+ }, '<body onpopstate="..."> should register a listener for the popstate event');
+ window.onpopstate = function () { inlinepop = true; };
+ history.go(-1);
+ setTimeout(step3,50); //.go is queued to end of thread
+}
+function step3() {
+ test(function () {
+ assert_true( inlinepop );
+ }, 'window.onpopstate should register a listener for the popstate event');
+ done();
+}
+ </script>
+ </head>
+ <body onpopstate="if( readyForPop ) { bodypop = true; }">
+
+ <noscript><p>Enable JavaScript and reload</p></noscript>
+ <div id="log"></div>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/006.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/006.html
new file mode 100644
index 0000000000..442b6f8f1e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/006.html
@@ -0,0 +1,53 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Firing popstate after onload, even if there is no pushed/replaced state</title>
+ <script type="text/javascript" src="/resources/testharness.js"></script>
+ <script type="text/javascript" src="/resources/testharnessreport.js"></script>
+ <script type="text/javascript">
+
+//spec (25 March 2011 draft) states that popstate must not fire after onload unless there is a pushed/replaced state that is navigated
+var popfired = false;
+setup({explicit_done:true});
+window.addEventListener('popstate',function (e) { popfired = true; },false);
+test(function () {
+ assert_equals( history.state, null );
+}, 'history.state should initially be null');
+window.onload = function () {
+ test(function () {
+ assert_false( popfired );
+ }, 'popstate event should not fire before onload fires');
+ test(function () {
+ assert_equals( history.state, null );
+ }, 'history.state should still be null onload');
+ popfired = false;
+ setTimeout(function () {
+ test(function () {
+ assert_false( popfired );
+ }, 'popstate event should not fire after onload fires');
+ test(function () {
+ assert_equals( history.state, null );
+ }, 'history.state should still be null after onload');
+ test(function () {
+ var failed = false, realstate = history.state;
+ try {
+ history.state = '';
+ } catch(e) {
+ failed = e;
+ }
+ assert_equals(history.state,realstate,'property was read/write');
+ assert_false(failed);
+ }, 'writing to history.state should be silently ignored and not throw an error');
+ done();
+ },100);
+};
+
+ </script>
+ </head>
+ <body>
+
+ <noscript><p>Enable JavaScript and reload</p></noscript>
+ <div id="log"></div>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/007.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/007.html
new file mode 100644
index 0000000000..decb197624
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/007.html
@@ -0,0 +1,66 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Firing popstate after onload with pushed state</title>
+ <meta name=timeout content=long>
+ <script type="text/javascript" src="/resources/testharness.js"></script>
+ <script type="text/javascript" src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+
+ <noscript><p>Enable JavaScript and reload</p></noscript>
+ <div id="log">It looks like the browser stopped loading the page when encountering a .go(-1) command pointing to a pushed state. This will break the tests.</div>
+ <script type="text/javascript">
+
+//spec (25 March 2011 draft) states that popstate must fire before onload if there is a pushed/replaced state that is navigated
+var popfired = false;
+setup({explicit_done:true});
+test(function () {
+ assert_equals( history.state, null );
+}, 'history.state should initially be null');
+window.addEventListener('popstate',function (e) { popfired = e.state; },false);
+test(function () {
+ history.pushState('state1','');
+ history.pushState('state2','');
+}, 'history.pushState support is needed for this testcase');
+test(function () {
+ assert_equals( history.state, 'state2' );
+}, 'history.state should reflect pushed state');
+if( history.pushState ) { history.go(-1); }
+window.onload = function () {
+ test(function () {
+ assert_true( !!popfired );
+ }, 'popstate event should fire before onload fires');
+ test(function () {
+ assert_equals( popfired, 'state1' );
+ }, 'the correct state should be restored when navigating during initial load');
+ test(function () {
+ assert_equals( history.state, 'state1' );
+ }, 'history.state should reflect the navigated state onload');
+ popfired = false;
+ setTimeout(function () {
+ test(function () {
+ assert_false( !!popfired );
+ }, 'popstate event should not fire after onload fires');
+ test(function () {
+ assert_equals( history.state, 'state1' );
+ }, 'history.state should reflect the navigated state after onload');
+ done();
+ if( history.pushState ) { history.go(-1); } //go back to the start to avoid state recovery when reloading
+ },100);
+};
+
+ </script>
+
+ <!--
+ Reuse an existing server side script to slow down page load so that
+ history.go(-1); above gets run before load event fires.
+ -->
+ <script>
+ // define TEST_DELAY so that executing delay.py doesn't warn about use
+ // of undefined variable.
+ var TEST_DELAY;
+ </script>
+ <script src="/xhr/resources/delay.py?ms=2500"></script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/008.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/008.html
new file mode 100644
index 0000000000..c8071e3156
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/008.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<html>
+
+<!-- configure this test below to point to the script -->
+
+ <head>
+ <title>history.pushState/replaceState resolving</title>
+ <script type="text/javascript" src="/resources/testharness.js"></script>
+ <script type="text/javascript" src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+
+ <p></p>
+ <noscript><p>Enable JavaScript and reload</p></noscript>
+ <div id="log"></div>
+ <script type="text/javascript">
+
+/*
+Location of the script (which must be hosted on a separate domain from this test) containing the test code:
+var beforehref = location.href;
+test(function () {
+ history.pushState('','','/testing_ignore_me_404');
+ assert_equals(location.href,beforehref.replace(/^(\w*:\/\/[^\/]*\/)[\w\W]*$/,'$1testing_ignore_me_404'));
+}, 'history.pushState URL resolving should be done relative to the document, not the script');
+test(function () {
+ history.replaceState('','','/testing_ignore_me_404_2');
+ assert_equals(location.href,beforehref.replace(/^(\w*:\/\/[^\/]*\/)[\w\W]*$/,'$1testing_ignore_me_404_2'));
+}, 'history.replaceState URL resolving should be done relative to the document, not the script');
+*/
+var scriptlocation = 'http://www.' + location.host + location.pathname.split("/").slice(0,-1).join("/") + "/008.js";
+
+if( location.protocol == 'file:' ) {
+ document.getElementsByTagName('p')[0].innerHTML = 'ERROR: This test cannot be run from file: (URL resolving will not work). It must be loaded over HTTP.';
+} else {
+ document.write('<script type="text\/javascript" src="'+scriptlocation+'"><\/script>');
+}
+ </script>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/008.js b/testing/web-platform/tests/html/browsers/history/the-history-interface/008.js
new file mode 100644
index 0000000000..96a1fe5d4a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/008.js
@@ -0,0 +1,11 @@
+var beforehref = location.href;
+
+test(function () {
+ history.pushState('','','/testing_ignore_me_404');
+ assert_equals(location.href,beforehref.replace(/^(\w*:\/\/[^\/]*\/)[\w\W]*$/,'$1testing_ignore_me_404'));
+}, 'history.pushState URL resolving should be done relative to the document, not the script');
+
+test(function () {
+ history.replaceState('','','/testing_ignore_me_404_2');
+ assert_equals(location.href,beforehref.replace(/^(\w*:\/\/[^\/]*\/)[\w\W]*$/,'$1testing_ignore_me_404_2'));
+}, 'history.replaceState URL resolving should be done relative to the document, not the script');
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/009-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/009-1.html
new file mode 100644
index 0000000000..00b72e8ec6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/009-1.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+ <head>
+ <title>history.pushState/replaceState and referer headers</title>
+ </head>
+ <body>
+
+ <noscript><p>Enable JavaScript and reload</p></noscript>
+ <div id="log"></div>
+ <script type="text/javascript">
+window.onload = function () {
+ setTimeout(function () {
+ try { history.pushState('','','009-2.html?1234'); } catch(e) {}
+ location.href = '009-3.html?pipe=sub';
+ },10);
+};
+ </script>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/009-3.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/009-3.html
new file mode 100644
index 0000000000..e58b8fa5e7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/009-3.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<html>
+ <head>
+ <title>history.pushState/replaceState and referer headers</title>
+ </head>
+ <body>
+
+ <noscript><p>Enable JavaScript and reload</p></noscript>
+ <div id="log"></div>
+ <script type="text/javascript">
+var httpReferer = "{{headers[referer]}}";
+var lastUrl = location.href.replace(/\/[^\/]*$/,'\/009-2.html?1234');
+parent.test(function () {
+ parent.assert_equals( httpReferer, lastUrl );
+}, 'HTTP Referer should use the pushed state');
+parent.test(function () {
+ parent.assert_equals( document.referrer, lastUrl );
+}, 'document.referrer should use the pushed state');
+window.onload = function () {
+ setTimeout(function () {
+ try { history.pushState('','','009-4.html?2345'); } catch(e) {}
+ location.href = '009-5.html?pipe=sub';
+ },10);
+};
+ </script>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/009-5.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/009-5.html
new file mode 100644
index 0000000000..068a089af4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/009-5.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+ <head>
+ <title>history.pushState/replaceState and referer headers</title>
+ </head>
+ <body>
+
+ <noscript><p>Enable JavaScript and reload</p></noscript>
+ <div id="log"></div>
+ <script type="text/javascript">
+var httpReferer = unescape("{{headers[referer]}}");
+var lastUrl = location.href.replace(/\/[^\/]*$/,'\/009-4.html?2345');
+parent.test(function () {
+ parent.assert_equals( httpReferer, lastUrl );
+}, 'HTTP Referer should use the replaced state');
+parent.test(function () {
+ parent.assert_equals( document.referrer, lastUrl );
+}, 'document.referrer should use the replaced state');
+parent.done();
+ </script>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/009.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/009.html
new file mode 100644
index 0000000000..c1ae0bbe03
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/009.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html>
+ <head>
+ <title>history.pushState/replaceState and referer headers</title>
+ <script type="text/javascript" src="/resources/testharness.js"></script>
+ <script type="text/javascript" src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+
+ <noscript><p>Enable JavaScript and reload</p></noscript>
+ <div id="log"></div>
+ <script type="text/javascript">
+setup({explicit_done:true});
+var iframe = document.createElement('iframe');
+window.onload = function () {
+ iframe.setAttribute('src','009-1.html');
+ document.body.appendChild(iframe)
+};
+ </script>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/010-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/010-1.html
new file mode 100644
index 0000000000..683397745c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/010-1.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+ <head>
+ <title>history.pushState/replaceState and referer headers (before onload)</title>
+ </head>
+ <body>
+
+ <noscript><p>Enable JavaScript and reload</p></noscript>
+ <div id="log"></div>
+ <script type="text/javascript">
+try { history.pushState('','','010-2.html?1234'); } catch(e) {}
+location.href = '010-3.html?pipe=sub';
+ </script>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/010-3.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/010-3.html
new file mode 100644
index 0000000000..b80f56c3dd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/010-3.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html>
+ <head>
+ <title>history.pushState/replaceState and referer headers (before onload)</title>
+ </head>
+ <body>
+
+ <noscript><p>Enable JavaScript and reload</p></noscript>
+ <div id="log"></div>
+ <script type="text/javascript">
+ var httpReferer = "{{headers[referer]}}";
+var lastUrl = location.href.replace(/\/[^\/]*$/,'\/010-2.html?1234');
+parent.test(function () {
+ parent.assert_equals( httpReferer, lastUrl );
+}, 'HTTP Referer should use the pushed state (before onload)');
+parent.test(function () {
+ parent.assert_equals( document.referrer, lastUrl );
+}, 'document.referrer should use the pushed state (before onload)');
+try { history.pushState('','','010-4.html?2345'); } catch(e) {}
+location.href = '010-5.html?pipe=sub';
+ </script>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/010-5.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/010-5.html
new file mode 100644
index 0000000000..d150449eb2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/010-5.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+ <head>
+ <title>history.pushState/replaceState and referer headers (before onload)</title>
+ </head>
+ <body>
+
+ <noscript><p>Enable JavaScript and reload</p></noscript>
+ <div id="log"></div>
+ <script type="text/javascript">
+var httpReferer = "{{headers[referer]}}";
+var lastUrl = location.href.replace(/\/[^\/]*$/,'\/010-4.html?2345');
+parent.test(function () {
+ parent.assert_equals( httpReferer, lastUrl );
+}, 'HTTP Referer should use the replaced state (before onload)');
+parent.test(function () {
+ parent.assert_equals( document.referrer, lastUrl );
+}, 'document.referrer should use the replaced state (before onload)');
+parent.done();
+ </script>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/010.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/010.html
new file mode 100644
index 0000000000..ca109a744b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/010.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html>
+ <head>
+ <title>history.pushState/replaceState and referer headers (before onload)</title>
+ <script type="text/javascript" src="/resources/testharness.js"></script>
+ <script type="text/javascript" src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+
+ <noscript><p>Enable JavaScript and reload</p></noscript>
+ <div id="log"></div>
+ <script type="text/javascript">
+setup({explicit_done:true});
+var iframe = document.createElement('iframe');
+window.onload = function () {
+ iframe.setAttribute('src','010-1.html');
+ document.body.appendChild(iframe)
+};
+ </script>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/011.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/011.html
new file mode 100644
index 0000000000..4043aff7fd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/011.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html>
+ <head>
+ <title>history.pushState before onload</title>
+ <script type="text/javascript" src="/resources/testharness.js"></script>
+ <script type="text/javascript" src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+
+ <noscript><p>Enable JavaScript and reload</p></noscript>
+ <div id="log"></div>
+ <script type="text/javascript">
+var newUrl = location.href.replace(/\/[^\/]*$/,'\/011-1.html');
+setup({explicit_done:true});
+test(function () {
+ history.pushState('','','011-1.html');
+}, 'pushState should be able to set the location state');
+test(function () {
+ assert_equals( location.href, newUrl );
+}, 'pushed location should be reflected immediately');
+window.onload = function () {
+ setTimeout(function () {
+ test(function () {
+ assert_equals( location.href, newUrl );
+ }, 'pushed location should be retained after the page has loaded');
+ done();
+ },10);
+};
+ </script>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/012.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/012.html
new file mode 100644
index 0000000000..f5e6251671
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/012.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html>
+ <head>
+ <title>history.replaceState before onload</title>
+ <script type="text/javascript" src="/resources/testharness.js"></script>
+ <script type="text/javascript" src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+
+ <noscript><p>Enable JavaScript and reload</p></noscript>
+ <div id="log"></div>
+ <script type="text/javascript">
+var newUrl = location.href.replace(/\/[^\/]*$/,'\/011-1.html');
+setup({explicit_done:true});
+test(function () {
+ history.replaceState('','','011-1.html');
+}, 'replaceState should be able to set the location state');
+test(function () {
+ assert_equals( location.href, newUrl );
+}, 'replaced location should be reflected immediately');
+window.onload = function () {
+ setTimeout(function () {
+ test(function () {
+ assert_equals( location.href, newUrl );
+ }, 'replaced location should be retained after the page has loaded');
+ done();
+ },10);
+};
+ </script>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/blank-new.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/blank-new.html
new file mode 100644
index 0000000000..2a545af0ed
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/blank-new.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<head>
+ <title>New page</title>
+</head>
+<body>This is a new page.</body>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/blank-old.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/blank-old.html
new file mode 100644
index 0000000000..a77c00fcc6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/blank-old.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<head>
+ <title>Old page</title>
+</head>
+<body>This is an old page.</body>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/blank.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/blank.html
new file mode 100644
index 0000000000..89c8724c09
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/blank.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Dummy page 1</title>
+ </head>
+ <body>
+ </body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/blank2.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/blank2.html
new file mode 100644
index 0000000000..f79982e328
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/blank2.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Dummy page 2</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+if( self == top || !parent.reportload ) {
+ document.write("<p>FAIL. Browser got confused when navigating forwards, and navigated the whole window to the iframe's location, instead of just navigating the iframe. It is not possible to run the testsuite.<\/p>");
+}
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/blank3.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/blank3.html
new file mode 100644
index 0000000000..2a8989f272
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/blank3.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Dummy page 3</title>
+ <script type="text/javascript">
+var forreal = true;
+ </script>
+ </head>
+ <body>
+ </body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_001.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_001.html
new file mode 100644
index 0000000000..21ba22f6fe
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_001.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>combination_history_001(Combine pushState and replaceSate methods.)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function () {
+ window.history.pushState(1, document.title, '?x=1');
+ assert_equals(history.state, 1, "first");
+
+ window.history.replaceState(2, document.title, '?x=1');
+ assert_equals(history.state, 2, "second")
+ }, "Combine pushState and replaceSate methods");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_002.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_002.html
new file mode 100644
index 0000000000..29e82f51be
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_002.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>combination_history_002(After calling of pushState method, check length.)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function () {
+ var first;
+ var second;
+ first = window.history.length;
+ window.history.pushState(1, document.title, '?x=1');
+ second = window.history.length;
+
+ assert_equals(second - first, 1, "second - first");
+ }, "After calling of pushState method, check length");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_003.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_003.html
new file mode 100644
index 0000000000..7467d9b294
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_003.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>combination_history_003(After calling of pushState and replaceState methods, check length.)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function () {
+ var first;
+ var second;
+ var third;
+ first = window.history.length;
+ window.history.pushState(1, document.title, '?x=1');
+ second = window.history.length;
+ window.history.replaceState(2, document.title, '?x=2');
+ third = window.history.length;
+
+ assert_equals(second - first, 1, "second - first");
+ assert_equals(third, second, "third");
+ }, "After calling of pushState and replaceState methods, check length");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_004.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_004.html
new file mode 100644
index 0000000000..4e38b56205
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_004.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>combination_history_004(After calling of back method, check length.)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var t = async_test("After calling of back method, check length");
+
+ var last;
+ t.step(function () {
+ window.history.pushState(1, document.title, '?x=1');
+ window.history.pushState(2, document.title, '?x=2');
+ last = window.history.length;
+
+ window.history.back();
+ });
+
+ window.addEventListener('popstate', t.step_func(function(e) {
+ assert_equals(e.state, 1, "state");
+ assert_equals(window.history.length, last, "last");
+ t.done();
+ }), false);
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_005.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_005.html
new file mode 100644
index 0000000000..4487678015
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_005.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>combination_history_005(After calling of forward method, check length.)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var t = async_test("After calling of forward method, check length");
+
+ var last;
+ var fired = false;
+ t.step(function () {
+ window.history.pushState(1, document.title, '?x=1');
+ window.history.pushState(2, document.title, '?x=2');
+ last = window.history.length;
+
+ window.history.back();
+ });
+
+ window.addEventListener('popstate', t.step_func(function(e) {
+ if(fired) {
+ assert_equals(e.state, 2, "state");
+ assert_equals(window.history.length, last, "last");
+ t.done();
+ }
+ fired = true;
+ window.history.forward();
+ }), false);
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_006.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_006.html
new file mode 100644
index 0000000000..305f593c09
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_006.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>combination_history_006(After calling of go method, check length.)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var t = async_test("After calling of go method, check length");
+
+ var last;
+ t.step(function () {
+ window.history.pushState(1, document.title, '?x=1');
+ window.history.pushState(2, document.title, '?x=2');
+
+ last = window.history.length;
+
+ window.history.go(-1);
+ });
+
+ window.addEventListener('popstate', t.step_func(function(e) {
+ assert_equals(e.state, 1, "state");
+ assert_equals(window.history.length, last, "last");
+ t.done();
+ }), false);
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_007.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_007.html
new file mode 100644
index 0000000000..cec9ea0981
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/combination_history_007.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>combination_history_007(After calling of back and pushState method, check length.)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var t = async_test("After calling of back and pushState method, check length");
+
+ var last;
+ t.step(function () {
+ window.history.pushState(1, document.title, '?x=1');
+ window.history.pushState(2, document.title, '?x=2');
+
+ last = window.history.length;
+
+ window.history.back();
+ });
+
+ window.addEventListener('popstate', t.step_func(function(e) {
+ assert_equals(e.state, 1, "state");
+ assert_equals(window.history.length, last, "last");
+ window.history.pushState(3, document.title, '?x=3');
+ assert_equals(window.history.length, last, "last");
+ t.done();
+ }), false);
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history-associated-with-document.window.js b/testing/web-platform/tests/html/browsers/history/the-history-interface/history-associated-with-document.window.js
new file mode 100644
index 0000000000..94c1b2cf6b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history-associated-with-document.window.js
@@ -0,0 +1,6 @@
+// META: title=the History object must be associated with the Document object, not the Window object
+// META: script=/common/object-association.js
+
+// See https://github.com/whatwg/html/issues/2566.
+
+testIsPerDocument("history");
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history-state-after-bfcache.window.js b/testing/web-platform/tests/html/browsers/history/the-history-interface/history-state-after-bfcache.window.js
new file mode 100644
index 0000000000..5f04890a72
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history-state-after-bfcache.window.js
@@ -0,0 +1,43 @@
+// META: title=Navigating back to a bfcached page does not reset history.state
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+
+// See https://github.com/whatwg/html/issues/6652.
+
+'use strict';
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+
+ // Open a window with noopener so that BFCache will work.
+ const rc = await rcHelper.addWindow(null, { features: "noopener" });
+
+ // Add a pageshow listener to stash the event, and set history.state using replaceState().
+ await rc.executeScript(() => {
+ window.addEventListener('pageshow', (event) => {
+ window.pageshowEvent = event;
+ });
+
+ history.replaceState({ foo: 'bar' }, '', '');
+ window.stashedHistoryState = history.state;
+ });
+
+ const rc2 = await rc.navigateToNew();
+ await rc2.historyBack();
+
+ assert_implements_optional(
+ await rc.executeScript(() => window.pageshowEvent.persisted),
+ 'precondition: document was bfcached'
+ );
+
+ assert_equals(
+ await rc.executeScript(() => history.state.foo),
+ 'bar',
+ 'history.state was restored correctly'
+ );
+
+ assert_true(
+ await rc.executeScript(() => window.stashedHistoryState === history.state),
+ 'history.state did not get serialized and deserialized');
+});
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history.js b/testing/web-platform/tests/html/browsers/history/the-history-interface/history.js
new file mode 100644
index 0000000000..bb5ee6dde0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history.js
@@ -0,0 +1,35 @@
+function parse_query() {
+ var query = location.search.slice(1);
+ var vars = query.split("&");
+ var fields = {};
+ vars.forEach(
+ function (x) {
+ var split = x.split("=");
+ return fields[split[0]] = split.slice(1).join("=");
+ });
+ return fields;
+}
+
+var query_parts = parse_query();
+var id = "id" in query_parts ? parseInt(query_parts.id) : 1;
+var urls_to_load = query_parts.urls.split(",");
+
+document.write(id);
+
+onunload = function() {};
+
+function queue_next() {
+ t = opener.t;
+ setTimeout(t.step_func(
+ function() {
+// opener.assert_equals(history.length, id);
+ if (urls_to_load[0]) {
+ var next_page = urls_to_load[0];
+ (next_page.indexOf("?") > -1) ? (next_page += "&") : (next_page += "?");
+ next_page += "urls=" + urls_to_load.slice(1).join(",");
+ next_page += "&id=" + ++id;
+ location = next_page;
+ }
+ }
+ ), 100);
+}
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_back-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_back-1.html
new file mode 100644
index 0000000000..5487786fc0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_back-1.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<script src="history.js"></script>
+<script>
+ onunload = function() {}
+
+ opener.pages.push(id);
+ if (!opener.started) {
+ onload = function() {
+ setTimeout(function() {
+ opener.started = true;
+ history.back();
+ }, 100);
+ }
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_back.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_back.html
new file mode 100644
index 0000000000..042da4e61b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_back.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>history_back</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var t = async_test("history back");
+
+ t.step(function () {
+ window.history.pushState(1, document.title, '?x=1');
+ window.history.pushState(2, document.title, '?x=2');
+
+ window.history.back();
+ });
+
+ window.addEventListener('popstate', t.step_func(function(e) {
+ assert_equals(e.state, 1, "history state");
+
+ t.done();
+ }), false);
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_back_1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_back_1.html
new file mode 100644
index 0000000000..0af3206643
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_back_1.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<title>history.back() with session history</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ var t = async_test();
+ started = false;
+ pages = []
+ timer = setInterval(
+ function() {
+ if (pages.length < 2)
+ return;
+ clearInterval(timer);
+ setTimeout(t.step_func(
+ function() {
+ assert_array_equals(pages, [2, 1], "Pages opened during history navigation");
+ t.done();
+ }
+ ), 500);
+ }, 50);
+ t.step(function() {
+ win = window.open("history_entry.html?urls=history_back-1.html");
+ t.add_cleanup(function() { win.close(); });
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_back_cross_realm_method.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_back_cross_realm_method.html
new file mode 100644
index 0000000000..47937ef583
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_back_cross_realm_method.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>history.back() uses this's associated document's browsing context to determine if navigation is allowed</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/history.html#dom-history-back">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe id="sandboxedIframe" srcdoc="hello" sandbox="allow-scripts allow-same-origin"></iframe>
+<script>
+const t = async_test();
+
+t.step(() => {
+ history.pushState({}, null, "?prev");
+ history.pushState({}, null, "?current");
+
+ sandboxedIframe.contentWindow.history.back.call(history);
+});
+
+window.onpopstate = t.step_func_done(() => {
+ assert_equals(location.search, "?prev");
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_entry.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_entry.html
new file mode 100644
index 0000000000..308371c57d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_entry.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<script src="history.js"></script>
+<script>
+ onload = function() {
+ if (!opener.started) {
+ queue_next();
+ } else {
+ opener.pages.push(id);
+ }
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward-1.html
new file mode 100644
index 0000000000..1bf7d6ee3b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward-1.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<script src="history.js"></script>
+<script>
+ onunload = function() {}
+
+ onload = function() {
+ if (!opener.started) {
+ queue_next();
+ } else {
+ opener.pages.push(id);
+ history.forward();
+ }
+ };
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward-2.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward-2.html
new file mode 100644
index 0000000000..cf8a8d20cd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward-2.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<script src="history.js"></script>
+<script>
+ onunload = function() {}
+
+ opener.pages.push(id);
+ if (!opener.started) {
+ onload = function() {
+ setTimeout(function() {
+ opener.started = true;
+ history.go(-1);
+ }, 100);
+ }
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward.html
new file mode 100644
index 0000000000..6c37f25215
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>history_forward</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var t = async_test("history forward");
+
+ var fired = false;
+ t.step(function () {
+ window.history.pushState(1, document.title, '?x=1');
+ window.history.pushState(2, document.title, '?x=2');
+
+ window.history.back();
+ });
+
+ window.addEventListener('popstate', t.step_func(function(e) {
+ if(fired) {
+ assert_equals(e.state, 2, "history state");
+
+ t.done();
+ }
+ fired = true;
+ window.history.forward();
+ }), false);
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward_1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward_1.html
new file mode 100644
index 0000000000..7817649f3a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward_1.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<title>history.forward() with session history</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ var t = async_test();
+ started = false;
+ pages = []
+ timer = setInterval(
+ function() {
+ if (pages.length < 3)
+ return;
+ clearInterval(timer);
+ setTimeout(t.step_func(
+ function() {
+ assert_array_equals(pages, [3, 2, 3], "Pages opened during history navigation");
+ t.done();
+ }
+ ), 500);
+ }, 50);
+ t.step(function() {
+ win = window.open("history_entry.html?urls=history_forward-1.html,history_forward-2.html");
+ t.add_cleanup(function() { win.close(); });
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward_cross_realm_method.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward_cross_realm_method.html
new file mode 100644
index 0000000000..7456099b8f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_forward_cross_realm_method.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>history.forward() uses this's associated document's browsing context to determine if navigation is allowed</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/history.html#dom-history-forward">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe id="sandboxedIframe" srcdoc="hello" sandbox="allow-scripts allow-same-origin"></iframe>
+<script>
+const t = async_test();
+
+t.step(() => {
+ history.pushState({}, null, "?prev");
+ history.pushState({}, null, "?current");
+ history.back();
+});
+
+let isCrossRealmForward = false;
+window.onpopstate = t.step_func(() => {
+ if (isCrossRealmForward) {
+ assert_equals(location.search, "?current");
+ t.done();
+ } else {
+ sandboxedIframe.contentWindow.history.forward.call(history);
+ isCrossRealmForward = true;
+ }
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_cross_realm_method.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_cross_realm_method.html
new file mode 100644
index 0000000000..d8852b9f6f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_cross_realm_method.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>history.go() uses this's associated document's browsing context to determine if navigation is allowed</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/history.html#dom-history-go">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe id="sandboxedIframe" srcdoc="hello" sandbox="allow-scripts allow-same-origin"></iframe>
+<script>
+const t = async_test();
+
+t.step(() => {
+ history.pushState({}, null, "?prev=2");
+ history.pushState({}, null, "?prev=1");
+ history.pushState({}, null, "?current");
+
+ sandboxedIframe.contentWindow.history.go.call(history, -2);
+});
+
+window.onpopstate = t.step_func_done(() => {
+ assert_equals(location.search, "?prev=2");
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_minus.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_minus.html
new file mode 100644
index 0000000000..b8fe75573d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_minus.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>history_go_minus</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var t = async_test("history go minus");
+
+ t.step(function () {
+ window.history.pushState(1, document.title, '?x=1');
+ window.history.pushState(2, document.title, '?x=2');
+
+ window.history.go(-1);
+ });
+
+ window.addEventListener('popstate', t.step_func(function(e) {
+ assert_equals(e.state, 1, "history state");
+
+ t.done();
+ }), false);
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_no_argument-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_no_argument-1.html
new file mode 100644
index 0000000000..6a2212f34b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_no_argument-1.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<script src="history.js"></script>
+<script>
+ onunload = function() {}
+
+ onload = function() {
+ if (!opener.started) {
+ queue_next();
+ } else {
+ opener.pages.push(id);
+ if (!opener.gone) {
+ history.go();
+ opener.gone = true;
+ }
+ }
+ };
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_no_argument.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_no_argument.html
new file mode 100644
index 0000000000..11f41614bd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_no_argument.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<title>history.go()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ var t = async_test();
+ started = false;
+ gone = false;
+ pages = []
+ timer = setInterval(
+ function() {
+ if (pages.length < 3)
+ return;
+ clearInterval(timer);
+ setTimeout(t.step_func(
+ function() {
+ assert_array_equals(pages, [3, 2, 2], "Pages opened during history navigation");
+ t.done();
+ }
+ ), 500);
+ }, 50);
+ t.step(function() {
+ win = window.open("history_entry.html?urls=history_go_no_argument-1.html,history_forward-2.html");
+ t.add_cleanup(function() { win.close(); });
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_plus.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_plus.html
new file mode 100644
index 0000000000..74d4c588ca
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_plus.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>history_go_plus</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var t = async_test("history go plus");
+
+ var fired = false;
+ t.step(function () {
+ window.history.pushState(1, document.title, '?x=1');
+ window.history.pushState(2, document.title, '?x=2');
+
+ window.history.back();
+ });
+
+ window.addEventListener('popstate', t.step_func(function(e) {
+ if(fired) {
+ assert_equals(e.state, 2, "history state");
+
+ t.done();
+ }
+ fired = true;
+ window.history.go(1);
+
+ }), false);
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_to_uri-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_to_uri-1.html
new file mode 100644
index 0000000000..49249ff000
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_to_uri-1.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<script src="history.js"></script>
+<script>
+ onunload = function() {}
+
+ onload = function() {
+ if (!opener.started) {
+ queue_next();
+ } else {
+ opener.pages.push(id);
+ if (!opener.gone) {
+ // This is meant to test that passing a string is not supported.
+ // According to the spec, the value passed to 'go' must be an int.
+ // Internet Explorer supports passing a string and will navigate
+ // to that Url. This test will protect against regressing in
+ // this area and reverting back to IE's incorrect behavior.
+ history.go("history_entry.html");
+ opener.gone = true;
+ }
+ }
+ };
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_to_uri.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_to_uri.html
new file mode 100644
index 0000000000..e0a891a5a2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_to_uri.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<title>history.go() negative tests</title>
+<link rel="author" title="John Jansen" href="mailto:johnjan@microsoft.com">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/browsers.html#dom-history-go">
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ var t = async_test();
+ started = false;
+ gone = false;
+ pages = []
+ timer = setInterval(
+ function() {
+ if (pages.length < 3)
+ return;
+ clearInterval(timer);
+ setTimeout(t.step_func(
+ function() {
+ assert_array_equals(pages, [3, 2, 2], "Pages opened during history navigation");
+ t.done();
+ }
+ ), 500);
+ }, 50);
+ t.step(function() {
+ win = window.open("history_entry.html?urls=history_go_to_uri-1.html,history_forward-2.html");
+ t.add_cleanup(function() { win.close(); });
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_undefined-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_undefined-1.html
new file mode 100644
index 0000000000..1bf7d6ee3b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_undefined-1.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<script src="history.js"></script>
+<script>
+ onunload = function() {}
+
+ onload = function() {
+ if (!opener.started) {
+ queue_next();
+ } else {
+ opener.pages.push(id);
+ history.forward();
+ }
+ };
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_undefined.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_undefined.html
new file mode 100644
index 0000000000..ddc0f588b5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_undefined.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<title>history.forward() with session history</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ var t = async_test();
+ started = false;
+ pages = []
+ timer = setInterval(
+ function() {
+ if (pages.length < 3)
+ return;
+ clearInterval(timer);
+ setTimeout(t.step_func(
+ function() {
+ assert_array_equals(pages, [3, 2, 2], "Pages opened during history navigation");
+ t.done();
+ }
+ ), 500);
+ }, 50);
+ t.step(function() {
+ win = window.open("history_entry.html?urls=history_go_undefined-1.html,history_forward-2.html");
+ t.add_cleanup(function() { win.close(); });
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_zero-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_zero-1.html
new file mode 100644
index 0000000000..17e520a810
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_zero-1.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<script src="history.js"></script>
+<script>
+ onunload = function() {}
+
+ onload = function() {
+ if (!opener.started) {
+ queue_next();
+ } else {
+ opener.pages.push(id);
+ if (!opener.gone) {
+ history.go(0);
+ opener.gone = true;
+ }
+ }
+ };
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_zero.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_zero.html
new file mode 100644
index 0000000000..b071b580c3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_zero.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<title>history.go(0)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ var t = async_test();
+ started = false;
+ gone = false;
+ pages = []
+ timer = setInterval(
+ function() {
+ if (pages.length < 3)
+ return;
+ clearInterval(timer);
+ setTimeout(t.step_func(
+ function() {
+ assert_array_equals(pages, [3, 2, 2], "Pages opened during history navigation");
+ t.done();
+ }
+ ), 500);
+ }, 50);
+ t.step(function() {
+ win = window.open("history_entry.html?urls=history_go_zero-1.html,history_forward-2.html");
+ t.add_cleanup(function() { win.close(); });
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_zero_which_document.window.js b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_zero_which_document.window.js
new file mode 100644
index 0000000000..f697783f3e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_go_zero_which_document.window.js
@@ -0,0 +1,27 @@
+// META: title=history.go(0) on an iframe must reload the iframe's document, not the parent document
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+
+promise_test(async () => {
+ const rcHelper = new RemoteContextHelper();
+ const main = await rcHelper.addWindow();
+ await main.addIframe();
+
+ await main.executeScript(() => {
+ window.didNotGetReloaded = true;
+
+ const iframe = document.querySelector("iframe");
+
+ // This goes beyond the original test case in https://github.com/whatwg/html/issues/2436, and
+ // tests where current realm != relevant realm. The spec says to use relevant realm so the
+ // result is still, iframe must reload, not parent.
+ History.prototype.go.call(iframe.contentWindow.history, 0);
+
+ return new Promise(resolve => {
+ iframe.addEventListener("load", resolve);
+ });
+ });
+
+ assert_true(await main.executeScript(() => window.didNotGetReloaded));
+});
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_properties_only_fully_active.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_properties_only_fully_active.html
new file mode 100644
index 0000000000..2b2c308526
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_properties_only_fully_active.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<title>history properties should throw SecurityError when not in a fully active Document</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+ <iframe id="child"></iframe>
+</body>
+<script>
+ test(function(t) {
+ var ifr = document.getElementById("child");
+ var cached_history = ifr.contentWindow.history;
+ var cached_DOMException = ifr.contentWindow.DOMException;
+ ifr.remove();
+ assert_throws_dom("SecurityError", cached_DOMException, function() { cached_history.length; });
+ assert_throws_dom("SecurityError", cached_DOMException, function() { cached_history.scrollRestoration; });
+ assert_throws_dom("SecurityError", cached_DOMException, function() { cached_history.state; });
+ assert_throws_dom("SecurityError", cached_DOMException, function() { cached_history.go(0); });
+ assert_throws_dom("SecurityError", cached_DOMException, function() { cached_history.back(); });
+ assert_throws_dom("SecurityError", cached_DOMException, function() { cached_history.forward(); });
+ assert_throws_dom("SecurityError", cached_DOMException, function() { cached_history.pushState(1, document.title, "?x=1"); });
+ assert_throws_dom("SecurityError", cached_DOMException, function() { cached_history.replaceState(2, document.title, "?x=2"); });
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate.html
new file mode 100644
index 0000000000..5180a3f6e5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>history_pushState</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function () {
+ window.history.pushState(1, document.title, '?x=1');
+ var state;
+ state = window.history.state;
+ assert_equals(state, 1, "history state");
+ }, "history pushState");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate_err.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate_err.html
new file mode 100644
index 0000000000..6fa0a8589d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate_err.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>history_pushState SECURITY_ERR</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function () {
+ assert_throws_dom("SecurityError", function () {
+ window.history.pushState(1, document.title, 'http://www.microsoft.com/test.html');
+ });
+ }, "history pushState SECURITY_ERR");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate_nooptionalparam.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate_nooptionalparam.html
new file mode 100644
index 0000000000..8e4b049a19
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate_nooptionalparam.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>history_pushState_NoOptionalParam</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function () {
+ window.history.pushState(1, document.title);
+
+ var state;
+ state = window.history.state;
+ assert_equals(state, 1, "history state");
+ }, "history pushState NoOptionalParam");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate_url.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate_url.html
new file mode 100644
index 0000000000..cfbca35bfd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate_url.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>History pushState sets the url</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+async_test(function(t) {
+ var oldLocation = window.location.toString();
+ window.history.pushState(null, "", "#hash");
+ assert_equals(oldLocation + "#hash", window.location.toString(), "pushState updates url");
+ history.back();
+ window.onhashchange = () => {
+ assert_equals(oldLocation, window.location.toString(), 'history traversal restores old url');
+ t.done();
+ };
+}, "history pushState sets url");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate_url_rewriting.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate_url_rewriting.html
new file mode 100644
index 0000000000..03c2afe8ea
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_pushstate_url_rewriting.html
@@ -0,0 +1,176 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>URL rewriting allowed/disallowed for history.pushState()</title>
+<link rel="help" href="https://github.com/whatwg/html/issues/6836">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+"use strict";
+setup({ explicit_done: true });
+
+const baseWithUsernamePassword = new URL(location.href);
+baseWithUsernamePassword.username = "username";
+baseWithUsernamePassword.password = "password";
+
+const blobURL = URL.createObjectURL(new Blob(["foo"], { type: "text/html" }));
+const blobURL2 = URL.createObjectURL(new Blob(["bar"], { type: "text/html" }));
+
+const basicCases = [
+ [new URL("/common/blank.html", location.href), new URL("/common/blank.html#newhash", location.href), true],
+ [new URL("/common/blank.html", location.href), new URL("/common/blank.html?newsearch", location.href), true],
+ [new URL("/common/blank.html", location.href), new URL("/newpath", location.href), true],
+ [new URL("/common/blank.html", location.href), new URL("/common/blank.html", baseWithUsernamePassword), false],
+ [new URL("/common/blank.html", location.href), blobURL, false],
+ [new URL("/common/blank.html", location.href), "about:blank", false],
+ [new URL("/common/blank.html", location.href), "about:srcdoc", false],
+ [blobURL, blobURL, true],
+ [blobURL, blobURL + "#newhash", true],
+ [blobURL, blobURL + "?newsearch", false],
+ [blobURL, "blob:newpath", false],
+ [blobURL, "blob:" + self.origin + "/syntheticblob", false],
+ [blobURL, blobURL2, false],
+
+ // Note: these are cases where we create the iframe pointing at the initial URL,
+ // so its origin will actually be self.origin.
+ ["about:blank", "about:blank", true],
+ ["about:blank", "about:srcdoc", false],
+ ["about:blank", "about:blank?newsearch", false],
+ ["about:blank", "about:blank#newhash", true],
+ ["about:blank", self.origin + "/blank", false],
+
+ // javascript: URL navigation changes the URL to the creator document's URL, so these should all
+ // not work because you can't rewrite a HTTP(S) URL to a javascript: URL.
+ [new URL("/common/blank.html", location.href), "javascript:'foo'", false],
+ ["javascript:'foo'", "javascript:'foo'", false],
+ ["javascript:'foo'", "javascript:'foo'?newsearch", false],
+ ["javascript:'foo'", "javascript:'foo'#newhash", false],
+].map(([from, to, expectedToWork]) => [from.toString(), to.toString(), expectedToWork]);
+
+for (const [from, to, expectedToWork] of basicCases) {
+ // Otherwise the messages are not consistent between runs which breaks some systems.
+ const fromForTitle = from.replaceAll(blobURL, "blob:(a blob URL for this origin)")
+ .replaceAll(blobURL2, "blob:(another blob URL for this origin)");
+ const toForTitle = to.replaceAll(blobURL, "blob:(a blob URL for this origin)")
+ .replaceAll(blobURL2, "blob:(another blob URL for this origin)");
+
+ promise_test(async () => {
+ const iframe = document.createElement("iframe");
+ iframe.src = from;
+ const loadPromise = new Promise(r => iframe.onload = r);
+
+ document.body.append(iframe);
+ await loadPromise;
+
+ if (expectedToWork) {
+ iframe.contentWindow.history.pushState(null, "", to);
+ assert_equals(iframe.contentWindow.location.href, to);
+ } else {
+ assert_throws_dom("SecurityError", iframe.contentWindow.DOMException, () => {
+ iframe.contentWindow.history.pushState(null, "", to);
+ });
+ }
+ }, `${fromForTitle} to ${toForTitle} should ${expectedToWork ? "" : "not"} work`);
+}
+
+const srcdocCases = [
+ ["about:srcdoc", true],
+ ["about:srcdoc?newsearch", false],
+ ["about:srcdoc#newhash", true],
+ [self.origin + "/srcdoc", false]
+];
+
+for (const [to, expectedToWork] of srcdocCases) {
+ promise_test(async () => {
+ const iframe = document.createElement("iframe");
+ iframe.srcdoc = "foo";
+ const loadPromise = new Promise(r => iframe.onload = r);
+
+ document.body.append(iframe);
+ await loadPromise;
+
+ if (expectedToWork) {
+ iframe.contentWindow.history.pushState(null, "", to);
+ assert_equals(iframe.contentWindow.location.href, to);
+ } else {
+ assert_throws_dom("SecurityError", iframe.contentWindow.DOMException, () => {
+ iframe.contentWindow.history.pushState(null, "", to);
+ });
+ }
+ }, `about:srcdoc to ${to} should ${expectedToWork ? "" : "not"} work`);
+}
+
+// We need to test these separately since they're cross-origin.
+
+const sandboxedCases = [
+ [new URL("resources/url-rewriting-helper.html", location.href), new URL("resources/url-rewriting-helper.html", location.href), true],
+ [new URL("resources/url-rewriting-helper.html", location.href), new URL("resources/url-rewriting-helper.html#newhash", location.href), true],
+ [new URL("resources/url-rewriting-helper.html", location.href), new URL("resources/url-rewriting-helper.html?newsearch", location.href), true],
+ [new URL("resources/url-rewriting-helper.html", location.href), new URL("/newpath", location.href), true],
+ [new URL("resources/url-rewriting-helper.html", location.href), new URL("resources/url-rewriting-helper.html", baseWithUsernamePassword), false],
+].map(([from, to, expectedToWork]) => [from.toString(), to.toString(), expectedToWork]);
+
+for (const [from, to, expectedToWork] of sandboxedCases) {
+ promise_test(async () => {
+ const iframe = document.createElement("iframe");
+ iframe.src = from;
+ iframe.sandbox = "allow-scripts";
+ const loadPromise = new Promise(r => iframe.onload = r);
+
+ document.body.append(iframe);
+ await loadPromise;
+
+ const messagePromise = new Promise(r => window.addEventListener("message", r, { once: true }));
+ iframe.contentWindow.postMessage(to, "*");
+ const { data } = await messagePromise;
+
+ if (expectedToWork) {
+ assert_equals(data.result, "no exception");
+ assert_equals(data.locationHref, to);
+ } else {
+ assert_equals(data.result, "exception");
+ assert_equals(data.exceptionName, "SecurityError");
+ }
+ }, `sandboxed ${from} to ${to} should ${expectedToWork ? "" : "not"} work`);
+}
+
+fetch("resources/url-rewriting-helper.html").then(r => r.text()).then(htmlInside => {
+ const dataURLStart = "data:text/html;base64," + btoa(htmlInside);
+
+ const dataURLCases = [
+ [dataURLStart, dataURLStart, true],
+ [dataURLStart, dataURLStart + "#newhash", true],
+ [dataURLStart, dataURLStart + "?newsearch", false],
+ [dataURLStart, "data:newpath", false]
+ ];
+
+ for (const [from, to, expectedToWork] of dataURLCases) {
+ // Otherwise the messages are unreadably long.
+ const fromForTitle = from.replaceAll(dataURLStart, "data:(script to run this test)");
+ const toForTitle = to.replaceAll(dataURLStart, "data:(script to run this test)");
+
+ promise_test(async () => {
+ const iframe = document.createElement("iframe");
+ iframe.src = from;
+ const loadPromise = new Promise(r => iframe.onload = r);
+
+ document.body.append(iframe);
+ await loadPromise;
+
+ const messagePromise = new Promise(r => window.addEventListener("message", r, { once: true }));
+ iframe.contentWindow.postMessage(to, "*");
+ const { data } = await messagePromise;
+ if (expectedToWork) {
+ assert_equals(data.result, "no exception");
+ assert_equals(data.locationHref, to);
+ } else {
+ assert_equals(data.result, "exception");
+ assert_equals(data.exceptionName, "SecurityError");
+ }
+ }, `${fromForTitle} to ${toForTitle} should ${expectedToWork ? "" : "not"} work`);
+ }
+
+ done();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_replacestate.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_replacestate.html
new file mode 100644
index 0000000000..794c2f3713
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_replacestate.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>history_replaceState</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function () {
+ window.history.replaceState(1, document.title, '?x=1');
+
+ var second;
+ second = window.history.state;
+ assert_equals(second, 1, "history state");
+ }, "history replaceState");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_replacestate_err.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_replacestate_err.html
new file mode 100644
index 0000000000..15d2181820
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_replacestate_err.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>history_replaceState SECURITY_ERR</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function () {
+ assert_throws_dom("SecurityError", function () {
+ window.history.replaceState(1, document.title, 'http://www.microsoft.com/test.html');
+ });
+ }, "history replaceState SECURITY_ERR");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_replacestate_nooptionalparam.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_replacestate_nooptionalparam.html
new file mode 100644
index 0000000000..838467d782
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_replacestate_nooptionalparam.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>history_replaceStateNoOptionalParam</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function () {
+ window.history.replaceState(1, document.title);
+
+ var second;
+ second = window.history.state;
+ assert_equals(second, 1, "history state");
+ }, "history replaceStateNoOptionalParam");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/history_state.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_state.html
new file mode 100644
index 0000000000..2ee2356b1a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/history_state.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>history_state</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function () {
+ var first;
+ var second;
+
+ first = window.history.state;
+ window.history.pushState(1, document.title, '?x=1');
+
+ second = window.history.state;
+ assert_equals(first, null, "first");
+ assert_equals(second, 1, "second");
+ }, "history state");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/iframe_history_go_0.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/iframe_history_go_0.html
new file mode 100644
index 0000000000..f93f4c864e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/iframe_history_go_0.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<head>
+ <title>iframe_history_go_0</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<iframe></iframe>
+<script>
+promise_test(async (t) => {
+ let iframe = null;
+ const OLD_URL = 'blank-old.html';
+ const NEW_URL = 'blank-new.html';
+
+ await new Promise(resolve => {
+ iframe = document.createElement('iframe');
+ iframe.onload = () => resolve();
+ iframe.src = OLD_URL;
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+ });
+
+ assert_equals(iframe.contentDocument.body.textContent, 'This is an old page.\n');
+
+ await new Promise(resolve => {
+ iframe.onload = () => resolve();
+ iframe.src = NEW_URL;
+ });
+
+ assert_equals(iframe.contentDocument.body.textContent, 'This is a new page.\n');
+
+ await new Promise(resolve => {
+ iframe.onload = () => resolve();
+ iframe.contentWindow.history.go(0);
+ });
+
+ assert_equals(iframe.contentDocument.body.textContent, 'This is a new page.\n');
+}, 'iframe\'s history.go(0) performs a location.reload()');
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/001-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/001-1.html
new file mode 100644
index 0000000000..9aa5d30d16
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/001-1.html
@@ -0,0 +1,72 @@
+<!doctype html>
+<script>
+var o = opener;
+
+var frameloaded = null;
+
+o.t.step(function() {o.assert_equals(history.length, 1)});
+
+onload = function () {
+ o.t.step(function() {
+ o.assert_equals(history.length, 1);
+ o.t.done();
+ });
+
+ o.t1.step(function() {
+ var iframe = document.createElement("iframe");
+ iframe.src = "filler.html?id=2";
+ document.body.appendChild(iframe);
+ frameloaded = o.t1.step_func(function () {
+ o.assert_equals(history.length, 1);
+ setTimeout(o.t1.step_func(function () {
+ o.assert_equals(history.length, 1);
+ iframe.src = "filler.html?id=3";
+ frameloaded = o.t2.step_func(function() {
+ o.assert_equals(history.length, 2);
+ history.go(-1);
+ frameloaded = o.t3.step_func(function() {
+ o.assert_equals(history.length, 2);
+ var parts = iframe.contentWindow.location.href.split("/")
+ o.assert_equals(parts[parts.length - 1], "filler.html?id=2");
+ o.t3.done();
+ o.t4.step(function() {
+ var iframe0 = document.getElementsByTagName("iframe")[0];
+ iframe0.src = "filler.html?id=4"
+ frameloaded = o.t4.step_func(function() {
+ o.assert_equals(history.length, 2);
+ var parts = iframe0.contentWindow.location.href.split("/")
+ o.assert_equals(parts[parts.length - 1], "filler.html?id=4");
+ //This is the point at which gecko and webkit stop running tests
+ history.go(-1);
+ frameloaded = o.t5.step_func(function() {
+ o.assert_equals(history.length, 2);
+ var parts = iframe0.contentWindow.location.href.split("/")
+ o.assert_equals(parts[parts.length - 1], "filler.html?id=1");
+ var parts = iframe.contentWindow.location.href.split("/")
+ o.assert_equals(parts[parts.length - 1], "filler.html?id=2");
+ history.go(1);
+ frameloaded = o.t6.step_func(function() {
+ o.assert_equals(history.length, 2);
+ var parts = iframe0.contentWindow.location.href.split("/")
+ o.assert_equals(parts[parts.length - 1], "filler.html?id=4");
+ var parts = iframe.contentWindow.location.href.split("/")
+ o.assert_equals(parts[parts.length - 1], "filler.html?id=2");
+ o.t6.done();
+ });
+ o.t5.done();
+ });
+ o.t4.done();
+ });
+ });
+ });
+ o.t2.done();
+ });
+ o.t1.done();
+ }, 500))
+ });
+ });
+
+}
+</script>
+
+<iframe src="filler.html?id=1"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/001.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/001.html
new file mode 100644
index 0000000000..c9d1c6416d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/001.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<title>Joint session history with single iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+setup({timeout:10000});
+var t = async_test("Session history length on initial load");
+var t1 = async_test("Session history length on adding new iframe");
+var t2 = async_test("Navigating second iframe");
+var t3 = async_test("Traversing history back (1)");
+var t4 = async_test("Navigating first iframe");
+var t5 = async_test("Traversing history back (2)");
+var t6 = async_test("Traversing history forward");
+var w = window.open("001-1.html");
+//add_completion_callback(function() {w.close()});
+</script>
+
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/002-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/002-1.html
new file mode 100644
index 0000000000..ed69d679da
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/002-1.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<script>
+var o = opener;
+
+var frameloaded = null;
+
+o.t.step(function() {o.assert_equals(history.length, 1)});
+
+onload = function () {
+ o.t.step(function() {
+ o.assert_equals(history.length, 1);
+ o.t.done();
+ });
+
+ o.t1.step(function() {
+ var iframe = document.createElement("iframe");
+ iframe.src = "filler.html?id=2";
+ document.body.appendChild(iframe);
+ o.assert_equals(history.length, 1);
+ frameloaded = o.t2.step_func(function() {
+ iframe.contentDocument.open();
+ iframe.contentDocument.write("3<script>onpageshow = function() {alert('pageshow'); parent.frameloaded()}<\/script>");
+ iframe.contentDocument.close();
+ frameloaded = o.t2.step_func(function () {
+ o.assert_equals(history.length, 2);
+ o.t2.done();
+ });
+ });
+ o.t1.done();
+ });
+
+}
+</script>
+
+<iframe src="filler.html?id=1"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/002.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/002.html
new file mode 100644
index 0000000000..b08c19e52d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/002.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<title>Joint session history with single iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+setup({timeout:10000});
+var t = async_test("Session history length on initial load");
+var t1 = async_test("Session history length on adding new iframe");
+var t2 = async_test("Navigating second iframe");
+<!-- var t3 = async_test("Traversing history back (1)"); -->
+<!-- var t4 = async_test("Navigating first iframe"); -->
+<!-- var t5 = async_test("Traversing history back (2)"); -->
+<!-- var t6 = async_test("Traversing history forward"); -->
+var w = window.open("002-1.html");
+//add_completion_callback(function() {w.close()});
+</script>
+
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/filler.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/filler.html
new file mode 100644
index 0000000000..93e3c7ccfc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/joint_session_history/filler.html
@@ -0,0 +1,5 @@
+<!doctype html>
+<script>
+document.write(location.search)
+onpageshow = function() {if (parent.frameloaded) {parent.frameloaded()}}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/history.js b/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/history.js
new file mode 100644
index 0000000000..bb5ee6dde0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/history.js
@@ -0,0 +1,35 @@
+function parse_query() {
+ var query = location.search.slice(1);
+ var vars = query.split("&");
+ var fields = {};
+ vars.forEach(
+ function (x) {
+ var split = x.split("=");
+ return fields[split[0]] = split.slice(1).join("=");
+ });
+ return fields;
+}
+
+var query_parts = parse_query();
+var id = "id" in query_parts ? parseInt(query_parts.id) : 1;
+var urls_to_load = query_parts.urls.split(",");
+
+document.write(id);
+
+onunload = function() {};
+
+function queue_next() {
+ t = opener.t;
+ setTimeout(t.step_func(
+ function() {
+// opener.assert_equals(history.length, id);
+ if (urls_to_load[0]) {
+ var next_page = urls_to_load[0];
+ (next_page.indexOf("?") > -1) ? (next_page += "&") : (next_page += "?");
+ next_page += "urls=" + urls_to_load.slice(1).join(",");
+ next_page += "&id=" + ++id;
+ location = next_page;
+ }
+ }
+ ), 100);
+}
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/history_entry.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/history_entry.html
new file mode 100644
index 0000000000..e5929ddbe8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/history_entry.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<script src="history.js"></script>
+<script>
+ onload = function() {
+ if (!opener.started) {
+ queue_next();
+ } else {
+ opener.pages.push(id);
+ opener.start_test_wait();
+ }
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_history_unload_prompt_1-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_history_unload_prompt_1-1.html
new file mode 100644
index 0000000000..8c4401836a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_history_unload_prompt_1-1.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<script src="history.js"></script>
+<script>
+ onbeforeunload = function() {opener.beforeunload_ran = true; return "Opt to stay on the page"};
+
+ opener.pages.push(id);
+ if (!opener.started) {
+ onload = function() {
+ setTimeout(function() {
+ opener.started = true;
+ history.back();
+ }, 100);
+ }
+ }
+</script>
+<p>You should see/have seen a prompt asking if you want to leave the page.</p>
+<p>Opt to stay on the page</p>
+<button onclick="onbeforeunload = null; opener.start_test_wait(); document.getElementsByTagName('button')[0].disabled = true;">Click here</button>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_history_unload_prompt_1-manual.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_history_unload_prompt_1-manual.html
new file mode 100644
index 0000000000..587cdfb124
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_history_unload_prompt_1-manual.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<title>Traversing the history, prompt in before unload, navigation denied</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ setup({timeout:3600000});
+ var t = async_test();
+ started = false;
+ pages = []
+ timer = null;
+ beforeunload_ran = false;
+ start_test_wait = t.step_func(
+ function() {
+ clearTimeout(timer);
+ timer = setTimeout(t.step_func(
+ function() {
+ try {
+ assert_true(beforeunload_ran, "beforeunload event handler ran");
+ assert_array_equals(pages, [2], "Pages opened during history navigation");
+ t.done();
+ } finally {
+ win.close();
+ }
+ }
+ ), 500);
+ }
+ );
+ t.step(function() {win = window.open("history_entry.html?urls=traverse_the_history_unload_prompt_1-1.html");
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_history_unload_prompt_2-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_history_unload_prompt_2-1.html
new file mode 100644
index 0000000000..608a579e69
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_history_unload_prompt_2-1.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<script src="history.js"></script>
+<script>
+ onbeforeunload = function() {opener.beforeunload_ran = true; return "Opt to leave the page"};
+
+ opener.pages.push(id);
+ if (!opener.started) {
+ onload = function() {
+ setTimeout(function() {
+ opener.started = true;
+ history.back();
+ }, 100);
+ }
+ }
+</script>
+<p>You should see/have seen a prompt asking if you want to leave the page.</p>
+<p>Opt to leave the page</p>
+<p>If you weren't navigated away after opting to leave the page, that's a FAIL</p>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_history_unload_prompt_2-manual.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_history_unload_prompt_2-manual.html
new file mode 100644
index 0000000000..94b66f8b55
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_history_unload_prompt_2-manual.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<title>Traversing the history, prompt in before unload, navigation allowed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ setup({timeout:3600000});
+ var t = async_test();
+ started = false;
+ pages = []
+ timer = null;
+ beforeunload_ran = false;
+ start_test_wait = t.step_func(
+ function() {
+ clearTimeout(timer);
+ timer = setTimeout(t.step_func(
+ function() {
+ try {
+ assert_true(beforeunload_ran, "beforeunload event handler ran");
+ assert_array_equals(pages, [2,1], "Pages opened during history navigation");
+ t.done();
+ } finally {
+ win.close();
+ }
+ }
+ ), 500);
+ }
+ );
+ t.step(function() {win = window.open("history_entry.html?urls=traverse_the_history_unload_prompt_2-1.html");
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_session_history_unload_prompt_1-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_session_history_unload_prompt_1-1.html
new file mode 100644
index 0000000000..c0079b6bec
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_session_history_unload_prompt_1-1.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<script src="history.js"></script>
+<script>
+ onunload = function(e) {opener.unload_ran = true; return "Now refuse to leave the current page"}
+
+ opener.pages.push(id);
+ if (!opener.started) {
+ onload = function() {
+ setTimeout(function() {
+ opener.started = true;
+ history.back();
+ }, 100);
+ }
+ } else {
+ opener.start_test_wait();
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_session_history_unload_prompt_1-manual.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_session_history_unload_prompt_1-manual.html
new file mode 100644
index 0000000000..7f96a39240
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/non-automated/traverse_the_session_history_unload_prompt_1-manual.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<title>Traversing the history, unload event is fired on doucment</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ var t = async_test();
+ started = false;
+ pages = []
+ timer = null;
+ unload_ran = false;
+ start_test_wait = t.step_func(
+ function() {
+ clearTimeout(timer);
+ timer = setTimeout(t.step_func(
+ function() {
+ try {
+ assert_array_equals(pages, [2], "Pages opened during history navigation");
+ assert_true(unload_ran, "Unload event handler ran");
+ t.done();
+ } finally {
+ // win.close();
+ }
+ }
+ ), 500);
+ }
+ );
+ t.step(function() {win = window.open("history_entry.html?urls=traverse_the_history_unload_prompt_1-1.html");
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/pushstate-base.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/pushstate-base.html
new file mode 100644
index 0000000000..ea95d812d7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/pushstate-base.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>history.pushState() with an empty string URL and base URL different from document's URL</title>
+<link rel="help" href="https://github.com/whatwg/html/issues/9343">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<base href="/the-base">
+
+<script>
+"use strict";
+
+test(() => {
+ const before = location.pathname;
+
+ history.pushState(null, null, "");
+ assert_equals(location.pathname, before);
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/pushstate-whitespace.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/pushstate-whitespace.html
new file mode 100644
index 0000000000..72d9be6a9c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/pushstate-whitespace.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>history.pushState() with a whitespace URL</title>
+<link rel="help" href="https://github.com/whatwg/html/issues/9343">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+test(() => {
+ location.hash = "test";
+
+ const before = location.pathname;
+
+ history.pushState(null, null, " ");
+ assert_equals(location.pathname, before, "pathname");
+ assert_equals(location.hash, "", "hash");
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/pushstate.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/pushstate.html
new file mode 100644
index 0000000000..1953709269
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/pushstate.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>history.pushState() with an empty string URL</title>
+<link rel="help" href="https://github.com/whatwg/html/issues/9343">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+test(() => {
+ location.hash = "test";
+
+ history.pushState(null, null, "");
+ assert_equals(location.hash, "#test");
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/replacestate-base.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/replacestate-base.html
new file mode 100644
index 0000000000..224e928de6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/replacestate-base.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>history.replaceState() with an empty string URL and base URL different from document's URL</title>
+<link rel="help" href="https://github.com/whatwg/html/issues/9343">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<base href="/the-base">
+
+<script>
+"use strict";
+
+test(() => {
+ const before = location.pathname;
+
+ history.replaceState(null, null, "");
+ assert_equals(location.pathname, before);
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/replacestate-whitespace.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/replacestate-whitespace.html
new file mode 100644
index 0000000000..7261cdf3fa
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/replacestate-whitespace.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>history.replaceState() with a whitespace URL</title>
+<link rel="help" href="https://github.com/whatwg/html/issues/9343">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+test(() => {
+ location.hash = "test";
+
+ const before = location.pathname;
+
+ history.replaceState(null, null, " ");
+ assert_equals(location.pathname, before, "pathname");
+ assert_equals(location.hash, "", "hash");
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/replacestate.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/replacestate.html
new file mode 100644
index 0000000000..66b4cc6f82
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/replacestate.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>history.replaceState() with an empty string URL</title>
+<link rel="help" href="https://github.com/whatwg/html/issues/9343">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+test(() => {
+ location.hash = "test";
+
+ history.replaceState(null, null, "");
+ assert_equals(location.hash, "#test");
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/resources/message-opener.sub.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/resources/message-opener.sub.html
new file mode 100644
index 0000000000..441c08c0ea
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/resources/message-opener.sub.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+
+<script>
+"use strict";
+
+opener.postMessage("{{GET[id]}}", "*");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/resources/traverse-during-beforeunload.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/resources/traverse-during-beforeunload.html
new file mode 100644
index 0000000000..53a4a1886e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/resources/traverse-during-beforeunload.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+
+<script>
+"use strict";
+
+window.addEventListener("beforeunload", () => {
+ history.back();
+});
+
+location.href = "message-opener.sub.html?id=destination";
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/resources/traverse-during-unload.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/resources/traverse-during-unload.html
new file mode 100644
index 0000000000..d5ffb7abae
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/resources/traverse-during-unload.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+
+<script>
+"use strict";
+
+window.addEventListener("unload", () => {
+ history.back();
+});
+
+location.href = "message-opener.sub.html?id=destination";
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/resources/url-rewriting-helper.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/resources/url-rewriting-helper.html
new file mode 100644
index 0000000000..847d16cd1a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/resources/url-rewriting-helper.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script>
+ window.onmessage = ({ data }) => {
+ try {
+ history.pushState(null, "", data);
+ } catch (e) {
+ parent.postMessage({ result: "exception", exceptionName: e.name }, "*");
+ return;
+ }
+ parent.postMessage({ result: "no exception", locationHref: location.href }, "*");
+ };
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse-during-beforeunload.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse-during-beforeunload.html
new file mode 100644
index 0000000000..cb8cdca7ff
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse-during-beforeunload.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Traversing the history during beforeunload</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+"use strict";
+
+async_test(t => {
+ const w = window.open("resources/message-opener.sub.html?id=start");
+ t.add_cleanup(() => w.close());
+
+ const messages = [];
+ window.addEventListener("message", t.step_func(({ data }) => {
+ messages.push(data);
+
+ if (messages.length === 1) {
+ assert_array_equals(messages, ["start"]);
+ w.location.href = "resources/traverse-during-beforeunload.html";
+ } else if (messages.length === 2) {
+ assert_array_equals(messages, ["start", "destination"]);
+ t.done();
+ }
+ }));
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse-during-unload.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse-during-unload.html
new file mode 100644
index 0000000000..6f6e984402
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse-during-unload.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Traversing the history during unload</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+"use strict";
+
+async_test(t => {
+ const w = window.open("resources/message-opener.sub.html?id=start");
+ t.add_cleanup(() => w.close());
+
+ const messages = [];
+ window.addEventListener("message", t.step_func(({ data }) => {
+ messages.push(data);
+
+ if (messages.length === 1) {
+ assert_array_equals(messages, ["start"]);
+ w.location.href = "resources/traverse-during-unload.html";
+ } else if (messages.length === 2) {
+ assert_array_equals(messages, ["start", "destination"]);
+ t.done();
+ }
+ }));
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_1-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_1-1.html
new file mode 100644
index 0000000000..13e89e7bc0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_1-1.html
@@ -0,0 +1,16 @@
+<!doctype html>
+4
+<script>
+ onunload = function() {}
+
+ opener.pages.push(4);
+ if (!opener.started) {
+ onload = function() {
+ setTimeout(function() {
+ opener.started = true;
+ history.go(-2);
+ history.go(-1);
+ }, 100);
+ }
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_1.html
new file mode 100644
index 0000000000..9b59bb0587
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_1.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<title>Multiple history traversals from the same task</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ var t = async_test();
+ started = false;
+ pages = []
+ timer = null;
+ start_test_wait = t.step_func(
+ function() {
+ clearTimeout(timer);
+ timer = setTimeout(t.step_func(
+ function() {
+ assert_array_equals(pages, [4, 2], "Pages opened during history navigation");
+ t.done();
+ }
+ ), 500);
+ }
+ );
+ t.step(function() {win = window.open("history_entry.html?urls=history_entry.html,history_entry.html,traverse_the_history_1-1.html");
+ t.add_cleanup(() => { win.close() });
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_2-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_2-1.html
new file mode 100644
index 0000000000..fecf060eb7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_2-1.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<script src="history.js"></script>
+<script>
+ opener.pages.push(id);
+ if (!opener.started) {
+ onload = function() {
+ setTimeout(function() {
+ opener.started = true;
+ history.go(-3);
+ history.go(-2);
+ history.go(1);
+ }, 100);
+ }
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_2.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_2.html
new file mode 100644
index 0000000000..429a97d835
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_2.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<title>Multiple history traversals, last would be aborted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ var t = async_test();
+ started = false;
+ pages = []
+ timer = setInterval(
+ function() {
+ if (pages.length < 2)
+ return;
+ clearInterval(timer);
+ setTimeout(t.step_func(
+ function() {
+ assert_array_equals(pages, [6, 3], "Pages opened during history navigation");
+ t.done();
+ }
+ ), 500);
+ }, 50);
+ t.step(function() {
+ win = window.open("history_entry.html?urls=history_entry.html,history_entry.html,history_entry.html,history_entry.html,traverse_the_history_2-1.html");
+ t.add_cleanup(function() { win.close(); });
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_3-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_3-1.html
new file mode 100644
index 0000000000..e7836fedbe
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_3-1.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<script src="history.js"></script>
+<script>
+ opener.pages.push(id);
+ if (!opener.started) {
+ onload = function() {
+ setTimeout(function() {
+ opener.started = true;
+ history.go(-2);
+ history.go(-1);
+ history.go(3);
+ }, 100);
+ }
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_3.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_3.html
new file mode 100644
index 0000000000..429a97d835
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_3.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<title>Multiple history traversals, last would be aborted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ var t = async_test();
+ started = false;
+ pages = []
+ timer = setInterval(
+ function() {
+ if (pages.length < 2)
+ return;
+ clearInterval(timer);
+ setTimeout(t.step_func(
+ function() {
+ assert_array_equals(pages, [6, 3], "Pages opened during history navigation");
+ t.done();
+ }
+ ), 500);
+ }, 50);
+ t.step(function() {
+ win = window.open("history_entry.html?urls=history_entry.html,history_entry.html,history_entry.html,history_entry.html,traverse_the_history_2-1.html");
+ t.add_cleanup(function() { win.close(); });
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_4-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_4-1.html
new file mode 100644
index 0000000000..9211da1172
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_4-1.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<script src="history.js"></script>
+<script>
+ opener.pages.push(id);
+ if (!opener.started) {
+ onload = function() {
+ setTimeout(function() {
+ opener.started = true;
+ history.go(-10); //Outside the range
+ history.go(-1);
+ history.go(-2);
+ }, 100);
+ }
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_4.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_4.html
new file mode 100644
index 0000000000..136093a2c1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_4.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<title>Multiple history traversals, last would be aborted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ var t = async_test();
+ started = false;
+ pages = []
+ timer = setInterval(
+ function() {
+ if (pages.length < 2)
+ return;
+ clearTimeout(timer);
+ setTimeout(t.step_func(
+ function() {
+ assert_array_equals(pages, [6, 5], "Pages opened during history navigation");
+ t.done();
+ }
+ ), 500);
+ }, 50);
+ t.step(function() {
+ win = window.open("history_entry.html?urls=history_entry.html,history_entry.html,history_entry.html,history_entry.html,traverse_the_history_4-1.html");
+ t.add_cleanup(function() { win.close(); });
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_5-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_5-1.html
new file mode 100644
index 0000000000..6a28a85683
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_5-1.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<script src="history.js"></script>
+<script>
+ opener.pages.push(id);
+ if (!opener.started) {
+ onload = function() {
+ setTimeout(function() {
+ opener.started = true;
+ history.go(10); //Outside the range
+ history.go(-1);
+ history.go(-2);
+ }, 100);
+ }
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_5.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_5.html
new file mode 100644
index 0000000000..4d3d643425
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_5.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<title>Multiple history traversals, last would be aborted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ var t = async_test();
+ started = false;
+ pages = []
+ timer = setInterval(
+ function() {
+ if (pages.length < 2)
+ return;
+ clearInterval(timer);
+ setTimeout(t.step_func(
+ function() {
+ assert_array_equals(pages, [6, 5], "Pages opened during history navigation");
+ t.done();
+ }
+ ), 500);
+ }, 50);
+ t.step(function() {
+ win = window.open("history_entry.html?urls=history_entry.html,history_entry.html,history_entry.html,history_entry.html,traverse_the_history_5-1.html");
+ t.add_cleanup(function() { win.close(); });
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_unload_1-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_unload_1-1.html
new file mode 100644
index 0000000000..b9d8a0c694
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_unload_1-1.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<script src="history.js"></script>
+<script>
+ onunload = function() {opener.unload_ran = true;}
+
+ opener.pages.push(id);
+ if (!opener.started) {
+ onload = function() {
+ setTimeout(function() {
+ opener.started = true;
+ history.back();
+ }, 100);
+ }
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_unload_1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_unload_1.html
new file mode 100644
index 0000000000..4ef0ea583d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_unload_1.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<title>Traversing the history, unload event is fired on doucment</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ var t = async_test();
+ started = false;
+ pages = []
+ unload_ran = false;
+ timer = setInterval(
+ function() {
+ if (pages.length < 2 || !unload_ran)
+ return;
+ clearInterval(timer);
+ setTimeout(t.step_func(
+ function() {
+ assert_array_equals(pages, [2, 1], "Pages opened during history navigation");
+ assert_true(unload_ran, "Unload event handler ran");
+ t.done();
+ }
+ ), 500);
+ }, 50);
+ t.step(function() {
+ win = window.open("history_entry.html?urls=traverse_the_history_unload_1-1.html");
+ t.add_cleanup(function() { win.close(); });
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_1-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_1-1.html
new file mode 100644
index 0000000000..945c8d81f8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_1-1.html
@@ -0,0 +1,12 @@
+<!doctype html>
+2
+<script>
+ onunload = function() {}
+ opener.pages.push(2);
+ onload = function() {
+ setTimeout(function() {
+ document.write("<!doctype html>3<script>opener.pages.push(3); if(!opener.started) {opener.started = true; history.go(-1);}<\/script>");
+ document.close();
+ }, 100);
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_1.html
new file mode 100644
index 0000000000..404d61d0cf
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_1.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<title>Traverse the history after document.write after the load event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ var t = async_test();
+ started = false;
+ pages = []
+ start_test_wait = t.step_func(
+ function() {
+ check_result = t.step_func(
+ function() {
+ if (pages.length < 3) {
+ setTimeout(check_result, 500);
+ return
+ }
+ assert_array_equals(pages, [2, 3, 1], "Pages opened during history navigation");
+ t.done();
+ }
+ )
+ setTimeout(check_result, 500);
+ }
+ );
+ t.step(function() {
+ win = window.open("history_entry.html?urls=traverse_the_history_write_after_load_1-1.html");
+ t.add_cleanup(function() {win.close()});
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_2-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_2-1.html
new file mode 100644
index 0000000000..922e1832a6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_2-1.html
@@ -0,0 +1,9 @@
+<!doctype html>
+3
+<script>
+ onunload = function() {}
+ opener.pages.push(3);
+ onload = function() {
+ document.write("<!doctype html>4<script>opener.pages.push(4); if(!opener.started) {opener.started = true; history.go(-2);}<\/script>");
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_2.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_2.html
new file mode 100644
index 0000000000..7372e9f4ae
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_2.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<title>Traverse the history back and forward when a history entry is written after the load event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ var t = async_test();
+ started = false;
+ pages = []
+ timer = setInterval(
+ function() {
+ if (pages.length < 5)
+ return;
+ setTimeout(t.step_func(function() {
+ //The pass condition here is based on the idea that the spec is wrong and browsers are right
+ assert_array_equals(pages, [3, 4, 2, 3, 4], "Pages opened during history navigation");
+ t.done();
+ }), 500);
+ }, 50);
+ t.step(function() {
+ win = window.open("history_entry.html?urls=history_forward-1.html,traverse_the_history_write_onload_2-1.html");
+ t.add_cleanup(function() {win.close()});
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_onload_1-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_onload_1-1.html
new file mode 100644
index 0000000000..70d946c818
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_onload_1-1.html
@@ -0,0 +1,9 @@
+<!doctype html>
+2
+<script>
+ onunload = function() {}
+ opener.pages.push(2);
+ onload = function() {
+ document.write("<!doctype html>3<script>opener.pages.push(3); if(!opener.started) {opener.started = true; history.go(-1);}<\/script>");
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_onload_1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_onload_1.html
new file mode 100644
index 0000000000..febb4334da
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_onload_1.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<title>Traverse the history when a history entry is written in the load event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ var t = async_test();
+ started = false;
+ pages = []
+ timer = setInterval(
+ function() {
+ if (pages.length < 3)
+ return;
+ clearInterval(timer);
+ setTimeout(t.step_func(
+ function() {
+ //The pass condition here is based on the idea that the spec is wrong and browsers are right
+ assert_array_equals(pages, [2, 3, 1], "Pages opened durning history navigation");
+ t.done();
+ }
+ ), 500);
+ }, 50);
+ t.step(function() {
+ win = window.open("history_entry.html?urls=traverse_the_history_write_onload_1-1.html");
+ t.add_cleanup(function() { win.close(); });
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_onload_2-1.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_onload_2-1.html
new file mode 100644
index 0000000000..2716514109
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_onload_2-1.html
@@ -0,0 +1,9 @@
+<!doctype html>
+3
+<script>
+ onunload = function() {}
+ opener.pages.push(3);
+ onload = function() {
+ document.write("<!doctype html>4<script>opener.pages.push(4); if(!opener.started) {opener.started = true; history.go(-1);}<\/script>");
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_onload_2.html b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_onload_2.html
new file mode 100644
index 0000000000..aa97d22c02
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_onload_2.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<title>Traverse the history back and forward when a history entry is written in the load event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ var t = async_test();
+ started = false;
+ pages = []
+ timer = setInterval(
+ function() {
+ if (pages.length < 5)
+ return;
+ clearInterval(timer);
+ setTimeout(t.step_func(
+ function() {
+ //The pass condition here is based on the idea that the spec is wrong and browsers are right
+ assert_array_equals(pages, [3, 4, 2, 3, 4], "Pages opened durning history navigation");
+ t.done();
+ }
+ ), 500);
+ }, 50);
+ t.step(function() {
+ win = window.open("history_entry.html?urls=history_forward-1.html,traverse_the_history_write_onload_2-1.html");
+ t.add_cleanup(function() { win.close(); });
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/allow_prototype_cycle_through_location.sub.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/allow_prototype_cycle_through_location.sub.html
new file mode 100644
index 0000000000..f72ed1eaf2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/allow_prototype_cycle_through_location.sub.html
@@ -0,0 +1,197 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+
+ <title>Location objects' custom [[GetPrototypeOf]] trap permit [[Prototype]] chain cycles to be created through them</title>
+
+ <link rel="author" title="Jeff Walden" href="http://whereswalden.com/" />
+ <link rel="help" href="https://tc39.github.io/ecma262/#sec-ordinarysetprototypeof" />
+ <link rel="help" href="https://html.spec.whatwg.org/multipage/browsers.html#location-getprototypeof" />
+
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<hr />
+
+<iframe id="same-origin-different-window"></iframe>
+<iframe id="cross-origin-joined-via-document-domain"></iframe>
+
+<script>
+"use strict";
+
+// Handle same-origin, same-window testing first, before any async-requiring
+// testing.
+test(function() {
+ var LocationPrototype = Location.prototype;
+ var ObjectPrototype = Object.prototype;
+
+ var loc = window.location;
+
+ var locProto = Object.getPrototypeOf(loc);
+ assert_equals(locProto, LocationPrototype,
+ "loc's initial [[Prototype]]");
+
+ var originalLocProtoProto = Object.getPrototypeOf(locProto);
+ assert_equals(originalLocProtoProto, ObjectPrototype,
+ "Location.prototype's initial [[Prototype]]");
+
+ Object.setPrototypeOf(locProto, loc);
+
+ assert_equals(Object.getPrototypeOf(locProto), loc,
+ "LocationPrototype's new [[Prototype]]");
+ assert_equals(Object.getPrototypeOf(loc), locProto,
+ "loc's new [[Prototype]]");
+
+ // Reset so as not to muck with testharness.js expectations.
+ Object.setPrototypeOf(locProto, originalLocProtoProto);
+}, "same-origin, same-window location cycle");
+
+var pathdir =
+ location.pathname.substring(0, location.pathname.lastIndexOf('/') + 1);
+
+var triggerCrossOriginTest = (function() {
+ var crossOrigin =
+ document.getElementById("cross-origin-joined-via-document-domain");
+
+ var t = async_test("cross-origin location has null prototype");
+
+ return new Promise(function(resolve, reject) {
+ crossOrigin.onload = t.step_func_done(function(e) {
+ try {
+ var win = crossOrigin.contentWindow;
+
+ var loc = win.location;
+
+ // Between un-opted-in windows, location objects appear to have null
+ // [[Prototype]].
+ assert_equals(Object.getPrototypeOf(loc), null,
+ "cross-origin unjoined location's [[Prototype]");
+
+ resolve();
+ } catch (e) {
+ reject(e);
+ throw e;
+ }
+ });
+
+ crossOrigin.src =
+ "//{{domains[www]}}:" + location.port + pathdir + "cross_origin_joined_frame.sub.html";
+ })
+ .catch(t.unreached_func("crossOrigin onload/src setting"));
+})();
+
+var triggerSameOriginTest = (function() {
+ var sameOriginDifferentWindow =
+ document.getElementById("same-origin-different-window");
+
+ var t = async_test("same-origin, different-window location cycle");
+
+ return new Promise(function(resolve, reject) {
+ sameOriginDifferentWindow.onload = t.step_func_done(function() {
+ try {
+ var win = sameOriginDifferentWindow.contentWindow;
+
+ var loc = win.location;
+ var LocationPrototype = win.Location.prototype;
+ var ObjectPrototype = win.Object.prototype;
+
+ var locProto = Object.getPrototypeOf(loc);
+ assert_equals(locProto, LocationPrototype,
+ "loc's initial [[Prototype]]");
+
+ var originalLocProtoProto = Object.getPrototypeOf(locProto);
+ assert_equals(originalLocProtoProto, ObjectPrototype,
+ "Location.prototype's initial [[Prototype]]");
+
+ Object.setPrototypeOf(locProto, loc);
+
+ assert_equals(Object.getPrototypeOf(locProto), loc,
+ "LocationPrototype's new [[Prototype]]");
+ assert_equals(Object.getPrototypeOf(loc), locProto,
+ "loc's new [[Prototype]]");
+
+ // Reset so as not to muck with testharness.js expectations.
+ Object.setPrototypeOf(locProto, originalLocProtoProto);
+
+ resolve();
+ } catch (e) {
+ reject(e);
+ throw e;
+ }
+ });
+
+ sameOriginDifferentWindow.src = "same_origin_frame.html";
+ })
+ .catch(t.unreached_func("sameOriginDifferentWindow onload/src setting"));
+})();
+
+function crossOriginJoinTest() {
+ var win =
+ document.getElementById("cross-origin-joined-via-document-domain")
+ .contentWindow;
+
+ assert_equals(document.domain, "{{host}}");
+
+ var loc = win.location;
+
+ var threw = false;
+ try {
+ // Still cross-origin until the document.domain set below.
+ win.Location;
+ } catch (e) {
+ threw = true;
+ }
+
+ assert_equals(threw, true,
+ "accessing win.Location before joining win's origin");
+
+ // Join with other frames that have set |document.domain| to this same
+ // value -- namely, this cross-origin frame. Now access between the two
+ // windows should be permitted.
+ assert_equals(document.domain, "{{host}}",
+ "initial document.domain sanity check");
+ document.domain = "{{host}}";
+
+ var LocationPrototype = win.Location.prototype;
+ var ObjectPrototype = win.Object.prototype;
+
+ var locProto = Object.getPrototypeOf(loc);
+ assert_equals(locProto, LocationPrototype,
+ "loc's initial [[Prototype]]");
+
+ var originalLocProtoProto = Object.getPrototypeOf(locProto);
+ assert_equals(originalLocProtoProto, ObjectPrototype,
+ "Location.prototype's initial [[Prototype]]");
+
+ Object.setPrototypeOf(locProto, loc);
+
+ assert_equals(Object.getPrototypeOf(locProto), loc,
+ "LocationPrototype's new [[Prototype]]");
+ assert_equals(Object.getPrototypeOf(loc), locProto,
+ "loc's new [[Prototype]]");
+
+ // Reset so as not to muck with testharness.js expectations.
+ Object.setPrototypeOf(locProto, originalLocProtoProto);
+}
+
+function run() {
+ var t =
+ async_test("cross-origin, but joined via document.domain, location cycle");
+
+ // The cross-origin/joined case must be tested after both unjoined same-origin
+ // and unjoined cross-origin tests: by mucking with document.domain, the
+ // cross-origin/joined case makes it impossible to perform those tests.
+ t.step(function() {
+ Promise.all([triggerCrossOriginTest, triggerSameOriginTest])
+ .then(t.step_func_done(crossOriginJoinTest),
+ t.unreached_func("cross-origin joined error case"));
+ });
+}
+run();
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/assign_after_load-1.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/assign_after_load-1.html
new file mode 100644
index 0000000000..3d2b897221
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/assign_after_load-1.html
@@ -0,0 +1,9 @@
+<!doctype html>
+1
+<script>
+onload = parent.t.step_func(function() {
+ setTimeout(function() {
+ location = location.toString().replace("assign_after_load-1.html", "assign_after_load-2.html");
+ }, 100);
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/assign_after_load-2.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/assign_after_load-2.html
new file mode 100644
index 0000000000..94679571be
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/assign_after_load-2.html
@@ -0,0 +1,7 @@
+<!doctype html>
+2
+<script>
+onload = parent.t.step_func(function() {
+ setTimeout(function() {parent.do_test()}, 100);
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/assign_after_load.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/assign_after_load.html
new file mode 100644
index 0000000000..00dc931d4e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/assign_after_load.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<title>Assignment to location after document is completely loaded</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe></iframe>
+<script>
+var t = async_test();
+var history_length;
+
+onload = t.step_func(function() {
+ setTimeout(function() {
+ history_length = history.length;
+ document.getElementsByTagName("iframe")[0].src = "assign_after_load-1.html";
+ }, 100);
+});
+
+do_test = t.step_func(function() {
+ assert_equals(history.length, history_length + 2);
+ t.done();
+});
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/assign_before_load-1.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/assign_before_load-1.html
new file mode 100644
index 0000000000..2549867c8f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/assign_before_load-1.html
@@ -0,0 +1,7 @@
+<!doctype html>
+1
+<script>
+onload = parent.t.step_func(function() {
+ location = location.toString().replace("assign_before_load-1.html", "assign_before_load-2.html");
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/assign_before_load-2.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/assign_before_load-2.html
new file mode 100644
index 0000000000..94679571be
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/assign_before_load-2.html
@@ -0,0 +1,7 @@
+<!doctype html>
+2
+<script>
+onload = parent.t.step_func(function() {
+ setTimeout(function() {parent.do_test()}, 100);
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/assign_before_load.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/assign_before_load.html
new file mode 100644
index 0000000000..62a2aa7c60
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/assign_before_load.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<title>Assignment to location before document is completely loaded</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe></iframe>
+<script>
+var t = async_test();
+var history_length;
+
+onload = t.step_func(function() {
+ setTimeout(function() {
+ history_length = history.length;
+ document.getElementsByTagName("iframe")[0].src = "assign_before_load-1.html";
+ }, 100);
+});
+
+do_test = t.step_func(function() {
+ assert_equals(history.length, history_length + 1);
+ t.done();
+});
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/cross_origin_joined_frame.sub.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/cross_origin_joined_frame.sub.html
new file mode 100644
index 0000000000..a3ffdd005a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/cross_origin_joined_frame.sub.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Cross-origin subframe for Location cyclic [[Prototype]] test</title>
+ <link rel="author" title="Jeff Walden" href="http://whereswalden.com/" />
+</head>
+<body>
+<script>
+document.domain = "{{host}}";
+</script>
+<!-- this should be accessible to the parent once it sets document.domain -->
+<p>Cross-origin iframe with joined <code>document.domain</code></p>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/document_location.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/document_location.html
new file mode 100644
index 0000000000..3bdb028212
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/document_location.html
@@ -0,0 +1,39 @@
+<!doctype html>
+<title>document.location</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(function() {
+ var doc = document.implementation.createHTMLDocument("");
+ assert_equals(doc.location, null);
+}, "document not in a browsing context");
+
+test(function() {
+ assert_equals(document.location, location);
+}, "document.location equals window.location");
+
+test(function() {
+ var desc1 = Object.getOwnPropertyDescriptor(new Document(), "location");
+ assert_not_equals(desc1, undefined);
+ assert_equals(typeof desc1.get, "function");
+
+ var desc2 = Object.getOwnPropertyDescriptor(new Document(), "location");
+ assert_not_equals(desc2, undefined);
+ assert_equals(typeof desc2.get, "function");
+
+ assert_equals(desc1.get, desc2.get);
+}, "Attribute getter deduplication");
+
+test(function() {
+ var desc1 = Object.getOwnPropertyDescriptor(new Document(), "location");
+ assert_not_equals(desc1, undefined);
+ assert_equals(typeof desc1.set, "function");
+
+ var desc2 = Object.getOwnPropertyDescriptor(new Document(), "location");
+ assert_not_equals(desc2, undefined);
+ assert_equals(typeof desc2.set, "function");
+
+ assert_equals(desc1.set, desc2.set);
+}, "Attribute setter deduplication");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-non-configurable-toString-valueOf.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-non-configurable-toString-valueOf.html
new file mode 100644
index 0000000000..80760ac9e4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-non-configurable-toString-valueOf.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Same-origin Location objects have non-configurable "toString" and "valueOf" properties</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/history.html#location-defineownproperty">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+test(() => {
+ assert_own_property(location, "toString");
+ const origToString = location.toString;
+
+ assert_throws_js(TypeError, () => {
+ Object.defineProperty(location, "toString", {
+ get() {},
+ set(_v) {},
+ enumerable: true,
+ configurable: true,
+ });
+ });
+
+ assert_equals(location.toString, origToString);
+}, "'toString' redefinition with accessor fails");
+
+test(() => {
+ assert_own_property(location, "valueOf");
+ const origValueOf = location.valueOf;
+
+ assert_throws_js(TypeError, () => {
+ Object.defineProperty(location, "valueOf", {
+ get() {},
+ enumerable: false,
+ configurable: true,
+ });
+ });
+
+ assert_equals(location.valueOf, origValueOf);
+}, "'valueOf' redefinition with accessor fails");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-origin-idna.sub.window.js b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-origin-idna.sub.window.js
new file mode 100644
index 0000000000..83b030f886
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-origin-idna.sub.window.js
@@ -0,0 +1,11 @@
+async_test(t => {
+ const frame = document.createElement("iframe"),
+ asciiOrigin = location.protocol + "//{{domains[天気の良い日]}}:" + location.port,
+ path = new URL("resources/post-your-origin.html", location).pathname;
+ frame.src = asciiOrigin + path;
+ self.onmessage = t.step_func_done(e => {
+ assert_equals(e.data.origin, asciiOrigin);
+ });
+ document.body.appendChild(frame);
+ t.add_cleanup(() => frame.remove());
+}, "Test that location.origin returns ASCII");
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-pathname-setter-question-mark.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-pathname-setter-question-mark.html
new file mode 100644
index 0000000000..09546020f7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-pathname-setter-question-mark.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<title>Set location.pathname to ?</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<iframe src=/common/blank.html></iframe>
+<script>
+async_test((t) => {
+ onload = t.step_func(() => {
+ self[0].frameElement.onload = t.step_func_done(() => {
+ assert_equals(self[0].location.pathname, "/%3F")
+ })
+ self[0].location.pathname = "?"
+ })
+})
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prevent-extensions.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prevent-extensions.html
new file mode 100644
index 0000000000..a8c777aded
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prevent-extensions.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>[[PreventExtensions]] on a Location object should return false</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/history.html#location-preventextensions">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ Object.preventExtensions(location);
+ });
+}, "Object.preventExtensions throws a TypeError");
+
+test(() => {
+ assert_false(Reflect.preventExtensions(location));
+}, "Reflect.preventExtensions returns false");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter-non-broken-weird.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter-non-broken-weird.html
new file mode 100644
index 0000000000..544eb4ad9a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter-non-broken-weird.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<title>Set location.protocol from an HTTP URL</title>
+<!-- In particular, valid non-broken schemes that are nevertheless not going to work -->
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<iframe src=/common/blank.html></iframe>
+<iframe src=/common/blank.html></iframe>
+<iframe src=/common/blank.html></iframe>
+<iframe src=/common/blank.html></iframe>
+<iframe src=/common/blank.html></iframe>
+<iframe src=/common/blank.html></iframe>
+<script>
+self.onload = () => {
+ [
+ 'x',
+ 'data',
+ 'file',
+ 'ftp',
+ 'http+x'
+ ].forEach((val, index) => {
+ async_test((t) => {
+ self[index].location.protocol = val
+ t.step_timeout(() => {
+ assert_equals(self[index].location.protocol, location.protocol)
+ assert_equals(self[index].location.host, location.host)
+ assert_equals(self[index].location.port, location.port)
+ t.done()
+ // Experimentally, 4 seconds is long enough for the navigation to
+ // complete, if it happens.
+ }, 4000)
+ }, "Set location.protocol to " + val)
+ })
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter-non-broken.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter-non-broken.html
new file mode 100644
index 0000000000..585016eae9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter-non-broken.html
@@ -0,0 +1,63 @@
+<!doctype html>
+<title>Set location.protocol to a non-broken-non-functioning scheme</title>
+<!-- In particular, valid non-broken schemes that are nevertheless not going to work -->
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+self.onload = () => {
+ [
+ 'x',
+ 'data',
+ // 'mailto' opens an email client in Chrome and Firefox and then ends up passing anyway...
+ 'file',
+ 'ftp',
+ 'http+x'
+ ].forEach((val) => {
+ async_test((t) => {
+ // HTTP URL <iframe>
+ const frame = document.createElement("iframe")
+ t.add_cleanup(() => frame.remove())
+ frame.src = "/common/blank.html"
+ frame.onload = t.step_func(() => {
+ frame.contentWindow.location.protocol = val
+ t.step_timeout(() => {
+ assert_equals(frame.contentWindow.location.protocol, location.protocol)
+ assert_equals(frame.contentWindow.location.host, location.host)
+ assert_equals(frame.contentWindow.location.port, location.port)
+ t.done()
+ // Matches the timeout from location-protocol-setter-non-broken-weird.html which suggests
+ // that 4 seconds is enough for a navigation to complete.
+ }, 4000)
+ })
+ document.body.appendChild(frame)
+ }, "Set HTTP URL frame location.protocol to " + val)
+
+ async_test((t) => {
+ // data URL <iframe>
+ const dataFrame = document.createElement("iframe")
+ t.add_cleanup(() => dataFrame.remove())
+ const channel = new MessageChannel()
+ dataFrame.src = `data:text/html,<script>
+onmessage = (e) => {
+ let result = false;
+ try {
+ location.protocol = e.data
+ } catch(e) {
+ result = true
+ }
+ setTimeout(() => e.ports[0].postMessage([result, location.protocol]), 4000)
+}
+<\/script>`
+ dataFrame.onload = t.step_func(() => {
+ dataFrame.contentWindow.postMessage(val, "*", [channel.port2])
+ })
+ channel.port1.onmessage = t.step_func_done((e) => {
+ assert_false(e.data[0])
+ assert_equals(e.data[1], "data:")
+ })
+ document.body.appendChild(dataFrame)
+ }, "Set data URL frame location.protocol to " + val)
+ })
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter-sameish.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter-sameish.html
new file mode 100644
index 0000000000..c581ae35aa
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter-sameish.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<title>Set location.protocol to the scheme it already was</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<iframe src=/common/blank.html></iframe>
+<iframe src=/common/blank.html></iframe>
+<iframe src=/common/blank.html></iframe>
+<iframe src=/common/blank.html></iframe>
+<script>
+self.onload = () => {
+ [
+ "http",
+ "ht\x0Atp",
+ "http\x0A",
+ "\x09ht\x09\x0AtP"
+ ].forEach((val, index) => {
+ async_test(t => {
+ self[index].frameElement.onload = t.step_func_done(() => {
+ assert_equals(self[index].location.protocol, "http:");
+ });
+ self[index].location.protocol = val;
+ }, `Set location.protocol to ${encodeURI(val)} (percent-encoded here for clarity)`);
+ });
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter-with-colon.sub.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter-with-colon.sub.html
new file mode 100644
index 0000000000..0612b5c709
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter-with-colon.sub.html
@@ -0,0 +1,52 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<iframe id="existing" src="resources/post-your-protocol.html?existing"></iframe>
+<iframe id="http-and-gunk" src="resources/post-your-protocol.html?http-and-gunk"></iframe>
+<!-- iframe id="https-and-gunk" src="resources/post-your-protocol.html?https-and-gunk"></iframe -->
+<script>
+// NOTE: we do not listen to message events until our load event fires, so we
+// only get them for the things we actually care about.
+var wrapper_test = async_test("General setup");
+var tests = {
+ "existing": { test: async_test("Set location.protocol = location.protocol"),
+ result: location.protocol },
+ "http-and-gunk": { test: async_test("Set location.protocol to http:gunk"),
+ result: "http:" },
+ // We should really test the "https:gunk" case too, and assert that it ends up
+ // with a protocol of "https:", but can't. See comments below for why.
+};
+
+function messageListener(e) {
+ var data = e.data;
+ var id = data.id;
+ var t = tests[id].test;
+ t.step(function() {
+ assert_equals(data.protocol, tests[id].result, "Protocol should match");
+ })
+ t.done();
+}
+
+addEventListener("load", wrapper_test.step_func_done(function() {
+ addEventListener("message", messageListener);
+
+ tests["existing"].test.step(function() {
+ var loc = document.getElementById("existing").contentWindow.location;
+ loc.protocol = loc.protocol;
+ });
+ tests["http-and-gunk"].test.step(function() {
+ var loc = document.getElementById("http-and-gunk").contentWindow.location;
+ loc.protocol = "http:gunk";
+ });
+ // I wish we could test the https bit, but can't figure out a non-racy way to
+ // do it, because we need to change both protocol (to https) _and_ port to
+ // {{ports[https][0]}} to get a successful load unless we're running on the
+ // default http port, but the setter uses the current value, which doesn't get
+ // updated sync, as the url to start with for the set. Oh, and there's no
+ // good way to detect when the port set is "done" either.
+}));
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter.html
new file mode 100644
index 0000000000..3d051e8cfb
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-protocol-setter.html
@@ -0,0 +1,104 @@
+<!doctype html>
+<title>Set location.protocol to schemes that throw</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<iframe src="data:text/html,<script>
+onmessage = (e) => {
+ let results = [];
+ e.data.forEach((val) => {
+ try {
+ location.protocol = val;
+ results.push('failure')
+ } catch(e) {
+ results.push(e.name)
+ }
+ });
+ parent.postMessage(results, '*')
+}
+</script>"></iframe>
+<iframe srcdoc="<script>
+onmessage = (e) => {
+ let results = [];
+ e.data.forEach((val) => {
+ try {
+ location.protocol = val;
+ results.push('failure')
+ } catch(e) {
+ results.push(e.name)
+ }
+ });
+ parent.postMessage(results, '*')
+}
+</script>"></iframe>
+<script>
+ const broken = [
+ '\x00',
+ '\x01',
+ '\x09', // becomes the empty string
+ '\x0A', // becomes the empty string
+ '\x0C',
+ '\x0D',
+ '\x20',
+ '\x21',
+ '\x7F',
+ '\x80',
+ '\xFF',
+ ':',
+ '†',
+ '\x00x',
+ '\x01x',
+ '\x20x',
+ '\x21x',
+ '\x7Fx',
+ '\x80x',
+ '\xFFx',
+ ':x',
+ '†x',
+ '\x00X',
+ '\x01X',
+ '\x20X',
+ '\x21X',
+ '\x7FX',
+ '\x80X',
+ '\xFFX',
+ ':X',
+ '†X',
+ 'x\x00',
+ 'x\x01',
+ 'x\x20',
+ 'x\x21',
+ 'x\x7F',
+ 'x\x80',
+ 'x\xFF',
+ 'x†',
+ 'X\x00',
+ 'X\x01',
+ 'X\x20',
+ 'X\x21',
+ 'X\x7F',
+ 'X\x80',
+ 'X\xFF',
+ 'X†',
+ ];
+
+ broken.forEach(val => {
+ test(() => {
+ assert_throws_dom("SyntaxError", () => { location.protocol = val })
+ }, `${encodeURI(val)} (percent-encoded here for clarity) is not a scheme`)
+ })
+ let c = 0
+ async_test((t) => {
+ self.onload = t.step_func(() => {
+ self.onmessage = t.step_func((e) => {
+ assert_array_equals(e.data, broken.map(() => "SyntaxError"))
+ c++
+ if(c === 2) {
+ t.done()
+ }
+ })
+ self[0].postMessage(broken, "*")
+ self[1].postMessage(broken, "*")
+ })
+ }, "Equivalent tests for data URL and srcdoc <iframe>s")
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-no-toString-valueOf.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-no-toString-valueOf.html
new file mode 100644
index 0000000000..56316320af
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-no-toString-valueOf.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Location.prototype objects don't have own "toString" and "valueOf" properties</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+test(t => {
+ assert_not_own_property(Location.prototype, "toString");
+ t.add_cleanup(() => { delete Location.prototype.toString; });
+
+ let val;
+ Object.defineProperty(Location.prototype, "toString", {
+ get: () => val,
+ set: newVal => { val = newVal; },
+ enumerable: false,
+ configurable: true,
+ });
+
+ Location.prototype.toString = 2;
+ assert_equals(Location.prototype.toString, 2);
+}, "'toString' accessor property is defined");
+
+test(t => {
+ assert_not_own_property(Location.prototype, "toString");
+ t.add_cleanup(() => { delete Location.prototype.toString; });
+
+ Location.prototype.toString = 4;
+ assert_equals(Location.prototype.toString, 4);
+}, "'toString' data property is created via [[Set]]");
+
+test(t => {
+ assert_not_own_property(Location.prototype, "valueOf");
+ t.add_cleanup(() => { delete Location.prototype.valueOf; });
+
+ Object.defineProperty(Location.prototype, "valueOf", {
+ get: () => 6,
+ enumerable: true,
+ configurable: true,
+ });
+
+ assert_equals(Location.prototype.valueOf, 6);
+}, "'valueOf' accessor property is defined");
+
+test(t => {
+ assert_not_own_property(Location.prototype, "valueOf");
+ t.add_cleanup(() => { delete Location.prototype.valueOf; });
+
+ Location.prototype.valueOf = 8;
+ assert_equals(Object.getOwnPropertyDescriptor(Location.prototype, "valueOf").value, 8);
+}, "'valueOf' data property is created via [[Set]]");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-cross-origin-domain.sub.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-cross-origin-domain.sub.html
new file mode 100644
index 0000000000..1e677b0365
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-cross-origin-domain.sub.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>[[SetPrototypeOf]] on a Location object should not allow changing its value: cross-origin via document.domain</title>
+<link rel="help" href="http://html.spec.whatwg.org/multipage/#location-setprototypeof">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/test-setting-immutable-prototype.js"></script>
+
+<iframe src="/common/domain-setter.sub.html"></iframe>
+
+<script>
+"use strict";
+// This page does *not* set document.domain, so it's cross-origin with the iframe
+setup({ explicit_done: true });
+
+window.onload = () => {
+ const targetLocation = frames[0].location;
+ const origProto = Object.getPrototypeOf(targetLocation);
+
+ test(() => {
+ assert_equals(Object.getPrototypeOf(targetLocation), null);
+ }, "Cross-origin via document.domain: the prototype is null");
+
+ testSettingImmutablePrototype("Cross-origin via document.domain", targetLocation, origProto,
+ { isSameOriginDomain: false });
+
+ done();
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-cross-origin.sub.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-cross-origin.sub.html
new file mode 100644
index 0000000000..7bb6a27e21
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-cross-origin.sub.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>[[SetPrototypeOf]] on a Location object should not allow changing its value: cross-origin</title>
+<link rel="help" href="http://html.spec.whatwg.org/multipage/#location-setprototypeof">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/test-setting-immutable-prototype.js"></script>
+
+<iframe src="//{{domains[www]}}:{{ports[http][1]}}/common/blank.html"></iframe>
+
+<script>
+"use strict";
+setup({ explicit_done: true });
+
+window.onload = () => {
+ const targetLocation = frames[0].location;
+
+ test(() => {
+ assert_equals(Object.getPrototypeOf(targetLocation), null);
+ }, "Cross-origin: the prototype is null");
+
+ testSettingImmutablePrototype("Cross-origin", targetLocation, null, { isSameOriginDomain: false });
+
+ done();
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-goes-cross-origin-domain.sub.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-goes-cross-origin-domain.sub.html
new file mode 100644
index 0000000000..8966a814c5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-goes-cross-origin-domain.sub.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>[[SetPrototypeOf]] on a Location object should not allow changing its value: cross-origin via document.domain after initially getting the object</title>
+<link rel="help" href="http://html.spec.whatwg.org/multipage/#location-setprototypeof">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/test-setting-immutable-prototype.js"></script>
+
+<iframe src="/common/blank.html"></iframe>
+
+<script>
+"use strict";
+setup({ explicit_done: true });
+
+window.onload = () => {
+ const targetLocation = frames[0].location;
+ const origProto = Object.getPrototypeOf(targetLocation);
+
+ test(() => {
+ assert_not_equals(origProto, null);
+ }, "Same-origin (for now): the prototype is accessible");
+
+ document.domain = "{{host}}";
+
+ test(() => {
+ assert_equals(Object.getPrototypeOf(targetLocation), null);
+ }, "Became cross-origin via document.domain: the prototype is now null");
+
+ testSettingImmutablePrototype("Became cross-origin via document.domain", targetLocation, null, { isSameOriginDomain: false });
+
+ testSettingImmutablePrototypeToNewValueOnly(
+ "Became cross-origin via document.domain", targetLocation, origProto,
+ "the original value from before going cross-origin", { isSameOriginDomain: false });
+
+ done();
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-same-origin-domain.sub.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-same-origin-domain.sub.html
new file mode 100644
index 0000000000..8ec7585daa
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-same-origin-domain.sub.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>[[SetPrototypeOf]] on a Location object should not allow changing its value: cross-origin, but same-origin-domain</title>
+<link rel="help" href="http://html.spec.whatwg.org/multipage/#location-setprototypeof">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/test-setting-immutable-prototype.js"></script>
+
+<iframe src="//{{domains[www]}}:{{ports[http][1]}}/common/domain-setter.sub.html"></iframe>
+
+<script>
+"use strict";
+document.domain = "{{host}}";
+setup({ explicit_done: true });
+
+window.onload = () => {
+ const targetLocation = frames[0].location;
+ const origProto = Object.getPrototypeOf(targetLocation);
+
+ test(() => {
+ assert_not_equals(origProto, null);
+ }, "Same-origin-domain prerequisite check: the original prototype is accessible");
+
+ testSettingImmutablePrototype("Same-origin-domain", targetLocation, origProto, { isSameOriginDomain: true }, frames[0]);
+
+ done();
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-same-origin.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-same-origin.html
new file mode 100644
index 0000000000..f912004f73
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-prototype-setting-same-origin.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>[[SetPrototypeOf]] on a location object should not allow changing its value: same-origin</title>
+<link rel="help" href="http://html.spec.whatwg.org/multipage/#location-setprototypeof">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="/common/test-setting-immutable-prototype.js"></script>
+
+<script>
+"use strict";
+
+const origProto = Object.getPrototypeOf(location);
+
+test(() => {
+ assert_not_equals(origProto, null);
+}, "Same-origin prerequisite check: the original prototype is accessible");
+
+testSettingImmutablePrototype("Same-origin", location, origProto, { isSameOriginDomain: true });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-stringifier.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-stringifier.html
new file mode 100644
index 0000000000..48bad7af0e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-stringifier.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<title>Location stringifier</title>
+<link rel="author" title="Ms2ger" href="mailto:ms2ger@gmail.com">
+<link rel="help" href="https://webidl.spec.whatwg.org/#es-stringifier">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/common/stringifiers.js></script>
+<div id=log></div>
+<script>
+test_stringifier_attribute(location, "href", true);
+
+test(function() {
+ const prop1 = Object.getOwnPropertyDescriptor(location, "toString"),
+ prop2 = Object.getOwnPropertyDescriptor(location, "href")
+
+ assert_true(prop1.enumerable)
+ assert_false(prop1.writable)
+ assert_false(prop1.configurable)
+
+ assert_true(prop2.enumerable)
+ assert_false(prop2.configurable)
+ assert_equals(typeof prop2.get, "function")
+})
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-symbol-toprimitive.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-symbol-toprimitive.html
new file mode 100644
index 0000000000..e666a3e703
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-symbol-toprimitive.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<title>Location Symbol.toPrimitive</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+test(() => {
+ assert_equals(location[Symbol.toPrimitive], undefined)
+ const prop = Object.getOwnPropertyDescriptor(location, Symbol.toPrimitive)
+ assert_false(prop.enumerable)
+ assert_false(prop.writable)
+ assert_false(prop.configurable)
+})
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-tojson.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-tojson.html
new file mode 100644
index 0000000000..5f20a6e15c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-tojson.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<title>Location has no toJSON</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+test(() => {
+ assert_equals(location.toJSON, undefined)
+ assert_equals(Object.getOwnPropertyDescriptor(location, "toJSON"), undefined)
+ assert_false(location.hasOwnProperty("toJSON"))
+})
+</script>
+<!-- See https://github.com/whatwg/html/pull/2294 for context. (And the HTML Standard of course.) -->
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-valueof.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-valueof.html
new file mode 100644
index 0000000000..978bbb63a0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-valueof.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<title>Location valueOf</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+test(() => {
+ assert_equals(location.valueOf, Object.prototype.valueOf)
+ assert_equals(typeof location.valueOf.call(5), "object")
+ const prop = Object.getOwnPropertyDescriptor(location, "valueOf")
+ assert_false(prop.enumerable)
+ assert_false(prop.writable)
+ assert_false(prop.configurable)
+})
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location_assign.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_assign.html
new file mode 100644
index 0000000000..55f26d5660
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_assign.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>location_assign</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function () {
+ var href = location.href;
+ location.assign('#x');
+
+ assert_equals((href + "#x"), location.href, "location href");
+
+ }, "location assign");
+
+ test(function () {
+ var href = location.href;
+ assert_throws_dom('SYNTAX_ERR', function() { location.assign("http://:"); });
+ assert_equals(location.href, href);
+ }, "URL that fails to parse");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location_assign_about_blank-1.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_assign_about_blank-1.html
new file mode 100644
index 0000000000..b43598f2cd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_assign_about_blank-1.html
@@ -0,0 +1,2 @@
+<!doctype html>
+Filler text
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location_assign_about_blank.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_assign_about_blank.html
new file mode 100644
index 0000000000..f3f7cf26b8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_assign_about_blank.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<title>location.assign with initial about:blank browsing context</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe></iframe>
+<script>
+var t = async_test();
+var history_length;
+onload = t.step_func(function() {
+ setTimeout(t.step_func(function() {
+ var iframe = document.getElementsByTagName("iframe")[0];
+ iframe.onload = t.step_func(function() {
+ setTimeout(t.step_func(function() {
+ assert_equals(history.length, history_length);
+ t.done();
+ }), 100);
+ });
+ history_length = history.length;
+ iframe.src = "location_assign_about_blank-1.html"
+ }), 100);
+});
+</script>
+
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location_hash.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_hash.html
new file mode 100644
index 0000000000..c063e285ea
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_hash.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>location_hash</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <iframe id="srcdoc-iframe"
+ srcdoc="<div style='height: 200vh'></div><div id='test'></div>"></iframe>
+ <script>
+ function resetHash() {
+ location.hash = "";
+ }
+
+ test(function (t) {
+ t.add_cleanup(resetHash);
+ window.history.pushState(1, document.title, '#x=1');
+ var hash = location.hash;
+
+ assert_equals(hash, "#x=1", "hash");
+
+ }, "location hash");
+
+ var t = async_test("Setting location.hash on srcdoc iframe");
+ addEventListener("load", t.step_func_done(function() {
+ var frameWin = document.getElementById("srcdoc-iframe").contentWindow;
+ assert_equals(frameWin.location.href, "about:srcdoc");
+ assert_equals(frameWin.scrollY, 0, "Should not have scrolled yet");
+ frameWin.location.hash = "test";
+ assert_equals(frameWin.location.href, "about:srcdoc#test");
+ assert_true(frameWin.scrollY > frameWin.innerHeight,
+ "Should have scrolled by more than one viewport height");
+ }));
+
+ test(function(t) {
+ t.add_cleanup(resetHash);
+ location.hash = "test";
+ assert_equals(location.hash, "#test");
+ }, "Setting hash should automatically include hash character");
+
+ test(function(t) {
+ t.add_cleanup(resetHash);
+ location.hash = "#not encoded";
+ assert_equals(location.hash, "#not%20encoded");
+ }, "Setting hash should encode incompatible characters");
+
+ test(function(t) {
+ t.add_cleanup(resetHash);
+ location.hash = "#already%20encoded";
+ assert_equals(location.hash, "#already%20encoded");
+ }, "Setting hash to an already encoded value should not double encode it");
+
+ test(function(t) {
+ t.add_cleanup(resetHash);
+ location.hash = "#mixed encoding%20here";
+ assert_equals(location.hash, "#mixed%20encoding%20here");
+ }, "Setting hash which is partially encoded should only encode incompatible characters");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location_host.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_host.html
new file mode 100644
index 0000000000..d93bf47e50
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_host.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>location_host</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function () {
+ var host = location.host;
+ var url = location.href;
+
+ var pos = url.indexOf("//");
+ if (pos != -1) {
+ url = url.substr(pos+2, url.length-pos-2);
+ pos = url.indexOf("/");
+ if (pos != -1)
+ url = url.substr(0, pos);
+ }
+
+ assert_equals(host, url, "host");
+
+ }, "location host");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location_hostname.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_hostname.html
new file mode 100644
index 0000000000..2ffa0e5fc8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_hostname.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>location_hostname</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function () {
+ var hostname = location.hostname;
+ var url = location.href;
+
+ var pos = url.indexOf("//");
+ if (pos != -1) {
+ url = url.substr(pos+2, url.length-pos-2);
+ pos = url.indexOf(":");
+ if (pos != -1) {
+ url = url.substr(0, pos);
+ } else {
+ pos = url.indexOf("/");
+ if (pos != -1)
+ url = url.substr(0, pos);
+ }
+ }
+
+ assert_equals(hostname, url, "hostname");
+
+ }, "location hostname");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location_href.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_href.html
new file mode 100644
index 0000000000..1aa85dcdc8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_href.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>location_href</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function () {
+ var href = location.href;
+
+ assert_equals(href, document.URL, "href");
+
+ }, "location href");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location_origin.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_origin.html
new file mode 100644
index 0000000000..2325f4018a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_origin.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset="utf-8">
+<title></title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ test(function () {
+ assert_equals(
+ location.origin,
+ location.protocol + '//' + location.host,
+ "origin"
+ );
+ }, "location origin");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location_pathname.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_pathname.html
new file mode 100644
index 0000000000..dea05d2f37
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_pathname.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>location_pathname</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function () {
+ var pathname = location.pathname;
+ var url = location.href
+
+ url = url.replace(location.protocol + "//" + location.host, "");
+
+ assert_equals(pathname, url, "pathname");
+
+ }, "location pathname");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location_port.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_port.html
new file mode 100644
index 0000000000..fa1308ca5d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_port.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>location_port</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function () {
+ var port = location.port;
+ var url = location.href;
+
+ var pos = url.indexOf("//");
+ if (pos != -1) {
+ url = url.substr(pos+2, url.length-pos-2);
+ pos = url.indexOf("/");
+ if (pos != -1)
+ url = url.substr(0, pos);
+ pos = url.indexOf(":");
+ if (pos != -1)
+ url = url.substr(pos+1, url.length-pos-1);
+ }
+
+ assert_equals(port, url, "port");
+
+ }, "location port");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location_protocol.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_protocol.html
new file mode 100644
index 0000000000..d28bd56393
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_protocol.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>location_protocol</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function () {
+ var protocol = location.protocol;
+ var url = location.href;
+
+ var pos = url.indexOf("//");
+ if (pos != -1) {
+ url = url.substr(0, pos);
+ }
+
+ assert_equals(protocol, url, "protocol");
+
+ }, "location protocol");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location_reload-iframe.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_reload-iframe.html
new file mode 100644
index 0000000000..f08cf5de3e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_reload-iframe.html
@@ -0,0 +1,4 @@
+<script>
+ parent._ping(window.location.href)
+ if (parent._pingCount < 5) { location.reload(); }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location_reload.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_reload.html
new file mode 100644
index 0000000000..0a2d21d8d2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_reload.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>location_reload</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body onload="startTest();">
+ <div id="log"></div>
+
+ <iframe></iframe>
+
+ <script>
+ var history_length;
+ async_test(function(t) {
+
+ var url = new URL("./location_reload-iframe.html", window.location).href;
+
+ window._pingCount = 0;
+ window._ping = t.step_func(function(innerURL) {
+ // Some browsers keep 'about:blank' in the session history
+ if (_pingCount == 0) {
+ history_length = history.length;
+ }
+ assert_equals(url, innerURL, "iframe url (" + _pingCount + ")");
+ assert_equals(history_length, history.length, "history length (" + _pingCount + ")");
+ _pingCount++;
+ if (_pingCount == 5) {
+ t.done();
+ }
+ });
+ });
+
+ function startTest() {
+ var url = new URL("./location_reload-iframe.html", window.location).href;
+ var iframe = document.querySelector("iframe");
+ iframe.src = url;
+ history_length = history.length;
+ }
+ </script>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location_reload_javascript_url.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_reload_javascript_url.html
new file mode 100644
index 0000000000..737cafbcd3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_reload_javascript_url.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>location_reload_javascript_url</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+
+ <iframe></iframe>
+
+ <script>
+ async_test(function(t) {
+ const URL = "/common/blank.html";
+ const URL2 = "/common/blank.html#foo";
+ const JS_URL_TEXT = "javascript generated page";
+ const JS_URL = "javascript:'<html>" + JS_URL_TEXT + "</html>'";
+
+ var iframe = document.querySelector("iframe");
+ var count = 0;
+ iframe.onload = t.step_func(function() {
+ // The URL should initially be "blank.html", and then "blank.html#foo";
+ // The textContent of the iframe's document should initially be blank,
+ // then become js generated text, and then be blank again after reload.
+ switch (count) {
+ case 0:
+ assert_equals(iframe.contentWindow.document.URL,
+ location.href.replace(location.pathname, URL),
+ "iframe url (" + count + ")");
+ assert_equals(iframe.contentDocument.body.textContent, "",
+ "text of blank page");
+ iframe.contentWindow.location = JS_URL;
+ iframe.contentWindow.location = URL2;
+ break;
+ case 1:
+ assert_equals(iframe.contentWindow.document.URL,
+ location.href.replace(location.pathname, URL2),
+ "iframe url (" + count + ")");
+ assert_equals(iframe.contentDocument.body.textContent,
+ JS_URL_TEXT, "text of js generated page");
+ iframe.contentWindow.location.reload();
+ break;
+ case 2:
+ assert_equals(iframe.contentWindow.document.URL,
+ location.href.replace(location.pathname, URL2),
+ "iframe url (" + count + ")");
+ assert_equals(iframe.contentDocument.body.textContent, "",
+ "text of blank page");
+ t.done();
+ break;
+ }
+ count++;
+ });
+ iframe.src = URL;
+ });
+ </script>
+
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location_replace.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_replace.html
new file mode 100644
index 0000000000..0593420d40
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_replace.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>location_replace</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function () {
+ var href = location.href;
+ location.replace('#x');
+
+ assert_equals((href + "#x"), location.href, "location href");
+
+ }, "location replace");
+
+ test(function () {
+ var href = location.href;
+ assert_throws_dom('SYNTAX_ERR', function() { location.replace("//"); });
+ assert_equals(location.href, href);
+ }, "URL that fails to parse");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location_search.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_search.html
new file mode 100644
index 0000000000..f9db757841
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/location_search.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>location_search</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function () {
+ window.history.pushState(1, document.title, '?x=1');
+ var search = location.search;
+
+ assert_equals(search, "?x=1", "search");
+
+ }, "location search");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/no-browsing-context.window.js b/testing/web-platform/tests/html/browsers/history/the-location-interface/no-browsing-context.window.js
new file mode 100644
index 0000000000..4077d90971
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/no-browsing-context.window.js
@@ -0,0 +1,86 @@
+test(() => {
+ const frame = document.body.appendChild(document.createElement("iframe")),
+ win = frame.contentWindow,
+ loc = win.location;
+ frame.remove();
+ assert_equals(win.location, loc);
+}, "Window and Location are 1:1 after browsing context removal");
+
+function bcLessLocation() {
+ const frame = document.body.appendChild(document.createElement("iframe")),
+ win = frame.contentWindow,
+ loc = win.location;
+ frame.remove();
+ return loc;
+}
+
+[
+ {
+ "property": "href",
+ "expected": "about:blank",
+ "values": ["https://example.com/", "/", "http://test:test/", "test test", "test:test", "chrome:fail"]
+ },
+ {
+ "property": "protocol",
+ "expected": "about:",
+ "values": ["http", "about", "test"]
+ },
+ {
+ "property": "host",
+ "expected": "",
+ "values": ["example.com", "test test", "()"]
+ },
+ {
+ "property": "hostname",
+ "expected": "",
+ "values": ["example.com"]
+ },
+ {
+ "property": "port",
+ "expected": "",
+ "values": ["80", "", "443", "notaport"]
+ },
+ {
+ "property": "pathname",
+ "expected": "blank",
+ "values": ["/", "x"]
+ },
+ {
+ "property": "search",
+ "expected": "",
+ "values": ["test"]
+ },
+ {
+ "property": "hash",
+ "expected": "",
+ "values": ["test", "#"]
+ }
+].forEach(testSetup => {
+ testSetup.values.forEach(value => {
+ test(() => {
+ const loc = bcLessLocation();
+ loc[testSetup.property] = value;
+ assert_equals(loc[testSetup.property], testSetup.expected);
+ }, "Setting `" + testSetup.property + "` to `" + value + "` of a `Location` object sans browsing context is a no-op");
+ });
+});
+
+test(() => {
+ const loc = bcLessLocation();
+ assert_equals(loc.origin, "null");
+}, "Getting `origin` of a `Location` object sans browsing context should be \"null\"");
+
+["assign", "replace", "reload"].forEach(method => {
+ ["about:blank", "https://example.com/", "/", "http://test:test/", "test test", "test:test", "chrome:fail"].forEach(value => {
+ test(() => {
+ const loc = bcLessLocation();
+ loc[method](value);
+ assert_equals(loc.href, "about:blank");
+ }, "Invoking `" + method + "` with `" + value + "` on a `Location` object sans browsing context is a no-op");
+ });
+});
+
+test(() => {
+ const loc = bcLessLocation();
+ assert_array_equals(loc.ancestorOrigins, []);
+}, "Getting `ancestorOrigins` of a `Location` object sans browsing context should be []");
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_assign_during_load-1.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_assign_during_load-1.html
new file mode 100644
index 0000000000..c762ece3bc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_assign_during_load-1.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<script>
+opener.history_length = history.length;
+</script>
+<a onclick="location = 'manual_click_assign_during_load-2.html'; return false;" href>Click Here</a>
+<p>Filler image to keep the page loading:</p>
+<img src="/images/smiley.png?pipe=trickle(20:d1:r2)">
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_assign_during_load-2.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_assign_during_load-2.html
new file mode 100644
index 0000000000..1bf7f41e00
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_assign_during_load-2.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<p>This window should close itself and the test result appear in the original window
+<script>
+onload = function() {
+ setTimeout(function() {opener.do_test(history.length); window.close();}, 100);
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_assign_during_load-manual.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_assign_during_load-manual.html
new file mode 100644
index 0000000000..45a0e3e2fd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_assign_during_load-manual.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<title>Assignment to location with click during load</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<p>The popup blocker must be disabled for this test</p>
+<div id="log"></div>
+<script>
+setup({timeout:3600000});
+var t = async_test();
+var win = window.open("manual_click_assign_during_load-1.html");
+
+var history_length;
+do_test = t.step_func(function(new_length) {
+ assert_equals(new_length, history_length + 1);
+ t.done();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_location_replace_during_load-1.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_location_replace_during_load-1.html
new file mode 100644
index 0000000000..e9d03e9364
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_location_replace_during_load-1.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<script>
+opener.history_length = history.length;
+</script>
+<a onclick="location.replace('manual_click_location_replace_during_load-2.html'); return false;" href>Click Here</a>
+<p>Filler image to keep the page loading:</p>
+<img>
+<script>
+document.images[0].src = "/images/smiley.png?pipe=trickle(20:d1:r2)&random=" + Math.random();
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_location_replace_during_load-2.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_location_replace_during_load-2.html
new file mode 100644
index 0000000000..1bf7f41e00
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_location_replace_during_load-2.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<p>This window should close itself and the test result appear in the original window
+<script>
+onload = function() {
+ setTimeout(function() {opener.do_test(history.length); window.close();}, 100);
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_location_replace_during_load-manual.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_location_replace_during_load-manual.html
new file mode 100644
index 0000000000..a453de34bf
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_location_replace_during_load-manual.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<title>location.replace with click during load</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<p>The popup blocker must be disabled for this test</p>
+<div id="log"></div>
+<script>
+setup({timeout:3600000});
+var t = async_test();
+var win = window.open("manual_click_location_replace_during_load-1.html");
+
+var history_length;
+do_test = t.step_func(function(new_length) {
+ assert_equals(new_length, history_length);
+ t.done();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_replace_during_load-manual.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_replace_during_load-manual.html
new file mode 100644
index 0000000000..482858bcd7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_click_replace_during_load-manual.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<title>Assignment to location with click during load</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<p>The popup blocker must be disabled for this test</p>
+<div id="log"></div>
+<script>
+setup({timeout:3600000});
+var t = async_test();
+var win = window.open("manual_click_replace_during_load-1.html");
+
+var history_length;
+do_test = t.step_func(function(new_length) {
+ assert_equals(new_length, history_length);
+ t.done();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_form_submit_assign_during_load-1.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_form_submit_assign_during_load-1.html
new file mode 100644
index 0000000000..08f7e2dd68
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_form_submit_assign_during_load-1.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<script>
+opener.history_length = history.length;
+</script>
+<form onsubmit="location = 'manual_form_submit_assign_during_load-2.html'; return false;">
+<input type=submit value="Click Me">
+</form>
+<p>Filler image to keep the page loading:</p>
+<img src="/images/smiley.png?pipe=trickle(20:d1:r2)">
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_form_submit_assign_during_load-2.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_form_submit_assign_during_load-2.html
new file mode 100644
index 0000000000..1bf7f41e00
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_form_submit_assign_during_load-2.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<p>This window should close itself and the test result appear in the original window
+<script>
+onload = function() {
+ setTimeout(function() {opener.do_test(history.length); window.close();}, 100);
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_form_submit_assign_during_load-manual.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_form_submit_assign_during_load-manual.html
new file mode 100644
index 0000000000..d71b206ac5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/manual_form_submit_assign_during_load-manual.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<title>Assignment to location with form submit during load</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<p>The popup blocker must be disabled for this test</p>
+<div id="log"></div>
+<script>
+setup({timeout:3600000});
+var t = async_test();
+var win = window.open("manual_form_submit_assign_during_load-1.html");
+
+var history_length;
+do_test = t.step_func(function(new_length) {
+ assert_equals(new_length, history_length + 1);
+ t.done();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/reload_in_resize-1.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/reload_in_resize-1.html
new file mode 100644
index 0000000000..05b44f4c40
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/reload_in_resize-1.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<p>Resize this window. FAIL if the window doesn't close shortly afterwards.</p>
+<script>
+onload = opener.t.step_func(function() {
+ opener.load_count++;
+ if (opener.load_count > 1) {
+ opener.do_test();
+ }
+})
+
+onresize = opener.t.step_func(function() {
+ opener.flag_resized();
+ location.reload();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/reload_in_resize-manual.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/reload_in_resize-manual.html
new file mode 100644
index 0000000000..5cb3fac964
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/non-automated/reload_in_resize-manual.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<title>Reload called from resize event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<p>Resize the popup window. That window should then close and the result be presented here. If that window doesn't close after resize that's a FAIL.</p>
+<div id="log"></div>
+<script>
+setup({timeout:3600000})
+var t = async_test();
+var load_count = 0;
+var resized = false;
+var win = window.open("reload_in_resize-1.html")
+
+flag_resized = t.step_func(function() {
+ resized = true;
+ setTimeout(do_test, 1000);
+});
+
+do_test = t.step_func(function() {
+ win.close();
+ assert_true(resized, "Resize event happened");
+ assert_equals(load_count, 1, "Number of load events");
+ t.done();
+});
+</script>
+
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/per-global.window.js b/testing/web-platform/tests/html/browsers/history/the-location-interface/per-global.window.js
new file mode 100644
index 0000000000..b2956fd21f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/per-global.window.js
@@ -0,0 +1,3 @@
+// META: script=/common/object-association.js
+
+testIsPerWindow("location");
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_open_write-1.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_open_write-1.html
new file mode 100644
index 0000000000..e1a2e811c9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_open_write-1.html
@@ -0,0 +1,19 @@
+<!doctype html>
+1
+<script>
+function f() {
+ opener.postMessage("original", "*");
+ if (opener.data.length >= 2) {
+ // If we proceed here, then our document.write will be racing with the
+ // setTimeout in our opener. Just stop.
+ return;
+ }
+ setTimeout(function () {
+ document.open();
+ document.write("<!doctype html>2<script>opener.postMessage('written', '*');<\/script>");
+ document.close();
+ });
+}
+
+window.onload = f
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_open_write.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_open_write.html
new file mode 100644
index 0000000000..905ef88743
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_open_write.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<title>Reload document with document.open and document.written content</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var win = window.open("reload_document_open_write-1.html");
+var t = async_test();
+
+var data = [];
+
+window.onmessage = t.step_func(function(e) {
+ data.push(e.data);
+ if (data.length == 2) {
+ win.location.reload();
+ } else if (data.length >= 3) {
+ setTimeout(t.step_func(function() {
+ assert_array_equals(data, ["original", "written", "original"]);
+ t.done();
+ }), 500);
+ }
+});
+
+add_completion_callback(function() {win.close()});
+</script>
+
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_write-1.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_write-1.html
new file mode 100644
index 0000000000..9a08433920
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_write-1.html
@@ -0,0 +1,4 @@
+<script>
+document.write(Math.random());
+opener.postMessage(document.body.innerHTML, "*");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_write.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_write.html
new file mode 100644
index 0000000000..fb5fddc7da
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_write.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<title>Reload document with document.written content</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var win = window.open("reload_document_write-1.html");
+var t = async_test();
+
+window.onmessage = t.step_func(function(e) {
+ var initial_value = e.data;
+ win.location.reload();
+ window.onmessage = t.step_func(function(e) {
+ assert_not_equals(e.data, initial_value);
+ t.done();
+ });
+});
+
+add_completion_callback(function() {win.close()});
+</script>
+
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_write_onload-1.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_write_onload-1.html
new file mode 100644
index 0000000000..36445af3c8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_write_onload-1.html
@@ -0,0 +1,9 @@
+<script>
+function f() {
+ opener.postMessage("original", "*");
+ document.write("<!doctype html>2<script>opener.postMessage('written', '*');<\/script>");
+ document.close();
+}
+
+window.onload = f
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_write_onload.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_write_onload.html
new file mode 100644
index 0000000000..b2cf31147a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_write_onload.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<title>Reload document with document.written content written in load event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var win = window.open("reload_document_write_onload-1.html");
+var t = async_test();
+
+var data = [];
+
+window.onmessage = t.step_func(function(e) {
+ data.push(e.data);
+ if (data.length < 3) {
+ win.location.reload();
+ } else {
+ setTimeout(t.step_func(function() {
+ assert_array_equals(data, ["original", "written", "written"]);
+ t.done();
+ }), 500);
+ }
+});
+
+add_completion_callback(function() {win.close()});
+</script>
+
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_post_1-manual.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_post_1-manual.html
new file mode 100644
index 0000000000..95ef660559
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_post_1-manual.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<title>Reload document with POST</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var win = window.open("resources/reload_post_1-1.py");
+var t = async_test();
+var posted = false;
+var reloaded = false;
+
+next = t.step_func(function() {
+
+if (posted && !reloaded) {
+ reloaded = true;
+ win.location.reload();
+} else if (posted && reloaded) {
+ t.done();
+} else {
+ posted = true;
+ win.document.forms[0].submit();
+}
+
+});
+
+add_completion_callback(function() {win.close()});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/resources/post-your-origin.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/resources/post-your-origin.html
new file mode 100644
index 0000000000..a8a614c182
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/resources/post-your-origin.html
@@ -0,0 +1,3 @@
+<script>
+parent.postMessage({origin: location.origin}, "*");
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/resources/post-your-protocol.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/resources/post-your-protocol.html
new file mode 100644
index 0000000000..c50d623893
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/resources/post-your-protocol.html
@@ -0,0 +1,4 @@
+<script>
+ var id = location.search.substring(1);
+ parent.postMessage({ id: id, protocol: location.protocol }, "*");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/resources/reload_post_1-1.py b/testing/web-platform/tests/html/browsers/history/the-location-interface/resources/reload_post_1-1.py
new file mode 100644
index 0000000000..56397f07b4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/resources/reload_post_1-1.py
@@ -0,0 +1,13 @@
+def main(request, response):
+ headers = [(b"Content-Type", b"text/html")]
+ return headers, u'''
+ <script>
+ onload = function() {opener.next()}
+ document.write(Math.random());
+ </script>
+ <form method="POST" action="">
+ <input type=hidden name=test value=test>
+ <input type=submit>
+ </form>
+ <button onclick="location.reload()">Reload</button>
+ '''
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/same-hash.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/same-hash.html
new file mode 100644
index 0000000000..430a57662a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/same-hash.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Using the location interface to navigate to the same hash as the current one</title>
+<link rel="help" href="https://github.com/whatwg/html/issues/7386">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe id="i" srcdoc="<div style='height: 200vh'></div><div id='te&lt;st'></div>"></iframe>
+
+<script type="module">
+setup({ explicit_done: true });
+await new Promise(r => window.onload = r);
+
+for (const value of ["#te<st", "te<st", "#te%3Cst", "te%3Cst"]) {
+ promise_test(async t => {
+ t.add_cleanup(() => { i.contentWindow.location.hash = ""; });
+ assert_equals(i.contentWindow.scrollY, 0, "Setup: iframe starts at top");
+
+ i.contentWindow.location.hash = "te<st";
+ await delayForFragmentNavigationScrolling(t);
+
+ assert_greater_than(i.contentWindow.scrollY, i.contentWindow.innerHeight, "First hash assignment scrolls the iframe");
+
+ i.contentWindow.scroll({ top: 0, behavior: "instant" });
+ assert_equals(i.contentWindow.scrollY, 0, "Resetting the scroll position must work");
+
+ i.contentWindow.location.hash = value;
+ await delayForFragmentNavigationScrolling(t);
+
+ assert_equals(i.contentWindow.scrollY, 0, "Reassigning the same hash must not change the scroll position");
+ }, `Using location.hash = "${value}" must not reset scroll position`);
+}
+
+// These don't canonicalize to the current value of location.hash; the post-parsing version of
+// "te<st" is "te%3Cst", uppercase.
+for (const value of ["#te%3cst", "te%3cst"]) {
+ promise_test(async t => {
+ t.add_cleanup(() => { i.contentWindow.location.hash = ""; });
+ assert_equals(i.contentWindow.scrollY, 0, "Setup: iframe starts at top");
+
+ i.contentWindow.location.hash = "te<st";
+ await delayForFragmentNavigationScrolling(t);
+
+ assert_greater_than(i.contentWindow.scrollY, i.contentWindow.innerHeight, "First hash assignment scrolls the iframe");
+
+ i.contentWindow.scroll({ top: 0, behavior: "instant" });
+ assert_equals(i.contentWindow.scrollY, 0, "Resetting the scroll position must work");
+
+ i.contentWindow.location.hash = value;
+ await delayForFragmentNavigationScrolling(t);
+
+ assert_greater_than(i.contentWindow.scrollY, i.contentWindow.innerHeight, "Reassigning the same-ish hash scrolls the iframe");
+ }, `Using location.hash = "${value}" must reset scroll position`);
+}
+
+for (const value of ["about:srcdoc#te<st", "about:srcdoc#te%3cst", "about:srcdoc#te%3Cst"]) {
+ promise_test(async t => {
+ t.add_cleanup(() => { i.contentWindow.location.hash = ""; });
+ assert_equals(i.contentWindow.scrollY, 0, "Setup: iframe starts at top");
+
+ i.contentWindow.location.hash = "te<st";
+ await delayForFragmentNavigationScrolling(t);
+
+ assert_greater_than(i.contentWindow.scrollY, i.contentWindow.innerHeight, "First hash assignment scrolls the iframe");
+
+ i.contentWindow.scroll({ top: 0, behavior: "instant" });
+ assert_equals(i.contentWindow.scrollY, 0, "Resetting the scroll position must work");
+
+ i.contentWindow.location.href = value;
+ await delayForFragmentNavigationScrolling(t);
+
+ assert_greater_than(i.contentWindow.scrollY, i.contentWindow.innerHeight, "Setting href must scroll the iframe");
+ }, `Using location.href = "${value}" must reset scroll position`);
+
+ promise_test(async t => {
+ t.add_cleanup(() => { i.contentWindow.location.hash = ""; });
+ assert_equals(i.contentWindow.scrollY, 0, "Setup: iframe starts at top");
+
+ i.contentWindow.location.hash = "te<st";
+ await delayForFragmentNavigationScrolling(t);
+
+ assert_greater_than(i.contentWindow.scrollY, i.contentWindow.innerHeight, "First hash assignment scrolls the iframe");
+
+ i.contentWindow.scroll({ top: 0, behavior: "instant" });
+ assert_equals(i.contentWindow.scrollY, 0, "Resetting the scroll position must work");
+
+ i.contentWindow.location.assign(value);
+ await delayForFragmentNavigationScrolling(t);
+
+ assert_greater_than(i.contentWindow.scrollY, i.contentWindow.innerHeight, "Setting href must scroll the iframe");
+ }, `Using location.assign("${value}") must reset scroll position`);
+}
+
+function delayForFragmentNavigationScrolling(t) {
+ // Scroll behavior for fragment navigation is set to "auto" in the spec, so we can't guarantee it's instant.
+ // In practice 10 milliseconds seems to be enough.
+ return new Promise(r => t.step_timeout(r, 10));
+}
+
+done();
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/same_origin_frame.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/same_origin_frame.html
new file mode 100644
index 0000000000..953e696b2a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/same_origin_frame.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Same-origin subframe for Location cyclic [[Prototype]] test</title>
+ <link rel="author" title="Jeff Walden" href="http://whereswalden.com/" />
+</head>
+<body>
+<!-- nothing to do, this window should be accessible to the parent frame -->
+<p>Same-origin iframe</p>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_assign_during_load-1.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_assign_during_load-1.html
new file mode 100644
index 0000000000..9561cabdd1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_assign_during_load-1.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<script>
+opener.history_length = history.length;
+</script>
+<a onclick="location = 'scripted_click_assign_during_load-2.html'; return false;" href>Click Here</a>
+<script>
+document.links[0].click()
+</script>
+<p>Filler image to keep the page loading:</p>
+<img src="/images/smiley.png?pipe=trickle(20:d1:r2)">
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_assign_during_load-2.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_assign_during_load-2.html
new file mode 100644
index 0000000000..1bf7f41e00
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_assign_during_load-2.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<p>This window should close itself and the test result appear in the original window
+<script>
+onload = function() {
+ setTimeout(function() {opener.do_test(history.length); window.close();}, 100);
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_assign_during_load.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_assign_during_load.html
new file mode 100644
index 0000000000..7ccc6cdc09
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_assign_during_load.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<title>Assignment to location with click during load</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<p>The popup blocker must be disabled for this test</p>
+<div id="log"></div>
+<script>
+setup({timeout:3600000});
+var t = async_test();
+var win = window.open("scripted_click_assign_during_load-1.html");
+
+var history_length;
+do_test = t.step_func(function(new_length) {
+ assert_equals(new_length, history_length);
+ t.done();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_location_assign_during_load-1.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_location_assign_during_load-1.html
new file mode 100644
index 0000000000..05bb42f967
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_location_assign_during_load-1.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<script>
+opener.history_length = history.length;
+</script>
+<a onclick="location.assign('scripted_click_location_assign_during_load-2.html'); return false;" href>Click Here</a>
+<script>
+document.links[0].click()
+</script>
+<p>Filler image to keep the page loading:</p>
+<img>
+<script>
+document.images[0].src = "/images/smiley.png?pipe=trickle(20:d1:r2)&random=" + Math.random()
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_location_assign_during_load-2.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_location_assign_during_load-2.html
new file mode 100644
index 0000000000..1bf7f41e00
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_location_assign_during_load-2.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<p>This window should close itself and the test result appear in the original window
+<script>
+onload = function() {
+ setTimeout(function() {opener.do_test(history.length); window.close();}, 100);
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_location_assign_during_load.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_location_assign_during_load.html
new file mode 100644
index 0000000000..64f3ff942f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_click_location_assign_during_load.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<title>location.assign with click during load</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<p>The popup blocker must be disabled for this test</p>
+<div id="log"></div>
+<script>
+var t = async_test();
+var win = window.open("scripted_click_location_assign_during_load-1.html");
+
+var history_length;
+do_test = t.step_func(function(new_length) {
+ assert_equals(new_length, history_length + 1);
+ t.done();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_form_submit_assign_during_load-1.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_form_submit_assign_during_load-1.html
new file mode 100644
index 0000000000..ae07ac5cfc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_form_submit_assign_during_load-1.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<script>
+opener.history_length = history.length;
+</script>
+<form onsubmit="location = 'scripted_form_submit_assign_during_load-2.html'; return false;">
+<input type=submit value="Click Me">
+</form>
+<script>
+document.forms[0].elements[0].click()
+</script>
+<p>Filler image to keep the page loading:</p>
+<img src="/images/smiley.png?pipe=trickle(20:d1:r2)">
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_form_submit_assign_during_load-2.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_form_submit_assign_during_load-2.html
new file mode 100644
index 0000000000..1bf7f41e00
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_form_submit_assign_during_load-2.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<p>This window should close itself and the test result appear in the original window
+<script>
+onload = function() {
+ setTimeout(function() {opener.do_test(history.length); window.close();}, 100);
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_form_submit_assign_during_load.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_form_submit_assign_during_load.html
new file mode 100644
index 0000000000..2a6eba63ab
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/scripted_form_submit_assign_during_load.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<title>Assignment to location with form submit during load</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<p>The popup blocker must be disabled for this test</p>
+<div id="log"></div>
+<script>
+setup({timeout:3600000});
+var t = async_test();
+var win = window.open("scripted_form_submit_assign_during_load-1.html");
+
+var history_length;
+do_test = t.step_func(function(new_length) {
+ assert_equals(new_length, history_length);
+ t.done();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/security_location_0.htm b/testing/web-platform/tests/html/browsers/history/the-location-interface/security_location_0.htm
new file mode 100644
index 0000000000..f509c23b18
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/security_location_0.htm
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Location interface Security</title>
+ <link rel="author" title="Microsoft" href="http://www.microsoft.com/" />
+ <link rel="help" href="https://html.spec.whatwg.org/multipage/#security-location" />
+ <meta name="assert" content="access location object from different origins doesn't raise SECURITY_ERR exception" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <p>Access location object from different origins doesn't raise SECURITY_ERR exception</p>
+ <div id=log></div>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script>
+ var runTest = async_test("Accessing location object from different origins doesn't raise SECURITY_ERR exception").step_func_done(function() {
+ var frame = document.getElementById('testframe');
+ frame.setAttribute('onload', '');
+ frame.contentWindow.location = get_host_info().HTTP_REMOTE_ORIGIN + "/";
+ });
+ </script>
+ <iframe id='testframe' onload="runTest()">Test Frame</iframe>
+ <script>
+ document.getElementById('testframe').setAttribute('src', get_host_info().HTTP_REMOTE_ORIGIN + '/');
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload.html b/testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload.html
new file mode 100644
index 0000000000..367d7eea3e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload.html
@@ -0,0 +1,37 @@
+
+<!doctype html>
+<meta charset=utf-8>
+<title>Navigation in onload handler</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+ var testFiles = [
+ "navigation-in-onload_form-submission-1.html",
+ "navigation-in-onload_form-submission-iframe.html",
+ "navigation-in-onload_form-submission-dynamic-iframe.html"
+ ]
+
+ var t = async_test();
+
+ function scheduleNextTest() {
+ setTimeout(runNextTest, 0);
+ }
+
+ function runNextTest() {
+ var file = testFiles.shift();
+ if (!file) {
+ t.done();
+ return;
+ }
+
+ window.open(file);
+ }
+
+ function verify(actual, expected, desc) {
+ setTimeout(t.step_func(function() {
+ assert_equals(actual, expected, desc);
+ }), 0);
+ }
+
+</script>
+<body onload="scheduleNextTest();"></body>
diff --git a/testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload_form-submission-1.html b/testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload_form-submission-1.html
new file mode 100644
index 0000000000..2079224467
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload_form-submission-1.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Navigation in onload handler through form submission</title>
+ <script>
+ function redirect() {
+ document.querySelector("#redirectionForm").submit();
+ }
+ </script>
+ </head>
+ <body onload="redirect();">
+ <form id="redirectionForm" action="navigation-in-onload_form-submission-2.html" method="get"></form>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload_form-submission-2.html b/testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload_form-submission-2.html
new file mode 100644
index 0000000000..11fbe2a2ba
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload_form-submission-2.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Navigation in onload handler through form submission</title>
+ <script>
+
+ // Verify is called after onload event to ensure history has been stable.
+ function verify() {
+ // Navigation in onload handler through form submission should not
+ // increse history length.
+ var runner = window.top.opener;
+ runner.verify(history.length, 1,
+ "history.length of subtest '" + top.document.title + "'.");
+ runner.scheduleNextTest();
+ setTimeout(window.close.bind(top), 0);
+ }
+ </script>
+ </head>
+ <body onload="setTimeout(verify, 0);">
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload_form-submission-dynamic-iframe.html b/testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload_form-submission-dynamic-iframe.html
new file mode 100644
index 0000000000..aabae3d770
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload_form-submission-dynamic-iframe.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Navigation in onload handler through form submission in a dynamically created iframe</title>
+ <script>
+ function test() {
+ let testFrame = document.createElement("iframe");
+ testFrame.src = "navigation-in-onload_form-submission-1.html";
+ document.body.appendChild(testFrame);
+ }
+ </script>
+ </head>
+ <body onload="test();">
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload_form-submission-iframe.html b/testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload_form-submission-iframe.html
new file mode 100644
index 0000000000..5fa786d1f5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload_form-submission-iframe.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Navigation in onload handler through form submission in an iframe</title>
+ </head>
+ <body>
+ <iframe id="testFrame" src="navigation-in-onload_form-submission-1.html"></iframe>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/offline/application-cache-api/api_status_checking-manual.html b/testing/web-platform/tests/html/browsers/offline/application-cache-api/api_status_checking-manual.html
new file mode 100644
index 0000000000..a4a3b41a7d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/application-cache-api/api_status_checking-manual.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html manifest="../resources/manifest/clock.manifest">
+ <head>
+ <title>Offline Application Cache - API_status_CHECKING</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <ol>
+ <li>Refresh the page.</li>
+ </ol>
+
+ <div id="log"></div>
+ <script>
+ var t = async_test("checking status test"),
+ cache = window.applicationCache;
+
+ cache.onchecking = t.step_func_done(function() {
+ assert_equals(cache.status, cache.CHECKING, "cache.status should equals cache.CHECKING");
+ });
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/offline/application-cache-api/api_status_downloading-manual.html b/testing/web-platform/tests/html/browsers/offline/application-cache-api/api_status_downloading-manual.html
new file mode 100644
index 0000000000..c09d11d787
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/application-cache-api/api_status_downloading-manual.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html manifest="../resources/manifest/clock.manifest">
+ <head>
+ <title>Offline Application Cache - API_status_DOWNLOADING</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <ol>
+ <li>Modify the commented part in the manifest file (manifest/clock.manifest) on the server.</li>
+ <li>Refresh the page.</li>
+ </ol>
+ <div id="log"></div>
+
+ <script>
+ var t = async_test("downloading status test"),
+ cache = window.applicationCache;
+
+ cache.ondownloading = t.step_func_done(function() {
+ assert_equals(cache.status, cache.DOWNLOADING, "cache.status should equals cache.DOWNLOADING");
+ });
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/offline/application-cache-api/api_status_obsolete-manual.html b/testing/web-platform/tests/html/browsers/offline/application-cache-api/api_status_obsolete-manual.html
new file mode 100644
index 0000000000..77005644a2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/application-cache-api/api_status_obsolete-manual.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html manifest="../resources/manifest/clock.manifest">
+ <head>
+ <title>Offline Application Cache - API_status_OBSOLETE</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <ol>
+ <li>Remove the manifest file (manifest/clock.manifest) from the server.</li>
+ <li>Refresh the page.</li>
+ </ol>
+ <div id="log"></div>
+
+ <script>
+ var t = async_test("obsolete status test"),
+ cache = window.applicationCache;
+
+ cache.onobsolete = t.step_func_done(function() {
+ assert_equals(cache.status, cache.OBSOLETE, "cache.status should equals cache.OBSOLETE");
+ });
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/offline/application-cache-api/api_status_updateready-manual.html b/testing/web-platform/tests/html/browsers/offline/application-cache-api/api_status_updateready-manual.html
new file mode 100644
index 0000000000..7e1533374b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/application-cache-api/api_status_updateready-manual.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html manifest="../resources/manifest/clock.manifest">
+ <head>
+ <title>Offline Application Cache - API_status_UPDATEREADY</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <ol>
+ <li>Modify the commented part in the manifest file (manifest/clock.manifest) on the server.</li>
+ <li>Refresh the page.</li>
+ </ol>
+
+ <div id="log"></div>
+
+ <script>
+ var t = async_test("updateready status test"),
+ cache = window.applicationCache;
+
+ cache.onupdateready = t.step_func_done(function() {
+ assert_equals(cache.status, cache.UPDATEREADY, "cache.status should equals cache.UPDATEREADY");
+ });
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/offline/application-cache-api/api_swapcache-manual.html b/testing/web-platform/tests/html/browsers/offline/application-cache-api/api_swapcache-manual.html
new file mode 100644
index 0000000000..6649d980f6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/application-cache-api/api_swapcache-manual.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html manifest="../resources/manifest/clock.manifest">
+ <head>
+ <title>Offline Application Cache - API_swapCache</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <ol>
+ <li>Modify the part of comment in manifest file of server.</li>
+ <li>Refresh the page.</li>
+ </ol>
+
+ <div id="log"></div>
+
+ <script>
+ var t = async_test("swapCache method test");
+ var cache = window.applicationCache;
+
+ cache.onupdateready = t.step_func(function() {
+ try {
+ cache.swapCache();
+ t.done();
+ } catch (e) {
+ assert_unreached("swapCache method failed.");
+ }
+ });
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/offline/browser-state/navigator_online_event-manual.https.html b/testing/web-platform/tests/html/browsers/offline/browser-state/navigator_online_event-manual.https.html
new file mode 100644
index 0000000000..81cad4f4fe
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/browser-state/navigator_online_event-manual.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Offline Application Cache</title>
+ <link rel="stylesheet" href="../resources/css/result.css">
+ </head>
+ <body>
+ <h1>navigator_online_event</h1>
+
+ <ol>
+ <li>Change the 'work offline' mode.</li>
+ <li>If actual result and expected result are same, then test is <span class="manualpass">Pass</span>, otherwise <span class="manualfail">Fail</span>.</li>
+ </ol>
+
+ <hr>
+
+ <h2>Actual Result</h2>
+ <div id="actualResult">
+ <span id="actualMsg"></span>
+ </div>
+
+ <h2>Expected Result</h2>
+ <div id="expectedResult">
+ <span id="expectedMsg">apply 'work offline': offline event is raised.<p>release 'work offline': online event is raised.</span>
+ </div>
+ <script>
+
+ function showOnline(e) {
+ let msg = 'online event is raised';
+ if (e.target != window)
+ msg += ' (on the WRONG target)';
+ document.getElementById('actualMsg').innerHTML = msg + '.';
+ }
+
+ function showOffline(e) {
+ let msg = 'offline event is raised';
+ if (e.target != window)
+ msg += ' (on the WRONG target)';
+ document.getElementById('actualMsg').innerHTML = msg + '.';
+ }
+
+ window.addEventListener("online", showOnline, false);
+ window.addEventListener("offline", showOffline, false);
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/offline/browser-state/navigator_online_online.https.html b/testing/web-platform/tests/html/browsers/offline/browser-state/navigator_online_online.https.html
new file mode 100644
index 0000000000..81547c3fb9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/browser-state/navigator_online_online.https.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Offline Application Cache - navigator_online_online</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+
+ <script>
+ test(function() {
+ assert_true(navigator.onLine, "onLine test");
+ });
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/offline/changestonetworkingmodel/original-id.json b/testing/web-platform/tests/html/browsers/offline/changestonetworkingmodel/original-id.json
new file mode 100644
index 0000000000..2f77367c8f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/changestonetworkingmodel/original-id.json
@@ -0,0 +1 @@
+{"original_id":"changesToNetworkingModel"} \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/offline/introduction-4/event_downloading-manual.html b/testing/web-platform/tests/html/browsers/offline/introduction-4/event_downloading-manual.html
new file mode 100644
index 0000000000..26b003f06e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/introduction-4/event_downloading-manual.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html manifest="../resources/manifest/clock.manifest">
+ <head>
+ <title>Offline Application Cache - Event_downloading</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <ol>
+ <li>Modify the commented part of the manifest file (manifest/clock.manifest) on the server.</li>
+ <li>Refresh the page.</li>
+ </ol>
+
+ <div id="log"></div>
+
+ <script>
+ var t = async_test("downloading event test");
+ var cache = window.applicationCache;
+
+ cache.ondownloading = t.done();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/offline/introduction-4/event_error-manual.html b/testing/web-platform/tests/html/browsers/offline/introduction-4/event_error-manual.html
new file mode 100644
index 0000000000..19abb3d6bf
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/introduction-4/event_error-manual.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html manifest="../resources/manifest/clock.manifest">
+ <head>
+ <title>Offline Application Cache - Event_error</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <ol>
+ <li>Add a dummy file in the manifest file (manifest/clock.manifest).</li>
+ <li>Refresh the page.</li>
+ </ol>
+
+ <div id="log"></div>
+
+ <script>
+ var t = async_test("error event test");
+ var cache = window.applicationCache;
+
+ cache.onerror = t.done();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/offline/introduction-4/event_obsolete-manual.html b/testing/web-platform/tests/html/browsers/offline/introduction-4/event_obsolete-manual.html
new file mode 100644
index 0000000000..cab5e01cc7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/introduction-4/event_obsolete-manual.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html manifest="../resources/manifest/clock.manifest">
+ <head>
+ <title>Offline Application Cache - Event_obsolete</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <ol>
+ <li>Remove the manifest file (manifest/clock.manifest) from the server.</li>
+ <li>Refresh the page.</li>
+ </ol>
+
+ <div id="log"></div>
+
+ <script>
+ var t = async_test("obsolete event test");
+ var cache = window.applicationCache;
+
+ cache.onobsolete = t.done();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/offline/introduction-4/event_updateready-manual.html b/testing/web-platform/tests/html/browsers/offline/introduction-4/event_updateready-manual.html
new file mode 100644
index 0000000000..4de435144d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/introduction-4/event_updateready-manual.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html manifest="../resources/manifest/clock.manifest">
+ <head>
+ <title>Offline Application Cache - Event_updateready</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <ol>
+ <li>Modify the commented part in the manifest file (manifest/clock.manifest) on the server.</li>
+ <li>Refresh the page.</li>
+ </ol>
+ <div id="log"></div>
+
+ <script>
+ var t = async_test("updateready event test");
+ var cache = window.applicationCache;
+
+ cache.onupdateready = t.done();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/offline/introduction-4/event_updateready_swapcache-manual.html b/testing/web-platform/tests/html/browsers/offline/introduction-4/event_updateready_swapcache-manual.html
new file mode 100644
index 0000000000..da6cead026
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/introduction-4/event_updateready_swapcache-manual.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html manifest="../resources/manifest/clock.manifest">
+ <head>
+ <title>Offline Application Cache - Event_updateready_swapCache</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <ol>
+ <li>Modify the commented part in manifest file (manifest/clock.manifest) on the server.</li>
+ <li>Refresh the page.</li>
+ </ol>
+
+ <div id="log"></div>
+
+ <script>
+ var t = async_test("swapCache method test after updateready event is raised");
+ var cache = window.applicationCache;
+
+ cache.onupdateready = t.step_func(function() {
+ try {
+ cache.swapCache();
+ t.done();
+ } catch (e) {
+ assert_unreached("swapCache method failed.");
+ }
+ })
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/offline/manifest_main_empty-manual.https.html b/testing/web-platform/tests/html/browsers/offline/manifest_main_empty-manual.https.html
new file mode 100644
index 0000000000..317aaa1137
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/manifest_main_empty-manual.https.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html manifest="resources/manifest/clock.manifest">
+ <head>
+ <title>Offline Application Cache - manifest_main_empty</title>
+ <link rel="stylesheet" href="resources/css/result.css">
+ </head>
+ <body>
+ <ol>
+ <li>Disable the network connection.</li>
+ <li>Refresh the page.</li>
+ <li>If the page is normally displayed, then test is <span class="manualpass"><b>PASS</b></span>, otherwise <span class="manualfail"><b>FAIL</b></span>.</li>
+ </ol>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/offline/manifest_notchanged_online-manual.https.html b/testing/web-platform/tests/html/browsers/offline/manifest_notchanged_online-manual.https.html
new file mode 100644
index 0000000000..a464b426a7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/manifest_notchanged_online-manual.https.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html manifest="resources/manifest/clock.manifest">
+ <head>
+ <title>Offline Application Cache - manifest_notchanged_online</title>
+ <script src="resources/js/clock.js"></script>
+ <link rel="stylesheet" href="resources/css/result.css">
+ <link rel="stylesheet" href="resources/css/clock.css">
+ <link rel="stylesheet" href="resources/css/online.css" type="text/css" media="screen">
+ </head>
+ <body>
+ <ol>
+ <li>Remove time element of this html document and not change manifest file.</li>
+ <li>Refresh the page.</li>
+ <li>If the page is normally displayed, then test is <span class="manualpass"><b>PASS</b></span>, otherwise <span class="manualfail"><b>FAIL</b></span>.</li>
+ </ol>
+
+ <p class="connectivity" width="600">The time is: <output id="clock"></output></p>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/offline/manifest_section_empty-manual.https.html b/testing/web-platform/tests/html/browsers/offline/manifest_section_empty-manual.https.html
new file mode 100644
index 0000000000..eea2dbba35
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/manifest_section_empty-manual.https.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html manifest="resources/manifest/section_empty.manifest">
+ <head>
+ <title>Offline Application Cache - manifest_section_empty</title>
+ <script src="resources/js/clock.js"></script>
+ <link rel="stylesheet" href="resources/css/result.css">
+ <link rel="stylesheet" href="resources/css/clock.css">
+ <link rel="stylesheet" href="resources/css/online.css" type="text/css" media="screen">
+ </head>
+ <body>
+ <ol>
+ <li>Disable the network connection.</li>
+ <li>Refresh the page.</li>
+ <li>If the time element and colors of result elements are normally displayed, then test is <span class="manualpass"><b>PASS</b></span>, otherwise <span class="manualfail"><b>FAIL</b></span>.</li>
+ </ol>
+
+ <p class="connectivity" width="600">The time is: <output id="clock"></output></p>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/offline/manifest_section_many-manual.https.html b/testing/web-platform/tests/html/browsers/offline/manifest_section_many-manual.https.html
new file mode 100644
index 0000000000..9378df1b40
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/manifest_section_many-manual.https.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html manifest="resources/manifest/section_many.manifest">
+ <head>
+ <title>Offline Application Cache - manifest_section_many</title>
+ <script src="resources/js/clock.js"></script>
+ <link rel="stylesheet" href="resources/css/result.css">
+ <link rel="stylesheet" href="resources/css/clock.css">
+ <link rel="stylesheet" href="resources/css/online.css" type="text/css" media="screen">
+ </head>
+ <body>
+ <ol type="1">
+ <li>Disable the network connection.</li>
+ <li>Refresh the page.</li>
+ <li>If the time element and colors of result elements are normally displayed, then test is <span class="manualpass"><b>PASS</b></span>, otherwise <span class="manualfail"><b>FAIL</b></span>.</li>
+ </ol>
+
+ <p class="connectivity" width="600">The time is: <output id="clock"></output></p>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/offline/resources/css/clock.css b/testing/web-platform/tests/html/browsers/offline/resources/css/clock.css
new file mode 100644
index 0000000000..fa406d0fbe
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/resources/css/clock.css
@@ -0,0 +1 @@
+output { font: 1em sans-serif; } \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/offline/resources/css/offline.css b/testing/web-platform/tests/html/browsers/offline/resources/css/offline.css
new file mode 100644
index 0000000000..76b7f39853
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/resources/css/offline.css
@@ -0,0 +1,5 @@
+.connectivity {
+ color: #fff;
+ background: red;
+ padding: 20px;
+} \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/offline/resources/css/online.css b/testing/web-platform/tests/html/browsers/offline/resources/css/online.css
new file mode 100644
index 0000000000..39efcb2ab4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/resources/css/online.css
@@ -0,0 +1,5 @@
+.connectivity {
+ color: #fff;
+ background: blue;
+ padding: 20px;
+} \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/offline/resources/css/result.css b/testing/web-platform/tests/html/browsers/offline/resources/css/result.css
new file mode 100644
index 0000000000..7d784b8ab5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/resources/css/result.css
@@ -0,0 +1,11 @@
+.manualpass {
+ color: green;
+}
+.manualfail {
+ color: red;
+}.pass {
+ color: green;
+}
+.fail {
+ color: red;
+}
diff --git a/testing/web-platform/tests/html/browsers/offline/resources/html/clock.html b/testing/web-platform/tests/html/browsers/offline/resources/html/clock.html
new file mode 100644
index 0000000000..6b8949a6b1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/resources/html/clock.html
@@ -0,0 +1,12 @@
+<!-- clock.html -->
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Clock</title>
+ <script src="../js/clock.js"></script>
+ <link rel="stylesheet" href="../css/clock.css">
+ </head>
+ <body>
+ <p>The time is: <output id="clock"></output></p>
+ </body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/offline/resources/js/clock.js b/testing/web-platform/tests/html/browsers/offline/resources/js/clock.js
new file mode 100644
index 0000000000..1ac0dca539
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/resources/js/clock.js
@@ -0,0 +1,3 @@
+setTimeout(function () {
+ document.getElementById('clock').value = new Date();
+}, 1000); \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/offline/resources/manifest/clock.manifest b/testing/web-platform/tests/html/browsers/offline/resources/manifest/clock.manifest
new file mode 100644
index 0000000000..a61aae6c61
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/resources/manifest/clock.manifest
@@ -0,0 +1,17 @@
+CACHE MANIFEST
+
+# Version 1
+
+CACHE:
+../css/clock.css
+../js/clock.js
+../css/result.css
+../css/offline.css
+/resources/testharness.js
+/resources/testharnessreport.js
+
+NETWORK:
+../html/clock.html
+
+FALLBACK:
+../css/online.css ../css/offline.css \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/offline/resources/manifest/section_empty.manifest b/testing/web-platform/tests/html/browsers/offline/resources/manifest/section_empty.manifest
new file mode 100644
index 0000000000..a23b9013be
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/resources/manifest/section_empty.manifest
@@ -0,0 +1,10 @@
+CACHE MANIFEST
+
+# Version 1
+
+../css/clock.css
+../js/clock.js
+../css/result.css
+../css/online.css
+/resources/testharness.js
+/resources/testharnessreport.js \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/offline/resources/manifest/section_many.manifest b/testing/web-platform/tests/html/browsers/offline/resources/manifest/section_many.manifest
new file mode 100644
index 0000000000..7e5e5e9995
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/resources/manifest/section_many.manifest
@@ -0,0 +1,19 @@
+CACHE MANIFEST
+
+# Version 1
+
+CACHE:
+../css/clock.css
+../js/clock.js
+
+CACHE:
+../css/result.css
+../css/offline.css
+/resources/testharness.js
+/resources/testharnessreport.js
+
+NETWORK:
+../html/clock.html
+
+FALLBACK:
+../css/online.css ../css/offline.css \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/offline/resources/manifest/url_check.manifest b/testing/web-platform/tests/html/browsers/offline/resources/manifest/url_check.manifest
new file mode 100644
index 0000000000..041df5e55f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/resources/manifest/url_check.manifest
@@ -0,0 +1,17 @@
+CACHE MANIFEST
+
+# Version 1
+
+CACHE:
+../css/cl#ock.css
+../js/clock.js
+../css/result.css
+../css/offline.css
+/resources/testharness.js
+/resources/testharnessreport.js
+
+NETWORK:
+../html/clock.html
+
+FALLBACK:
+../css/online.css ../css/offline.css \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/offline/section_network_offline-manual.https.html b/testing/web-platform/tests/html/browsers/offline/section_network_offline-manual.https.html
new file mode 100644
index 0000000000..c4121f5bc5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/section_network_offline-manual.https.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html manifest="resources/manifest/clock.manifest">
+ <head>
+ <title>Offline Application Cache - Section_network_offline</title>
+ <link rel="stylesheet" href="resources/css/result.css">
+ </head>
+ <body>
+ <ol>
+ <li>Disable the network connection.</li>
+ <li>Refresh the page.</li>
+ <li>If only the frame element can't be loaded, then test is <span class="manualpass"><b>PASS</b></span>, otherwise <span class="manualfail"><b>FAIL</b></span>.</li>
+ </ol>
+
+ <IFRAME id="TestFrame" name="TestWindow" src="html/clock.html" width="600" height="50" scrolling="auto" frameborder="1">
+ </IFRAME>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/offline/section_network_online-manual.https.html b/testing/web-platform/tests/html/browsers/offline/section_network_online-manual.https.html
new file mode 100644
index 0000000000..a5d8e59406
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/offline/section_network_online-manual.https.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html manifest="resources/manifest/clock.manifest">
+ <head>
+ <title>Offline Application Cache - Section_network_online</title>
+ <link rel="stylesheet" href="resources/css/result.css">
+ </head>
+ <body>
+ <ol>
+ <li>Refresh the page.</li>
+ <li>If the frame element is loaded, then test is <span class="manualpass"><b>PASS</b></span>, otherwise <span class="manualfail"><b>FAIL</b></span>.</li>
+ </ol>
+
+ <IFRAME id="TestFrame" name="TestWindow" src="html/clock.html" width="600" height="50" scrolling="auto" frameborder="1">
+ </IFRAME>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-due-to-document-domain-only.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-due-to-document-domain-only.html
new file mode 100644
index 0000000000..425374faec
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-due-to-document-domain-only.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<title>Cross-origin due to document.domain</title>
+<meta charset=utf-8>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<iframe src=resources/cross-origin-due-to-document-domain-only-helper.html></iframe>
+<script>
+async_test(t => {
+ onload = t.step_func_done(() => {
+ const frame = document.querySelector("iframe");
+ const innerSelf = self[0];
+ const innerLocation = innerSelf.location;
+ const innerDocument = innerSelf.document;
+ assert_equals(innerLocation.host, location.host);
+ assert_true(innerSelf.expandosForever);
+ assert_true(innerLocation.expandosForever);
+ assert_equals(frame.contentWindow, innerSelf);
+ assert_equals(frame.contentDocument, innerDocument);
+ innerSelf.setDocumentDomain();
+ assert_throws_dom("SecurityError", () => innerSelf.expandosForever);
+ assert_throws_dom("SecurityError", () => innerLocation.expandosForever);
+ assert_throws_dom("SecurityError", () => innerLocation.host);
+ assert_equals(innerSelf.parent, self);
+ assert_throws_dom("SecurityError", () => innerSelf.frameElement);
+ assert_throws_dom("SecurityError", () => innerLocation.reload());
+ assert_equals(frame.contentWindow, innerSelf);
+ assert_equals(frame.contentDocument, null);
+ // Cross-origin Document object obtained before it became cross-origin has no protections
+ assert_equals(innerDocument.URL, frame.src);
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-caching.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-caching.html
new file mode 100644
index 0000000000..a8af18d106
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-caching.html
@@ -0,0 +1,49 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Cross-origin methods and accessors are cached per Realm via[[CrossOriginPropertyDescriptorMap]]</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/browsers.html#crossorigingetownpropertyhelper-(-o,-p-)">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="cross-origin-objects-function-common.js"></script>
+<div id=log></div>
+<script>
+"use strict";
+
+promise_test(async t => {
+ const w = await makeCrossOriginWindow(t);
+ for (const {key} of crossOriginWindowMethods) {
+ assert_equals(w[key], w[key], `w.${key} via [[Get]]`);
+ const desc1 = Object.getOwnPropertyDescriptor(w, key);
+ const desc2 = Object.getOwnPropertyDescriptor(w, key);
+ assert_equals(desc1.value, desc2.value, `w.${key} via [[GetOwnProperty]]`);
+ }
+}, "Cross-origin Window methods are cached");
+
+promise_test(async t => {
+ const w = await makeCrossOriginWindow(t);
+ for (const {key} of crossOriginWindowAccessors) {
+ const desc1 = Object.getOwnPropertyDescriptor(w, key);
+ const desc2 = Object.getOwnPropertyDescriptor(w, key);
+ assert_equals(desc1.get, desc2.get, `w.${key} getter`);
+ if (key === "location") {
+ assert_equals(desc1.set, desc2.set, `w.${key} setter`);
+ }
+ }
+}, "Cross-origin Window accessors are cached");
+
+promise_test(async t => {
+ const w = await makeCrossOriginWindow(t);
+ assert_equals(w.location.replace, w.location.replace, "via [[Get]]");
+ const desc1 = Object.getOwnPropertyDescriptor(w.location, "replace");
+ const desc2 = Object.getOwnPropertyDescriptor(w.location, "replace");
+ assert_equals(desc1.value, desc2.value, "via [[GetOwnProperty]]");
+}, "Cross-origin Location `replace` method is cached");
+
+promise_test(async t => {
+ const w = await makeCrossOriginWindow(t);
+ const desc1 = Object.getOwnPropertyDescriptor(w.location, "href");
+ const desc2 = Object.getOwnPropertyDescriptor(w.location, "href");
+ assert_equals(desc1.set, desc2.set);
+}, "Cross-origin Location `href` setter is cached");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-common.js b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-common.js
new file mode 100644
index 0000000000..3b93b498a2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-common.js
@@ -0,0 +1,34 @@
+"use strict";
+
+const crossOriginWindowMethods = [
+ {key: "close", length: 0},
+ {key: "focus", length: 0},
+ {key: "blur", length: 0},
+ {key: "postMessage", length: 1},
+];
+
+const crossOriginWindowAccessors = [
+ "window",
+ "self",
+ "location",
+ "closed",
+ "frames",
+ "length",
+ "top",
+ "opener",
+ "parent",
+].map(key => ({key}));
+
+const makeCrossOriginWindow = t => {
+ const iframe = document.createElement("iframe");
+ const path = location.pathname.slice(0, location.pathname.lastIndexOf("/")) + "/frame.html";
+ iframe.src = get_host_info().HTTP_REMOTE_ORIGIN + path;
+
+ return new Promise((resolve, reject) => {
+ iframe.onload = () => { resolve(iframe.contentWindow); };
+ iframe.onerror = reject;
+
+ document.body.append(iframe);
+ t.add_cleanup(() => { iframe.remove(); });
+ });
+};
diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-length.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-length.html
new file mode 100644
index 0000000000..466915a461
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-length.html
@@ -0,0 +1,45 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Cross-origin methods and accessors are created with correct 'length' property</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/browsers.html#crossorigingetownpropertyhelper-(-o,-p-)">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="cross-origin-objects-function-common.js"></script>
+<div id=log></div>
+<script>
+"use strict";
+
+promise_test(async t => {
+ const w = await makeCrossOriginWindow(t);
+ for (const {key, length} of crossOriginWindowMethods) {
+ assert_equals(w[key].length, length, `w.${key} via [[Get]]`);
+ const desc = Object.getOwnPropertyDescriptor(w, key);
+ assert_equals(desc.value.length, length, `w.${key} via [[GetOwnProperty]]`);
+ }
+}, "Cross-origin Window methods have correct 'length'");
+
+promise_test(async t => {
+ const w = await makeCrossOriginWindow(t);
+ for (const {key} of crossOriginWindowAccessors) {
+ const desc = Object.getOwnPropertyDescriptor(w, key);
+ assert_equals(desc.get.length, 0, `w.${key}`);
+ if (key === "location") {
+ assert_equals(desc.set.length, 1, `w.${key}`);
+ }
+ }
+}, "Cross-origin Window accessors have correct 'length'");
+
+promise_test(async t => {
+ const w = await makeCrossOriginWindow(t);
+ assert_equals(w.location.replace.length, 1);
+ const desc = Object.getOwnPropertyDescriptor(w.location, "replace");
+ assert_equals(desc.value.length, 1);
+}, "Cross-origin Location `replace` method has correct 'length'");
+
+promise_test(async t => {
+ const w = await makeCrossOriginWindow(t);
+ const desc = Object.getOwnPropertyDescriptor(w.location, "href");
+ assert_equals(desc.set.length, 1);
+}, "Cross-origin Location `href` setter has correct 'length'");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-name.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-name.html
new file mode 100644
index 0000000000..167c30e8f3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-name.html
@@ -0,0 +1,45 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Cross-origin methods and accessors are created with correct 'name' property</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/browsers.html#crossorigingetownpropertyhelper-(-o,-p-)">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="cross-origin-objects-function-common.js"></script>
+<div id=log></div>
+<script>
+"use strict";
+
+promise_test(async t => {
+ const w = await makeCrossOriginWindow(t);
+ for (const {key} of crossOriginWindowMethods) {
+ assert_equals(w[key].name, key, `w.${key} via [[Get]]`);
+ const desc = Object.getOwnPropertyDescriptor(w, key);
+ assert_equals(desc.value.name, key, `w.${key} via [[GetOwnProperty]]`);
+ }
+}, "Cross-origin Window methods have correct 'name'");
+
+promise_test(async t => {
+ const w = await makeCrossOriginWindow(t);
+ for (const {key} of crossOriginWindowAccessors) {
+ const desc = Object.getOwnPropertyDescriptor(w, key);
+ assert_equals(desc.get.name, `get ${key}`);
+ if (key === "location") {
+ assert_equals(desc.set.name, `set ${key}`);
+ }
+ }
+}, "Cross-origin Window accessors have correct 'name'");
+
+promise_test(async t => {
+ const w = await makeCrossOriginWindow(t);
+ assert_equals(w.location.replace.name, "replace");
+ const desc = Object.getOwnPropertyDescriptor(w.location, "replace");
+ assert_equals(desc.value.name, "replace");
+}, "Cross-origin Location `replace` method has correct 'name'");
+
+promise_test(async t => {
+ const w = await makeCrossOriginWindow(t);
+ const desc = Object.getOwnPropertyDescriptor(w.location, "href");
+ assert_equals(desc.set.name, "set href");
+}, "Cross-origin Location `href` setter has correct 'name'");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-on-new-window.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-on-new-window.html
new file mode 100644
index 0000000000..3ad0de6a3a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-on-new-window.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>Cross-origin behavior of Window and Location on new Window</title>
+<link rel="author" title="Bobby Holley (:bholley)" href="bobbyholley@gmail.com">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#security-window">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#security-location">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+setup({explicit_done: true});
+
+window.addEventListener('message', function onmessage(evt) {
+ window.removeEventListener('message', onmessage);
+ test(function() {
+ var results = evt.data;
+ assert_true(results.length > 0, 'Need results');
+ results.forEach(function(r) { assert_true(r.pass, r.message); });
+ }, "Cross-origin object identity preserved across document.domain");
+ win.close();
+ done();
+});
+var win = window.open('win-documentdomain.sub.html');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects.html
new file mode 100644
index 0000000000..d1b6cabc0f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects.html
@@ -0,0 +1,718 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>Cross-origin behavior of Window and Location</title>
+<link rel="author" title="Bobby Holley (:bholley)" href="bobbyholley@gmail.com">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#security-window">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#security-location">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<div id=log></div>
+<iframe id="B"></iframe>
+<iframe id="C"></iframe>
+<iframe id="D"></iframe>
+<iframe id="E"></iframe>
+<iframe id="F"></iframe>
+<iframe id="G"></iframe>
+<iframe id="H"></iframe>
+<script>
+
+/*
+ * Setup boilerplate. This gives us a same-origin window "B", cross-origin
+ * windows "C" and "D", initially same-origin but then changing document.domain
+ * windows "E" and "F", and not-same-site (also cross-origin, of course) windows
+ * "G" and "H".
+ */
+var host_info = get_host_info();
+
+setup({explicit_done: true});
+path = location.pathname.substring(0, location.pathname.lastIndexOf('/')) + '/frame.html';
+pathWithThen = location.pathname.substring(0, location.pathname.lastIndexOf('/')) + '/frame-with-then.html';
+var B = document.getElementById('B').contentWindow;
+var C = document.getElementById('C').contentWindow;
+var D = document.getElementById('D').contentWindow;
+var E = document.getElementById('E').contentWindow;
+var F = document.getElementById('F').contentWindow;
+var G = document.getElementById('G').contentWindow;
+var H = document.getElementById('H').contentWindow;
+B.frameElement.uriToLoad = path;
+C.frameElement.uriToLoad = get_host_info().HTTP_REMOTE_ORIGIN + path;
+D.frameElement.uriToLoad = get_host_info().HTTP_REMOTE_ORIGIN + pathWithThen;
+E.frameElement.uriToLoad = path + "?setdomain";
+F.frameElement.uriToLoad = pathWithThen + "?setdomain";
+G.frameElement.uriToLoad = get_host_info().HTTP_NOTSAMESITE_ORIGIN + path;
+H.frameElement.uriToLoad = get_host_info().HTTP_NOTSAMESITE_ORIGIN + pathWithThen;
+
+function winName(win) {
+ var iframes = document.getElementsByTagName('iframe');
+ iframes.find = Array.prototype.find;
+ var found = iframes.find(function (ifr) {
+ return ifr.contentWindow == win;
+ });
+ if (found) {
+ return found.id;
+ }
+ return "UNKNOWN";
+}
+
+function reloadSubframes(cb) {
+ var iframes = document.getElementsByTagName('iframe');
+ iframes.forEach = Array.prototype.forEach;
+ var count = 0;
+ function frameLoaded() {
+ this.onload = null;
+ if (++count == iframes.length)
+ cb();
+ }
+ iframes.forEach(function(ifr) { ifr.onload = frameLoaded; ifr.setAttribute('src', ifr.uriToLoad); });
+}
+function isObject(x) { return Object(x) === x; }
+
+/*
+ * Note: we eschew assert_equals in a lot of these tests, since the harness ends
+ * up throwing when it tries to format a message involving a cross-origin object.
+ */
+
+/*
+ * List of tests. Each test is actually a pair: an array of tests to run and a
+ * boolean for whether these are promise tests. We reload all the subframes in
+ * between running each toplevel test. This is done to avoid having to reload
+ * all the subframes for every single test, which is overkill: some of these
+ * tests are known to touch only one subframe. And doing it makes the test
+ * really slow.
+ */
+var testList = [];
+function addTest(func, desc) {
+ testList.push(
+ {tests: [
+ {func: func.bind(null, C),
+ desc: desc + " (cross-origin)"},
+ {func: func.bind(null, E),
+ desc: desc + " (same-origin + document.domain)"},
+ {func: func.bind(null, G),
+ desc: desc + " (cross-site)"}],
+ promiseTest: false});
+}
+
+function addPromiseTest(func, desc) {
+ testList.push(
+ {tests: [
+ {func: func.bind(null, C),
+ desc: desc + " (cross-origin)"},
+ {func: func.bind(null, E),
+ desc: desc + " (same-origin + document.domain)"},
+ {func: func.bind(null, G),
+ desc: desc + " (cross-site)"}],
+ promiseTest: true});
+}
+
+/**
+ * Similar helpers, but for the subframes that load frame-with-then.html
+ */
+function addThenTest(func, desc) {
+ testList.push(
+ {tests: [
+ {func: func.bind(null, D),
+ desc: desc + " (cross-origin)"},
+ {func: func.bind(null, F),
+ desc: desc + " (same-origin + document.domain)"},
+ {func: func.bind(null, H),
+ desc: desc + " (cross-site)"}],
+ promiseTest: false});
+}
+
+function addPromiseThenTest(func, desc) {
+ testList.push(
+ {tests: [
+ {func: func.bind(null, D),
+ desc: desc + " (cross-origin)"},
+ {func: func.bind(null, F),
+ desc: desc + " (same-origin + document.domain)"},
+ {func: func.bind(null, H),
+ desc: desc + " (cross-site)"}],
+ promiseTest: true});
+}
+
+/*
+ * Basic smoke tests for same-origin and cross-origin behaviors.
+ */
+
+addTest(function(win) {
+ // Note: we do not check location.host as its default port semantics are hard to reflect statically
+ assert_equals(location.hostname, host_info.ORIGINAL_HOST, 'Need to run the top-level test from domain ' + host_info.ORIGINAL_HOST);
+ assert_equals(get_port(location), host_info.HTTP_PORT, 'Need to run the top-level test from port ' + host_info.HTTP_PORT);
+ assert_equals(B.parent, window, "window.parent works same-origin");
+ assert_equals(win.parent, window, "window.parent works cross-origin");
+ assert_equals(B.location.pathname, path, "location.href works same-origin");
+ assert_throws_dom("SecurityError", function() { win.location.pathname; }, "location.pathname throws cross-origin");
+ assert_equals(B.frames, 'override', "Overrides visible in the same-origin case");
+ assert_equals(win.frames, win, "Overrides invisible in the cross-origin case");
+ assert_equals(B.focus, 'override', "Overrides visible in the same-origin case");
+ checkFunction(win.focus, Function.prototype);
+ assert_not_equals(win.focus, focus, "Overrides invisible in the cross-origin case");
+}, "Basic sanity-checking");
+
+/*
+ * Tests regarding which properties are allowed cross-origin.
+ *
+ * Also tests for [[GetOwnProperty]] and [[HasOwnProperty]] behavior.
+ */
+
+var allowedSymbols = [Symbol.toStringTag, Symbol.hasInstance,
+ Symbol.isConcatSpreadable];
+var windowAllowlists = {
+ namedFrames: ['donotleakme'],
+ indices: ['0', '1'],
+ getters: ['location', 'window', 'frames', 'self', 'top', 'parent',
+ 'opener', 'closed', 'length'],
+ setters: ['location'],
+ methods: ['postMessage', 'close', 'blur', 'focus'],
+ // These are methods which return promises and, therefore, when called with a
+ // cross-origin `this` object, do not throw immediately, but instead return a
+ // Promise which rejects with the same SecurityError that they would
+ // otherwise throw. They are not, however, cross-origin accessible.
+ promiseMethods: ['createImageBitmap', 'fetch'],
+}
+windowAllowlists.propNames = Array.from(new Set([...windowAllowlists.indices,
+ ...windowAllowlists.getters,
+ ...windowAllowlists.setters,
+ ...windowAllowlists.methods,
+ 'then'])).sort();
+windowAllowlists.props = windowAllowlists.propNames.concat(allowedSymbols);
+
+var locationAllowlists = {
+ getters: [],
+ setters: ['href'],
+ methods: ['replace'],
+ promiseMethods: [],
+}
+locationAllowlists.propNames = Array.from(new Set([...locationAllowlists.getters,
+ ...locationAllowlists.setters,
+ ...locationAllowlists.methods,
+ 'then'])).sort();
+
+// Define various sets of arguments to call cross-origin methods with. Arguments
+// for any cross-origin-callable method must be valid, and should aim to have no
+// side-effects. Any method without an entry in this list will be called with
+// an empty arguments list.
+var methodArgs = new Map(Object.entries({
+ // As a basic smoke test, we call one cross-origin-inaccessible method with
+ // both valid and invalid arguments to make sure that it rejects with the
+ // same SecurityError regardless.
+ assign: [
+ [],
+ ["javascript:undefined"],
+ ],
+ // Note: If we post a message to frame.html with a matching origin, its
+ // "onmessage" handler will change its `document.domain`, and potentially
+ // invalidate subsequent tests, so be sure to only pass non-matching origins.
+ postMessage: [
+ ["foo", "http://does-not.exist/"],
+ ["foo", {}],
+ ],
+ replace: [["javascript:undefined"]],
+}));
+
+addTest(function(win) {
+ for (var prop in window) {
+ if (windowAllowlists.props.indexOf(prop) != -1) {
+ win[prop]; // Shouldn't throw.
+ Object.getOwnPropertyDescriptor(win, prop); // Shouldn't throw.
+ assert_true(Object.prototype.hasOwnProperty.call(win, prop), "hasOwnProperty for " + String(prop));
+ } else {
+ assert_throws_dom("SecurityError", function() { win[prop]; }, "Should throw when accessing " + String(prop) + " on Window");
+ assert_throws_dom("SecurityError", function() { Object.getOwnPropertyDescriptor(win, prop); },
+ "Should throw when accessing property descriptor for " + prop + " on Window");
+ assert_throws_dom("SecurityError", function() { Object.prototype.hasOwnProperty.call(win, prop); },
+ "Should throw when invoking hasOwnProperty for " + prop + " on Window");
+ }
+ if (prop != 'location')
+ assert_throws_dom("SecurityError", function() { win[prop] = undefined; }, "Should throw when writing to " + prop + " on Window");
+ }
+ for (var prop of windowAllowlists.namedFrames) {
+ win[prop]; // Shouldn't throw.
+ var desc = Object.getOwnPropertyDescriptor(win, prop);
+ assert_false(desc.writable, "[[Writable]] for named frame " + String(prop));
+ assert_false(desc.enumerable, "[[Enumerable]] for named frame " + String(prop));
+ assert_true(desc.configurable, "[[Configurable]] for named frame " + String(prop));
+ assert_true(Object.prototype.hasOwnProperty.call(win, prop), "hasOwnProperty for " + String(prop));
+ }
+ for (var prop in location) {
+ if (prop == 'replace') {
+ win.location[prop]; // Shouldn't throw.
+ Object.getOwnPropertyDescriptor(win.location, prop); // Shouldn't throw.
+ assert_true(Object.prototype.hasOwnProperty.call(win.location, prop), "hasOwnProperty for " + prop);
+ assert_throws_dom("SecurityError", function() { win.location[prop] = undefined; }, "Should throw when writing to " + prop + " on Location");
+ }
+ else if (prop == 'href') {
+ Object.getOwnPropertyDescriptor(win.location, prop); // Shouldn't throw.
+ assert_true(Object.prototype.hasOwnProperty.call(win.location, prop), "hasOwnProperty for " + prop);
+ assert_throws_dom("SecurityError", function() { win.location[prop] },
+ "Should throw reading href on Location");
+ }
+ else {
+ assert_throws_dom("SecurityError", function() { win.location[prop]; }, "Should throw when accessing " + prop + " on Location");
+ assert_throws_dom("SecurityError", function() { Object.getOwnPropertyDescriptor(win.location, prop); },
+ "Should throw when accessing property descriptor for " + prop + " on Location");
+ assert_throws_dom("SecurityError", function() { Object.prototype.hasOwnProperty.call(win.location, prop); },
+ "Should throw when invoking hasOwnProperty for " + prop + " on Location");
+ assert_throws_dom("SecurityError", function() { win.location[prop] = undefined; }, "Should throw when writing to " + prop + " on Location");
+ }
+ }
+}, "Only certain properties are accessible cross-origin");
+
+addPromiseTest(async function(win, test_obj) {
+ async function checkProperties(objName, allowedlists) {
+ var localObj = window[objName];
+ var otherObj = win[objName];
+
+ for (var prop in localObj) {
+ let desc;
+ for (let obj = localObj; !desc; obj = Object.getPrototypeOf(obj)) {
+ desc = Object.getOwnPropertyDescriptor(obj, prop);
+
+ }
+
+ if ("value" in desc) {
+ if (typeof desc.value === "function" && String(desc.value).includes("[native code]")) {
+ if (allowedlists.promiseMethods.includes(prop)) {
+ await promise_rejects_dom(test_obj, "SecurityError", desc.value.call(otherObj),
+ `Should throw when calling ${objName}.${prop} with cross-origin this object`);
+ } else if (!allowedlists.methods.includes(prop)) {
+ for (let args of methodArgs.get(prop) || [[]]) {
+ assert_throws_dom("SecurityError", desc.value.bind(otherObj, ...args),
+ `Should throw when calling ${objName}.${prop} with cross-origin this object`);
+ }
+
+ } else {
+ for (let args of methodArgs.get(prop) || [[]]) {
+ desc.value.apply(otherObj, args); // Shouldn't throw.
+ }
+ }
+ }
+ } else {
+ if (desc.get) {
+ if (allowedlists.getters.includes(prop)) {
+ desc.get.call(otherObj); // Shouldn't throw.
+ } else {
+ assert_throws_dom("SecurityError", desc.get.bind(otherObj),
+ `Should throw when calling ${objName}.${prop} getter with cross-origin this object`);
+ }
+ }
+ if (desc.set) {
+ if (allowedlists.setters.includes(prop)) {
+ desc.set.call(otherObj, "javascript:undefined"); // Shouldn't throw.
+ } else {
+ assert_throws_dom("SecurityError", desc.set.bind(otherObj, "foo"),
+ `Should throw when calling ${objName}.${prop} setter with cross-origin this object`);
+ }
+ }
+ }
+ }
+ }
+
+ await checkProperties("location", locationAllowlists);
+ await checkProperties("window", windowAllowlists);
+}, "Only certain properties are usable as cross-origin this objects");
+
+/*
+ * ES Internal Methods.
+ */
+
+/*
+ * [[GetPrototypeOf]]
+ */
+addTest(function(win) {
+ assert_equals(Object.getPrototypeOf(win), null, "cross-origin Window proto is null");
+ assert_equals(Object.getPrototypeOf(win.location), null, "cross-origin Location proto is null (__proto__)");
+ var protoGetter = Object.getOwnPropertyDescriptor(Object.prototype, '__proto__').get;
+ assert_equals(protoGetter.call(win), null, "cross-origin Window proto is null");
+ assert_equals(protoGetter.call(win.location), null, "cross-origin Location proto is null (__proto__)");
+ assert_throws_dom("SecurityError", function() { win.__proto__; }, "__proto__ property not available cross-origin");
+ assert_throws_dom("SecurityError", function() { win.location.__proto__; }, "__proto__ property not available cross-origin");
+
+}, "[[GetPrototypeOf]] should return null");
+
+/*
+ * [[SetPrototypeOf]]
+ */
+addTest(function(win) {
+ assert_throws_dom("SecurityError", function() { win.__proto__ = new Object(); }, "proto set on cross-origin Window");
+ assert_throws_dom("SecurityError", function() { win.location.__proto__ = new Object(); }, "proto set on cross-origin Location");
+ var setters = [Object.getOwnPropertyDescriptor(Object.prototype, '__proto__').set];
+ if (Object.setPrototypeOf)
+ setters.push(function(p) { Object.setPrototypeOf(this, p); });
+ setters.forEach(function(protoSetter) {
+ assert_throws_js(TypeError, function() { protoSetter.call(win, new Object()); }, "proto setter |call| on cross-origin Window");
+ assert_throws_js(TypeError, function() { protoSetter.call(win.location, new Object()); }, "proto setter |call| on cross-origin Location");
+ });
+ // Hack to avoid "duplicate test name" harness issues.
+ setters.forEach(function(protoSetter) {
+ test(function() { protoSetter.call(win, null); },
+ "proto setter |call| on cross-origin Window with null (" + protoSetter + ", " + winName(win) + ")");
+ test(function() { protoSetter.call(win.location, null); },
+ "proto setter |call| on cross-origin Location with null (" + protoSetter + ", " + winName(win) + ")");
+ });
+ if (Reflect.setPrototypeOf) {
+ assert_false(Reflect.setPrototypeOf(win, new Object()),
+ "Reflect.setPrototypeOf on cross-origin Window");
+ assert_true(Reflect.setPrototypeOf(win, null),
+ "Reflect.setPrototypeOf on cross-origin Window with null");
+ assert_false(Reflect.setPrototypeOf(win.location, new Object()),
+ "Reflect.setPrototypeOf on cross-origin Location");
+ assert_true(Reflect.setPrototypeOf(win.location, null),
+ "Reflect.setPrototypeOf on cross-origin Location with null");
+ }
+}, "[[SetPrototypeOf]] should return false");
+
+/*
+ * [[IsExtensible]]
+ */
+addTest(function(win) {
+ assert_true(Object.isExtensible(win), "cross-origin Window should be extensible");
+ assert_true(Object.isExtensible(win.location), "cross-origin Location should be extensible");
+}, "[[IsExtensible]] should return true for cross-origin objects");
+
+/*
+ * [[PreventExtensions]]
+ */
+addTest(function(win) {
+ assert_throws_js(TypeError, function() { Object.preventExtensions(win) },
+ "preventExtensions on cross-origin Window should throw");
+ assert_throws_js(TypeError, function() { Object.preventExtensions(win.location) },
+ "preventExtensions on cross-origin Location should throw");
+ assert_false(Reflect.preventExtensions(win),
+ "Reflect.preventExtensions on cross-origin Window");
+ assert_false(Reflect.preventExtensions(win.location),
+ "Reflect.preventExtensions on cross-origin Location");
+}, "[[PreventExtensions]] should return false cross-origin objects");
+
+/*
+ * [[GetOwnProperty]]
+ */
+
+addTest(function(win) {
+ assert_true(isObject(Object.getOwnPropertyDescriptor(win, 'close')), "win.close is |own|");
+ assert_true(isObject(Object.getOwnPropertyDescriptor(win, 'top')), "win.top is |own|");
+ assert_true(isObject(Object.getOwnPropertyDescriptor(win.location, 'href')), "win.location.href is |own|");
+ assert_true(isObject(Object.getOwnPropertyDescriptor(win.location, 'replace')), "win.location.replace is |own|");
+}, "[[GetOwnProperty]] - Properties on cross-origin objects should be reported |own|");
+
+function checkPropertyDescriptor(desc, propName, expectWritable) {
+ const isSymbol = typeof(propName) === "symbol";
+ const isArrayIndexPropertyName = !isSymbol && !isNaN(parseInt(propName, 10));
+ propName = String(propName);
+ assert_true(isObject(desc), "property descriptor for " + propName + " should exist");
+ assert_equals(desc.configurable, true, "property descriptor for " + propName + " should be configurable");
+ if (!isArrayIndexPropertyName) {
+ assert_equals(desc.enumerable, false, "property descriptor for " + propName + " should not be enumerable");
+ if (isSymbol || propName == "then") {
+ assert_true("value" in desc,
+ "property descriptor for " + propName + " should be a value descriptor");
+ assert_equals(desc.value, undefined,
+ "symbol-named cross-origin visible prop " + propName +
+ " should come back as undefined");
+ }
+ } else {
+ assert_equals(desc.enumerable, true, "property descriptor for " + propName + " should be enumerable");
+ }
+ if ('value' in desc)
+ assert_equals(desc.writable, expectWritable, "property descriptor for " + propName + " should have writable: " + expectWritable);
+ else
+ assert_equals(typeof desc.set != 'undefined', expectWritable,
+ "property descriptor for " + propName + " should " + (expectWritable ? "" : "not ") + "have setter");
+}
+
+addTest(function(win) {
+ windowAllowlists.props.forEach(function(prop) {
+ var desc = Object.getOwnPropertyDescriptor(win, prop);
+ checkPropertyDescriptor(desc, prop, prop == 'location');
+ });
+ checkPropertyDescriptor(Object.getOwnPropertyDescriptor(win.location, 'replace'), 'replace', false);
+ checkPropertyDescriptor(Object.getOwnPropertyDescriptor(win.location, 'href'), 'href', true);
+ assert_equals(typeof Object.getOwnPropertyDescriptor(win.location, 'href').get, 'undefined', "Cross-origin location should have no href getter");
+ allowedSymbols.forEach(function(prop) {
+ var desc = Object.getOwnPropertyDescriptor(win.location, prop);
+ checkPropertyDescriptor(desc, prop, false);
+ });
+}, "[[GetOwnProperty]] - Property descriptors for cross-origin properties should be set up correctly");
+
+addThenTest(function(win) {
+ assert_equals(typeof win.then, "object");
+}, "[[GetOwnProperty]] - Subframe named 'then' should shadow the default 'then' value");
+
+addThenTest(function(win) {
+ assert_equals(typeof win.close, "function");
+ assert_equals(typeof win.open, "object");
+}, "[[GetOwnProperty]] - Subframes should be visible cross-origin only if their names don't match the names of cross-origin-exposed IDL properties");
+
+addTest(function(win) {
+ assert_equals(typeof Object.getOwnPropertyDescriptor(win, '0').value, "object");
+ assert_equals(typeof Object.getOwnPropertyDescriptor(win, '1').value, "object");
+ assert_throws_dom("SecurityError", function() {
+ Object.getOwnPropertyDescriptor(win, '2');
+ });
+}, "[[GetOwnProperty]] - Should be able to get a property descriptor for an indexed property only if it corresponds to a child window.");
+
+/*
+ * [[Delete]]
+ */
+addTest(function(win) {
+ assert_throws_dom("SecurityError", function() { delete win[0]; }, "Can't delete cross-origin indexed property");
+ assert_throws_dom("SecurityError", function() { delete win[100]; }, "Can't delete cross-origin indexed property");
+ assert_throws_dom("SecurityError", function() { delete win.location; }, "Can't delete cross-origin property");
+ assert_throws_dom("SecurityError", function() { delete win.parent; }, "Can't delete cross-origin property");
+ assert_throws_dom("SecurityError", function() { delete win.length; }, "Can't delete cross-origin property");
+ assert_throws_dom("SecurityError", function() { delete win.document; }, "Can't delete cross-origin property");
+ assert_throws_dom("SecurityError", function() { delete win.foopy; }, "Can't delete cross-origin property");
+ assert_throws_dom("SecurityError", function() { delete win.location.href; }, "Can't delete cross-origin property");
+ assert_throws_dom("SecurityError", function() { delete win.location.replace; }, "Can't delete cross-origin property");
+ assert_throws_dom("SecurityError", function() { delete win.location.port; }, "Can't delete cross-origin property");
+ assert_throws_dom("SecurityError", function() { delete win.location.foopy; }, "Can't delete cross-origin property");
+}, "[[Delete]] Should throw on cross-origin objects");
+
+/*
+ * [[DefineOwnProperty]]
+ */
+function checkDefine(obj, prop) {
+ var valueDesc = { configurable: true, enumerable: false, writable: false, value: 2 };
+ var accessorDesc = { configurable: true, enumerable: false, get: function() {} };
+ assert_throws_dom("SecurityError", function() { Object.defineProperty(obj, prop, valueDesc); }, "Can't define cross-origin value property " + prop);
+ assert_throws_dom("SecurityError", function() { Object.defineProperty(obj, prop, accessorDesc); }, "Can't define cross-origin accessor property " + prop);
+}
+addTest(function(win) {
+ checkDefine(win, 'length');
+ checkDefine(win, 'parent');
+ checkDefine(win, 'location');
+ checkDefine(win, 'document');
+ checkDefine(win, 'foopy');
+ checkDefine(win.location, 'href');
+ checkDefine(win.location, 'replace');
+ checkDefine(win.location, 'port');
+ checkDefine(win.location, 'foopy');
+}, "[[DefineOwnProperty]] Should throw for cross-origin objects");
+
+/*
+ * EnumerateObjectProperties (backed by [[OwnPropertyKeys]])
+ */
+
+addTest(function(win) {
+ let i = 0;
+ for (var prop in win) {
+ i++;
+ assert_true(windowAllowlists.indices.includes(prop), prop + " is not safelisted for a cross-origin Window");
+ }
+ assert_equals(i, windowAllowlists.indices.length, "Enumerate all enumerable safelisted cross-origin Window properties");
+ i = 0;
+ for (var prop in win.location) {
+ i++;
+ }
+ assert_equals(i, 0, "There's nothing to enumerate for cross-origin Location properties");
+}, "Can only enumerate safelisted enumerable properties");
+
+/*
+ * [[OwnPropertyKeys]]
+ */
+
+addTest(function(win) {
+ assert_array_equals(Object.getOwnPropertyNames(win).sort(),
+ windowAllowlists.propNames,
+ "Object.getOwnPropertyNames() gives the right answer for cross-origin Window");
+ assert_array_equals(Object.keys(win).sort(),
+ windowAllowlists.indices,
+ "Object.keys() gives the right answer for cross-origin Window");
+ assert_array_equals(Object.getOwnPropertyNames(win.location).sort(),
+ locationAllowlists.propNames,
+ "Object.getOwnPropertyNames() gives the right answer for cross-origin Location");
+ assert_equals(Object.keys(win.location).length, 0,
+ "Object.keys() gives the right answer for cross-origin Location");
+}, "[[OwnPropertyKeys]] should return all properties from cross-origin objects");
+
+addTest(function(win) {
+ assert_array_equals(Object.getOwnPropertySymbols(win), allowedSymbols,
+ "Object.getOwnPropertySymbols() should return the three symbol-named properties that are exposed on a cross-origin Window");
+ assert_array_equals(Object.getOwnPropertySymbols(win.location),
+ allowedSymbols,
+ "Object.getOwnPropertySymbols() should return the three symbol-named properties that are exposed on a cross-origin Location");
+}, "[[OwnPropertyKeys]] should return the right symbol-named properties for cross-origin objects");
+
+addTest(function(win) {
+ var allWindowProps = Reflect.ownKeys(win);
+ indexedWindowProps = allWindowProps.slice(0, windowAllowlists.indices.length);
+ stringWindowProps = allWindowProps.slice(0, -1 * allowedSymbols.length);
+ symbolWindowProps = allWindowProps.slice(-1 * allowedSymbols.length);
+ // stringWindowProps should have "then" last in this case. Do this
+ // check before we call stringWindowProps.sort() below.
+ assert_equals(stringWindowProps[stringWindowProps.length - 1], "then",
+ "'then' property should be added to the end of the string list if not there");
+ assert_array_equals(indexedWindowProps, windowAllowlists.indices,
+ "Reflect.ownKeys should start with the indices exposed on the cross-origin window.");
+ assert_array_equals(stringWindowProps.sort(), windowAllowlists.propNames,
+ "Reflect.ownKeys should continue with the cross-origin window properties for a cross-origin Window.");
+ assert_array_equals(symbolWindowProps, allowedSymbols,
+ "Reflect.ownKeys should end with the cross-origin symbols for a cross-origin Window.");
+
+ var allLocationProps = Reflect.ownKeys(win.location);
+ stringLocationProps = allLocationProps.slice(0, -1 * allowedSymbols.length);
+ symbolLocationProps = allLocationProps.slice(-1 * allowedSymbols.length);
+ assert_array_equals(stringLocationProps.sort(), locationAllowlists.propNames,
+ "Reflect.ownKeys should start with the cross-origin window properties for a cross-origin Location.")
+ assert_array_equals(symbolLocationProps, allowedSymbols,
+ "Reflect.ownKeys should end with the cross-origin symbols for a cross-origin Location.")
+}, "[[OwnPropertyKeys]] should place the symbols after the property names after the subframe indices");
+
+addThenTest(function(win) {
+ var stringProps = Object.getOwnPropertyNames(win);
+ // Named frames are not exposed via [[OwnPropertyKeys]].
+ assert_equals(stringProps.indexOf("a"), -1);
+ assert_equals(stringProps.indexOf("b"), -1);
+ assert_equals(typeof win.a, "object");
+ assert_equals(typeof win.b, "object");
+ assert_equals(stringProps[stringProps.length - 1], "then");
+ assert_equals(stringProps.indexOf("then"), stringProps.lastIndexOf("then"));
+}, "[[OwnPropertyKeys]] should not reorder where 'then' appears if it's a named subframe, nor add another copy of 'then'");
+
+addTest(function(win) {
+ assert_equals(B.eval('parent.' + winName(win)), win, "A and B observe the same identity for C's Window");
+ assert_equals(B.eval('parent.' + winName(win) + '.location'), win.location, "A and B observe the same identity for C's Location");
+}, "A and B jointly observe the same identity for cross-origin Window and Location");
+
+function checkFunction(f, proto) {
+ var name = f.name || '<missing name>';
+ assert_equals(typeof f, 'function', name + " is a function");
+ assert_equals(Object.getPrototypeOf(f), proto, f.name + " has the right prototype");
+}
+
+addTest(function(win) {
+ checkFunction(win.close, Function.prototype);
+ checkFunction(win.location.replace, Function.prototype);
+}, "Cross-origin functions get local Function.prototype");
+
+addTest(function(win) {
+ assert_true(isObject(Object.getOwnPropertyDescriptor(win, 'parent')),
+ "Need to be able to use Object.getOwnPropertyDescriptor do this test");
+ checkFunction(Object.getOwnPropertyDescriptor(win, 'parent').get, Function.prototype);
+ checkFunction(Object.getOwnPropertyDescriptor(win.location, 'href').set, Function.prototype);
+}, "Cross-origin Window accessors get local Function.prototype");
+
+addTest(function(win) {
+ checkFunction(close, Function.prototype);
+ assert_not_equals(close, B.close, 'same-origin Window functions get their own object');
+ assert_not_equals(close, win.close, 'cross-origin Window functions get their own object');
+ var close_B = B.eval('parent.' + winName(win) + '.close');
+ assert_not_equals(close, close_B, 'close_B is unique when viewed by the parent');
+ assert_not_equals(close_B, win.close, 'different Window functions per-incumbent script settings object');
+ checkFunction(close_B, B.Function.prototype);
+
+ checkFunction(location.replace, Function.prototype);
+ assert_not_equals(location.replace, win.location.replace, "cross-origin Location functions get their own object");
+ var replace_B = B.eval('parent.' + winName(win) + '.location.replace');
+ assert_not_equals(replace_B, win.location.replace, 'different Location functions per-incumbent script settings object');
+ checkFunction(replace_B, B.Function.prototype);
+}, "Same-origin observers get different functions for cross-origin objects");
+
+addTest(function(win) {
+ assert_true(isObject(Object.getOwnPropertyDescriptor(win, 'parent')),
+ "Need to be able to use Object.getOwnPropertyDescriptor do this test");
+ var get_self_parent = Object.getOwnPropertyDescriptor(window, 'parent').get;
+ var get_parent_A = Object.getOwnPropertyDescriptor(win, 'parent').get;
+ var get_parent_B = B.eval('Object.getOwnPropertyDescriptor(parent.' + winName(win) + ', "parent").get');
+ assert_not_equals(get_self_parent, get_parent_A, 'different Window accessors per-incumbent script settings object');
+ assert_not_equals(get_parent_A, get_parent_B, 'different Window accessors per-incumbent script settings object');
+ checkFunction(get_self_parent, Function.prototype);
+ checkFunction(get_parent_A, Function.prototype);
+ checkFunction(get_parent_B, B.Function.prototype);
+}, "Same-origin observers get different accessors for cross-origin Window");
+
+addTest(function(win) {
+ var set_self_href = Object.getOwnPropertyDescriptor(window.location, 'href').set;
+ var set_href_A = Object.getOwnPropertyDescriptor(win.location, 'href').set;
+ var set_href_B = B.eval('Object.getOwnPropertyDescriptor(parent.' + winName(win) + '.location, "href").set');
+ assert_not_equals(set_self_href, set_href_A, 'different Location accessors per-incumbent script settings object');
+ assert_not_equals(set_href_A, set_href_B, 'different Location accessors per-incumbent script settings object');
+ checkFunction(set_self_href, Function.prototype);
+ checkFunction(set_href_A, Function.prototype);
+ checkFunction(set_href_B, B.Function.prototype);
+}, "Same-origin observers get different accessors for cross-origin Location");
+
+addTest(function(win) {
+ assert_equals({}.toString.call(win), "[object Object]");
+ assert_equals({}.toString.call(win.location), "[object Object]");
+}, "{}.toString.call() does the right thing on cross-origin objects");
+
+addPromiseTest(function(win) {
+ return Promise.resolve(win).then((arg) => {
+ assert_equals(arg, win);
+ });
+}, "Resolving a promise with a cross-origin window without a 'then' subframe should work");
+
+addPromiseThenTest(function(win) {
+ return Promise.resolve(win).then((arg) => {
+ assert_equals(arg, win);
+ });
+}, "Resolving a promise with a cross-origin window with a 'then' subframe should work");
+
+addPromiseThenTest(function(win) {
+ return Promise.resolve(win.location).then((arg) => {
+ assert_equals(arg, win.location);
+ });
+}, "Resolving a promise with a cross-origin location should work");
+
+addTest(function(win) {
+ var desc = Object.getOwnPropertyDescriptor(window, "onmouseenter");
+ var f = () => {};
+
+ // Check that it has [LegacyLenientThis] behavior
+ assert_equals(desc.get.call({}), undefined, "getter should return undefined");
+ desc.set.call({}, f); // Should not throw.
+
+ // Check that we can apply it to a same-origin window.
+ assert_equals(desc.get.call(B), null, "Should be able to read the value");
+ desc.set.call(B, f);
+ assert_equals(desc.get.call(B), f, "Value should have updated");
+ // And reset it for our next test
+ desc.set.call(B, null);
+ assert_equals(desc.get.call(B), null, "Should have been reset");
+
+ // Check that applying it to a cross-origin window throws instead of doing
+ // the [LegacyLenientThis] behavior.
+ assert_throws_dom("SecurityError", () => {
+ desc.get.call(win);
+ }, "Should throw when getting cross-origin");
+ assert_throws_dom("SecurityError", () => {
+ desc.set.call(win, f);
+ }, "Should throw when setting cross-origin");
+}, "LegacyLenientThis behavior");
+
+// We do a fresh load of the subframes for each test to minimize side-effects.
+// It would be nice to reload ourselves as well, but we can't do that without
+// disrupting the test harness.
+function testDone() {
+ if (testList.length != 0) {
+ reloadSubframes(runNextTest);
+ } else {
+ done();
+ }
+}
+
+async function runNextTest() {
+ var entry = testList.shift();
+ if (entry.promiseTest) {
+ for (let t of entry.tests) {
+ await new Promise(resolve => {
+ promise_test(test_obj => {
+ return new Promise(res => res(t.func(test_obj))).finally(resolve);
+ }, t.desc);
+ });
+ }
+ } else {
+ for (let t of entry.tests) {
+ test(t.func, t.desc);
+ }
+ }
+ testDone();
+}
+reloadSubframes(runNextTest);
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/frame-with-then.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/frame-with-then.html
new file mode 100644
index 0000000000..3eedeca38a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/frame-with-then.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+ <script>
+ if (location.search == "?setdomain") {
+ document.domain = document.domain;
+ }
+ </script>
+ <body>
+ <!--- Some frames to test ordering -->
+ <iframe name="a"></iframe>
+ <!-- A subframe to test "then" behavior -->
+ <iframe name="then"></iframe>
+ <iframe name="b"></iframe>
+ <!-- Two subframes with names corresponding to IDL-defined properties; one
+ a cross-origin-exposed property and one not exposed cross-origin -->
+ <iframe name="close"></iframe>
+ <iframe name="open"></iframe>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/frame.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/frame.html
new file mode 100644
index 0000000000..ca2dd8ebf8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/frame.html
@@ -0,0 +1,51 @@
+<!doctype html>
+<html>
+<head>
+<script>
+ if (location.search == "?setdomain") {
+ document.domain = document.domain;
+ }
+
+ // Override the |frames| and |focus| property to test that such overrides are
+ // properly ignored cross-origin.
+ window.frames = "override";
+ window.focus = "override";
+
+ // Also add a |then| property to test that it doesn't get exposed.
+ window.then = "something";
+ window.location.then = "something-else";
+
+ // If we get a postMessage, we grab references to everything and set
+ // document.domain to trim off our topmost subdomain.
+ window.onmessage = function(evt) {
+ window.windowReferences = [];
+ window.locationReferences = [];
+ for (var i = 0; i < parent.length; ++i) {
+ windowReferences.push(parent[i]);
+ locationReferences.push(parent[i].location);
+ }
+ try {
+ document.domain = document.domain.substring(document.domain.indexOf('.') + 1);
+ evt.source.postMessage('PASS', '*');
+ } catch (e) {
+ evt.source.postMessage('FAIL: cannot trim off document.domain: ' + e, '*');
+ }
+ }
+
+ function checkWindowReferences() {
+ for (var i = 0; i < parent.length; ++i) {
+ if (windowReferences[i] != parent[i])
+ throw new Error("Window references don't match for " + i + " after document.domain");
+ if (locationReferences[i] != parent[i].location)
+ throw new Error("Location references don't match for " + i + " after document.domain");
+ }
+ return true;
+ }
+</script>
+</head>
+<body>
+ <!-- Two subframes to give us some indexed properties -->
+ <iframe></iframe>
+ <iframe name=donotleakme></iframe><!-- "donotleakme" is excluded as cross-origin named property due to [[HideFromKeys]] -->
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/location-properties-smoke-test.window.js b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/location-properties-smoke-test.window.js
new file mode 100644
index 0000000000..45b61d07f5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/location-properties-smoke-test.window.js
@@ -0,0 +1,51 @@
+// META: variant=?assign
+// META: variant=?customproperty
+// META: variant=?hash
+// META: variant=?host
+// META: variant=?hostname
+// META: variant=?pathname
+// META: variant=?port
+// META: variant=?protocol
+// META: variant=?reload
+// META: variant=?search
+// META: variant=?toString
+// META: variant=?valueOf
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/common/dispatcher/dispatcher.js
+
+const property = window.location.search.substr(1);
+
+promise_test(async t => {
+ const iframeContext = new RemoteContext(token());
+ const iframe = document.createElement("iframe");
+ iframe.src = get_host_info().REMOTE_ORIGIN +
+ "/common/dispatcher/remote-executor.html?uuid=" + iframeContext.context_id;
+ document.body.appendChild(iframe);
+
+ // Wait for the cross-origin document to be loaded inside the iframe.
+ assert_equals(
+ await iframeContext.execute_script(() => "Document loaded") ,
+ "Document loaded"
+ );
+
+ assert_throws_dom("SecurityError", () => {
+ const unused = iframe.contentWindow.location[property];
+ }, "Cross origin get of a location property should throw a security error");
+
+ assert_throws_dom("SecurityError", () => {
+ iframe.contentWindow.location[property] = "Random string";
+ }, "Cross origin set of a location property should throw a security error");
+
+ // Verify that the property was indeed not modified.
+ assert_not_equals(
+ await iframeContext.execute_script(property => location[property],
+ [property]),
+ "Random string",
+ );
+
+ assert_throws_dom("SecurityError", () => {
+ const unused = Object.getOwnPropertyDescriptor(
+ iframe.contentWindow.location, property);
+ }, "Cross origin get of descriptors should throw a security error");
+}, `Verifying that cross-origin access of '${property}' is restricted`);
diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/resources/cross-origin-due-to-document-domain-only-helper.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/resources/cross-origin-due-to-document-domain-only-helper.html
new file mode 100644
index 0000000000..10ac8ece0e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/resources/cross-origin-due-to-document-domain-only-helper.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<meta charset=utf-8>
+<script>
+self.expandosForever = true
+self.location.expandosForever = true
+function setDocumentDomain() {
+ document.domain = document.domain
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/win-documentdomain.sub.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/win-documentdomain.sub.html
new file mode 100644
index 0000000000..a315e21208
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/win-documentdomain.sub.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script>
+ function loadFrames() {
+ window.A = document.getElementById('A').contentWindow;
+ window.B = document.getElementById('B').contentWindow;
+ window.C = document.getElementById('C').contentWindow;
+ window.D = document.getElementById('D').contentWindow;
+
+ var path = location.pathname.substring(0, location.pathname.lastIndexOf('/')) + '/frame.html';
+ A.location = 'frame.html';
+ B.location = '//{{domains[www2]}}:' + get_port(location) + path;
+ C.location = '//{{domains[www2]}}:' + get_port(location) + path;
+ D.location = '//{{domains[www1]}}:' + get_port(location) + path;
+
+ var loadCount = 0;
+ function frameLoaded() {
+ if (++loadCount == 4)
+ go();
+ }
+ var iframes = document.getElementsByTagName('iframe');
+ for (var i = 0; i < iframes.length; i++) {
+ iframes[i].onload = frameLoaded;
+ }
+ }
+
+ var results = [];
+ function assert(cond, msg) {
+ results.push({pass: !!cond, message: msg});
+ }
+
+ function go() {
+ window.onmessage = function(evt) {
+ try {
+ assert(evt.data == "PASS", "frame.html processing should be PASS but got " + evt.data);
+ assert(B.checkWindowReferences(), "B's Window references are still self-consistent after document.domain");
+ for (var i = 0; i < window.length; ++i) {
+ assert(window[i] === B.windowReferences[i],
+ "Window reference " + i + " consistent between globals after document.domain");
+ assert(window[i].location === B.locationReferences[i],
+ "Location reference " + i + " consistent between globals after document.domain");
+ }
+ } catch(e) {
+ assert(false, "Should not receive exception: " + e);
+ }
+ opener.postMessage(results, '*');
+ };
+ A.document.domain = A.document.domain;
+ document.domain = document.domain;
+ B.postMessage('', '*');
+ }
+
+ </script>
+</head>
+<body onload="loadFrames()">
+ <iframe id="A"></iframe>
+ <iframe id="B"></iframe>
+ <iframe id="C"></iframe>
+ <iframe id="D"></iframe>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/window-location-and-location-href-cross-realm-set.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/window-location-and-location-href-cross-realm-set.html
new file mode 100644
index 0000000000..f03550a141
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/window-location-and-location-href-cross-realm-set.html
@@ -0,0 +1,106 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cross-realm [[Set]] to window.location and location.href throws an error of correct realm</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#window">
+<link rel="help" href="https://webidl.spec.whatwg.org/#Unforgeable">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body>
+<script>
+const URL_SAME_ORIGIN = get_host_info().ORIGINAL_HOST;
+const URL_CROSS_ORIGIN = get_host_info().HTTP_REMOTE_ORIGIN;
+const URL_VALID = "#foo";
+const URL_INVALID = "http://#";
+
+const { get: locationGet, set: locationSet } = Object.getOwnPropertyDescriptor(window, "location");
+const { get: hrefGet, set: hrefSet } = Object.getOwnPropertyDescriptor(location, "href");
+
+
+promise_test(async t => {
+ const sameOriginWindow = await makeWindow(t, URL_SAME_ORIGIN);
+ assert_throws_js(sameOriginWindow.TypeError, () => { Object.create(sameOriginWindow).location; });
+ assert_throws_js(sameOriginWindow.TypeError, () => { Reflect.get(sameOriginWindow, "location", {}); });
+ assert_throws_js(TypeError, () => { locationGet.call({}); });
+}, "Same-origin window.location getter throws TypeError in holder's realm on invalid |this| value");
+
+promise_test(async t => {
+ const sameOriginWindow = await makeWindow(t, URL_SAME_ORIGIN);
+ assert_throws_js(sameOriginWindow.TypeError, () => { Object.create(sameOriginWindow.location).href; });
+ assert_throws_js(sameOriginWindow.TypeError, () => { Reflect.get(sameOriginWindow.location, "href", {}); });
+ assert_throws_js(TypeError, () => { hrefGet(); });
+}, "Same-origin location.href getter throws TypeError in holder's realm on invalid |this| value");
+
+promise_test(async t => {
+ const crossOriginWindow = await makeWindow(t, URL_CROSS_ORIGIN);
+ assert_throws_dom("SECURITY_ERR", () => { crossOriginWindow.location.href; });
+ assert_throws_dom("SECURITY_ERR", () => { hrefGet.call(crossOriginWindow.location); });
+ assert_equals(Object.getOwnPropertyDescriptor(crossOriginWindow.location, "href").get, undefined);
+}, "Cross-origin location.href getter throws SecurityError in lexical realm");
+
+
+promise_test(async t => {
+ const sameOriginWindow = await makeWindow(t, URL_SAME_ORIGIN);
+ assert_throws_js(sameOriginWindow.TypeError, () => { Object.create(sameOriginWindow).location = URL_VALID; });
+ assert_throws_js(sameOriginWindow.TypeError, () => { Reflect.set(sameOriginWindow, "location", URL_VALID, {}); });
+ assert_throws_js(TypeError, () => { locationSet.call(() => {}, URL_VALID); });
+}, "Same-origin window.location setter throws TypeError in holder's realm on invalid |this| value");
+
+promise_test(async t => {
+ const sameOriginWindow = await makeWindow(t, URL_SAME_ORIGIN);
+ assert_throws_js(sameOriginWindow.TypeError, () => { Object.create(sameOriginWindow.location).href = URL_VALID; });
+ assert_throws_js(sameOriginWindow.TypeError, () => { Reflect.set(sameOriginWindow.location, "href", URL_VALID, {}); });
+ assert_throws_js(TypeError, () => { hrefSet.call(undefined, URL_VALID); });
+}, "Same-origin location.href setter throws TypeError in holder's realm on invalid |this| value");
+
+promise_test(async t => {
+ const crossOriginWindow = await makeWindow(t, URL_CROSS_ORIGIN);
+ assert_throws_js(TypeError, () => { Object.create(crossOriginWindow).location = URL_VALID; });
+ assert_throws_js(TypeError, () => { Reflect.set(crossOriginWindow, "location", URL_VALID, {}); });
+ assert_throws_js(TypeError, () => { locationSet.call([], URL_VALID); });
+}, "Cross-origin window.location setter throws TypeError in lexical realm on invalid |this| value");
+
+promise_test(async t => {
+ const crossOriginWindow = await makeWindow(t, URL_CROSS_ORIGIN);
+ assert_throws_js(TypeError, () => { Object.create(crossOriginWindow.location).href = URL_VALID; });
+ assert_throws_js(TypeError, () => { Reflect.set(crossOriginWindow.location, "href", URL_VALID, {}); });
+ assert_throws_js(TypeError, () => { hrefSet.call(null, URL_VALID); });
+}, "Cross-origin location.href setter throws TypeError in lexical realm on invalid |this| value");
+
+
+promise_test(async t => {
+ const sameOriginWindow = await makeWindow(t, URL_SAME_ORIGIN);
+ assert_throws_js(sameOriginWindow.TypeError, () => { sameOriginWindow.location = Symbol(); });
+
+ // The error originates in sameOriginWindow.location.href setter, hence it's not in realm of locationSet.
+ assert_throws_js(sameOriginWindow.TypeError, () => { locationSet.call(sameOriginWindow, Symbol()); });
+}, "Same-origin window.location` setter throws TypeError in holder's realm on non-coercible URL argument");
+
+promise_test(async t => {
+ const sameOriginWindow = await makeWindow(t, URL_SAME_ORIGIN);
+ assert_throws_js(sameOriginWindow.TypeError, () => { sameOriginWindow.location.href = Symbol(); });
+ assert_throws_js(TypeError, () => { hrefSet.call(sameOriginWindow.location, Symbol()); });
+}, "Same-origin location.href setter throws TypeError in holder's realm on non-coercible URL argument");
+
+promise_test(async t => {
+ const crossOriginWindow = await makeWindow(t, URL_CROSS_ORIGIN);
+ assert_throws_js(TypeError, () => { crossOriginWindow.location = Symbol(); });
+ assert_throws_js(TypeError, () => { locationSet.call(crossOriginWindow, Symbol()); });
+}, "Cross-origin window.location setter throws TypeError in lexical realm on non-coercible URL argument");
+
+promise_test(async t => {
+ const crossOriginWindow = await makeWindow(t, URL_CROSS_ORIGIN);
+ assert_throws_js(TypeError, () => { crossOriginWindow.location.href = Symbol(); });
+ assert_throws_js(TypeError, () => { hrefSet.call(crossOriginWindow.location, Symbol()); });
+}, "Cross-origin location.href setter throws TypeError in lexical realm on non-coercible URL argument");
+
+function makeWindow(t, src) {
+ return new Promise(resolve => {
+ const iframe = document.createElement("iframe");
+ t.add_cleanup(() => { iframe.remove(); });
+ iframe.onload = () => { resolve(iframe.contentWindow); };
+ iframe.src = src;
+ document.body.append(iframe);
+ });
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/inheritance/about-blank-iframe.html b/testing/web-platform/tests/html/browsers/origin/inheritance/about-blank-iframe.html
new file mode 100644
index 0000000000..fabde327a1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/inheritance/about-blank-iframe.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<html>
+ <head>
+ <title>about:blank in child browsing context aliases security origin</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ test(() => {
+ let iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ // Should not throw: srcdoc should always be same-origin.
+ iframe.contentWindow.document.body.innerHTML = '<p>Hello world!</p>';
+
+ // Explicitly set `domain` component of origin: any other same-origin
+ // browsing contexts are now cross-origin unless they also explicitly
+ // set document.domain to the same value.
+ document.domain = document.domain;
+ // Should not throw: the origin should be aliased, so setting
+ // document.domain in one Document should affect both Documents.
+ assert_equals(
+ iframe.contentWindow.document.body.textContent,
+ 'Hello world!');
+ });
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/origin/inheritance/about-blank-window.html b/testing/web-platform/tests/html/browsers/origin/inheritance/about-blank-window.html
new file mode 100644
index 0000000000..cc3177f943
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/inheritance/about-blank-window.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html>
+ <head>
+ <title>about:blank in auxiliary browsing context aliases security origin</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ test(() => {
+ let newWindow = window.open();
+ // Should not throw: the newly-opened window should be same-origin.
+ newWindow.document.body.innerHTML = '<p>Hello world!</p>';
+
+ // Explicitly set `domain` component of origin: any other same-origin
+ // browsing contexts are now cross-origin unless they also explicitly
+ // set document.domain to the same value.
+ document.domain = document.domain;
+ // Should not throw: the origin should be aliased, so setting
+ // document.domain in one Document should affect both Documents.
+ assert_equals(newWindow.document.body.textContent, 'Hello world!');
+ });
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/origin/inheritance/about-srcdoc.html b/testing/web-platform/tests/html/browsers/origin/inheritance/about-srcdoc.html
new file mode 100644
index 0000000000..971811ee66
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/inheritance/about-srcdoc.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<html>
+ <head>
+ <title>about:srcdoc aliases security origin</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ test(() => {
+ let iframe = document.createElement('iframe');
+ iframe.srcdoc = '<body></body>';
+ document.body.appendChild(iframe);
+ // Should not throw: srcdoc should always be same-origin.
+ iframe.contentWindow.document.body.innerHTML = '<p>Hello world!</p>';
+
+ // Explicitly set `domain` component of origin: any other same-origin
+ // browsing contexts are now cross-origin unless they also explicitly
+ // set document.domain to the same value.
+ document.domain = document.domain;
+ // Should not throw: the origin should be aliased, so setting
+ // document.domain in one Document should affect both Documents.
+ assert_equals(
+ iframe.contentWindow.document.body.textContent,
+ 'Hello world!');
+ });
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/origin/inheritance/document-write.https.window.js b/testing/web-platform/tests/html/browsers/origin/inheritance/document-write.https.window.js
new file mode 100644
index 0000000000..39dc3b1cc2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/inheritance/document-write.https.window.js
@@ -0,0 +1,10 @@
+// META: script=/common/get-host-info.sub.js
+
+// To use document.domain, we need to start from a subdomain.
+//
+// For document.domain setter to work, some web browser require the
+// |Origin-Agent-Cluster: ?0| header to be set uniformly on the origin.
+const origin = get_host_info().OTHER_ORIGIN;
+const openee = window.open(
+ origin + '/html/browsers/origin/inheritance/resources/document-write.html')
+fetch_tests_from_window(openee);
diff --git a/testing/web-platform/tests/html/browsers/origin/inheritance/document-write.https.window.js.headers b/testing/web-platform/tests/html/browsers/origin/inheritance/document-write.https.window.js.headers
new file mode 100644
index 0000000000..e007de4d7b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/inheritance/document-write.https.window.js.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?0
diff --git a/testing/web-platform/tests/html/browsers/origin/inheritance/javascript-url.html b/testing/web-platform/tests/html/browsers/origin/inheritance/javascript-url.html
new file mode 100644
index 0000000000..7dfb1130ce
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/inheritance/javascript-url.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<html>
+ <head>
+ <title>javascript: aliases security origin</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ promise_test(t => {
+ let iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ // Should not throw: srcdoc should always be same-origin.
+ iframe.contentDocument;
+
+ iframe.contentWindow.location = 'javascript:"Hello world!"';
+ return new Promise(resolve => {
+ iframe.addEventListener('load', resolve);
+ }).then(() => {
+ // Explicitly set `domain` component of origin: any other same-origin
+ // browsing contexts are now cross-origin unless they also explicitly
+ // set document.domain to the same value.
+ document.domain = document.domain;
+ // Should not throw: the origin should be aliased, so setting
+ // document.domain in one Document should affect both Documents.
+ assert_equals(
+ iframe.contentWindow.document.body.textContent,
+ 'Hello world!');
+ });
+ });
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/origin/inheritance/resources/document-write.html b/testing/web-platform/tests/html/browsers/origin/inheritance/resources/document-write.html
new file mode 100644
index 0000000000..7a6ff3118e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/inheritance/resources/document-write.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script src="/resources/testharness.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body></body>
+ <script>
+ const domain_start = document.domain;
+ const domain_new = domain_start.replace(/^[^.]+\./,'');
+
+ async_test(test => {
+ const iframe = document.createElement('iframe');
+ iframe.src = './iframe-with-about-blank-iframe.html';
+ iframe.onload = test.step_func_done(() => {
+ const doc0 = frames[0].frames[0].document;
+ const doc1 = frames[0].frames[1].document;
+
+ assert_equals(doc0.domain, domain_start);
+ assert_equals(doc1.domain, domain_start);
+
+ doc0.open();
+ doc1.open();
+ assert_equals(doc0.domain, domain_start);
+ assert_equals(doc1.domain, domain_start);
+
+ document.domain = domain_new;
+ assert_equals(doc0.domain, domain_start);
+ assert_equals(doc1.domain, domain_start);
+
+ doc0.close();
+ doc1.close();
+ assert_equals(doc0.domain, domain_start);
+ assert_equals(doc1.domain, domain_start);
+ });
+ document.body.appendChild(iframe);
+ }, "document.open() do not make the callee's origin to alias the caller's"
+ + " one");
+ </script>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/origin/inheritance/resources/document-write.html.headers b/testing/web-platform/tests/html/browsers/origin/inheritance/resources/document-write.html.headers
new file mode 100644
index 0000000000..e007de4d7b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/inheritance/resources/document-write.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?0
diff --git a/testing/web-platform/tests/html/browsers/origin/inheritance/resources/iframe-with-about-blank-iframe.html b/testing/web-platform/tests/html/browsers/origin/inheritance/resources/iframe-with-about-blank-iframe.html
new file mode 100644
index 0000000000..b3f5125233
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/inheritance/resources/iframe-with-about-blank-iframe.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <iframe src="about:blank"></iframe>
+ <iframe src=""></iframe>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/origin/inheritance/resources/iframe-with-about-blank-iframe.html.headers b/testing/web-platform/tests/html/browsers/origin/inheritance/resources/iframe-with-about-blank-iframe.html.headers
new file mode 100644
index 0000000000..e007de4d7b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/inheritance/resources/iframe-with-about-blank-iframe.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?0
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-bad-subdomain.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-bad-subdomain.sub.https.html
new file mode 100644
index 0000000000..3a45ee6d6a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-bad-subdomain.sub.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is site-keyed, child attempts to origin-key but uses a bad header value, child is a subdomain of the parent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testSameAgentCluster,
+ testGetter
+} from "../resources/helpers.mjs";
+
+let frameIndex = 0;
+for (const badValue of ["", "?0", "true", "\"?1\"", "1", "?2", "(?1)"]) {
+ promise_test(async () => {
+ await insertIframe("{{hosts[][www]}}", badValue);
+ }, `"${badValue}": frame insertion`);
+
+ // Since the header values are bad they should be site-keyed.
+ testSameAgentCluster([self, frameIndex], `"${badValue}"`);
+ testGetter(frameIndex, false, `"${badValue}"`);
+ ++frameIndex;
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-port.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-port.sub.https.html
new file mode 100644
index 0000000000..85fb1f64e0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-port.sub.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is site-keyed, child is origin-keyed, child is different-origin to the parent because of a port mismatch</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ await insertIframe("{{hosts[][]}}:{{ports[https][1]}}", "?1");
+});
+
+// Since they're different-origin, the child's request is respected, so the
+// parent ends up in the site-keyed agent cluster and the child in the
+// origin-keyed one.
+testDifferentAgentClusters([self, 0]);
+
+testGetter(self, false, "parent");
+testGetter(0, true, "child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-same.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-same.sub.https.html
new file mode 100644
index 0000000000..7ece02c81a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-same.sub.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is site-keyed, child is origin-keyed, child is same-origin to the parent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testSameAgentCluster,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ await insertIframe("{{hosts[][]}}", "?1");
+});
+
+// Since they're same-origin, and the parent loaded in the site-keyed agent
+// cluster, the child's request for origin-keying gets ignored, and both end up
+// site-keyed.
+testSameAgentCluster([self, 0]);
+
+testGetter(self, false, "parent");
+testGetter(0, false, "child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-subdomain-with-redirect.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-subdomain-with-redirect.sub.https.html
new file mode 100644
index 0000000000..994f80876d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-subdomain-with-redirect.sub.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is site-keyed, child is reached via a redirect response with no header, child final response does have the header</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ await insertIframe("{{hosts[][www]}}", "?1", { redirectFirst: true });
+});
+
+// Since they're different-origin, the child's request is respected, so the
+// parent ends up in the site-keyed agent cluster and the child in the
+// origin-keyed one.
+testDifferentAgentClusters([self, 0]);
+
+testGetter(self, false, "parent");
+testGetter(0, true, "child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-subdomain.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-subdomain.sub.https.html
new file mode 100644
index 0000000000..5fc2fa29f3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-subdomain.sub.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is site-keyed, child is origin-keyed, child is a subdomain of the parent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ await insertIframe("{{hosts[][www]}}", "?1");
+});
+
+// Since they're different-origin, the child's request is respected, so the
+// parent ends up in the site-keyed agent cluster and the child in the
+// origin-keyed one.
+testDifferentAgentClusters([self, 0]);
+
+testGetter(self, false, "parent");
+testGetter(0, true, "child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yeswithparams-subdomain.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yeswithparams-subdomain.sub.https.html
new file mode 100644
index 0000000000..3e7c8419b3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yeswithparams-subdomain.sub.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is site-keyed, child is origin-keyed using parameters on its structured header, child is a subdomain of the parent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ await insertIframe("{{hosts[][www]}}", "?1;param1;param2=value2");
+});
+
+// Since they're different-origin, the child's request is respected, so the
+// parent ends up in the site-keyed agent cluster and the child in the
+// origin-keyed one.
+testDifferentAgentClusters([self, 0]);
+
+testGetter(self, false, "parent");
+testGetter(0, true, "child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-port.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-port.sub.https.html
new file mode 100644
index 0000000000..f00814cfbf
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-port.sub.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is origin-keyed, child is site-keyed, child is is different-origin to the parent because of a port mismatch</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ await insertIframe("{{hosts[][]}}:{{ports[https][1]}}");
+});
+
+// Since they're different-origin, the parent's request is respected, as is the
+// child's non-request. So the parent ends up in the origin-keyed agent cluster
+// and the child ends up in the site-keyed one.
+testDifferentAgentClusters([self, 0]);
+
+testGetter(self, true, "parent");
+testGetter(0, false, "child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-port.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-port.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-port.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-same.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-same.sub.https.html
new file mode 100644
index 0000000000..307f8c48d7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-same.sub.https.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is origin-keyed, child is site-keyed, child is same-origin to the parent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testSameAgentCluster,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ await insertIframe("{{hosts[][]}}");
+});
+
+// Since they're same-origin, and the parent loaded with origin-keying, the
+// child's non-request gets ignored, and both end up origin-keyed.
+testSameAgentCluster([self, 0]);
+
+testGetter(self, true, "parent");
+testGetter(0, true, "child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-same.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-same.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-same.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-subdomain.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-subdomain.sub.https.html
new file mode 100644
index 0000000000..8c823fa36f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-subdomain.sub.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is origin-keyed, child is site-keyed, child is a subdomain of the parent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ await insertIframe("{{hosts[][www]}}");
+});
+
+// Since they're different-origin, the parent's request is respected, as is the
+// child's non-request. So the parent ends up in the origin-keyed agent cluster
+// and the child ends up in the site-keyed one.
+testDifferentAgentClusters([self, 0]);
+
+testGetter(self, true, "parent");
+testGetter(0, false, "child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-subdomain.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-subdomain.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-subdomain.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-port.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-port.sub.https.html
new file mode 100644
index 0000000000..5e431e6e41
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-port.sub.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is origin-keyed, child is site-keyed, child is different-origin to the parent because of a port mismatch</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ await insertIframe("{{hosts[][]}}:{{ports[https][1]}}", "?1");
+});
+
+// Both request origin-keying, so the parent ends up in one origin-keyed agent
+// cluster (the default port's origin), and the child ends up in a different
+// origin-keyed agent cluster (the other port's origin).
+testDifferentAgentClusters([self, 0]);
+
+testGetter(self, true, "parent");
+testGetter(0, true, "child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-port.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-port.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-port.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-same.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-same.sub.https.html
new file mode 100644
index 0000000000..3b8c214a61
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-same.sub.https.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is origin-keyed, child is site-keyed, child is same-origin to the parent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testSameAgentCluster,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ await insertIframe("{{hosts[][]}}", "?1");
+});
+
+// Both request origin-keying, and they're same-origin, so they both end up in
+// the same origin-keyed agent cluster.
+testSameAgentCluster([self, 0]);
+
+testGetter(self, true, "parent");
+testGetter(0, true, "child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-same.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-same.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-same.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-subdomain.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-subdomain.sub.https.html
new file mode 100644
index 0000000000..136a3a0bba
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-subdomain.sub.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is origin-keyed, child is site-keyed, child is a subdomain of the parent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ await insertIframe("{{hosts[][www]}}", "?1");
+});
+
+// Both request origin-keying, so the parent ends up in one origin-keyed agent
+// cluster (the base domain's origin), and the child ends up in a different
+// origin-keyed agent cluster (the www subdomain's origin).
+testDifferentAgentClusters([self, 0]);
+
+testGetter(self, true, "parent");
+testGetter(0, true, "child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-subdomain.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-subdomain.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-subdomain.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-no-subdomain-child2-yes-subdomain.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-no-subdomain-child2-yes-subdomain.sub.https.html
new file mode 100644
index 0000000000..1bb252f0ab
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-no-subdomain-child2-yes-subdomain.sub.https.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is site-keyed, subdomain child 1 is site-keyed, same-subdomain child 2 is origin-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testSameAgentCluster,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ // Must be sequential, not parallel: the site-keyed frame must load first.
+ await insertIframe("{{hosts[][www]}}");
+ await insertIframe("{{hosts[][www]}}", "?1");
+});
+
+
+// Since they're different-origin, the parent's non-request is respected, as is
+// child 1's non-request. child 2 requests origin-keying but is ignored, since
+// child 1 is in the same browsing context group.
+//
+// So, everyone ends up in the site-keyed agent cluster.
+testSameAgentCluster([self, 0], "Parent to child1");
+testSameAgentCluster([self, 1], "Parent to child2");
+testSameAgentCluster([0, 1], "child1 to child2");
+testSameAgentCluster([1, 0], "child2 to child1");
+
+testGetter(self, false, "parent");
+testGetter(0, false, "child1");
+testGetter(1, false, "child2");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-no-subdomain-child2-yes-subdomainport.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-no-subdomain-child2-yes-subdomainport.sub.https.html
new file mode 100644
index 0000000000..5b80c528f0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-no-subdomain-child2-yes-subdomainport.sub.https.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is site-keyed, subdomain child 1 is site-keyed, different-port-same-subdomain child 2 is origin-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testSameAgentCluster,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ // Order of loading should not matter, but we make it sequential to ensure the
+ // tests are deterministic.
+ await insertIframe("{{hosts[][www]}}");
+ await insertIframe("{{hosts[][www]}}:{{ports[https][1]}}", "?1");
+});
+
+// Since everybody is different-origin, everyone's requests/non-requests get
+// respected.
+//
+// So, the parent and child 1 end up in the site-keyed agent cluster, and child
+// 2 ends up in its own origin-keyed agent cluster.
+testSameAgentCluster([self, 0], "Parent to child1");
+testDifferentAgentClusters([self, 1], "Parent to child2");
+testDifferentAgentClusters([0, 1], "child1 to child2");
+testDifferentAgentClusters([1, 0], "child2 to child1");
+
+testGetter(self, false, "parent");
+testGetter(0, false, "child1");
+testGetter(1, true, "child2");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-no-subdomain1-child2-yes-subdomain2.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-no-subdomain1-child2-yes-subdomain2.sub.https.html
new file mode 100644
index 0000000000..bba13b82a4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-no-subdomain1-child2-yes-subdomain2.sub.https.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is site-keyed, subdomain child 1 is site-keyed, different-subdomain child 2 is origin-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testSameAgentCluster,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ // Order of loading should not matter, but we make it sequential to ensure the
+ // tests are deterministic.
+ await insertIframe("{{hosts[][www]}}");
+ await insertIframe("{{hosts[][www1]}}", "?1");
+});
+
+// Since everybody is different-origin, everyone's requests/non-requests get
+// respected.
+//
+// So, the parent and child 1 end up in the site-keyed agent cluster, and child
+// 2 ends up in its own origin-keyed agent cluster.
+testSameAgentCluster([self, 0], "Parent to child1");
+testDifferentAgentClusters([self, 1], "Parent to child2");
+testDifferentAgentClusters([0, 1], "child1 to child2");
+testDifferentAgentClusters([1, 0], "child2 to child1");
+
+testGetter(self, false, "parent");
+testGetter(0, false, "child1");
+testGetter(1, true, "child2");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-yes-subdomain-child2-no-port.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-yes-subdomain-child2-no-port.sub.https.html
new file mode 100644
index 0000000000..d01d180213
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-yes-subdomain-child2-no-port.sub.https.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is site-keyed, subdomain child 1 is origin-keyed, non-subdomain but different-port child 2 is site-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testSameAgentCluster,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ // Order of loading should not matter, but we make it sequential to ensure the
+ // tests are deterministic.
+ await insertIframe("{{hosts[][www]}}", "?1");
+ await insertIframe("{{hosts[][]}}:{{ports[https][1]}}");
+});
+
+// Everyone is different-origin, so everyone's request/non-request is
+// respected.
+//
+// So, the parent and child2 end up in the site-keyed agent cluster, and child1
+// ends up in an origin-keyed agent cluster.
+testDifferentAgentClusters([self, 0], "Parent to child1");
+testSameAgentCluster([self, 1], "Parent to child2");
+testDifferentAgentClusters([0, 1], "child1 to child2");
+testDifferentAgentClusters([1, 0], "child2 to child1");
+
+testGetter(self, false, "parent");
+testGetter(0, true, "child1");
+testGetter(1, false, "child2");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-yes-subdomain-child2-no-subdomain.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-yes-subdomain-child2-no-subdomain.sub.https.html
new file mode 100644
index 0000000000..9a245b3ace
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-yes-subdomain-child2-no-subdomain.sub.https.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is site-keyed, subdomain child 1 is origin-keyed, same-subdomain child 2 is site-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testSameAgentCluster,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ // Must be sequential, not parallel: the origin-keyed frame must load first.
+ await insertIframe("{{hosts[][www]}}", "?1");
+ await insertIframe("{{hosts[][www]}}");
+});
+
+
+// Since they're different-origin, the parent's non-request is respected, as is
+// child 1's request. child 2's non-request is ignored, since child 1 is in the
+// same browsing context group.
+//
+// So, the parent ends up in the site-keyed agent cluster, and both children end
+// up in an origin-keyed agent cluster.
+testDifferentAgentClusters([self, 0], "Parent to child1");
+testDifferentAgentClusters([self, 1], "Parent to child2");
+testSameAgentCluster([0, 1], "child1 to child2");
+testSameAgentCluster([1, 0], "child2 to child1");
+
+testGetter(self, false, "parent");
+testGetter(0, true, "child1");
+testGetter(1, true, "child2");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-no-subdomain.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-no-subdomain.sub.https.html
new file mode 100644
index 0000000000..c308b9a17a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-no-subdomain.sub.https.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is origin-keyed, subdomain child 1 is site-keyed, same-subdomain child 2 is site-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testSameAgentCluster,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ // Order of loading should not matter, but we make it sequential to ensure the
+ // tests are deterministic.
+ await insertIframe("{{hosts[][www]}}");
+ await insertIframe("{{hosts[][www]}}");
+});
+
+
+// Since everybody is different-origin, everyone's requests/non-requests get
+// respected.
+//
+// So, the parent ends up in its origin-keyed agent cluster, and child 1 and
+// child 2 both end up in the site-keyed agent cluster.
+testDifferentAgentClusters([self, 0], "Parent to child1");
+testDifferentAgentClusters([self, 1], "Parent to child2");
+testSameAgentCluster([0, 1], "child1 to child2");
+testSameAgentCluster([1, 0], "child2 to child1");
+
+testGetter(self, true, "parent");
+testGetter(0, false, "child1");
+testGetter(1, false, "child2");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-no-subdomain.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-no-subdomain.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-no-subdomain.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-no-subdomain2.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-no-subdomain2.sub.https.html
new file mode 100644
index 0000000000..767d908c21
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-no-subdomain2.sub.https.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is origin-keyed, subdomain child 1 is site-keyed, different-subdomain child 2 is site-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testSameAgentCluster,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ // Order of loading should not matter, but we make it sequential to ensure the
+ // tests are deterministic.
+ await insertIframe("{{hosts[][www]}}");
+ await insertIframe("{{hosts[][www1]}}");
+});
+
+
+// Since everybody is different-origin, everyone's requests/non-requests get
+// respected.
+//
+// So, the parent ends up in its origin-keyed agent cluster, and child 1 and
+// child 2 both end up in the site-keyed agent cluster.
+testDifferentAgentClusters([self, 0], "Parent to child1");
+testDifferentAgentClusters([self, 1], "Parent to child2");
+testSameAgentCluster([0, 1], "child1 to child2");
+testSameAgentCluster([1, 0], "child2 to child1");
+
+testGetter(self, true, "parent");
+testGetter(0, false, "child1");
+testGetter(1, false, "child2");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-no-subdomain2.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-no-subdomain2.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-no-subdomain2.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomain.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomain.sub.https.html
new file mode 100644
index 0000000000..45047f3ae1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomain.sub.https.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is origin-keyed, subdomain child 1 is site-keyed, same-subdomain child 2 is origin-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testSameAgentCluster,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ // Must be sequential, not parallel: the non-isolaed frame must load first.
+ await insertIframe("{{hosts[][www]}}");
+ await insertIframe("{{hosts[][www]}}", "?1");
+});
+
+
+// Since they're different-origin, the parent's request is respected, as is
+// child 1's non-request. child 2 requests origin-keying but is ignored, since
+// child 1 is in the same browsing context group.
+//
+// So, the parent ends up in the origin-keyed agent cluster, and both children
+// ends up in the site-keyed one.
+testDifferentAgentClusters([self, 0], "Parent to child1");
+testDifferentAgentClusters([self, 1], "Parent to child2");
+testSameAgentCluster([0, 1], "child1 to child2");
+testSameAgentCluster([1, 0], "child2 to child1");
+
+testGetter(self, true, "parent");
+testGetter(0, false, "child1");
+testGetter(1, false, "child2");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomain.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomain.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomain.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomain2.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomain2.sub.https.html
new file mode 100644
index 0000000000..202b916767
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomain2.sub.https.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is origin-keyed, subdomain child 1 is site-keyed, different-subdomain child 2 is origin-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ // Order of loading should not matter, but we make it sequential to ensure the
+ // tests are deterministic.
+ await insertIframe("{{hosts[][www]}}");
+ await insertIframe("{{hosts[][www1]}}", "?1");
+});
+
+
+// Since everybody is different-origin, everyone's requests/non-requests get
+// respected.
+//
+// So, the parent ends up in its origin-keyed agent cluster, child 1 ends up in
+// the site-keyed agent cluster, and child 2 ends up in a different origin-keyed
+// agent cluster.
+testDifferentAgentClusters([self, 0], "Parent to child1");
+testDifferentAgentClusters([self, 1], "Parent to child2");
+testDifferentAgentClusters([0, 1], "child1 to child2");
+testDifferentAgentClusters([1, 0], "child2 to child1");
+
+testGetter(self, true, "parent");
+testGetter(0, false, "child1");
+testGetter(1, true, "child2");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomain2.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomain2.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomain2.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomainport.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomainport.sub.https.html
new file mode 100644
index 0000000000..a1316731ac
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomainport.sub.https.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is origin-keyed, subdomain child 1 is site-keyed, different-port-same-subdomain child 2 is origin-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ // Order of loading should not matter, but we make it sequential to ensure the
+ // tests are deterministic.
+ await insertIframe("{{hosts[][www]}}");
+ await insertIframe("{{hosts[][www]}}:{{ports[https][1]}}", "?1");
+});
+
+
+// Since everybody is different-origin, everyone's requests/non-requests get
+// respected.
+//
+// So, the parent ends up in its origin-keyed agent cluster, child 1 ends up in
+// the site-keyed agent cluster, and child 2 ends up in a different origin-keyed
+// agent cluster.
+testDifferentAgentClusters([self, 0], "Parent to child1");
+testDifferentAgentClusters([self, 1], "Parent to child2");
+testDifferentAgentClusters([0, 1], "child1 to child2");
+testDifferentAgentClusters([1, 0], "child2 to child1");
+
+testGetter(self, true, "parent");
+testGetter(0, false, "child1");
+testGetter(1, true, "child2");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomainport.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomainport.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomainport.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-no-port.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-no-port.sub.https.html
new file mode 100644
index 0000000000..46bef4b9a9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-no-port.sub.https.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is origin-keyed, subdomain child 1 is origin-keyed, non-subdomain but different-port child 2 is site-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ // Order of loading should not matter, but we make it sequential to ensure the
+ // tests are deterministic.
+ await insertIframe("{{hosts[][www]}}", "?1");
+ await insertIframe("{{hosts[][]}}:{{ports[https][1]}}");
+});
+
+// Everyone is different-origin, so everyone's request/non-request is
+// respected.
+//
+// So, child2 ends up in the site-keyed agent cluster, and the parent and child1
+// end up in two separate origin-keyed agent clusters.
+testDifferentAgentClusters([self, 0], "Parent to child1");
+testDifferentAgentClusters([self, 1], "Parent to child2");
+testDifferentAgentClusters([0, 1], "child1 to child2");
+testDifferentAgentClusters([1, 0], "child2 to child1");
+
+testGetter(self, true, "parent");
+testGetter(0, true, "child1");
+testGetter(1, false, "child2");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-no-port.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-no-port.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-no-port.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-no-subdomain.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-no-subdomain.sub.https.html
new file mode 100644
index 0000000000..39dcfc04b0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-no-subdomain.sub.https.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is origin-keyed, subdomain child 1 is origin-keyed, same-subdomain child 2 is site-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testSameAgentCluster,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ // Must be sequential, not parallel: the origin-keyed frame must load first.
+ await insertIframe("{{hosts[][www]}}", "?1");
+ await insertIframe("{{hosts[][www]}}");
+});
+
+
+// Since they're different-origin, the parent's request is respected, as is
+// child 1's request. child 2's non-request is ignored, since child 1 is in the
+// same browsing context group.
+//
+// So, the parent ends up in the origin-keyed agent cluster, and both children
+// ends up in a different origin-keyed agent cluster.
+testDifferentAgentClusters([self, 0], "Parent to child1");
+testDifferentAgentClusters([self, 1], "Parent to child2");
+testSameAgentCluster([0, 1], "child1 to child2");
+testSameAgentCluster([1, 0], "child2 to child1");
+
+testGetter(self, true, "parent");
+testGetter(0, true, "child1");
+testGetter(1, true, "child2");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-no-subdomain.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-no-subdomain.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-no-subdomain.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomain.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomain.sub.https.html
new file mode 100644
index 0000000000..b6daf91b54
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomain.sub.https.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is origin-keyed, subdomain child 1 is origin-keyed, same-subdomain child 2 is site-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testSameAgentCluster,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ // Order of loading should not matter, but we make it sequential to ensure the
+ // tests are deterministic.
+ await insertIframe("{{hosts[][www]}}", "?1");
+ await insertIframe("{{hosts[][www]}}", "?1");
+});
+
+
+// Since they're different-origin, the parent's request is respected, as is
+// child 1's request. child 2's request is redundant, since child 1 is in the
+// same browsing context group.
+//
+// So, the parent ends up in the origin-keyed agent cluster, and both children
+// ends up in a different origin-keyed agent cluster.
+testDifferentAgentClusters([self, 0], "Parent to child1");
+testDifferentAgentClusters([self, 1], "Parent to child2");
+testSameAgentCluster([0, 1], "child1 to child2");
+testSameAgentCluster([1, 0], "child2 to child1");
+
+testGetter(self, true, "parent");
+testGetter(0, true, "child1");
+testGetter(1, true, "child2");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomain.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomain.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomain.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomain2.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomain2.sub.https.html
new file mode 100644
index 0000000000..b94f9392d4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomain2.sub.https.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is origin-keyed, subdomain child 1 is origin-keyed, different-subdomain child 2 is origin-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ // Order of loading should not matter, but we make it sequential to ensure the
+ // tests are deterministic.
+ await insertIframe("{{hosts[][www]}}", "?1");
+ await insertIframe("{{hosts[][www1]}}", "?1");
+});
+
+
+// Since everybody is different-origin, everyone's requests get
+// respected.
+//
+// So, the parent ends up in its origin-keyed agent cluster, child 1 ends up in
+// a second origin-keyed agent cluster, and child 2 ends up in a third
+// origin-keyed agent cluster.
+testDifferentAgentClusters([self, 0], "Parent to child1");
+testDifferentAgentClusters([self, 1], "Parent to child2");
+testDifferentAgentClusters([0, 1], "child1 to child2");
+testDifferentAgentClusters([1, 0], "child2 to child1");
+
+testGetter(self, true, "parent");
+testGetter(0, true, "child1");
+testGetter(1, true, "child2");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomain2.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomain2.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomain2.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomainport.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomainport.sub.https.html
new file mode 100644
index 0000000000..fb3fda1bf2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomainport.sub.https.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is origin-keyed, subdomain child 1 is origin-keyed, different-port-same-subdomain child 2 is origin-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+promise_setup(async () => {
+ // Order of loading should not matter, but we make it sequential to ensure the
+ // tests are deterministic.
+ await insertIframe("{{hosts[][www]}}", "?1");
+ await insertIframe("{{hosts[][www]}}:{{ports[https][1]}}", "?1");
+});
+
+
+// Since everybody is different-origin, everyone's requests get
+// respected.
+//
+// So, the parent ends up in its origin-keyed agent cluster, child 1 ends up in
+// a second origin-keyed agent cluster, and child 2 ends up in a third
+// origin-keyed agent cluster.
+testDifferentAgentClusters([self, 0], "Parent to child1");
+testDifferentAgentClusters([self, 1], "Parent to child2");
+testDifferentAgentClusters([0, 1], "child1 to child2");
+testDifferentAgentClusters([1, 0], "child2 to child1");
+
+testGetter(self, true, "parent");
+testGetter(0, true, "child1");
+testGetter(1, true, "child2");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomainport.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomainport.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomainport.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/META.yml b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/META.yml
new file mode 100644
index 0000000000..f21ce69f6a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/META.yml
@@ -0,0 +1,5 @@
+spec: https://html.spec.whatwg.org/multipage/#origin-keyed-agent-clusters
+suggested_reviewers:
+ - domenic
+ - annevk
+ - wjmaclean
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/README.md b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/README.md
new file mode 100644
index 0000000000..85ba3bce7f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/README.md
@@ -0,0 +1,30 @@
+# Origin-keyed agent clusters tests
+
+These are tests for the [origin-keyed agent clusters](https://html.spec.whatwg.org/multipage/origin.html#origin-keyed-agent-clusters)
+feature.
+
+## Test filenames
+
+The tests in `2-iframes` follow the file naming pattern
+
+```
+parent-[yes|no]-child1-[yes|no]-[designator]-child2-[yes|no]-[designator]
+```
+
+Here:
+
+* `yes` or `no` refers to whether the `Origin-Agent-Cluster` header is set or
+ unset.
+* `designator` explains how the child differs from the parent: e.g. by being a
+ subdomain, or having a different port, or both. There's also `same` if it's
+ same-origin.
+
+Other directories have variations on this, e.g. `1-iframe/` does the same thing
+but for a single `child` instead of `child1` and `child2`, and `navigation/`
+uses `1` and `2` to represent the two different locations the single iframe will
+be navigated to.
+
+## Coverage
+
+Header parsing is covered by a few tests in the `1-iframe/` subdirectory, and
+not duplicated to all other scenarios.
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/about-blank.https.sub.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/about-blank.https.sub.html
new file mode 100644
index 0000000000..556d528aa0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/about-blank.https.sub.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>The initial about:blank respects origin isolation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ setBothDocumentDomains,
+ testSameAgentCluster,
+ testDifferentAgentClusters,
+ testGetter
+} from "./resources/helpers.mjs";
+
+promise_setup(async () => {
+ await insertAboutBlankIframe();
+ await insertIframe("{{hosts[][www]}}");
+});
+
+// Since the initial about:blank inherits its origin from its parent, it is
+// same-origin with the parent, and thus cross-origin with child2.
+testSameAgentCluster([self, 0], "parent to about:blank");
+testDifferentAgentClusters([0, 1], "about:blank to child2");
+testDifferentAgentClusters([1, 0], "child2 to about:blank");
+
+testGetter(self, true, "parent");
+testGetter(0, true, "about:blank");
+testGetter(1, false, "child2");
+
+async function insertAboutBlankIframe() {
+ const iframe = await createBlankIframe();
+
+ // Now create and add the script, but don't navigate anywhere (since we want
+ // to stay on the initial about:blank).
+ // We need to absolutize the URL to since about:blank doesn't have a base URL.
+ const scriptURL = (new URL("./resources/send-header-page-script.mjs", import.meta.url)).href;
+ const script = iframe.contentDocument.createElement("script");
+ script.type = "module";
+ script.src = scriptURL;
+
+ await new Promise((resolve, reject) => {
+ script.onload = resolve;
+ script.onerror = () => reject(
+ new Error("Could not load the child frame script into the about:blank page")
+ );
+ iframe.contentDocument.body.append(script);
+ });
+
+ await setBothDocumentDomains(iframe.contentWindow);
+}
+
+function createBlankIframe() {
+ const iframe = document.createElement("iframe");
+ const promise = new Promise(resolve => {
+ iframe.addEventListener("load", resolve);
+ });
+ document.body.append(iframe);
+ return promise;
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/about-blank.https.sub.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/about-blank.https.sub.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/about-blank.https.sub.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/document-domain.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/document-domain.sub.https.html
new file mode 100644
index 0000000000..b4535d9e54
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/document-domain.sub.https.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Setting document.domain does not change same-originness when origin-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ Other tests check that using document.domain doesn't allow cross-origin
+ access. This test ensures a different, more subtle property: that
+ origin-keying makes document.domain into a no-op in other ways.
+-->
+
+<iframe src="resources/frame.html"></iframe>
+<iframe src="//{{domains[www1]}}:{{location[port]}}/html/browsers/origin/origin-keyed-agent-clusters/resources/frame.html"></iframe>
+
+<script type="module">
+setup({ explicit_done: true });
+
+window.onload = () => {
+ test(() => {
+ // Normally, setting document.domain to itself would change the domain
+ // component of the origin. Since the iframe does *not* set document.domain,
+ // the two would then be considered cross-origin.
+ document.domain = document.domain;
+
+ // However, because we're origin-keyed, this shouldn't have any impact. The
+ // test fails if this throws, and passes if it succeeds.
+ frames[0].document;
+ }, "Setting document.domain must not change same-originness");
+
+ test(() => {
+ assert_throws_dom("SecurityError", () => {
+ document.domain = "{{hosts[][nonexistent]}}";
+ });
+ }, "The registrable domain suffix check must happen before the bail-out");
+
+ async_test(t => {
+ frames[1].postMessage({
+ type: "set document.domain",
+ newValue: "{{host}}"
+ }, "*");
+
+ window.onmessage = t.step_func_done(e => {
+ assert_equals(e.data.type, "new document.domain");
+ assert_equals(e.data.result, "{{domains[www1]}}");
+ });
+ }, "Having an origin-keyed subdomain child try to set document.domain " +
+ "must not change the document.domain value it sees");
+
+ done();
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/document-domain.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/document-domain.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/document-domain.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/cross-origin-isolated.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/cross-origin-isolated.sub.https.html
new file mode 100644
index 0000000000..a521934cc9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/cross-origin-isolated.sub.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>window.originAgentCluster must be implied by cross-origin isolation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe src="//{{domains[www1]}}:{{location[port]}}/html/browsers/origin/origin-keyed-agent-clusters/resources/coep-frame.html"></iframe>
+
+<div id="log"></div>
+
+<script type="module">
+import { testGetter } from "../resources/helpers.mjs";
+
+setup({ explicit_done: true });
+
+window.onload = () => {
+ // Cross-origin isolated pages are always origin-keyed.
+ testGetter(self, true, "self");
+
+ // Child frames of cross-origin isolated pages must also be cross-origin
+ // isolated, and thus also origin-keyed. Make sure the implementation doesn't
+ // treat them specially in some weird way, for the purposes of this
+ // implication.
+ testGetter(0, true, "child");
+
+ done();
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/cross-origin-isolated.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/cross-origin-isolated.sub.https.html.headers
new file mode 100644
index 0000000000..5f8621ef83
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/cross-origin-isolated.sub.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/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/csp-sandbox-no.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/csp-sandbox-no.https.html
new file mode 100644
index 0000000000..e0b5f92376
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/csp-sandbox-no.https.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>window.originAgentCluster for a top-level frame sandboxed by CSP with no Origin-Agent-Cluster header</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import { testGetter } from "../resources/helpers.mjs";
+
+// Even without the header, sandboxing makes this page have an opaque origin,
+// so it is origin-keyed.
+testGetter(self, true);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/csp-sandbox-no.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/csp-sandbox-no.https.html.headers
new file mode 100644
index 0000000000..4705ce9ded
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/csp-sandbox-no.https.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: sandbox allow-scripts;
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/csp-sandbox-yes.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/csp-sandbox-yes.https.html
new file mode 100644
index 0000000000..a2220c5acc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/csp-sandbox-yes.https.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>window.originAgentCluster for a top-level frame sandboxed by CSP with an Origin-Agent-Cluster header</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import { testGetter } from "../resources/helpers.mjs";
+
+// We're definitely origin-keyed: both the CSP sandboxing and the
+// Origin-Agent-Cluster header should ensure this.
+testGetter(self, true);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/csp-sandbox-yes.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/csp-sandbox-yes.https.html.headers
new file mode 100644
index 0000000000..a52bf50900
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/csp-sandbox-yes.https.html.headers
@@ -0,0 +1,2 @@
+Content-Security-Policy: sandbox allow-scripts;
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-to-javascript-no.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-to-javascript-no.https.html
new file mode 100644
index 0000000000..06149cda8a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-to-javascript-no.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>window.originAgentCluster for a javascript: URL navigated to from a data: URL on a site-keyed page</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import runTest from "./resources/data-to-javascript-test.mjs";
+runTest();
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-to-javascript-yes.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-to-javascript-yes.https.html
new file mode 100644
index 0000000000..af6fea0ad9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-to-javascript-yes.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>window.originAgentCluster for a javascript: URL navigated to from a data: URL on an origin-keyed page</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import runTest from "./resources/data-to-javascript-test.mjs";
+runTest();
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-to-javascript-yes.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-to-javascript-yes.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-to-javascript-yes.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-url-no.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-url-no.https.html
new file mode 100644
index 0000000000..8ae564a072
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-url-no.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>window.originAgentCluster for a data: URL on a site-keyed page</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import runTest from "./resources/data-url-test.mjs";
+runTest();
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-url-yes.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-url-yes.https.html
new file mode 100644
index 0000000000..bcbf098a66
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-url-yes.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>window.originAgentCluster for a data: URL on an origin-keyed page</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import runTest from "./resources/data-url-test.mjs";
+runTest();
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-url-yes.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-url-yes.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-url-yes.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/javascript-url-no.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/javascript-url-no.https.html
new file mode 100644
index 0000000000..1b54ad42a4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/javascript-url-no.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>window.originAgentCluster for a javascript: URL on a site-keyed page</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import runTest from "./resources/data-url-test.mjs";
+runTest({ expected: false });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/javascript-url-yes.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/javascript-url-yes.https.html
new file mode 100644
index 0000000000..e2b7730dd2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/javascript-url-yes.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>window.originAgentCluster for a javascript: URL on an origin-keyed page</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import runTest from "./resources/data-url-test.mjs";
+runTest({ expected: true });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/javascript-url-yes.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/javascript-url-yes.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/javascript-url-yes.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/removed-iframe.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/removed-iframe.sub.https.html
new file mode 100644
index 0000000000..fcf5068908
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/removed-iframe.sub.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>window.originAgentCluster for a removed frame</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import { navigateIframe } from "../resources/helpers.mjs";
+
+promise_test(async () => {
+ // We cannot use insertIframe because it sets both `document.domain`s. That
+ // shouldn't matter, but Chrome has a bug (https://crbug.com/1095145), so
+ // let's avoid making the test needlessly fail because of that bug.
+ const iframe = document.createElement("iframe");
+ const navigatePromise = navigateIframe(iframe, "{{hosts[][]}}", "?1");
+ document.body.append(iframe);
+ await navigatePromise;
+
+ const frameWindow = iframe.contentWindow;
+
+ assert_equals(frameWindow.originAgentCluster, true, "before");
+ iframe.remove();
+ assert_equals(frameWindow.originAgentCluster, true, "after");
+}, "Removing the iframe does not change originAgentCluster");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/removed-iframe.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/removed-iframe.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/removed-iframe.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/data-to-javascript-test.mjs b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/data-to-javascript-test.mjs
new file mode 100644
index 0000000000..3a88253ee3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/data-to-javascript-test.mjs
@@ -0,0 +1,33 @@
+import { insertCustomIframe, testSupportScript } from "./helpers.mjs";
+import { waitForIframe, testGetter } from "../../resources/helpers.mjs";
+
+const testSupportScriptSuitableForNesting =
+ testSupportScript.replace('</script>', '</scri` + `pt>');
+
+export default () => {
+ promise_setup(async () => {
+ const jsURL = `javascript:'${testSupportScript}'`;
+ const iframe = await insertCustomIframe(`data:text/html,
+ Start page
+ <script>
+ window.onmessage = () => {
+ location.href = \`javascript:'End page${testSupportScriptSuitableForNesting}'\`;
+ };
+ </script>
+ `);
+
+ const waitPromise = waitForIframe(iframe, "javascript: URL");
+
+ // Kick off the navigation. We can't do it directly because only same-origin
+ // pages can navigate to a javascript: URL, and we're not same-origin with
+ // a data: URL.
+ iframe.contentWindow.postMessage(undefined, "*");
+
+ await waitPromise;
+ });
+
+ // The javascript: URL iframe inherits its origin from the previous occupant
+ // of the iframe, which is a data: URL, so it should always be true.
+
+ testGetter(0, true);
+};
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/data-url-test.mjs b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/data-url-test.mjs
new file mode 100644
index 0000000000..1a9b3be47f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/data-url-test.mjs
@@ -0,0 +1,13 @@
+import { insertCustomIframe, testSupportScript } from "./helpers.mjs";
+import { testGetter } from "../../resources/helpers.mjs";
+
+export default () => {
+ promise_setup(() => {
+ return insertCustomIframe(`data:text/html,${testSupportScript}`);
+ });
+
+ // The data: URL iframe has an opaque origin, so it should return true, since
+ // for them site === origin so they are always origin-keyed.
+
+ testGetter(0, true, "data: URL child");
+};
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/helpers.mjs b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/helpers.mjs
new file mode 100644
index 0000000000..4610ffcad0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/helpers.mjs
@@ -0,0 +1,28 @@
+import { waitForIframe } from "../../resources/helpers.mjs";
+
+/**
+ * Inserts an iframe, not specialized for origin-keyed agent cluster testing,
+ * pointing to a custom URL. This is just a wrapper to remove some boilerplate.
+ * @param {string} src - The src="" value for the iframe
+ */
+export async function insertCustomIframe(src) {
+ const iframe = document.createElement("iframe");
+ iframe.src = src;
+
+ const waitPromise = waitForIframe(iframe);
+ document.body.append(iframe);
+ await waitPromise;
+
+ return iframe;
+}
+
+/**
+ * This is the part of send-oac-header.py that allows us to reuse testGetter.
+ */
+export const testSupportScript = `
+ <script>
+ window.onmessage = () => {
+ parent.postMessage(self.originAgentCluster, "*");
+ };
+ </script>
+`;
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/javascript-url-test.mjs b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/javascript-url-test.mjs
new file mode 100644
index 0000000000..de474d8caf
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/javascript-url-test.mjs
@@ -0,0 +1,14 @@
+import { insertCustomIframe, testSupportScript } from "./helpers.mjs";
+import { testGetter } from "../../resources/helpers.mjs";
+
+export default ({ expected }) => {
+ promise_setup(() => {
+ return insertCustomIframe(`javascript:'${testSupportScript}'`);
+ });
+
+ // The javascript: URL iframe inherits its origin from the previous occupant
+ // of the iframe, which is about:blank, which in turn inherits from the
+ // parent. So, the caller needs to tell us what to expect.
+
+ testGetter(0, expected);
+};
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/sandboxed-iframe-test.sub.mjs b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/sandboxed-iframe-test.sub.mjs
new file mode 100644
index 0000000000..9357df00c5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/sandboxed-iframe-test.sub.mjs
@@ -0,0 +1,20 @@
+import {
+ navigateIframe,
+ testGetter
+} from "../../resources/helpers.mjs";
+
+export default () => {
+ // We do this manually instead of using insertIframe because we want to add a
+ // sandbox="" attribute and we don't want to set both document.domains.
+ promise_setup(() => {
+ const iframe = document.createElement("iframe");
+ iframe.sandbox = "allow-scripts";
+ const navigatePromise = navigateIframe(iframe, "{{hosts[][]}}", "?1");
+ document.body.append(iframe);
+ return navigatePromise;
+ });
+
+ // Sandboxed iframes have an opaque origin, so it should return true, since
+ // for them site === origin so they are always origin-keyed.
+ testGetter(0, true);
+};
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/sandboxed-same-origin-iframe-test.sub.mjs b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/sandboxed-same-origin-iframe-test.sub.mjs
new file mode 100644
index 0000000000..272f805870
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/resources/sandboxed-same-origin-iframe-test.sub.mjs
@@ -0,0 +1,20 @@
+import {
+ navigateIframe,
+ testGetter
+} from "../../resources/helpers.mjs";
+
+export default ({ expected }) => {
+ // We do this manually instead of using insertIframe because we want to add a
+ // sandbox="" attribute and we don't want to set both document.domains.
+ promise_setup(() => {
+ const iframe = document.createElement("iframe");
+ iframe.sandbox = "allow-scripts allow-same-origin";
+ const navigatePromise = navigateIframe(iframe, "{{hosts[][]}}", "?1");
+ document.body.append(iframe);
+ return navigatePromise;
+ });
+
+ // Since the allow-same-origin token is set, this should behave like a normal
+ // iframe, and follow the embedder.
+ testGetter(0, expected);
+};
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-iframe-no.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-iframe-no.https.html
new file mode 100644
index 0000000000..29758a17b8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-iframe-no.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>window.originAgentCluster for a sandboxed iframe on a site-keyed page</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import runTest from "./resources/sandboxed-iframe-test.sub.mjs";
+runTest();
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-iframe-yes.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-iframe-yes.https.html
new file mode 100644
index 0000000000..5eb5d08d10
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-iframe-yes.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>window.originAgentCluster for a sandboxed iframe on an origin-keyed page</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import runTest from "./resources/sandboxed-iframe-test.sub.mjs";
+runTest();
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-iframe-yes.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-iframe-yes.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-iframe-yes.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-same-origin-iframe-no.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-same-origin-iframe-no.https.html
new file mode 100644
index 0000000000..3ed4096f39
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-same-origin-iframe-no.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>window.originAgentCluster for a sandboxed, but same-origin, iframe on a site-keyed page</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import runTest from "./resources/sandboxed-same-origin-iframe-test.sub.mjs";
+runTest({ expected: false });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-same-origin-iframe-yes.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-same-origin-iframe-yes.https.html
new file mode 100644
index 0000000000..c7ea5f0693
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-same-origin-iframe-yes.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>window.originAgentCluster for a sandboxed, but same-origin, iframe on an origin-keyed page</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import runTest from "./resources/sandboxed-same-origin-iframe-test.sub.mjs";
+runTest({ expected: true });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-same-origin-iframe-yes.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-same-origin-iframe-yes.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-same-origin-iframe-yes.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/going-back.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/going-back.sub.https.html
new file mode 100644
index 0000000000..a593619ea6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/going-back.sub.https.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is origin-keyed, child1 is site-keyed, child1 navigates to a different site, child2 gets inserted and is origin-keyed, child1 navigates back</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ navigateIframe,
+ waitForIframe,
+ setBothDocumentDomains,
+ testDifferentAgentClusters,
+ testSameAgentCluster,
+} from "./resources/helpers.mjs";
+
+let frame1;
+promise_setup(async () => {
+ frame1 = await insertIframe("{{hosts[][www]}}");
+});
+
+// Since they're different-origin, the parent's origin-keying request is
+// respected, as is the child's non-request. So the parent ends up in the
+// origin-keyed agent cluster and the child ends up in the site-keyed one.
+testDifferentAgentClusters([self, 0], "Before navigation: parent to child1");
+
+// Navigate the iframe to a different site. These, of course, must not be in the
+// same agent cluster.
+
+promise_test(async () => {
+ await navigateIframe(frame1, "{{hosts[alt][]}}");
+}, "Navigation");
+
+// Now insert a second iframe, pointing to the same place as the first one
+// originally did, but this time with origin-keying requested. Because of the
+// historical map of agent cluster keys for the browsing context group, the new
+// iframe should still end up in the site-keyed agent cluster.
+
+promise_test(async () => {
+ await insertIframe("{{hosts[][www]}}", "?1");
+}, "Inserting a second iframe");
+
+testDifferentAgentClusters([self, 1], "After navigation: parent to child2");
+
+// Now navigate the first iframe back. The resulting Document should be put in
+// the site-keyed agent cluster, together with the second iframe's Document.
+
+promise_test(async () => {
+ const waitPromise = waitForIframe(frame1);
+ history.back();
+ await waitPromise;
+
+ await setBothDocumentDomains(frames[0]);
+}, "Going back in history (navigating back the first iframe)");
+
+testDifferentAgentClusters([self, 0], "After back: parent to child1");
+testDifferentAgentClusters([self, 1], "After back: parent to child2");
+testSameAgentCluster([0, 1], "child1 to child2");
+testSameAgentCluster([1, 0], "child2 to child1");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/going-back.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/going-back.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/going-back.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-same-2-yes-port.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-same-2-yes-port.sub.https.html
new file mode 100644
index 0000000000..8237f2f23f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-same-2-yes-port.sub.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is site-keyed, navigate a frame from same-origin site-keyed to different-origin (different-port) origin-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ navigateIframe,
+ setBothDocumentDomains,
+ testSameAgentCluster,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+let frame1;
+promise_setup(async () => {
+ frame1 = await insertIframe("{{hosts[][]}}");
+});
+
+// Nobody requested origin-keying yet.
+
+testSameAgentCluster([self, 0], "Before: parent to child");
+testGetter(self, false, "before parent");
+testGetter(0, false, "before child");
+
+promise_test(async () => {
+ await navigateIframe(frame1, "{{hosts[][]}}:{{ports[https][1]}}", "?1");
+ await setBothDocumentDomains(frames[0]);
+}, "Navigation");
+
+// Since the new page is different-origin, its origin-keying request should be
+// respected.
+
+testDifferentAgentClusters([self, 0], "After: parent to child");
+testGetter(self, false, "after parent");
+testGetter(0, true, "after child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-same-2-yes-subdomain.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-same-2-yes-subdomain.sub.https.html
new file mode 100644
index 0000000000..00d8c3164a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-same-2-yes-subdomain.sub.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is site-keyed, navigate a frame from same-origin site-keyed to different-origin (subdomain) origin-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ navigateIframe,
+ setBothDocumentDomains,
+ testSameAgentCluster,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+let frame1;
+promise_setup(async () => {
+ frame1 = await insertIframe("{{hosts[][]}}");
+});
+
+// Nobody requested origin-keying yet.
+
+testSameAgentCluster([self, 0], "Before: parent to child");
+testGetter(self, false, "before parent");
+testGetter(0, false, "before child");
+
+promise_test(async () => {
+ await navigateIframe(frame1, "{{hosts[][www]}}", "?1");
+ await setBothDocumentDomains(frames[0]);
+}, "Navigation");
+
+// Since the new page is different-origin, its origin-keying request should be
+// respected.
+
+testDifferentAgentClusters([self, 0], "After: parent to child");
+testGetter(self, false, "after parent");
+testGetter(0, true, "after child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-subdomain-2-yes-subdomain.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-subdomain-2-yes-subdomain.sub.https.html
new file mode 100644
index 0000000000..803e684e1c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-subdomain-2-yes-subdomain.sub.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is site-keyed, navigate a frame from a subdomain site-keyed to the same subdomain origin-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ navigateIframe,
+ setBothDocumentDomains,
+ testSameAgentCluster,
+ testGetter
+} from "../resources/helpers.mjs";
+
+let frame1;
+promise_setup(async () => {
+ frame1 = await insertIframe("{{hosts[][www]}}");
+});
+
+// Nobody requested origin-keying yet.
+
+testSameAgentCluster([self, 0], "Before: parent to child");
+testGetter(self, false, "before parent");
+testGetter(0, false, "before child");
+
+promise_test(async () => {
+ await navigateIframe(frame1, "{{hosts[][www]}}", "?1");
+ await setBothDocumentDomains(frames[0]);
+}, "Navigation");
+
+// Because this subdomain was previously site-keyed, the second load's
+// origin-keying request is ignored; instead we continue with site-keying.
+
+testSameAgentCluster([self, 0], "After: parent to child");
+testGetter(self, false, "after parent");
+testGetter(0, false, "after child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-subdomain-2-yes-subdomain2.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-subdomain-2-yes-subdomain2.sub.https.html
new file mode 100644
index 0000000000..b96d10afd1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-subdomain-2-yes-subdomain2.sub.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is site-keyed, navigate a frame from a subdomain site-keyed to a second-subdomain origin-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ navigateIframe,
+ setBothDocumentDomains,
+ testSameAgentCluster,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+let frame1;
+promise_setup(async () => {
+ frame1 = await insertIframe("{{hosts[][www]}}");
+});
+
+// Nobody requested origin-keying yet.
+
+testSameAgentCluster([self, 0], "Before: parent to child");
+testGetter(self, false, "before parent");
+testGetter(0, false, "before child");
+
+promise_test(async () => {
+ await navigateIframe(frame1, "{{hosts[][www1]}}", "?1");
+ await setBothDocumentDomains(frames[0]);
+}, "Navigation");
+
+// Because we're going to a different subdomain (and thus different origin), the
+// origin-keying request is respected.
+
+testDifferentAgentClusters([self, 0], "After: parent to child");
+testGetter(self, false, "after parent");
+testGetter(0, true, "after child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-subdomain-yes-2-subdomain2-no.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-subdomain-yes-2-subdomain2-no.sub.https.html
new file mode 100644
index 0000000000..a70ed56670
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-subdomain-yes-2-subdomain2-no.sub.https.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is site-keyed, navigate a frame from a subdomain origin-keyed to a second-subdomain site-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ navigateIframe,
+ setBothDocumentDomains,
+ testSameAgentCluster,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+let frame1;
+promise_setup(async () => {
+ frame1 = await insertIframe("{{hosts[][www]}}", "?1");
+});
+
+// Since they are different-origin, the child's origin-keying request is
+// respected.
+
+testDifferentAgentClusters([self, 0], "Before: parent to child");
+testGetter(self, false, "before parent");
+testGetter(0, true, "before child");
+
+promise_test(async () => {
+ await navigateIframe(frame1, "{{hosts[][www1]}}");
+ await setBothDocumentDomains(frames[0]);
+}, "Navigation");
+
+// Make sure that the different-subdomain page (which doesn't request
+// origin-keying) doesn't somehow get origin-keyed just because its predecessor
+// was.
+
+testSameAgentCluster([self, 0], "After: parent to child");
+testGetter(self, false, "after parent");
+testGetter(0, false, "after child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-yes-subdomain-2-no-subdomain.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-yes-subdomain-2-no-subdomain.sub.https.html
new file mode 100644
index 0000000000..38e2630128
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-yes-subdomain-2-no-subdomain.sub.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is site-keyed, navigate a frame from a subdomain origin-keyed to the same subdomain site-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ navigateIframe,
+ setBothDocumentDomains,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+let frame1;
+promise_setup(async () => {
+ frame1 = await insertIframe("{{hosts[][www]}}", "?1");
+});
+
+// Since they are different-origin, the child's origin-keying request is
+// respected.
+
+testDifferentAgentClusters([self, 0], "Before: parent to child");
+testGetter(self, false, "before parent");
+testGetter(0, true, "before child");
+
+promise_test(async () => {
+ await navigateIframe(frame1, "{{hosts[][www]}}");
+ await setBothDocumentDomains(frames[0]);
+}, "Navigation");
+
+// Because this subdomain was previously origin-keyed, the second load's
+// non-request is ignored; instead we continue origin-keying.
+
+testDifferentAgentClusters([self, 0], "After: parent to child");
+testGetter(self, false, "after parent");
+testGetter(0, true, "after child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-yes-1-no-same-2-no-port.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-yes-1-no-same-2-no-port.sub.https.html
new file mode 100644
index 0000000000..6211845be1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-yes-1-no-same-2-no-port.sub.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is origin-keyed, navigate a frame from same-origin site-keyed to different-origin (different-port) origin-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ navigateIframe,
+ setBothDocumentDomains,
+ testSameAgentCluster,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+let frame1;
+promise_setup(async () => {
+ frame1 = await insertIframe("{{hosts[][]}}");
+});
+
+// Since the parent is origin-keyed, the same-origin child's non-request is
+// ignored, so it gets origin-keyed too.
+
+testSameAgentCluster([self, 0], "Before: parent to child");
+testGetter(self, true, "before parent");
+testGetter(0, true, "before child");
+
+promise_test(async () => {
+ await navigateIframe(frame1, "{{hosts[][]}}:{{ports[https][1]}}");
+ await setBothDocumentDomains(frames[0]);
+}, "Navigation");
+
+// Since the new page is different-origin, its non-request should be respected.
+
+testDifferentAgentClusters([self, 0], "After: parent to child");
+testGetter(self, true, "after parent");
+testGetter(0, false, "after child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-yes-1-no-same-2-no-port.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-yes-1-no-same-2-no-port.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-yes-1-no-same-2-no-port.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-yes-1-no-same-2-no-subdomain.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-yes-1-no-same-2-no-subdomain.sub.https.html
new file mode 100644
index 0000000000..ead56754a7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-yes-1-no-same-2-no-subdomain.sub.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent is origin-keyed, navigate a frame from same-origin site-keyed to different-origin (subdomain) origin-keyed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ navigateIframe,
+ setBothDocumentDomains,
+ testSameAgentCluster,
+ testDifferentAgentClusters,
+ testGetter
+} from "../resources/helpers.mjs";
+
+let frame1;
+promise_setup(async () => {
+ frame1 = await insertIframe("{{hosts[][]}}");
+});
+
+// Since the parent is origin-keyed, the same-origin child's non-request is
+// ignored, so it gets origin-keyed too.
+
+testSameAgentCluster([self, 0], "Before: parent to child");
+testGetter(self, true, "before parent");
+testGetter(0, true, "before child");
+
+promise_test(async () => {
+ await navigateIframe(frame1, "{{hosts[][www]}}");
+ await setBothDocumentDomains(frames[0]);
+}, "Navigation");
+
+// Since the new page is different-origin, its non-request should be respected.
+
+testDifferentAgentClusters([self, 0], "After: parent to child");
+testGetter(self, true, "after parent");
+testGetter(0, false, "after child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-yes-1-no-same-2-no-subdomain.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-yes-1-no-same-2-no-subdomain.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-yes-1-no-same-2-no-subdomain.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/insecure-http.sub.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/insecure-http.sub.html
new file mode 100644
index 0000000000..6f9e5d8b73
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/insecure-http.sub.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent requests origin-keying, child requests origin-keying, child is a subdomain of the parent, but all over insecure HTTP</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testSameAgentCluster,
+ testGetter
+} from "./resources/helpers.mjs";
+
+promise_setup(async () => {
+ await insertIframe("{{hosts[][www]}}", "?1");
+});
+
+// All origin-keying requests are ignored, since this is over insecure HTTP.
+// So both end up in the site-keyed agent cluster.
+testSameAgentCluster([self, 0]);
+
+testGetter(self, false, "parent");
+testGetter(0, false, "child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/insecure-http.sub.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/insecure-http.sub.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/insecure-http.sub.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups-crash.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups-crash.https.html
new file mode 100644
index 0000000000..dcfb5eb277
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups-crash.https.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Crash test for https://crbug.com/1099718</title>
+
+<div id="log"></div>
+
+<script>
+window.open("resources/crashy-popup.sub.html", "windowName1", "noopener");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-no-openee-yes-port.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-no-openee-yes-port.sub.https.html
new file mode 100644
index 0000000000..a0bf569b12
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-no-openee-yes-port.sub.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Opener is site-keyed, openee is origin-keyed, openee is different-origin to the opener because of a port mismatch</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ openWindow,
+ testOpenedWindowIsInADifferentAgentCluster,
+ testGetter
+} from "../resources/helpers.mjs";
+
+let openee;
+promise_setup(async () => {
+ openee = await openWindow("{{hosts[][]}}:{{ports[https][1]}}", "?1");
+});
+
+// Since they're different-origin, the openee's origin-keying request is
+// respected, so the opener ends up in the site-keyed agent cluster and the
+// openee in the origin-keyed one.
+testOpenedWindowIsInADifferentAgentCluster(() => openee);
+
+testGetter(self, false, "opener");
+testGetter(() => openee, true, "openee");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-no-openee-yes-same.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-no-openee-yes-same.sub.https.html
new file mode 100644
index 0000000000..196dff1449
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-no-openee-yes-same.sub.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Opener is site-keyed, openee is origin-keyed, openee is same-origin to the opener</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ openWindow,
+ testOpenedWindowIsInSameAgentCluster,
+ testGetter
+} from "../resources/helpers.mjs";
+
+let openee;
+promise_setup(async () => {
+ openee = await openWindow("{{hosts[][]}}", "?1");
+});
+
+// Since they're same-origin, and the opener loaded with site-keying, the
+// child's request for origin-keying gets ignored, and both end up site-keyed.
+testOpenedWindowIsInSameAgentCluster(() => openee);
+
+testGetter(self, false, "opener");
+testGetter(() => openee, false, "openee");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-no-openee-yes-subdomain.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-no-openee-yes-subdomain.sub.https.html
new file mode 100644
index 0000000000..f96d2273d5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-no-openee-yes-subdomain.sub.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Opener is site-keyed, openee is origin-keyed, openee is a subdomain of the opener</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ openWindow,
+ testOpenedWindowIsInADifferentAgentCluster,
+ testGetter
+} from "../resources/helpers.mjs";
+
+let openee;
+promise_setup(async () => {
+ openee = await openWindow("{{hosts[][www]}}", "?1");
+});
+
+// Since they're different-origin, the openee's origin-keying request is
+// respected, so the opener ends up in the site-keyed agent cluster and the
+// openee in the origin-keyed one.
+testOpenedWindowIsInADifferentAgentCluster(() => openee);
+
+testGetter(self, false, "opener");
+testGetter(() => openee, true, "openee");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-port.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-port.sub.https.html
new file mode 100644
index 0000000000..51c5a208c5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-port.sub.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Opener is origin-keyed, openee is site-keyed, openee is different-origin to the opener because of a port mismatch</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ openWindow,
+ testOpenedWindowIsInADifferentAgentCluster,
+ testGetter
+} from "../resources/helpers.mjs";
+
+let openee;
+promise_setup(async () => {
+ openee = await openWindow("{{hosts[][]}}:{{ports[https][1]}}");
+});
+
+// Since they're different-origin, the openee's non-request is respected, so the
+// opener ends up in the origin-keyed agent cluster and the openee in the
+// site-keyed one.
+testOpenedWindowIsInADifferentAgentCluster(() => openee);
+
+testGetter(self, true, "opener");
+testGetter(() => openee, false, "openee");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-port.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-port.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-port.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-same.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-same.sub.https.html
new file mode 100644
index 0000000000..562ab40c68
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-same.sub.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Opener is origin-keyed, openee is site-keyed, openee is same-origin to the opener</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ openWindow,
+ testOpenedWindowIsInSameAgentCluster,
+ testGetter
+} from "../resources/helpers.mjs";
+
+let openee;
+promise_setup(async () => {
+ openee = await openWindow("{{hosts[][]}}");
+});
+
+// Since they're same-origin, the openee's non-request is ignored, so both end
+// up in the origin-keyed agent cluster.
+testOpenedWindowIsInSameAgentCluster(() => openee);
+
+testGetter(self, true, "opener");
+testGetter(() => openee, true, "openee");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-same.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-same.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-same.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-subdomain.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-subdomain.sub.https.html
new file mode 100644
index 0000000000..d7d4791459
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-subdomain.sub.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Opener is origin-keyed, openee is site-keyed, openee is a subdomain of the opener</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ openWindow,
+ testOpenedWindowIsInADifferentAgentCluster,
+ testGetter
+} from "../resources/helpers.mjs";
+
+let openee;
+promise_setup(async () => {
+ openee = await openWindow("{{hosts[][www]}}");
+});
+
+// Since they're different-origin, the openee's non-request is respected, so the
+// opener ends up in the origin-keyed agent cluster and the openee in the
+// site-keyed one.
+testOpenedWindowIsInADifferentAgentCluster(() => openee);
+
+testGetter(self, true, "opener");
+testGetter(() => openee, false, "openee");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-subdomain.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-subdomain.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-subdomain.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-port.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-port.sub.https.html
new file mode 100644
index 0000000000..32a5066d2e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-port.sub.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Opener is origin-keyed, openee is origin-keyed, openee is different-origin to the opener because of a port mismatch</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ openWindow,
+ testOpenedWindowIsInADifferentAgentCluster,
+ testGetter
+} from "../resources/helpers.mjs";
+
+let openee;
+promise_setup(async () => {
+ openee = await openWindow("{{hosts[][]}}:{{ports[https][1]}}", "?1");
+});
+
+// Both request origin-keying, so the opener ends up in one origin-keyed agent
+// cluster (the default port's origin), and the openee ends up in a different
+// origin-keyed agent cluster (the other port's origin).
+testOpenedWindowIsInADifferentAgentCluster(() => openee);
+
+testGetter(self, true, "opener");
+testGetter(() => openee, true, "openee");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-port.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-port.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-port.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-same.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-same.sub.https.html
new file mode 100644
index 0000000000..a85decac3c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-same.sub.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Opener is origin-keyed, openee is origin-keyed, openee is same-origin to the opener</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ openWindow,
+ testOpenedWindowIsInSameAgentCluster,
+ testGetter
+} from "../resources/helpers.mjs";
+
+let openee;
+promise_setup(async () => {
+ openee = await openWindow("{{hosts[][]}}", "?1");
+});
+
+// Both request origin-keying, and they're same-origin, so they both end up in
+// the same origin-keyed agent cluster.
+testOpenedWindowIsInSameAgentCluster(() => openee);
+
+testGetter(self, true, "opener");
+testGetter(() => openee, true, "openee");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-same.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-same.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-same.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-subdomain.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-subdomain.sub.https.html
new file mode 100644
index 0000000000..148b39af23
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-subdomain.sub.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Opener is origin-keyed, openee is origin-keyed, openee is a subdomain of the opener</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ openWindow,
+ testOpenedWindowIsInADifferentAgentCluster,
+ testGetter
+} from "../resources/helpers.mjs";
+
+let openee;
+promise_setup(async () => {
+ openee = await openWindow("{{hosts[][www]}}", "?1");
+});
+
+// Both request origin-keyed, so the opener ends up in one origin-keyed agent
+// cluster (the base domain's origin), and the openee ends up in a different
+// origin-keyed agent cluster (the www subdomain's origin).
+testOpenedWindowIsInADifferentAgentCluster(() => openee);
+
+testGetter(self, true, "opener");
+testGetter(() => openee, true, "openee");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-subdomain.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-subdomain.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-subdomain.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/regression-1399759.https.sub.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/regression-1399759.https.sub.html
new file mode 100644
index 0000000000..d0b09f335d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/regression-1399759.https.sub.html
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta name="variant" content="?pipe=header(Origin-Agent-Cluster,%3F0)">
+<meta name="variant" content="?pipe=header(Origin-Agent-Cluster,%3F1)">
+<title>Origin-Isolation after navigating about:blank.</title>
+<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>
+</head>
+<body>
+</body>
+<script>
+// Regression test for crbug.com/1399759. This is mainly based on
+// html/infrastructure/urls/terminology-0/document-base-url-initiated-grand-parent.https.window.html,
+// but restricts itself to the exact error condition.
+//
+// This test is run in two variants which differ in the Origin-Agent-Cluster
+// http header values, ?0 and ?1. The test should pass in either case, but the
+// regression we're testing for involves inconsistent clustering decisions,
+// which requires clustering to be enabled in the first place.
+promise_test(async test => {
+ // Create a cross-origin iframe. Use the executor.html, so we can ask it
+ // to execute scripts for us.
+ const child_token = token();
+ const iframe = document.createElement("iframe");
+ iframe.src = get_host_info().HTTPS_REMOTE_ORIGIN +
+ `/common/dispatcher/executor.html?uuid=${child_token}`;
+ document.body.appendChild(iframe);
+
+ // The child creates a grand child in an iframe.
+ const reply_token = token();
+ send(child_token, `
+ const iframe = document.createElement("iframe");
+ iframe.src = "/common/blank.html";
+ iframe.onload = () => {
+ send("${reply_token}", "grand child loaded");
+ };
+ document.body.appendChild(iframe);
+ `);
+ assert_equals(await receive(reply_token), "grand child loaded");
+ const grandchild = iframe.contentWindow[0];
+
+ // Navigate the grand-child toward about:blank.
+ grandchild.location = "about:blank";
+ assert_equals(await receive(reply_token), "grand child loaded");
+
+ // This document and grandchild are same-origin, because about:blank
+ // inherits its origin from the initiator of the navigation, which is us.
+ // This access should not throw.
+ grandchild.document;
+}, "Check the baseURL of an about:blank document cross-origin with its parent");
+
+promise_test(async test => {
+ // This tests the same setup as above, but with about:srcdoc. Since one
+ // cannot just navigate to about:srcdoc, we'll have to include an extra
+ // step: Create an iframe with srcdoc attribute; navigate away; then
+ // navigate to about:srcdoc.
+ // srcdoc does not inherit the origin from the initiator - unlike
+ // about:blank - and so in this case the grandchild.document access should
+ // throw.
+
+ // Create a cross-origin iframe. Use the executor.html, so we can ask it
+ // to execute scripts for us.
+ const child_token = token();
+ const iframe = document.createElement("iframe");
+ iframe.src = get_host_info().HTTPS_REMOTE_ORIGIN +
+ `/common/dispatcher/executor.html?uuid=${child_token}`;
+ document.body.appendChild(iframe);
+
+ // The child creates a grand child in an iframe, using the srcdoc attribute.
+ const reply_token = token();
+ send(child_token, `
+ const iframe = document.createElement("iframe");
+ iframe.onload = () => {
+ send("${reply_token}", "grand child loaded");
+ };
+ iframe.srcdoc = "nothing interesting";
+ document.body.appendChild(iframe);
+ `);
+ assert_equals(await receive(reply_token), "grand child loaded");
+ const grandchild = iframe.contentWindow[0];
+
+ // Navigate the grand child toward a regular URL.
+ grandchild.location = get_host_info().HTTPS_REMOTE_ORIGIN + "/common/blank.html";
+ assert_equals(await receive(reply_token), "grand child loaded");
+
+ // Navigate the grand-child back, to about:srcdoc.
+ grandchild.location = "about:srcdoc";
+ assert_equals(await receive(reply_token), "grand child loaded");
+
+ // This document and grandchild are cross-origin. about:srcdoc does not
+ // inherits its origin from the initiator of the navigation. This access
+ // should throw:
+ assert_throws_dom("SecurityError", () => { grandchild.document; });
+}, "Check that about:srcdoc navigation does not follow about:blank rules.");
+</script>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/removing-iframes.sub.https.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/removing-iframes.sub.https.html
new file mode 100644
index 0000000000..b83aa9f5be
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/removing-iframes.sub.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>A site-keyed child at a given origin causes future children to also be site-keyed even after the iframe is removed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script type="module">
+import {
+ insertIframe,
+ testSameAgentCluster,
+ testDifferentAgentClusters,
+ testGetter
+} from "./resources/helpers.mjs";
+
+let frame1;
+promise_setup(async () => {
+ frame1 = await insertIframe("{{hosts[][www]}}");
+});
+
+// Since they're different-origin, the parent's origin-keying request is
+// respected, as is the child's non-request. So the parent ends up in the
+// origin-keyed agent cluster and the child ends up in the site-keyed one.
+testDifferentAgentClusters([self, 0], "Before");
+testGetter(self, true, "parent");
+testGetter(0, false, "child1");
+
+promise_test(async () => {
+ frame1.remove();
+
+ await insertIframe("{{hosts[][www]}}", "?1");
+ await insertIframe("{{hosts[][www1]}}");
+}, "Remove the iframe and insert new ones");
+
+// Because of the historical presence of a site-keyed {{hosts[][www]}} iframe,
+// the origin-keying request for child 2 will be ignored. So,
+// child 2 and child 3 both end up in the site-keyed agent cluster.
+testDifferentAgentClusters([self, 0], "Parent to child2");
+testDifferentAgentClusters([self, 1], "Parent to child3");
+testSameAgentCluster([0, 1], "child2 to child3");
+testSameAgentCluster([1, 0], "child3 to child2");
+
+testGetter(0, false, "child2");
+testGetter(1, false, "child3");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/removing-iframes.sub.https.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/removing-iframes.sub.https.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/removing-iframes.sub.https.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/README.md b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/README.md
new file mode 100644
index 0000000000..9292fe3894
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/README.md
@@ -0,0 +1,6 @@
+Why are there `.headers` files here for the `.mjs` scripts?
+
+Because `../getter-special-cases/sandboxed-iframe.sub.https.html` is testing an
+opaque origin, which is cross-origin with these scripts. Since
+`<script type="module">` respects the same-origin policy, we need CORS headers
+to allow them to be accessed.
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/coep-frame.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/coep-frame.html
new file mode 100644
index 0000000000..7cbd89b943
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/coep-frame.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>A page with COEP set that will respond when asked</title>
+
+<script type="module" src="send-header-page-script.mjs"></script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/coep-frame.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/coep-frame.html.headers
new file mode 100644
index 0000000000..4e798cd9f5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/coep-frame.html.headers
@@ -0,0 +1,2 @@
+Cross-Origin-Embedder-Policy: require-corp
+Cross-Origin-Resource-Policy: cross-origin
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/crashy-popup.sub.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/crashy-popup.sub.html
new file mode 100644
index 0000000000..45c8d5074d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/crashy-popup.sub.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>This page helps exhibit a crash bug when window.open()ed (see ../popups-crash.https.html)</title>
+
+<iframe src="https://{{hosts[][www]}}:{{ports[https][0]}}/html/browsers/origin/origin-keyed-agent-clusters/resources/send-oac-header.py"></iframe>
+<iframe src="https://{{hosts[][www]}}:{{ports[https][0]}}/html/browsers/origin/origin-keyed-agent-clusters/resources/send-oac-header.py?header=?1"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/crashy-popup.sub.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/crashy-popup.sub.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/crashy-popup.sub.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/frame.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/frame.html
new file mode 100644
index 0000000000..537de07b14
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/frame.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>A frame included by a test page</title>
+
+<script>
+window.onmessage = e => {
+ if (e.data.type === "set document.domain") {
+ document.domain = e.data.newValue;
+ e.source.postMessage({ type: "new document.domain", result: document.domain }, "*");
+ }
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/frame.html.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/frame.html.headers
new file mode 100644
index 0000000000..79a20f30fc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/frame.html.headers
@@ -0,0 +1 @@
+Origin-Agent-Cluster: ?1
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/helpers.mjs b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/helpers.mjs
new file mode 100644
index 0000000000..6bad76e3d9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/helpers.mjs
@@ -0,0 +1,390 @@
+/**
+ * Inserts an iframe usable for origin-keyed agent cluster testing, and returns
+ * a promise fulfilled when the iframe is loaded and its document.domain is set.
+ * The iframe will point to the send-oac-header.py file, on the designated
+ * host.
+ * @param {string} host - The host used to calculate the iframe's src=""
+ * @param {string=} header - The value of the Origin-Agent-Cluster header that
+ * the iframe will set. Omit this to set no header.
+ * @param {object=} options - Rarely-used options.
+ * @param {boolean=} options.redirectFirst - Whether to do a 302 redirect first
+ * before arriving at the page that sets the header. The redirecting page will
+ * not set the Origin-Agent-Cluster header.
+ * @returns {HTMLIFrameElement} The created iframe element
+ */
+export async function insertIframe(host, header, { redirectFirst = false } = {}) {
+ const iframe = document.createElement("iframe");
+ const navigatePromise = navigateIframe(iframe, host, header, { redirectFirst });
+ document.body.append(iframe);
+ await navigatePromise;
+ await setBothDocumentDomains(iframe.contentWindow);
+ return iframe;
+}
+
+/**
+ * Navigates an iframe to a page for origin-keyed agent cluster testing, similar
+ * to insertIframe but operating on an existing iframe.
+ * @param {HTMLIFrameElement} iframeEl - The <iframe> element to navigate
+ * @param {string} host - The host to calculate the iframe's new src=""
+ * @param {string=} header - The value of the Origin-Agent-Cluster header that
+ * the newly-navigated-to page will set. Omit this to set no header.
+ * @param {object=} options - Rarely-used options.
+ * @param {boolean=} options.redirectFirst - Whether to do a 302 redirect first
+ * before arriving at the page that sets the header. The redirecting page will
+ * not set the Origin-Agent-Cluster header.
+ * @returns {Promise} a promise fulfilled when the load event fires, or rejected
+ * if the error event fires
+ */
+export function navigateIframe(iframeEl, host, header, { redirectFirst = false } = {}) {
+ const url = getSendHeaderURL(host, header, { redirectFirst });
+
+ const waitPromise = waitForIframe(iframeEl, url);
+ iframeEl.src = url;
+ return waitPromise;
+}
+
+/**
+ * Returns a promise that is fulfilled when an iframe's load event fires, or
+ * rejected when its error event fires.
+ * @param {HTMLIFrameElement} iframeEl - The <iframe> element to wait on
+ * @param {string} destinationForErrorMessage - A string used in the promise
+ * rejection error message, if the error event fires
+ * @returns {Promise} a promise fulfilled when the load event fires, or rejected
+ * if the error event fires
+ */
+export function waitForIframe(iframeEl, destinationForErrorMessage) {
+ return new Promise((resolve, reject) => {
+ iframeEl.addEventListener("load", () => resolve());
+ iframeEl.addEventListener(
+ "error",
+ () => reject(new Error(`Could not navigate to ${destinationForErrorMessage}`))
+ );
+ });
+}
+
+/**
+ * Opens a new window usable for origin-keyed agent cluster testing, and returns
+ * a promise fulfilled when the window is loaded and its document.domain is set.
+ * The window will point to the send-oac-header.py file, on the designated host.
+ *
+ * The opened window will be automatically closed when all the tests complete.
+ * @param {string} host - The host used to calculate the window's URL
+ * @param {string=} header - The value of the Origin-Agent-Cluster header that
+ * the opened window's page will set. Omit this to set no header.
+ * @returns {WindowProxy} The created window
+ */
+export async function openWindow(host, header) {
+ const url = getSendHeaderURL(host, header, { sendLoadedMessage: true });
+ const openedWindow = window.open(url);
+
+ add_completion_callback(() => openedWindow.close());
+
+ const whatHappened = await waitForMessage(openedWindow);
+ assert_equals(whatHappened, "loaded");
+
+ await setBothDocumentDomains(openedWindow);
+
+ return openedWindow;
+}
+
+/**
+ * Expands into a pair of promise_test() calls to ensure that two Windows are in
+ * the same agent cluster, by checking both that we can send a
+ * WebAssembly.Module, and that we can synchronously access the DOM.
+ * @param {Array} testFrames - An array of either the form [self, frameIndex] or
+ * [frameIndex1, frameIndex2], indicating the two Windows under test. E.g.
+ * [self, 0] or [0, 1].
+ * @param {string=} testLabelPrefix - A prefix used in the test names. This can
+ * be omitted if testSameAgentCluster is only used once in a test file.
+ */
+export function testSameAgentCluster(testFrames, testLabelPrefix) {
+ const prefix = testLabelPrefix === undefined ? "" : `${testLabelPrefix}: `;
+
+ if (testFrames[0] === self) {
+ // Between parent and a child at the index given by testFrames[1]
+
+ promise_test(async () => {
+ const frameWindow = frames[testFrames[1]];
+ const frameElement = document.querySelectorAll("iframe")[testFrames[1]];
+
+ // Must not throw
+ frameWindow.document;
+
+ // Must not throw
+ frameWindow.location.href;
+
+ assert_not_equals(frameElement.contentDocument, null, "contentDocument");
+
+ const whatHappened = await accessFrameElement(frameWindow);
+ assert_equals(whatHappened, "frameElement accessed successfully");
+ }, `${prefix}setting document.domain must give sync access`);
+ } else {
+ // Between the two children at the index given by testFrames[0] and
+ // testFrames[1]
+
+ promise_test(async () => {
+ const whatHappened1 = await accessDocumentBetween(testFrames);
+ assert_equals(whatHappened1, "accessed document successfully");
+
+ const whatHappened2 = await accessLocationHrefBetween(testFrames);
+ assert_equals(whatHappened2, "accessed location.href successfully");
+
+ // We don't test contentDocument/frameElement for these because accessing
+ // those via siblings has to go through the parent anyway.
+ }, `${prefix}setting document.domain must give sync access`);
+ }
+}
+
+/**
+ * Expands into a pair of promise_test() calls to ensure that two Windows are in
+ * different agent clusters, by checking both that we cannot send a
+ * WebAssembly.Module, and that we cannot synchronously access the DOM.
+ * @param {Array} testFrames - An array of either the form [self, frameIndex] or
+ * [frameIndex1, frameIndex2], indicating the two Windows under test. E.g.
+ * [self, 0] or [0, 1].
+ * @param {string=} testLabelPrefix - A prefix used in the test names. This can
+ * be omitted if testDifferentAgentClusters is only used once in a test file.
+ */
+export function testDifferentAgentClusters(testFrames, testLabelPrefix) {
+ const prefix = testLabelPrefix === undefined ? "" : `${testLabelPrefix}: `;
+
+ if (testFrames[0] === self) {
+ // Between parent and a child at the index given by testFrames[1]
+
+ promise_test(async () => {
+ // In general, cross-origin sharing of WebAssembly.Module is prohibited,
+ // so if we're in different agent clusters, it's definitely prohibited.
+ // Basic tests for this cross-origin prohibition are elsewhere; we include
+ // these here as an extra check to make sure there's no weird interactions
+ // with Origin-Agent-Cluster.
+ const frameWindow = frames[testFrames[1]];
+ const whatHappened = await sendWasmModule(frameWindow);
+
+ assert_equals(whatHappened, "messageerror");
+ }, `${prefix}messageerror event must occur`);
+
+ promise_test(async () => {
+ const frameWindow = frames[testFrames[1]];
+ const frameElement = document.querySelectorAll("iframe")[testFrames[1]];
+
+ assert_throws_dom("SecurityError", DOMException, () => {
+ frameWindow.document;
+ });
+
+ assert_throws_dom("SecurityError", DOMException, () => {
+ frameWindow.location.href;
+ });
+
+ assert_equals(frameElement.contentDocument, null, "contentDocument");
+
+ const whatHappened = await accessFrameElement(frameWindow);
+ assert_equals(whatHappened, "null");
+ }, `${prefix}setting document.domain must not give sync access`);
+ } else {
+ // Between the two children at the index given by testFrames[0] and
+ // testFrames[1]
+
+ promise_test(async () => {
+ const whatHappened = await sendWasmModuleBetween(testFrames);
+ assert_equals(whatHappened, "messageerror");
+ }, `${prefix}messageerror event must occur`);
+
+ promise_test(async () => {
+ const whatHappened1 = await accessDocumentBetween(testFrames);
+ assert_equals(whatHappened1, "SecurityError");
+
+ const whatHappened2 = await accessLocationHrefBetween(testFrames);
+ assert_equals(whatHappened2, "SecurityError");
+
+ // We don't test contentDocument/frameElement for these because accessing
+ // those via siblings has to go through the parent anyway.
+ }, `${prefix}setting document.domain must not give sync access`);
+ }
+}
+
+/**
+ * Expands into a pair of promise_test() calls to ensure that the given window,
+ * opened by window.open(), is in a different agent cluster from the current
+ * (opener) window.
+ * @param {function} openedWindowGetter - A function that returns the opened
+ * window
+ */
+export function testOpenedWindowIsInADifferentAgentCluster(openedWindowGetter) {
+ promise_test(async () => {
+ const whatHappened = await sendWasmModule(openedWindowGetter());
+
+ assert_equals(whatHappened, "messageerror");
+ }, `messageerror event must occur`);
+
+ promise_test(async () => {
+ assert_throws_dom("SecurityError", DOMException, () => {
+ openedWindowGetter().document;
+ });
+
+ assert_throws_dom("SecurityError", DOMException, () => {
+ openedWindowGetter().location.href;
+ });
+ }, `setting document.domain must not give sync access`);
+}
+
+/**
+ * Expands into a pair of promise_test() calls to ensure that the given window,
+ * opened by window.open(), is in the same agent cluster as the current
+ * (opener) window.
+ * @param {function} openedWindowGetter - A function that returns the opened
+ * window
+ */
+export function testOpenedWindowIsInSameAgentCluster(openedWindowGetter) {
+ promise_test(async () => {
+ const whatHappened = await sendWasmModule(openedWindowGetter());
+
+ assert_equals(whatHappened, "WebAssembly.Module message received");
+ }, `message event must occur`);
+
+ promise_test(async () => {
+ // Must not throw
+ openedWindowGetter().document;
+
+ // Must not throw
+ openedWindowGetter().location.href;
+ }, `setting document.domain must give sync access`);
+}
+
+/**
+ * Creates a promise_test() to check the value of the originAgentCluster getter
+ * in the given testFrame.
+ * @param {Window|number|function} testFrame - Either self, or a frame index to
+ test, or a function that returns a Window to test.
+ * @param {boolean} expected - The expected value for originAgentCluster.
+ * @param {string=} testLabelPrefix - A prefix used in the test names. This can
+ * be omitted if the function is only used once in a test file.
+ */
+export function testGetter(testFrame, expected, testLabelPrefix) {
+ const prefix = testLabelPrefix === undefined ? "" : `${testLabelPrefix}: `;
+
+ promise_test(async () => {
+ if (testFrame === self) {
+ assert_equals(self.originAgentCluster, expected);
+ } else if (typeof testFrame === "number") {
+ const frameWindow = frames[testFrame];
+ const result = await accessOriginAgentCluster(frameWindow);
+ assert_equals(result, expected);
+ } else {
+ assert_equals(typeof testFrame, "function",
+ "testFrame argument must be self, a number, or a function");
+ const result = await accessOriginAgentCluster(testFrame());
+ assert_equals(result, expected);
+ }
+ }, `${prefix}originAgentCluster must equal ${expected}`);
+}
+
+/**
+ * Sends a WebAssembly.Module instance to the given Window, and waits for it to
+ * send back a message indicating whether it got the module or got a
+ * messageerror event. (This relies on the given Window being derived from
+ * insertIframe or navigateIframe.)
+ * @param {Window} frameWindow - The destination Window
+ * @returns {Promise} A promise which will be fulfilled with either
+ * "WebAssembly.Module message received" or "messageerror"
+ */
+export async function sendWasmModule(frameWindow) {
+ // This function is coupled to ./send-oac-header.py, which ensures that
+ // sending such a message will result in a message back.
+ frameWindow.postMessage(await createWasmModule(), "*");
+ return waitForMessage(frameWindow);
+}
+
+/**
+ * Sets document.domain (to itself) for both the current Window and the given
+ * Window. The latter relies on the given Window being derived from insertIframe
+ * or navigateIframe.
+ * @param frameWindow - The other Window whose document.domain is to be set
+ * @returns {Promise} A promise which will be fulfilled after both
+ * document.domains are set
+ */
+export async function setBothDocumentDomains(frameWindow) {
+ // By setting both this page's document.domain and the iframe's
+ // document.domain to the same value, we ensure that they can synchronously
+ // access each other, unless they are origin-keyed.
+ // NOTE: document.domain being unset is different than it being set to its
+ // current value. It is a terrible API.
+ document.domain = document.domain;
+
+ // This function is coupled to ./send-oac-header.py, which ensures that
+ // sending such a message will result in a message back.
+ frameWindow.postMessage({ command: "set document.domain", newDocumentDomain: document.domain }, "*");
+ const whatHappened = await waitForMessage(frameWindow);
+ assert_equals(whatHappened, "document.domain is set");
+}
+
+async function accessOriginAgentCluster(frameWindow) {
+ // This function is coupled to ./send-oac-header.py, which ensures that
+ // sending such a message will result in a message back.
+ frameWindow.postMessage({ command: "get originAgentCluster" }, "*");
+ return waitForMessage(frameWindow);
+}
+
+function getSendHeaderURL(host, header, { sendLoadedMessage = false, redirectFirst = false } = {}) {
+ const url = new URL("send-oac-header.py", import.meta.url);
+ url.host = host;
+ if (header !== undefined) {
+ url.searchParams.set("header", header);
+ }
+ if (sendLoadedMessage) {
+ url.searchParams.set("send-loaded-message", "");
+ }
+ if (redirectFirst) {
+ url.searchParams.set("redirect-first", "");
+ }
+
+ return url.href;
+}
+
+async function sendWasmModuleBetween(testFrames) {
+ const sourceFrame = frames[testFrames[0]];
+ const indexIntoParentFrameOfDestination = testFrames[1];
+
+ sourceFrame.postMessage({ command: "send WASM module", indexIntoParentFrameOfDestination }, "*");
+ return waitForMessage(sourceFrame);
+}
+
+async function accessDocumentBetween(testFrames) {
+ const sourceFrame = frames[testFrames[0]];
+ const indexIntoParentFrameOfDestination = testFrames[1];
+
+ sourceFrame.postMessage({ command: "access document", indexIntoParentFrameOfDestination }, "*");
+ return waitForMessage(sourceFrame);
+}
+
+async function accessLocationHrefBetween(testFrames) {
+ const sourceFrame = frames[testFrames[0]];
+ const indexIntoParentFrameOfDestination = testFrames[1];
+
+ sourceFrame.postMessage({ command: "access location.href", indexIntoParentFrameOfDestination }, "*");
+ return waitForMessage(sourceFrame);
+}
+
+async function accessFrameElement(frameWindow) {
+ frameWindow.postMessage({ command: "access frameElement" }, "*");
+ return waitForMessage(frameWindow);
+}
+
+function waitForMessage(expectedSource) {
+ return new Promise(resolve => {
+ const handler = e => {
+ if (e.source === expectedSource) {
+ resolve(e.data);
+ window.removeEventListener("message", handler);
+ }
+ };
+ window.addEventListener("message", handler);
+ });
+}
+
+// Any WebAssembly.Module will work fine for our tests; we just want to find out
+// if it gives message or messageerror. So, we reuse one from the /wasm/ tests.
+async function createWasmModule() {
+ const response = await fetch("/wasm/serialization/module/resources/incrementer.wasm");
+ const ab = await response.arrayBuffer();
+ return WebAssembly.compile(ab);
+}
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/helpers.mjs.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/helpers.mjs.headers
new file mode 100644
index 0000000000..cb762eff80
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/helpers.mjs.headers
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: *
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/send-header-page-script.mjs b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/send-header-page-script.mjs
new file mode 100644
index 0000000000..17b00684d6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/send-header-page-script.mjs
@@ -0,0 +1,63 @@
+import { sendWasmModule } from "./helpers.mjs";
+
+// This is done for the window.open() case. For <iframe>s we use the
+// <iframe> element's load event instead.
+const usp = new URLSearchParams(location.search);
+if (usp.has("send-loaded-message")) {
+ opener.postMessage("loaded", "*");
+}
+
+window.onmessage = async (e) => {
+ // These could come from the parent, opener, or siblings.
+ if (e.data.constructor === WebAssembly.Module) {
+ e.source.postMessage("WebAssembly.Module message received", "*");
+ }
+
+ // These could come from the parent or opener.
+ if (e.data.command === "set document.domain") {
+ document.domain = e.data.newDocumentDomain;
+ e.source.postMessage("document.domain is set", "*");
+ } else if (e.data.command === "get originAgentCluster") {
+ e.source.postMessage(self.originAgentCluster, "*");
+ }
+
+ // These only come from the parent.
+ if (e.data.command === "send WASM module") {
+ const destinationFrameWindow = parent.frames[e.data.indexIntoParentFrameOfDestination];
+ const whatHappened = await sendWasmModule(destinationFrameWindow);
+ parent.postMessage(whatHappened, "*");
+ } else if (e.data.command === "access document") {
+ const destinationFrameWindow = parent.frames[e.data.indexIntoParentFrameOfDestination];
+ try {
+ destinationFrameWindow.document;
+ parent.postMessage("accessed document successfully", "*");
+ } catch (e) {
+ parent.postMessage(e.name, "*");
+ }
+ } else if (e.data.command === "access location.href") {
+ const destinationFrameWindow = parent.frames[e.data.indexIntoParentFrameOfDestination];
+ try {
+ destinationFrameWindow.location.href;
+ parent.postMessage("accessed location.href successfully", "*");
+ } catch (e) {
+ parent.postMessage(e.name, "*");
+ }
+ } else if (e.data.command === "access frameElement") {
+ if (frameElement === null) {
+ parent.postMessage("null", "*");
+ } else if (frameElement?.constructor?.name === "HTMLIFrameElement") {
+ parent.postMessage("frameElement accessed successfully", "*");
+ } else {
+ parent.postMessage("something wierd happened", "*");
+ }
+ }
+
+ // We could also receive e.data === "WebAssembly.Module message received",
+ // but that's handled by await sendWasmModule() above.
+};
+
+window.onmessageerror = e => {
+ e.source.postMessage("messageerror", "*");
+};
+
+document.body.textContent = location.href;
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/send-header-page-script.mjs.headers b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/send-header-page-script.mjs.headers
new file mode 100644
index 0000000000..cb762eff80
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/send-header-page-script.mjs.headers
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: *
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/send-oac-header.py b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/send-oac-header.py
new file mode 100644
index 0000000000..cc8860fe75
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/resources/send-oac-header.py
@@ -0,0 +1,43 @@
+def main(request, response):
+ """Send a response with the Origin-Agent-Cluster header given in the
+ "header" query parameter, or no header if that is not provided. Other query
+ parameters (only their presence/absence matters) are "send-loaded-message"
+ and "redirect-first", which modify the behavior a bit.
+
+ In either case, the response will listen for various messages posted and
+ coordinate with the sender. See ./helpers.mjs for how these handlers are
+ used.
+ """
+
+ if b"redirect-first" in request.GET:
+ # Create a new query string, which is the same as the one we're given but
+ # with the redirect-first component stripped out. This allows tests to use
+ # any value (or no value) for the other query params, in combination with
+ # redirect-first.
+ query_string_pieces = []
+ if b"header" in request.GET:
+ query_string_pieces.append(b"header=" + request.GET.first(b"header"))
+ if b"send-loaded-message" in request.GET:
+ query_string_pieces.append(b"send-loaded-message")
+ query_string = b"?" + b"&".join(query_string_pieces)
+
+ return (
+ 302,
+ [(b"Location", b"/html/browsers/origin/origin-keyed-agent-clusters/resources/send-oac-header.py" + query_string)],
+ u""
+ )
+
+ if b"header" in request.GET:
+ header = request.GET.first(b"header")
+ response.headers.set(b"Origin-Agent-Cluster", header)
+
+ response.headers.set(b"Content-Type", b"text/html")
+
+ return u"""
+ <!DOCTYPE html>
+ <meta charset="utf-8">
+ <title>Helper page for origin-keyed agent cluster tests</title>
+
+ <body>
+ <script type="module" src="send-header-page-script.mjs"></script>
+ """
diff --git a/testing/web-platform/tests/html/browsers/origin/origin-of-data-document.html b/testing/web-platform/tests/html/browsers/origin/origin-of-data-document.html
new file mode 100644
index 0000000000..448f47fa24
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/origin-of-data-document.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>Origin of document produced from a 'data:' URL</title>
+ <link rel="help" href="https://html.spec.whatwg.org/multipage/browsers.html#origin">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ async_test(function (t) {
+ var i = document.createElement('iframe');
+ i.src = "data:text/html,<script>" +
+ " window.parent.postMessage('Hello!', '*');" +
+ "</scr" + "ipt>";
+
+ window.addEventListener("message", t.step_func_done(function (e) {
+ assert_equals(e.origin, "null", "Messages sent from a 'data:' URL should have an opaque origin (which serializes to 'null').");
+ assert_throws_dom("SecurityError", function () {
+ var couldAccessCrossOriginProperty = e.source.location.href;
+ }, "The 'data:' frame should be cross-origin: 'window.location.href'");
+ assert_equals(i.contentDocument, null, "The 'data:' iframe should be unable to access its contentDocument.");
+ }));
+
+ document.body.appendChild(i);
+ }, "The origin of a 'data:' document in a frame is opaque.");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain.html b/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain.html
new file mode 100644
index 0000000000..d3af35c6d7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<html>
+ <head>
+ <title>document.domain's getter</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script>
+ test(function() {
+ assert_equals(typeof document.domain, "string", "document.domain is a string");
+ assert_not_equals(document.domain, "", "document.domain is not empty");
+ }, "basics");
+
+ test(function() {
+ assert_equals(document.domain, window.location.hostname, "equals location.hostname");
+ }, "current document");
+
+ test(function() {
+ var doc = new Document();
+ assert_equals(doc.domain, window.location.hostname, "equals location.hostname");
+ }, "new Document()");
+
+ async_test(t => {
+ const client = new XMLHttpRequest();
+ client.open("GET", "/common/blank.html");
+ client.responseType = "document"
+ client.send();
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.response.domain, window.location.hostname);
+ });
+ }, "XMLHttpRequest's response document");
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_access_details.sub.html b/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_access_details.sub.html
new file mode 100644
index 0000000000..eb02c96f1d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_access_details.sub.html
@@ -0,0 +1,305 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="support/document_domain_frame.sub.js"></script>
+<body>
+<script>
+promise_test(async (t) => {
+ let frame1 = await createFrame(t, "control1-1", "{{domains[www1]}}");
+ let frame2 = await createFrame(t, "control1-2", "{{domains[www1]}}");
+ let result = await postMessageToFrame(frame1, { 'poke-at-sibling': "control1-2" });
+ assert_equals(result.data, "omg!");
+}, "Access allowed if same-origin with no 'document.domain' modification. (Sanity check)");
+
+promise_test(async (t) => {
+ let frame1 = await createFrame(t, "control2-1", "{{domains[www1]}}");
+ let frame2 = await createFrame(t, "control2-2", "{{domains[www2]}}");
+ let result = await postMessageToFrame(frame1, { 'poke-at-sibling': "control2-2" });
+ assert_equals(result.data, "SecurityError");
+}, "Access not allowed if different-origin with no 'document.domain' modification. (Sanity check)");
+
+promise_test(async (t) => {
+ let frame1 = await createFrame(t, "one-set-one-not-1", "{{domains[www1]}}");
+ let frame2 = await createFrame(t, "one-set-one-not-2", "{{domains[www1]}}");
+ await postMessageToFrame(frame1, { domain: "{{domains[www1]}}" });
+
+ let result = await postMessageToFrame(frame1, { 'poke-at-sibling': "one-set-one-not-2" });
+ assert_equals(result.data, "SecurityError");
+
+ result = await postMessageToFrame(frame2, { 'poke-at-sibling': "one-set-one-not-1" });
+ assert_equals(result.data, "SecurityError");
+}, "Access disallowed if same-origin but only one sets document.domain.");
+
+promise_test(async (t) => {
+ var frame1 = await createFrame(t, "both-set-to-existing-1", "{{domains[www1]}}");
+ var frame2 = await createFrame(t, "both-set-to-existing-2", "{{domains[www1]}}");
+ let result = await postMessageToFrame(frame1, { domain: "{{domains[www1]}}" });
+ assert_equals(result.data, "Done");
+
+ result = await postMessageToFrame(frame2, { domain: "{{domains[www1]}}" });
+ assert_equals(result.data, "Done");
+
+ result = await postMessageToFrame(frame1, { 'poke-at-sibling': "both-set-to-existing-2" });
+ assert_equals(result.data, "omg!");
+
+ result = await postMessageToFrame(frame2, { 'poke-at-sibling': "both-set-to-existing-1" });
+ assert_equals(result.data, "omg!");
+}, "Access allowed if same-origin and both set document.domain to existing value.");
+
+promise_test(async (t) => {
+ var frame1 = await createFrame(t, "both-set-to-parent-1", "{{domains[www1]}}");
+ var frame2 = await createFrame(t, "both-set-to-parent-2", "{{domains[www2]}}");
+ let result = await postMessageToFrame(frame1, { domain: "{{domains[]}}" });
+ assert_equals(result.data, "Done");
+
+ result = await postMessageToFrame(frame2, { domain: "{{domains[]}}" });
+ assert_equals(result.data, "Done");
+
+ result = await postMessageToFrame(frame1, { 'poke-at-sibling': "both-set-to-parent-2" });
+ assert_equals(result.data, "omg!");
+
+ result = await postMessageToFrame(frame2, { 'poke-at-sibling': "both-set-to-parent-1" });
+ assert_equals(result.data, "omg!");
+}, "Access allowed if different-origin but both set document.domain to parent domain.");
+
+promise_test(async (t) => {
+ var frame1 = await createFrame(t, "allow-then-revoke-1", "{{domains[www1]}}");
+ var frame2 = await createFrame(t, "allow-then-revoke-2", "{{domains[www1]}}");
+ let result = await postMessageToFrame(frame1, { domain: "{{domains[www1]}}" });
+ assert_equals(result.data, "Done");
+
+ result = await postMessageToFrame(frame2, { domain: "{{domains[www1]}}" });
+ assert_equals(result.data, "Done");
+
+ result = await postMessageToFrame(frame1, { 'poke-at-sibling': "allow-then-revoke-2" });
+ assert_equals(result.data, "omg!");
+
+ result = await postMessageToFrame(frame2, { 'poke-at-sibling': "allow-then-revoke-1" });
+ assert_equals(result.data, "omg!");
+
+ result = await postMessageToFrame(frame1, { domain: "{{domains[]}}" });
+ assert_equals(result.data, "Done");
+
+ result = await postMessageToFrame(frame1, { 'poke-at-sibling': "allow-then-revoke-2" });
+ assert_equals(result.data, "SecurityError");
+
+ result = await postMessageToFrame(frame2, { 'poke-at-sibling': "allow-then-revoke-1" });
+ assert_equals(result.data, "SecurityError");
+}, "Access disallowed again if same-origin, both set document-domain to existing value, then one sets to parent.");
+
+promise_test(async (t) => {
+ let frame1 = await createFrame(t, "revoke-Window-1", "{{domains[www1]}}");
+ let frame2 = await createFrame(t, "revoke-Window-2", "{{domains[www1]}}");
+
+ let result = await postMessageToFrame(frame1, { cache: ["parent", "revoke-Window-2"] });
+ assert_equals(result.data, "cached");
+
+ result = await postMessageToFrame(frame1, 'touch-cached');
+ assert_equals(result.data, "Reachable 1");
+
+ result = await postMessageToFrame(frame2, { cache: ["parent", "revoke-Window-1"] });
+ assert_equals(result.data, "cached");
+
+ result = await postMessageToFrame(frame1, 'touch-cached');
+ assert_equals(result.data, "Reachable 1");
+
+ result = await postMessageToFrame(frame1, { domain: "{{domains[www1]}}" });
+ assert_equals(result.data, "Done");
+
+ result = await postMessageToFrame(frame1, 'touch-cached');
+ assert_equals(result.data, "SecurityError");
+
+ result = await postMessageToFrame(frame2, 'touch-cached');
+ assert_equals(result.data, "SecurityError");
+}, "Access is revoked to Window object when we stop being same effective script origin due to document.domain.");
+
+promise_test(async (t) => {
+ let frame1 = await createFrame(t, "revoke-Location-1", "{{domains[www1]}}");
+ let frame2 = await createFrame(t, "revoke-Location-2", "{{domains[www1]}}");
+
+ let result = await postMessageToFrame(frame1, { cache: ["parent", "revoke-Location-2", "location"] });
+ assert_equals(result.data, "cached");
+
+ result = await postMessageToFrame(frame1, 'touch-cached');
+ assert_equals(result.data, "Reachable 3");
+
+ result = await postMessageToFrame(frame2, { cache: ["parent", "revoke-Location-1", "location"] });
+ assert_equals(result.data, "cached");
+
+ result = await postMessageToFrame(frame1, 'touch-cached');
+ assert_equals(result.data, "Reachable 3");
+
+ result = await postMessageToFrame(frame1, { domain: "{{domains[www1]}}" });
+ assert_equals(result.data, "Done");
+
+ result = await postMessageToFrame(frame1, 'touch-cached');
+ assert_equals(result.data, "SecurityError");
+
+ result = await postMessageToFrame(frame2, 'touch-cached');
+ assert_equals(result.data, "SecurityError");
+}, "Access is revoked to Location object when we stop being same effective script origin due to document.domain.");
+
+promise_test(async (t) => {
+ let frame1 = await createFrame(t, "no-revoke-Document-1", "{{domains[www1]}}");
+ let frame2 = await createFrame(t, "no-revoke-Document-2", "{{domains[www1]}}");
+
+ let result = await postMessageToFrame(frame1, { cache: ["parent", "no-revoke-Document-2", "document"] });
+ assert_equals(result.data, "cached");
+
+ result = await postMessageToFrame(frame1, 'touch-cached');
+ assert_equals(result.data, "Reachable 4");
+
+ result = await postMessageToFrame(frame2, { cache: ["parent", "no-revoke-Document-1", "document"] });
+ assert_equals(result.data, "cached");
+
+ result = await postMessageToFrame(frame2, 'touch-cached');
+ assert_equals(result.data, "Reachable 4");
+
+ result = await postMessageToFrame(frame1, { domain: "{{domains[www1]}}" });
+ assert_equals(result.data, "Done");
+
+ result = await postMessageToFrame(frame1, 'touch-cached');
+ assert_equals(result.data, "Reachable 4");
+
+ result = await postMessageToFrame(frame2, 'touch-cached');
+ assert_equals(result.data, "Reachable 4");
+}, "Access is not revoked to Document object when we stop being same effective script origin due to document.domain.");
+
+promise_test(async (t) => {
+ let frame1 = await createFrame(t, "no-revoke-object-1", "{{domains[www1]}}");
+ let frame2 = await createFrame(t, "no-revoke-object-2", "{{domains[www1]}}");
+
+ let result = await postMessageToFrame(frame1, { cache: ["parent", "no-revoke-object-2", "bar"] });
+ assert_equals(result.data, "cached");
+
+ result = await postMessageToFrame(frame1, 'touch-cached');
+ assert_equals(result.data, "Reachable 2");
+
+ result = await postMessageToFrame(frame2, { cache: ["parent", "no-revoke-object-1", "bar"] });
+ assert_equals(result.data, "cached");
+
+ result = await postMessageToFrame(frame1, 'touch-cached');
+ assert_equals(result.data, "Reachable 2");
+
+ result = await postMessageToFrame(frame1, { domain: "{{domains[www1]}}" });
+ assert_equals(result.data, "Done");
+
+ result = await postMessageToFrame(frame1, 'touch-cached');
+ assert_equals(result.data, "Reachable 2");
+
+ result = await postMessageToFrame(frame2, 'touch-cached');
+ assert_equals(result.data, "Reachable 2");
+}, "Access is not revoked to random object when we stop being same effective script origin due to document.domain.");
+
+promise_test(async (t) => {
+ let frame1 = await createFrame(t, "join-and-diverge-1", "{{domains[www2.www1]}}");
+ let frame2 = await createFrame(t, "join-and-diverge-2", "{{domains[www1.www1]}}");
+
+ // Make sure we can't touch each other.
+ let result = await postMessageToFrame(frame1, { 'poke-at-sibling': "join-and-diverge-2" });
+ assert_equals(result.data, "SecurityError");
+
+ result = await postMessageToFrame(frame2, { 'poke-at-sibling': "join-and-diverge-1" });
+ assert_equals(result.data, "SecurityError");
+
+ result = await postMessageToFrame(frame1, { cache: ["parent", "join-and-diverge-2", "bar"] });
+ assert_equals(result.data, "SecurityError");
+
+ result = await postMessageToFrame(frame2, { cache: ["parent", "join-and-diverge-1", "document"] });
+ assert_equals(result.data, "SecurityError");
+
+ // Let's join up now.
+ result = await postMessageToFrame(frame1, { domain: "{{domains[www1]}}" });
+ assert_equals(result.data, "Done");
+
+ result = await postMessageToFrame(frame2, { domain: "{{domains[www1]}}" });
+ assert_equals(result.data, "Done");
+
+ // Now we should be able to touch each other.
+ result = await postMessageToFrame(frame1, { 'poke-at-sibling': "join-and-diverge-2" });
+ assert_equals(result.data, "omg!");
+
+ result = await postMessageToFrame(frame2, { 'poke-at-sibling': "join-and-diverge-1" });
+ assert_equals(result.data, "omg!");
+
+ // Cache a random object and a document.
+ result = await postMessageToFrame(frame1, { cache: ["parent", "join-and-diverge-2", "bar"] });
+ assert_equals(result.data, "cached");
+
+ result = await postMessageToFrame(frame1, 'touch-cached');
+ assert_equals(result.data, "Reachable 2");
+
+ result = await postMessageToFrame(frame2, { cache: ["parent", "join-and-diverge-1", "document"] });
+ assert_equals(result.data, "cached");
+
+ result = await postMessageToFrame(frame2, 'touch-cached');
+ assert_equals(result.data, "Reachable 4");
+
+ // OK, now let's diverge
+ result = await postMessageToFrame(frame1, { domain: "{{domains[]}}" });
+ assert_equals(result.data, "Done");
+
+ // We should still be able to touch our cached things.
+ result = await postMessageToFrame(frame1, 'touch-cached');
+ assert_equals(result.data, "Reachable 2");
+
+ result = await postMessageToFrame(frame2, 'touch-cached');
+ assert_equals(result.data, "Reachable 4");
+}, "Access evolves correctly for non-cross-origin objects when we join up via document.domain and then diverge again.");
+
+promise_test(async (t) => {
+ let frame1 = await createFrame(t, "join-and-diverge-cross-origin-1", "{{domains[www2.www1]}}");
+ let frame2 = await createFrame(t, "join-and-diverge-cross-origin-2", "{{domains[www1.www1]}}");
+
+ // Make sure we can't touch each other.
+ let result = await postMessageToFrame(frame1, { 'poke-at-sibling': "join-and-diverge-cross-origin-2" });
+ assert_equals(result.data, "SecurityError");
+
+ result = await postMessageToFrame(frame2, { 'poke-at-sibling': "join-and-diverge-cross-origin-1" });
+ assert_equals(result.data, "SecurityError");
+
+ result = await postMessageToFrame(frame1, { cache: ["parent", "join-and-diverge-2", "bar"] });
+ assert_equals(result.data, "SecurityError");
+
+ result = await postMessageToFrame(frame2, { cache: ["parent", "join-and-diverge-1", "document"] });
+ assert_equals(result.data, "SecurityError");
+
+ // Let's join up now.
+ result = await postMessageToFrame(frame1, { domain: "{{domains[www1]}}" });
+ assert_equals(result.data, "Done");
+
+ result = await postMessageToFrame(frame2, { domain: "{{domains[www1]}}" });
+ assert_equals(result.data, "Done");
+
+ // Now we should be able to touch each other.
+ result = await postMessageToFrame(frame1, { 'poke-at-sibling': "join-and-diverge-cross-origin-2" });
+ assert_equals(result.data, "omg!");
+
+ result = await postMessageToFrame(frame2, { 'poke-at-sibling': "join-and-diverge-cross-origin-1" });
+ assert_equals(result.data, "omg!");
+
+ // Cache a window and a location
+ result = await postMessageToFrame(frame1, { cache: ["parent", "join-and-diverge-cross-origin-2"] });
+ assert_equals(result.data, "cached");
+
+ result = await postMessageToFrame(frame1, 'touch-cached');
+ assert_equals(result.data, "Reachable 1");
+
+ result = await postMessageToFrame(frame2, { cache: ["parent", "join-and-diverge-cross-origin-1", "location"] });
+ assert_equals(result.data, "cached");
+
+ result = await postMessageToFrame(frame2, 'touch-cached');
+ assert_equals(result.data, "Reachable 3");
+
+ // OK, now let's diverge
+ result = await postMessageToFrame(frame1, { domain: "{{domains[]}}" });
+ assert_equals(result.data, "Done");
+
+ // Now our cross-origin objects should start denying access.
+ result = await postMessageToFrame(frame1, 'touch-cached');
+ assert_equals(result.data, "SecurityError");
+
+ result = await postMessageToFrame(frame2, 'touch-cached');
+ assert_equals(result.data, "SecurityError");
+}, "Access evolves correctly for cross-origin objects when we join up via document.domain and then diverge again.");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_setter.html b/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_setter.html
new file mode 100644
index 0000000000..2539528341
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_setter.html
@@ -0,0 +1,76 @@
+<!doctype html>
+<html>
+ <head>
+ <title>document.domain's setter</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <iframe id="iframe"></iframe>
+ <script>
+ var host_info = get_host_info();
+ var HTTP_PORT = host_info.HTTP_PORT;
+ var ORIGINAL_HOST = host_info.ORIGINAL_HOST;
+ var SUFFIX_HOST = ORIGINAL_HOST.substring(ORIGINAL_HOST.lastIndexOf('.') + 1); // e.g. "test"
+ var REMOTE_HOST = host_info.REMOTE_HOST;
+ var iframe = document.getElementById("iframe");
+ var iframe_url = new URL("support/document_domain_setter_iframe.html", document.location);
+ iframe_url.hostname = REMOTE_HOST;
+ iframe.src = iframe_url;
+
+ test(function() {
+ assert_throws_dom("SecurityError", function() { document.domain = SUFFIX_HOST; });
+ assert_throws_dom("SecurityError", function() { document.domain = "." + SUFFIX_HOST; });
+ assert_throws_dom("SecurityError", function() { document.domain = REMOTE_HOST; });
+ assert_throws_dom("SecurityError", function() { document.domain = "example.com"; });
+ }, "failed setting of document.domain");
+
+ async_test(function(t) {
+ iframe.addEventListener("load", t.step_func_done(function() {
+ // Before setting document.domain, the iframe is not
+ // same-origin-domain, so security checks fail.
+ assert_equals(iframe.contentDocument, null);
+ assert_throws_dom("SecurityError", () => iframe.contentWindow.frameElement);
+ assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.origin; });
+ assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.href; });
+ assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.protocol; });
+ assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.host; });
+ assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.port; });
+ assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.hostname; });
+ assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.pathname; });
+ assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.hash; });
+ assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.search; });
+ assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.toString(); });
+ // Set document.domain
+ document.domain = ORIGINAL_HOST;
+ // After setting document.domain, the iframe is
+ // same-origin-domain, so security checks pass.
+ assert_equals(iframe.contentDocument.domain, document.domain);
+ assert_equals(iframe.contentWindow.frameElement, iframe);
+ assert_equals(iframe.contentWindow.origin, iframe_url.origin);
+ assert_equals(iframe.contentWindow.location.href, iframe_url.href);
+ assert_equals(iframe.contentWindow.location.protocol, iframe_url.protocol);
+ assert_equals(iframe.contentWindow.location.host, iframe_url.host);
+ assert_equals(iframe.contentWindow.location.port, iframe_url.port);
+ assert_equals(iframe.contentWindow.location.hostname, iframe_url.hostname);
+ assert_equals(iframe.contentWindow.location.pathname, iframe_url.pathname);
+ assert_equals(iframe.contentWindow.location.hash, iframe_url.hash);
+ assert_equals(iframe.contentWindow.location.search, iframe_url.search);
+ assert_equals(iframe.contentWindow.location.search, iframe_url.search);
+ assert_equals(iframe.contentWindow.location.toString(), iframe_url.toString());
+ // document.open checks for same-origin, not same-origin-domain,
+ // https://github.com/whatwg/html/issues/2282
+ assert_throws_dom("SecurityError", iframe.contentWindow.DOMException,
+ function() { iframe.contentDocument.open(); });
+ }));
+ }, "same-origin-domain iframe");
+
+ test(() => {
+ assert_throws_dom("SecurityError", () => { (new Document).domain = document.domain });
+ assert_throws_dom("SecurityError", () => { document.implementation.createHTMLDocument().domain = document.domain });
+ assert_throws_dom("SecurityError", () => { document.implementation.createDocument(null, "").domain = document.domain });
+ }, "failed setting of document.domain for documents without browsing context");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_setter_srcdoc.html b/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_setter_srcdoc.html
new file mode 100644
index 0000000000..65a7f5c898
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_setter_srcdoc.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<!-- SEKRITS! -->
+<input id="sekrit" value="omg!">
+
+<script>
+ function postMessageToFrame(frame, message) {
+ return new Promise(resolve => {
+ var c = new MessageChannel();
+ c.port1.onmessage = e => {
+ resolve({ data: e.data, frame: frame })
+ };
+ frame.contentWindow.postMessage(message, '*', [c.port2]);
+ });
+ }
+
+ function createFrame() {
+ return new Promise(resolve => {
+ var i = document.createElement('iframe');
+ i.srcdoc = `
+ <script>
+ window.addEventListener('message', e => {
+ if (e.data.domain !== undefined) {
+ try {
+ document.domain = e.data.domain;
+ e.ports[0].postMessage('Done');
+ } catch(error) {
+ e.ports[0].postMessage(error.name);
+ }
+ } else if (e.data == 'poke-at-parent') {
+ try {
+ var sekrit = window.parent.document.body.querySelector('#sekrit').value;
+ e.ports[0].postMessage(sekrit);
+ } catch(error) {
+ e.ports[0].postMessage(error.name);
+ }
+ }
+ });
+ window.parent.postMessage('Hi!', '*');
+ </scr` + `ipt>`;
+ window.addEventListener('message', m => {
+ if (m.source == i.contentWindow)
+ resolve(i);
+ });
+ document.body.appendChild(i);
+ });
+ }
+
+ promise_test(t => {
+ return createFrame()
+ .then(f => postMessageToFrame(f, 'poke-at-parent'))
+ .then(result => {
+ assert_equals(result.data, document.querySelector('#sekrit').value);
+ result.frame.remove();
+ });
+ }, "srcdoc can access with no 'document.domain' modification.");
+
+ promise_test(t => {
+ return createFrame()
+ .then(f => postMessageToFrame(f, { domain: window.location.hostname }))
+ .then(result => {
+ assert_equals(result.data, 'Done');
+ return postMessageToFrame(result.frame, 'poke-at-parent')
+ .then(result => {
+ assert_equals(result.data, document.querySelector('#sekrit').value);
+ result.frame.remove();
+ });
+ });
+ }, "srcdoc can access with valid 'document.domain'.");
+
+ promise_test(t => {
+ return createFrame()
+ .then(f => {
+ document.domain = window.location.hostname;
+ return postMessageToFrame(f, 'poke-at-parent');
+ })
+ .then(result => {
+ assert_equals(result.data, document.querySelector('#sekrit').value);
+ result.frame.remove();
+ });
+ }, "srcdoc can access when parent modifies 'document.domain'.");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/sandboxed-document_domain.html b/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/sandboxed-document_domain.html
new file mode 100644
index 0000000000..ae1a0ccd56
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/sandboxed-document_domain.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<title>Sandboxed document.domain</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ test(() => {
+ assert_throws_dom("SecurityError", () => { document.domain = document.domain });
+ });
+ test(() => {
+ assert_throws_dom("SecurityError", () => { (new Document).domain = document.domain });
+ });
+ test(() => {
+ assert_throws_dom("SecurityError", () => { document.implementation.createHTMLDocument().domain = document.domain });
+ });
+ test(() => {
+ assert_throws_dom("SecurityError", () => { document.implementation.createDocument(null, "").domain = document.domain });
+ });
+ test(() => {
+ assert_throws_dom("SecurityError", () => { document.createElement("template").content.ownerDocument.domain = document.domain });
+ });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/sandboxed-document_domain.html.headers b/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/sandboxed-document_domain.html.headers
new file mode 100644
index 0000000000..82e8023d0b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/sandboxed-document_domain.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: sandbox allow-scripts allow-same-origin
diff --git a/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_frame.html b/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_frame.html
new file mode 100644
index 0000000000..61f54af359
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_frame.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<script>
+ let cache = window;
+ // "foo" needs to be a var so it's a property on the global.
+ var foo = 'Reachable 1';
+ // "bar" needs to be a var so it's a property on the global.
+ var bar = { foo: 'Reachable 2' };
+ location.foo = 'Reachable 3';
+ document.foo = 'Reachable 4';
+ window.addEventListener('message', e => {
+ if (e.data.domain !== undefined) {
+ try {
+ document.domain = e.data.domain;
+ e.ports[0].postMessage('Done');
+ } catch(error) {
+ e.ports[0].postMessage(error.name);
+ }
+ } else if (e.data['poke-at-sibling'] !== undefined) {
+ try {
+ var sekrit = parent[e.data['poke-at-sibling']].document.body.querySelector('#sekrit').value;
+ e.ports[0].postMessage(sekrit);
+ } catch(error) {
+ e.ports[0].postMessage(error.name);
+ }
+ } else if (e.data.cache != undefined) {
+ let path = e.data.cache;
+ try {
+ while (path.length != 0) {
+ cache = cache[path.shift()];
+ }
+ e.ports[0].postMessage('cached');
+ } catch (error) {
+ e.ports[0].postMessage(error.name);
+ }
+ } else if (e.data == 'touch-cached') {
+ try {
+ e.ports[0].postMessage(cache.foo);
+ } catch (error) {
+ e.ports[0].postMessage(error.name);
+ }
+ } else if (e.data == 'poke-at-parent') {
+ try {
+ var sekrit = window.parent.document.body.querySelector('#sekrit').value;
+ e.ports[0].postMessage(sekrit);
+ } catch(error) {
+ e.ports[0].postMessage(error.name);
+ }
+ }
+ });
+ window.parent.postMessage('Hi!', '*');
+</script>
+<input id="sekrit" value="omg!">
diff --git a/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_frame.sub.js b/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_frame.sub.js
new file mode 100644
index 0000000000..b6631ea4f1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_frame.sub.js
@@ -0,0 +1,65 @@
+/**
+ * Utilities to be used with document_domain_frame.html.
+ */
+
+/**
+ * Send a message to the frame and resolve a promise when a response is received.
+ *
+ * Supported messages:
+ *
+ * 1) { domain: something }. Has the subframe try to set document.domain to the
+ * given value, and message back 'Done' if that succeeds or an error name if it
+ * fails.
+ *
+ * 2) 'poke-at-parent'. Has the subframe try to synchronously attempt to access
+ * the parent's DOM, read out a string value, and message it back to the parent.
+ * Again, sends back the error name if that fails.
+ *
+ * 3) { 'poke-at-sibling': name }. Has the subframe try to synchronously
+ * attempt to access the DOM of the sibling with the given name, read out a
+ * string value, and message it back to the parent.
+ */
+function postMessageToFrame(frame, message) {
+ return new Promise(resolve => {
+ var c = new MessageChannel();
+ c.port1.onmessage = e => {
+ resolve({ data: e.data, frame: frame })
+ };
+ frame.contentWindow.postMessage(message, '*', [c.port2]);
+ });
+}
+
+/**
+ * Create a frame that loads document_domain_frame.html and resolves a promise
+ * when the frame is loaded enough to be sending and receiving messages.
+ *
+ * If a "name" argument is provided, that name is used for the iframe, so
+ *
+ * If a "hostname" argument is provided, that hostname is used for the load, to
+ * allow testing details of the behavior when different sorts of hostnames are
+ * used.
+ */
+function createFrame(t, name, hostname) {
+ return new Promise(resolve => {
+ var i = document.createElement('iframe');
+ if (hostname) {
+ i.src = `//${hostname}:{{location[port]}}/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_frame.html`;
+ } else {
+ i.src = "support/document_domain_frame.html";
+ }
+ if (name) {
+ i.name = name;
+ }
+ var listener = m => {
+ if (m.source == i.contentWindow)
+ resolve(i);
+ }
+ window.addEventListener('message', listener);
+ t.add_cleanup(() => {
+ i.remove();
+ window.removeEventListener('message', listener);
+ });
+ document.body.appendChild(i);
+ });
+}
+
diff --git a/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_setter_iframe.html b/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_setter_iframe.html
new file mode 100644
index 0000000000..d3d5260af3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/origin/relaxing-the-same-origin-restriction/support/document_domain_setter_iframe.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html>
+ <head>
+ <title></title>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script>
+ document.domain = get_host_info().ORIGINAL_HOST;
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/inner-iframe.html b/testing/web-platform/tests/html/browsers/sandboxing/inner-iframe.html
new file mode 100644
index 0000000000..229f6b3d85
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/inner-iframe.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script>
+ window.onload = function() {
+ top.calledFromIframe();
+ }
+ </script>
+ </head>
+ <body>
+ <div id="inner">foo</div>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/noscript-iframe.html b/testing/web-platform/tests/html/browsers/sandboxing/noscript-iframe.html
new file mode 100644
index 0000000000..677b5fc83a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/noscript-iframe.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<noscript>PASS</noscript>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/popup-from-initial-empty-sandboxed-document.window.js b/testing/web-platform/tests/html/browsers/sandboxing/popup-from-initial-empty-sandboxed-document.window.js
new file mode 100644
index 0000000000..1ae4fad0cb
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/popup-from-initial-empty-sandboxed-document.window.js
@@ -0,0 +1,46 @@
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/dispatcher/dispatcher.js
+
+// Regression test for: https://crbug.com/1256822.
+//
+// From a sandboxed iframe allowing popups, scripts, and same-origin. Open a
+// popup using the WindowProxy of a new iframe that is still on the initial
+// empty document. Check that the sandbox flags are properly inherited.
+
+// Return true if the execution context is sandboxed.
+const isSandboxed = () => {
+ try {
+ // Setting document.domain in sandboxed document throw errors.
+ document.domain = document.domain;
+ return false;
+ } catch (error) {
+ return true;
+ }
+}
+
+promise_test(async test => {
+ // 1. Create a sandboxed iframe, allowing popups, same-origin and scripts.
+ const iframe_token = token();
+ const iframe_document = new RemoteContext(iframe_token);
+ const iframe_url = remoteExecutorUrl(iframe_token);
+ const iframe = document.createElement("iframe");
+ iframe.sandbox = "allow-same-origin allow-scripts allow-popups";
+ iframe.src = iframe_url;
+ document.body.appendChild(iframe);
+ assert_true(await iframe_document.execute_script(isSandboxed),
+ "iframe is sandboxed");
+
+ // 2. From the sandboxed iframe, create an empty iframe, and open a popup
+ // using it's WindowProxy. The popup must inherit sandbox flags.
+ const popup_token = token();
+ const popup_document = new RemoteContext(popup_token);
+ const popup_url = remoteExecutorUrl(popup_token);
+ iframe_document.execute_script((popup_url) => {
+ let iframe = document.createElement("iframe");
+ iframe.name = "iframe_name";
+ document.body.appendChild(iframe);
+ iframe_name.open(popup_url);
+ }, [popup_url.href]);
+ assert_true(await popup_document.execute_script(isSandboxed), "popup is sandboxed");
+});
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/resources/check-sandbox-flags.html b/testing/web-platform/tests/html/browsers/sandboxing/resources/check-sandbox-flags.html
new file mode 100644
index 0000000000..0dc95315f1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/resources/check-sandbox-flags.html
@@ -0,0 +1,8 @@
+<script>
+ try {
+ document.domain = document.domain;
+ parent.postMessage('document-domain-is-allowed', '*');
+ } catch (error) {
+ parent.postMessage('document-domain-is-disallowed', '*');
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/resources/document-open.html b/testing/web-platform/tests/html/browsers/sandboxing/resources/document-open.html
new file mode 100644
index 0000000000..136c494d5a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/resources/document-open.html
@@ -0,0 +1,14 @@
+<script>
+ onload = () => {
+ document.write(`
+ <script>
+ try {
+ document.domain = document.domain;
+ parent.postMessage('document-domain-is-allowed', '*');
+ } catch (error) {
+ parent.postMessage('document-domain-is-disallowed', '*');
+ }
+ </sc`+`ript>
+ `);
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/resources/execute-postmessage.html b/testing/web-platform/tests/html/browsers/sandboxing/resources/execute-postmessage.html
new file mode 100644
index 0000000000..89bd268f9c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/resources/execute-postmessage.html
@@ -0,0 +1,5 @@
+<script>
+ // Execute arbitrary code from somewhere else, via postMessage.
+ window.addEventListener("message", event => eval(event.data));
+ window.opener.postMessage("ready", "*");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/resources/post-done-to-opener.html b/testing/web-platform/tests/html/browsers/sandboxing/resources/post-done-to-opener.html
new file mode 100644
index 0000000000..b47f0f274e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/resources/post-done-to-opener.html
@@ -0,0 +1,3 @@
+<script>
+ window.opener.top.postMessage("DONE", "*");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/resources/sandbox-inherited-from-initiator-response-helper.html b/testing/web-platform/tests/html/browsers/sandboxing/resources/sandbox-inherited-from-initiator-response-helper.html
new file mode 100644
index 0000000000..29c7f12441
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/resources/sandbox-inherited-from-initiator-response-helper.html
@@ -0,0 +1,15 @@
+<script>
+ const iframe_1_script = encodeURI(`
+ <script>
+ try {
+ document.domain = document.domain;
+ parent.postMessage("not sandboxed", "*");
+ } catch (exception) {
+ parent.postMessage("sandboxed", "*");
+ }
+ </scr`+`ipt>
+ `);
+
+ const iframe_1 = parent.document.querySelector("#iframe_1");
+ iframe_1.src = `data:text/html,${iframe_1_script}`;
+</script>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/resources/sandbox-inherited-from-initiator-response-helper.html.headers b/testing/web-platform/tests/html/browsers/sandboxing/resources/sandbox-inherited-from-initiator-response-helper.html.headers
new file mode 100644
index 0000000000..82e8023d0b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/resources/sandbox-inherited-from-initiator-response-helper.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: sandbox allow-scripts allow-same-origin
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/resources/sandbox-javascript-window-open.html b/testing/web-platform/tests/html/browsers/sandboxing/resources/sandbox-javascript-window-open.html
new file mode 100644
index 0000000000..909956a54f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/resources/sandbox-javascript-window-open.html
@@ -0,0 +1,18 @@
+<script>
+ // Forward message from the openee toward the parent.
+ window.addEventListener("message", event => top.postMessage(event.data, "*"));
+
+ let check_sandboxed = `"
+ <script>
+ try {
+ document.domain = document.domain;
+ opener.postMessage('allow-document-domain', '*');
+ } catch (error) {
+ opener.postMessage('disallow-document-domain', '*');
+ }
+ </scr`+`ipt>
+ "`;
+
+ window.open('about:blank', "window_name");
+ window.open("javascript:" + check_sandboxed, "window_name");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-allow-same-origin.html b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-allow-same-origin.html
new file mode 100644
index 0000000000..d6b3b099f2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-allow-same-origin.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>DOM access in sandbox="allow-same-origin" iframe</title>
+ <link rel="author" title="Kinuko Yasuda" href="mailto:kinuko@chromium.org">
+ <link rel="help" href="http://www.w3.org/html/wg/drafts/html/master/browsers.html#sandboxing">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+
+ <body>
+ <h1>DOM access in sandbox="allow-same-origin" iframe</h1>
+ <script type="text/javascript">
+ var t = async_test("DOM access in sandbox='allow-same-origin' iframe is allowed")
+ var called = 0;
+ function calledFromIframe() {
+ called++;
+ }
+ function loaded() {
+ assert_equals(document.getElementById('sandboxedframe').contentWindow.document.getElementById('inner').innerHTML, 'foo');
+ assert_equals(called, 0);
+ t.done();
+ }
+ </script>
+
+ <iframe src="/html/browsers/sandboxing/inner-iframe.html" style="visibility:hidden;display:none" sandbox="allow-same-origin" id="sandboxedframe" onload="loaded();"></iframe>
+
+ <div id="log"></div>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-allow-scripts.html b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-allow-scripts.html
new file mode 100644
index 0000000000..6cf3f5a4a8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-allow-scripts.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Script execution in sandbox="allow-scripts" iframe</title>
+ <link rel="author" title="Kinuko Yasuda" href="mailto:kinuko@chromium.org">
+ <link rel="help" href="http://www.w3.org/html/wg/drafts/html/master/browsers.html#sandboxing">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+
+ <body>
+ <h1>Script execution in sandbox="allow-scripts" iframe</h1>
+ <script type="text/javascript">
+ var t = async_test("Running script from sandbox='allow-scripts' iframe is allowed")
+ var called = 0;
+ function calledFromIframe() {
+ called++;
+ }
+ function loaded() {
+ assert_equals(called, 1);
+ t.done();
+ }
+ </script>
+
+ <iframe src="/html/browsers/sandboxing/inner-iframe.html" style="visibility:hidden;display:none" sandbox="allow-scripts allow-same-origin" id="sandboxedframe" onload="loaded();"></iframe>
+
+ <div id="log"></div>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-disallow-popups.html b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-disallow-popups.html
new file mode 100644
index 0000000000..8e4b34eb8b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-disallow-popups.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>window.open in sandbox iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<body>
+<script>
+setup({single_test: true});
+// check that the popup's URL is not loaded
+const uuid = token();
+async function assert_popup_not_loaded() {
+ const response = await fetch(`/fetch/api/resources/stash-take.py?key=${uuid}`);
+ assert_equals(await response.json(), null); // is "loaded" if it loads
+}
+
+// check for message from the iframe
+window.onmessage = e => {
+ assert_equals(e.data, 'null', 'return value of window.open (stringified)');
+ step_timeout(async () => {
+ await assert_popup_not_loaded();
+ done();
+ }, 1000);
+};
+const iframe = document.createElement('iframe');
+iframe.sandbox = 'allow-scripts';
+iframe.srcdoc = `
+ <script>
+ let result;
+ try {
+ result = window.open('/fetch/api/resources/stash-put.py?key=${uuid}&value=loaded', '_blank');
+ } catch(ex) {
+ result = ex;
+ }
+ parent.postMessage(String(result), '*');
+ <\/script>
+`;
+document.body.appendChild(iframe);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-disallow-same-origin.html b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-disallow-same-origin.html
new file mode 100644
index 0000000000..0dae0137ac
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-disallow-same-origin.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Access to sandbox iframe</title>
+ <link rel="author" title="Kinuko Yasuda" href="mailto:kinuko@chromium.org">
+ <link rel="help" href="https://html.spec.whatwg.org/multipage/#sandboxed-origin-browsing-context-flag">
+ <link rel="help" href="https://html.spec.whatwg.org/multipage/#integration-with-idl">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+
+ <body>
+ <h1>Access to sandbox iframe</h1>
+ <script type="text/javascript">
+ var t = async_test("Access to sandbox iframe is disallowed")
+ var called = 0;
+ function calledFromIframe() {
+ called++;
+ }
+ function loaded() {
+ t.step(() => {
+ assert_throws_dom("SecurityError", () => {
+ document.getElementById('sandboxedframe').contentWindow.document;
+ });
+ assert_equals(called, 0);
+ t.done();
+ });
+ }
+ </script>
+
+ <iframe src="/html/browsers/sandboxing/inner-iframe.html" style="visibility:hidden;display:none" sandbox id="sandboxedframe" onload="loaded();"></iframe>
+ </body>
+
+ <div id="log"></div>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-disallow-scripts-via-unsandboxed-popup.tentative.html b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-disallow-scripts-via-unsandboxed-popup.tentative.html
new file mode 100644
index 0000000000..3c8c0b346a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-disallow-scripts-via-unsandboxed-popup.tentative.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+ <script>
+ async_test(t => {
+ let i = document.createElement('iframe');
+ i.sandbox = "allow-same-origin allow-popups allow-popups-to-escape-sandbox";
+ i.srcdoc = `<a target='_blank' rel='opener'
+ href="javascript:window.opener.top.postMessage('FAIL', '*');">Click me!</a>
+ <a target='_blank' rel='opener'
+ href="./resources/post-done-to-opener.html">Click me next!</a>`;
+
+ i.onload = _ => {
+ // Since the frame is sandboxed, but allow-same-origin, we can reach into it to grab the
+ // anchor element to click. We'll click the `javascript:` URL first, then pop up a new
+ // window that posts `DONE`.
+ //
+ // TODO(mkwst): This feels like a race, but it's one that we consistently win when I'm
+ // running the test locally 10,000 times. Good enough!™
+ i.contentDocument.body.querySelectorAll('a')[0].click();
+ i.contentDocument.body.querySelectorAll('a')[1].click();
+ };
+ document.body.appendChild(i);
+
+ window.addEventListener('message', t.step_func(e => {
+ assert_not_equals(e.data, "FAIL");
+ if (e.data == "DONE")
+ t.done();
+ }));
+ }, "Sandboxed => unsandboxed popup");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-disallow-scripts.html b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-disallow-scripts.html
new file mode 100644
index 0000000000..1bc116ada4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-disallow-scripts.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Script execution in sandbox iframe</title>
+ <link rel="author" title="Kinuko Yasuda" href="mailto:kinuko@chromium.org">
+ <link rel="help" href="http://www.w3.org/html/wg/drafts/html/master/browsers.html#sandboxing">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+
+ <body>
+ <h1>Script execution in sandbox iframe</h1>
+ <script type="text/javascript">
+ var t = async_test("Running script from sandbox iframe is disallowed")
+ var called = 0;
+ function calledFromIframe() {
+ called++;
+ }
+ function loaded() {
+ assert_equals(called, 0);
+ t.done();
+ }
+ </script>
+
+ <iframe src="/html/browsers/sandboxing/inner-iframe.html" style="visibility:hidden;display:none" sandbox id="sandboxedframe" onload="loaded();"></iframe>
+
+ <div id="log"></div>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-document-open-mutation.window.js b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-document-open-mutation.window.js
new file mode 100644
index 0000000000..713ca612c5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-document-open-mutation.window.js
@@ -0,0 +1,37 @@
+// Return whether the current context is sandboxed or not. The implementation do
+// not matter much, but might have to change over time depending on what side
+// effect sandbox flag have. Feel free to update as needed.
+const is_sandboxed = () => {
+ try {
+ document.domain = document.domain;
+ return "not sandboxed";
+ } catch (error) {
+ return "sandboxed";
+ }
+};
+
+promise_test(async test => {
+ const message = new Promise(r => window.addEventListener("message", r));
+
+ const iframe_unsandboxed = document.createElement("iframe");
+ document.body.appendChild(iframe_unsandboxed);
+
+ const iframe_sandboxed = document.createElement("iframe");
+ iframe_sandboxed.sandbox = "allow-same-origin allow-scripts";
+ document.body.appendChild(iframe_sandboxed);
+
+ iframe_sandboxed.srcdoc = `
+ <script>
+ parent.frames[0].document.write(\`
+ <script>
+ const is_sandboxed = ${is_sandboxed};
+ window.parent.postMessage(is_sandboxed(), '*');
+ </scr\`+\`ipt>
+ \`);
+ parent.frames[0].document.close();
+ </scr`+`ipt>
+ `;
+ assert_equals((await message).data, "not sandboxed");
+
+}, "Using document.open() against a document from a different window must not" +
+ " mutate the other window's sandbox flags");
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-document-open.html b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-document-open.html
new file mode 100644
index 0000000000..3f754374ba
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-document-open.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>
+ Check sandbox-flags aren't lost after using document.open().
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+promise_test(async test => {
+ let message = new Promise(resolve =>
+ window.addEventListener("message", event => resolve(event.data))
+ );
+
+ let iframe = document.createElement("iframe");
+ iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
+ iframe.setAttribute("src", "./resources/document-open.html")
+ document.body.appendChild(iframe);
+
+ assert_equals(await message, "document-domain-is-disallowed");
+}, "document.open()");
+
+promise_test(async test => {
+ let iframe = document.createElement("iframe");
+ iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
+ iframe.setAttribute("src", "/common/blank.html");
+ let loaded = new Promise(resolve => iframe.onload = resolve);
+ document.body.appendChild(iframe);
+ await loaded;
+
+ let message = new Promise(resolve =>
+ window.addEventListener("message", event => resolve(event.data))
+ );
+
+ iframe.contentDocument.write(`
+ <script>
+ try {
+ document.domain = document.domain;
+ parent.postMessage('document-domain-is-allowed', '*');
+ } catch (error) {
+ parent.postMessage('document-domain-is-disallowed', '*');
+ }
+ </sc`+`ript>
+ `);
+
+ assert_equals(await message, "document-domain-is-disallowed");
+}, "other_document.open()");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-inherited-from-initiator-frame.html b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-inherited-from-initiator-frame.html
new file mode 100644
index 0000000000..ab87fce5e0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-inherited-from-initiator-frame.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Inherit sandbox flags from the initiator's frame</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+// Check sandbox flags are properly inherited when a document initiate a
+// navigation inside another frame that it doesn't own directly.
+
+// This check the sandbox flags defined by the frame. See also the other test
+// about sandbox flags defined by the response (e.g. CSP sandbox):
+// => sandbox-inherited-from-initiators-response.html
+
+// Return a promise, resolving when |element| triggers |event_name| event.
+let future = (element, event_name) => {
+ return new Promise(resolve => {
+ element.addEventListener(event_name, event => resolve(event))
+ });
+};
+
+promise_test(async test => {
+ const iframe_1 = document.createElement("iframe");
+ const iframe_2 = document.createElement("iframe");
+
+ iframe_1.id = "iframe_1";
+ iframe_2.id = "iframe_2";
+
+ const iframe_1_script = encodeURI(`
+ <script>
+ try {
+ document.domain = document.domain;
+ parent.postMessage("not sandboxed", "*");
+ } catch (exception) {
+ parent.postMessage("sandboxed", "*");
+ }
+ </scr`+`ipt>
+ `);
+
+ const iframe_2_script = `
+ <script>
+ const iframe_1 = parent.document.querySelector("#iframe_1");
+ iframe_1.src = "data:text/html,${iframe_1_script}";
+ </scr`+`ipt>
+ `;
+
+ iframe_2.sandbox = "allow-scripts allow-same-origin";
+ iframe_2.srcdoc = iframe_2_script;
+
+ // Insert |iframe_1|. It will load the initial empty document, with no sandbox
+ // flags.
+ const iframe_1_load_1 = future(iframe_1, "load");
+ document.body.appendChild(iframe_1);
+ await iframe_1_load_1;
+
+ // Insert |iframe_2|. It will load with sandbox flags. It will make |iframe_1|
+ // to navigate toward a data-url, which should inherit the sandbox flags.
+ const iframe_1_reply = future(window, "message");
+ document.body.appendChild(iframe_2);
+ const result = await iframe_1_reply;
+
+ assert_equals("sandboxed", result.data);
+})
+</script>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-inherited-from-initiator-response.html b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-inherited-from-initiator-response.html
new file mode 100644
index 0000000000..638f1ba783
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-inherited-from-initiator-response.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Inherit sandbox flags from the initiator's response</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+// Check sandbox flags are properly inherited when a document initiate a
+// navigation inside another frame that it doesn't own directly.
+
+// This check the sandbox flags defined by the response (e.g. CSP sandbox). See
+// also the other test about sandbox flags inherited from the frame.
+// => sandbox-inherited-from-initiators-frame.html
+
+// Return a promise, resolving when |element| triggers |event_name| event.
+let future = (element, event_name) => {
+ return new Promise(resolve => {
+ element.addEventListener(event_name, event => resolve(event))
+ });
+};
+
+promise_test(async test => {
+ const iframe_1 = document.createElement("iframe");
+ const iframe_2 = document.createElement("iframe");
+
+ iframe_1.id = "iframe_1";
+ iframe_2.id = "iframe_2";
+
+ iframe_2.src =
+ "./resources/sandbox-inherited-from-initiator-response-helper.html";
+
+ // Insert |iframe_1|. It will load the initial empty document, with no sandbox
+ // flags.
+ const iframe_1_load_1 = future(iframe_1, "load");
+ document.body.appendChild(iframe_1);
+ await iframe_1_load_1;
+
+ // Insert |iframe_2|. It will load with sandbox flags. It will make |iframe_1|
+ // to navigate toward a data-url, which should inherit the sandbox flags.
+ const iframe_1_reply = future(window, "message");
+ document.body.appendChild(iframe_2);
+ const result = await iframe_1_reply;
+
+ assert_equals("sandboxed", result.data);
+})
+</script>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-inherited-from-required-csp.html b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-inherited-from-required-csp.html
new file mode 100644
index 0000000000..04f485cc66
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-inherited-from-required-csp.html
@@ -0,0 +1,154 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Inherit sandbox from CSP embedded enforcement</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body>
+<script>
+// Check sandbox flags are properly defined when its parent requires them and
+// the child allows it.
+
+const same_origin = get_host_info().HTTP_ORIGIN;
+const cross_origin = get_host_info().HTTP_REMOTE_ORIGIN;
+const check_sandbox_url =
+ "/html/browsers/sandboxing/resources/check-sandbox-flags.html?pipe=";
+const allow_csp_from_star = "|header(Allow-CSP-From,*)";
+
+// Return a promise, resolving when |element| triggers |event_name| event.
+const future = (element, event_name, source) => {
+ return new Promise(resolve => {
+ element.addEventListener(event_name, event => {
+ if (!source || source.contentWindow == event.source)
+ resolve(event)
+ })
+ });
+};
+
+const check_sandbox_script = `
+<script>
+ try {
+ document.domain = document.domain;
+ parent.postMessage("document-domain-is-allowed", "*");
+ } catch (exception) {
+ parent.postMessage("document-domain-is-disallowed", "*");
+ }
+</scr`+`ipt>
+`;
+
+const sandbox_policy = "sandbox allow-scripts allow-same-origin";
+
+// Test using the modern async/await primitives are easier to read/write.
+// However they run sequentially, contrary to async_test. This is the parallel
+// version, to avoid timing out.
+let promise_test_parallel = (promise, description) => {
+ async_test(test => {
+ promise(test)
+ .then(() => {test.done();})
+ .catch(test.step_func(error => { throw error; }));
+ }, description);
+};
+
+promise_test_parallel(async test => {
+ const iframe = document.createElement("iframe");
+ iframe.csp = sandbox_policy;
+
+ // The <iframe> immediately hosts the initial empty document after being
+ // appended into the DOM. It will, as long as its 'src' isn't loaded. That's
+ // why a page do not load is being used.
+ iframe.src = "/fetch/api/resources/infinite-slow-response.py";
+ document.body.appendChild(iframe);
+
+ const iframe_reply = future(window, "message", iframe);
+ iframe.contentDocument.write(check_sandbox_script);
+ const result = await iframe_reply;
+ iframe.remove();
+
+ assert_equals(result.data, "document-domain-is-disallowed");
+}, "initial empty document");
+
+promise_test_parallel(async test => {
+ const iframe = document.createElement("iframe");
+ iframe.src = "data:text/html,dummy";
+
+ const iframe_load_1 = future(iframe, "load");
+ document.body.appendChild(iframe);
+ await iframe_load_1;
+
+ const iframe_load_2 = future(iframe, "load");
+ iframe.csp = sandbox_policy;
+ iframe.src = "about:blank";
+ await iframe_load_2;
+
+ const iframe_reply = future(window, "message", iframe);
+ iframe.contentDocument.write(check_sandbox_script);
+ const result = await iframe_reply;
+
+ assert_equals(result.data, "document-domain-is-disallowed");
+}, "about:blank");
+
+promise_test_parallel(async test => {
+ const iframe = document.createElement("iframe");
+ iframe.csp = sandbox_policy;
+ iframe.src =
+ `data:text/html,${encodeURI(check_sandbox_script)}`;
+
+ const iframe_reply = future(window, "message", iframe);
+ document.body.appendChild(iframe);
+ const result = await iframe_reply;
+
+ assert_equals(result.data, "document-domain-is-disallowed");
+}, "data-url");
+
+promise_test_parallel(async test => {
+ const iframe = document.createElement("iframe");
+ iframe.csp = sandbox_policy;
+ iframe.srcdoc = check_sandbox_script;
+
+ const iframe_reply = future(window, "message", iframe);
+ document.body.appendChild(iframe);
+ const result = await iframe_reply;
+
+ assert_equals(result.data, "document-domain-is-disallowed");
+}, "srcdoc");
+
+promise_test_parallel(async test => {
+ const iframe = document.createElement("iframe");
+ iframe.csp = sandbox_policy;
+
+ const blob = new Blob([check_sandbox_script], { type: "text/html" });
+
+ iframe.src = URL.createObjectURL(blob);
+
+ const iframe_reply = future(window, "message", iframe);
+ document.body.appendChild(iframe);
+ const result = await iframe_reply;
+
+ assert_equals(result.data, "document-domain-is-disallowed");
+}, "blob URL");
+
+promise_test_parallel(async test => {
+ const iframe = document.createElement("iframe");
+ iframe.csp = sandbox_policy;
+ iframe.src = same_origin + check_sandbox_url + allow_csp_from_star;
+
+ const iframe_reply = future(window, "message", iframe);
+ document.body.appendChild(iframe);
+ const result = await iframe_reply;
+
+ assert_equals(result.data, "document-domain-is-disallowed");
+}, "same-origin");
+
+promise_test_parallel(async test => {
+ const iframe = document.createElement("iframe");
+ iframe.csp = sandbox_policy;
+ iframe.src = cross_origin + check_sandbox_url + allow_csp_from_star;
+
+ const iframe_reply = future(window, "message", iframe);
+ document.body.appendChild(iframe);
+ const result = await iframe_reply;
+
+ assert_equals(result.data, "document-domain-is-disallowed");
+}, "cross-origin");
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-initial-empty-document-toward-same-origin.html b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-initial-empty-document-toward-same-origin.html
new file mode 100644
index 0000000000..d1306c9703
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-initial-empty-document-toward-same-origin.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>
+ Check sandbox-flags inheritance in case of javascript window reuse.
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+promise_test(async test => {
+ let message = new Promise(resolve =>
+ window.addEventListener("message", event => resolve(event.data))
+ );
+
+ // Create an initial empty document in the iframe, sandboxed. It will attempt
+ // to load a slow page, but won't have time.
+ let iframe = document.createElement("iframe");
+ iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
+ iframe.src = "/fetch/api/resources/infinite-slow-response.py";
+ document.body.appendChild(iframe);
+
+ // Remove sandbox flags. This should apply to documents committed from
+ // navigations started after this instruction.
+ iframe.removeAttribute("sandbox");
+ iframe.src = "./resources/check-sandbox-flags.html";
+
+ // The window is reused, but the new sandbox flags should be used.
+ assert_equals(await message, "document-domain-is-allowed");
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-javascript-window-open.html b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-javascript-window-open.html
new file mode 100644
index 0000000000..fd21e9bb02
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-javascript-window-open.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>window.open in sandbox iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<body>
+<script>
+promise_test(async test => {
+ let message = new Promise(resolve => {
+ window.addEventListener("message", event => resolve(event.data));
+ });
+ let iframe = document.createElement("iframe");
+ iframe.sandbox = "allow-scripts allow-popups allow-same-origin";
+ iframe.src = "./resources/sandbox-javascript-window-open.html";
+ document.body.appendChild(iframe);
+ assert_equals(await message, "disallow-document-domain");
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-navigation-timing-iframe.tentative.html b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-navigation-timing-iframe.tentative.html
new file mode 100644
index 0000000000..43726e7720
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-navigation-timing-iframe.tentative.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script>
+ let result;
+ try {
+ parent.document.getElementsByClassName('script');
+ result = 'iframe not sandboxed'
+ } catch (e) {
+ result = 'iframe sandboxed(' + e.message + ')';
+ }
+ window.onmessage = m => {
+ window.parent.postMessage({
+ result: result
+ }, '*');
+ };
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-navigation-timing.tentative.html b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-navigation-timing.tentative.html
new file mode 100644
index 0000000000..686f1c0c9f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-navigation-timing.tentative.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Sandbox Navigation Timing</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<html></html>
+<script>
+ const sandboxUrl = location.pathname.substring(0, location.pathname.lastIndexOf('/') + 1) + 'sandbox-navigation-timing-iframe.tentative.html';
+ async_test(t => {
+ const iframe = document.createElement('iframe');
+ iframe.src = sandboxUrl;
+ document.body.appendChild(iframe); // Navigation starts; value of sandbox flags locked on.
+ // This should not affect the sandbox value used for both about:blank document
+ // and the final document in iframe.
+ iframe.sandbox = 'allow-scripts';
+ const iframeAboutBlankDocument = iframe.contentDocument;
+
+ iframe.onload = t.step_func(() => {
+ const iframeAboutBlankContents = iframeAboutBlankDocument.querySelectorAll('body');
+ assert_equals(iframeAboutBlankContents[0].tagName, "BODY",
+ "about:blank document's contents should still be accessible");
+
+ iframe.contentWindow.postMessage("is iframe sandboxed?", "*");
+ });
+ window.onmessage = t.step_func_done(e => {
+ assert_equals(e.data.result, 'iframe not sandboxed');
+ });
+ }, 'setting sandbox attribute should not affect current document in iframe');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-new-execution-context-iframe.html b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-new-execution-context-iframe.html
new file mode 100644
index 0000000000..801e78f9c0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-new-execution-context-iframe.html
@@ -0,0 +1,5 @@
+<body>
+ <script>
+ Object.getPrototypeOf(document).changeFromSandboxedIframe = "change from sandboxed iframe";
+ </script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-new-execution-context.html b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-new-execution-context.html
new file mode 100644
index 0000000000..dc1953aee6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-new-execution-context.html
@@ -0,0 +1,39 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Reuse of iframe about:blank document execution context</title>
+ <link rel="author" title="Dan Clark" href="mailto:daniec@microsoft.com">
+ <link rel="help" href="http://www.w3.org/html/wg/drafts/html/master/browsers.html#sandboxing">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+
+ <body>
+ <h1>Reuse of iframe about:blank document execution context in sandbox="allow-scripts" iframe</h1>
+ <script type="text/javascript">
+ async_test(t => {
+ let iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+
+ let iframeAboutBlankDocument = iframe.contentDocument;
+ assert_equals(iframeAboutBlankDocument.URL, "about:blank");
+
+ iframe.sandbox = "allow-scripts";
+ iframe.src = './sandbox-new-execution-context-iframe.html';
+
+ iframe.onload = t.step_func_done(() => {
+ assert_equals(iframe.contentDocument, null,
+ "New document in sandboxed iframe should have opaque origin");
+
+ assert_equals(Object.getPrototypeOf(iframeAboutBlankDocument).changeFromSandboxedIframe, undefined,
+ "Sandboxed iframe contents should not have been able to mess with type system of about:blank document");
+
+ let iframeAboutBlankContents = iframeAboutBlankDocument.querySelectorAll('body');
+ assert_equals(iframeAboutBlankContents[0].tagName, "BODY",
+ "about:blank document's contents should still be accessible");
+ });
+ },"iframe with sandbox should load with new execution context");
+ </script>
+ <div id="log"></div>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-parse-noscript-ref.html b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-parse-noscript-ref.html
new file mode 100644
index 0000000000..9cf92768f7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-parse-noscript-ref.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>noscript parsing when sandbox disables scripting</title>
+<iframe srcdoc="PASS" sandbox></iframe>
+<iframe srcdoc="PASS" sandbox></iframe>
+<iframe srcdoc="P<b>AS</b>S" sandbox></iframe>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-parse-noscript.html b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-parse-noscript.html
new file mode 100644
index 0000000000..bb7ced0a14
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-parse-noscript.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>noscript parsing when sandbox disables scripting</title>
+<link rel=match href=/html/browsers/sandboxing/sandbox-parse-noscript-ref.html>
+<iframe srcdoc="<noscript>PASS</noscript>" sandbox></iframe>
+<iframe src="noscript-iframe.html" sandbox></iframe>
+<iframe srcdoc="<noscript>P<b>AS</b>S</noscript>" sandbox></iframe>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/sandbox-window-open-srcdoc.html b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-window-open-srcdoc.html
new file mode 100644
index 0000000000..6fbff6df82
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/sandbox-window-open-srcdoc.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>window.open("about:srcdoc") from a sandboxed iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+// Check what happens when executing window.open("about:srcdoc") from a
+// sandboxed iframe. Srcdoc can't be loaded in the main frame. It should
+// result in an error page. The error page should be cross-origin with the
+// opener.
+//
+// This test covers an interesting edge case. A main frame should inherit
+// sandbox flags. However the document loaded is an internal error page. This
+// might trigger some assertions, especially if the implementation wrongly
+// applies the sandbox flags of the opener to the internal error page document.
+//
+// This test is mainly a coverage test. It passes if it doesn't crash.
+async_test(test => {
+ let iframe = document.createElement("iframe");
+ iframe.sandbox = "allow-scripts allow-popups allow-same-origin";
+ iframe.srcdoc = `
+ <script>
+ let w = window.open();
+ onunload = () => w.close();
+
+ let notify = () => {
+ try {
+ w.origin; // Will fail after navigating to about:srcdoc.
+ parent.postMessage("pending", "*");
+ } catch (e) {
+ parent.postMessage("done", "*");
+ };
+ };
+
+ addEventListener("message", notify);
+ notify();
+
+ w.location = "about:srcdoc"; // Error page.
+ </scr`+`ipt>
+ `;
+
+ let closed = false;
+ addEventListener("message", event => {
+ closed = (event.data === "done");
+ iframe.contentWindow.postMessage("ping","*");
+ });
+
+ document.body.appendChild(iframe);
+ test.step_wait_func_done(()=>closed);
+}, "window.open('about:srcdoc') from sandboxed srcdoc doesn't crash.");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/sandboxing/window-open-blank-from-different-initiator.html b/testing/web-platform/tests/html/browsers/sandboxing/window-open-blank-from-different-initiator.html
new file mode 100644
index 0000000000..91817c3db4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/sandboxing/window-open-blank-from-different-initiator.html
@@ -0,0 +1,90 @@
+<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>
+
+// This is a regression test for https://crbug.com/1170038.
+//
+// A document creates a popup and makes it navigate elsewhere. The navigation
+// will never commit. The popup has not completed any real load outside of the
+// initial empty document. Then from a different window with a different CSP
+// policy, make it navigate to about:blank.
+//
+// Web browser behavior might change depending on whether a pending navigation
+// exists for in the popup or not. Both are tested here.
+
+const same_origin = get_host_info().HTTP_ORIGIN;
+
+// Return a promise, resolving when |element| triggers |event_name| event.
+const future = (element, event_name) => {
+ return new Promise(resolve => element.addEventListener(event_name, resolve));
+};
+
+// `createNewPopup` is a function returning a new window that has not committed
+// any real load in its frame, outside of the initial empty document. The two
+// tests below vary depending on whether there is a pending navigation in the
+// frame or not.
+const runTest = (description, createNewPopup) => {
+ promise_test(async test => {
+ // Open a same-origin window with a different CSP.
+ const executor_path =
+ "/html/browsers/sandboxing/resources/execute-postmessage.html?pipe=";
+ const csp = "|header(Content-Security-Policy, " +
+ "sandbox" +
+ " allow-scripts" +
+ " allow-popups" +
+ " allow-same-origin" +
+ " allow-popups-to-escape-sandbox";
+ const executor = window.open(same_origin + executor_path + csp);
+ const executor_reply = await future(window, "message");
+ assert_equals(executor_reply.data, "ready");
+
+ const popup_name = token();
+ const popup = await createNewPopup(test, popup_name);
+
+ // Request the first real load from a DIFFERENT window with different CSPs.
+ const first_real_load = future(popup, "load");
+ executor.postMessage(`window.open("about:blank", "${popup_name}");`);
+ await first_real_load;
+
+ // The new blank document in the popup must inherit CSPs from |executor|:
+ let is_sandboxed = future(window, "message");
+ popup.document.write(`
+ <script>
+ try {
+ document.domain = document.domain;
+ opener.opener.postMessage("not sandboxed", "*");
+ } catch (error) {
+ opener.opener.postMessage("sandboxed", "*");
+ }
+ </scr`+`ipt>
+ `);
+ assert_equals((await is_sandboxed).data, "sandboxed");
+ }, description);
+}
+
+// Open a new window and start loading from an unresponsive server. The frame
+// will be left with the initial empty document and a pending navigation.
+runTest("One pending navigation", async (test, popup_name) => {
+ const unresponsive_path =
+ "/fetch/api/resources/infinite-slow-response.py";
+ return window.open(same_origin + unresponsive_path, popup_name);
+});
+
+// Open a new window and start loading. The response is a 204 and the navigation
+// is canceled. As a result, the frame will be left with the initial empty
+// document and NO pending navigation.
+runTest("No pending navigation", async (test, popup_name) => {
+ const no_content_path = "/common/blank.html?pipe=status(204)"
+ const popup = window.open(same_origin + no_content_path, popup_name);
+
+ // Unfortunately, there are no web API to detect a navigation has been
+ // canceled. Waiting using setTimeout is the only possible way to wait for it.
+ await new Promise(r => test.step_timeout(r, 1000));
+
+ return popup;
+});
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/BarProp.window.js b/testing/web-platform/tests/html/browsers/the-window-object/BarProp.window.js
new file mode 100644
index 0000000000..266779ee0b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/BarProp.window.js
@@ -0,0 +1,59 @@
+function assert_barProps(barPropObjects, visible) {
+ let lastBarProp = undefined;
+ for (const currentBarProp of barPropObjects) {
+ assert_not_equals(currentBarProp, lastBarProp, "BarBrop objects of different properties are identical");
+ assert_equals(currentBarProp.visible, visible, "a BarProp's visible is wrong");
+ lastBarProp = currentBarProp;
+ }
+}
+
+function assert_identical_barProps(barProps, w, oldBarPropObjects, visible) {
+ barProps.map(val => w[val]).map((val, index) => {
+ assert_equals(val, oldBarPropObjects[index], "BarProp identity not preserved");
+ });
+ assert_barProps(oldBarPropObjects, visible);
+}
+
+async_test(t => {
+ const frame = document.body.appendChild(document.createElement("iframe")),
+ frameW = frame.contentWindow,
+ barProps = ["locationbar", "menubar", "personalbar", "scrollbars", "statusbar", "toolbar"],
+ barPropObjects = barProps.map(val => frameW[val]);
+
+ assert_barProps(barPropObjects, true);
+ frame.remove();
+ assert_identical_barProps(barProps, frameW, barPropObjects, false);
+ t.step_timeout(() => {
+ assert_identical_barProps(barProps, frameW, barPropObjects, false);
+ t.done();
+ }, 0);
+}, "BarBrop objects of a nested Window");
+
+async_test(t => {
+ const openee = window.open("/common/blank.html"),
+ barProps = ["locationbar", "menubar", "personalbar", "scrollbars", "statusbar", "toolbar"],
+ barPropObjects = barProps.map(val => openee[val]);
+
+ // This is used to demonstrate that the Document is replaced while the global object (not the
+ // global this object) stays the same
+ openee.tiedToGlobalObject = openee.document;
+
+ assert_barProps(barPropObjects, true);
+ openee.onload = t.step_func(() => {
+ assert_own_property(openee, "tiedToGlobalObject");
+ assert_not_equals(openee.tiedToGlobalObject, openee.document);
+
+ assert_identical_barProps(barProps, openee, barPropObjects, true);
+
+ openee.onpagehide = t.step_func(() => {
+ assert_identical_barProps(barProps, openee, barPropObjects, true);
+ t.step_timeout(() => {
+ assert_identical_barProps(barProps, openee, barPropObjects, false);
+ t.done();
+ }, 0);
+ });
+
+ openee.close();
+ assert_identical_barProps(barProps, openee, barPropObjects, true);
+ });
+}, "BarProp objects of an auxiliary Window");
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/Document-defaultView.html b/testing/web-platform/tests/html/browsers/the-window-object/Document-defaultView.html
new file mode 100644
index 0000000000..dbc75d30b2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/Document-defaultView.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Document#defaultView</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+test(function() {
+ assert_equals(document.defaultView, window);
+}, "Document in a browsing context");
+
+test(function() {
+ var d = new Document();
+ assert_equals(d.defaultView, null);
+}, "Document created with the Document constructor");
+
+test(function() {
+ var d = document.implementation.createDocument(null, null);
+ assert_equals(d.defaultView, null);
+}, "Document created with createDocument");
+
+test(function() {
+ var d = document.implementation.createHTMLDocument();
+ assert_equals(d.defaultView, null);
+}, "Document created with createHTMLDocument");
+
+test(function() {
+ var parser = new DOMParser();
+ var d = parser.parseFromString("<foo\/\>", "application/xml");
+ assert_equals(d.defaultView, null);
+}, "Document created with XML DOMParser");
+
+test(function() {
+ var parser = new DOMParser();
+ var d = parser.parseFromString("bar", "text/html");
+ assert_equals(d.defaultView, null);
+}, "Document created with HTML DOMParser");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/Window-document.html b/testing/web-platform/tests/html/browsers/the-window-object/Window-document.html
new file mode 100644
index 0000000000..9b27f5f7c7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/Window-document.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Window#document</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+async_test(function() {
+ var URL = "/common/blank.html";
+
+ var iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ var initialWindow = iframe.contentWindow;
+ var initialDocument = initialWindow.document;
+ assert_equals(initialDocument.URL, "about:blank");
+ iframe.src = URL;
+ iframe.onload = this.step_func_done(function() {
+ assert_equals(iframe.contentWindow, initialWindow);
+ assert_equals(initialDocument.URL, "about:blank");
+ var loadedDocument = initialWindow.document;
+ assert_equals(loadedDocument.URL, location.href.replace(location.pathname, URL));
+ assert_not_equals(initialDocument, loadedDocument);
+ });
+}, "Document in a browsing context");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/indexed-browsing-contexts-01.html b/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/indexed-browsing-contexts-01.html
new file mode 100644
index 0000000000..9710d15fb2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/indexed-browsing-contexts-01.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: the browsing contexts must be sorted in the order that their containers were inserted into the Document</title>
+<link rel="author" title="Intel" href="http://www.intel.com/" />
+<link rel="help" href="https://html.spec.whatwg.org/multipage/multipage/browsers.html#accessing-other-browsing-contexts" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+
+var t1 = async_test("The window's length must return the number of child browsing contexts(in iframe)");
+function on_load1(fr) {
+ t1.step(function () {
+ var doc = fr.contentDocument;
+ var fr3 = doc.createElement("iframe");
+ fr3.setAttribute("id", "fr3");
+ doc.body.insertBefore(fr3, doc.getElementById("tbl"));
+
+ assert_equals(fr.contentWindow.length, 3, "The window.length should be 3.");
+ assert_array_equals([fr.contentWindow[0].frameElement, fr.contentWindow[1].frameElement, fr.contentWindow[2].frameElement],
+ [fr.contentDocument.getElementById("fr4"), fr.contentDocument.getElementById("fr5"), fr.contentDocument.getElementById("fr3")],
+ "The child browsing contexts must be sorted in the order that their containers were inserted into the Document.");
+ });
+ t1.done();
+}
+
+var t2 = async_test("The window's length must return zero if it has no child browsing context");
+function on_load2(fr) {
+ t2.step(function () {
+ assert_equals(fr.contentWindow.length, 0, "The window.length should be 0.");
+ });
+ t2.done();
+}
+
+</script>
+<iframe id="fr1" src="test1.html" style="display:none" onload="on_load1(this)"></iframe>
+<iframe id="fr2" src="test2.html" style="display:none" onload="on_load2(this)"></iframe>
+<script>
+
+test(function () {
+ assert_equals(window.length, 2, "The window.length should be 2.");
+ assert_array_equals([window[0].frameElement, window[1].frameElement],
+ [document.getElementById("fr1"), document.getElementById("fr2")],
+ "The child browsing contexts must be sorted in the tree order.");
+}, "The window's length must return the number of child browsing contexts");
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/indexed-browsing-contexts-02.html b/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/indexed-browsing-contexts-02.html
new file mode 100644
index 0000000000..d09c944fd8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/indexed-browsing-contexts-02.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<head>
+ <meta charset="utf-8">
+ <title>HTML Test: the browsing contexts created by various container elements</title>
+ <link rel="author" title="Intel" href="http://www.intel.com/" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script>
+
+ var t1 = async_test("Accessing child browsing contexts 1");
+ var t2 = async_test("Accessing child browsing contexts 2");
+ var t3 = async_test("Accessing child browsing contexts 3");
+ function on_load() {
+ //Child browsing contexts created by iframe, object and embed elements.
+ t1.step(function () {
+ assert_equals(window.length, 3, "The top browsing context should have 3 child browsing contexts.");
+ });
+ t1.step(function () {
+ assert_equals(window[0].name, "win1", "The browsing context name should be 'win1'.");
+ assert_equals(window[1].name, "win2", "The browsing context name should be 'win2'.");
+ assert_equals(window[2].name, "win3", "The browsing context name should be 'win3'.");
+ });
+ t1.done();
+
+ //Child browsing contexts created by frame elements.
+ t2.step(function () {
+ assert_equals(document.getElementById("fr").contentWindow.length, 2,
+ "The child browsing context created by the iframe element should have 2 child browsing contexts.");
+ });
+ t2.step(function () {
+ assert_equals(document.getElementById("fr").contentWindow[0].name, "win4",
+ "The browsing context name should be 'win4'.");
+ assert_equals(document.getElementById("fr").contentWindow[1].name, "win5",
+ "The browsing context name should be 'win5'.");
+ });
+ t2.done();
+
+ //The child browsing context will be removed if the data attribute of the associated object element is removed.
+ t3.step(function () {
+ document.getElementById("obj").removeAttribute("type");
+ assert_equals(window.length, 3, "The top browsing context should have 3 child browsing contexts.");
+ document.getElementById("obj").removeAttribute("data");
+ assert_equals(window.length, 3, "The top browsing context should have 3 child browsing contexts.");
+
+ setTimeout(function () {
+ assert_equals(window.length, 2, "The top browsing context should have 2 child browsing contexts.");
+ }, 1);
+ });
+ t3.done();
+ }
+
+ </script>
+</head>
+<body onload="on_load()">
+ <div id="log"></div>
+ <div style="display:none">
+ <iframe id="fr" name="win1" src="test3.html"></iframe>
+ <object id="obj" name="win2" type="text/html" data="about:blank"></object>
+ <object type="image/png" src="/images/green.png"></object>
+ <embed id="emb" name="win3" type="image/svg+xml" src="/images/green.svg"></embed>
+ </div>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/indexed-browsing-contexts-03.html b/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/indexed-browsing-contexts-03.html
new file mode 100644
index 0000000000..1548891175
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/indexed-browsing-contexts-03.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<head>
+ <meta charset="utf-8">
+ <title>HTML Test: indexed property of a Window object</title>
+ <link rel="author" title="Intel" href="http://www.intel.com/" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script>
+
+ var t1 = async_test("Indexed child browsing contexts");
+ function on_load() {
+ t1.step(function () {
+ assert_equals(window[0], document.getElementsByTagName("object")[0].contentWindow,
+ "The first child browsing context's container should be the object element.");
+ assert_equals(window[1], document.getElementsByTagName("iframe")[0].contentWindow,
+ "The second child browsing context's container should be the iframe element.");
+ });
+ t1.done();
+ }
+
+ </script>
+</head>
+<body onload="on_load()">
+ <div id="log"></div>
+ <div style="display:none">
+ <div id="0"></div>
+ <object name="0" type="text/html" data="test2.html"></object>
+ <iframe name="0" src="about:blank"></iframe>
+ </div>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/iterator.html b/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/iterator.html
new file mode 100644
index 0000000000..76dc7dbae6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/iterator.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>window[@@iterator]</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(function() {
+ assert_false(Symbol.iterator in window);
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/test1.html b/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/test1.html
new file mode 100644
index 0000000000..f85f90f7c6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/test1.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: child browsing contexts created by iframe elements</title>
+<link rel="author" title="Intel" href="http://www.intel.com/" />
+<table id="tbl">
+ <tr>
+ <td>
+ <iframe id="fr4" src=""></iframe>
+ </td>
+ </tr>
+ <iframe id="fr5" src="about:blank"></iframe>
+</table>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/test2.html b/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/test2.html
new file mode 100644
index 0000000000..d6a16647fe
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/test2.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: child browsing contexts created by object and embed elements</title>
+<link rel="author" title="Intel" href="http://www.intel.com/" />
+<object type="image/png" src="/images/green.png"></object>
+<embed type="image/png" src="/images/green.png"></embed>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/test3.html b/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/test3.html
new file mode 100644
index 0000000000..a62fdbaae7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/test3.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: child browsing contexts created by frame elements</title>
+<link rel="author" title="Intel" href="http://www.intel.com/" />
+<frameset>
+ <frame name="win4"></frame>
+ <frame name="win5"></frame>
+</frameset>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/window_length.html b/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/window_length.html
new file mode 100644
index 0000000000..c9559b531a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/accessing-other-browsing-contexts/window_length.html
@@ -0,0 +1,51 @@
+<!doctype html>
+<title>window.length</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+var iframe;
+var subframe;
+var other_window;
+test(function() {assert_equals(window.length, 0)}, "No child browsing contexts");
+test(function() {
+ iframe = document.createElement("iframe");
+ assert_equals(window.length, 0)
+}, "iframe not inserted into the document");
+
+test(function() {
+ document.body.appendChild(iframe);
+ assert_equals(window.length, 1)
+}, "One iframe inserted into the document");
+
+test(function() {
+ subframe = document.createElement("iframe");
+ iframe.contentDocument.body.appendChild(subframe);
+ assert_equals(window.length, 1);
+}, "Child browsing context has a child browsing context");
+
+test(function() {
+ try {
+ assert_equals(iframe.contentWindow.length, 1);
+ } finally {
+ subframe.parentNode.removeChild(subframe);
+ }
+}, "window.length in child frame");
+
+test(function() {
+ iframe.parentNode.removeChild(iframe);
+ other_window = window.open();
+ assert_equals(window.length, 0);
+ assert_equals(other_window.length, 0);
+}, "Opened window")
+
+test(function() {
+ other_window.document.body.appendChild(iframe);
+ try {
+ assert_equals(other_window.length, 1);
+ } finally {
+ other_window.close();
+ }
+}, "Iframe in opened window")
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/close-method.window.js b/testing/web-platform/tests/html/browsers/the-window-object/close-method.window.js
new file mode 100644
index 0000000000..048c5f7236
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/close-method.window.js
@@ -0,0 +1,39 @@
+function assert_closed_opener(w, closed, opener) {
+ assert_equals(w.closed, closed);
+ assert_equals(w.opener, opener);
+}
+
+async_test(t => {
+ const openee = window.open();
+ assert_closed_opener(openee, false, self);
+ openee.onpagehide = t.step_func(() => {
+ assert_closed_opener(openee, true, self);
+ t.step_timeout(() => {
+ assert_closed_opener(openee, true, null);
+ t.done();
+ }, 0);
+ });
+ openee.close();
+ assert_closed_opener(openee, true, self);
+}, "window.close() queues a task to discard, but window.closed knows immediately");
+
+async_test(t => {
+ const openee = window.open("", "greatname");
+ assert_closed_opener(openee, false, self);
+ openee.close();
+ assert_closed_opener(openee, true, self);
+ const openee2 = window.open("", "greatname");
+ assert_not_equals(openee, openee2);
+ assert_closed_opener(openee, true, self); // Ensure second window.open() call was synchronous
+ openee2.onpagehide = t.step_func(() => {
+ assert_closed_opener(openee2, true, self);
+ t.step_timeout(() => {
+ assert_closed_opener(openee, true, null);
+ assert_closed_opener(openee2, true, null);
+ t.done();
+ }, 0);
+ });
+ openee2.close();
+ assert_closed_opener(openee, true, self); // Ensure second close() call was synchronous
+ assert_closed_opener(openee2, true, self);
+}, "window.close() affects name targeting immediately");
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/closed-attribute.window.js b/testing/web-platform/tests/html/browsers/the-window-object/closed-attribute.window.js
new file mode 100644
index 0000000000..88a3beba6f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/closed-attribute.window.js
@@ -0,0 +1,69 @@
+// META: script=/common/get-host-info.sub.js
+
+function closedTest(newWindow, closeNewWindowsBrowsingContext) {
+ assert_equals(newWindow.closed, false);
+ closeNewWindowsBrowsingContext();
+ assert_equals(newWindow.closed, true);
+}
+
+test(() => {
+ const frame = document.body.appendChild(document.createElement("iframe"));
+ closedTest(frame.contentWindow, () => frame.remove());
+}, "closed and same-origin nested browsing context");
+
+test(() => {
+ const openee = window.open();
+ closedTest(openee, () => openee.close());
+
+ // close() is a no-op once "is closing" is set
+ openee.close();
+ assert_equals(openee.closed, true);
+}, "closed/close() and same-origin auxiliary browsing context");
+
+const support = new URL("support/closed.html", location.href).pathname;
+[
+ {
+ type: "cross-origin",
+ url: `${get_host_info().HTTP_REMOTE_ORIGIN}${support}`
+ },
+ {
+ type: "cross-site",
+ url: `${get_host_info().HTTP_NOTSAMESITE_ORIGIN}${support}`
+ }
+].forEach(val => {
+ async_test(t => {
+ const frame = document.createElement("iframe"),
+ ident = `${val.type}-nested-bc`;
+ frame.src = `${val.url}?window=parent&ident=${ident}`;
+ const listener = t.step_func(e => {
+ if (e.data === ident) {
+ closedTest(frame.contentWindow, () => frame.remove());
+ self.removeEventListener("message", listener);
+ t.done();
+ }
+ });
+ // Use a message event rather than onload for consistency with auxiliary browsing contexts.
+ self.addEventListener("message", listener);
+ document.body.append(frame);
+ }, `closed and ${val.type} nested browsing context`);
+
+ async_test(t => {
+ const ident = `${val.type}-auxiliary-bc`,
+ support = new URL("support/closed.html", location.href).pathname,
+ openee = window.open(`${val.url}?window=opener&ident=${ident}`),
+ listener = t.step_func(e => {
+ if (e.data === ident) {
+ closedTest(openee, () => openee.close());
+
+ // close() is a no-op once "is closing" is set
+ openee.close();
+ assert_equals(openee.closed, true);
+
+ self.removeEventListener("message", listener);
+ t.done();
+ }
+ });
+ // As there's no cross-origin onload, use a message event.
+ self.addEventListener("message", listener);
+ }, `closed/close() and ${val.type} auxiliary browsing context`);
+});
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/defaultstatus.html b/testing/web-platform/tests/html/browsers/the-window-object/defaultstatus.html
new file mode 100644
index 0000000000..f683f130cd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/defaultstatus.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>window.defaultStatus and window.defaultstatus are not supported</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel="help" href="https://developer.mozilla.org/en-US/docs/Web/API/Window/defaultStatus">
+<link rel="help" href="https://crbug.com/692835">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+test(() => {
+ assert_false(window.hasOwnProperty('defaultStatus'));
+ assert_false(window.hasOwnProperty('defaultstatus'));
+ assert_equals(window.defaultStatus,undefined);
+ assert_equals(window.defaultstatus,undefined);
+}, "The window.defaultStatus and window.defaultstatus attributes are not supported");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/document-attribute.window.js b/testing/web-platform/tests/html/browsers/the-window-object/document-attribute.window.js
new file mode 100644
index 0000000000..f13acdb8a3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/document-attribute.window.js
@@ -0,0 +1,15 @@
+async_test(t => {
+ const frame = document.createElement("iframe");
+ frame.onload = t.step_func(() => {
+ const frameW = frame.contentWindow,
+ frameD = frame.contentDocument;
+ assert_equals(frameW.document, frameD);
+ frame.remove();
+ assert_equals(frameW.document, frameD);
+ t.step_timeout(() => {
+ assert_equals(frameW.document, frameD);
+ t.done();
+ }, 100);
+ });
+ document.body.append(frame);
+}, "Window object's document IDL attribute and discarding the browsing context");
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/focus.window.js b/testing/web-platform/tests/html/browsers/the-window-object/focus.window.js
new file mode 100644
index 0000000000..6ec7feee28
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/focus.window.js
@@ -0,0 +1,15 @@
+async_test(t => {
+ const input = document.body.appendChild(document.createElement("input"));
+ input.onfocus = t.step_func(() => {
+ const frame = document.body.appendChild(document.createElement("iframe")),
+ frameW = frame.contentWindow;
+ frameW.onfocus = t.unreached_func();
+ frame.remove();
+ frameW.focus();
+ t.step_timeout(() => {
+ assert_equals(document.activeElement, input);
+ t.done();
+ }, 100);
+ });
+ input.focus();
+});
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_1-1.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_1-1.html
new file mode 100644
index 0000000000..217608e46e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_1-1.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<iframe></iframe>
+<script>
+var t = opener.t;
+
+onload = t.step_func(function() {
+ setTimeout(t.step_func(function() {
+ var history_length = history.length;
+ var iframe = document.getElementsByTagName("iframe")[0];
+ iframe.onload = t.step_func(function() {
+ opener.assert_equals(history.length, history_length + 1);
+ iframe.parentNode.removeChild(iframe);
+ opener.assert_equals(history.length, history_length);
+ t.done();
+ window.close();
+ });
+ iframe.src = "discard_iframe_history_1-2.html;";
+ }), 100);
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_1-2.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_1-2.html
new file mode 100644
index 0000000000..b43598f2cd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_1-2.html
@@ -0,0 +1,2 @@
+<!doctype html>
+Filler text
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_1.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_1.html
new file mode 100644
index 0000000000..4d1e473fc0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_1.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<title>Removing iframe from document removes it from history</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+var w = window.open("discard_iframe_history_1-1.html");
+</script>
+
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_2-1.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_2-1.html
new file mode 100644
index 0000000000..61e5891ebd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_2-1.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<iframe></iframe>
+<script>
+var t = opener.t;
+
+onload = t.step_func(function() {
+ setTimeout(t.step_func(function() {
+ var history_length = history.length;
+ var iframe = document.getElementsByTagName("iframe")[0];
+ iframe.onload = t.step_func(function() {
+ setTimeout(t.step_func(function() {
+ opener.assert_equals(history.length, history_length + 1, "History length before iframe removal");
+ document.body.innerHTML = "";
+ opener.assert_equals(history.length, history_length, "History length after iframe removal");
+ t.done();
+ window.close();
+ }), 100);
+ });
+ iframe.src = "discard_iframe_history_1-2.html";
+ }), 100);
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_2.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_2.html
new file mode 100644
index 0000000000..89d0fb4c64
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_2.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<title>Removing iframe from document via innerHTML removes it from history</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+var w = window.open("discard_iframe_history_2-1.html");
+</script>
+
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_3-1.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_3-1.html
new file mode 100644
index 0000000000..de3f075d64
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_3-1.html
@@ -0,0 +1,21 @@
+<script>
+history_lengths = [];
+
+var t = opener.t;
+
+push_length = t.step_func(function () {
+ history_lengths.push(history.length)
+});
+
+do_test = t.step_func(function () {
+ try {
+ var start_length = history_lengths[0];
+ expected = [start_length, start_length + 1, start_length];
+ opener.assert_array_equals(history_lengths, expected);
+ t.done();
+ } finally {
+ window.close();
+ }
+});
+</script>
+<iframe src="discard_iframe_history_3-2.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_3-2.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_3-2.html
new file mode 100644
index 0000000000..95f9fce5d8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_3-2.html
@@ -0,0 +1,4 @@
+<a href="discard_iframe_history_3-3.html" onclick="parent.push_length()">Click me</a>
+<script>
+onload = function() {setTimeout(parent.t.step_func(function() {document.links[0].click()}), 100)}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_3-3.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_3-3.html
new file mode 100644
index 0000000000..4672b0ec30
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_3-3.html
@@ -0,0 +1,4 @@
+<button onclick="var p = parent; p.push_length(); frameElement.parentNode.removeChild(frameElement); p.push_length(); p.do_test();">Click me</button>
+<script>
+onload = function() {setTimeout(parent.t.step_func(function() {document.getElementsByTagName("button")[0].click()}), 100)}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_3.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_3.html
new file mode 100644
index 0000000000..38dbdbd0fa
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_3.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<title>Removing iframe from document removes it from history</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+var w = window.open("discard_iframe_history_3-1.html");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_4-1.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_4-1.html
new file mode 100644
index 0000000000..1b5726cdcd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_4-1.html
@@ -0,0 +1,21 @@
+<script>
+history_lengths = [];
+
+var t = opener.t;
+
+push_length = t.step_func(function () {
+ history_lengths.push(history.length)
+});
+
+do_test = t.step_func(function () {
+ try {
+ var start_length = history_lengths[0];
+ expected = [start_length, start_length + 1, start_length];
+ opener.assert_array_equals(history_lengths, expected);
+ t.done();
+ } finally {
+ window.close();
+ }
+});
+</script>
+<iframe src="discard_iframe_history_4-2.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_4-2.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_4-2.html
new file mode 100644
index 0000000000..979b2b28e4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_4-2.html
@@ -0,0 +1,4 @@
+<a href="discard_iframe_history_4-3.html" onclick="parent.push_length()">Click me</a>
+<script>
+onload = function() {setTimeout(parent.t.step_func(function() {document.links[0].click()}), 100)}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_4-3.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_4-3.html
new file mode 100644
index 0000000000..b4308f439a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_4-3.html
@@ -0,0 +1,4 @@
+<button onclick="var p = parent; p.push_length(); frameElement.parentNode.innerHTML = ''; p.push_length(); p.do_test();">Click me</button>
+<script>
+onload = function() {setTimeout(parent.t.step_func(function() {document.getElementsByTagName("button")[0].click()}), 100)}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_4.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_4.html
new file mode 100644
index 0000000000..ffd444e3bf
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/discard_iframe_history_4.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<title>Removing iframe from document removes it from history</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+var w = window.open("discard_iframe_history_4-1.html");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_1-1.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_1-1.html
new file mode 100644
index 0000000000..9969427989
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_1-1.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<iframe></iframe>
+<script>
+var t = opener.t;
+var iframe = document.getElementsByTagName("iframe")[0];
+var history_length;
+
+function load_frame(src) {
+ history_length = history.length;
+ iframe.src = src;
+ var button = document.getElementsByTagName("button")[0];
+ button.parentNode.removeChild(button);
+}
+
+remove_frame = t.step_func(function() {
+ try {
+ opener.assert_equals(history.length, history_length + 1, "History length after loading page in iframe");
+ iframe.parentNode.removeChild(iframe);
+ opener.assert_equals(history.length, history_length, "History length after removing iframe");
+ t.done();
+ } finally {
+ window.close();
+ }
+});
+
+</script>
+<button onclick="load_frame('discard_iframe_history_1-2.html')">Click here</button>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_1-2.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_1-2.html
new file mode 100644
index 0000000000..8c3d1a9dad
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_1-2.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<button onclick="parent.remove_frame()">Click here</button>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_1-manual.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_1-manual.html
new file mode 100644
index 0000000000..ca9dd91f2e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_1-manual.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<title>Removing iframe from document removes it from history</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+setup({timeout:3600000})
+var t = async_test();
+var w = window.open("discard_iframe_history_1-1.html");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_2-1.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_2-1.html
new file mode 100644
index 0000000000..bc01cae88c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_2-1.html
@@ -0,0 +1,19 @@
+<script>
+history_lengths = [];
+
+function push_length() {
+ history_lengths.push(history.length)
+}
+
+do_test = opener.t.step_func(function () {
+ try {
+ var start_length = history_lengths[0];
+ expected = [start_length, start_length + 1, start_length];
+ opener.assert_array_equals(history_lengths, expected);
+ opener.t.done();
+ } finally {
+ window.close();
+ }
+});
+</script>
+<iframe src="discard_iframe_history_2-2.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_2-2.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_2-2.html
new file mode 100644
index 0000000000..b25bf5f001
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_2-2.html
@@ -0,0 +1 @@
+<a href="discard_iframe_history_2-3.html" onclick="parent.push_length()">Click me</a>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_2-3.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_2-3.html
new file mode 100644
index 0000000000..68847e9a7c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_2-3.html
@@ -0,0 +1 @@
+<button onclick="var p = parent; p.push_length(); frameElement.parentNode.removeChild(frameElement); p.push_length(); p.do_test();">Click me</button>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_2-manual.html b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_2-manual.html
new file mode 100644
index 0000000000..57eaabcd0c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/garbage-collection-and-browsing-contexts/non-automated/discard_iframe_history_2-manual.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<title>Removing iframe from document removes it from history</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+setup({timeout:3600000})
+var t = async_test();
+var w = window.open("discard_iframe_history_2-1.html");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/historical.window.js b/testing/web-platform/tests/html/browsers/the-window-object/historical.window.js
new file mode 100644
index 0000000000..653f12b464
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/historical.window.js
@@ -0,0 +1,4 @@
+test(() => {
+ assert_false("showModalDialog" in window)
+ assert_false("showModalDialog" in Window.prototype)
+}, "showModalDialog() has been removed from the platform")
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/length-attribute.window.js b/testing/web-platform/tests/html/browsers/the-window-object/length-attribute.window.js
new file mode 100644
index 0000000000..d56e1e4692
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/length-attribute.window.js
@@ -0,0 +1,24 @@
+async_test(t => {
+ const frame = document.createElement("iframe");
+ frame.srcdoc = "<iframe name=x srcdoc='<iframe name=z></iframe>'></iframe><iframe name=y></iframe>";
+ frame.onload = t.step_func_done(() => {
+ const frameW = frame.contentWindow;
+ assert_equals(frameW.length, 2);
+ assert_not_equals(frameW.x, undefined);
+ assert_not_equals(frameW.y, undefined);
+ assert_equals(frameW.z, undefined);
+ assert_equals(frameW.x, frameW[0]);
+ assert_equals(frameW.y, frameW[1]);
+ const xFrameW = frameW.x;
+ assert_equals(xFrameW.length, 1);
+ assert_not_equals(xFrameW.z, undefined);
+ assert_equals(xFrameW.z, xFrameW[0]);
+ frame.remove();
+ assert_equals(frameW.length, 0);
+ assert_equals(frameW.x, undefined);
+ assert_equals(frameW[0], undefined);
+ assert_equals(xFrameW.length, 0);
+ assert_equals(xFrameW.z, undefined);
+ });
+ document.body.append(frame);
+}, "Window object's length IDL attribute (and named access)");
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/name-attribute.window.js b/testing/web-platform/tests/html/browsers/the-window-object/name-attribute.window.js
new file mode 100644
index 0000000000..f266dd7acb
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/name-attribute.window.js
@@ -0,0 +1,18 @@
+test(() => {
+ const frame = document.createElement("iframe"),
+ name = "A",
+ name2 = "B";
+ frame.setAttribute("name", name);
+ document.body.append(frame);
+ const frameW = frame.contentWindow;
+ assert_equals(frameW.name, name);
+ frameW.name = name2;
+ assert_equals(frame.getAttribute("name"), name);
+ assert_equals(frameW.name, name2);
+ frame.remove();
+ assert_equals(frame.getAttribute("name"), name);
+ assert_equals(frameW.name, "");
+ frameW.name = name2;
+ assert_equals(frame.getAttribute("name"), name);
+ assert_equals(frameW.name, "");
+}, "Window object's name IDL attribute");
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/cross-global-npo.html b/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/cross-global-npo.html
new file mode 100644
index 0000000000..2acad0734c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/cross-global-npo.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Named access across globals</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+async_test(function() {
+ var iframe = document.createElement("iframe");
+ iframe.src = "cross-global-support.html";
+ document.body.appendChild(iframe);
+ iframe.onload = this.step_func_done(function() {
+ var name = "named";
+ var win = iframe.contentWindow;
+ var element = win.document.getElementById(name);
+
+ var expectedValues = [
+ // [value, is own property]
+ [element, false, "window"],
+ [element, false, "Window.prototype"],
+ [element, true, "named prototype object"],
+ [undefined, false, "EventTarget.prototype"],
+ [undefined, false, "Object.prototype"],
+ ];
+ for (var object = win; object; object = Object.getPrototypeOf(object)) {
+ var expected = expectedValues.shift();
+ assert_equals(object[name], expected[0], "[[Get]] on " + expected[2]);
+ var desc = Object.getOwnPropertyDescriptor(object, name);
+ if (expected[1]) {
+ assert_not_equals(desc, undefined, "[[GetOwnProperty]] on " + expected[2] + " should return something");
+ assert_equals(desc.value, element, "[[GetOwnProperty]] on " + expected[2]);
+ } else {
+ assert_equals(desc, undefined, "[[GetOwnProperty]] on " + expected[2]);
+ }
+ }
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/cross-global-support.html b/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/cross-global-support.html
new file mode 100644
index 0000000000..9d7b9f8a25
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/cross-global-support.html
@@ -0,0 +1,4 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Named access across globals: support file</title>
+<span id="named"></span>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/named-objects.html b/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/named-objects.html
new file mode 100644
index 0000000000..d5b1789d59
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/named-objects.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: Named access on the Window object</title>
+<link rel="author" title="Intel" href="http://www.intel.com/">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/multipage/browsers.html#named-access-on-the-window-object">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<div style="display:none">
+ <p name="a" id="p1"></p>
+ <a name="a" id="a1" href="#"></a>
+ <area name="a" id="area1"></area>
+ <embed name="a" id="embed1"></embed>
+ <form name="a" id="form1"></form>
+ <img name="a" id="img1">
+ <object name="a" id="obj1"></object>
+ <span name="a" id="span1"></span>
+
+ <b id="b" name="c"></b>
+ <a name="c"></a>
+ <iframe name="c" id="fm1"></iframe>
+ <iframe name="c" id="fm2" src="test.html" onload="on_load()"></iframe>
+ <input id="b"></input>
+ <span id="d"></span>
+ <a name=""></a>
+ <b id=""></b>
+</div>
+<script>
+
+test(function() {
+ assert_equals(window['c'], document.getElementById("fm1").contentWindow, "The first iframe's window should be returned.");
+}, "Check if the first nested browsing context is returned by window['c']");
+
+test(function() {
+ assert_true(window['a'] instanceof HTMLCollection);
+ assert_array_equals(window['a'],
+ [ document.getElementById('embed1'),
+ document.getElementById('form1'), document.getElementById('img1'),
+ document.getElementById('obj1') ],
+ "The elements are not in tree order.");
+
+ document.getElementById('form1').setAttribute("name", "");
+ document.getElementById('embed1').setAttribute("name", "");
+ assert_array_equals(window['a'],
+ [ document.getElementById('img1'),
+ document.getElementById('obj1') ],
+ "Window['a'] should not contain the elements with empty name attribute.");
+}, "Check if window['a'] contains all embed, form, img, and object elements, and their order");
+
+var t = async_test("Check that window['fs'] does not return the frameset element with name='fs' (historical)");
+function on_load () {
+ t.step(function () {
+ assert_equals(document.getElementById('fm2').contentWindow['fs'],
+ undefined,
+ "The frameset element should not be returned.");
+ });
+ t.done();
+}
+
+test(function() {
+ assert_true(window['b'] instanceof HTMLCollection);
+ assert_array_equals(window['b'], [document.getElementsByTagName('b')[0], document.getElementsByTagName('input')[0]]);
+
+ document.getElementsByTagName('b')[0].setAttribute("id", "");
+ assert_equals(window['b'], document.getElementsByTagName('input')[0],
+ "The window['b'] should not contain the elements with empty id attribute.");
+}, "Check if window['b'] returns the elements with the id='b'");
+
+test(function() {
+ assert_equals(window['d'], document.getElementById('d'));
+}, "Check if window['d'] returns the element with id='d'");
+
+test(function() {
+ assert_equals(window[''], undefined, "The window[''] should be undefined");
+}, "Check widow[''] when there are some elements with empty id or name attribute");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/navigated-named-objects.window.js b/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/navigated-named-objects.window.js
new file mode 100644
index 0000000000..59d94efc99
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/navigated-named-objects.window.js
@@ -0,0 +1,67 @@
+// META: script=/common/get-host-info.sub.js
+
+function echoURL(content) {
+ return `/common/echo.py?content=${encodeURIComponent(content)}`;
+}
+
+function setSrc(frame, type, content) {
+ if (type === "same-origin") {
+ frame.src = echoURL(content);
+ } else if (type === "cross-site") {
+ frame.src = `${get_host_info().HTTP_NOTSAMESITE_ORIGIN}${echoURL(content)}`;
+ } else {
+ frame.srcdoc = content;
+ }
+}
+
+["srcdoc", "same-origin", "cross-site"].forEach(type => {
+ const initialType = type === "srcdoc" ? type : "same-origin";
+
+ [
+ {
+ "namedObject": "<div id=abc></div>",
+ "namedObjectLocalName": "div"
+ },
+ {
+ "namedObject": "<object name=abc></object>",
+ "namedObjectLocalName": "object"
+ },
+ {
+ "namedObject": "<iframe id=abc></iframe>",
+ "namedObjectLocalName": "iframe"
+ }
+ ].forEach(testData => {
+ async_test(t => {
+ const frame = document.createElement("iframe");
+ t.add_cleanup(() => frame.remove());
+ setSrc(frame, initialType, `<script>function f() { return abc }</script>${testData.namedObject}`);
+ frame.onload = t.step_func(() => {
+ const f = frame.contentWindow.f,
+ associatedAbc = f();
+ frame.onload = t.step_func_done(() => {
+ assert_equals(f(), associatedAbc);
+ assert_equals(associatedAbc.localName, testData.namedObjectLocalName);
+ });
+ setSrc(frame, type, "<span id=abc></span>");
+ });
+ document.body.append(frame);
+ }, `Window's associated Document object is used for finding named objects (<${testData.namedObjectLocalName}> via ${type} <iframe>)`);
+ });
+
+ async_test(t => {
+ const frame = document.createElement("iframe");
+ t.add_cleanup(() => frame.remove());
+ setSrc(frame, initialType, "<script>function f() { return abc }</script><object name=abc data='about:blank'></object>");
+ frame.onload = t.step_func(() => {
+ const f = frame.contentWindow.f,
+ associatedAbc = f(),
+ associatedAbcContainer = associatedAbc.frameElement;
+ frame.onload = t.step_func_done(() => {
+ assert_equals(f(), associatedAbcContainer);
+ assert_equals(associatedAbcContainer.contentWindow, null);
+ });
+ setSrc(frame, type, "<span id=abc></span>");
+ });
+ document.body.append(frame);
+ }, `Window's associated Document object is used for finding named objects (<object> with browsing ccontext via ${type} <iframe)>`);
+});
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/prototype.html b/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/prototype.html
new file mode 100644
index 0000000000..910374381b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/prototype.html
@@ -0,0 +1,94 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Named access with shadowing properties</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+test(function() {
+ var name = "named1";
+ window[name] = "shadowing";
+ var element = document.createElement("span");
+ element.id = name;
+ document.body.appendChild(element);
+
+ assert_equals(window[name], "shadowing");
+ assert_equals(Object.getOwnPropertyDescriptor(window, name).value, "shadowing");
+
+ assert_equals(Window.prototype[name], element);
+ assert_equals(Object.getOwnPropertyDescriptor(Window.prototype, name), undefined);
+
+ var npo = Object.getPrototypeOf(Window.prototype);
+ assert_equals(npo[name], element);
+ assert_equals(Object.getOwnPropertyDescriptor(npo, name).value, element);
+
+ assert_equals(EventTarget.prototype[name], undefined);
+ assert_equals(Object.getOwnPropertyDescriptor(EventTarget.prototype, name), undefined);
+}, "Property on window.");
+
+test(function() {
+ var name = "named2";
+ Window.prototype[name] = "shadowing";
+ var element = document.createElement("span");
+ element.id = name;
+ document.body.appendChild(element);
+
+ assert_equals(window[name], "shadowing");
+ assert_equals(Object.getOwnPropertyDescriptor(window, name), undefined);
+
+ assert_equals(Window.prototype[name], "shadowing");
+ assert_equals(Object.getOwnPropertyDescriptor(Window.prototype, name).value, "shadowing");
+
+ var npo = Object.getPrototypeOf(Window.prototype);
+ assert_equals(npo[name], element);
+ assert_equals(Object.getOwnPropertyDescriptor(npo, name).value, element);
+
+ assert_equals(EventTarget.prototype[name], undefined);
+ assert_equals(Object.getOwnPropertyDescriptor(EventTarget.prototype, name), undefined);
+}, "Property on Window.prototype.");
+
+test(function() {
+ var name = "named3";
+ EventTarget.prototype[name] = "shadowing";
+ var element = document.createElement("span");
+ element.id = name;
+ document.body.appendChild(element);
+
+ assert_equals(window[name], "shadowing");
+ assert_equals(Object.getOwnPropertyDescriptor(window, name), undefined);
+
+ assert_equals(Window.prototype[name], "shadowing");
+ assert_equals(Object.getOwnPropertyDescriptor(Window.prototype, name), undefined);
+
+ var npo = Object.getPrototypeOf(Window.prototype);
+ assert_equals(npo[name], "shadowing");
+ assert_equals(Object.getOwnPropertyDescriptor(npo, name), undefined);
+
+ assert_equals(EventTarget.prototype[name], "shadowing");
+ assert_equals(Object.getOwnPropertyDescriptor(EventTarget.prototype, name).value, "shadowing");
+}, "Property on EventTarget.prototype.");
+
+test(function() {
+ var name = "named4";
+ Object.prototype[name] = "shadowing";
+ var element = document.createElement("span");
+ element.id = name;
+ document.body.appendChild(element);
+
+ assert_equals(window[name], "shadowing");
+ assert_equals(Object.getOwnPropertyDescriptor(window, name), undefined);
+
+ assert_equals(Window.prototype[name], "shadowing");
+ assert_equals(Object.getOwnPropertyDescriptor(Window.prototype, name), undefined);
+
+ var npo = Object.getPrototypeOf(Window.prototype);
+ assert_equals(npo[name], "shadowing");
+ assert_equals(Object.getOwnPropertyDescriptor(npo, name), undefined);
+
+ assert_equals(EventTarget.prototype[name], "shadowing");
+ assert_equals(Object.getOwnPropertyDescriptor(EventTarget.prototype, name), undefined);
+
+ assert_equals(Object.prototype[name], "shadowing");
+ assert_equals(Object.getOwnPropertyDescriptor(Object.prototype, name).value, "shadowing");
+}, "Property on Object.prototype.");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/strict-mode-redefine-readonly-property.html b/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/strict-mode-redefine-readonly-property.html
new file mode 100644
index 0000000000..ce1da3747a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/strict-mode-redefine-readonly-property.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<link rel="help" href="https://crbug.com/1448846">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+test(() => {
+ "use strict";
+ const pathname = document.location.pathname;
+ const form = document.createElement('form');
+ form.name = 'location';
+ document.body.appendChild(form);
+ assert_equals(document.location?.pathname, pathname);
+ form.remove();
+}, "Adding/removing form with a name referring to a non-configurable property");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/test.html b/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/test.html
new file mode 100644
index 0000000000..c3b3cc1852
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/test.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: Named Object</title>
+<link rel="author" title="Intel" href="http://www.intel.com/">
+<frameset name="fs" id="fs1">
+ <frame></frame>
+</frameset>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/window-named-properties.html b/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/window-named-properties.html
new file mode 100644
index 0000000000..bd3929dd89
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/window-named-properties.html
@@ -0,0 +1,83 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Changes to named properties of the window object</title>
+<link rel="author" title="Ms2ger" href="ms2ger@gmail.com">
+<link rel="author" title="Boris Zbarsky" href="bzbarsky@mit.edu">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#window">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#dom-window-nameditem">
+<link rel="help" href="https://webidl.spec.whatwg.org/#named-properties-object">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<iframe name="bar"></iframe>
+<iframe name="baz"></iframe>
+<iframe name="baz"></iframe>
+<iframe name="constructor"></iframe>
+<iframe id="quux"></iframe>
+<script>
+function assert_data_propdesc(pd, Writable, Enumerable, Configurable) {
+ assert_equals(typeof pd, "object");
+ assert_equals(pd.writable, Writable);
+ assert_equals(pd.enumerable, Enumerable);
+ assert_equals(pd.configurable, Configurable);
+}
+test(function() {
+ assert_true("bar" in window, "bar not in window");
+ assert_equals(window["bar"],
+ document.getElementsByTagName("iframe")[0].contentWindow);
+}, "Static name");
+
+test(function() {
+ assert_true("quux" in window, "quux not in window");
+ assert_equals(window["quux"],
+ document.getElementsByTagName("iframe")[4]);
+}, "Static id");
+
+test(function() {
+ assert_true("bar" in Window.prototype, "bar in Window.prototype");
+ assert_false(Window.prototype.hasOwnProperty("bar"), "Window.prototype.hasOwnProperty(\"bar\")");
+
+ var gsp = Object.getPrototypeOf(Object.getPrototypeOf(window));
+ assert_true("bar" in gsp, "bar in gsp");
+ assert_true(gsp.hasOwnProperty("bar"), "gsp.hasOwnProperty(\"bar\")");
+ assert_data_propdesc(Object.getOwnPropertyDescriptor(gsp, "bar"),
+ true, false, true);
+}, "Static name on the prototype");
+test(function() {
+ assert_equals(window.constructor, Window);
+ assert_false(window.hasOwnProperty("constructor"), "window.constructor should not be an own property.");
+
+ var proto = Object.getPrototypeOf(window);
+ assert_equals(proto.constructor, Window);
+ assert_true("constructor" in proto, "constructor in proto");
+ assert_data_propdesc(Object.getOwnPropertyDescriptor(proto, "constructor"),
+ true, false, true);
+
+ var gsp = Object.getPrototypeOf(proto);
+ assert_true("constructor" in gsp, "constructor in gsp");
+ assert_false(gsp.hasOwnProperty("constructor"), "gsp.hasOwnProperty(\"constructor\")");
+ assert_equals(Object.getOwnPropertyDescriptor(gsp, "constructor"), undefined);
+}, "constructor");
+test(function() {
+ var gsp = Object.getPrototypeOf(Object.getPrototypeOf(window));
+ assert_equals(gsp.baz, document.getElementsByTagName("iframe")[1].contentWindow);
+}, "duplicate property names")
+var t = async_test("Dynamic name")
+var t2 = async_test("Ghost name")
+t.step(function() {
+ var iframe = document.getElementsByTagName("iframe")[0];
+ iframe.setAttribute("srcdoc", "<script>window.name='foo'<\/script>");
+ iframe.onload = function() {
+ t.step(function() {
+ assert_true("foo" in window, "foo not in window");
+ assert_equals(window["foo"], iframe.contentWindow);
+ });
+ t.done();
+ t2.step(function() {
+ assert_false("bar" in window, "bar still in window");
+ assert_equals(window["bar"], undefined);
+ });
+ t2.done();
+ };
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/window-null-names.html b/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/window-null-names.html
new file mode 100644
index 0000000000..6801ef9d8a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/named-access-on-the-window-object/window-null-names.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Named access with null characters</title>
+<link rel="author" title="Ms2ger" href="ms2ger@gmail.com">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#window">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#dom-window-nameditem">
+<link rel="help" href="https://webidl.spec.whatwg.org/#named-properties-object">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+test(function() {
+ var iframe = document.createElement("iframe")
+ iframe.name = "a\0b"
+ document.body.appendChild(iframe)
+ assert_equals(window["a\0b"], iframe.contentWindow)
+ assert_equals(window["ab"], undefined)
+ assert_equals(window["a"], undefined)
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/navigate-to-about-blank-while-initial-load-pending.html b/testing/web-platform/tests/html/browsers/the-window-object/navigate-to-about-blank-while-initial-load-pending.html
new file mode 100644
index 0000000000..3a0def8ae6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/navigate-to-about-blank-while-initial-load-pending.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test navigating to about:blank while window.open initial load pending.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+async_test(t => {
+ // Open a new window and initiate a navigation. The test does not actually
+ // expect this navigation to complete so it does not matter what URL is
+ // used other than it must not be about:blank. The intent is to start a
+ // navigation to some URL and then assign about:blank to the location
+ // attribute. This assignment should stop the inital navigation and start a
+ // new navigation to about:blank. When the about:blank page finishes loading
+ // the load event is expected to fire and the document URL should to be set to
+ // about:blank.
+ var window1 = window.open('resources/post-to-opener.html', '_blank');
+ t.add_cleanup(() => {
+ window1.close();
+ });
+ window1.location = 'about:blank';
+ window1.onload = t.step_func_done(e => {
+ assert_equals(window1.document.URL, "about:blank");
+ });
+}, 'Navigating to about:blank while window.open initial load pending.');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/noopener-noreferrer-BarProp.window.js b/testing/web-platform/tests/html/browsers/the-window-object/noopener-noreferrer-BarProp.window.js
new file mode 100644
index 0000000000..a75a034650
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/noopener-noreferrer-BarProp.window.js
@@ -0,0 +1,23 @@
+const barProps = ["locationbar", "menubar", "personalbar", "scrollbars", "statusbar", "toolbar"];
+
+test(() => {
+ for(const prop of barProps) {
+ assert_true(window[prop].visible);
+ }
+}, "All bars visible");
+
+["noopener", "noreferrer"].forEach(openerStyle => {
+ async_test(t => {
+ const channelName = "5454" + openerStyle + "34324";
+ const channel = new BroadcastChannel(channelName);
+ window.open("support/BarProp-target.html?" + channelName, "", openerStyle);
+ channel.onmessage = t.step_func_done(e => {
+ // Send message first so if asserts throw the popup is still closed
+ channel.postMessage(null);
+
+ for(const prop of barProps) {
+ assert_true(e.data[prop]);
+ }
+ });
+ }, `window.open() with ${openerStyle} should have all bars visible`);
+});
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/noopener-noreferrer-sizing.window.js b/testing/web-platform/tests/html/browsers/the-window-object/noopener-noreferrer-sizing.window.js
new file mode 100644
index 0000000000..cc53ba5f2f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/noopener-noreferrer-sizing.window.js
@@ -0,0 +1,17 @@
+const windowProps = ["innerWidth", "innerHeight"];
+
+["noopener", "noreferrer"].forEach(openerStyle => {
+ async_test(t => {
+ const channelName = "34342" + openerStyle + "8907";
+ const channel = new BroadcastChannel(channelName);
+ window.open("support/sizing-target.html?" + channelName, "", openerStyle);
+ channel.onmessage = t.step_func_done(e => {
+ // Send message first so if asserts throw the popup is still closed
+ channel.postMessage(null);
+
+ for(const prop of windowProps) {
+ assert_equals(window[prop], e.data[prop]);
+ }
+ });
+ }, `window.open() with ${openerStyle} should have equal viewport width and height`);
+});
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/callback.js b/testing/web-platform/tests/html/browsers/the-window-object/open-close/callback.js
new file mode 100644
index 0000000000..ae51265a21
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/callback.js
@@ -0,0 +1 @@
+opener.callback() \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_beforeunload-1.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_beforeunload-1.html
new file mode 100644
index 0000000000..6f44d8a83e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_beforeunload-1.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<script>
+onload = function() {opener.postMessage("loaded", "*")};
+onbeforeunload = function() {
+ opener.callback();
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_beforeunload.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_beforeunload.html
new file mode 100644
index 0000000000..dcb8830ab6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_beforeunload.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<title>Running beforeunload handler in window.close()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+var w = window.open("close_beforeunload-1.html");
+onmessage = t.step_func(function(event) {
+ if (event.data != "loaded") {
+ return;
+ }
+ w.close();
+});
+callback = function() {t.done()}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_pagehide-1.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_pagehide-1.html
new file mode 100644
index 0000000000..5631b539a6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_pagehide-1.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<script>
+onload = function() {opener.postMessage("loaded", "*")};
+onpagehide = function() {
+ opener.callback();
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_pagehide.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_pagehide.html
new file mode 100644
index 0000000000..4d726437c2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_pagehide.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<title>Running pagehide handler in window.close()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+var w = window.open("close_pagehide-1.html");
+onmessage = t.step_func(function(event) {
+ if (event.data != "loaded") {
+ return;
+ }
+ w.close();
+});
+callback = function() {t.done()}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_script_defer-1.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_script_defer-1.html
new file mode 100644
index 0000000000..c50eddd41f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_script_defer-1.html
@@ -0,0 +1 @@
+<!doctype html>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_script_defer.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_script_defer.html
new file mode 100644
index 0000000000..1217882b16
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_script_defer.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<title>Running defer script in window.close()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+t.step(function() {
+ var w = window.open("close_script_defer-1.html");
+ w.document.open()
+ w.document.write("<script defer src='callback.js'><\/script>")
+ setTimeout(function() {
+ w.close();
+ }, 1000);
+})
+setTimeout(function() {t.done();}, 1000)
+callback = t.step(function() {assert_unreached()})
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_unload-1.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_unload-1.html
new file mode 100644
index 0000000000..9a9e304e84
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_unload-1.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<script>
+onload = function() {opener.postMessage("loaded", "*")};
+onunload = function() {
+ opener.callback();
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_unload.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_unload.html
new file mode 100644
index 0000000000..e4d231b285
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/close_unload.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<title>Running unload handler in window.close()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test();
+var w = window.open("close_unload-1.html");
+onmessage = t.step_func(function(event) {
+ if (event.data != "loaded") {
+ return;
+ }
+ w.close();
+});
+callback = function() {t.done()}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/creating_browsing_context_test_01.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/creating_browsing_context_test_01.html
new file mode 100644
index 0000000000..062f61949d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/creating_browsing_context_test_01.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>[Browsing Context] : [APIs for creating browsing_contexts by name]</title>
+<link rel="author" title="Duhyeong Kim" href="mailto:dduskim@gmail.com">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#apis-for-creating-and-navigating-browsing-contexts-by-name">
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(function() {
+ var currentUrl = 'http://' + window.location.host + '/common/blank.html';
+ var win = window.open(currentUrl, '', 'height=1,width=1');
+ this.add_cleanup(function() { win.close(); });
+ win.onload = this.step_func_done(function () {
+ assert_equals(win.location.href, currentUrl, 'should be equal to result url');
+ });
+}, 'first argument: absolute url');
+
+test(function() {
+ var win = window.open('', '', 'height=1,width=1');
+ this.add_cleanup(function() { win.close(); });
+ assert_equals(win.location.href, 'about:blank', 'win.location.href');
+ assert_equals(win.document.charset, 'UTF-8', 'win.document.charset');
+}, 'first argument: empty url');
+
+test(function () {
+ var win = window.open('', 'testWindow', 'height=1,width=1');
+ win.close();
+ assert_equals(win.name, 'testWindow', 'should have a browsing context name');
+}, 'second argument: passing a non-empty name');
+
+test(function () {
+ var win = window.open('', '', 'height=1,width=1');
+ this.add_cleanup(function() { win.close(); });
+ assert_equals(win.name, '', 'window should not have a name');
+ win.name = 'testWindow';
+ assert_equals(win.name, 'testWindow', 'window should have a name');
+}, 'second argument: setting name after opening');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/no_window_open_when_term_nesting_level_nonzero.window.js b/testing/web-platform/tests/html/browsers/the-window-object/open-close/no_window_open_when_term_nesting_level_nonzero.window.js
new file mode 100644
index 0000000000..3dff403b9c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/no_window_open_when_term_nesting_level_nonzero.window.js
@@ -0,0 +1,113 @@
+test(function() {
+ var test_window = window.open('', '', 'height=1,width=1');
+ var test_document = test_window.document;
+
+ var frame = test_document.createElement('iframe');
+ test_document.body.appendChild(frame);
+
+ frame.contentWindow.onpagehide = function(evt) {
+ assert_equals(frame.contentWindow.open('', '', 'height=1,width=1'), null,
+ "expected no popup during pagehide");
+ };
+ frame.contentDocument.onvisibilitychange = function(evt) {
+ assert_equals(frame.contentWindow.open('', '', 'height=1,width=1'), null,
+ "expected no popup during visibilitychange");
+ };
+ frame.contentWindow.onbeforeunload = function(evt) {
+ assert_equals(frame.contentWindow.open('', '', 'height=1,width=1'), null,
+ "expected no popup during beforeunload");
+ };
+ frame.contentWindow.onunload = function(evt) {
+ assert_equals(frame.contentWindow.open('', '', 'height=1,width=1'), null,
+ "expected no popup during unload");
+ };
+
+ frame.remove();
+}, 'no popups with frame removal');
+
+async_test(function(t) {
+ var test_window = window.open('', '', 'height=1,width=1');
+ var test_document = test_window.document;
+
+ var frame = test_document.createElement('iframe');
+ test_document.body.appendChild(frame);
+
+ frame.contentWindow.onpagehide = t.step_func(function(evt) {
+ assert_equals(frame.contentWindow.open('', '', 'height=1,width=1'), null,
+ "expected no popup during pagehide");
+ });
+ frame.contentDocument.onvisibilitychange = t.step_func(function(evt) {
+ assert_equals(frame.contentWindow.open('', '', 'height=1,width=1'), null,
+ "expected no popup during visibilitychange");
+ });
+ frame.contentWindow.onbeforeunload = t.step_func(function(evt) {
+ assert_equals(frame.contentWindow.open('', '', 'height=1,width=1'), null,
+ "expected no popup during beforeunload");
+ });
+ frame.contentWindow.onunload = t.step_func(function(evt) {
+ assert_equals(frame.contentWindow.open('', '', 'height=1,width=1'), null,
+ "expected no popup during unload");
+ });
+
+ frame.onload = t.step_func_done();
+
+ frame.contentWindow.location.href = "about:blank";
+}, 'no popups with frame navigation');
+
+async_test(function(t) {
+ var test_window = window.open('', '', 'height=1,width=1');
+ var test_document = test_window.document;
+
+ var frame = test_document.createElement('iframe');
+ test_document.body.appendChild(frame);
+
+ frame.contentWindow.onpagehide = t.step_func(function(evt) {
+ assert_equals(test_window.open('', '', 'height=1,width=1'), null,
+ "expected no popup during pagehide");
+ });
+ frame.contentDocument.onvisibilitychange = t.step_func(function(evt) {
+ assert_equals(test_window.open('', '', 'height=1,width=1'), null,
+ "expected no popup during visibilitychange");
+ });
+ frame.contentWindow.onbeforeunload = t.step_func(function(evt) {
+ assert_equals(test_window.open('', '', 'height=1,width=1'), null,
+ "expected no popup during beforeunload");
+ });
+ frame.contentWindow.onunload = t.step_func(function(evt) {
+ assert_equals(test_window.open('', '', 'height=1,width=1'), null,
+ "expected no popup during unload");
+ });
+
+ frame.onload = t.step_func_done();
+
+ frame.contentWindow.location.href = "about:blank";
+}, 'no popups from synchronously reachable window');
+
+async_test(function(t) {
+ var test_window = window.open('', '', 'height=1,width=1');
+ var test_document = test_window.document;
+
+ var frame = test_document.createElement('iframe');
+ test_document.body.appendChild(frame);
+
+ frame.contentWindow.onpagehide = t.step_func(function(evt) {
+ assert_equals(window.open('', '', 'height=1,width=1'), null,
+ "expected no popup during pagehide");
+ });
+ frame.contentDocument.onvisibilitychange = t.step_func(function(evt) {
+ assert_equals(window.open('', '', 'height=1,width=1'), null,
+ "expected no popup during visibilitychange");
+ });
+ frame.contentWindow.onbeforeunload = t.step_func(function(evt) {
+ assert_equals(window.open('', '', 'height=1,width=1'), null,
+ "expected no popup during beforeunload");
+ });
+ frame.contentWindow.onunload = t.step_func(function(evt) {
+ assert_equals(window.open('', '', 'height=1,width=1'), null,
+ "expected no popup during unload");
+ });
+
+ frame.onload = t.step_func_done();
+
+ frame.contentWindow.location.href = "about:blank";
+}, 'no popups from another synchronously reachable window');
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/001-1.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/001-1.html
new file mode 100644
index 0000000000..7dd48b41c2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/001-1.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<p>Now open a new tab and navigate to <a href="001-2.html">001-2</a></p>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/001-2.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/001-2.html
new file mode 100644
index 0000000000..b1413861a3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/001-2.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<script>
+var result = "FAIL";
+if (opener != null) {
+ result = "FAIL (did you open this page in a new tab?)";
+} else {
+ var w = window.open("", "test_name");
+ if (w.location.href !== "about:blank") {
+ result = "FAIL (didn't open an about:blank browsing context)";
+ } else {
+ w.close();
+ result = "PASS";
+ }
+ document.write(result);
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/001.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/001.html
new file mode 100644
index 0000000000..7b0f21ec04
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/001.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<title>Accessing named windows from outside the unit of related browsing contexts</title>
+<a href="001-1.html" target="test_name">Click here</a>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/002-1.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/002-1.html
new file mode 100644
index 0000000000..0e210f351b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/002-1.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<p>Now open a new tab and navigate to <a></a></p>
+<script>
+href = window.location.href.replace("http://", "http://www.").replace("002-1.html", "002-2.html");
+var a = document.getElementsByTagName("a")[0];
+a.href = href;
+a.textContent = href;
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/002-2.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/002-2.html
new file mode 100644
index 0000000000..b1413861a3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/002-2.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<script>
+var result = "FAIL";
+if (opener != null) {
+ result = "FAIL (did you open this page in a new tab?)";
+} else {
+ var w = window.open("", "test_name");
+ if (w.location.href !== "about:blank") {
+ result = "FAIL (didn't open an about:blank browsing context)";
+ } else {
+ w.close();
+ result = "PASS";
+ }
+ document.write(result);
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/002.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/002.html
new file mode 100644
index 0000000000..b568ae8d48
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/non_automated/002.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<title>Accessing different-origin named windows from outside the unit of related browsing contexts</title>
+<a href="002-1.html" target="test_name">Click here</a>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-is-popup-condition.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-is-popup-condition.html
new file mode 100644
index 0000000000..4f52e2e8de
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-is-popup-condition.html
@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML: window.open `features`: condition for is popup</title>
+<meta name="variant" content="?single-1">
+<meta name="variant" content="?single-2">
+<meta name="variant" content="?position">
+<meta name="variant" content="?combination">
+<meta name=timeout content=long>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/window-object.html#window-open-steps">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedPostMessage.js"></script>
+<script>
+var windowURL = 'resources/is-popup-barprop.html';
+
+var target = document.location.search.substring(1);
+
+// features, visible
+// NOTE: visible == !isPopup
+var tests = {
+ "single-1": [
+ // Empty feature results in non-popup.
+ [undefined, true],
+
+ // The explicit popup feature.
+ ["popup", false],
+ ["popup=1", false],
+ ["popup=true", false],
+ ["popup=0", true],
+
+ // Other feature alone results in popup.
+ ["location", false],
+ ["location=yes", false],
+ ["location=true", false],
+ ["location=no", false],
+
+ ["toolbar", false],
+ ["toolbar=yes", false],
+ ["toolbar=true", false],
+ ["toolbar=no", false],
+
+ ["menubar", false],
+ ["menubar=yes", false],
+ ["menubar=true", false],
+ ["menubar=no", false],
+
+ ["resizable", false],
+ ["resizable=yes", false],
+ ["resizable=true", false],
+ ["resizable=no", false],
+ ],
+ "single-2": [
+ ["scrollbars", false],
+ ["scrollbars=yes", false],
+ ["scrollbars=true", false],
+ ["scrollbars=no", false],
+
+ ["status", false],
+ ["status=yes", false],
+ ["status=true", false],
+ ["status=no", false],
+
+ ["titlebar", false],
+ ["titlebar=yes", false],
+ ["titlebar=true", false],
+ ["titlebar=no", false],
+
+ ["close", false],
+ ["close=yes", false],
+ ["close=true", false],
+ ["close=no", false],
+
+ ["minimizable", false],
+ ["minimizable=yes", false],
+ ["minimizable=true", false],
+ ["minimizable=no", false],
+
+ ["personalbar", false],
+ ["personalbar=yes", false],
+ ["personalbar=true", false],
+ ["personalbar=no", false],
+ ],
+ "position": [
+ ["left=500", false],
+ ["screenX=500", false],
+
+ ["top=500", false],
+ ["screenY=500", false],
+
+ ["width=500", false],
+ ["innerWidth=500", false],
+
+ ["outerWidth=500", false],
+
+ ["height=500", false],
+ ["innerHeight=500", false],
+
+ ["outerHeight=500", false],
+ ],
+ "combination": [
+ // The following combination results in non-popup.
+ ["location,toolbar,menubar,resizable,scrollbars,status", true],
+
+ // Either location or toolbar is required for non-popup.
+ ["location,menubar,resizable,scrollbars,status", true],
+ ["toolbar,menubar,resizable,scrollbars,status", true],
+
+ ["resizable,scrollbars,status", false],
+ ["location=no,menubar=no,resizable,scrollbars,status", false],
+
+ // menubar is required for non-popup.
+ ["location,toolbar,resizable,scrollbars,status", false],
+
+ // resizable is required for non-popup, but defaults to true
+ ["location,toolbar,menubar,scrollbars,status", true],
+ ["location,toolbar,menubar,resizable=no,scrollbars,status", false],
+
+ // scrollbars is required for non-popup.
+ ["location,toolbar,menubar,resizable,status", false],
+
+ // status is required for non-popup.
+ ["location,toolbar,menubar,resizable,scrollbars", false],
+
+ // The explicit popup feature has priority than others.
+ ["popup=1,location,toolbar,menubar,resizable,scrollbars,status", false],
+ ["popup=yes,location,toolbar,menubar,resizable,scrollbars,status", false],
+ ["popup=true,location,toolbar,menubar,resizable,scrollbars,status", false],
+ ["popup=0,location,toolbar,menubar,resizable,scrollbars", true],
+ ],
+};
+
+tests[target].forEach(([features, visible]) => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.locationbar, visible, `window.locationbar.visible`);
+ assert_equals(data.menubar, visible, `window.menubar.visible`);
+ assert_equals(data.personalbar, visible, `window.personalbar.visible`);
+ assert_equals(data.scrollbars, visible, `window.scrollbars.visible`);
+ assert_equals(data.statusbar, visible, `window.statusbar.visible`);
+ assert_equals(data.toolbar, visible, `window.toolbar.visible`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL), '', features);
+ }, `${format_value(features)} should set BarProp visibility to ${visible}`);
+});
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-negative-innerwidth-innerheight.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-negative-innerwidth-innerheight.html
new file mode 100644
index 0000000000..019ee9d730
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-negative-innerwidth-innerheight.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML: window.open `features`: negative values for legacy `innerwidth`, `innerheight`</title>
+<meta name=timeout content=long>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#apis-for-creating-and-navigating-browsing-contexts-by-name">
+
+<!-- user agents are not required to support open features other than `noopener`
+ and on some platforms position and size features don't make sense -->
+<meta name="flags" content="may" />
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedPostMessage.js"></script>
+<script>
+var windowURL = 'resources/message-opener.html';
+var featuresPrefix = `top=0,left=0,`;
+
+// https://html.spec.whatwg.org/multipage/infrastructure.html#rules-for-parsing-integers
+
+setup (() => {
+ // Before running tests, open a window using features that mimic
+ // what would happen if the features tested here were set to invalid
+ // values in later tests.
+ // In cases where the value for `innerwidth` or `innerheight` is
+ // is less than the browser's minimum allowed value for that dimension,
+ // but NOT 0, the value affected will become the browser's minimum allowed value.
+
+ // This should result in a minimally-sized window for later comparison
+ var featureString = `${featuresPrefix}innerwidth=1,innerheight=1`;
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage((data, e) => {
+ e.source.close();
+ runWindowTests(data);
+ });
+ var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+});
+
+function runWindowTests (baselineDimensions) {
+
+ // Negative values for `innerwidth` should result in a window with minimum
+ // valid allowed width
+ [ 'innerwidth=-404',
+ 'innerwidth=-404.5',
+ 'innerwidth=-404e1'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}height=405,${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.width, baselineDimensions.width, `"${feature} is negative and should result in a minimally-wide window"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + "&expected_innerWidth=" + baselineDimensions.width, '', featureString);
+ }, `features "${feature}" should NOT set "width=404"`);
+ });
+
+ // Negative values for `innerheight` should result in a window with minimum
+ // valid allowed height
+ [ 'innerheight=-404',
+ 'innerheight=-404.5',
+ 'innerheight=-404e1'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}width=404,${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.height, baselineDimensions.height, `"${feature} is negative and should result in a minimal-height window"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerHeight=' + baselineDimensions.height, '', featureString);
+ }, `features "${feature}" should NOT set "height=404"`);
+ });
+}
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-negative-screenx-screeny.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-negative-screenx-screeny.html
new file mode 100644
index 0000000000..6e316db482
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-negative-screenx-screeny.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML: window.open `features`: negative values for legacy `screenx`, `screeny`</title>
+<meta name=timeout content=long>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#apis-for-creating-and-navigating-browsing-contexts-by-name">
+
+<!-- user agents are not required to support open features other than `noopener`
+ and on some platforms position and size features don't make sense -->
+<meta name="flags" content="may" />
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedPostMessage.js"></script>
+<script>
+var featuresPrefix = `width=401,height=404,`;
+var windowURL = 'resources/message-opener.html';
+
+// https://html.spec.whatwg.org/multipage/infrastructure.html#rules-for-parsing-integers
+
+setup (() => {
+ // Before running tests, open a window using features that mimic
+ // what would happen if the feature tested here were set to 0
+ // for comparison later.
+ var featureString = `${featuresPrefix}top=0,left=0`;
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage((data, e) => {
+ e.source.close();
+ runWindowTests(data);
+ });
+ var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+});
+
+function runWindowTests (baselineDimensions) {
+ // Negative values should be interpreted as 0
+ [ 'screeny=-204',
+ 'screeny=-204.5',
+ 'screeny=-0'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}left=0,${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.top, baselineDimensions.top, `"${feature} is negative and should be set to 0"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenY=' + baselineDimensions.top, '', featureString);
+ }, `features "${feature}" should NOT set "top=204"`);
+ });
+
+ // Negative values should be interpreted as 0
+ [ 'screenx=-204',
+ 'screenx=-204.5',
+ 'screenx=-0'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}top=0,${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.left, baselineDimensions.left, `"${feature} is negative and should be set to 0"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenX=' + baselineDimensions.left, '', featureString);
+ }, `features "${feature}" should NOT set "left=204"`);
+ });
+}
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-negative-top-left.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-negative-top-left.html
new file mode 100644
index 0000000000..316b01a401
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-negative-top-left.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML: window.open `features`: negative values for `top`, `left`</title>
+<meta name=timeout content=long>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#apis-for-creating-and-navigating-browsing-contexts-by-name">
+
+<!-- user agents are not required to support open features other than `noopener`
+ and on some platforms position and size features don't make sense -->
+<meta name="flags" content="may" />
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedPostMessage.js"></script>
+<script>
+var featuresPrefix = `width=401,height=404,`;
+var windowURL = 'resources/message-opener.html';
+
+// https://html.spec.whatwg.org/multipage/infrastructure.html#rules-for-parsing-integers
+
+setup (() => {
+ // Before running tests, open a window using features that mimic
+ // what would happen if the feature tested here were set to 0
+ // for comparison later.
+ var featureString = `${featuresPrefix}top=0,left=0`;
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage((data, e) => {
+ e.source.close();
+ runWindowTests(data);
+ });
+ var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+});
+
+function runWindowTests (baselineDimensions) {
+
+ // Negative values for `top`, `left` should be interpreted as 0
+ [ 'top=-204',
+ 'top=-204.5',
+ 'top=-0'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}left=0,${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.top, baselineDimensions.top, `"${feature} is negative and should be set to 0"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenY=' + baselineDimensions.top, '', featureString);
+ }, `features "${feature}" should NOT set "top=204"`);
+ });
+
+// Negative values for `top`, `left` should be interpreted as 0
+ [ 'left=-204',
+ 'left=-204.5',
+ 'left=-0'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}top=0,${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.left, baselineDimensions.left, `"${feature} is negative and should be set to 0"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenX=' + baselineDimensions.left, '', featureString);
+ }, `features "${feature}" should NOT set "left=204"`);
+ });
+}
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-negative-width-height.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-negative-width-height.html
new file mode 100644
index 0000000000..3cb155620d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-negative-width-height.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML: window.open `features`: negative values for `width`, `height`</title>
+<meta name=timeout content=long>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#apis-for-creating-and-navigating-browsing-contexts-by-name">
+
+<!-- user agents are not required to support open features other than `noopener`
+ and on some platforms position and size features don't make sense -->
+<meta name="flags" content="may" />
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedPostMessage.js"></script>
+<script>
+var featuresPrefix = `top=0,left=0,`;
+var windowURL = 'resources/message-opener.html';
+
+// https://html.spec.whatwg.org/multipage/infrastructure.html#rules-for-parsing-integers
+
+setup (() => {
+ // Before running tests, open a window using features that mimic
+ // what would happen if the features tested here were set to invalid
+ // values in later tests.
+ // In cases where the value for `width` or `height` is
+ // is less than the browser's minimum allowed value for that dimension,
+ // but NOT 0, the value affected will become the browser's minimum allowed value.
+
+ // This should result in a minimally-sized window for later comparison
+ var featureString = `${featuresPrefix}width=1,height=1`;
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage((data, e) => {
+ e.source.close();
+ runWindowTests(data);
+ });
+ var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+});
+
+function runWindowTests (baselineDimensions) {
+
+ // Negative values for `width` should result in a window with minimum
+ // valid allowed width
+ [ 'width=-404',
+ 'width=-404.5',
+ 'width=-404e1'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}height=405,${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.width, baselineDimensions.width, `"${feature} is negative and should result in a minimally-wide window"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerWidth=' + baselineDimensions.width, '', featureString);
+ }, `features "${feature}" should NOT set "width=404"`);
+ });
+
+ // Negative values for `height` should result in a window with minimum
+ // valid allowed height
+ [ 'height=-404',
+ 'height=-404.5',
+ 'height=-404e1'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}width=404,${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.height, baselineDimensions.height, `"${feature} is negative and should result in a minimal-height window"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerHeight=' + baselineDimensions.height, '', featureString);
+ }, `features "${feature}" should NOT set "height=404"`);
+ });
+}
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-height.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-height.html
new file mode 100644
index 0000000000..d8ee866c50
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-height.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML: window.open `features`: non-integer values for feature `height`</title>
+<meta name=timeout content=long>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#apis-for-creating-and-navigating-browsing-contexts-by-name">
+
+<!-- user agents are not required to support open features other than `noopener`
+ and on some platforms position and size features don't make sense -->
+<meta name="flags" content="may" />
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedPostMessage.js"></script>
+<script>
+var featuresPrefix = `top=0,left=0,width=401,`;
+var windowURL = 'resources/message-opener.html';
+
+// https://html.spec.whatwg.org/multipage/infrastructure.html#rules-for-parsing-integers
+
+setup (() => {
+ // Before running tests, open a window using features that mimic
+ // what would happen if the feature tested here were set to 0
+ // for comparison later.
+ var featureString = `${featuresPrefix}height=0`;
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage((data, e) => {
+ e.source.close();
+ runWindowTests(data);
+ });
+ var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+});
+
+function runWindowTests (baselineDimensions) {
+ // The absence of the sizing feature should have the same behavior
+ // as that feature set to 0
+ [ featuresPrefix,
+ 'top=0,left=0',
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.height, baselineDimensions.height);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerHeight=' + baselineDimensions.height, '', feature);
+ }, `${feature}: absence of feature "height" should be treated same as "height=0"`);
+ });
+
+ // When code point in first position is not an ASCII digit, "+" or "-",
+ // that's an error and the value becomes 0
+ [ 'height=/404',
+ 'height=_404',
+ 'height=L404'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.height, baselineDimensions.height, `"${feature} begins with an invalid character and should be ignored"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerHeight=' + baselineDimensions.height, '', featureString);
+ }, `features "${feature}" should NOT set "height=404"`);
+ });
+
+ // Codepoints that are valid ASCII digits should be collected
+ // Non-ASCII digits and subsequent code points are ignored
+ [ 'height=405.5',
+ 'height=405.32',
+ 'height=405LLl',
+ 'height=405^4',
+ 'height=405*3',
+ 'height=405/5',
+ 'height=405 ',
+ 'height=405e1',
+ 'height=405e-1'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.height, 405, `"${featureString} value after first non-digit will be ignored"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + "&expected_innerHeight=405", '', featureString);
+ }, `features "${feature}" should set "height=405"`);
+ });
+
+}
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-innerheight.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-innerheight.html
new file mode 100644
index 0000000000..f191d875d8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-innerheight.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML: window.open `features`: non-integer values for legacy feature `innerheight`</title>
+<meta name=timeout content=long>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#apis-for-creating-and-navigating-browsing-contexts-by-name">
+
+<!-- user agents are not required to support open features other than `noopener`
+ and on some platforms position and size features don't make sense -->
+<meta name="flags" content="may" />
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedPostMessage.js"></script>
+<script>
+var featuresPrefix = `top=0,left=0,width=401,`;
+var windowURL = 'resources/message-opener.html';
+
+// https://html.spec.whatwg.org/multipage/infrastructure.html#rules-for-parsing-integers
+
+setup (() => {
+ // Before running tests, open a window using features that mimic
+ // what would happen if the feature tested here were set to 0
+ // for comparison later.
+ var featureString = `${featuresPrefix}height=0`;
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage((data, e) => {
+ e.source.close();
+ runWindowTests(data);
+ });
+ var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+});
+
+function runWindowTests (baselineDimensions) {
+ // When code point in first position is not an ASCII digit, "+" or "-",
+ // that's an error and the value becomes 0
+ [ 'innerheight=/404',
+ 'innerheight=_404',
+ 'innerheight=L404'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.height, baselineDimensions.height, `"${feature} begins with an invalid character and should be ignored"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerHeight=' + baselineDimensions.height, '', featureString);
+ }, `features "${feature}" should NOT set "height=404"`);
+ });
+
+ // Codepoints that are valid ASCII digits should be collected
+ // Non-ASCII digits and subsequent code points are ignored
+ [ 'innerheight=405.5',
+ 'innerheight=405.32',
+ 'innerheight=405LLl',
+ 'innerheight=405^4',
+ 'innerheight=405*3',
+ 'innerheight=405/5',
+ 'innerheight=405 ',
+ 'innerheight=405e1',
+ 'innerheight=405e-1'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.height, 405, `"${featureString} value after first non-digit will be ignored"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + "&expected_innerHeight=405", '', featureString);
+ }, `features "${feature}" should set "height=405"`);
+ });
+
+}
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-innerwidth.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-innerwidth.html
new file mode 100644
index 0000000000..d1ddc5e43a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-innerwidth.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML: window.open `features`: non-integer values for legacy feature `innerwidth`</title>
+<meta name=timeout content=long>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#apis-for-creating-and-navigating-browsing-contexts-by-name">
+
+<!-- user agents are not required to support open features other than `noopener`
+ and on some platforms position and size features don't make sense -->
+<meta name="flags" content="may" />
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedPostMessage.js"></script>
+<script>
+var featuresPrefix = `top=0,left=0,height=401,`;
+var windowURL = 'resources/message-opener.html';
+
+// https://html.spec.whatwg.org/multipage/infrastructure.html#rules-for-parsing-integers
+
+setup (() => {
+ // Before running tests, open a window using features that mimic
+ // what would happen if the feature tested here were set to 0
+ // for comparison later.
+ var featureString = `${featuresPrefix}width=0`;
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage((data, e) => {
+ e.source.close();
+ runWindowTests(data);
+ });
+ var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+});
+
+function runWindowTests (baselineDimensions) {
+ // When code point in first position is not an ASCII digit, "+" or "-",
+ // that's an error and the value becomes 0
+ [ 'innerwidth=/404',
+ 'innerwidth=_404',
+ 'innerwidth=L404'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.width, baselineDimensions.width, `"${feature} begins with an invalid character and should be ignored"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerWidth=' + baselineDimensions.width, '', featureString);
+ }, `features "${feature}" should NOT set "width=404"`);
+ });
+
+ // Codepoints that are valid ASCII digits should be collected
+ // Non-ASCII digits and subsequent code points are ignored
+ [ 'innerwidth=405.5',
+ 'innerwidth=405.32',
+ 'innerwidth=405LLl',
+ 'innerwidth=405^4',
+ 'innerwidth=405*3',
+ 'innerwidth=405/5',
+ 'innerwidth=405 ',
+ 'innerwidth=405e1',
+ 'innerwidth=405e-1'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.width, 405, `"${featureString} value after first non-digit will be ignored"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerWidth=405', '', featureString);
+ }, `features "${feature}" should set "width=405"`);
+ });
+}
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-left.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-left.html
new file mode 100644
index 0000000000..c771204dc4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-left.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML: window.open `features`: non-integer values for feature `left`</title>
+<meta name=timeout content=long>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#apis-for-creating-and-navigating-browsing-contexts-by-name">
+
+<!-- user agents are not required to support open features other than `noopener`
+ and on some platforms position and size features don't make sense -->
+<meta name="flags" content="may" />
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedPostMessage.js"></script>
+<script>
+
+var featuresPrefix = `width=401,height=204,top=0,`;
+var windowURL = 'resources/message-opener.html';
+
+// https://html.spec.whatwg.org/multipage/infrastructure.html#rules-for-parsing-integers
+
+setup (() => {
+ // Before running tests, open a window using features that mimic
+ // what would happen if the feature tested here were set to 0
+ // for comparison later.
+ var featureString = `${featuresPrefix}left=0`;
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage((data, e) => {
+ e.source.close();
+ runWindowTests(data);
+ });
+ var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+});
+
+function runWindowTests (baselineDimensions) {
+ // When code point in first position is not an ASCII digit, "+" or "-",
+ // that's an error and the value becomes 0
+ [ 'left=/104',
+ 'left=_104',
+ 'left=L104'
+ ].forEach(feature => {
+ async_test(t => {
+ var featureString = `${featuresPrefix}${feature}`;
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.left, baselineDimensions.left, `"${feature} begins with an invalid character and should be ignored"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenX=' + baselineDimensions.left, '', featureString);
+ }, `features "${feature}" should NOT set "left=104"`);
+ });
+
+ // Codepoints that are valid ASCII digits should be collected
+ // Non-ASCII digits and subsequent code points are ignored
+ [ 'left=105.5',
+ 'left=105.32',
+ 'left=105LLl',
+ 'left=105^4',
+ 'left=105*3',
+ 'left=105/5',
+ 'left=105 ',
+ 'left=105e1',
+ 'left=105e-1'
+ ].forEach(feature => {
+ async_test(t => {
+ var featureString = `${featuresPrefix}${feature}`;
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.left, 105, `"${featureString} value after first non-digit will be ignored"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenX=105', '', featureString);
+ }, `features "${feature}" should set "left=105"`);
+ });
+}
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-screenx.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-screenx.html
new file mode 100644
index 0000000000..49a3783257
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-screenx.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML: window.open `features`: non-integer values for legacy feature `screenx`</title>
+<meta name=timeout content=long>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#apis-for-creating-and-navigating-browsing-contexts-by-name">
+
+<!-- user agents are not required to support open features other than `noopener`
+ and on some platforms position and size features don't make sense -->
+<meta name="flags" content="may" />
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedPostMessage.js"></script>
+<script>
+var featuresPrefix = `width=401,height=204,top=0,`;
+var windowURL = 'resources/message-opener.html';
+
+// https://html.spec.whatwg.org/multipage/infrastructure.html#rules-for-parsing-integers
+
+setup (() => {
+ // Before running tests, open a window using features that mimic
+ // what would happen if the feature tested here were set to 0
+ // for comparison later.
+ var featureString = `${featuresPrefix}left=0`;
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage((data, e) => {
+ e.source.close();
+ runWindowTests(data);
+ });
+ var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+});
+
+function runWindowTests (baselineDimensions) {
+ // When code point in first position is not an ASCII digit, "+" or "-",
+ // that's an error and the value becomes 0
+ [ 'screenx=/104',
+ 'screenx=_104',
+ 'screenx=L104'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.left, baselineDimensions.left, `"${feature} begins with an invalid character and should be ignored"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenX=' + baselineDimensions.left, '', featureString);
+ }, `features "${feature}" should NOT set "screenx=104"`);
+ });
+
+ // Codepoints that are valid ASCII digits should be collected
+ // Non-ASCII digits and subsequent code points are ignored
+ [ 'screenx=105.5',
+ 'screenx=105.32',
+ 'screenx=105LLl',
+ 'screenx=105^4',
+ 'screenx=105*3',
+ 'screenx=105/5',
+ 'screenx=105 ',
+ 'screenx=105e1',
+ 'screenx=105e-1'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.left, 105, `"${featureString} value after first non-digit will be ignored"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenX=105', '', featureString);
+ }, `features "${feature}" should set "screenx=105"`);
+ });
+}
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-screeny.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-screeny.html
new file mode 100644
index 0000000000..5183405445
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-screeny.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML: window.open `features`: non-integer values for legacy feature `screeny`</title>
+<meta name=timeout content=long>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#apis-for-creating-and-navigating-browsing-contexts-by-name">
+
+<!-- user agents are not required to support open features other than `noopener`
+ and on some platforms position and size features don't make sense -->
+<meta name="flags" content="may" />
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedPostMessage.js"></script>
+<script>
+var featuresPrefix = `height=401,width=402,left=0,`;
+var windowURL = 'resources/message-opener.html';
+
+// https://html.spec.whatwg.org/multipage/infrastructure.html#rules-for-parsing-integers
+
+setup (() => {
+ // Before running tests, open a window using features that mimic
+ // what would happen if the feature tested here were set to 0
+ // for comparison later.
+ var featureString = `${featuresPrefix}top=0`;
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage((data, e) => {
+ e.source.close();
+ runWindowTests(data);
+ });
+ var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+});
+
+function runWindowTests (baselineDimensions) {
+ // When code point in first position is not an ASCII digit, "+" or "-",
+ // that's an error and the value becomes 0
+ [ 'screeny=/404',
+ 'screeny=_404',
+ 'screeny=L404'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.top, baselineDimensions.top, `"${feature} begins with an invalid character and should be ignored"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenY=' + baselineDimensions.top, '', featureString);
+ }, `features "${feature}" should NOT set "screeny=404"`);
+ });
+
+ // Codepoints that are valid ASCII digits should be collected
+ // Non-ASCII digits and subsequent code points are ignored
+ [ 'screeny=405.5',
+ 'screeny=405.32',
+ 'screeny=405LLl',
+ 'screeny=405^4',
+ 'screeny=405*3',
+ 'screeny=405/5',
+ 'screeny=405 ',
+ 'screeny=405e1',
+ 'screeny=405e-1'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.top, 405, `"${featureString} value after first non-digit will be ignored"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenY=405', '', featureString);
+ }, `features "${feature}" should set "screeny=405"`);
+ });
+
+}
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-top.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-top.html
new file mode 100644
index 0000000000..a7926e748b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-top.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML: window.open `features`: non-integer values for feature `top`</title>
+<meta name=timeout content=long>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#apis-for-creating-and-navigating-browsing-contexts-by-name">
+
+<!-- user agents are not required to support open features other than `noopener`
+ and on some platforms position and size features don't make sense -->
+<meta name="flags" content="may" />
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedPostMessage.js"></script>
+<script>
+var windowURL = 'resources/message-opener.html';
+var featuresPrefix = `width=401,height=204,left=0,`;
+
+setup (() => {
+ // Before running tests, open a window using features that mimic
+ // what would happen if the feature tested here were set to 0
+ // for comparison later.
+ var featureString = `${featuresPrefix}top=0`;
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage((data, e) => {
+ e.source.close();
+ runWindowTests(data);
+ });
+ var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+});
+
+function runWindowTests (baselineDimensions) {
+ // When code point in first position is not an ASCII digit, "+" or "-",
+ // that's an error and the value becomes 0
+ [ 'top=/104',
+ 'top=_104',
+ 'top=L104'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.top, baselineDimensions.top, `"${feature} begins with an invalid character and should be ignored"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenY=' + baselineDimensions.top, '', featureString);
+ }, `features "${feature}" should NOT set "top=104"`);
+ });
+
+ // Codepoints that are valid ASCII digits should be collected
+ // Non-ASCII digits and subsequent code points are ignored
+ [ 'top=105.5',
+ 'top=105.32',
+ 'top=105LLl',
+ 'top=105^4',
+ 'top=105*3',
+ 'top=105/5',
+ 'top=105 ',
+ 'top=105e1',
+ 'top=105e-1'
+ ].forEach(feature => {
+ async_test(t => {
+ var featureString = `${featuresPrefix}${feature}`;
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.top, 105, `"${feature} value after first non-digit will be ignored"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenY=105', '', featureString);
+ }, `features "${feature}" should set "top=105"`);
+ });
+}
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-width.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-width.html
new file mode 100644
index 0000000000..6878063ed7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-non-integer-width.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML: window.open `features`: non-integer values for feature `width`</title>
+<meta name=timeout content=long>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#apis-for-creating-and-navigating-browsing-contexts-by-name">
+
+<!-- user agents are not required to support open features other than `noopener`
+ and on some platforms position and size features don't make sense -->
+<meta name="flags" content="may" />
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedPostMessage.js"></script>
+<script>
+var featuresPrefix = `top=0,left=0,height=401,`;
+var windowURL = 'resources/message-opener.html';
+
+// https://html.spec.whatwg.org/multipage/infrastructure.html#rules-for-parsing-integers
+
+setup (() => {
+ // Before running tests, open a window using features that mimic
+ // what would happen if the feature tested here were set to 0
+ // for comparison later.
+ var featureString = `${featuresPrefix}width=0`;
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage((data, e) => {
+ e.source.close();
+ runWindowTests(data);
+ });
+ var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+});
+
+function runWindowTests (baselineDimensions) {
+
+ // The absence of the sizing feature should have the same behavior
+ // as that feature set to 0
+ [ featuresPrefix,
+ 'top=0,left=0',
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.width, baselineDimensions.width);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerWidth=' + baselineDimensions.width, '', feature);
+ }, `${feature}: absence of feature "width" should be treated same as "width=0"`);
+ });
+
+ // When code point in first position is not an ASCII digit, "+" or "-",
+ // that's an error and the value becomes 0
+ [ 'width=/404',
+ 'width=_404',
+ 'width=L404'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.width, baselineDimensions.width, `"${feature} begins with an invalid character and should be ignored"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerWidth=' + baselineDimensions.width, '', featureString);
+ }, `features "${feature}" should NOT set "width=404"`);
+ });
+
+ // Codepoints that are valid ASCII digits should be collected
+ // Non-ASCII digits and subsequent code points are ignored
+ [ 'width=405.5',
+ 'width=405.32',
+ 'width=405LLl',
+ 'width=405^4',
+ 'width=405*3',
+ 'width=405/5',
+ 'width=405 ',
+ 'width=405e1',
+ 'width=405e-1'
+ ].forEach(feature => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ var featureString = `${featuresPrefix}${feature}`;
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.width, 405, `"${featureString} value after first non-digit will be ignored"`);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + "&expected_innerWidth=405", '', featureString);
+ }, `features "${feature}" should set "width=405"`);
+ });
+}
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-innerheight-innerwidth.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-innerheight-innerwidth.html
new file mode 100644
index 0000000000..cf3ad25514
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-innerheight-innerwidth.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML: window.open `features`: tokenization -- legacy size features `innerheight`, `innerwidth`</title>
+<meta name=timeout content=long>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#apis-for-creating-and-navigating-browsing-contexts-by-name">
+
+<!-- user agents are not required to support open features other than `noopener`
+ and on some platforms position and size features don't make sense -->
+<meta name="flags" content="may" />
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedPostMessage.js"></script>
+<script>
+var windowURL = 'resources/message-opener.html';
+var width = 'width=401,';
+var height = 'height=402,';
+
+[ 'innerwidth=401',
+ ' innerwidth = 401',
+ 'innerwidth==401',
+ '\ninnerwidth= 401',
+ ',innerwidth=401,,',
+ 'INNERWIDTH=401',
+ 'innerWidth=401'
+].forEach((features, idx, arr) => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.width, 401);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerWidth=401', '', height + features);
+ }, `${format_value(features)} should set width of opened window`);
+});
+
+[ 'innerheight=402',
+ ' innerheight = 402',
+ 'innerheight==402',
+ '\ninnerheight= 402',
+ ',innerheight=402,,',
+ 'INNERHEIGHT=402',
+ 'innerHeight=402'
+].forEach((features, idx, arr) => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.height, 402);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerHeight=402', '', width + features);
+ }, `${format_value(features)} should set height of opened window`);
+});
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-noopener.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-noopener.html
new file mode 100644
index 0000000000..c955e67789
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-noopener.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML: window.open `features`: tokenization -- `noopener`</title>
+<meta name=timeout content=long>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#apis-for-creating-and-navigating-browsing-contexts-by-name">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/tokenization-noopener-noreferrer.js"></script>
+<script>
+ booleanTests("noopener");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-noreferrer.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-noreferrer.html
new file mode 100644
index 0000000000..4807f634fd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-noreferrer.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML: window.open `features`: tokenization -- `noreferrer`</title>
+<meta name=timeout content=long>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#apis-for-creating-and-navigating-browsing-contexts-by-name">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/tokenization-noopener-noreferrer.js"></script>
+<script>
+ booleanTests("noreferrer");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-screenx-screeny.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-screenx-screeny.html
new file mode 100644
index 0000000000..5a53fefc40
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-screenx-screeny.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML: window.open `features`: tokenization -- legacy position features `screenx`, `screeny`</title>
+<meta name=timeout content=long>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#apis-for-creating-and-navigating-browsing-contexts-by-name">
+
+<!-- user agents are not required to support open features other than `noopener`
+ and on some platforms position and size features don't make sense -->
+<meta name="flags" content="may" />
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedPostMessage.js"></script>
+<script>
+var windowURL = 'resources/message-opener.html';
+var width = 'width=401,';
+var height = 'height=402,';
+
+[ 'screenx=141',
+ ' screenx = 141',
+ 'screenx==141',
+ '\nscreenx= 141',
+ ',screenx=141,,',
+ 'SCREENX=141',
+ 'screenX=141'
+].forEach((features, idx, arr) => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.left, 141);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenX=141', '', width + height + features);
+ }, `${format_value(features)} should set left position of opened window`);
+});
+
+[ 'screeny=142',
+ ' screeny = 142',
+ 'screeny==142',
+ '\nscreeny= 142',
+ ',screeny=142,,',
+ 'SCREENY=142',
+ 'screenY=142'
+].forEach((features, idx, arr) => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.top, 142);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenY=142', '', width + height + features);
+ }, `${format_value(features)} should set top position of opened window`);
+});
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-top-left.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-top-left.html
new file mode 100644
index 0000000000..842cbcf820
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-top-left.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML: window.open `features`: tokenization -- position features `top` and `left`</title>
+<meta name=timeout content=long>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#apis-for-creating-and-navigating-browsing-contexts-by-name">
+
+<!-- user agents are not required to support open features other than `noopener`
+ and on some platforms position and size features don't make sense -->
+<meta name="flags" content="may" />
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedPostMessage.js"></script>
+<script>
+var windowURL = 'resources/message-opener.html';
+var width = 'width=401,';
+var height = 'height=402,';
+
+[ 'left=141',
+ ' left = 141',
+ 'left==141',
+ '\nleft= 141',
+ ',left=141,,',
+ 'LEFT=141'
+].forEach((features, idx, arr) => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.left, 141);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenX=141', '', width + height + features);
+ }, `"${features}" should set left position of opened window`);
+});
+
+[ 'top=142',
+ ' top = 142',
+ 'top==142',
+ '\ttop= 142',
+ ',top=142,,',
+ 'TOP=142'
+].forEach((features, idx, arr) => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.top, 142);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenY=142', '', width + height + features);
+ }, `${format_value(features)} should set top position of opened window`);
+});
+
+[ 'top=152,left=152',
+ 'top=152,,left=152,',
+ 'top=152==left=152',
+ ',,top= 152, left=152'
+].forEach((features, idx, arr) => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.top, 152);
+ assert_equals(data.left, 152);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenX=152&expected_screenY=152', '', width + height + features);
+ }, `${format_value(features)} should set top and left position of opened window`);
+});
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-width-height.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-width-height.html
new file mode 100644
index 0000000000..ff61199179
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/open-features-tokenization-width-height.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML: window.open `features`: tokenization -- size features `width` and `height`</title>
+<meta name=timeout content=long>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#apis-for-creating-and-navigating-browsing-contexts-by-name">
+
+<!-- user agents are not required to support open features other than `noopener`
+ and on some platforms position and size features don't make sense -->
+<meta name="flags" content="may" />
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedPostMessage.js"></script>
+<script>
+var windowURL = 'resources/message-opener.html';
+var width = 'width=401,';
+var height = 'height=402,';
+
+[ 'width=401',
+ ' width = 401',
+ 'width==401',
+ '\nwidth= 401',
+ ',width=401,,',
+ 'WIDTH=401'
+].forEach((features, idx, arr) => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.width, 401);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerWidth=401', '', height + features);
+ }, `${format_value(features)} should set width of opened window`);
+});
+
+[ 'height=402',
+ ' height = 402',
+ 'height==402',
+ '\nheight= 402',
+ ',height=402,,',
+ 'HEIGHT=402'
+].forEach((features, idx, arr) => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.height, 402);
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerHeight=402', '', width + features);
+ }, `${format_value(features)} should set height of opened window`);
+});
+
+[ 'height=402,width=401',
+ ' height = 402 , width = 401 ,',
+ 'height==402 width = 401',
+ '\nheight= 402,,width=\n401',
+ ',height=402,,width==401',
+ 'HEIGHT=402, WIDTH=401'
+].forEach((features, idx, arr) => {
+ async_test(t => {
+ var prefixedMessage = new PrefixedMessageTest();
+ prefixedMessage.onMessage(t.step_func_done((data, e) => {
+ e.source.close();
+ assert_equals(data.height, 402);
+ assert_equals(data.width, 401)
+ }));
+ var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerHeight=402&expected_innerWidth=401', '', features);
+ }, `${format_value(features)} should set height and width of opened window`);
+});
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/resources/close-self.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/resources/close-self.html
new file mode 100644
index 0000000000..0c0cf9fc49
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/resources/close-self.html
@@ -0,0 +1,3 @@
+<script>
+ window.close();
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/resources/is-popup-barprop.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/resources/is-popup-barprop.html
new file mode 100644
index 0000000000..5636e29878
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/resources/is-popup-barprop.html
@@ -0,0 +1,15 @@
+<script src="/common/PrefixedPostMessage.js"></script>
+<script>
+var prefixedMessage = new PrefixedMessageResource();
+function sendBarProps() {
+ prefixedMessage.postToOpener({
+ locationbar: window.locationbar.visible,
+ menubar: window.menubar.visible,
+ personalbar: window.personalbar.visible,
+ scrollbars: window.scrollbars.visible,
+ statusbar: window.statusbar.visible,
+ toolbar: window.toolbar.visible,
+ });
+}
+window.addEventListener('load', sendBarProps);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/resources/message-opener.html b/testing/web-platform/tests/html/browsers/the-window-object/open-close/resources/message-opener.html
new file mode 100644
index 0000000000..39ad139769
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/resources/message-opener.html
@@ -0,0 +1,52 @@
+<script src="/common/PrefixedPostMessage.js"></script>
+<script>
+var prefixedMessage = new PrefixedMessageResource();
+var max = 150, attempts = 0;
+
+const urlParams = new URLSearchParams(location.search);
+const expected_innerWidth = urlParams.get('expected_innerWidth');
+const expected_innerHeight = urlParams.get('expected_innerHeight');
+const expected_screenX = urlParams.get('expected_screenX');
+const expected_screenY = urlParams.get('expected_screenY');
+let should_wait_until_settled = expected_innerWidth === null && expected_innerHeight === null && expected_screenX === null && expected_screenY === null;
+
+function sendCoordinates() {
+ // Certain windowing systems position windows asynchronously.
+ // As a result, the window may not be positioned yet when the
+ // load event fires. To accommodate this, allow waiting up to
+ // 15 seconds for positioning to take place.
+ if ((!window.screenX && expected_screenX) &&
+ (!window.screenY && expected_screenY) && ++attempts < max) {
+ setTimeout(sendCoordinates, 100);
+ return;
+ }
+ if (expected_innerWidth && window.innerWidth != expected_innerWidth && ++attempts < max) {
+ setTimeout(sendCoordinates, 10);
+ return;
+ }
+ if (expected_innerHeight && window.innerHeight != expected_innerHeight && ++attempts < max) {
+ setTimeout(sendCoordinates, 10);
+ return;
+ }
+ if (expected_screenX && window.screenX != expected_screenX && ++attempts < max) {
+ setTimeout(sendCoordinates, 10);
+ return;
+ }
+ if (expected_screenY && window.screenY != expected_screenY && ++attempts < max) {
+ setTimeout(sendCoordinates, 10);
+ return;
+ }
+ if (should_wait_until_settled) {
+ should_wait_until_settled = false;
+ setTimeout(sendCoordinates, 300);
+ return;
+ }
+ prefixedMessage.postToOpener({
+ left: window.screenX,
+ top: window.screenY,
+ width: window.innerWidth,
+ height: window.innerHeight
+ });
+}
+window.addEventListener('load', sendCoordinates);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/open-close/resources/tokenization-noopener-noreferrer.js b/testing/web-platform/tests/html/browsers/the-window-object/open-close/resources/tokenization-noopener-noreferrer.js
new file mode 100644
index 0000000000..a9d42e26de
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/open-close/resources/tokenization-noopener-noreferrer.js
@@ -0,0 +1,152 @@
+function booleanTests(feature) {
+ const windowURL = 'resources/close-self.html';
+ // Tests for how windows features are tokenized into 'name', 'value'
+ // window features separators are ASCII whitespace, '=' and ','
+
+ const featureUpper = feature.toUpperCase(),
+ featureSplitBegin = feature.slice(0, 2),
+ featureSplitEnd = feature.slice(2),
+ featureMixedCase = featureSplitBegin.toUpperCase() + featureSplitEnd;
+ featureMixedCase2 = featureSplitBegin + featureSplitEnd.toUpperCase();
+
+ test (t => {
+ // Tokenizing `name`: initial window features separators are ignored
+ // Each of these variants should tokenize to (`${feature}`, '')
+ [
+ ` ${feature}`,
+ `=${feature}`,
+ `,,${feature}`,
+ `,=, ${feature}`,
+ `\n=${feature}=`,
+ `\t${feature}`,
+ `\r,,,=${feature}`,
+ `\u000C${feature}`
+ ].forEach(variant => {
+ const win = window.open(windowURL, "", variant);
+ assert_equals(win, null, `"${variant}" should activate feature "${feature}"`);
+ });
+ }, `Tokenization of "${feature}" should skip window features separators before feature`);
+
+ test (t => {
+ // Tokenizing `name`: lowercase conversion
+ // Each of these variants should tokenize as feature (`${feature}`, '')
+ // except where indicated
+ // Note also that `value` is lowercased during tokenization
+ [
+ `${featureUpper}`,
+ `${featureMixedCase}`,
+ ` ${featureMixedCase2}`,
+ `=${featureUpper}`,
+ `${featureUpper}=1`,
+ `${featureUpper}=1`,
+ `${featureUpper}=yes`,
+ `${feature}=YES`,
+ ].forEach(variant => {
+ const win = window.open(windowURL, '', variant);
+ assert_equals(win, null, `"${variant}" should activate feature "${feature}"`);
+ });
+ }, `Feature "${feature}" should be converted to ASCII lowercase`);
+
+ test (t => {
+ // After `name` has been collected, ignore any window features separators until '='
+ // except ',' OR a non-window-features-separator — break in those cases
+ // i.e. ignore whitespace until '=' unless a ',' is encountered first
+ // Each of these variants should tokenize as feature ('noopener', '')
+ [
+ `${feature}`,
+ ` ${feature}\r`,
+ `${feature}\n =`,
+ `${feature},`,
+ `${feature} =,`,
+ `, ${feature} =`,
+ `${feature},=`,
+ `${feature} foo`,
+ `foo ${feature}=1`,
+ `foo=\u000Cbar\u000C${feature}`
+ ].forEach(variant => {
+ const win = window.open(windowURL, '', variant);
+ assert_equals(win, null, `"${variant}" should activate feature "${feature}"`);
+ });
+ }, `After "${feature}", tokenization should skip window features separators that are not "=" or ","`);
+
+ test (t => {
+ // After initial '=', tokenizing should ignore all separators except ','
+ // before collecting `value`
+ // Each of these variants should tokenize as feature ('noopener', '')
+ // Except where indicated
+ [
+ `${feature}= yes`,
+ `${feature}==,`,
+ `${feature}=\n ,`,
+ `${feature} = \t ,`,
+ `${feature}\n=\r 1,`,
+ `${feature}=,yes`,
+ `${feature}= yes=,`,
+ `${feature} = \u000Cyes`
+ ].forEach(variant => {
+ const win = window.open(windowURL, '', variant);
+ assert_equals(win, null, `"${variant}" should activate feature "${feature}"`);
+ });
+ }, `Tokenizing "${feature}" should ignore window feature separators except "," after initial "=" and before value`);
+
+ test (t => {
+ // Tokenizing `value` should collect any non-separator code points until first separator
+ [
+ `${feature}=1`,
+ `${feature}=yes`,
+ `${feature} = yes ,`,
+ `${feature}=\nyes ,`,
+ `${feature}=yes yes`,
+ `${feature}=yes\ts`,
+ `${feature}==`,
+ `${feature}=1\n,`,
+ `==${feature}===`,
+ `${feature}==\u000C`
+ ].forEach(variant => {
+ const win = window.open(windowURL, '', variant);
+ assert_equals(win, null, `"${variant}" should set "${feature}"`);
+ });
+ }, `Tokenizing "${feature}" should read characters until first window feature separator as \`value\``);
+
+ test (t => {
+ [
+ `${feature}=1`,
+ `${feature}=2`,
+ `${feature}=12345`,
+ `${feature}=1.5`,
+ `${feature}=-1`,
+ ].forEach(variant => {
+ const win = window.open(windowURL, '', variant);
+ assert_equals(win, null, `"${variant}" should activate feature "${feature}"`);
+ });
+ }, 'Integer values other than 0 should activate the feature');
+
+ test (t => {
+ [
+ `${feature}=0`,
+ `${feature}=0.5`,
+ `${feature}=error`,
+ ].forEach(variant => {
+ const win = window.open(windowURL, '', variant);
+ assert_not_equals(win, null, `"${variant}" should NOT activate feature "${feature}"`);
+ });
+ }, `Integer value of 0 should not activate "${feature}"`);
+
+ test (t => {
+ [
+ `-${feature}`,
+ `${featureUpper}RRR`,
+ `${featureMixedCase}R`,
+ `${featureSplitBegin}_${featureSplitEnd}`,
+ ` ${featureSplitBegin} ${featureSplitEnd}`,
+ `${featureSplitBegin}\n${featureSplitEnd}`,
+ `${featureSplitBegin},${featureSplitEnd}`,
+ `\0${feature}`,
+ `${feature}\u0000=yes`,
+ `foo=\u000C${feature}`
+ ].forEach(variant => {
+ const win = window.open(windowURL, '', variant);
+ assert_not_equals(win, null, `"${variant}" should NOT activate feature "${feature}"`);
+ });
+ }, `Invalid feature names should not tokenize as "${feature}"`);
+}
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/proxy-getOwnPropertyDescriptor.html b/testing/web-platform/tests/html/browsers/the-window-object/proxy-getOwnPropertyDescriptor.html
new file mode 100644
index 0000000000..4ea9504870
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/proxy-getOwnPropertyDescriptor.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>getOwnPropertyDescriptor() is correct for Proxy with host object target</title>
+ <link rel="help" href="https://html.spec.whatwg.org/multipage/#window">
+ <link rel="help" href="https://webidl.spec.whatwg.org/#Unforgeable">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<script>
+'use strict';
+
+const assert_accessor_descriptor_equals = (actual, expected) => {
+ assert_equals(actual.get, expected.get, 'get');
+ assert_equals(actual.set, expected.set, 'set');
+ assert_equals(actual.enumerable, expected.enumerable, 'enumerable');
+ assert_equals(actual.configurable, expected.configurable, 'configurable');
+};
+
+const assert_data_descriptor_equals = (actual, expected) => {
+ assert_equals(actual.value, expected.value, 'value');
+ assert_equals(actual.writable, expected.writable, 'writable');
+ assert_equals(actual.enumerable, expected.enumerable, 'enumerable');
+ assert_equals(actual.configurable, expected.configurable, 'configurable');
+};
+
+test(() => {
+ const windowProxy = new Proxy(window, {});
+ name = 'old_name';
+ const descriptor = Object.getOwnPropertyDescriptor(windowProxy, 'name');
+
+ assert_equals(descriptor.get.call(window), 'old_name');
+ descriptor.set.call(window, 'new_name');
+ assert_equals(name, 'new_name');
+ assert_true(descriptor.enumerable);
+ assert_true(descriptor.configurable);
+}, 'Window target, no trap, "name" attribute');
+
+test(() => {
+ let trapCalls = 0;
+ const windowProxy = new Proxy(window, {
+ getOwnPropertyDescriptor(...args) {
+ trapCalls++;
+ return Reflect.getOwnPropertyDescriptor(...args);
+ },
+ });
+
+ assert_accessor_descriptor_equals(
+ Object.getOwnPropertyDescriptor(windowProxy, 'document'),
+ Object.getOwnPropertyDescriptor(window, 'document')
+ );
+ assert_equals(trapCalls, 1);
+}, 'Window target, forwarding trap, [LegacyUnforgeable] "document" attribute');
+
+test(() => {
+ const trapResult = {get() {}, set(_val) {}, enumerable: false, configurable: true};
+ const windowProxy = new Proxy(new Proxy(window, {}), {
+ getOwnPropertyDescriptor: () => trapResult,
+ });
+
+ assert_accessor_descriptor_equals(
+ Object.getOwnPropertyDescriptor(windowProxy, 'onclick'),
+ trapResult
+ );
+}, 'Window proxy target, custom trap, "onclick" event handler attribute');
+
+test(() => {
+ let trapCalls = 0;
+ const documentProxy = new Proxy(document, {
+ getOwnPropertyDescriptor(...args) {
+ trapCalls++;
+ return Reflect.getOwnPropertyDescriptor(...args);
+ },
+ });
+
+ assert_accessor_descriptor_equals(
+ Object.getOwnPropertyDescriptor(documentProxy, 'location'),
+ Object.getOwnPropertyDescriptor(document, 'location')
+ );
+ assert_equals(trapCalls, 1);
+}, 'Document target, forwarding trap, [LegacyUnforgeable] "location" attribute');
+
+test(() => {
+ const trapResult = {value: 4, writable: false, enumerable: true, configurable: true};
+ const documentProxy = new Proxy(new Proxy(document, {}), {
+ getOwnPropertyDescriptor: () => trapResult,
+ });
+
+ assert_data_descriptor_equals(
+ Object.getOwnPropertyDescriptor(documentProxy, 'foo'),
+ trapResult
+ );
+}, 'Document proxy target, custom trap, non-existent value attribute');
+
+test(() => {
+ const locationProxy = new Proxy(location, {});
+ location.hash = '#old';
+ const descriptor = Object.getOwnPropertyDescriptor(locationProxy, 'hash');
+
+ assert_equals(descriptor.get.call(location), '#old');
+ descriptor.set.call(location, '#new');
+ assert_equals(location.hash, '#new');
+ assert_true(descriptor.enumerable);
+ assert_false(descriptor.configurable);
+}, 'Location target, no trap, [LegacyUnforgeable] "hash" attribute');
+
+test(() => {
+ let trapCalls = 0;
+ const locationProxy = new Proxy(new Proxy(location, {}), {
+ getOwnPropertyDescriptor(...args) {
+ trapCalls++;
+ return Reflect.getOwnPropertyDescriptor(...args);
+ },
+ });
+
+ assert_data_descriptor_equals(
+ Object.getOwnPropertyDescriptor(locationProxy, 'reload'),
+ Object.getOwnPropertyDescriptor(location, 'reload')
+ );
+ assert_equals(trapCalls, 1);
+}, 'Location proxy target, forwarding trap, [LegacyUnforgeable] "reload" method');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/security-window/window-security.https.html b/testing/web-platform/tests/html/browsers/the-window-object/security-window/window-security.https.html
new file mode 100644
index 0000000000..68af5bd90b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/security-window/window-security.https.html
@@ -0,0 +1,200 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: Window Security</title>
+<link rel="author" title="Intel" href="http://www.intel.com/" />
+<link rel="help" href="https://html.spec.whatwg.org/multipage/multipage/browsers.html#the-window-object" />
+<link rel="help" href="https://html.spec.whatwg.org/multipage/multipage/timers.html#timers" />
+<link rel="help" href="https://html.spec.whatwg.org/multipage/multipage/webappapis.html#atob" />
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#windowsessionstorage" />
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#windowlocalstorage" />
+<link rel="help" href="https://html.spec.whatwg.org/multipage/multipage/browsers.html#window" />
+<link rel="help" href="http://dev.w3.org/csswg/cssom/#extensions-to-the-window-interface" />
+<link rel="help" href="http://dev.w3.org/csswg/cssom-view/#extensions-to-the-window-interface" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test("Window Security testing");
+
+function fr_load() {
+ fr = document.getElementById("fr");
+
+ t.step(function () {
+ //SecurityError should be thrown
+ [
+ //attributes
+ {name: "devicePixelRatio"},
+ {name: "document"},
+ {name: "external"},
+ {name: "frameElement"},
+ {name: "history"},
+ {name: "innerWidth"},
+ {name: "innerHeight"},
+ {name: "locationbar"},
+ {name: "localStorage"},
+ {name: "menubar"},
+ {name: "name"},
+ {name: "navigator"},
+ {name: "onabort"},
+ {name: "onafterprint"},
+ {name: "onbeforeprint"},
+ {name: "onbeforeunload"},
+ {name: "onblur"},
+ {name: "oncancel"},
+ {name: "oncanplay"},
+ {name: "oncanplaythrough"},
+ {name: "onchange"},
+ {name: "onclick"},
+ {name: "onclose"},
+ {name: "oncontextmenu"},
+ {name: "oncuechange"},
+ {name: "ondblclick"},
+ {name: "ondrag"},
+ {name: "ondragend"},
+ {name: "ondragenter"},
+ {name: "ondragleave"},
+ {name: "ondragover"},
+ {name: "ondragstart"},
+ {name: "ondrop"},
+ {name: "ondurationchange"},
+ {name: "onemptied"},
+ {name: "onended"},
+ {name: "onerror"},
+ {name: "onfocus"},
+ {name: "onhashchange"},
+ {name: "oninput"},
+ {name: "oninvalid"},
+ {name: "onkeydown"},
+ {name: "onkeypress"},
+ {name: "onkeyup"},
+ {name: "onload"},
+ {name: "onloadeddata"},
+ {name: "onloadedmetadata"},
+ {name: "onloadstart"},
+ {name: "onmessage"},
+ {name: "onmousedown"},
+ {name: "onmousemove"},
+ {name: "onmouseout"},
+ {name: "onmouseover"},
+ {name: "onmouseup"},
+ {name: "onmousewheel"},
+ {name: "onoffline"},
+ {name: "ononline"},
+ {name: "onpause"},
+ {name: "onplay"},
+ {name: "onplaying"},
+ {name: "onpagehide"},
+ {name: "onpageshow"},
+ {name: "onpopstate"},
+ {name: "onprogress"},
+ {name: "onratechange"},
+ {name: "onreset"},
+ {name: "onresize"},
+ {name: "onscroll"},
+ {name: "onseeked"},
+ {name: "onseeking"},
+ {name: "onselect"},
+ {name: "onstalled"},
+ {name: "onstorage"},
+ {name: "onsubmit"},
+ {name: "onsuspend"},
+ {name: "ontimeupdate"},
+ {name: "onunload"},
+ {name: "onvolumechange"},
+ {name: "onwaiting"},
+ {name: "pageXOffset"},
+ {name: "pageYOffset"},
+ {name: "personalbar"},
+ {name: "screen"},
+ {name: "scrollbars"},
+ {name: "statusbar"},
+ {name: "status"},
+ {name: "screenX"},
+ {name: "screenY"},
+ {name: "sessionStorage"},
+ {name: "toolbar"},
+ //methods
+ {name: "alert", isMethod: true},
+ {name: "clearInterval", isMethod: true, args:[1]},
+ {name: "clearTimeout", isMethod: true, args:[function () {}, 1]},
+ {name: "confirm", isMethod: true},
+ {name: "getComputedStyle", isMethod: true, args:[document.body, null]},
+ {name: "getSelection", isMethod: true},
+ {name: "matchMedia", isMethod: true, args:["(min-width:50px)"]},
+ {name: "moveBy", isMethod: true, args:[10, 10]},
+ {name: "moveTo", isMethod: true, args:[10, 10]},
+ {name: "open", isMethod: true},
+ {name: "print", isMethod: true},
+ {name: "prompt", isMethod: true},
+ {name: "resizeTo", isMethod: true, args:[10, 10]},
+ {name: "resizeBy", isMethod: true, args:[10, 10]},
+ {name: "scroll", isMethod: true, args:[10, 10]},
+ {name: "scrollTo", isMethod: true, args:[10, 10]},
+ {name: "scrollBy", isMethod: true, args:[10, 10]},
+ {name: "setInterval", isMethod: true, args:[function () {}, 1]},
+ {name: "setTimeout", isMethod: true, args:[function () {}, 1]},
+ {name: "stop", isMethod: true},
+ ].forEach(function (item) {
+ test(function () {
+ assert_true(item.name in window, "window." + item.name + " should exist.");
+ assert_throws_dom("SecurityError", function () {
+ if (item.isMethod)
+ if (item.args)
+ fr.contentWindow[item.name](item.args[0], item.args[1]);
+ else
+ fr.contentWindow[item.name]();
+ else
+ fr.contentWindow[item.name];
+ }, "A SecurityError exception should be thrown.");
+ }, "A SecurityError exception must be thrown when window." + item.name + " is accessed from a different origin.");
+ });
+
+ //SecurityError should not be thrown
+ [
+ //attributes
+ {name: "closed"},
+ {name: "frames"},
+ {name: "length"},
+ {name: "location"},
+ {name: "opener"},
+ {name: "parent"},
+ {name: "self"},
+ {name: "top"},
+ {name: "window"},
+ //methods
+ {name: "blur", isMethod: true},
+ {name: "close", isMethod: true},
+ {name: "focus", isMethod: true},
+ {name: "postMessage", isMethod: true, args: [{msg: 'foo'}, "*"]}
+ ].forEach(function (item) {
+ test(function () {
+ assert_true(item.name in window, "window." + item.name + " should exist.");
+ try {
+ if (item.isMethod)
+ if (item.args)
+ fr.contentWindow[item.name](item.args[0], item.args[1]);
+ else
+ fr.contentWindow[item.name]();
+ else
+ fr.contentWindow[item.name];
+ } catch (e) {
+ assert_unreached("An unexpected exception was thrown.");
+ }
+ }, "A SecurityError exception should not be thrown when window." + item.name + " is accessed from a different origin.");
+ });
+ });
+ t.done();
+}
+
+</script>
+<script>
+onload = function() {
+ var frame = document.createElement('iframe');
+ frame.id = "fr";
+ frame.setAttribute("style", "display:none");
+ frame.setAttribute('src', get_host_info().HTTPS_REMOTE_ORIGIN + "/");
+ frame.setAttribute("onload", "fr_load()");
+ document.body.appendChild(frame);
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/self-et-al.window.js b/testing/web-platform/tests/html/browsers/the-window-object/self-et-al.window.js
new file mode 100644
index 0000000000..c42522803a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/self-et-al.window.js
@@ -0,0 +1,43 @@
+function delayed_assert_done(t, w, windowProxySelfReference) {
+ // Let's make sure nobody is being sneaky
+ t.step_timeout(() => {
+ t.step_timeout(() => {
+ assert_equals(w[windowProxySelfReference], w, `${windowProxySelfReference} got cleared after some time`);
+ t.done();
+ }, 0);
+ }, 0);
+}
+
+[
+ "frames",
+ "globalThis",
+ "self",
+ "window"
+].forEach(windowProxySelfReference => {
+ async_test(t => {
+ const frame = document.body.appendChild(document.createElement("iframe")),
+ otherW = frame.contentWindow;
+ assert_equals(otherW[windowProxySelfReference], otherW, `${windowProxySelfReference} is broken`);
+ frame.remove();
+ assert_equals(otherW[windowProxySelfReference], otherW, `${windowProxySelfReference} got cleared after browsing context removal`);
+ assert_true(otherW.closed);
+
+ delayed_assert_done(t, otherW, windowProxySelfReference);
+ }, `iframeWindow.${windowProxySelfReference} before and after removal`);
+
+ async_test(t => {
+ const otherW = window.open();
+ assert_equals(otherW[windowProxySelfReference], otherW, `${windowProxySelfReference} is broken`);
+ otherW.onpagehide = t.step_func(() => {
+ assert_equals(otherW[windowProxySelfReference], otherW, `${windowProxySelfReference} got cleared after browsing context pagehide`);
+ t.step_timeout(() => {
+ assert_equals(otherW.opener, null); // Ensure browsing context is discarded
+ assert_equals(otherW[windowProxySelfReference], otherW, `${windowProxySelfReference} got cleared after browsing context removal`);
+ delayed_assert_done(t, otherW, windowProxySelfReference);
+ }, 0);
+ });
+ otherW.close();
+ assert_equals(otherW[windowProxySelfReference], otherW, `${windowProxySelfReference} got cleared after browsing context closure`);
+ assert_true(otherW.closed);
+ }, `popupWindow.${windowProxySelfReference} before, after closing, and after discarding`)
+});
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/support/BarProp-target.html b/testing/web-platform/tests/html/browsers/the-window-object/support/BarProp-target.html
new file mode 100644
index 0000000000..9921e7a577
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/support/BarProp-target.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<script>
+ const barProps = ["locationbar", "menubar", "personalbar", "scrollbars", "statusbar", "toolbar"];
+ const barPropsObj = {};
+ const channelName = location.search.substr(1);
+ const channel = new BroadcastChannel(channelName);
+ for (const prop of barProps) {
+ barPropsObj[prop] = window[prop].visible;
+ }
+ channel.postMessage(barPropsObj);
+
+ // Because messages are not delivered synchronously and because closing a
+ // browsing context prompts the eventual clearing of all task sources, this
+ // document should not be closed until the opener document has confirmed
+ // receipt.
+ channel.onmessage = () => { window.close() };
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/support/closed.html b/testing/web-platform/tests/html/browsers/the-window-object/support/closed.html
new file mode 100644
index 0000000000..3b70598e34
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/support/closed.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<!--
+ There's two URL parameters supported here:
+
+ window: The property name of a getter on the global object that returns the relevant WindowProxy
+ object to message.
+ ident: A reasonably unique identifier that will be echoed as the message.
+-->
+<script>
+ const params = new URLSearchParams(location.search);
+ self[params.get("window")].postMessage(params.get("ident"), "*");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/support/noopener-target.html b/testing/web-platform/tests/html/browsers/the-window-object/support/noopener-target.html
new file mode 100644
index 0000000000..41e197a746
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/support/noopener-target.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<script>
+ var channelName = location.search.substr(1);
+ var channel = new BroadcastChannel(channelName);
+ channel.postMessage({ name: window.name,
+ haveOpener: window.opener !== null });
+
+ // Because messages are not delivered synchronously and because closing a
+ // browsing context prompts the eventual clearing of all task sources, this
+ // document should not be closed until the opener document has confirmed
+ // receipt.
+ channel.onmessage = function() {
+ window.close();
+ };
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/support/noreferrer-target.html b/testing/web-platform/tests/html/browsers/the-window-object/support/noreferrer-target.html
new file mode 100644
index 0000000000..c2446c6fe9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/support/noreferrer-target.html
@@ -0,0 +1,13 @@
+<script>
+ const channelName = location.search.substr(1),
+ channel = new BroadcastChannel(channelName);
+ channel.postMessage({ name: window.name,
+ haveOpener: window.opener !== null,
+ referrer: document.referrer });
+
+ // Because messages are not delivered synchronously and because closing a
+ // browsing context prompts the eventual clearing of all task sources, this
+ // document should not be closed until the opener document has confirmed
+ // receipt.
+ channel.onmessage = () => window.close();
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/support/same-origin-iframe.html b/testing/web-platform/tests/html/browsers/the-window-object/support/same-origin-iframe.html
new file mode 100644
index 0000000000..763d9e466b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/support/same-origin-iframe.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script>
+ window.addEventListener('load', () => { window.didLoadFrame = true; });
+ </script>
+ </head>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/support/sizing-target.html b/testing/web-platform/tests/html/browsers/the-window-object/support/sizing-target.html
new file mode 100644
index 0000000000..7cd5348a85
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/support/sizing-target.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<script>
+ const windowProps = ["innerWidth", "innerHeight"];
+ const windowPropsObj = {};
+ const channelName = location.search.substr(1);
+ const channel = new BroadcastChannel(channelName);
+ for (const prop of windowProps) {
+ windowPropsObj[prop] = window[prop];
+ }
+ channel.postMessage(windowPropsObj);
+
+ // Because messages are not delivered synchronously and because closing a
+ // browsing context prompts the eventual clearing of all task sources, this
+ // document should not be closed until the opener document has confirmed
+ // receipt.
+ channel.onmessage = () => { window.close() };
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/support/window-open-popup-target.html b/testing/web-platform/tests/html/browsers/the-window-object/support/window-open-popup-target.html
new file mode 100644
index 0000000000..a0588de829
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/support/window-open-popup-target.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<script>
+ var channelName = window.name;
+ var channel = new BroadcastChannel(channelName);
+ const allBarProps = [
+ window.locationbar.visible,
+ window.menubar.visible,
+ window.personalbar.visible,
+ window.scrollbars.visible,
+ window.statusbar.visible,
+ window.toolbar.visible
+ ];
+ const allTrue = allBarProps.every(x=>x);
+ const allFalse = allBarProps.every(x=>!x);
+ channel.postMessage({isPopup: allFalse, mixedState: !allTrue && !allFalse});
+
+ // Because messages are not delivered synchronously and because closing a
+ // browsing context prompts the eventual clearing of all task sources, this
+ // document should not be closed until the opener document has confirmed
+ // receipt.
+ channel.onmessage = function() {
+ window.close();
+ };
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/support/windowFeature-values-target.html b/testing/web-platform/tests/html/browsers/the-window-object/support/windowFeature-values-target.html
new file mode 100644
index 0000000000..3a78ddf660
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/support/windowFeature-values-target.html
@@ -0,0 +1,24 @@
+<script>
+ const channelName = location.search.substr(1),
+ channel = new BroadcastChannel(channelName);
+
+ const haveOpener = window.opener !== null;
+ const haveReferrer = document.referrer !== null && document.referrer !== "";
+ const allBarProps = [
+ window.locationbar.visible,
+ window.menubar.visible,
+ window.personalbar.visible,
+ window.scrollbars.visible,
+ window.statusbar.visible,
+ window.toolbar.visible
+ ];
+ const isPopup = allBarProps.every(x=>!x);
+
+ channel.postMessage({haveOpener, haveReferrer, isPopup});
+
+ // Because messages are not delivered synchronously and because closing a
+ // browsing context prompts the eventual clearing of all task sources, this
+ // document should not be closed until the opener document has confirmed
+ // receipt.
+ channel.onmessage = () => window.close();
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/window-aliases.html b/testing/web-platform/tests/html/browsers/the-window-object/window-aliases.html
new file mode 100644
index 0000000000..135be02a30
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/window-aliases.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Aliases of the window object</title>
+<link rel="author" title="Ms2ger" href="mailto:Ms2ger@gmail.com">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#dom-window">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#dom-frames">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#dom-self">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+var global = this;
+
+test(function() {
+ assert_equals(window, global);
+ assert_equals(window.window, global);
+}, "window should be the global object");
+
+test(function() {
+ assert_equals(frames, global);
+ assert_equals(window.frames, global);
+}, "frames should be the global object");
+
+test(function() {
+ assert_equals(self, global);
+ assert_equals(window.self, global);
+}, "self should be the global object");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/window-indexed-access-vs-named-access.html b/testing/web-platform/tests/html/browsers/the-window-object/window-indexed-access-vs-named-access.html
new file mode 100644
index 0000000000..23b9124ef7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/window-indexed-access-vs-named-access.html
@@ -0,0 +1,58 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Interactions between indexed and named access on the Window object</title>
+<link rel="author" title="Delan Azabani" href="dazabani@igalia.com">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#accessing-other-browsing-contexts">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#named-access-on-the-window-object">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=0></div>
+<div id=4></div>
+<iframe name=3></iframe>
+<iframe name=2></iframe>
+<iframe name=1></iframe>
+<script>
+const divs = document.querySelectorAll("div");
+const iframes = document.querySelectorAll("iframe");
+const wp = Object.getPrototypeOf(window);
+test(function() {
+ assert_equals(window[0], iframes[0].contentWindow);
+ assert_equals(window["0"], iframes[0].contentWindow);
+}, "WindowProxy: document-tree child navigable with index 0 (indexed access)");
+test(function() {
+ assert_equals(window[1], iframes[1].contentWindow);
+ assert_equals(window["1"], iframes[1].contentWindow);
+}, "WindowProxy: document-tree child navigable with index 1 (indexed access)");
+test(function() {
+ assert_equals(window[2], iframes[2].contentWindow);
+ assert_equals(window["2"], iframes[2].contentWindow);
+}, "WindowProxy: document-tree child navigable with index 2 (indexed access)");
+test(function() {
+ assert_equals(window[3], iframes[0].contentWindow);
+ assert_equals(window["3"], iframes[0].contentWindow);
+}, "WindowProxy: document-tree child navigable with target name 3 (named access)");
+test(function() {
+ assert_equals(window[4], divs[1]);
+ assert_equals(window["4"], divs[1]);
+}, "WindowProxy: element with id 4 (named access)");
+test(function() {
+ assert_equals(wp[0], divs[0]);
+ assert_equals(wp["0"], divs[0]);
+}, "Window prototype: element with id 0 (named access)");
+test(function() {
+ assert_equals(wp[1], iframes[2].contentWindow);
+ assert_equals(wp["1"], iframes[2].contentWindow);
+}, "Window prototype: document-tree child navigable with target name 1 (named access)");
+test(function() {
+ assert_equals(wp[2], iframes[1].contentWindow);
+ assert_equals(wp["2"], iframes[1].contentWindow);
+}, "Window prototype: document-tree child navigable with target name 2 (named access)");
+test(function() {
+ assert_equals(wp[3], iframes[0].contentWindow);
+ assert_equals(wp["3"], iframes[0].contentWindow);
+}, "Window prototype: document-tree child navigable with target name 3 (named access)");
+test(function() {
+ assert_equals(wp[4], divs[1]);
+ assert_equals(wp["4"], divs[1]);
+}, "Window prototype: element with id 4 (named access)");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/window-indexed-properties-delete-no-cache.html b/testing/web-platform/tests/html/browsers/the-window-object/window-indexed-properties-delete-no-cache.html
new file mode 100644
index 0000000000..22262943aa
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/window-indexed-properties-delete-no-cache.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Deletion of WindowProxy's indexed properties is not cached</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/window-object.html#windowproxy-delete">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+const iframe = document.createElement("iframe");
+iframe.srcdoc = "";
+
+test(() => {
+ assert_equals(window.length, 0);
+ for (let i = 0; i < 1e5; i++) {
+ assert_true(delete window[0]);
+ }
+
+ document.body.append(iframe);
+ assert_false(delete window[0]);
+}, "Absence of index '0' is not cached");
+
+test(() => {
+ assert_equals(window.length, 1);
+ for (let i = 0; i < 1e5; i++) {
+ assert_false(delete window[0]);
+ }
+
+ iframe.remove();
+ assert_true(delete window[0]);
+}, "Presence of index '0' is not cached");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/window-indexed-properties-strict.html b/testing/web-platform/tests/html/browsers/the-window-object/window-indexed-properties-strict.html
new file mode 100644
index 0000000000..2ee10a7a64
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/window-indexed-properties-strict.html
@@ -0,0 +1,75 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Indexed properties of the window object (strict mode)</title>
+<link rel="author" title="Ms2ger" href="ms2ger@gmail.com">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#window">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#dom-window-item">
+<link rel="help" href="https://webidl.spec.whatwg.org/#getownproperty">
+<link rel="help" href="https://webidl.spec.whatwg.org/#defineownproperty">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<iframe></iframe>
+<script>
+test(function() {
+ "use strict";
+ assert_false("-1" in window, "-1 not in window");
+ assert_equals(window[-1], undefined);
+ window[-1] = "foo";
+ assert_equals(window[-1], "foo");
+});
+test(function() {
+ "use strict";
+ assert_throws_js(TypeError, function() {
+ window[0] = "foo";
+ });
+ assert_throws_js(TypeError, () => Object.defineProperty(window, 0, { value: "bar" }))
+ assert_throws_js(TypeError, () => Object.defineProperty(window, 0, { get() { return "baz" } }))
+ assert_throws_js(TypeError, () => Object.defineProperty(window, 0, { set(v) { return "qux" } }))
+ assert_equals(window[0],
+ document.getElementsByTagName("iframe")[0].contentWindow);
+ assert_throws_js(TypeError, () => delete window[0]);
+});
+test(function() {
+ "use strict";
+ assert_throws_js(TypeError, function() {
+ window[1] = "foo";
+ });
+ assert_throws_js(TypeError, () => Object.defineProperty(window, 1, { value: "bar" }))
+ assert_throws_js(TypeError, () => Object.defineProperty(window, 1, { get() { return "baz" } }))
+ assert_throws_js(TypeError, () => Object.defineProperty(window, 1, { set(v) { return "qux" } }))
+ assert_equals(window[1], undefined);
+ assert_equals(Object.getOwnPropertyDescriptor(window, 1), undefined);
+ assert_equals(delete window[1], true);
+});
+test(function() {
+ "use strict";
+ assert_throws_js(TypeError, () => { window[4294967294] = 1; });
+ assert_false(Reflect.set(window, 4294967294, 2));
+ assert_false(Reflect.defineProperty(window, 4294967294, { value: 3 }));
+ assert_throws_js(TypeError, () => Object.defineProperty(window, 4294967294, { get: () => 4 }));
+ assert_equals(window[4294967294], undefined);
+ assert_false(4294967294 in window);
+ assert_true(delete window[4294967294]);
+}, "Borderline numeric key: 2 ** 32 - 2 is an index (strict mode)");
+test(function() {
+ "use strict";
+ window[4294967295] = 1;
+ assert_equals(window[4294967295], 1);
+ assert_true(Reflect.set(window, 4294967295, 2));
+ assert_equals(window[4294967295], 2);
+ assert_true(Reflect.defineProperty(window, 4294967295, { value: 3 }));
+ assert_equals(window[4294967295], 3);
+ Object.defineProperty(window, 4294967295, { get: () => 4 });
+ assert_equals(window[4294967295], 4);
+ assert_true(delete window[4294967295]);
+ assert_false(4294967295 in window);
+}, "Borderline numeric key: 2 ** 32 - 1 is not an index (strict mode)");
+test(function() {
+ "use strict";
+ var proto = Window.prototype;
+ [-1, 0, 1].forEach(function(idx) {
+ assert_false(idx in proto, idx + " in proto");
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/window-indexed-properties.html b/testing/web-platform/tests/html/browsers/the-window-object/window-indexed-properties.html
new file mode 100644
index 0000000000..57f18c5944
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/window-indexed-properties.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Indexed properties of the window object (non-strict mode)</title>
+<link rel="author" title="Ms2ger" href="ms2ger@gmail.com">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#window">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#dom-window-item">
+<link rel="help" href="https://webidl.spec.whatwg.org/#getownproperty">
+<link rel="help" href="https://webidl.spec.whatwg.org/#defineownproperty">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<iframe></iframe>
+<script>
+test(function() {
+ assert_false("-1" in window, "-1 not in window");
+ assert_equals(window[-1], undefined);
+ window[-1] = "foo";
+ assert_equals(window[-1], "foo");
+});
+test(() => {
+ const desc = Object.getOwnPropertyDescriptor(window, "0");
+ assert_true(desc.configurable);
+ assert_true(desc.enumerable);
+ assert_false(desc.writable);
+}, "Ensure indexed properties have the correct configuration");
+test(function() {
+ window[0] = "foo";
+ assert_throws_js(TypeError, () => Object.defineProperty(window, 0, { value: "bar" }))
+ assert_throws_js(TypeError, () => Object.defineProperty(window, 0, { get() { return "baz" } }))
+ assert_throws_js(TypeError, () => Object.defineProperty(window, 0, { set() { return "quz" } }))
+ assert_equals(window[0],
+ document.getElementsByTagName("iframe")[0].contentWindow);
+ assert_equals(delete window[0], false);
+});
+test(function() {
+ window[1] = "foo";
+ assert_throws_js(TypeError, () => Object.defineProperty(window, 1, { value: "bar" }))
+ assert_throws_js(TypeError, () => Object.defineProperty(window, 1, { get() { return "baz" } }))
+ assert_throws_js(TypeError, () => Object.defineProperty(window, 1, { set(v) { return "quz" } }))
+ assert_equals(window[1], undefined);
+ assert_equals(Object.getOwnPropertyDescriptor(window, 1), undefined);
+ assert_equals(delete window[1], true);
+});
+test(function() {
+ window[4294967294] = 1;
+ assert_false(Reflect.set(window, 4294967294, 2));
+ assert_false(Reflect.defineProperty(window, 4294967294, { value: 3 }));
+ assert_throws_js(TypeError, () => Object.defineProperty(window, 4294967294, { get: () => 4 }));
+ assert_equals(window[4294967294], undefined);
+ assert_false(4294967294 in window);
+ assert_true(delete window[4294967294]);
+}, "Borderline numeric key: 2 ** 32 - 2 is an index");
+test(function() {
+ window[4294967295] = 1;
+ assert_equals(window[4294967295], 1);
+ assert_true(Reflect.set(window, 4294967295, 2));
+ assert_equals(window[4294967295], 2);
+ assert_true(Reflect.defineProperty(window, 4294967295, { value: 3 }));
+ assert_equals(window[4294967295], 3);
+ Object.defineProperty(window, 4294967295, { get: () => 4 });
+ assert_equals(window[4294967295], 4);
+ assert_true(delete window[4294967295]);
+ assert_false(4294967295 in window);
+}, "Borderline numeric key: 2 ** 32 - 1 is not an index");
+test(function() {
+ var proto = Window.prototype;
+ [-1, 0, 1].forEach(function(idx) {
+ assert_false(idx in proto, idx + " in proto");
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/window-open-defaults.window.js b/testing/web-platform/tests/html/browsers/the-window-object/window-open-defaults.window.js
new file mode 100644
index 0000000000..1b2d68a462
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/window-open-defaults.window.js
@@ -0,0 +1,12 @@
+async_test(t => {
+ const frame = document.createElement("iframe");
+ t.add_cleanup(() => frame.remove());
+ frame.name = "foo";
+ frame.src = "/common/blank.html";
+ frame.onload = t.step_func(() => {
+ frame.onload = t.unreached_func();
+ t.step_timeout(() => t.done(), 500);
+ assert_equals(window[0], window.open(undefined, "foo"));
+ });
+ document.body.append(frame);
+}, "window.open()'s url parameter default");
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/window-open-invalid-url.html b/testing/web-platform/tests/html/browsers/the-window-object/window-open-invalid-url.html
new file mode 100644
index 0000000000..cabc143b9d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/window-open-invalid-url.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>window.open() with an invalid URL</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+test(() => {
+ assert_throws_dom("SyntaxError", () => window.open("https://example.com\u0000mozilla.org"));
+}, "Window.open should throw SyntaxError when an invalid url is passed as the first parameter");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/window-open-noopener.html b/testing/web-platform/tests/html/browsers/the-window-object/window-open-noopener.html
new file mode 100644
index 0000000000..8d3a95df63
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/window-open-noopener.html
@@ -0,0 +1,137 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>window.open() with "noopener" tests</title>
+
+<meta name="variant" content="?indexed">
+<meta name="variant" content="?_self">
+<meta name="variant" content="?_parent">
+<meta name="variant" content="?_top">
+<meta name=timeout content=long>
+
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+var testData = [
+ { testDescription: "window.open() with 'noopener' should reuse existing target",
+ secondWindowFeatureString: "noopener",
+ shouldReturnWindow: false },
+ { testDescription: "noopener=1 means the same as noopener",
+ secondWindowFeatureString: "noopener=1",
+ shouldReturnWindow: false },
+ { testDescription: "noopener=true means the same as noopener",
+ secondWindowFeatureString: "noopener=true",
+ shouldReturnWindow: false },
+ { testDescription: "noopener=0 means lack of noopener",
+ secondWindowFeatureString: "noopener=0",
+ shouldReturnWindow: true },
+ { testDescription: "noopener separated only by spaces should work",
+ secondWindowFeatureString: "make me noopener",
+ shouldReturnWindow: false },
+ { testDescription: "Trailing noopener should work",
+ secondWindowFeatureString: "abc def, \n\r noopener",
+ shouldReturnWindow: false },
+ { testDescription: "Leading noopener should work",
+ secondWindowFeatureString: "noopener \f\t , hey, there",
+ shouldReturnWindow: false },
+ { testDescription: "Interior noopener should work",
+ secondWindowFeatureString: "and now, noopener , hey, there",
+ shouldReturnWindow: false },
+ { testDescription: "noreferrer should also suppress opener when reusing existing target",
+ secondWindowFeatureString: "noreferrer",
+ shouldReturnWindow: false },
+];
+
+/**
+ * Loop over our testData array and kick off an async test for each entry. Each
+ * async test opens a window using window.open() with some per-test unique name,
+ * then tries to do a second window.open() call with the same name and the
+ * test-specific feature string. It then checks whether that second
+ * window.open() call reuses the existing window, whether the return value of
+ * the second window.open() call is correct (it should be null in the noopener
+ * cases and non-null in the cases when the existing window gets reused) and so
+ * forth.
+ */
+function indexedTests() {
+ var tests = [];
+ for(var i = 0; i < testData.length; ++i) {
+ var test = testData[i];
+ var t = async_test(test.testDescription);
+ tests.push(t);
+ t.secondWindowFeatureString = test.secondWindowFeatureString;
+ t.windowName = "someuniquename" + i;
+
+ if (test.shouldReturnWindow) {
+ t.step(function() {
+ var windowName = this.windowName;
+
+ var w1 = window.open("", windowName);
+ this.add_cleanup(function() { w1.close(); });
+
+ assert_equals(w1.opener, window);
+
+ var w2 = window.open("", windowName, this.secondWindowFeatureString);
+ assert_equals(w2, w1);
+ assert_equals(w2.opener, w1.opener);
+ assert_equals(w2.opener, window);
+ this.done();
+ });
+ } else {
+ t.step(function() {
+ var w1;
+ this.add_cleanup(function() {
+ w1.close();
+ channel.postMessage(null);
+ });
+
+ var windowName = this.windowName;
+ var channel = new BroadcastChannel(windowName);
+
+ channel.onmessage = this.step_func_done(function(e) {
+ var data = e.data;
+ assert_equals(data.name, windowName, "Should have the right name");
+ assert_equals(data.haveOpener, true, "Should still have opener");
+ assert_equals(w1.opener, window);
+ assert_not_equals(w1.location.href, "about:blank", "Should have navigated");
+ });
+
+ w1 = window.open("", windowName);
+ assert_equals(w1.opener, window);
+
+ var w2 = window.open("support/noopener-target.html?" + windowName,
+ windowName, this.secondWindowFeatureString);
+ assert_equals(w2, null);
+
+ assert_equals(w1.opener, window);
+ });
+ }
+ }
+}
+
+/**
+ * Loop over the special targets that ignore noopener and check that doing a
+ * window.open() with those targets correctly reuses the existing window.
+ */
+function specialTargetTest(target) {
+ if (["_self", "_parent", "_top"].includes(target)) {
+ var t = async_test("noopener window.open targeting " + target);
+ t.openedWindow = window.open(`javascript:var w2 = window.open("", "${target}", "noopener"); this.checkValues(w2); this.close(); void(0);`);
+ assert_equals(t.openedWindow.opener, window);
+ t.openedWindow.checkValues = t.step_func_done(function(win) {
+ assert_equals(win, this.openedWindow);
+ });
+ } else {
+ throw 'testError: special target must be one of: _self, _parent, _top'
+ }
+}
+
+/**
+ * Parse the Query string, check if it matches keyword 'indexed' to run the indexed tests,
+ * otherwise test it as a special target
+ */
+var variant = window.location.href.split("?")[1]
+if(variant == "indexed") {
+ indexedTests();
+} else {
+ specialTargetTest(variant);
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/window-open-noreferrer.html b/testing/web-platform/tests/html/browsers/the-window-object/window-open-noreferrer.html
new file mode 100644
index 0000000000..92b72cdb5f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/window-open-noreferrer.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>window.open() with "noreferrer" tests</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+async_test(t => {
+ const channelName = "343243423432",
+ channel = new BroadcastChannel(channelName);
+ window.open("support/noreferrer-target.html?" + channelName, "", "noreferrer");
+ channel.onmessage = t.step_func_done(e => {
+ // Send message first so if asserts throw the popup is still closed
+ channel.postMessage(null);
+
+ assert_equals(e.data.name, "");
+ assert_equals(e.data.referrer, "");
+ assert_equals(e.data.haveOpener, false);
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/window-open-popup-behavior.html b/testing/web-platform/tests/html/browsers/the-window-object/window-open-popup-behavior.html
new file mode 100644
index 0000000000..258698d94d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/window-open-popup-behavior.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>Window.open popup behavior</title>
+<link rel="author" href="masonf@chromium.org">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/window-object.html#window-open-steps">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+function testOne(windowFeatures, expectPopup) {
+ const windowName = Math.round(Math.random()*1e12);
+ const channel = new BroadcastChannel(windowName);
+ var w;
+ promise_test(() => {
+ return new Promise(resolve => {
+ w = window.open("support/window-open-popup-target.html", windowName, windowFeatures);
+ channel.addEventListener('message', resolve);
+ }).then(e => {
+ // Send message first so if asserts throw the popup is still closed
+ channel.postMessage(null);
+ assert_false(e.data.mixedState, "No mixed state");
+ assert_equals(e.data.isPopup, expectPopup, "Popup state");
+ });
+ },`${windowFeatures} (expect ${expectPopup ? "popup" : "tab"})`);
+}
+
+// No windowpreferences at all - tab.
+testOne(undefined, /*expectPopup=*/false);
+
+// Test all permutations of these properties:
+const features = ["location","toolbar","menubar","resizable","scrollbars","status"];
+const nProps = features.length;
+const skip = 7; // To speed up the test, don't test all values. Skip 7 to pseudo-randomize.
+for(let i=0;i<2**nProps;i+=skip) {
+ const enableVec = Number(i).toString(2).padStart(nProps,'0').split('').map(s => (s==="1"));
+ let windowFeatures = [];
+ for(let i=0;i<nProps;++i) {
+ if (enableVec[i])
+ windowFeatures.push(features[i] + "=yes");
+ }
+ windowFeatures = windowFeatures.join(',');
+ // We get a popup we got windowFeatures, and any of them are false:
+ const expectPopup = !!windowFeatures.length && (!(enableVec[0] || enableVec[1]) || !enableVec[2] || !enableVec[3] || !enableVec[4] || !enableVec[5]);
+ testOne(windowFeatures, expectPopup);
+ testOne(windowFeatures + ",noopener", /*expectPopup=*/false);
+ testOne(windowFeatures + ",noreferrer", /*expectPopup=*/false);
+ testOne(windowFeatures + ",popup", /*expectPopup=*/true); // "popup" feature = popup
+ testOne(windowFeatures + ",noopener,noreferrer,popup", /*expectPopup=*/false);
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/window-open-windowfeatures-values.html b/testing/web-platform/tests/html/browsers/the-window-object/window-open-windowfeatures-values.html
new file mode 100644
index 0000000000..32551dd8d7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/window-open-windowfeatures-values.html
@@ -0,0 +1,72 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>window.open() windowFeature value parsing</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/window-object.html#concept-window-open-features-parse-boolean">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+function testValueGeneric(val, expectTrue, property, testFn) {
+ const windowFeatureStr = val === "" ? property : `${property}=${val}`;
+ async_test(t => {
+ const windowName = '' + Math.round(Math.random()*1e12);
+ const channel = new BroadcastChannel(windowName);
+ channel.onmessage = t.step_func_done(e => {
+ // Send message first so if asserts throw the popup is still closed
+ channel.postMessage(null);
+ testFn(e.data);
+ });
+ window.open("support/windowFeature-values-target.html?" + windowName, windowName, windowFeatureStr);
+ },`Test ${windowFeatureStr}, expected interpretation is ${expectTrue ? 'true' : 'false'}`);
+}
+
+function testValueForNoReferrer(val, expectTrue) {
+ testValueGeneric(val, expectTrue, "noreferrer", (data) => {
+ if (expectTrue) {
+ assert_false(data.haveReferrer);
+ assert_false(data.haveOpener);
+ } else {
+ assert_true(data.haveReferrer);
+ assert_true(data.haveOpener);
+ }
+ });
+}
+
+function testValueForNoOpener(val, expectTrue) {
+ testValueGeneric(val, expectTrue, "noopener", (data) => {
+ assert_equals(data.haveOpener, !expectTrue);
+ });
+}
+
+function testValueForPopup(val, expectTrue) {
+ testValueGeneric(val, expectTrue, "popup", (data) => {
+ assert_equals(data.isPopup, expectTrue);
+ });
+}
+
+function testValue(val, expectTrue) {
+ const quotes = val === "" ? [''] : ['','"',"'"];
+ let noQuotes = true;
+ for (const quote of quotes) {
+ const thisExpectTrue = expectTrue && noQuotes;
+ const thisVal = quote + val + quote;
+ testValueForNoReferrer(thisVal, thisExpectTrue);
+ testValueForNoOpener(thisVal, thisExpectTrue);
+ testValueForPopup(thisVal, thisExpectTrue);
+ noQuotes = false;
+ }
+}
+
+testValue('',true); // Just the parameter means true
+testValue('yes',true); // Yes means true
+testValue('true',true); // True means true
+testValue('foo',false); // If parsing as an integer is an error, false
+testValue('0',false); // 0 is false
+testValue('00',false); // 0 is false
+testValue('1',true); // Non-zero is true
+testValue('99999',true); // Non-zero is true
+testValue('-1',true); // Non-zero is true
+testValue('1foo',true); // This parses to 1
+testValue('0foo',false); // This parses to 0
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/window-opener-unconfigurable.window.js b/testing/web-platform/tests/html/browsers/the-window-object/window-opener-unconfigurable.window.js
new file mode 100644
index 0000000000..2b9bda6792
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/window-opener-unconfigurable.window.js
@@ -0,0 +1,17 @@
+test(t => {
+ let desc = Object.getOwnPropertyDescriptor(self, "opener");
+ assert_true(!!desc.get, "Initially {get: function}");
+ assert_true(!!desc.set, "Initially {set: function}");
+ assert_true(desc.configurable, "Initially {configurable: true}");
+ assert_true(desc.enumerable, "Initially {enumerable: true}");
+
+ Object.defineProperty(self, "opener", {configurable: false});
+
+ desc = Object.getOwnPropertyDescriptor(self, "opener");
+ assert_true(!!desc.get, "Still has {get: function}");
+ assert_true(!!desc.set, "Still has {set: function}");
+ assert_false(desc.configurable, "Changed to {configurable: false}");
+ assert_true(desc.enumerable, "Still has {enumerable: true}");
+
+ assert_throws_js(TypeError, () => self.opener = "something", "Throws a TypeError due to {configurable: false}");
+}, "Corner case: self.opener is set while it's not configurable");
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/window-properties.https.html b/testing/web-platform/tests/html/browsers/the-window-object/window-properties.https.html
new file mode 100644
index 0000000000..2fb408cdc9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/window-properties.https.html
@@ -0,0 +1,359 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Properties of the window object</title>
+<link rel="author" title="Ms2ger" href="mailto:Ms2ger@gmail.com">
+<link rel="help" href="http://ecma-international.org/ecma-262/5.1/#sec-15.1">
+<link rel="help" href="https://webidl.spec.whatwg.org/#interface-prototype-object">
+<link rel="help" href="https://webidl.spec.whatwg.org/#es-attributes">
+<link rel="help" href="https://webidl.spec.whatwg.org/#es-operations">
+<link rel="help" href="https://dom.spec.whatwg.org/#eventtarget">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#window">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#windowtimers">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#windowbase64">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#windowsessionstorage">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#windowlocalstorage">
+<link rel="help" href="https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#dom-window-getselection">
+<link rel="help" href="http://dev.w3.org/csswg/cssom/#widl-def-Window">
+<link rel="help" href="http://dev.w3.org/csswg/cssom-view/#widl-def-Window">
+<iframe id="iframe" style="visibility: hidden" src="/resources/blank.html"></iframe>
+<iframe id="detachedIframe" style="visibility: hidden" src="/resources/blank.html"></iframe>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+function assert_data_propdesc(pd, Writable, Enumerable, Configurable) {
+ assert_equals(typeof pd, "object");
+ assert_equals(pd.writable, Writable);
+ assert_equals(pd.enumerable, Enumerable);
+ assert_equals(pd.configurable, Configurable);
+}
+function assert_accessor_propdesc(pd, hasSetter, Enumerable, Configurable) {
+ assert_equals(typeof pd, "object");
+ assert_equals(typeof pd.get, "function");
+ assert_true("set" in pd,
+ "Should always have a setter property on the property descriptor");
+ assert_equals(typeof pd.set, hasSetter ? "function" : "undefined");
+ assert_equals(pd.enumerable, Enumerable);
+ assert_equals(pd.configurable, Configurable);
+}
+
+var unforgeableAttributes = [
+ "window",
+ "document",
+ "location",
+ "top"
+];
+
+var replaceableAttributes = [
+ "self",
+ "locationbar",
+ "menubar",
+ "personalbar",
+ "scrollbars",
+ "statusbar",
+ "toolbar",
+ "frames",
+ "parent",
+ "external",
+ "length",
+ "origin",
+
+ // CSSOM-View
+ "screen",
+ "scrollX",
+ "scrollY",
+ "pageXOffset",
+ "pageYOffset",
+ "innerWidth",
+ "innerHeight",
+ "screenLeft",
+ "screenTop",
+ "screenX",
+ "screenY",
+ "outerWidth",
+ "outerHeight",
+ "devicePixelRatio",
+];
+
+var methods = [
+ "close",
+ "stop",
+ "focus",
+ "blur",
+ "open",
+ "alert",
+ "confirm",
+ "prompt",
+ "print",
+ "postMessage",
+
+ // WindowBase64
+ "btoa",
+ "atob",
+
+ // WindowTimers
+ "setTimeout",
+ "clearTimeout",
+ "setInterval",
+ "clearInterval",
+
+ // Microtask queuing
+ "queueMicrotask",
+
+ // ImageBitmap
+ "createImageBitmap",
+
+ // HTML Editing APIs
+ "getSelection",
+
+ // CSSOM
+ "getComputedStyle",
+
+ // CSSOM-View
+ "matchMedia",
+ "moveBy",
+ "moveTo",
+ "resizeBy",
+ "resizeTo",
+ "scroll",
+ "scrollTo",
+ "scrollBy"
+];
+
+var readonlyAttributes = [
+ "history",
+ "frameElement",
+ "navigator",
+
+ // WindowSessionStorage
+ "sessionStorage",
+
+ // WindowLocalStorage
+ "localStorage",
+];
+
+var writableAttributes = [
+ "name",
+ "status",
+ "opener",
+ "onabort",
+ "onafterprint",
+ "onbeforeprint",
+ "onbeforeunload",
+ "onblur",
+ "oncancel",
+ "oncanplay",
+ "oncanplaythrough",
+ "onchange",
+ "onclick",
+ "onclose",
+ "oncontextmenu",
+ "oncuechange",
+ "ondblclick",
+ "ondrag",
+ "ondragend",
+ "ondragenter",
+ "ondragleave",
+ "ondragover",
+ "ondragstart",
+ "ondrop",
+ "ondurationchange",
+ "onemptied",
+ "onended",
+ "onerror",
+ "onfocus",
+ "onhashchange",
+ "oninput",
+ "oninvalid",
+ "onkeydown",
+ "onkeypress",
+ "onkeyup",
+ "onload",
+ "onloadeddata",
+ "onloadedmetadata",
+ "onloadstart",
+ "onmessage",
+ "onmousedown",
+ "onmousemove",
+ "onmouseout",
+ "onmouseover",
+ "onmouseup",
+ "onmousewheel",
+ "onmove",
+ "onoffline",
+ "ononline",
+ "onpause",
+ "onplay",
+ "onplaying",
+ "onpagehide",
+ "onpageshow",
+ "onpopstate",
+ "onprogress",
+ "onratechange",
+ "onreset",
+ "onresize",
+ "onscroll",
+ "onseeked",
+ "onseeking",
+ "onselect",
+ "onstalled",
+ "onstorage",
+ "onsubmit",
+ "onsuspend",
+ "ontimeupdate",
+ "onunload",
+ "onvolumechange",
+ "onwaiting"
+];
+
+test(function() {
+ // 15.1.1 Value Properties of the Global Object
+ ["NaN", "Infinity", "undefined"].forEach(function(id) {
+ test(function() {
+ assert_true(id in window, id + " in window");
+ assert_data_propdesc(Object.getOwnPropertyDescriptor(window, id),
+ false, false, false);
+ }, "Value Property: " + id);
+ });
+}, "Value Properties of the Global Object");
+test(function() {
+ // 15.1.2 Function Properties of the Global Object
+ ["eval", "parseInt", "parseFloat", "isNaN", "isFinite"].forEach(function(id) {
+ test(function() {
+ assert_true(id in window, id + " in window");
+ assert_data_propdesc(Object.getOwnPropertyDescriptor(window, id),
+ true, false, true);
+ }, "Function Property: " + id);
+ });
+}, "Function Properties of the Global Object");
+test(function() {
+ // 15.1.3 URI Handling Function Properties
+ ["decodeURI", "decodeURIComponent", "encodeURI", "encodeURIComponent"].forEach(function(id) {
+ test(function() {
+ assert_true(id in window, id + " in window");
+ assert_data_propdesc(Object.getOwnPropertyDescriptor(window, id),
+ true, false, true);
+ }, "URI Handling Function Property: " + id);
+ });
+}, "URI Handling Function Properties");
+test(function() {
+ // 15.1.4 Constructor Properties of the Global Object
+ ["Object", "Function", "Array", "String", "Boolean", "Number", "Date",
+ "RegExp", "Error", "EvalError", "RangeError", "ReferenceError",
+ "SyntaxError", "TypeError", "URIError"].forEach(function(id) {
+ test(function() {
+ assert_true(id in window, id + " in window");
+ assert_data_propdesc(Object.getOwnPropertyDescriptor(window, id),
+ true, false, true);
+ }, "Constructor Property: " + id);
+ });
+}, "Constructor Properties of the Global Object");
+test(function() {
+ // 15.1.5 Other Properties of the Global Object
+ ["Math", "JSON"].forEach(function(id) {
+ test(function() {
+ assert_true(id in window, id + " in window");
+ assert_data_propdesc(Object.getOwnPropertyDescriptor(window, id),
+ true, false, true);
+ }, "Other Property: " + id);
+ });
+}, "Other Properties of the Global Object");
+test(function() {
+ // EventTarget interface
+ ["addEventListener", "removeEventListener", "dispatchEvent"].forEach(function(id) {
+ test(function() {
+ var EventTargetProto = EventTarget.prototype;
+ assert_true(id in window, id + " in window");
+ assert_equals(window[id], EventTargetProto[id]);
+ assert_data_propdesc(Object.getOwnPropertyDescriptor(EventTargetProto, id),
+ true, true, true);
+ assert_equals(Object.getOwnPropertyDescriptor(window, id), undefined);
+ }, "EventTarget method: " + id);
+ });
+}, "EventTarget interface");
+test(function() {
+ // Window interface
+ methods.forEach(function(id) {
+ test(function() {
+ var WindowProto = Window.prototype;
+ assert_true(id in window, id + " in window");
+ assert_false(id in WindowProto, id + " in Window.prototype");
+ assert_data_propdesc(Object.getOwnPropertyDescriptor(window, id),
+ true, true, true);
+ }, "Window method: " + id);
+ });
+ readonlyAttributes.forEach(function(id) {
+ test(function() {
+ var WindowProto = Window.prototype;
+ assert_true(id in window, id + " in window");
+ assert_false(id in WindowProto, id + " in Window.prototype");
+ assert_accessor_propdesc(Object.getOwnPropertyDescriptor(window, id),
+ false, true, true);
+ }, "Window readonly attribute: " + id);
+ });
+ writableAttributes.forEach(function(id) {
+ test(function() {
+ var WindowProto = Window.prototype;
+ assert_true(id in window, id + " in window");
+ assert_false(id in WindowProto, id + " in Window.prototype");
+ assert_accessor_propdesc(Object.getOwnPropertyDescriptor(window, id),
+ true, true, true);
+ }, "Window attribute: " + id);
+ });
+ unforgeableAttributes.forEach(function(id) {
+ test(function() {
+ var WindowProto = Window.prototype;
+ assert_true(id in window, id + " in window");
+ assert_false(id in WindowProto, id + " in Window.prototype");
+ // location has a [PutForwards] extended attribute.
+ assert_accessor_propdesc(Object.getOwnPropertyDescriptor(window, id),
+ id === "location", true, false);
+ }, "Window unforgeable attribute: " + id);
+ });
+ replaceableAttributes.forEach(function(id) {
+ test(function() {
+ var WindowProto = Window.prototype;
+ assert_true(id in window, id + " in window");
+ assert_false(id in WindowProto, id + " in Window.prototype");
+ assert_accessor_propdesc(Object.getOwnPropertyDescriptor(window, id),
+ true, true, true);
+ }, "Window replaceable attribute: " + id);
+ });
+}, "Window interface");
+test(function() {
+ assert_equals(window.constructor, Window);
+ assert_false(window.hasOwnProperty("constructor"), "window.constructor should not be an own property.");
+ assert_data_propdesc(Object.getOwnPropertyDescriptor(Window.prototype, "constructor"),
+ true, false, true);
+}, "constructor");
+var selfReferentialAccessors = ['window', 'self', 'frames'];
+// Test a variety of access methods
+var detached = window.detachedIframe;
+var detachedWindow = detached.contentWindow;
+detachedIframe.remove();
+selfReferentialAccessors.forEach(function(id) {
+ test(function() {
+ assert_equals(eval(`window.${id}`), window);
+ assert_equals(window[id], window);
+ assert_equals(Object.getOwnPropertyDescriptor(window, id).get.call(window), window);
+ assert_equals(Reflect.get(window, id), window);
+ }, "Window readonly getter: " + id);
+
+ test(function() {
+ var globalObject = iframe.contentWindow;
+ var _eval = globalObject.eval;
+ assert_equals(globalObject.eval(`window.${id}`), globalObject);
+ assert_equals(globalObject[id], globalObject);
+ assert_equals(_eval(`Object.getOwnPropertyDescriptor(window, "${id}").get.call(window)`), globalObject);
+ assert_equals(_eval(`Reflect.get(window, "${id}")`), globalObject);
+ }, "Window readonly getter in iframe: " + id);
+ test(function() {
+ var globalObject = detachedWindow;
+ var _eval = globalObject.eval;
+ assert_equals(_eval(`window.${id}`), globalObject);
+ assert_equals(globalObject[id], globalObject);
+ assert_equals(_eval(`Object.getOwnPropertyDescriptor(window, "${id}").get.call(window)`), globalObject);
+ assert_equals(_eval(`Reflect.get(window, "${id}")`), globalObject);
+ }, "Window readonly getter in detached iframe: " + id);
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/window-prototype-chain.html b/testing/web-platform/tests/html/browsers/the-window-object/window-prototype-chain.html
new file mode 100644
index 0000000000..14dbca35e8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/window-prototype-chain.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Prototype chain of the window object</title>
+<link rel="author" title="Ms2ger" href="ms2ger@gmail.com">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#window">
+<link rel="help" href="https://dom.spec.whatwg.org/#eventtarget">
+<link rel="help" href="https://webidl.spec.whatwg.org/#interface-prototype-object">
+<link rel="help" href="https://webidl.spec.whatwg.org/#named-properties-object">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+test(function() {
+ assert_class_string(window, "Window");
+}, "window object");
+test(function() {
+ var proto = Object.getPrototypeOf(window);
+ assert_equals(proto, Window.prototype);
+}, "Window.prototype");
+test(function() {
+ var gsp = Object.getPrototypeOf(Object.getPrototypeOf(window));
+ assert_class_string(gsp, "WindowProperties");
+}, "Global scope polluter");
+test(function() {
+ var protoproto = Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(window)));
+ assert_equals(protoproto, EventTarget.prototype);
+}, "EventTarget.prototype");
+test(function() {
+ var protoprotoproto = Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(window))));
+ assert_equals(protoprotoproto, Object.prototype);
+}, "Object.prototype");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-window-object/window-reuse-in-nested-browsing-contexts.tentative.html b/testing/web-platform/tests/html/browsers/the-window-object/window-reuse-in-nested-browsing-contexts.tentative.html
new file mode 100644
index 0000000000..03c7a68dfd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/window-reuse-in-nested-browsing-contexts.tentative.html
@@ -0,0 +1,158 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+const setupIframe = (t, attrs) => {
+ const iframe = document.createElement('iframe');
+ for (const [key, value] of Object.entries(attrs))
+ iframe[key] = value;
+ const watcher = new EventWatcher(t, iframe, ['load']);
+ document.body.appendChild(iframe);
+ return {iframe, watcher};
+};
+
+promise_test(async t => {
+ const {iframe, watcher} = setupIframe(t, {});
+
+ // Per https://whatwg.org/c/iframe-embed-object.html#process-the-iframe-attributes,
+ // the task to perform the "iframe load event steps" should still be queued.
+ // If a browser already fired a load event, the test will fail here since
+ // EventWatcher will have received an unexpected load event.
+
+ iframe.contentWindow.persistedString = 'Hello world!';
+ iframe.src = 'support/same-origin-iframe.html';
+ await watcher.wait_for(['load']);
+
+ assert_true(iframe.contentWindow.didLoadFrame);
+ // The <iframe>'s session history has only one Document, and that Document is
+ // the initial about:blank Document. The Window object should be reused per
+ // https://whatwg.org/c/iframe-embed-object.html#process-the-iframe-attributes.
+ assert_equals(iframe.contentWindow.persistedString, 'Hello world!');
+}, 'synchronously navigate iframe with no initial src.');
+
+promise_test(async t => {
+ const {iframe, watcher} = setupIframe(t, {});
+
+ // Per https://whatwg.org/c/iframe-embed-object.html#process-the-iframe-attributes,
+ // the task to perform the "iframe load event steps" should still be queued.
+ await watcher.wait_for(['load']);
+
+ iframe.contentWindow.persistedString = 'Hello world!';
+ iframe.src = 'support/same-origin-iframe.html';
+ await watcher.wait_for(['load']);
+
+ assert_true(iframe.contentWindow.didLoadFrame);
+ // The <iframe>'s session history has only one Document, and that Document is
+ // the initial about:blank Document. The Window object should be reused per
+ // https://whatwg.org/c/iframe-embed-object.html#process-the-iframe-attributes.
+ assert_equals(iframe.contentWindow.persistedString, 'Hello world!');
+}, 'after the first iframe load event, navigate iframe with no initial src.');
+
+// Per https://whatwg.org/c/iframe-embed-object.html#otherwise-steps-for-iframe-or-frame-elements,
+// setting the <iframe> src to an empty string before inserting the <iframe>
+// into the document should begin an attempt to navigate to a resource with
+// url == "about:blank".
+promise_test(async t => {
+ const {iframe, watcher} = setupIframe(t, {src: ''});
+
+ // Per https://whatwg.org/c/browsing-the-web.html#navigate, the "about:blank"
+ // resource should be obtained "in parallel". If a browser performs this step
+ // synchronously, the test will fail here since EventWatcher will have
+ // received an unexpected load event.
+
+ iframe.contentWindow.persistedString = 'Hello world!';
+ // An attempt to navigate to "about:blank" already exists but should not have
+ // matured yet since the resource should be obtained "in parallel". The new
+ // navigation attempt will cancel the preexisting attempt.
+ iframe.src = 'support/same-origin-iframe.html';
+ await watcher.wait_for(['load']);
+
+ assert_true(iframe.contentWindow.didLoadFrame);
+ // The navigation attempt to "about:blank" was cancelled, so the <iframe>'s
+ // session history has only one Document, and that Document is the
+ // initial about:blank Document. The Window object should be reused per
+ // https://whatwg.org/c/iframe-embed-object.html#process-the-iframe-attributes.
+ assert_equals(iframe.contentWindow.persistedString, 'Hello world!');
+}, 'synchronously navigate iframe with initial src == "".');
+
+promise_test(async t => {
+ const {iframe, watcher} = setupIframe(t, {src: ''});
+
+ // Per https://whatwg.org/c/browsing-the-web.html#navigate, the "about:blank"
+ // resource should be obtained "in parallel".
+ await watcher.wait_for(['load']);
+
+ iframe.contentWindow.persistedString = 'Hello world!';
+ iframe.src = 'support/same-origin-iframe.html';
+ await watcher.wait_for(['load']);
+
+ assert_true(iframe.contentWindow.didLoadFrame);
+ // A non-initial navigation to about:blank was committed, so the <iframe>
+ // element is no longer displaying the initial about:blank Document. Per
+ // https://whatwg.org/c/browsing-the-web.html#initialise-the-document-object,
+ // the Window object must not be reused.
+ assert_equals(iframe.contentWindow.persistedString, undefined);
+}, 'after the first iframe load event, navigate iframe with initial src == "".');
+
+promise_test(async t => {
+ const {iframe, watcher} = setupIframe(t, {src: 'about:blank'});
+
+ // Per https://whatwg.org/c/browsing-the-web.html#navigate, the "about:blank"
+ // resource should be obtained "in parallel". If a browser performs this step
+ // synchronously, the test will fail here since EventWatcher will have
+ // received an unexpected load event.
+
+ iframe.contentWindow.persistedString = 'Hello world!';
+ // An attempt to navigate to "about:blank" already exists but should not have
+ // matured yet since the resource should be obtained "in parallel". The new
+ // navigation attempt will cancel the preexisting attempt.
+ iframe.src = 'support/same-origin-iframe.html';
+ await watcher.wait_for(['load']);
+
+ assert_true(iframe.contentWindow.didLoadFrame);
+ // The navigation attempt to "about:blank" was cancelled, so the <iframe>'s
+ // session history has only one Document, and that Document is the
+ // initial about:blank Document. The Window object should be reused per
+ // https://whatwg.org/c/iframe-embed-object.html#process-the-iframe-attributes.
+ assert_equals(iframe.contentWindow.persistedString, 'Hello world!');
+}, 'synchronously navigate iframe with initial src == "about:blank".');
+
+promise_test(async t => {
+ const {iframe, watcher} = setupIframe(t, {src: 'about:blank'});
+
+ // Per https://whatwg.org/c/browsing-the-web.html#navigate, the "about:blank"
+ // resource should be obtained "in parallel".
+ await watcher.wait_for(['load']);
+
+ iframe.contentWindow.persistedString = 'Hello world!';
+ iframe.src = 'support/same-origin-iframe.html';
+ await watcher.wait_for(['load']);
+
+ assert_true(iframe.contentWindow.didLoadFrame);
+ // A non-initial navigation to about:blank was committed, so the <iframe>
+ // element is no longer displaying the initial about:blank Document. Per
+ // https://whatwg.org/c/browsing-the-web.html#initialise-the-document-object,
+ // the Window object must not be reused.
+ assert_equals(iframe.contentWindow.persistedString, undefined);
+}, 'after the first iframe load event, navigate iframe with initial src == "about:blank".');
+
+// Per https://whatwg.org/c/iframe-embed-object.html#otherwise-steps-for-iframe-or-frame-elements,
+// setting <iframe> src before inserting the <iframe> into the document should
+// begin an attempt to navigate to the value of the src attribute.
+promise_test(async t => {
+ const {iframe, watcher} = setupIframe(t, {src: 'support/same-origin-iframe.html'});
+
+ iframe.contentWindow.persistedString = 'Hello world!';
+ // Completion of the attempt to navigate happens "in parallel".
+ await watcher.wait_for(['load']);
+
+ assert_true(iframe.contentWindow.didLoadFrame);
+ // The <iframe>'s session history has only one Document, and that Document is
+ // the initial about:blank Document. The Window object should be reused per
+ // https://whatwg.org/c/iframe-embed-object.html#process-the-iframe-attributes.
+ assert_equals(iframe.contentWindow.persistedString, 'Hello world!');
+}, 'iframe with initial src == same-origin resource.');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/document-tree-child-browsing-context-name-property-set.sub.html b/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/document-tree-child-browsing-context-name-property-set.sub.html
new file mode 100644
index 0000000000..171aa01999
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/document-tree-child-browsing-context-name-property-set.sub.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>document-tree child browsing context name property set</title>
+<link rel="help" href="https://html.spec.whatwg.org/C/#document-tree-child-browsing-context-name-property-set">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe src="//{{domains[www]}}:{{ports[http][1]}}/common/window-name-setter.html#spices"></iframe>
+<iframe name="spices"></iframe>
+<iframe name="fruits"></iframe>
+
+<script>
+"use strict";
+setup({ explicit_done: true });
+
+window.onload = () => {
+ test(() => {
+ assert_equals(window.spices, undefined);
+ assert_not_equals(window.fruits, undefined);
+ assert_equals(window.fruits, window[2]);
+ }, "Cross origin child window's name shadows the second candidate of a same origin iframe");
+
+ done();
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-define-own-property-unforgeable-same-origin.html b/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-define-own-property-unforgeable-same-origin.html
new file mode 100644
index 0000000000..ac4eb458c3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-define-own-property-unforgeable-same-origin.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>[[DefineOwnProperty]] on a WindowProxy forwards to OrdinaryDefineOwnProperty for same-origin objects</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/window-object.html#windowproxy-defineownproperty">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+for (const key of ["window", "document", "location", "top"]) {
+ const { get, set } = Object.getOwnPropertyDescriptor(window, key);
+
+ test(() => {
+ Object.defineProperty(window, key, {});
+ assert_true(Reflect.defineProperty(window, key, { configurable: false }), "[[Configurable]]: false");
+ Object.defineProperty(window, key, { enumerable: true });
+
+ assert_true(Reflect.defineProperty(window, key, { get }), "[[Get]]: unchanged");
+ Object.defineProperty(window, key, { set });
+ assert_true(Reflect.defineProperty(window, key, { get, set }), "[[Get]]: unchanged, [[Set]]: unchanged");
+
+ Object.defineProperty(window, key, { get, set, enumerable: true, configurable: false });
+ }, `[[DefineOwnProperty]] success: "${key}"`);
+
+ test(() => {
+ assert_throws_js(TypeError, () => {
+ Object.defineProperty(window, key, { configurable: true });
+ }, "[[Configurable]]: true");
+
+ assert_false(Reflect.defineProperty(window, key, { enumerable: false }), "[[Enumerable]]: false");
+
+ assert_throws_js(TypeError, () => {
+ Object.defineProperty(window, key, { get() {}, set });
+ }, "[[Get]]: changed, [[Set]]: unchanged");
+
+ assert_false(Reflect.defineProperty(window, key, { get, set() {} }), "[[Get]]: unchanged, [[Set]]: changed");
+
+ assert_throws_js(TypeError, () => {
+ Object.defineProperty(window, key, { writable: false, configurable: true });
+ }, "[[Writable]]: false, [[Configurable]]: true");
+
+ assert_false(Reflect.defineProperty(window, key, { value: window[key], enumerable: true }), "[[Value]], [[Enumerable]]: true");
+ }, `[[DefineOwnProperty]] failure: "${key}"`);
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prevent-extensions.html b/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prevent-extensions.html
new file mode 100644
index 0000000000..97a156290a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prevent-extensions.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>[[PreventExtensions]] on a WindowProxy object should return false</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/window-object.html#windowproxy-preventextensions">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ Object.preventExtensions(window);
+ });
+}, "Object.preventExtensions throws a TypeError");
+
+test(() => {
+ assert_false(Reflect.preventExtensions(window));
+}, "Reflect.preventExtensions returns false");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-cross-origin-domain.sub.html b/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-cross-origin-domain.sub.html
new file mode 100644
index 0000000000..a5ae78cc87
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-cross-origin-domain.sub.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>[[SetPrototypeOf]] on a WindowProxy object should not allow changing its value: cross-origin via document.domain</title>
+<link rel="help" href="http://html.spec.whatwg.org/multipage/#windowproxy-setprototypeof">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/test-setting-immutable-prototype.js"></script>
+
+<iframe src="/common/domain-setter.sub.html"></iframe>
+
+<script>
+"use strict";
+// This page does *not* set document.domain, so it's cross-origin with the iframe
+setup({ explicit_done: true });
+
+window.onload = () => {
+ const target = frames[0];
+
+ test(() => {
+ assert_equals(Object.getPrototypeOf(target), null);
+ }, "Cross-origin via document.domain: the prototype is null");
+
+ testSettingImmutablePrototype("Cross-origin via document.domain", target, null, { isSameOriginDomain: false });
+
+ done();
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-cross-origin.sub.html b/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-cross-origin.sub.html
new file mode 100644
index 0000000000..a5c1a2f102
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-cross-origin.sub.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>[[SetPrototypeOf]] on a WindowProxy object should not allow changing its value: cross-origin</title>
+<link rel="help" href="http://html.spec.whatwg.org/multipage/#windowproxy-setprototypeof">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/test-setting-immutable-prototype.js"></script>
+
+<iframe src="//{{domains[www]}}:{{ports[http][1]}}/common/blank.html"></iframe>
+
+<script>
+"use strict";
+setup({ explicit_done: true });
+
+window.onload = () => {
+ const target = frames[0];
+
+ test(() => {
+ assert_equals(Object.getPrototypeOf(target), null);
+ }, "Cross-origin: the prototype is null");
+
+ testSettingImmutablePrototype("Cross-origin", target, null, { isSameOriginDomain: false });
+
+ done();
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-goes-cross-origin-domain.sub.html b/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-goes-cross-origin-domain.sub.html
new file mode 100644
index 0000000000..c2a6def6c2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-goes-cross-origin-domain.sub.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>[[SetPrototypeOf]] on a WindowProxy object should not allow changing its value: cross-origin via document.domain after initially getting the object</title>
+<link rel="help" href="http://html.spec.whatwg.org/multipage/#windowproxy-setprototypeof">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/test-setting-immutable-prototype.js"></script>
+
+<iframe src="/common/blank.html"></iframe>
+
+<script>
+"use strict";
+window.onload = () => {
+ const target = frames[0];
+ const origProto = Object.getPrototypeOf(target);
+
+ test(() => {
+ assert_not_equals(origProto, null);
+ }, "Same-origin (for now): the prototype is accessible");
+
+ document.domain = "{{host}}";
+
+ test(() => {
+ assert_equals(Object.getPrototypeOf(target), null);
+ }, "Became cross-origin via document.domain: the prototype is now null");
+
+ testSettingImmutablePrototype("Became cross-origin via document.domain", target, null, { isSameOriginDomain: false });
+
+ testSettingImmutablePrototypeToNewValueOnly(
+ "Became cross-origin via document.domain", target, origProto,
+ "the original value from before going cross-origin", { isSameOriginDomain: false });
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-same-origin-domain.sub.html b/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-same-origin-domain.sub.html
new file mode 100644
index 0000000000..fb18822ac5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-same-origin-domain.sub.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>[[SetPrototypeOf]] on a WindowProxy object should not allow changing its value: cross-origin, but same-origin-domain</title>
+<link rel="help" href="http://html.spec.whatwg.org/multipage/#windowproxy-setprototypeof">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/test-setting-immutable-prototype.js"></script>
+
+<iframe src="//{{domains[www]}}:{{ports[http][1]}}/common/domain-setter.sub.html"></iframe>
+
+<script>
+"use strict";
+document.domain = "{{host}}";
+setup({ explicit_done: true });
+
+window.onload = () => {
+ const target = frames[0];
+ const origProto = Object.getPrototypeOf(target);
+
+ test(() => {
+ assert_not_equals(origProto, null);
+ }, "Same-origin-domain prerequisite check: the original prototype is accessible");
+
+ testSettingImmutablePrototype("Same-origin-domain", target, origProto, { isSameOriginDomain: true }, frames[0]);
+
+ done();
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-same-origin.html b/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-same-origin.html
new file mode 100644
index 0000000000..5779199ca3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-windowproxy-exotic-object/windowproxy-prototype-setting-same-origin.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>[[SetPrototypeOf]] on a WindowProxy object should not allow changing its value: same-origin</title>
+<link rel="help" href="http://html.spec.whatwg.org/multipage/#windowproxy-setprototypeof">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/test-setting-immutable-prototype.js"></script>
+
+<script>
+"use strict";
+
+const origProto = Object.getPrototypeOf(window);
+
+test(() => {
+ assert_not_equals(origProto, null);
+}, "Same-origin prerequisite check: the original prototype is accessible");
+
+testSettingImmutablePrototype("Same-origin", window, origProto, { isSameOriginDomain: true });
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-closed.html b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-closed.html
new file mode 100644
index 0000000000..106c3f97cb
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-closed.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<meta charset="utf-8">
+<html>
+ <head>
+ <title>Auxiliary Browsing Contexts: window.opener when Opener Removed/Closed</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/PrefixedLocalStorage.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var prefixedLocalStorage;
+ setup (() => prefixedLocalStorage = new PrefixedLocalStorageTest());
+ async_test(t => {
+ t.add_cleanup (() => prefixedLocalStorage.cleanup());
+ var a = document.createElement('a');
+ a.href = prefixedLocalStorage.url('resources/open-closer.html');
+ a.target = '_blank';
+ prefixedLocalStorage.onSet('openerIsNull', t.step_func_done(e => {
+ // The window for this auxiliary browsing context's opener
+ // has been closed and discarded, so the aux browsing context
+ // should now report `null` for `window.opener`
+ assert_equals(e.newValue, 'true');
+ }));
+ document.body.append(a);
+ a.click();
+ }, 'An auxiliary browsing context should report `null` for `window.opener` when that browsing context is discarded');
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-multiple.html b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-multiple.html
new file mode 100644
index 0000000000..e71d4dc868
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-multiple.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Auxiliary Browsing Contexts: window.opener, multiple</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/PrefixedLocalStorage.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var prefixedLocalStorage;
+ setup (() => prefixedLocalStorage = new PrefixedLocalStorageTest());
+ async_test(t => {
+ t.add_cleanup (() => prefixedLocalStorage.cleanup());
+ var a = document.createElement('a');
+ a.href = prefixedLocalStorage.url('resources/multiple-opener.html');
+ a.target = 'multipleOpener';
+ window.name = 'topOpener';
+ document.body.appendChild(a);
+ window.addEventListener('message', t.step_func_done(e => {
+ var aux1 = e.data.aux1; // First opened context
+ var aux2 = e.data.aux2; // Context opened by first-opened context
+ assert_equals(aux1.name, 'multipleOpener');
+ assert_equals(aux1.openerName, window.name);
+ assert_equals(aux1.isTop, true);
+ assert_equals(aux2.name, 'multipleOpenee');
+ assert_equals(aux2.openerName, aux1.name);
+ assert_equals(aux2.isTop, true);
+ }));
+ a.click();
+ }, 'An auxiliary browsing context should be able to open another auxiliary browsing context');
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-noopener.html b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-noopener.html
new file mode 100644
index 0000000000..086a96442d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-noopener.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Auxiliary Browsing Contexts: window.opener noopener</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/PrefixedLocalStorage.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var prefixedLocalStorage;
+ setup(() => prefixedLocalStorage = new PrefixedLocalStorageTest());
+ async_test(t => {
+ t.add_cleanup(() => prefixedLocalStorage.cleanup());
+ prefixedLocalStorage.onSet('openerIsNull', t.step_func_done(e => {
+ assert_equals(e.newValue, 'true');
+ }));
+ window.open(prefixedLocalStorage.url('resources/no-opener.html'),
+ 'iShouldNotHaveAnOpener',
+ 'noopener');
+ }, 'Auxiliary browsing context created via `window.open` setting `noopener` should report `window.opener` `null`');
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-noreferrer.html b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-noreferrer.html
new file mode 100644
index 0000000000..b8226bd2b9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-noreferrer.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Auxiliary Browsing Contexts: window.opener noreferrer</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/PrefixedLocalStorage.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var prefixedLocalStorage;
+ setup(() => prefixedLocalStorage = new PrefixedLocalStorageTest());
+ async_test(t => {
+ t.add_cleanup(() => prefixedLocalStorage.cleanup());
+ var a = document.createElement('a');
+ a.href = prefixedLocalStorage.url('resources/no-opener.html');
+ a.target = '_blank';
+ a.rel = 'noreferrer';
+ window.name = 'topWindow';
+ document.body.appendChild(a);
+ prefixedLocalStorage.onSet('openerIsNull', t.step_func_done(e => {
+ assert_equals(e.newValue, 'true');
+ }));
+ a.click();
+ }, 'Auxiliary browsing context created with `rel="noreferrer"` should report `window.opener` `null`');
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-setter.html b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-setter.html
new file mode 100644
index 0000000000..ac6e47b846
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-setter.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Auxiliary Browsing Contexts: window.opener setter</title>
+ <meta name=timeout content=long>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/PrefixedLocalStorage.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var prefixedLocalStorage;
+ setup(() => prefixedLocalStorage = new PrefixedLocalStorageTest());
+ async_test(t => {
+ t.add_cleanup(() => prefixedLocalStorage.cleanup());
+ prefixedLocalStorage.onSet('openerIsNull', t.step_func_done(e => {
+ assert_equals(e.newValue, 'true');
+ }));
+ window.open(prefixedLocalStorage.url('resources/opener-setter.html'),
+ 'iShouldSetOpenerToNull');
+ }, 'Auxiliary browsing context created via `window.open` and setting `window.opener` to `null` should report `window.opener` `null`');
+ async_test(t => {
+ t.add_cleanup(() => prefixedLocalStorage.cleanup());
+ prefixedLocalStorage.onSet('openerIsTest', t.step_func_done(e => {
+ assert_equals(e.newValue, 'true');
+ }));
+ window.open(prefixedLocalStorage.url('resources/opener-setter.html'),
+ 'iShouldSetOpenerToTest');
+ }, 'Auxiliary browsing context created via `window.open` and setting `window.opener` to `test` should report `test`');
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-setter.window.js b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-setter.window.js
new file mode 100644
index 0000000000..6d540ce97c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener-setter.window.js
@@ -0,0 +1,38 @@
+[
+ undefined,
+ 42,
+ function() { return "hi" },
+ "hi",
+ {},
+ [],
+ Symbol()
+].forEach(val => {
+ test(t => {
+ const frame = document.body.appendChild(document.createElement("iframe")),
+ win = frame.contentWindow;
+ t.add_cleanup(() => frame.remove());
+
+ assert_own_property(win, "opener");
+ assert_equals(win.opener, null);
+ const beforeDesc = Object.getOwnPropertyDescriptor(win, "opener"),
+ openerGet = beforeDesc.get,
+ openerSet = beforeDesc.set;
+ assert_own_property(beforeDesc, "get");
+ assert_own_property(beforeDesc, "set");
+ assert_true(beforeDesc.enumerable);
+ assert_true(beforeDesc.configurable);
+
+ win.opener = val;
+ assert_equals(win.opener, val);
+ assert_equals(openerGet(), null);
+
+ const desc = Object.getOwnPropertyDescriptor(win, "opener");
+ assert_equals(desc.value, val);
+ assert_true(desc.writable);
+ assert_true(desc.enumerable);
+ assert_true(desc.configurable);
+
+ openerSet("x");
+ assert_equals(win.opener, "x");
+ }, "Setting window.opener to " + String(val)); // String() needed for symbols
+});
diff --git a/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener.html b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener.html
new file mode 100644
index 0000000000..c43d3bd3bf
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/opener.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Auxiliary Browsing Contexts: window.opener</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/PrefixedLocalStorage.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var prefixedLocalStorage;
+ setup (() => {
+ window.name = 'topWindow';
+ prefixedLocalStorage = new PrefixedLocalStorageTest();
+ });
+
+ function cleanup () {
+ prefixedLocalStorage.setItem('closeAll', 'true');
+ prefixedLocalStorage.clear();
+ }
+
+ function testOpener (t, target) {
+ t.add_cleanup(cleanup);
+ window.addEventListener('message', t.step_func(e => {
+ if (e.data.name === target) {
+ // The opener IDL attribute...must return the WindowProxy object of the
+ // browsing context from which the current browsing context was created
+ assert_equals(e.data.openerName, 'topWindow');
+ // Auxiliary browsing contexts are always top-level browsing contexts
+ assert_equals(e.data.isTop, true);
+ t.done();
+ }
+ }));
+ }
+
+ async_test(t => {
+ var target = 'windowOpenerA';
+ var a = document.createElement('a');
+ a.href = prefixedLocalStorage.url('resources/message-window-opener.html');
+ a.target = target;
+ document.body.appendChild(a);
+ testOpener(t, target);
+ a.click();
+ }, 'Newly-created auxiliary browsing context should report `window.opener`');
+
+ async_test(t => {
+ var target = 'windowOpenerB';
+ testOpener(t, target);
+ window.open(prefixedLocalStorage.url('resources/message-window-opener.html'),
+ target);
+ }, 'Browsing context created with `window.open` should report `window.opener`');
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/close-opener.html b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/close-opener.html
new file mode 100644
index 0000000000..f41773ed2c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/close-opener.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset="utf-8">
+<html>
+<body onload="closeOpener()">
+<p>This window should close its opener.</p>
+<script src="/common/PrefixedLocalStorage.js"></script>
+<script>
+var prefixedLocalStorage = new PrefixedLocalStorageResource({
+ close_on_cleanup: true
+});
+var prefixedLocalStorage = new PrefixedLocalStorageResource({
+ close_on_cleanup: true
+});
+function closeOpener () {
+ if (window.opener) {
+ window.opener.close();
+
+ // Give the browsing context a chance to dispose of itself
+ function waitForContextDiscard () {
+ if (window.opener === null) {
+ return prefixedLocalStorage.setItem('openerIsNull', 'true');
+ }
+ return setTimeout(waitForContextDiscard, 0);
+ }
+ waitForContextDiscard();
+ }
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/message-window-opener.html b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/message-window-opener.html
new file mode 100644
index 0000000000..8105650b61
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/message-window-opener.html
@@ -0,0 +1,14 @@
+<script src="/common/PrefixedLocalStorage.js"></script>
+<script>
+var prefixedLocalStorage = new PrefixedLocalStorageResource({
+ close_on_cleanup: true
+});
+
+if (window.opener) {
+ window.opener.postMessage ({
+ name : window.name,
+ openerName: window.opener.name,
+ isTop : window.top === window
+ }, '*');
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/multiple-opener.html b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/multiple-opener.html
new file mode 100644
index 0000000000..2e63b9f4c6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/multiple-opener.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html>
+<script src="/common/PrefixedLocalStorage.js"></script>
+<body onload="openNested()">
+<script>
+var prefixedLocalStorage = new PrefixedLocalStorageResource({
+ close_on_cleanup: true
+});
+function openNested () {
+ // Listen for message from opened context and pass through to this
+ // context's opener
+ window.addEventListener('message', (e) => {
+ if (window.opener) {
+ window.opener.postMessage({
+ aux2: e.data, // From multipleOpenee
+ aux1: { // This context
+ name : window.name,
+ openerName : window.opener.name,
+ isTop : window.top === window
+ }
+ }, '*');
+ }
+ });
+ var a = document.createElement('a');
+ a.target = 'multipleOpenee';
+ a.href = prefixedLocalStorage.url('message-window-opener.html');
+ document.body.appendChild(a);
+ a.click();
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/no-opener.html b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/no-opener.html
new file mode 100644
index 0000000000..afd72f948d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/no-opener.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<meta charset="utf-8">
+<html>
+<p>This window should have no opener.</p>
+<script src="/common/PrefixedLocalStorage.js"></script>
+<script>
+var prefixedLocalStorage = new PrefixedLocalStorageResource({
+ close_on_cleanup: true
+});
+function checkOpener () {
+ return prefixedLocalStorage.setItem('openerIsNull', window.opener === null);
+}
+</script>
+<body onload="checkOpener()">
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/open-closer.html b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/open-closer.html
new file mode 100644
index 0000000000..7575c7c95f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/open-closer.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<meta charset="utf-8">
+<html>
+<body onload="openAuxiliary()">
+<a rel="opener" target="_blank">Open auxiliary context that will close this window (its opener)</a>
+<script src="/common/PrefixedLocalStorage.js"></script>
+<script>
+function openAuxiliary () {
+ var prefixedLocalStorage = new PrefixedLocalStorageResource();
+ var a = document.body.querySelector('a');
+ a.href = prefixedLocalStorage.url('close-opener.html');
+ document.body.append(a);
+ a.click();
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/opener-setter.html b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/opener-setter.html
new file mode 100644
index 0000000000..4112dae0ce
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/auxiliary-browsing-contexts/resources/opener-setter.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<meta charset="utf-8">
+<html>
+<p>This window should set the window.opener attribute</p>
+<script src="/common/PrefixedLocalStorage.js"></script>
+<script>
+var prefixedLocalStorage = new PrefixedLocalStorageResource({
+ close_on_cleanup: true
+});
+function checkOpener () {
+ if (window.name == 'iShouldSetOpenerToNull') {
+ window.opener = null;
+ return prefixedLocalStorage.setItem('openerIsNull', window.opener === null);
+ }
+ if (window.name == 'iShouldSetOpenerToTest') {
+ window.opener = 'test';
+ return prefixedLocalStorage.setItem('openerIsTest', window.opener === "test");
+ }
+}
+</script>
+<body onload="checkOpener()">
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_blank-001.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_blank-001.html
new file mode 100644
index 0000000000..a1416f2eb8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_blank-001.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: Browsing context - `_blank` name keyword</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(t => {
+ var window1 = window.open('about:blank', '_blank');
+ var window2 = window.open('about:blank', '_blank');
+ var window3 = window.open('about:blank', '_blank');
+ t.add_cleanup(() => {
+ window1.close();
+ window2.close();
+ window3.close();
+ });
+ assert_not_equals(window1, window2);
+ assert_not_equals(window2, window3);
+ assert_not_equals(window1, window3);
+}, 'window.open into `_blank` should create a new browsing context each time');
+
+test(t => {
+ var window1 = window.open('about:blank', '_bLAnk');
+ var window2 = window.open('about:blank', '_bLAnk');
+ var window3 = window.open('about:blank', '_bLAnk');
+ t.add_cleanup(() => {
+ window1.close();
+ window2.close();
+ window3.close();
+ });
+ assert_not_equals(window1, window2);
+ assert_not_equals(window2, window3);
+ assert_not_equals(window1, window3);
+}, '`_blank` should be ASCII case-insensitive');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_blank-002.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_blank-002.html
new file mode 100644
index 0000000000..aba9d52ba0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_blank-002.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<title>Link with target=_blank, rel=noreferrer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedLocalStorage.js"></script>
+<div id="log"></div>
+<a href="resources/report-has-opener.html" rel="noreferrer" target="_blank">Link</a>
+<script>
+var prefixedStorage;
+setup (() => prefixedStorage = new PrefixedLocalStorageTest());
+
+async_test(t => {
+ t.add_cleanup(() => prefixedStorage.cleanup());
+ var a = document.getElementsByTagName('a')[0];
+ a.href = prefixedStorage.url(a.href);
+ prefixedStorage.onSet('hasOpener', t.step_func_done(e => {
+ assert_equals(e.newValue, 'false');
+ }));
+ a.click();
+}, 'Context for opened noreferrer link targeted to "_blank" should not have opener reference');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_blank-003.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_blank-003.html
new file mode 100644
index 0000000000..5571344c85
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_blank-003.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>Link with target=_blank, no rel</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedLocalStorage.js"></script>
+<div id="log"></div>
+<a href="resources/report-has-opener.html" target="_blank">Link</a>
+<script>
+var prefixedStorage;
+setup(() => prefixedStorage = new PrefixedLocalStorageTest());
+async_test(t => {
+ t.add_cleanup(() => prefixedStorage.cleanup());
+ prefixedStorage.onSet('hasOpener', t.step_func_done(e => {
+ assert_equals(e.newValue, 'false');
+ }));
+ var a = document.getElementsByTagName('a')[0];
+ a.href = prefixedStorage.url(a.href);
+ a.click();
+}, 'Context created by link targeting "_blank" should not have opener reference');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_parent-001.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_parent-001.html
new file mode 100644
index 0000000000..35cbc101c7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_parent-001.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: Choose browsing context - '_parent'</title>
+<link rel="author" title="Intel" href="http://www.intel.com/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+async_test(t => {
+ window.addEventListener('message', t.step_func_done(e => {
+ assert_equals(e.data.name, 'parentWin');
+ }));
+}, 'The parent browsing context must be chosen if the given name is `_parent`');
+</script>
+<iframe id="embedded" src="resources/choose-_parent-001-iframe-1.html" name="parentWin" style="display:none"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_parent-002.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_parent-002.html
new file mode 100644
index 0000000000..7b7d561030
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_parent-002.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: Choose browsing context - '_parent' (nested contexts)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+async_test(t => {
+ var topWindow;
+ t.add_cleanup(() => topWindow.close());
+ window.addEventListener('message', t.step_func_done(e => {
+ assert_equals(e.data.name, 'iframeParent');
+ assert_false(e.data.isTop, 'window.parent is not top');
+ }));
+ topWindow = window.open('resources/choose-_parent-002-window.html', '_blank');
+}, 'choosing _parent context: multiple nested contexts');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_parent-003.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_parent-003.html
new file mode 100644
index 0000000000..20dc9b0d2a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_parent-003.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: Choose browsing context - '_parent' (via window.open)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+async_test(t => {
+ var topWindow;
+ t.add_cleanup(() => topWindow.close());
+ window.addEventListener('message', t.step_func_done(e => {
+ assert_equals(e.data.name, 'parentTopReplace');
+ assert_equals(e.data.isTop, true);
+ }));
+ topWindow = window.open('resources/choose-_parent-003-window.html', 'parentTopReplace');
+}, '_parent should reuse window.parent context');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_parent-004.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_parent-004.html
new file mode 100644
index 0000000000..c79378018a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_parent-004.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: Choose browsing context - '_parent' (case-sensitivity)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedLocalStorage.js"></script>
+<body>
+<div id="log"></div>
+
+<script>
+var prefixedStorage;
+var iframe;
+setup(() => prefixedStorage = new PrefixedLocalStorageTest());
+
+async_test(t => {
+ t.add_cleanup(() => prefixedStorage.cleanup());
+ var testFunc = (function (t) {
+ var completed = 0;
+ var testCount = 2;
+ return function (actual, expected) {
+ assert_equals(actual, expected);
+ if (++completed >= testCount) {
+ t.done();
+ }
+ }
+ }(t));
+
+ prefixedStorage.onSet('isTop', t.step_func(e => {
+ testFunc(e.newValue, 'false');
+ }));
+ prefixedStorage.onSet('name', t.step_func(e => {
+ testFunc(e.newValue, 'parentWin');
+ }));
+ iframe = document.createElement('iframe');
+ iframe.src = prefixedStorage.url('resources/choose-_parent-004-iframe-1.html');
+ iframe.name = 'parentWin';
+ document.body.appendChild(iframe);
+}, 'choosing _parent context should be case-insensitive');
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_self-001.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_self-001.html
new file mode 100644
index 0000000000..ed7666846d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_self-001.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: Choose browsing context - the given name is '_self'</title>
+<link rel="author" title="Intel" href="http://www.intel.com/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe src="resources/choose-_self-001-iframe.html" style="display:none"></iframe>
+<script>
+async_test(t => {
+ window.addEventListener('message', t.step_func_done(e => {
+ assert_equals(e.data.name, 'myownself');
+ }), false);
+}, 'The current browsing context must be chosen if the given name is "_self"');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_self-002.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_self-002.html
new file mode 100644
index 0000000000..2e798f5493
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_self-002.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: Choose browsing context - '_self' (case-sensitivity)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedLocalStorage.js"></script>
+<body>
+<div id="log"></div>
+
+<script>
+var prefixedStorage;
+setup(() => prefixedStorage = new PrefixedLocalStorageTest());
+
+async_test(t => {
+ var iframe;
+
+ var testFunc = (function (t) {
+ var completed = 0;
+ var testCount = 2;
+ return function (actual, expected) {
+ assert_equals(actual, expected);
+ if (++completed >= testCount) {
+ t.done();
+ }
+ }
+ }(t));
+
+ t.add_cleanup(() => prefixedStorage.cleanup());
+
+ prefixedStorage.onSet('isTop', t.step_func(e => {
+ testFunc(e.newValue, 'false');
+ }));
+ prefixedStorage.onSet('name', t.step_func(e => {
+ testFunc(e.newValue, 'testWin');
+ }));
+
+ iframe = document.createElement('iframe');
+ iframe.name = 'testWin';
+ iframe.src = prefixedStorage.url('resources/choose-_self-002-iframe.html');
+ document.body.appendChild(iframe);
+
+}, 'choosing _self context should be case-insensitive');
+
+</script>
+
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_top-001.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_top-001.html
new file mode 100644
index 0000000000..de4c6ad115
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_top-001.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<title>HTML Test: Browsing context name - _top (current is top)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedLocalStorage.js"></script>
+<div id="log"></div>
+<script>
+var prefixedStorage;
+setup (() => prefixedStorage = new PrefixedLocalStorageTest());
+
+async_test(t => {
+ t.add_cleanup(() => prefixedStorage.cleanup());
+
+ var testFunc = (function (t) {
+ var completed = 0;
+ var testCount = 2;
+ return function (actual, expected) {
+ assert_equals(actual, expected);
+ if (++completed >= testCount) {
+ t.done();
+ }
+ }
+ }(t));
+
+ prefixedStorage.onSet('isTop', t.step_func(e => {
+ testFunc(e.newValue, 'true');
+ }));
+ prefixedStorage.onSet('name', t.step_func(e => {
+ testFunc(e.newValue, 'topWin1');
+ }));
+
+ window.open(prefixedStorage.url('resources/open-in-_top.html'), '_blank');
+}, 'Should choose current browsing context for "_top" if current is top');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_top-002.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_top-002.html
new file mode 100644
index 0000000000..f29da80c6e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_top-002.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<title>HTML Test: Browsing context name - _top</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedLocalStorage.js"></script>
+<div id="log"></div>
+<script>
+var prefixedStorage;
+setup (() => prefixedStorage = new PrefixedLocalStorageTest());
+
+async_test(t => {
+ t.add_cleanup(() => prefixedStorage.cleanup());
+
+ var testFunc = (function (t) {
+ var completed = 0;
+ var testCount = 2;
+ return function (actual, expected) {
+ assert_equals(actual, expected);
+ if (++completed >= testCount) {
+ t.done();
+ }
+ }
+ }(t));
+
+ prefixedStorage.onSet('isTop', t.step_func(e => {
+ testFunc(e.newValue, 'true');
+ }));
+ prefixedStorage.onSet('name', t.step_func(e => {
+ testFunc(e.newValue, 'topWin2');
+ }));
+ window.open(prefixedStorage.url('resources/choose-_top-002-window.html'), '_blank');
+}, 'Should choose top browsing context for "_top" if current is not top');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_top-003.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_top-003.html
new file mode 100644
index 0000000000..e068f8cc1f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-_top-003.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: Choose browsing context - '_top' (case-sensitivity)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/PrefixedLocalStorage.js"></script>
+<body>
+<div id="log"></div>
+
+<script>
+var prefixedStorage;
+setup(() => prefixedStorage = new PrefixedLocalStorageTest());
+
+async_test(t => {
+ var testFunc = (function (t) {
+ var completed = 0;
+ var testCount = 2;
+ return function (actual, expected) {
+ assert_equals(actual, expected);
+ if (++completed >= testCount) {
+ t.done();
+ }
+ }
+ }(t));
+
+ t.add_cleanup(() => prefixedStorage.cleanup());
+
+ prefixedStorage.onSet('isTop', t.step_func(e => {
+ testFunc(e.newValue, 'true');
+ }));
+ prefixedStorage.onSet('name', t.step_func(e => {
+ testFunc(e.newValue, 'topWin');
+ }));
+
+ window.open(prefixedStorage.url('resources/choose-_top-003-iframe-1.html'), '_blank');
+}, 'choosing _top context should be case-insensitive');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-default-001.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-default-001.html
new file mode 100644
index 0000000000..c21159e0a6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-default-001.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: Browsing context - Default name</title>
+<link rel="author" title="Intel" href="http://www.intel.com/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe src="/common/blank.html" style="display:none"></iframe>
+<object id="obj" type="text/html" data="about:blank"></object>
+<embed id="embedded" type="image/svg+xml" src="/images/green.svg" width="0" height="0" />
+<script>
+test(t => {
+ assert_equals(window.frames[0].name, "");
+ assert_equals(document.getElementById("embedded").name, "");
+ assert_equals(window["obj"].name, "");
+}, "A embedded browsing context has empty-string default name");
+
+test(t => {
+ var win = window.open("about:blank", "_blank");
+ assert_equals(win.name, "");
+ win.close();
+}, "A browsing context which is opened by window.open() method with '_blank' parameter has empty-string default name");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-default-002.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-default-002.html
new file mode 100644
index 0000000000..748ee68973
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-default-002.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: Browsing context names - empty string</title>
+<link rel="author" title="Intel" href="http://www.intel.com/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+async_test(t => {
+ window.addEventListener('message', t.step_func_done(e => {
+ assert_equals(e.data.isTop, false);
+ assert_equals(e.data.name, 'hellothere', 'Empty-string browsing context should choose current context');
+ }), false);
+}, 'The current browsing context must be chosen if the given name is empty string');
+</script>
+<iframe name="hellothere" src="resources/choose-default-002-iframe.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-existing-001.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-existing-001.html
new file mode 100644
index 0000000000..fdf74b8a79
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/choose-existing-001.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: Choose browsing context - the given name is same as an existing browsing context's name</title>
+<link rel="author" title="Intel" href="http://www.intel.com/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe src="resources/choose-existing-001-iframe.html" style="display:none"></iframe>
+<iframe name="iExist" style="display:none"></iframe>
+<script>
+async_test(t => {
+ window.addEventListener('message', t.step_func_done(e => {
+ assert_equals(e.data.name, 'iExist');
+ }), false);
+
+}, 'An existing browsing context must be chosen if the given name is the same as its name');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-001-iframe-1.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-001-iframe-1.html
new file mode 100644
index 0000000000..04dd74f1a5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-001-iframe-1.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: browsing context name - parent</title>
+<link rel="author" title="Intel" href="http://www.intel.com/">
+<iframe src="open-in-_parent.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-002-iframe.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-002-iframe.html
new file mode 100644
index 0000000000..52da8986b0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-002-iframe.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: browsing context name - parent: nested context</title>
+<iframe name="iframeChild" src="open-in-_parent.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-002-window.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-002-window.html
new file mode 100644
index 0000000000..558193742f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-002-window.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: browsing context name - parent: top-level context</title>
+<iframe name="iframeParent" src="choose-_parent-002-iframe.html"></iframe>
+<script>
+// Relay a message from child context to opener context
+window.addEventListener('message', e => {
+ if (window.opener) {
+ window.opener.postMessage(e.data, '*');
+ }
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-003-iframe.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-003-iframe.html
new file mode 100644
index 0000000000..1412dc2e79
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-003-iframe.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: browsing context name - parent</title>
+<script>
+window.open("post-to-opener.html", "_parent");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-003-window.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-003-window.html
new file mode 100644
index 0000000000..bd1802aed6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-003-window.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: browsing context name - parent: top-level context (gets replaced)</title>
+<iframe name="iframeOpener" src="choose-_parent-003-iframe.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-004-iframe-1.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-004-iframe-1.html
new file mode 100644
index 0000000000..7a8cbecb27
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-004-iframe-1.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: browsing context name - parent</title>
+<script src="/common/PrefixedLocalStorage.js"></script>
+<body>
+<script>
+var prefixedStorage = new PrefixedLocalStorageResource({
+ close_on_cleanup: true
+});
+var iframe = document.createElement('iframe');
+iframe.src = prefixedStorage.url('choose-_parent-004-iframe-2.html');
+document.body.appendChild(iframe);
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-004-iframe-2.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-004-iframe-2.html
new file mode 100644
index 0000000000..33ead5a538
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_parent-004-iframe-2.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: browsing context name - parent (case-insensitive)</title>
+<script src="/common/PrefixedLocalStorage.js"></script>
+<script>
+var prefixedStorage = new PrefixedLocalStorageResource();
+window.open(prefixedStorage.url('report-is-top.html'), '_pARent');
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_self-001-iframe.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_self-001-iframe.html
new file mode 100644
index 0000000000..0ad79d6e16
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_self-001-iframe.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: browsing context name - self</title>
+<link rel="author" title="Intel" href="http://www.intel.com/">
+<script>
+window.name = 'myownself';
+var win = window.open('post-to-top.html', '_self');
+win.close();
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_self-002-iframe.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_self-002-iframe.html
new file mode 100644
index 0000000000..9d88305e09
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_self-002-iframe.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: browsing context name - self (case-insensitive)</title>
+<link rel="author" title="Intel" href="http://www.intel.com/">
+<script src="/common/PrefixedLocalStorage.js"></script>
+<script>
+var prefixedStorage = new PrefixedLocalStorageResource({
+ close_on_cleanup: true
+});
+var win = window.open(prefixedStorage.url('report-is-top.html'), '_sElF');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_top-002-window.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_top-002-window.html
new file mode 100644
index 0000000000..d71384b72f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_top-002-window.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/common/PrefixedLocalStorage.js"></script>
+<title>HTML Test: browsing context name - _top</title>
+<body>
+<script>
+var prefixedStorage = new PrefixedLocalStorageResource({
+ close_on_cleanup:true
+});
+window.name = 'topWin2';
+var iframe = document.createElement('iframe');
+iframe.src = prefixedStorage.url('open-in-_top.html');
+// Append iframe that will open another document into `_top` (this context)
+document.body.appendChild(iframe);
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_top-003-iframe-1.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_top-003-iframe-1.html
new file mode 100644
index 0000000000..aecc2fd88a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_top-003-iframe-1.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: browsing context name - top (case-insensitive)</title>
+<script src="/common/PrefixedLocalStorage.js"></script>
+<body>
+</body>
+<script>
+var prefixedStorage = new PrefixedLocalStorageResource({
+ close_on_cleanup: true
+});
+window.name = 'topWin';
+var iframe = document.createElement('iframe');
+iframe.src = prefixedStorage.url('choose-_top-003-iframe-2.html');
+document.body.appendChild(iframe);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_top-003-iframe-2.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_top-003-iframe-2.html
new file mode 100644
index 0000000000..a1a7e1dda7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-_top-003-iframe-2.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: browsing context name - top (case-insensitive)</title>
+<script src="/common/PrefixedLocalStorage.js"></script>
+<script>
+var prefixedStorage = new PrefixedLocalStorageResource({
+ close_on_cleanup: true
+});
+window.open(prefixedStorage.url("report-is-top.html"), "_ToP");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-default-002-iframe.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-default-002-iframe.html
new file mode 100644
index 0000000000..567e4ea310
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-default-002-iframe.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<title>HTML Test: browsing context name - Empty string</title>
+<link rel="author" title="Intel" href="http://www.intel.com/">
+<body onload="followLink()">
+</body>
+<script>
+function followLink() {
+ var a = document.createElement('a');
+ a.href = 'post-to-top.html';
+ a.target = ''; // Target is empty string
+ document.body.appendChild(a);
+ a.click();
+}
+</script>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-existing-001-iframe.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-existing-001-iframe.html
new file mode 100644
index 0000000000..cb0b554854
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/choose-existing-001-iframe.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>This is a test page</title>
+<link rel="author" title="Intel" href="http://www.intel.com/">
+<script>
+window.open("post-to-top.html", "iExist");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/open-in-_parent.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/open-in-_parent.html
new file mode 100644
index 0000000000..81d0735c60
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/open-in-_parent.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: browsing context name - parent</title>
+<link rel="author" title="Intel" href="http://www.intel.com/">
+<script>
+
+window.open("post-to-top.html", "_parent");
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/open-in-_top.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/open-in-_top.html
new file mode 100644
index 0000000000..929d52d15e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/open-in-_top.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/common/PrefixedLocalStorage.js"></script>
+<title>HTML Test: browsing context name - _top</title>
+<script>
+var prefixedStorage = new PrefixedLocalStorageResource();
+window.name = 'topWin1';
+window.open(prefixedStorage.url('report-is-top.html'), '_top');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/post-to-opener.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/post-to-opener.html
new file mode 100644
index 0000000000..3e9b7aaccb
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/post-to-opener.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: post window's name to top browsing context</title>
+<link rel="author" title="Intel" href="http://www.intel.com/">
+<script>
+if (window.opener) {
+ window.opener.postMessage({
+ name: window.name,
+ isTop: (window.top === window)
+ }, "*");
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/post-to-top.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/post-to-top.html
new file mode 100644
index 0000000000..aebb57b019
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/post-to-top.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: post window's name to top browsing context</title>
+<link rel="author" title="Intel" href="http://www.intel.com/">
+<script>
+top.postMessage({
+ name: window.name,
+ isTop: (window.top === window)
+}, "*");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/report-has-opener.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/report-has-opener.html
new file mode 100644
index 0000000000..37d8cedc6d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/report-has-opener.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<script src="/common/PrefixedLocalStorage.js"></script>
+<script>
+var prefixedStorage = new PrefixedLocalStorageResource({
+ close_on_cleanup: true
+});
+prefixedStorage.setItem('hasOpener', window.opener !== null);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/report-is-top.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/report-is-top.html
new file mode 100644
index 0000000000..8711235982
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-names/resources/report-is-top.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<meta charset="utf-8">
+<script src="/common/PrefixedLocalStorage.js"></script>
+<script>
+var prefixedStorage = new PrefixedLocalStorageResource({
+ close_on_cleanup: true
+});
+prefixedStorage.setItem('isTop', window === window.top);
+prefixedStorage.setItem('name', window.name);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context-window.html b/testing/web-platform/tests/html/browsers/windows/browsing-context-window.html
new file mode 100644
index 0000000000..f4f3c715ac
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context-window.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<html>
+ <head>
+ <title>HTML Test: Newly-Created browsing context Window and `this`</title>
+ <link rel="author" title="Intel" href="http://www.intel.com/" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+
+ /**
+ * Test for creating a new browsing context, [Browsing Contexts](#windows).
+ * This test is separate from the rest of the browsing-content-creation tests
+ * because the `iframe` has a src and thus its document won't be `about:blank`.
+ */
+ var doc, iframe;
+
+ setup(function () {
+ iframe = document.createElement("iframe");
+ iframe.src = get_host_info().HTTP_REMOTE_ORIGIN + "/html/browsers/windows/resources/browsing-context-window.html";
+ document.body.appendChild(iframe);
+ doc = iframe.contentDocument;
+ });
+
+ async_test(function (t) {
+ window.onmessage = t.step_func(function (e) {
+ assert_equals(e.data.thisWindowEquivalency, true, "The global `this` for the created browsing context should be a reference to Window through WindowProxy");
+ t.done();
+ });
+ });
+
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/browsing-context.html b/testing/web-platform/tests/html/browsers/windows/browsing-context.html
new file mode 100644
index 0000000000..59367b0428
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/browsing-context.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<html>
+ <head>
+ <title>HTML Test: Browsing context is first created</title>
+ <link rel="author" title="Intel" href="http://www.intel.com/" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var doc, iframe;
+
+ setup(function () {
+ // Create new browsing context via iframe
+ iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ doc = iframe.contentDocument;
+ });
+
+ test(function () {
+ assert_equals(doc.compatMode, "BackCompat", "The compatMode of a document without a document type declaration should be 'BackCompat'."); // Quirksmode
+ assert_equals(doc.contentType, "text/html", "The document should be an HTML document.");
+ assert_equals(doc.readyState, "complete", "The readyState attribute should be 'complete'.");
+ // Document metadata...
+ assert_equals(doc.documentURI, "about:blank", "The document's address should be 'about:blank'.");
+ assert_equals(doc.URL, "about:blank", "The document's address should be 'about:blank'.");
+ assert_equals(doc.doctype, null, "The docType of a document without a document type declaration should be null.");
+ assert_equals(doc.characterSet, "UTF-8", "The document's encoding should be 'UTF-8'.");
+ }, "Check that browsing context has new, ready HTML document");
+
+ test(function () {
+ assert_equals(doc.childNodes.length, 1, "The document must have only one child.");
+ assert_equals(doc.documentElement.tagName, "HTML");
+ assert_equals(doc.documentElement.childNodes.length, 2, "The HTML element should have 2 children.");
+ assert_equals(doc.documentElement.childNodes[0].tagName, "HEAD", "The first child of HTML element should be a HEAD element.");
+ assert_false(doc.documentElement.childNodes[0].hasChildNodes(), "The HEAD element should not have children.");
+ assert_equals(doc.documentElement.childNodes[1].tagName, "BODY", "The second child of HTML element should be a BODY element.");
+ assert_false(doc.documentElement.childNodes[1].hasChildNodes(), "The BODY element should not have children.");
+ }, "Check that new document nodes extant, empty");
+
+ test(function () {
+ assert_equals(doc.referrer, document.URL, "The document's referrer should be its creator document's URL.");
+ assert_equals(iframe.contentWindow.parent.document, document);
+ }, "Check the document properties corresponding to the creator browsing context");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/clear-window-name.https.html b/testing/web-platform/tests/html/browsers/windows/clear-window-name.https.html
new file mode 100644
index 0000000000..698de8a1ca
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/clear-window-name.https.html
@@ -0,0 +1,122 @@
+<!doctype html>
+<html>
+<head>
+ <title>Clear window.name when cross-origin</title>
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/utils.js"></script>
+</head>
+<body>
+ <script>
+
+function anchorClick(n) {
+ const hyperlink = document.body.appendChild(document.createElement("a"))
+ hyperlink.rel = "noopener";
+ hyperlink.target = "_blank";
+ hyperlink.href = n;
+ hyperlink.click();
+}
+
+async function pollResultAndCheck(t, id, expected) {
+ const stashURL = new URL("resources/window-name-stash.py", location);
+ stashURL.searchParams.set('id', id);
+
+ let res = "NONE";
+ while (res == "NONE") {
+ await new Promise(resolve => { t.step_timeout(resolve, 100); });
+
+ const response = await fetch(stashURL);
+ res = await response.text();
+ }
+ if (res !== expected) {
+ assert_unreached('Stash result does not equal expected result.')
+ }
+}
+
+promise_test(async t => {
+ const id = token();
+
+ window.open(`resources/window-name.sub.html?report=${id}|close`, id);
+ await pollResultAndCheck(t, id, id);
+}, "Window.name is not reset when there is an opener around");
+
+promise_test(async t => {
+ const id = token();
+
+ window.open(`resources/window-name.sub.html?cross|same|report=${id}|close`, id);
+ await pollResultAndCheck(t, id, id);
+}, "Window.name is not reset when there is an opener around (cross-origin)");
+
+promise_test(async t => {
+ const id = token();
+
+ window.open(`resources/window-name.sub.html?report=${id}|close`, id, "noopener");
+ await pollResultAndCheck(t, id, id);
+}, "Window.name is not reset at the first navigation away from initial about:blank with noopener");
+
+promise_test(async t => {
+ const id = token();
+
+ window.open(`resources/window-name.sub.html?cross|same|report=${id}|close`, id, "noopener");
+ await pollResultAndCheck(t, id, "");
+}, "Window.name is reset at the first cross-origin navigation with noopener");
+
+promise_test(async t => {
+ const id = token();
+
+ let win = window.open(`resources/window-name.sub.html?report=${id}|close`, id);
+ win.opener = null;
+ await pollResultAndCheck(t, id, id);
+}, "Window.name is not reset at the first navigation away from initial about:blank with window.opener set to null");
+
+promise_test(async t => {
+ const id = token();
+
+ let win = window.open(`resources/window-name.sub.html?same|report=${id}|close`, id);
+ win.opener = null;
+ await pollResultAndCheck(t, id, id);
+}, "Window.name is not reset at the same-origin navigation with window.opener set to null");
+
+promise_test(async t => {
+ const id = token();
+
+ let win = window.open(`resources/window-name.sub.html?cross|same|report=${id}|close`, id);
+ win.opener = null;
+ await pollResultAndCheck(t, id, "");
+}, "Window.name is reset at the first first cross-origin navigation with window.opener set to null");
+
+promise_test(async t => {
+ const id = token();
+
+ anchorClick(`resources/window-name.sub.html?set=${id}|report=${id}|close`);
+ await pollResultAndCheck(t, id, id);
+}, "Window.name is set by the window");
+
+promise_test(async t => {
+ const id = token();
+
+ anchorClick(`resources/window-name.sub.html?set=${id}|cross|same|report=${id}|close`);
+ await pollResultAndCheck(t, id, "");
+}, "Window.name is reset at the first cross-origin navigation");
+
+promise_test(async t => {
+ const id = token();
+
+ window.open(`resources/window-name.sub.html?open|navOpener=about:blank|reportOpener=${id}|closeOpener|close`, id, "noopener");
+ await pollResultAndCheck(t, id, id);
+}, "window.name is not reset after navigating to an about:blank page from a non-about:blank page");
+
+
+promise_test(async t => {
+ const id = token();
+ const domain = window.location.host;
+
+ anchorClick(`resources/window-name.sub.html?sub|set=${id}|setDomain=${domain}|sub|report=${id}|close`);
+ await pollResultAndCheck(t, id, id);
+}, "Window.name is not reset if the document.domain is set to the parent domain");
+
+
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/dangling-markup-window-name.html b/testing/web-platform/tests/html/browsers/windows/dangling-markup-window-name.html
new file mode 100644
index 0000000000..1bc8bb9f7f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/dangling-markup-window-name.html
@@ -0,0 +1,97 @@
+<!doctype html>
+<html>
+<head>
+ <title>Dangling Markup in target</title>
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/utils.js"></script>
+</head>
+<body>
+ <script>
+ function anchorClick(target, id) {
+ const hyperlink = document.body.appendChild(document.createElement('a'));
+ if (target) {
+ hyperlink.target = target;
+ }
+ hyperlink.href = `resources/window-name.sub.html?report=${id}|close`;
+ hyperlink.click();
+ }
+
+ async function pollResultAndCheck(t, id, expected) {
+ const stashURL = new URL('resources/window-name-stash.py', location);
+ stashURL.searchParams.set('id', id);
+
+ let res = 'NONE';
+ while (res == 'NONE') {
+ await new Promise(resolve => { t.step_timeout(resolve, 100); });
+
+ const response = await fetch(stashURL);
+ res = await response.text();
+ }
+ if (res !== expected) {
+ assert_unreached('Stash result does not equal expected result.')
+ }
+ }
+
+ promise_test(async t => {
+ const id = token();
+ const value = '\n<' + id;
+
+ window.open(`resources/window-name.sub.html?report=${id}|close`, value);
+ await pollResultAndCheck(t, id, value);
+ }, 'Dangling Markup in target is not reset when set by window.open');
+
+ promise_test(async t => {
+ const id = token();
+ const value = '\n<' + id;
+
+ anchorClick(value, id)
+ await pollResultAndCheck(t, id, '');
+ }, 'Dangling Markup with "\\n" in target is reset when set by <a> tag');
+
+ promise_test(async t => {
+ const id = token();
+ const value = '\r<' + id;
+
+ anchorClick(value, id)
+ await pollResultAndCheck(t, id, '');
+ }, 'Dangling Markup with "\\r" in target is reset when set by <a> tag');
+
+ promise_test(async t => {
+ const id = token();
+ const value = '\t<' + id;
+
+ anchorClick(value, id)
+ await pollResultAndCheck(t, id, '');
+ }, 'Dangling Markup with "\\t" in target is reset when set by <a> tag');
+
+ promise_test(async t => {
+ const id = token();
+ const value = '\n<' + id;
+
+ const form = document.body.appendChild(document.createElement('form'));
+ form.target = value;
+ form.method = 'GET';
+ form.action = 'resources/window-name.sub.html';
+ const input = form.appendChild(document.createElement('input'));
+ input.type = 'hidden';
+ input.name = 'report';
+ input.value = `${id}|close`;
+ form.submit();
+
+ await pollResultAndCheck(t, id, '');
+ }, 'Dangling Markup in target is reset when set by <form> tag');
+
+ promise_test(async t => {
+ const id = token();
+ const value = '\n<' + id;
+ const base = document.head.appendChild(document.createElement('base'));
+ base.target = value;
+
+ anchorClick(null, id)
+ await pollResultAndCheck(t, id, '');
+ }, 'Dangling Markup in target is reset when set by <base> tag');
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/document-domain-nested-navigate.window.js b/testing/web-platform/tests/html/browsers/windows/document-domain-nested-navigate.window.js
new file mode 100644
index 0000000000..f51eed5ca9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/document-domain-nested-navigate.window.js
@@ -0,0 +1,16 @@
+async_test(t => {
+ // Setting document.domain makes this document cross-origin with that of the frame. However,
+ // about:blank will end up reusing the origin of this document, at which point the frame's
+ // document is no longer cross-origin.
+ const frame = document.body.appendChild(document.createElement('iframe'));
+ document.domain = document.domain;
+ frame.src = "/common/blank.html";
+ frame.onload = t.step_func(() => {
+ assert_throws_dom("SecurityError", () => window[0].document);
+ frame.src = "about:blank";
+ frame.onload = t.step_func_done(() => {
+ // Ensure we can access the child browsing context after navigation to non-initial about:blank
+ assert_equals(window[0].document, frame.contentDocument);
+ });
+ });
+}, "Navigated frame to about:blank and document.domain");
diff --git a/testing/web-platform/tests/html/browsers/windows/document-domain-nested-set.window.js b/testing/web-platform/tests/html/browsers/windows/document-domain-nested-set.window.js
new file mode 100644
index 0000000000..13c0d50ba0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/document-domain-nested-set.window.js
@@ -0,0 +1,10 @@
+test(() => {
+ // As the initial:about frame document reuses the origin of this document, setting document.domain
+ // from the frame, resulting in a origin mutation, has no effect on these documents being able to
+ // reach each other, as they share the same "physical" origin.
+ document.body.appendChild(document.createElement('iframe'));
+ const script = document.createElement("script");
+ script.text = "document.domain = document.domain";
+ window[0].document.body.appendChild(script);
+ assert_equals(window[0].document.body.localName, "body");
+}, "Initial about:blank frame and document.domain in the frame");
diff --git a/testing/web-platform/tests/html/browsers/windows/document-domain-nested.window.js b/testing/web-platform/tests/html/browsers/windows/document-domain-nested.window.js
new file mode 100644
index 0000000000..2e39d05f03
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/document-domain-nested.window.js
@@ -0,0 +1,9 @@
+test(() => {
+ // As the initial:about frame document reuses the origin of this document, setting document.domain
+ // from this document, resulting in a origin mutation, has no effect on these documents being able
+ // to reach each other, as they share the same "physical" origin.
+ document.body.appendChild(document.createElement('iframe'));
+ document.domain = document.domain;
+ // Ensure we can still access the child browsing context
+ assert_equals(window[0].document.body.localName, "body");
+}, "Initial about:blank frame and document.domain");
diff --git a/testing/web-platform/tests/html/browsers/windows/document-domain-removed-iframe.html b/testing/web-platform/tests/html/browsers/windows/document-domain-removed-iframe.html
new file mode 100644
index 0000000000..7403ebfaad
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/document-domain-removed-iframe.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>document.domain and removed iframe interaction</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!-- This is a test for https://crbug.com/1095145 where window
+ properties become undefined for document.domain-using removed
+ iframes -->
+
+<div id="log"></div>
+
+<script>
+"use strict";
+
+promise_test(t => {
+ return new Promise(resolve => {
+ const iframe = document.createElement("iframe");
+ iframe.onload = t.step_func(() => {
+ const { contentWindow } = iframe;
+ assert_equals(contentWindow.status, "");
+ resolve();
+ });
+ iframe.src = "/common/blank.html";
+ document.body.append(iframe);
+ });
+}, "No removal, no document.domain");
+
+promise_test(t => {
+ return new Promise(resolve => {
+ const iframe = document.createElement("iframe");
+ iframe.onload = t.step_func(() => {
+ const { contentWindow } = iframe;
+ iframe.remove();
+ assert_equals(contentWindow.status, "");
+ resolve();
+ });
+ iframe.src = "/common/blank.html";
+ document.body.append(iframe);
+ });
+}, "Removal, no document.domain");
+
+promise_test(t => {
+ return new Promise(resolve => {
+ const iframe = document.createElement("iframe");
+ iframe.onload = t.step_func(() => {
+ document.domain = document.domain;
+ const { contentWindow } = iframe;
+ assert_equals(contentWindow.status, "");
+ resolve();
+ });
+ iframe.src = "resources/document-domain-setter.html";
+ document.body.append(iframe);
+ });
+}, "No removal, document.domain");
+
+promise_test(t => {
+ return new Promise(resolve => {
+ const iframe = document.createElement("iframe");
+ iframe.onload = t.step_func(() => {
+ document.domain = document.domain; // technically we already did this above
+ const { contentWindow } = iframe;
+ iframe.remove();
+ assert_equals(contentWindow.status, "");
+ resolve();
+ });
+ iframe.src = "resources/document-domain-setter.html";
+ document.body.append(iframe);
+ });
+}, "Removal, document.domain");
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/embedded-opener-a-form.html b/testing/web-platform/tests/html/browsers/windows/embedded-opener-a-form.html
new file mode 100644
index 0000000000..e1ec760b92
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/embedded-opener-a-form.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<title>opener and embedded documents; using a and form</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<iframe name=matchesastring></iframe>
+<a href=/common/blank.html target=matchesastring>&lt;a></a>
+<form action=/common/blank.html target=matchesastring><input type=submit value="<form>"></form>
+<script>
+async_test(t => {
+ const frame = document.querySelector("iframe");
+ let counter = 0;
+ frame.onload = t.step_func(() => {
+ // Firefox and Chrome/Safari load differently
+ if (frame.contentWindow.location.href === "about:blank") {
+ return;
+ }
+
+ // Test bits
+ assert_equals(frame.contentWindow.opener, null);
+ if (counter === 0) {
+ document.querySelector("input").click();
+ } else {
+ t.done();
+ }
+ counter++;
+ });
+ document.querySelector("a").click();
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/embedded-opener-remove-frame.html b/testing/web-platform/tests/html/browsers/windows/embedded-opener-remove-frame.html
new file mode 100644
index 0000000000..a66f52e5f6
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/embedded-opener-remove-frame.html
@@ -0,0 +1,66 @@
+<!doctype html>
+<title>opener and discarded browsing contexts</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<iframe name=matchesastring></iframe>
+<script>
+function testOpener(t, otherW, thisW, discardOtherBC, isDiscardedFromATask) {
+ assert_equals(otherW.opener, thisW, "opener before removal");
+
+ const openerDesc = Object.getOwnPropertyDescriptor(otherW, "opener"),
+ openerGet = openerDesc.get;
+
+ assert_equals(openerGet(), thisW, "opener before removal via directly invoking the getter");
+ discardOtherBC();
+ if (isDiscardedFromATask) {
+ t.step_timeout(() => {
+ testOpenerRemainder(t, otherW, openerDesc, openerGet);
+ }, 250);
+ } else {
+ testOpenerRemainder(t, otherW, openerDesc, openerGet);
+ }
+}
+
+function testOpenerRemainder(t, otherW, openerDesc, openerGet) {
+ assert_equals(otherW.opener, null, "opener after removal");
+ assert_equals(openerGet(), null, "opener after removal via directly invoking the getter");
+
+ otherW.opener = null;
+ assert_equals(openerGet(), null, "opener after setting it null via directly invoking the getter");
+ const openerDescNull = Object.getOwnPropertyDescriptor(otherW, "opener");
+ assert_not_equals(openerDescNull, openerDesc);
+ assert_object_equals(openerDescNull, openerDesc);
+
+ otherW.opener = "immaterial";
+ assert_equals(openerGet(), null, "opener after setting it \"immaterial\" via directly invoking the getter");
+ const openerDescImmaterial = Object.getOwnPropertyDescriptor(otherW, "opener");
+ assert_equals(openerDescImmaterial.value, "immaterial");
+ assert_true(openerDescImmaterial.writable);
+ assert_true(openerDescImmaterial.enumerable);
+ assert_true(openerDescImmaterial.configurable);
+
+ t.done();
+}
+
+async_test(t => {
+ const frame = document.querySelector("iframe"),
+ frameW = frame.contentWindow;
+ frame.onload = t.step_func(() => {
+ // Firefox and Chrome/Safari load differently
+ if (frame.contentWindow.location.href === "about:blank") {
+ return;
+ }
+
+ testOpener(t, frameW, window, () => frame.remove(), false);
+ });
+ window.open("/common/blank.html", "matchesastring");
+}, "opener of discarded nested browsing context");
+
+async_test(t => {
+ const popupW = window.open("/common/blank.html");
+ popupW.onload = t.step_func(() => {
+ testOpener(t, popupW, window, () => popupW.close(), true);
+ });
+}, "opener of discarded auxiliary browsing context");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/embedded-opener.html b/testing/web-platform/tests/html/browsers/windows/embedded-opener.html
new file mode 100644
index 0000000000..8e02664342
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/embedded-opener.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<title>opener and embedded documents; using window.open()</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<iframe name=matchesastring></iframe>
+<script>
+async_test(t => {
+ const frame = document.querySelector("iframe");
+ frame.onload = t.step_func(() => {
+ // Firefox and Chrome/Safari load differently
+ if (frame.contentWindow.location.href === "about:blank") {
+ return;
+ }
+
+ // Test bits
+ assert_equals(frame.contentWindow.opener, window, "opener before setting it to null");
+
+ const openerDesc = Object.getOwnPropertyDescriptor(frame.contentWindow, "opener"),
+ openerGet = openerDesc.get;
+
+ assert_equals(openerGet(), window, "opener before setting it to null via directly invoking the getter");
+ frame.contentWindow.opener = null;
+ frame.contentWindow.opener = "immaterial";
+ assert_equals(openerGet(), null, "opener after setting it to null via directly invoking the getter");
+ assert_equals(frame.contentWindow.opener, "immaterial");
+
+ t.done();
+ });
+ window.open("/common/blank.html", "matchesastring");
+});
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/iframe-cross-origin-print.sub.html b/testing/web-platform/tests/html/browsers/windows/iframe-cross-origin-print.sub.html
new file mode 100644
index 0000000000..d07db13776
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/iframe-cross-origin-print.sub.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<link rel=match href="iframe-nested-print-ref.html">
+<style>
+ body { margin: 0 }
+</style>
+<iframe frameborder=0 scrolling=no src="//{{hosts[alt][www]}}:{{ports[http][0]}}{{location[path]}}/../resources/iframe-nested-cross-origin.html"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/windows/iframe-cross-origin-scaled-print.sub.html b/testing/web-platform/tests/html/browsers/windows/iframe-cross-origin-scaled-print.sub.html
new file mode 100644
index 0000000000..2442563082
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/iframe-cross-origin-scaled-print.sub.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<link rel=match href="iframe-nested-scaled-print-ref.html">
+<style>
+ body { margin: 0 }
+ div {
+ transform-origin: top left;
+ transform: scale(2);
+ overflow: hidden;
+ }
+ iframe {
+ width: 100px;
+ height: 50px;
+ }
+</style>
+<div>
+<iframe frameborder=0 scrolling=no src="//{{hosts[alt][www]}}:{{ports[http][0]}}{{location[path]}}/../resources/iframe-nested-printing-pass.html"></iframe>
+</div>
diff --git a/testing/web-platform/tests/html/browsers/windows/iframe-nested-print-ref.html b/testing/web-platform/tests/html/browsers/windows/iframe-nested-print-ref.html
new file mode 100644
index 0000000000..c36c459881
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/iframe-nested-print-ref.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<style>
+ body { margin: 0 }
+ p {
+ /* 2 times the default margin of <body>, to account for the doubly-nested document */
+ margin: 16px;
+ }
+</style>
+<p>PASS
diff --git a/testing/web-platform/tests/html/browsers/windows/iframe-nested-print.html b/testing/web-platform/tests/html/browsers/windows/iframe-nested-print.html
new file mode 100644
index 0000000000..844f6ba373
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/iframe-nested-print.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<link rel=match href="iframe-nested-print-ref.html">
+<style>
+ body { margin: 0 }
+</style>
+<iframe frameborder=0 scrolling=no srcdoc="<iframe scrolling=no frameborder=0 srcdoc='PASS'></iframe>"></iframe>
diff --git a/testing/web-platform/tests/html/browsers/windows/iframe-nested-scaled-print-ref.html b/testing/web-platform/tests/html/browsers/windows/iframe-nested-scaled-print-ref.html
new file mode 100644
index 0000000000..fceaf1e52c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/iframe-nested-scaled-print-ref.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<style>
+ body { margin: 0 }
+ div {
+ transform-origin: top left;
+ transform: scale(2);
+ }
+ iframe {
+ width: 100px;
+ height: 50px;
+ }
+</style>
+<div>
+<iframe frameborder=0 scrolling=no src="resources/iframe-nested-printing-pass.html"></iframe>
+</div>
diff --git a/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/frameElement-siblings.sub.html b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/frameElement-siblings.sub.html
new file mode 100644
index 0000000000..0c814d26d9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/frameElement-siblings.sub.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>window.frameElement access to a same-origin-domain sibling</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe src="//{{hosts[][]}}:{{ports[http][0]}}/html/browsers/windows/nested-browsing-contexts/resources/frameElement-sibling-accessed.html"></iframe>
+<iframe src="//{{hosts[][www]}}:{{ports[http][0]}}/html/browsers/windows/nested-browsing-contexts/resources/frameElement-sibling-accessor.html"></iframe>
+
+<script>
+"use strict";
+setup({ explicit_done: true });
+
+window.onload = () => {
+ promise_test(async () => {
+ frames[1].postMessage({}, "*");
+ const result = await waitForMessage();
+
+ assert_equals(result, "SecurityError");
+ }, "it must give a \"SecurityError\" DOMException if the pages are different-origin domain");
+
+ promise_test(async () => {
+ document.domain = document.domain;
+
+ frames[0].postMessage({ newDocumentDomain: document.domain }, "*");
+ assert_equals(await waitForMessage(), "done");
+
+ frames[1].postMessage({ newDocumentDomain: document.domain }, "*");
+ const result = await waitForMessage();
+
+ assert_equals(result, "HTMLIFrameElement");
+ }, "it must return the iframe element if the pages are same-origin domain");
+
+ done();
+};
+
+function waitForMessage() {
+ return new Promise(resolve => {
+ window.addEventListener("message", e => resolve(e.data), { once: true });
+ });
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/frameElement.sub.html b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/frameElement.sub.html
new file mode 100644
index 0000000000..7ea1182081
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/frameElement.sub.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>HTML Test: window.frameElement</title>
+ <link rel="author" title="Intel" href="http://www.intel.com/" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <!-- t1 (same-origin)-->
+ <iframe id="iframe_0"></iframe>
+ <iframe id="iframe_1" src="./resources/frameElement-nested-frame.html"></iframe>
+ <object id="object_id" name="object_name" type="text/html" data="about:blank"></object>
+ <embed id="embed_id" name="embed_name" type="image/svg+xml" src="/images/green.svg" />
+
+ <!-- t2 (cross-origin) -->
+ <iframe name="iframe_2" src="http://{{hosts[alt][]}}:{{ports[http][0]}}/html/browsers/windows/nested-browsing-contexts/resources/frameElement-nested-frame.html"></iframe>
+
+ <!-- t3 (cross-origin) -->
+ <iframe id="iframe_3" src="http://{{hosts[alt][]}}:{{ports[http][0]}}/html/browsers/windows/nested-browsing-contexts/resources/frameElement-window-post.html"></iframe>
+
+ <script>
+ test(function() {
+ assert_equals(window.frameElement, null,
+ "The frameElement attribute should be null.");
+ }, "The window's frameElement attribute must return null if it is not a nested browsing context");
+
+ var t1 = async_test("The window's frameElement attribute must return its container element if it is a nested browsing context");
+ window.addEventListener("load", t1.step_func_done(function() {
+ assert_equals(frames[0].frameElement, document.getElementById("iframe_0"),
+ "The frameElement attribute should be the first iframe element.");
+ assert_equals(window["object_name"].frameElement, document.getElementById("object_id"),
+ "The frameElement attribute should be the object element.");
+ assert_equals(window["embed_name"].frameElement, document.getElementById("embed_id"),
+ "The frameElement attribute should be the embed element.");
+ assert_equals(document.getElementById("iframe_1").contentWindow[0].frameElement,
+ document.getElementById("iframe_1").contentDocument.getElementById("f1"),
+ "The frameElement attribute should be the frame element in 'resources/frameElement-nested-frame.html'.");
+ }));
+
+ var t2 = async_test("The SecurityError must be thrown if the window accesses to frameElement attribute of a Window which does not have the same effective script origin");
+ window.addEventListener("load", t2.step_func_done(function() {
+ assert_throws_dom("SecurityError", function() {
+ frames["iframe_2"].frameElement;
+ },
+ "The SecurityError exception should be thrown.");
+ }));
+
+ var t3 = async_test("The window's frameElement attribute must return null if the container's document does not have the same effective script origin");
+ window.addEventListener("load", function() {
+ window.addEventListener("message", function(event) {
+ var data = JSON.parse(event.data);
+ if (data.name == "testcase3") {
+ t3.step(function() {
+ assert_equals(data.result, "window.frameElement = null",
+ "The frameElement attribute should be null.");
+ t3.done();
+ });
+ }
+ }, false);
+ document.getElementById("iframe_3").contentWindow.postMessage(null, "*");
+ })
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/name-attribute.window.js b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/name-attribute.window.js
new file mode 100644
index 0000000000..69908af71b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/name-attribute.window.js
@@ -0,0 +1,58 @@
+// META: script=/common/get-host-info.sub.js
+
+[
+ "frame", // This works without <frameset>, so great
+ "iframe",
+ "object",
+ "embed",
+].forEach(element => {
+ [
+ null,
+ "",
+ "initialvalue"
+ ].forEach(initialNameValue => {
+ [
+ "same-origin",
+ "cross-origin"
+ ].forEach(originType => {
+ async_test(t => {
+ const ident = element + initialNameValue + originType,
+ file = `${new URL("resources/post-to-parent.html", location.href).pathname}?ident=${ident}`,
+ child = originType === "same-origin" ? file : `${get_host_info().HTTP_REMOTE_ORIGIN}${file}`,
+ frame = document.createElement(element),
+ expectedNameValue = initialNameValue || "";
+ let state = "set";
+ const listener = t.step_func(e => {
+ if (e.data.ident === ident) {
+ assert_equals(e.data.name, expectedNameValue); // This check is always the same
+ if (state === "set") {
+ frame.setAttribute("name", "meh");
+ state = "remove"
+ e.source.postMessage(null, "*");
+ return;
+ }
+ if (state === "remove") {
+ frame.removeAttribute("name");
+ state = "done";
+ e.source.postMessage(null, "*");
+ return;
+ }
+ if (state === "done") {
+ t.done();
+ }
+ }
+ });
+ frame.setAttribute(element === "object" ? "data" : "src", child);
+ if (initialNameValue !== null) {
+ frame.setAttribute("name", initialNameValue);
+ }
+ t.add_cleanup(() => {
+ self.removeEventListener("message", listener);
+ frame.remove();
+ });
+ self.addEventListener("message", listener);
+ document.body.append(frame);
+ }, `${originType} <${element}${initialNameValue !== null ? ' name=' + initialNameValue : ''}>`);
+ });
+ });
+});
diff --git a/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/frameElement-nested-frame.html b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/frameElement-nested-frame.html
new file mode 100644
index 0000000000..d066b8d4cb
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/frameElement-nested-frame.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>HTML Test: child browsing context created by the frame element</title>
+<link rel="author" title="Intel" href="http://www.intel.com/" />
+<frameset>
+ <frame id="f1" name="frame">
+</frameset>
diff --git a/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/frameElement-sibling-accessed.html b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/frameElement-sibling-accessed.html
new file mode 100644
index 0000000000..15245981ce
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/frameElement-sibling-accessed.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>This page will set its document.domain on request so that its sibling can access it</title>
+
+<h1>I did get loaded</h1>
+
+<script>
+"use strict";
+
+window.onmessage = e => {
+ const { newDocumentDomain } = e.data;
+ document.domain = newDocumentDomain;
+
+ parent.postMessage("done", "*");
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/frameElement-sibling-accessor.html b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/frameElement-sibling-accessor.html
new file mode 100644
index 0000000000..4b4c7a87bb
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/frameElement-sibling-accessor.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>This page will attempt to access the frameElement of its sibling and report the results on request</title>
+
+<h1>I did get loaded</h1>
+
+<script>
+"use strict";
+
+window.onmessage = e => {
+ const { newDocumentDomain } = e.data;
+ if (newDocumentDomain) {
+ document.domain = newDocumentDomain;
+ }
+
+ const siblingWindow = parent.frames[0];
+
+ try {
+ const { frameElement } = siblingWindow;
+
+ let result = "something wierd happened";
+ if (frameElement === null) {
+ result = "null";
+ } else if (frameElement.constructor) {
+ result = frameElement.constructor.name;
+ }
+
+ parent.postMessage(result, "*");
+ } catch (e) {
+ parent.postMessage(e.name, "*");
+ }
+};
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/frameElement-window-post.html b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/frameElement-window-post.html
new file mode 100644
index 0000000000..d67bde26f4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/frameElement-window-post.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>Testcase 3: frameElement attribute must return null if the container\'s document does not have the same effective script origin</title>
+<script>
+window.addEventListener("message", function (event) {
+ try {
+ var result = "window.frameElement = " + window.frameElement;
+ } catch (e) {
+ result = e.message;
+ }
+ event.source.postMessage(JSON.stringify({name: "testcase3", result: result}),
+ "*");
+}, false);
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/post-to-opener.html b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/post-to-opener.html
new file mode 100644
index 0000000000..65a825f573
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/post-to-opener.html
@@ -0,0 +1,7 @@
+<script>
+ if (window.opener) {
+ window.opener.postMessage({
+ "parent_isTop": (window.parent === window)
+ }, "*");
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/post-to-parent.html b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/post-to-parent.html
new file mode 100644
index 0000000000..302e9d9cc1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/resources/post-to-parent.html
@@ -0,0 +1,6 @@
+<script>
+const ident = new URL(location).searchParams.get("ident"),
+ post = () => parent.postMessage({ name: window.name, ident }, "*");
+onmessage = () => post();
+post();
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/window-parent-null.html b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/window-parent-null.html
new file mode 100644
index 0000000000..428312086e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/window-parent-null.html
@@ -0,0 +1,66 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>window.parent: `null`</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+async_test(t => {
+ var iframe = document.createElement('iframe');
+ iframe.onload = t.step_func_done(() => {
+ var iWindow = iframe.contentWindow;
+ assert_equals(iWindow.parent, window);
+ document.body.removeChild(iframe);
+ assert_equals(iWindow.parent, null);
+ });
+
+ document.body.appendChild(iframe);
+}, '`window.parent` is null when browsing context container element removed');
+
+async_test(t => {
+ var iframe = document.createElement('iframe');
+ var iframe2, removedEl;
+
+ var testFunc = t.step_func(() => {
+ var frameWindow = iframe.contentWindow;
+ var frame2Window = iframe2.contentWindow;
+
+ assert_equals(frameWindow.parent, window);
+ assert_equals(frame2Window.parent, frameWindow);
+
+ iframe.removeEventListener('load', nestFrame);
+ iframe2.removeEventListener('load', testFunc);
+ removedEl = document.body.removeChild(iframe);
+
+ assert_equals(frameWindow.parent, null);
+ assert_equals(frame2Window.parent, null);
+
+ removedEl.addEventListener('load', t.step_func_done(() => {
+ // reattached iframe's browsing context will report window.parent again
+ assert_equals(removedEl.contentWindow.parent, window);
+ // The following window s are no longer referenced by active browsing contexts
+ assert_equals(frameWindow.parent, null);
+ assert_equals(frame2Window.parent, null);
+ // Per #the-iframe-element, reattaching a removed iframe will result in the
+ // browser creating a new browsing context once again, with a fresh
+ // document in our case, so the second iframe or any other elements
+ // previously added to iframe.contentDocument will be gone
+ assert_equals(removedEl.contentDocument.querySelector('iframe'), null);
+ assert_equals(removedEl.contentDocument.querySelector('hr'), null);
+ }));
+ document.body.appendChild(removedEl);
+ });
+
+ var nestFrame = function () {
+ iframe.contentDocument.body.appendChild(document.createElement('hr'));
+ iframe2 = iframe.contentDocument.createElement('iframe');
+ // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1229707
+ iframe2.src = '/common/blank.html';
+ iframe2.addEventListener('load', testFunc);
+ iframe.contentDocument.body.appendChild(iframe2);
+ };
+
+ iframe.addEventListener('load', nestFrame);
+ document.body.appendChild(iframe);
+}, '`window.parent` null when parent browsing context container removed');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/window-parent.html b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/window-parent.html
new file mode 100644
index 0000000000..ef662dca5e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/window-parent.html
@@ -0,0 +1,44 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>window.parent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ test(function() {
+ assert_equals(window, parent)
+ }, '`window.parent` for top-level browsing context');
+
+async_test(t => {
+ var iframe = document.createElement('iframe');
+ iframe.onload = t.step_func_done(function () {
+ var iWindow = iframe.contentWindow;
+ assert_equals(iWindow.parent, window);
+ });
+ document.body.appendChild(iframe);
+}, '`window.parent` on single nested browsing context');
+
+async_test(t => {
+ var iframe = document.createElement('iframe');
+ var iframe2;
+
+ var testFunc = t.step_func_done(function () {
+ var frameWindow = iframe.contentWindow;
+ var frame2Window = iframe2.contentWindow;
+ assert_equals(frameWindow.parent, window);
+ assert_equals(frame2Window.parent, frameWindow);
+ assert_not_equals(frame2Window.parent, window);
+ });
+
+ var nestFrame = function () {
+ iframe2 = iframe.contentDocument.createElement('iframe');
+ // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1229707
+ iframe2.src = '/common/blank.html';
+ iframe2.addEventListener('load', testFunc);
+ iframe.contentDocument.body.appendChild(iframe2);
+ };
+
+ iframe.addEventListener('load', nestFrame);
+ document.body.appendChild(iframe);
+}, '`window.parent` for multiple nested browsing contexts');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/window-top-null.html b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/window-top-null.html
new file mode 100644
index 0000000000..cf7fcf2ae1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/window-top-null.html
@@ -0,0 +1,66 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>window.top: `null`</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+async_test(function (t) {
+ var iframe = document.createElement('iframe');
+ iframe.onload = t.step_func_done(() => {
+ var iWindow = iframe.contentWindow;
+ /**
+ * `top` should return the top-level browsing context but will return
+ * `null` if none exists, such as when any of the BC's ancestor browsing
+ * context container's document elements are disconnected/removed.
+ */
+ assert_equals(iWindow.top, window);
+ document.body.removeChild(iframe);
+ assert_equals(iWindow.top, null);
+ });
+
+ document.body.appendChild(iframe);
+}, '`window.top` is null when browsing context container element removed');
+
+async_test(t => {
+ var iframe = document.createElement('iframe');
+ var iframe2, removedEl;
+
+ var testFunc = t.step_func(() => {
+ var frameWindow = iframe.contentWindow;
+ var frame2Window = iframe2.contentWindow;
+
+ assert_equals(frameWindow.top, window);
+ assert_equals(frame2Window.top, window);
+
+ iframe.removeEventListener('load', nestFrame);
+ iframe2.removeEventListener('load', testFunc);
+
+ removedEl = document.body.removeChild(iframe);
+
+ assert_equals(frameWindow.top, null);
+ assert_equals(frame2Window.top, null);
+
+ removedEl.addEventListener('load', t.step_func_done(() => {
+ // reattached iframe's browsing context will report window.top
+ assert_equals(removedEl.contentWindow.top, window);
+ // removed/re-added iframes will have new browsing context / window
+ assert_equals(frameWindow.top, null);
+ assert_equals(frame2Window.top, null);
+ }));
+
+ document.body.appendChild(removedEl);
+ });
+
+ var nestFrame = function () {
+ iframe2 = iframe.contentDocument.createElement('iframe');
+ // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1229707
+ iframe2.src = '/common/blank.html';
+ iframe2.addEventListener('load', testFunc);
+ iframe.contentDocument.body.appendChild(iframe2);
+ };
+
+ iframe.addEventListener('load', nestFrame);
+ document.body.appendChild(iframe);
+}, '`window.top`null when any ancestor browsing context container removed');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/window-top.html b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/window-top.html
new file mode 100644
index 0000000000..e5590adeb8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/nested-browsing-contexts/window-top.html
@@ -0,0 +1,65 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>window.top</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(function() {
+ assert_equals(window, top)
+}, "Top level browsing context");
+
+function step_func(test) {
+ return function (top_pointer) {
+ test.step(function() {assert_equals(top_pointer, window);})
+ test.done();
+ }
+}
+
+var t1 = async_test("One nested iframe");
+t1.step(function() {
+ var iframe = document.createElement("iframe");
+ //iframe.src = "data:text/html,"
+
+ iframe.onload = t1.step_func(
+ function() {
+ var doc = iframe.contentDocument;
+ iframe.contentWindow.test_func = step_func(t1);
+
+ var script = doc.createElement("script")
+ script.textContent = "test_func(top);"
+ doc.body.appendChild(script);
+ });
+ document.body.appendChild(iframe);
+});
+
+var t2 = async_test("Two nested iframes");
+t2.step(function() {
+ var iframe = document.createElement("iframe");
+ //iframe.src = "data:text/html,"
+
+ iframe.onload = t2.step_func(
+ function() {
+ var doc = iframe.contentDocument;
+ iframe2 = document.createElement("iframe");
+ //iframe2.src = "data:text/html,"
+ // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1229707
+ iframe2.src = '/common/blank.html';
+
+ iframe2.onload = t2.step_func(
+ function() {
+ var doc2 = iframe2.contentDocument;
+
+ iframe2.contentWindow.test_func = step_func(t2);
+
+ var script = doc2.createElement("script")
+ script.textContent = "test_func(top);"
+ doc2.body.appendChild(script);
+ });
+ doc.body.appendChild(iframe2);
+ });
+
+ document.body.appendChild(iframe);
+});
+
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/noreferrer-cross-origin-close-manual.sub.html b/testing/web-platform/tests/html/browsers/windows/noreferrer-cross-origin-close-manual.sub.html
new file mode 100644
index 0000000000..8e2740c268
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/noreferrer-cross-origin-close-manual.sub.html
@@ -0,0 +1,3 @@
+<meta charset=utf-8>
+<p>Follow this link to open a new browsing context and then confirm it can be closed:
+<a rel=noreferrer target=reallydoesnotmatter href="//天気の良い日.{{location[host]}}/html/browsers/windows/resources/window-close-button.html">link</a>.
diff --git a/testing/web-platform/tests/html/browsers/windows/noreferrer-cross-origin-manual.html b/testing/web-platform/tests/html/browsers/windows/noreferrer-cross-origin-manual.html
new file mode 100644
index 0000000000..f5879ee6d7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/noreferrer-cross-origin-manual.html
@@ -0,0 +1,10 @@
+<ol>
+ <li><p>After clicking these two links in order a single browsing context should be open showing
+ <code>example.org</code>:
+ <a target=doesnotmatter href="http://example.com/">one</a>,
+ <a target=doesnotmatter href="http://example.org/">two</a>.
+
+ <li><p>After clicking these two links two browsing contexts should have been opened:
+ <a rel=noreferrer target=reallydoesnotmatter href="http://example.com/">one</a>,
+ <a rel=noreferrer target=reallydoesnotmatter href="http://example.com/">two</a>.
+</ol>
diff --git a/testing/web-platform/tests/html/browsers/windows/noreferrer-cross-origin-window-name-manual.sub.html b/testing/web-platform/tests/html/browsers/windows/noreferrer-cross-origin-window-name-manual.sub.html
new file mode 100644
index 0000000000..c598ffbfaa
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/noreferrer-cross-origin-window-name-manual.sub.html
@@ -0,0 +1,3 @@
+<meta charset=utf-8>
+<p>Follow this link to open a new browsing context and then confirm it says "idonteven":
+<a rel=noreferrer target=idonteven href="//天気の良い日.{{location[host]}}/html/browsers/windows/resources/echo-window-name.html">link</a>.
diff --git a/testing/web-platform/tests/html/browsers/windows/noreferrer-null-opener.html b/testing/web-platform/tests/html/browsers/windows/noreferrer-null-opener.html
new file mode 100644
index 0000000000..f308abc05e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/noreferrer-null-opener.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<title>rel=noreferrer nullifies window.opener</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ async_test(function(t) {
+ localStorage.clear()
+
+ var hyperlink = document.body.appendChild(document.createElement("a"))
+ hyperlink.rel = "noreferrer"
+ hyperlink.target = "_blank"
+ hyperlink.href = "resources/window-opener.html"
+ hyperlink.click()
+ document.body.removeChild(hyperlink)
+
+ addEventListener("storage", function(e) {
+ t.step(function() {
+ assert_equals(e.newValue, "null")
+ localStorage.clear()
+ t.done()
+ })
+ })
+ })
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/noreferrer-window-name.html b/testing/web-platform/tests/html/browsers/windows/noreferrer-window-name.html
new file mode 100644
index 0000000000..5fe649d172
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/noreferrer-window-name.html
@@ -0,0 +1,86 @@
+<!doctype html>
+<title>rel=noreferrer and reuse of names</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ async_test(function(t) {
+ localStorage.clear()
+
+ function makeHyperlink(n) {
+ var hyperlink = document.body.appendChild(document.createElement("a"))
+ hyperlink.rel = "noreferrer"
+ hyperlink.target = "sufficientlyrandomwindownameamiright"
+ hyperlink.href = "resources/noreferrer-window-name.html#" + n
+ return hyperlink
+ }
+
+ var hyperlink1 = makeHyperlink(1),
+ hyperlink2 = makeHyperlink(2)
+
+ t.add_cleanup(function() {
+ localStorage.setItem("x", "close")
+ localStorage.clear()
+ document.body.removeChild(hyperlink1)
+ document.body.removeChild(hyperlink2)
+ })
+
+ addEventListener("storage", function(e) {
+ t.step(function() {
+ if(localStorage.getItem("window1") && localStorage.getItem("window2")) {
+ localStorage.setItem("x", "close")
+ t.done()
+ }
+ })
+ })
+
+ hyperlink1.click()
+ hyperlink2.click()
+ }, "Following a noreferrer link with a named target should not cause creation of a window that can be targeted by another noreferrer link with the same named target");
+
+ async_test(function(t) {
+ var ifr = document.createElement("iframe");
+ ifr.name = "sufficientlyrandomwindownameamiright2";
+ ifr.onload = t.step_func(function() {
+ var hyperlink = document.body.appendChild(document.createElement("a"));
+ t.add_cleanup(function() {
+ hyperlink.remove();
+ });
+ hyperlink.rel = "noreferrer";
+ hyperlink.href = URL.createObjectURL(new Blob(["hello subframe"],
+ { type: "text/html"}));
+ hyperlink.target = "sufficientlyrandomwindownameamiright2";
+ ifr.onload = t.step_func_done(function() {
+ assert_equals(ifr.contentDocument.documentElement.textContent,
+ "hello subframe");
+ });
+ hyperlink.click();
+ });
+ document.body.appendChild(ifr);
+ t.add_cleanup(function() {
+ ifr.remove();
+ });
+ }, "Targeting a rel=noreferrer link at an existing named subframe should work");
+
+ async_test(function(t) {
+ var win = window.open("", "sufficientlyrandomwindownameamiright3");
+ t.add_cleanup(function() {
+ win.close();
+ });
+
+ var hyperlink = document.body.appendChild(document.createElement("a"));
+ t.add_cleanup(function() {
+ hyperlink.remove();
+ });
+ hyperlink.rel = "noreferrer";
+ hyperlink.href = URL.createObjectURL(new Blob(["hello window"],
+ { type: "text/html"}));
+ hyperlink.target = "sufficientlyrandomwindownameamiright3";
+ win.onload = t.step_func_done(function() {
+ assert_equals(win.document.documentElement.textContent,
+ "hello window");
+ });
+ hyperlink.click();
+ }, "Targeting a rel=noreferrer link at an existing named window should work");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/opener-cross-origin-manual.sub.html b/testing/web-platform/tests/html/browsers/windows/opener-cross-origin-manual.sub.html
new file mode 100644
index 0000000000..0c018663a2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/opener-cross-origin-manual.sub.html
@@ -0,0 +1,10 @@
+<ol>
+ <li><p>Clicking this link must navigate this page to a resource that contains "THE END":
+ <a href=//{{domains[www1]}}:{{location[port]}}/html/browsers/windows/resources/opener-cross-origin.html target=_blank>test</a>
+
+ <li><p>Clicking this link must open a new browsing context that is empty:
+ <a rel=noreferrer href=//{{domains[www1]}}:{{location[port]}}/html/browsers/windows/resources/opener-cross-origin.html target=_blank>test</a>
+
+ <li><p>Clicking this link must navigate this page to a resource that contains "THE END":
+ <a href=//{{domains[www1]}}:{{location[port]}}/html/browsers/windows/resources/opener-cross-origin-embed.sub.html target=_blank>test</a>
+</ol>
diff --git a/testing/web-platform/tests/html/browsers/windows/opener-string.window.js b/testing/web-platform/tests/html/browsers/windows/opener-string.window.js
new file mode 100644
index 0000000000..a39c584d20
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/opener-string.window.js
@@ -0,0 +1,14 @@
+test(t => {
+ const popup = window.open();
+ t.add_cleanup(() => popup.close());
+ assert_equals(popup.opener, self, "The opener of the popup is me");
+ assert_equals(Object.getOwnPropertyDescriptor(popup, "opener").writable, undefined);
+
+ popup.opener = "blah";
+ assert_equals(popup.opener, "blah", "The popup's opener is now a string");
+ assert_equals(Object.getOwnPropertyDescriptor(popup, "opener").writable, true);
+
+ const openerGetter = Object.getOwnPropertyDescriptor(self, "opener").get;
+ const popupOpener = openerGetter.call(popup);
+ assert_equals(popupOpener, self, "The underlying opener of the popup is still me");
+}, "Setting popup.opener to a string");
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/first-party-to-first-party-cross-partition.sub.html b/testing/web-platform/tests/html/browsers/windows/post-message/first-party-to-first-party-cross-partition.sub.html
new file mode 100644
index 0000000000..f91d9403ea
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/first-party-to-first-party-cross-partition.sub.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>postMessage: First-Party to First-Party, Cross-Partition</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+// Here's the set-up for this test:
+// Step 1. (Site 1 Window) set up listener and open Site 2 Window.
+// Step 2. (Site 2 Window) send "Site 2 Window" message to Site 1 Window.
+// Step 3. (Site 1 Window) receive "Site 2 Window" message and exit.
+
+async_test(t => {
+ // Step 3
+ const listener = t.step_func(e => {
+ if (e.data === "Site 2 Window") {
+ t.done();
+ }
+ });
+ // Step 1
+ window.addEventListener("message", listener);
+ const site2Window = window.open("http://{{hosts[alt][]}}:{{ports[http][0]}}/html/browsers/windows/post-message/resources/first-party-to-first-party-cross-partition-window.html", "", "noopener=false");
+ t.add_cleanup(() => site2Window.close());
+}, "postMessage: First-Party to First-Party, Cross-Partition");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/first-party-to-first-party-same-partition.html b/testing/web-platform/tests/html/browsers/windows/post-message/first-party-to-first-party-same-partition.html
new file mode 100644
index 0000000000..6fc915298b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/first-party-to-first-party-same-partition.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>postMessage: First-Party to First-Party, Same-Partition</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+// Here's the set-up for this test:
+// Step 1. (Site 1 Window A) set up listener and open Site 1 Window B.
+// Step 2. (Site 1 Window B) send "Site 1 Window B" message to Site 1 Window A.
+// Step 3. (Site 1 Window A) receive "Site 1 Window B" message and exit.
+
+async_test(t => {
+ // Step 3
+ const listener = t.step_func(e => {
+ if (e.data === "Site 1 Window B") {
+ t.done();
+ }
+ });
+ // Step 1
+ window.addEventListener("message", listener);
+ const site1WindowB = window.open("/html/browsers/windows/post-message/resources/first-party-to-first-party-same-partition-window.html", "", "noopener=false");
+ t.add_cleanup(() => site1WindowB.close());
+}, "postMessage: First-Party to First-Party, Same-Partition");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/first-party-to-third-party-cross-partition-cross-origin.sub.html b/testing/web-platform/tests/html/browsers/windows/post-message/first-party-to-third-party-cross-partition-cross-origin.sub.html
new file mode 100644
index 0000000000..de776f8381
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/first-party-to-third-party-cross-partition-cross-origin.sub.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>postMessage: First-Party to Third-Party, Cross-Partition, Cross-Origin</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+// Here's the set-up for this test:
+// Step 1. (Site 1 Window) set up listener and open Site 2 Window.
+// Step 2. (Site 2 Window) open Site 3 Frame.
+// Step 3. (Site 3 Frame) send "Site 3 Frame" message to Site 1 Window.
+// Step 4. (Site 1 Window) receive "Site 1 Frame" message and exit.
+
+async_test(t => {
+ // Step 4
+ const listener = t.step_func(e => {
+ if (e.data === "Site 3 Frame") {
+ t.done();
+ }
+ });
+ // Step 1
+ window.addEventListener("message", listener);
+ const site2Window = window.open("https://{{hosts[alt][]}}:{{ports[https][0]}}/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-cross-origin-window.sub.https.html", "", "noopener=false");
+ t.add_cleanup(() => site2Window.close());
+}, "postMessage: First-Party to Third-Party, Cross-Partition, Cross-Origin");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/first-party-to-third-party-cross-partition-same-origin.sub.html b/testing/web-platform/tests/html/browsers/windows/post-message/first-party-to-third-party-cross-partition-same-origin.sub.html
new file mode 100644
index 0000000000..490b647fa4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/first-party-to-third-party-cross-partition-same-origin.sub.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>postMessage: First-Party to Third-Party, Cross-Partition, Same-Origin</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+// Here's the set-up for this test:
+// Step 1. (Site 1 Window) set up listener and open Site 2 Window.
+// Step 2. (Site 2 Window) open Site 1 Frame.
+// Step 3. (Site 1 Frame) send "Site 1 Frame" message to Site 1 Window.
+// Step 4. (Site 1 Window) receive "Site 1 Frame" message and exit.
+
+async_test(t => {
+ // Step 4
+ const listener = t.step_func(e => {
+ if (e.data === "Site 1 Frame") {
+ t.done();
+ }
+ });
+ // Step 1
+ window.addEventListener("message", listener);
+ const site2Window = window.open("http://{{hosts[alt][]}}:{{ports[http][0]}}/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-same-origin-window.sub.html", "", "noopener=false");
+ t.add_cleanup(() => site2Window.close());
+}, "postMessage: First-Party to Third-Party, Cross-Partition, Same-Origin");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-first-party-cross-partition-window.html b/testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-first-party-cross-partition-window.html
new file mode 100644
index 0000000000..cdfa9d95ca
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-first-party-cross-partition-window.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<meta charset=utf-8>
+<body>
+<script>
+// Step 2 (html/browsers/windows/post-message/first-party-to-first-party-cross-partition.sub.html)
+window.opener.postMessage("Site 2 Window", "*");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-first-party-same-partition-window.html b/testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-first-party-same-partition-window.html
new file mode 100644
index 0000000000..4baf45d3c8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-first-party-same-partition-window.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<meta charset=utf-8>
+<body>
+<script>
+// Step 2 (html/browsers/windows/post-message/first-party-to-first-party-same-partition.html)
+window.opener.postMessage("Site 1 Window B", window.opener.origin);
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-cross-origin-iframe.https.html b/testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-cross-origin-iframe.https.html
new file mode 100644
index 0000000000..eef594831a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-cross-origin-iframe.https.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<meta charset=utf-8>
+<body>
+<script>
+// Step 3 (html/browsers/windows/post-message/first-party-to-third-party-cross-partition-cross-origin.sub.html)
+window.top.opener.postMessage("Site 3 Frame", "*");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-cross-origin-window.sub.https.html b/testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-cross-origin-window.sub.https.html
new file mode 100644
index 0000000000..7edfbd9328
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-cross-origin-window.sub.https.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<meta charset=utf-8>
+<body>
+<!--Step 2 (html/browsers/windows/post-message/first-party-to-third-party-cross-partition-cross-origin.sub.html)-->
+<iframe src="https://{{host}}:{{ports[https][0]}}/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-cross-origin-iframe.https.html"></iframe>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-same-origin-iframe.html b/testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-same-origin-iframe.html
new file mode 100644
index 0000000000..eb17036c8c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-same-origin-iframe.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<meta charset=utf-8>
+<body>
+<script>
+// Step 3 (html/browsers/windows/post-message/first-party-to-third-party-cross-partition-same-origin.sub.html)
+window.top.opener.postMessage("Site 1 Frame", window.top.opener.origin);
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-same-origin-window.sub.html b/testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-same-origin-window.sub.html
new file mode 100644
index 0000000000..f99b96c466
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-same-origin-window.sub.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<meta charset=utf-8>
+<body>
+<!--Step 2 (html/browsers/windows/post-message/first-party-to-third-party-cross-partition-same-origin.sub.html)-->
+<iframe src="http://{{host}}:{{ports[http][0]}}/html/browsers/windows/post-message/resources/first-party-to-third-party-cross-partition-same-origin-iframe.html"></iframe>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-cross-origin-iframe.sub.html b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-cross-origin-iframe.sub.html
new file mode 100644
index 0000000000..348efb3f9c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-cross-origin-iframe.sub.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<meta charset=utf-8>
+<body>
+<script>
+// Step 4 (html/browsers/windows/post-message/third-party-to-first-party-cross-partition-cross-origin.sub.html)
+let site2Window;
+const listener = e => {
+ if (e.data === "Site 3 Window") {
+ site2Window.close();
+ window.top.postMessage("Site 2 Frame", "*");
+ }
+};
+// Step 2 (html/browsers/windows/post-message/third-party-to-first-party-cross-partition-cross-origin.sub.html)
+window.addEventListener("message", listener);
+site2Window = window.open("https://{{hosts[alt][]}}:{{ports[https][0]}}/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-cross-origin-window.https.html", "", "noopener=false");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-cross-origin-window.https.html b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-cross-origin-window.https.html
new file mode 100644
index 0000000000..0045909494
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-cross-origin-window.https.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<meta charset=utf-8>
+<body>
+<script>
+// Step 3 (html/browsers/windows/post-message/third-party-to-first-party-cross-partition-cross-origin.sub.html)
+window.opener.postMessage("Site 3 Window", "*");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-same-origin-iframe.sub.html b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-same-origin-iframe.sub.html
new file mode 100644
index 0000000000..405b1053d3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-same-origin-iframe.sub.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<meta charset=utf-8>
+<body>
+<script>
+// Step 4 (html/browsers/windows/post-message/third-party-to-first-party-cross-partition-same-origin.sub.html)
+let site2Window;
+const listener = e => {
+ if (e.data === "Site 2 Window") {
+ site2Window.close();
+ window.top.postMessage("Site 2 Frame", "*");
+ }
+};
+// Step 2 (html/browsers/windows/post-message/third-party-to-first-party-cross-partition-same-origin.sub.html)
+window.addEventListener("message", listener);
+site2Window = window.open("http://{{hosts[alt][]}}:{{ports[http][0]}}/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-same-origin-window.html", "", "noopener=false");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-same-origin-window.html b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-same-origin-window.html
new file mode 100644
index 0000000000..0ae51e3fc1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-same-origin-window.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<meta charset=utf-8>
+<body>
+<script>
+// Step 3 (html/browsers/windows/post-message/third-party-to-first-party-cross-partition-same-origin.sub.html)
+window.opener.postMessage("Site 2 Window", window.opener.origin);
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-cross-origin-iframe-a.sub.html b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-cross-origin-iframe-a.sub.html
new file mode 100644
index 0000000000..a11f3640e8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-cross-origin-iframe-a.sub.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<meta charset=utf-8>
+<body>
+<script>
+// Step 5 (html/browsers/windows/post-message/third-party-to-third-party-cross-partition-cross-origin.sub.html)
+let site3Window;
+const listener = e => {
+ if (e.data === "Site 4 Frame") {
+ site3Window.close();
+ window.top.postMessage("Site 2 Frame", "*");
+ }
+};
+// Step 2 (html/browsers/windows/post-message/third-party-to-third-party-cross-partition-cross-origin.sub.html)
+window.addEventListener("message", listener);
+site3Window = window.open("https://{{host}}:{{ports[https][0]}}/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-cross-origin-window.sub.https.html", "", "noopener=false");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-cross-origin-iframe-b.https.html b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-cross-origin-iframe-b.https.html
new file mode 100644
index 0000000000..f56e2b35d8
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-cross-origin-iframe-b.https.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<meta charset=utf-8>
+<body>
+<script>
+// Step 4 (html/browsers/windows/post-message/third-party-to-third-party-cross-partition-cross-origin.sub.html)
+window.top.opener.postMessage("Site 4 Frame", "*");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-cross-origin-window.sub.https.html b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-cross-origin-window.sub.https.html
new file mode 100644
index 0000000000..1307287587
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-cross-origin-window.sub.https.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<meta charset=utf-8>
+<body>
+<!--Step 3 (html/browsers/windows/post-message/third-party-to-third-party-cross-partition-cross-origin.sub.html)-->
+<iframe src="https://{{hosts[alt][]}}:{{ports[https][0]}}/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-cross-origin-iframe-b.https.html"></iframe>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-same-origin-iframe-a.sub.https.html b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-same-origin-iframe-a.sub.https.html
new file mode 100644
index 0000000000..ec551bfdec
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-same-origin-iframe-a.sub.https.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<meta charset=utf-8>
+<body>
+<script>
+// Step 5 (html/browsers/windows/post-message/third-party-to-third-party-cross-partition-same-origin.sub.html)
+let site3Window;
+const listener = e => {
+ if (e.data === "Site 2 Frame B") {
+ site3Window.close();
+ window.top.postMessage("Site 2 Frame A", "*");
+ }
+};
+// Step 2 (html/browsers/windows/post-message/third-party-to-third-party-cross-partition-same-origin.sub.html)
+window.addEventListener("message", listener);
+site3Window = window.open("https://{{host}}:{{ports[https][0]}}/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-same-origin-window.sub.https.html", "", "noopener=false");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-same-origin-iframe-b.https.html b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-same-origin-iframe-b.https.html
new file mode 100644
index 0000000000..fc15b1f125
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-same-origin-iframe-b.https.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<meta charset=utf-8>
+<body>
+<script>
+// Step 4 (html/browsers/windows/post-message/third-party-to-third-party-cross-partition-same-origin.sub.html)
+window.top.opener.postMessage("Site 2 Frame B", window.top.opener.origin);
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-same-origin-window.sub.https.html b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-same-origin-window.sub.https.html
new file mode 100644
index 0000000000..f26f709db3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-same-origin-window.sub.https.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<meta charset=utf-8>
+<body>
+<!--Step 3 (html/browsers/windows/post-message/third-party-to-third-party-cross-partition-same-origin.sub.html)-->
+<iframe src="https://{{hosts[alt][]}}:{{ports[https][0]}}/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-same-origin-iframe-b.https.html"></iframe>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-same-partition-iframe-a.sub.html b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-same-partition-iframe-a.sub.html
new file mode 100644
index 0000000000..339dc9f06e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-same-partition-iframe-a.sub.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<meta charset=utf-8>
+<body>
+<script>
+// Step 5 (html/browsers/windows/post-message/third-party-to-third-party-same-partition.sub.html)
+let site2WindowB;
+const listener = e => {
+ if (e.data === "Site 2 Frame B") {
+ site2WindowB.close();
+ window.top.postMessage("Site 2 Frame A", "*");
+ }
+};
+// Step 2 (html/browsers/windows/post-message/third-party-to-third-party-same-partition.sub.html)
+window.addEventListener("message", listener);
+site2WindowB = window.open("http://{{host}}:{{ports[http][0]}}/html/browsers/windows/post-message/resources/third-party-to-third-party-same-partition-window.sub.html", "", "noopener=false");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-same-partition-iframe-b.html b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-same-partition-iframe-b.html
new file mode 100644
index 0000000000..daaf236bfc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-same-partition-iframe-b.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<meta charset=utf-8>
+<body>
+<script>
+// Step 4 (html/browsers/windows/post-message/third-party-to-third-party-same-partition.sub.html)
+window.top.opener.postMessage("Site 2 Frame B", window.top.opener.origin);
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-same-partition-window.sub.html b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-same-partition-window.sub.html
new file mode 100644
index 0000000000..7cea58f235
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/resources/third-party-to-third-party-same-partition-window.sub.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<meta charset=utf-8>
+<body>
+<!--Step 3 (html/browsers/windows/post-message/third-party-to-third-party-same-partition.sub.html)-->
+<iframe src="http://{{hosts[alt][]}}:{{ports[http][0]}}/html/browsers/windows/post-message/resources/third-party-to-third-party-same-partition-iframe-b.html"></iframe>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-first-party-cross-partition-cross-origin.sub.html b/testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-first-party-cross-partition-cross-origin.sub.html
new file mode 100644
index 0000000000..2618e966de
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-first-party-cross-partition-cross-origin.sub.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>postMessage: Third-Party to First-Party, Cross-Partition, Cross-Origin</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+// Here's the set-up for this test:
+// Step 1. (Site 1 Window) set up listener and open Site 2 Frame.
+// Step 2. (Site 2 Frame) set up listener and open Site 3 Window.
+// Step 3. (Site 3 Window) send "Site 3 Window" message to Site 2 Frame.
+// Step 4. (Site 2 Frame) receive "Site 3 Window" message and send "Site 2 Frame" message to Site 1 Window.
+// Step 5. (Site 1 Window) receive "Site 2 Frame" message and exit.
+
+async_test(t => {
+ // Step 5
+ const listener = t.step_func(e => {
+ if (e.data === "Site 2 Frame") {
+ t.done();
+ }
+ });
+ // Step 1
+ window.addEventListener("message", listener);
+ const site2Frame = document.createElement("iframe");
+ site2Frame.src = "http://{{hosts[alt][]}}:{{ports[http][0]}}/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-cross-origin-iframe.sub.html";
+ document.body.appendChild(site2Frame);
+}, "postMessage: Third-Party to First-Party, Cross-Partition, Cross-Origin");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-first-party-cross-partition-same-origin.sub.html b/testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-first-party-cross-partition-same-origin.sub.html
new file mode 100644
index 0000000000..735b458841
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-first-party-cross-partition-same-origin.sub.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>postMessage: Third-Party to First-Party, Cross-Partition, Same-Origin</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+// Here's the set-up for this test:
+// Step 1. (Site 1 Window) set up listener and open Site 2 Frame.
+// Step 2. (Site 2 Frame) set up listener and open Site 2 Window.
+// Step 3. (Site 2 Window) send "Site 2 Window" message to Site 2 Frame.
+// Step 4. (Site 2 Frame) receive "Site 2 Window" message and send "Site 2 Frame" message to Site 1 Window.
+// Step 5. (Site 1 Window) receive "Site 2 Frame" message and exit.
+
+async_test(t => {
+ // Step 5
+ const listener = t.step_func(e => {
+ if (e.data === "Site 2 Frame") {
+ t.done();
+ }
+ });
+ // Step 1
+ window.addEventListener("message", listener);
+ const site2Frame = document.createElement("iframe");
+ site2Frame.src = "http://{{hosts[alt][]}}:{{ports[http][0]}}/html/browsers/windows/post-message/resources/third-party-to-first-party-cross-partition-same-origin-iframe.sub.html";
+ document.body.appendChild(site2Frame);
+}, "postMessage: Third-Party to First-Party, Cross-Partition, Same-Origin");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-third-party-cross-partition-cross-origin.sub.html b/testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-third-party-cross-partition-cross-origin.sub.html
new file mode 100644
index 0000000000..1083071f7d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-third-party-cross-partition-cross-origin.sub.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>postMessage: Third-Party to Third-Party, Cross-Partition, Cross-Origin</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+// Here's the set-up for this test:
+// Step 1. (Site 1 Window) set up listener and open Site 2 Frame.
+// Step 2. (Site 2 Frame) set up listener and open Site 3 Window.
+// Step 3. (Site 3 Window) open Site 4 Frame.
+// Step 4. (Site 4 Frame) send "Site 4 Frame" message to Site 2 Frame.
+// Step 5. (Site 2 Frame) receive "Site 4 Frame" message and send "Site 2 Frame" message to Site 1 Window.
+// Step 6. (Site 1 Window) receive "Site 2 Frame" message and exit.
+
+async_test(t => {
+ // Step 6
+ const listener = t.step_func(e => {
+ if (e.data === "Site 2 Frame") {
+ t.done();
+ }
+ });
+ // Step 1
+ window.addEventListener("message", listener);
+ const site2FrameA = document.createElement("iframe");
+ site2FrameA.src = "http://{{hosts[alt][]}}:{{ports[http][0]}}/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-cross-origin-iframe-a.sub.html";
+ document.body.appendChild(site2FrameA);
+}, "postMessage: Third-Party to Third-Party, Cross-Partition, Cross-Origin");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-third-party-cross-partition-same-origin.sub.html b/testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-third-party-cross-partition-same-origin.sub.html
new file mode 100644
index 0000000000..9caf6c11e4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-third-party-cross-partition-same-origin.sub.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>postMessage: Third-Party to Third-Party, Cross-Partition, Same-Origin</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+// Here's the set-up for this test:
+// Step 1. (Site 1 Window) set up listener and open Site 2 Frame A.
+// Step 2. (Site 2 Frame A) set up listener and open Site 3 Window.
+// Step 3. (Site 3 Window) open Site 2 Frame B.
+// Step 4. (Site 2 Frame B) send "Site 2 Frame B" message to Site 2 Frame A.
+// Step 5. (Site 2 Frame A) receive "Site 2 Frame B" message and send "Site 2 Frame A" message to Site 1 Window.
+// Step 6. (Site 1 Window) receive "Site 2 Frame A" message and exit.
+
+async_test(t => {
+ // Step 6
+ const listener = t.step_func(e => {
+ if (e.data === "Site 2 Frame A") {
+ t.done();
+ }
+ });
+ // Step 1
+ window.addEventListener("message", listener);
+ const site2FrameA = document.createElement("iframe");
+ site2FrameA.src = "https://{{hosts[alt][]}}:{{ports[https][0]}}/html/browsers/windows/post-message/resources/third-party-to-third-party-cross-partition-same-origin-iframe-a.sub.https.html";
+ document.body.appendChild(site2FrameA);
+}, "postMessage: Third-Party to Third-Party, Cross-Partition, Same-Origin");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-third-party-same-partition.sub.html b/testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-third-party-same-partition.sub.html
new file mode 100644
index 0000000000..c90a055268
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/post-message/third-party-to-third-party-same-partition.sub.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>postMessage: Third-Party to Third-Party, Same-Partition</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+// Here's the set-up for this test:
+// Step 1. (Site 1 Window A) set up listener and open Site 2 Frame A.
+// Step 2. (Site 2 Frame A) set up listener and open Site 1 Window B.
+// Step 3. (Site 1 Window B) open Site 2 Frame B.
+// Step 4. (Site 2 Frame B) send "Site 2 Frame B" message to Site 2 Frame A.
+// Step 5. (Site 2 Frame A) receive "Site 2 Frame B" message and send "Site 2 Frame A" message to Site 1 Window A.
+// Step 6. (Site 1 Window A) receive "Site 2 Frame A" message and exit.
+
+async_test(t => {
+ // Step 6
+ const listener = t.step_func(e => {
+ if (e.data === "Site 2 Frame A") {
+ t.done();
+ }
+ });
+ // Step 1
+ window.addEventListener("message", listener);
+ const site2FrameA = document.createElement("iframe");
+ site2FrameA.src = "http://{{hosts[alt][]}}:{{ports[http][0]}}/html/browsers/windows/post-message/resources/third-party-to-third-party-same-partition-iframe-a.sub.html";
+ document.body.appendChild(site2FrameA);
+}, "postMessage: Third-Party to Third-Party, Same-Partition");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/resources/browsing-context-window.html b/testing/web-platform/tests/html/browsers/windows/resources/browsing-context-window.html
new file mode 100644
index 0000000000..c1594f637e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/resources/browsing-context-window.html
@@ -0,0 +1,7 @@
+<script>
+if (window.parent) {
+ window.parent.postMessage({
+ "thisWindowEquivalency": (window === this)
+ }, "*");
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/resources/document-domain-setter.html b/testing/web-platform/tests/html/browsers/windows/resources/document-domain-setter.html
new file mode 100644
index 0000000000..3b14255571
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/resources/document-domain-setter.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Helper page that sets document.domain</title>
+<script>
+"use strict";
+document.domain = document.domain;
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/resources/echo-window-name.html b/testing/web-platform/tests/html/browsers/windows/resources/echo-window-name.html
new file mode 100644
index 0000000000..a437fecb2c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/resources/echo-window-name.html
@@ -0,0 +1 @@
+<script>document.write(name)</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/resources/iframe-nested-cross-origin.html b/testing/web-platform/tests/html/browsers/windows/resources/iframe-nested-cross-origin.html
new file mode 100644
index 0000000000..a1a66ed842
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/resources/iframe-nested-cross-origin.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<iframe scrolling=no frameborder=0 srcdoc='PASS'></iframe>
diff --git a/testing/web-platform/tests/html/browsers/windows/resources/iframe-nested-printing-pass.html b/testing/web-platform/tests/html/browsers/windows/resources/iframe-nested-printing-pass.html
new file mode 100644
index 0000000000..c2dca9185f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/resources/iframe-nested-printing-pass.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<style>
+body {
+ margin: calc(0.5px * 2 / 3);
+}
+</style>
+<body>
+PASS
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/resources/message-parent.html b/testing/web-platform/tests/html/browsers/windows/resources/message-parent.html
new file mode 100644
index 0000000000..ba60618ad4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/resources/message-parent.html
@@ -0,0 +1,5 @@
+<script>
+ window.parent.postMessage({
+ "name": window.name,
+ }, "*");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/resources/nested-post-to-opener.html b/testing/web-platform/tests/html/browsers/windows/resources/nested-post-to-opener.html
new file mode 100644
index 0000000000..e92b69d7e7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/resources/nested-post-to-opener.html
@@ -0,0 +1,12 @@
+<body>
+<script>
+ var i = document.createElement("iframe");
+ i.name = "nested1";
+ document.body.appendChild(i);
+
+ window.opener.postMessage({
+ "name": window.name,
+ "isTop": window.top === window
+ }, "*");
+</script>
+</body>
diff --git a/testing/web-platform/tests/html/browsers/windows/resources/noreferrer-window-name.html b/testing/web-platform/tests/html/browsers/windows/resources/noreferrer-window-name.html
new file mode 100644
index 0000000000..8c106ca886
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/resources/noreferrer-window-name.html
@@ -0,0 +1,8 @@
+<script>
+ addEventListener("storage", function(e) {
+ if(e.newValue === "close") {
+ close()
+ }
+ })
+ localStorage.setItem("window" + location.hash.slice(1), "tralala")
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/resources/opener-cross-origin-embed.sub.html b/testing/web-platform/tests/html/browsers/windows/resources/opener-cross-origin-embed.sub.html
new file mode 100644
index 0000000000..db0c003141
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/resources/opener-cross-origin-embed.sub.html
@@ -0,0 +1,2 @@
+<meta charset=utf-8>
+<iframe src=//{{domains[élève]}}:{{location[port]}}/html/browsers/windows/resources/opener-cross-origin.html></iframe>
diff --git a/testing/web-platform/tests/html/browsers/windows/resources/opener-cross-origin-end.txt b/testing/web-platform/tests/html/browsers/windows/resources/opener-cross-origin-end.txt
new file mode 100644
index 0000000000..8b74fbab8a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/resources/opener-cross-origin-end.txt
@@ -0,0 +1 @@
+THE END
diff --git a/testing/web-platform/tests/html/browsers/windows/resources/opener-cross-origin.html b/testing/web-platform/tests/html/browsers/windows/resources/opener-cross-origin.html
new file mode 100644
index 0000000000..7c536b9213
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/resources/opener-cross-origin.html
@@ -0,0 +1,4 @@
+<script>
+ parent.opener.location.href = "./opener-cross-origin-end.txt"
+ parent.window.close()
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/resources/post-to-opener.html b/testing/web-platform/tests/html/browsers/windows/resources/post-to-opener.html
new file mode 100644
index 0000000000..453fec97a7
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/resources/post-to-opener.html
@@ -0,0 +1,8 @@
+<script>
+ if (window.opener) {
+ window.opener.postMessage({
+ "name": window.name,
+ "isTop": window.top === window
+ }, "*");
+ }
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/resources/restore-window-name-back.sub.html b/testing/web-platform/tests/html/browsers/windows/resources/restore-window-name-back.sub.html
new file mode 100644
index 0000000000..ea2c3a92b4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/resources/restore-window-name-back.sub.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<script>
+ onload = () => {
+ window.name = "clear";
+ history.back();
+ };
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/html/browsers/windows/resources/restore-window-name.sub.html b/testing/web-platform/tests/html/browsers/windows/resources/restore-window-name.sub.html
new file mode 100644
index 0000000000..a1e573a7c0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/resources/restore-window-name.sub.html
@@ -0,0 +1,43 @@
+<!doctype html>
+<html>
+<head>
+<title>popup helper for restore window.name test</title>
+<script>
+
+const search = window.location.search.replace("?", "");
+const name = search.split("=")[1];
+
+if (!search.startsWith("name=")) {
+ throw new Error("Unsupported test!");
+}
+
+function startTest() {
+ if (window.name === "start") {
+ window.name = name;
+ const url = new URL(window.location);
+ url.host = "{{hosts[alt][]}}:{{ports[https][0]}}";
+ url.pathname = url.pathname.slice(0, url.pathname.lastIndexOf("/") + 1);
+ url.pathname += "restore-window-name-back.sub.html";
+ window.location = url.href;
+ }
+}
+
+function verifyResult() {
+ const result = document.getElementById("result");
+ if (window.name === "start") {
+ result.textContent = "Please first click the above 'start test' link.";
+ return;
+ }
+
+ result.textContent = window.name === name ? "PASS" : "FAIL";
+}
+
+</script>
+</head>
+<body>
+ <p>Please first click the 'start test' link. And then, click the 'Verify Result' button to verify the result. You should get a PASS after clicking the button</p>
+ <a id="navigate" href="javascript:void(0)" onclick="startTest()">start test</a><br>
+ <button onclick="verifyResult()">Verify Result</button>
+ Result: <a id="result"></a>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/resources/target-cross-origin.sub.html b/testing/web-platform/tests/html/browsers/windows/resources/target-cross-origin.sub.html
new file mode 100644
index 0000000000..8472f96b90
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/resources/target-cross-origin.sub.html
@@ -0,0 +1,3 @@
+<meta charset=utf-8>
+<p>Follow this link to open a new browsing context in a separate origin.
+<a target=second href="{{location[scheme]}}://{{domains[www2]}}:{{location[port]}}/html/browsers/windows/resources/target-cross-origin.sub.html">link</a>.
diff --git a/testing/web-platform/tests/html/browsers/windows/resources/window-close-button.html b/testing/web-platform/tests/html/browsers/windows/resources/window-close-button.html
new file mode 100644
index 0000000000..38ec2aef5c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/resources/window-close-button.html
@@ -0,0 +1 @@
+<p>Clicking this button should close this browsing context: <button onclick=window.close()>button</button>
diff --git a/testing/web-platform/tests/html/browsers/windows/resources/window-name-stash.py b/testing/web-platform/tests/html/browsers/windows/resources/window-name-stash.py
new file mode 100644
index 0000000000..411a4587bc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/resources/window-name-stash.py
@@ -0,0 +1,13 @@
+def main(request, response):
+ key = request.GET.first(b"id")
+ if request.method == "POST":
+ value = request.GET.first(b"value")
+ request.server.stash.take(key)
+ request.server.stash.put(key, value)
+ return b"OK"
+ else:
+ value = request.server.stash.take(key)
+ if value is not None:
+ return value
+ else:
+ return b"NONE"
diff --git a/testing/web-platform/tests/html/browsers/windows/resources/window-name.sub.html b/testing/web-platform/tests/html/browsers/windows/resources/window-name.sub.html
new file mode 100644
index 0000000000..9255ba3401
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/resources/window-name.sub.html
@@ -0,0 +1,102 @@
+<!doctype html>
+<title>popup helper</title>
+<script>
+
+const search = decodeURIComponent(window.location.search.replace("?", ""));
+const steps = search.split("|");
+
+async function proceedTest() {
+ while (steps.length) {
+ const step = steps.shift();
+
+ if (step.startsWith("report=")) {
+ const id = step.split("=")[1];
+ const stashURL = new URL("window-name-stash.py", location);
+ stashURL.searchParams.set('id', id);
+ stashURL.searchParams.set('value', window.name);
+
+ await fetch(stashURL, { method: "POST" });
+ continue;
+ }
+
+ if (step === "close") {
+ window.close();
+ break;
+ }
+
+ if (step === "cross") {
+ const url = new URL(window.location);
+ url.host = "{{hosts[alt][]}}:{{ports[https][0]}}";
+ url.search = "?" + steps.join("|");
+ window.location = url.href;
+ break;
+ }
+
+ if (step === "same") {
+ const url = new URL(window.location);
+ url.host = "{{host}}:{{ports[https][0]}}";
+ url.search = "?" + steps.join("|");
+ window.location = url.href;
+ break;
+ }
+
+ if (step === "sub") {
+ const url = new URL(window.location);
+ url.host = "{{hosts[][www]}}:{{ports[https][0]}}";
+ url.search = "?" + steps.join("|");
+ window.location = url.href;
+ break;
+ }
+
+ if (step === "closeOpener") {
+ if (window.opener) {
+ window.opener.close();
+ }
+ continue;
+ }
+
+ if (step.startsWith("navOpener=")) {
+ if (!window.opener) {
+ continue;
+ }
+
+ let url = step.split("=")[1];
+ window.opener.location.href = url;
+
+ continue;
+ }
+
+ if (step === "open") {
+ const url = new URL(window.location);
+ url.host = "{{host}}:{{ports[https][0]}}";
+ url.search = "?" + steps.join("|");
+ window.open(url);
+ break;
+ }
+
+ if (step.startsWith("reportOpener=")) {
+ const id = step.split("=")[1];
+ const stashURL = new URL("window-name-stash.py", location);
+ stashURL.searchParams.set('id', id);
+ stashURL.searchParams.set('value', window.opener.name);
+
+ await fetch(stashURL, { method: "POST" });
+ continue;
+ }
+
+ if (step.startsWith("set=")) {
+ window.name = step.split("=")[1];
+ continue;
+ }
+
+ if (step.startsWith("setDomain=")) {
+ document.domain = step.split("=")[1];
+ continue;
+ }
+
+ throw new Error("Unsupported step!");
+ }
+}
+
+proceedTest();
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/resources/window-opener.html b/testing/web-platform/tests/html/browsers/windows/resources/window-opener.html
new file mode 100644
index 0000000000..c734eb3056
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/resources/window-opener.html
@@ -0,0 +1,4 @@
+<script>
+ localStorage.setItem("opener", window.opener)
+ window.close()
+</script>
diff --git a/testing/web-platform/tests/html/browsers/windows/restore-window-name-manual.https.html b/testing/web-platform/tests/html/browsers/windows/restore-window-name-manual.https.html
new file mode 100644
index 0000000000..13e8381e31
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/restore-window-name-manual.https.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+<head>
+ <title>Restore window.name when navigating back from a cross-origin</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/utils.js"></script>
+</head>
+<body>
+ <script>
+ function runTest() {
+ const name = token();
+ window.open(`resources/restore-window-name.sub.html?name=${name}`, "start");
+ }
+
+ </script>
+ <p>Please click the following link and follow the instructions in the page for testing</p>
+ <a target="start" href="javascript:void(0)" onclick="runTest()">run test</a>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/targeting-cross-origin-nested-browsing-contexts.html b/testing/web-platform/tests/html/browsers/windows/targeting-cross-origin-nested-browsing-contexts.html
new file mode 100644
index 0000000000..44d4fbad6b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/targeting-cross-origin-nested-browsing-contexts.html
@@ -0,0 +1,39 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Targeting nested browsing contexts</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script>
+ async_test(function (t) {
+ var windowsToClose = [];
+ window.onmessage = t.step_func(function (e) {
+ if (e.data.name == "openee") {
+ var a = document.body.appendChild(document.createElement('a'));
+ a.target = "nested1";
+ a.href = "resources/post-to-opener.html";
+ a.click();
+ windowsToClose.push(e.source);
+ } else {
+ assert_equals(e.data.name, "nested1");
+ assert_equals(e.data.isTop, true);
+ windowsToClose.push(e.source);
+ windowsToClose.forEach(function (w) {
+ w.close();
+ });
+ t.done();
+ }
+ });
+
+ var a = document.body.appendChild(document.createElement('a'));
+ a.target = "openee";
+ a.href = get_host_info().HTTP_REMOTE_ORIGIN + "/html/browsers/windows/resources/nested-post-to-opener.html";
+ a.click();
+ });
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/html/browsers/windows/targeting-multiple-cross-origin-manual.sub.html b/testing/web-platform/tests/html/browsers/windows/targeting-multiple-cross-origin-manual.sub.html
new file mode 100644
index 0000000000..c8f8cfbe29
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/targeting-multiple-cross-origin-manual.sub.html
@@ -0,0 +1,9 @@
+<meta charset=utf-8>
+<p>Follow this link to open a new browsing context in a separate origin. Follow the instructions
+in that new window, and then come back to this window.
+<a target=first href="{{location[scheme]}}://{{domains[天気の良い日]}}:{{location[port]}}/html/browsers/windows/resources/target-cross-origin.sub.html">link</a>.
+
+<p>Once you come back to this page, follow this link.
+<a target=second href="resources/echo-window-name.html">link</a>.
+
+<p>After clicking that link, you should have three additional windows open.
diff --git a/testing/web-platform/tests/html/browsers/windows/targeting-with-embedded-null-in-target.html b/testing/web-platform/tests/html/browsers/windows/targeting-with-embedded-null-in-target.html
new file mode 100644
index 0000000000..7407248ffe
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/windows/targeting-with-embedded-null-in-target.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Targeting with embedded null in target</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+ <iframe name="abc">
+ </iframe>
+ <a href="resources/message-parent.html" target="abc">Click me</a>
+ <script>
+ var t = async_test();
+ var target_name = "abc\u0000def";
+
+ onmessage = t.step_func_done(function (e) {
+ assert_equals(e.data.name, target_name,
+ "Should come from a window with the right name");
+ assert_equals(e.source, frames[1],
+ "Should come frome the right window");
+ });
+
+ t.step(function() {
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute("name", target_name);
+ document.body.appendChild(iframe);
+ var a = document.querySelector("a");
+ a.target = target_name;
+ a.click();
+ });
+ </script>
+</body>
+</html>