summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/navigation-api
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/navigation-api')
-rw-r--r--testing/web-platform/tests/navigation-api/META.yml4
-rw-r--r--testing/web-platform/tests/navigation-api/commit-behavior/after-transition-new-navigation-before-commit.html25
-rw-r--r--testing/web-platform/tests/navigation-api/commit-behavior/after-transition-push.html23
-rw-r--r--testing/web-platform/tests/navigation-api/commit-behavior/after-transition-reload.html19
-rw-r--r--testing/web-platform/tests/navigation-api/commit-behavior/after-transition-replace.html19
-rw-r--r--testing/web-platform/tests/navigation-api/commit-behavior/after-transition-traversal-commit-new-navigation-before-commit.html36
-rw-r--r--testing/web-platform/tests/navigation-api/commit-behavior/after-transition-traverse.html29
-rw-r--r--testing/web-platform/tests/navigation-api/commit-behavior/after-transition-uncancelable.html23
-rw-r--r--testing/web-platform/tests/navigation-api/commit-behavior/after-transition-window-stop-before-commit.html33
-rw-r--r--testing/web-platform/tests/navigation-api/commit-behavior/commit-throws.html95
-rw-r--r--testing/web-platform/tests/navigation-api/commit-behavior/multiple-intercept.html83
-rw-r--r--testing/web-platform/tests/navigation-api/commit-behavior/resources/after-transition-commit-helpers.js90
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/anchor-click.html18
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/constructor.html32
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/history-back-same-doc.html24
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/history-pushState.html16
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/history-replaceState.html18
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/location-api.html18
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/navigate-from-initial-about-blank-same-doc-popup.html15
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/navigate-from-initial-about-blank-same-doc.html12
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/navigate-from-initial-about-blank.html11
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/navigation-back-forward-cross-doc.html20
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/navigation-back-forward-same-doc.html40
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-cross-doc.html12
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-intercept.html21
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-preventDefault.html10
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-replace-cross-doc.html12
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-replace-intercept.html23
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-replace-same-doc.html23
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-same-doc.html21
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/navigation-reload-cross-doc.html12
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/navigation-reload-intercept.html21
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/navigation-updateCurrentEntry.html20
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/not-on-load.html17
-rw-r--r--testing/web-platform/tests/navigation-api/currententrychange-event/properties.html14
-rw-r--r--testing/web-platform/tests/navigation-api/focus-reset/autofocus.html185
-rw-r--r--testing/web-platform/tests/navigation-api/focus-reset/basic.html63
-rw-r--r--testing/web-platform/tests/navigation-api/focus-reset/change-focus-again-in-blur-during-intercept.html35
-rw-r--r--testing/web-platform/tests/navigation-api/focus-reset/change-focus-back-to-origial-during-intercept.html36
-rw-r--r--testing/web-platform/tests/navigation-api/focus-reset/change-focus-during-intercept.html34
-rw-r--r--testing/web-platform/tests/navigation-api/focus-reset/change-focus-then-remove-during-intercept.html40
-rw-r--r--testing/web-platform/tests/navigation-api/focus-reset/focus-reset-timing.html61
-rw-r--r--testing/web-platform/tests/navigation-api/focus-reset/multiple-intercept.html69
-rw-r--r--testing/web-platform/tests/navigation-api/focus-reset/resources/helpers.mjs73
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-origin-traversal-does-not-fire-navigate.html25
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-crossdocument-crossorigin-sameorigindomain.sub.html41
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-crossdocument-crossorigin.html30
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-crossdocument-sameorigin.html29
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-samedocument-crossorigin-sameorigindomain.sub.html41
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-samedocument-crossorigin.html39
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-samedocument-sameorigin.html27
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-crossdocument-crossorigin-sameorigindomain.sub.html34
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-crossdocument-crossorigin.html23
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-crossdocument-sameorigin.html26
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-samedocument-crossorigin-sameorigindomain.sub.html34
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-samedocument-crossorigin.html32
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-samedocument-sameorigin.html26
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-crossdocument-crossorigin-sameorigindomain.sub.html35
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-crossdocument-crossorigin.html24
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-crossdocument-sameorigin.html26
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-samedocument-crossorigin-sameorigindomain.sub.html35
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-samedocument-crossorigin.html33
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-samedocument-sameorigin.html26
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/resources/cross-origin-iframe-helper.html28
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/resources/document-domain-setter.sub.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-crossdocument-crossorigin-sameorigindomain.sub.html41
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-crossdocument-crossorigin.html30
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-crossdocument-sameorigin.html27
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-samedocument-crossorigin-sameorigindomain.sub.html41
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-samedocument-crossorigin.html39
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-samedocument-sameorigin.html27
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/defaultPrevented-navigation-preempted.html19
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/defaultPrevented-window-stop-after-dispatch.html16
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/event-constructor.html96
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-after-dispatch.html16
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-and-navigate.html27
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-canceled-event.html17
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-cross-document-same-origin.html21
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-cross-origin.html18
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-detach-multiple.html18
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-detach.html17
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-handler-null-or-undefined.html21
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-handler-returns-non-promise.html18
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-handler-throws.html26
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-history-pushState.html23
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-history-replaceState.html23
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-multiple-times-reject.html38
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-multiple-times.html41
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-navigation-back.html19
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-on-synthetic-event.html17
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-popstate.html26
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-reject.html29
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-resolve.html20
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/intercept-same-document-history-back.html40
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-cross-origin.html24
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-download-userInitiated.html29
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-download.html34
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-fragment.html25
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-same-origin-cross-document.html25
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-userInitiated.html29
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-with-target.html31
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-after-detach.html33
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-dynamic-index.html34
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-back-forward.html26
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-navigate.html23
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-reload.html25
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-form-get.html27
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-form-reload.html28
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-form-traverse.html44
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-form-userInitiated.html30
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-form-with-target.html29
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-form.html25
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-after-fragment.html31
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-after-pushState.html31
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-cross-document.html32
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-noop.html32
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-history-go-0.html27
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-history-pushState.html29
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-history-replaceState.html29
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-iframe-location.html30
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-location.html23
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-meta-refresh.html27
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-cross-document.html35
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-same-document-in-iframe.html33
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-same-document.html32
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-navigate.html22
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-to-javascript.html18
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-to-srcdoc.html34
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-window-open-self.html23
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigate-window-open.html30
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigatesuccess-cross-document.html14
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigatesuccess-same-document.html10
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigation-back-cross-document-preventDefault.html33
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigation-back-same-document-preventDefault.html26
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigation-traverseTo-in-iframe-same-document-preventDefault.html43
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigation-traverseTo-navigates-top-and-same-doc-child-and-cross-doc-child.html49
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigation-traverseTo-same-document-preventDefault-multiple-windows.html23
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/navigation-traverseTo-top-cancels-cross-document-child.html26
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/replaceState-in-unload-then-remove-iframe.html16
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/replaceState-inside-back-handler.html15
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/resources/meta-refresh.html4
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/resources/navigatesuccess-cross-document-helper.html6
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/resources/opener-postMessage-onload.html6
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/same-url-replace-cross-document.html23
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/same-url-replace-same-document.html23
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/signal-abort-detach-in-onnavigate.html21
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/signal-abort-intercept.html18
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/signal-abort-preventDefault.html19
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/signal-abort-window-stop-after-intercept.html40
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/signal-abort-window-stop-in-onnavigate.html24
-rw-r--r--testing/web-platform/tests/navigation-api/navigate-event/signal-abort-window-stop.html23
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/after-detach.html30
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/current-basic.html107
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/entries-across-origins.html33
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-bfcache-in-iframe.html36
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-bfcache.html60
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-blank-navigation-from-cross-origin.html22
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-blank-navigation.html25
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-blob-navigation.html25
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-cross-document-forward-pruning.html33
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-javascript-url-navigation.html38
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-navigations-in-multiple-windows.html34
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-srcdoc-navigation.html25
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/entries-array-equality.html8
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/entries-in-new-javascript-url-iframe.html15
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/entries-in-new-srcdoc-iframe.html15
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/entries-when-inactive.html16
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/entry-after-detach.html20
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/index-not-in-entries.html24
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-back-cross-document.html15
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-back-same-document.html24
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-location-reload-intercept.html23
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-location-reload.html18
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-location-replace-cross-origin.html17
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-location-replace.html18
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/no-referrer-dynamic-url-censored.html33
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/no-referrer-from-meta-url-censored.html32
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/no-referrer-url-censored.html32
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/opaque-origin-data-url.html17
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/opaque-origin.html9
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/resources/is_uuid.js3
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/resources/key-navigate-back-cross-document-helper.html27
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/resources/no-referrer-meta.html2
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/resources/no-referrer.html1
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/resources/no-referrer.html.headers1
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/resources/opaque-origin-page.html25
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/resources/post-entries-length-to-top.html7
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/resources/post-key-to-top.html3
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/sameDocument-after-fragment-navigate.html30
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/sameDocument-after-navigate-restore.html25
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/sameDocument-after-navigate.html14
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-history-entry/state-after-navigate-restore.html27
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/back-forward-multiple-frames.html74
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/disambigaute-back.html32
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/disambigaute-forward.html40
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/disambigaute-traverseTo-back-multiple.html35
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/disambigaute-traverseTo-forward-multiple.html39
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/forward-to-pruned-entry.html33
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/navigate-base-url.html15
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/navigate-from-initial-about-blank-gc.html18
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/navigate-from-initial-about-blank-src.html21
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/navigate-from-initial-about-blank.html22
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-push-not-loaded.html20
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-push-same-url-cross-document.html25
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-push-same-url.html26
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-state-replace.html16
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-state.html16
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/navigate-info-and-state.html18
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/navigate-intercept-history-state.html14
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/navigate-relative-url.html16
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/navigate-replace-cross-document.html29
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/navigate-replace-same-document.html21
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/navigate-same-document.html23
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/navigate-state-repeated-await.html13
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/navigate-state-repeated.html20
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/reload-base-url.html16
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/reload-info.html47
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/reload-navigation-timing.html15
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/reload-no-args.html46
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/reload-service-worker-fetch-event.html28
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/reload-state-and-info.html40
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/reload-state-undefined.html39
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/resources/fetch-event-test-worker.js7
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/resources/navigate-parent.html6
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/resources/navigate-sibling.html6
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/resources/navigation-back.html6
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/resources/page-with-base-url-common.html2
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/resources/service-worker-page.html2
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/resources/slow-no-store.py6
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-204-205-download.html52
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-already-detached.html30
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-beforeunload.html42
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-forward-initial-about-blank.html25
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-forward-opaque-origin.html9
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-forward-out-of-bounds.html16
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-intercept-rejected.html28
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-intercept.html25
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/back.html23
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward-already-detached.html33
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward-beforeunload.html45
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward-intercept-rejected.html29
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward-intercept.html28
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward.html24
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-204-205-download.html42
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-already-detached.html19
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-beforeunload.html31
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-cross-document.html16
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-detach-in-onnavigate.html18
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-detach-in-serialization.html24
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-file-url.html10
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-initial-about-blank-cross-document.html21
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-initial-about-blank.html20
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-intercept-interrupted.html23
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-intercept-rejected.html17
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-intercept.html15
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-interrupted-within-onnavigate.html28
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-interrupted.html21
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-invalid-url.html10
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-opaque-origin.html9
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-preventDefault.html12
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-push-initial-about-blank.html21
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-push-javascript-url.html19
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-beforeunload-unserializablestate.html31
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-detached-unserializablestate.html20
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-invalidurl-beforeunload.html31
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-invalidurl-detached.html20
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-invalidurl-unload.html27
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-invalidurl-unserializablestate.html12
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-unload-unserializablestate.html27
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-unload.html27
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-unserializable-state.html21
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate.html12
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-already-detached.html19
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-beforeunload.html31
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-detach-in-onnavigate.html18
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-detach-in-serialization.html24
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-initial-about-blank.html20
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-intercept-rejected.html17
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-intercept.html14
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-preventDefault.html12
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-rejection-order-beforeunload-unserializablestate.html31
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-rejection-order-detached-unserializablestate.html20
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-rejection-order-unload-unserializablestate.html32
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-unload.html27
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-unserializable-state.html21
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload.html16
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/resources/204-205-download-on-second-visit.py24
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/resources/back-forward-opaque-origin-page.html28
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/resources/helpers.js127
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/resources/navigate-opaque-origin-page.html16
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-already-detached.html20
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-beforeunload.html31
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-cross-document-preventDefault.html29
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-current.html17
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-detach-cross-document-before-navigate-event.html32
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-detach-cross-document.html29
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-detach-same-document-before-navigate-event.html31
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-detach-same-document.html28
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-intercept-rejected.html30
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-intercept.html27
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-invalid-key.html10
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-repeated.html24
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo.html25
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/sandboxing-back-parent.html38
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/sandboxing-back-sibling.html44
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/sandboxing-navigate-parent.html16
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/sandboxing-navigate-sibling.html19
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-after-adding-iframe.html34
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-after-data-url.html33
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-cross-document.html41
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-detach-between-navigate-and-navigatesuccess.html54
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-multiple-steps.html25
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-navigates-multiple-iframes.html45
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-same-document.html43
-rw-r--r--testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-with-cross-origin-in-history.html37
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/README.md26
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/anchor-download-intercept-reject.html54
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/anchor-download-intercept.html48
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/anchor-download.html24
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/back-same-document-intercept-reject.html54
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/back-same-document-intercept.html48
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/back-same-document.html40
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/currententrychange-before-popstate-intercept.html48
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/currententrychange-dispose-ordering.html26
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/intercept-async.html55
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-canceled.html39
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-double-intercept.html61
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-intercept-reentrant.html60
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-intercept-reject.html50
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-intercept.html44
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-204-205-download-then-same-document.html66
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-canceled.html42
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-commit-after-transition-intercept.html60
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-cross-document-double.html52
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-cross-document-event-order.html32
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-double-intercept.html68
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-in-transition-finished.html69
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-intercept-stop.html52
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-intercept.html47
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-same-document-intercept-reentrant.html66
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-same-document-intercept-reject.html53
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-same-document.html39
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/reload-canceled.html42
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/reload-intercept-reject.html53
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/reload-intercept.html47
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/resources/helpers.mjs193
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/resources/notify-top-early.html6
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/transition-cross-document.html44
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/transition-finished-mark-as-handled.html20
-rw-r--r--testing/web-platform/tests/navigation-api/ordering-and-transition/transition-realms-and-identity.html41
-rw-r--r--testing/web-platform/tests/navigation-api/per-entry-events/dispose-after-bfcache.html32
-rw-r--r--testing/web-platform/tests/navigation-api/per-entry-events/dispose-cross-document.html37
-rw-r--r--testing/web-platform/tests/navigation-api/per-entry-events/dispose-for-full-session-history.tentative.html20
-rw-r--r--testing/web-platform/tests/navigation-api/per-entry-events/dispose-for-navigation-in-child.html41
-rw-r--r--testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-intercept.html71
-rw-r--r--testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-navigate-during.html50
-rw-r--r--testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-reload-with-intercept.html17
-rw-r--r--testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-replace-with-intercept.html36
-rw-r--r--testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-replaceState.html28
-rw-r--r--testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document.html65
-rw-r--r--testing/web-platform/tests/navigation-api/per-entry-events/dispose-skip-current-on-truncate.html40
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-basic.html20
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-change-history-scroll-restoration-during-promise.html32
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-explicit-scroll.html29
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-intercept-handler-modifies.html33
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-push.html29
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-reject.html23
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-reload.html41
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-replace.html29
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-timing.html48
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-with-history-scroll-restoration-manual.html23
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/manual-basic.html20
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/manual-immediate-scroll.html24
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-after-dispatch.html33
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-after-resolve.html27
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-before-after-transition-commit.html32
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-fragment-does-not-exist.html32
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-push.html31
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-reload.html45
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-repeated.html29
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-replace.html31
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-resets-when-no-fragment.html33
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/scroll-after-preventDefault.html21
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/scroll-on-synthetic-event.html19
-rw-r--r--testing/web-platform/tests/navigation-api/scroll-behavior/scroll-without-intercept.html19
-rw-r--r--testing/web-platform/tests/navigation-api/state/cross-document-away-and-back.html31
-rw-r--r--testing/web-platform/tests/navigation-api/state/cross-document-getState-undefined.html17
-rw-r--r--testing/web-platform/tests/navigation-api/state/cross-document-getState.html19
-rw-r--r--testing/web-platform/tests/navigation-api/state/cross-document-location-api.html20
-rw-r--r--testing/web-platform/tests/navigation-api/state/history-pushState.html11
-rw-r--r--testing/web-platform/tests/navigation-api/state/history-replaceState.html11
-rw-r--r--testing/web-platform/tests/navigation-api/state/location-reload.html19
-rw-r--r--testing/web-platform/tests/navigation-api/state/same-document-away-and-back-location-api.html47
-rw-r--r--testing/web-platform/tests/navigation-api/state/same-document-away-and-back-navigation-api.html53
-rw-r--r--testing/web-platform/tests/navigation-api/updateCurrentEntry-method/basic.html25
-rw-r--r--testing/web-platform/tests/navigation-api/updateCurrentEntry-method/cross-document-away-and-back.html31
-rw-r--r--testing/web-platform/tests/navigation-api/updateCurrentEntry-method/cross-document-location-api.html20
-rw-r--r--testing/web-platform/tests/navigation-api/updateCurrentEntry-method/exception-order-initial-about-blank-unserializablestate.html13
-rw-r--r--testing/web-platform/tests/navigation-api/updateCurrentEntry-method/exception-order-not-fully-active-unserializablestate.html20
-rw-r--r--testing/web-platform/tests/navigation-api/updateCurrentEntry-method/history-pushState.html11
-rw-r--r--testing/web-platform/tests/navigation-api/updateCurrentEntry-method/history-replaceState.html11
-rw-r--r--testing/web-platform/tests/navigation-api/updateCurrentEntry-method/initial-about-blank.html13
-rw-r--r--testing/web-platform/tests/navigation-api/updateCurrentEntry-method/location-reload.html19
-rw-r--r--testing/web-platform/tests/navigation-api/updateCurrentEntry-method/no-args.html15
-rw-r--r--testing/web-platform/tests/navigation-api/updateCurrentEntry-method/not-fully-active.html20
-rw-r--r--testing/web-platform/tests/navigation-api/updateCurrentEntry-method/opaque-origin.html9
-rw-r--r--testing/web-platform/tests/navigation-api/updateCurrentEntry-method/resources/opaque-origin-page.html11
-rw-r--r--testing/web-platform/tests/navigation-api/updateCurrentEntry-method/same-document-away-and-back-location-api.html40
-rw-r--r--testing/web-platform/tests/navigation-api/updateCurrentEntry-method/unserializable.html29
409 files changed, 11951 insertions, 0 deletions
diff --git a/testing/web-platform/tests/navigation-api/META.yml b/testing/web-platform/tests/navigation-api/META.yml
new file mode 100644
index 0000000000..de4f6c9a33
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/META.yml
@@ -0,0 +1,4 @@
+spec: https://wicg.github.io/navigation-api/
+suggested_reviewers:
+ - domenic
+ - natechapin
diff --git a/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-new-navigation-before-commit.html b/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-new-navigation-before-commit.html
new file mode 100644
index 0000000000..2d09d40dc9
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-new-navigation-before-commit.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../navigation-methods/return-value/resources/helpers.js"></script>
+<body>
+<script>
+promise_test(async t => {
+ navigation.addEventListener("navigate", e => e.intercept({ commit: "after-transition" }), { once: "true" });
+
+ let navigateerror_called = false;
+ navigation.onnavigateerror = t.step_func(() => {
+ navigateerror_called = true;
+ assert_equals(location.hash, "");
+ });
+
+ let promises_should_not_commit = navigation.navigate("#ShouldNotCommit");
+ let promises_fulfilled = navigation.navigate("#1");
+ await assertBothRejectDOM(t, promises_should_not_commit, "AbortError");
+ await assertBothFulfill(t, promises_fulfilled, navigation.currentEntry);
+
+ assert_equals(location.hash, "#1");
+ assert_true(navigateerror_called);
+}, "Cancel a { commit: 'after-transition' } navigation before commit() by starting a new navigation");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-push.html b/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-push.html
new file mode 100644
index 0000000000..34bdcf7cac
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-push.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../navigation-methods/return-value/resources/helpers.js"></script>
+<script src="resources/after-transition-commit-helpers.js"></script>
+<body>
+<script>
+let tests = [
+ { mode: "rejectBeforeCommit", description: "{ commit: 'after-transition' } for a push navigation, reject before commit" },
+ { mode: "rejectAfterCommit", description: "{ commit: 'after-transition' } for a push navigation, reject after commit" },
+ { mode: "successExplicitCommit", description: "{ commit: 'after-transition' } for a push navigation, explicit commit()" },
+ { mode: "successNoExplicitCommit", description: "{ commit: 'after-transition' } for a push navigation, commit when handler resolves" }
+];
+
+let onload_promise = new Promise(resolve => window.onload = resolve);
+for (let test of tests) {
+ promise_test(async t => {
+ await onload_promise;
+ await testAfterTransitionCommit(t, "push", test.mode);
+ }, test.description);
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-reload.html b/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-reload.html
new file mode 100644
index 0000000000..203150eb05
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-reload.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../navigation-methods/return-value/resources/helpers.js"></script>
+<script src="resources/after-transition-commit-helpers.js"></script>
+<body>
+<script>
+let tests = [
+ { mode: "rejectBeforeCommit", description: "{ commit: 'after-transition' } for a reload navigation, reject before commit" },
+ { mode: "rejectAfterCommit", description: "{ commit: 'after-transition' } for a reload navigation, reject after commit" },
+ { mode: "successExplicitCommit", description: "{ commit: 'after-transition' } for a reload navigation, explicit commit()" },
+ { mode: "successNoExplicitCommit", description: "{ commit: 'after-transition' } for a reload navigation, commit when handler resolves" }
+];
+
+for (let test of tests) {
+ promise_test(t => testAfterTransitionCommit(t, "reload", test.mode), test.description);
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-replace.html b/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-replace.html
new file mode 100644
index 0000000000..2fd48736a3
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-replace.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../navigation-methods/return-value/resources/helpers.js"></script>
+<script src="resources/after-transition-commit-helpers.js"></script>
+<body>
+<script>
+let tests = [
+ { mode: "rejectBeforeCommit", description: "{ commit: 'after-transition' } for a replace navigation, reject before commit" },
+ { mode: "rejectAfterCommit", description: "{ commit: 'after-transition' } for a replace navigation, reject after commit" },
+ { mode: "successExplicitCommit", description: "{ commit: 'after-transition' } for a replace navigation, explicit commit()" },
+ { mode: "successNoExplicitCommit", description: "{ commit: 'after-transition' } for a replace navigation, commit when handler resolves" }
+];
+
+for (let test of tests) {
+ promise_test(t => testAfterTransitionCommit(t, "replace", test.mode), test.description);
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-traversal-commit-new-navigation-before-commit.html b/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-traversal-commit-new-navigation-before-commit.html
new file mode 100644
index 0000000000..9e74e10676
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-traversal-commit-new-navigation-before-commit.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../navigation-methods/return-value/resources/helpers.js"></script>
+<body>
+<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));
+
+ await navigation.navigate("#1").finished;
+
+ let navigateerror_called = false;
+ navigation.onnavigateerror = t.step_func(() => {
+ navigateerror_called = true;
+ assert_equals(location.hash, "#1");
+ });
+
+ // Go back and wait for the navigate event to fire. This traversal will be deferred.
+ let promises_should_not_commit = assertBothRejectDOM(t, navigation.back(), "AbortError");
+ navigation.addEventListener("navigate", e => e.intercept({ commit: "after-transition", handler: () => new Promise(r => t.step_timeout(r, 1000)) }), { once: "true" });
+ await new Promise(resolve => navigation.addEventListener("navigate", resolve, { once: "true" }));
+
+ // While the traversal is deferred, start a new navigation and commit immediately.
+ navigation.addEventListener("navigate", e => e.intercept(), { once: "true" });
+ let promises_fulfilled = navigation.navigate("#2");
+
+ await promises_should_not_commit;
+ await assertBothFulfill(t, promises_fulfilled, navigation.currentEntry);
+
+ assert_equals(location.hash, "#2");
+ assert_true(navigateerror_called);
+}, "Cancel a { commit: 'after-transition' } traversal before commit() by starting a new navigation");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-traverse.html b/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-traverse.html
new file mode 100644
index 0000000000..7dd1226e54
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-traverse.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../navigation-methods/return-value/resources/helpers.js"></script>
+<script src="resources/after-transition-commit-helpers.js"></script>
+<body>
+<script>
+let tests = [
+ { mode: "rejectBeforeCommit", destinationIndex: 0, description: "{ commit: 'after-transition' } for a traverse navigation, reject before commit" },
+ { mode: "rejectAfterCommit", destinationIndex: 1, description: "{ commit: 'after-transition' } for a traverse navigation, reject after commit" },
+ { mode: "successExplicitCommit", destinationIndex: 2, description: "{ commit: 'after-transition' } for a traverse navigation, explicit commit()" },
+ { mode: "successNoExplicitCommit", destinationIndex: 3, description: "{ commit: 'after-transition' } for a traverse navigation, commit when handler resolves" }
+];
+
+// Push a bunch of history entries so each test case can target a unique entry.
+history.pushState("", "", "#1");
+history.pushState("", "", "#2");
+history.pushState("", "", "#3");
+history.pushState("", "", "#4");
+
+let onload_promise = new Promise(resolve => window.onload = resolve);
+for (let test of tests) {
+ promise_test(async t => {
+ await onload_promise;
+ await testAfterTransitionCommit(t, "traverse", test.mode, test.destinationIndex);
+ }, test.description);
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-uncancelable.html b/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-uncancelable.html
new file mode 100644
index 0000000000..45bde9f090
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-uncancelable.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<body>
+<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));
+
+ await i.contentWindow.navigation.navigate("#1").finished;
+
+ i.contentWindow.navigation.onnavigate = t.step_func(e => {
+ assert_false(e.cancelable);
+ // intercept() should throw with commit: "after-transition" because e.cancelable is false.
+ let iframe_constructor = i.contentWindow.DOMException;
+ assert_throws_dom("InvalidStateError", iframe_constructor, () => e.intercept({ commit: "after-transition" }));
+ });
+ await i.contentWindow.navigation.back().finished;
+}, "{ commit: 'after-transition' } for an uncancelable traverse navigation");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-window-stop-before-commit.html b/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-window-stop-before-commit.html
new file mode 100644
index 0000000000..0f5e57d5b6
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/commit-behavior/after-transition-window-stop-before-commit.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../navigation-methods/return-value/resources/helpers.js"></script>
+<body>
+<script>
+promise_test(async t => {
+ // Wait for after the load event because window.stop() hangs the test harness
+ // if called before the load event.
+ await new Promise(resolve => window.onload = () => t.step_timeout(resolve, 0));
+
+ navigation.onnavigate = e => {
+ e.intercept({ commit: "after-transition" });
+ };
+
+ navigation.onnavigatesuccess = t.unreached_func("navigatesuccess must not fire");
+ let navigateerror_called = false;
+ navigation.onnavigateerror = t.step_func(() => {
+ navigateerror_called = true;
+ assert_equals(location.hash, "");
+ });
+
+ let promises = navigation.navigate("#ShouldNotCommit");
+ assert_equals(location.hash, "");
+
+ window.stop();
+ await assertBothRejectDOM(t, promises, "AbortError");
+
+ assert_equals(location.hash, "");
+ assert_true(navigateerror_called);
+}, " { commit: 'after-transition' } with window.stop() before commit");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/commit-behavior/commit-throws.html b/testing/web-platform/tests/navigation-api/commit-behavior/commit-throws.html
new file mode 100644
index 0000000000..54abdbfd0f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/commit-behavior/commit-throws.html
@@ -0,0 +1,95 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+promise_test(async t => {
+ navigation.onnavigate = t.step_func(e => {
+ assert_throws_dom("InvalidStateError", () => e.commit());
+ });
+ await navigation.navigate("#").finished;
+}, "commit() before intercept()");
+
+promise_test(async t => {
+ navigation.onnavigate = t.step_func(e => {
+ e.intercept({ handler: t.step_func(() => {
+ assert_throws_dom("InvalidStateError", () => e.commit());
+ }) });
+ });
+ await navigation.navigate("#").finished;
+}, "commit() without commit behavior specified");
+
+promise_test(async t => {
+ navigation.onnavigate = t.step_func(e => {
+ e.intercept({
+ handler: t.step_func(() => {
+ assert_throws_dom("InvalidStateError", () => e.commit());
+ }),
+ commit: "immediate"
+ });
+ });
+ await navigation.navigate("#").finished;
+}, "commit() with { commit: immediate }");
+
+promise_test(async t => {
+ navigation.onnavigate = t.step_func(e => {
+ e.intercept({ commit: "after-transition" });
+ assert_throws_dom("InvalidStateError", () => e.commit());
+ });
+ await navigation.navigate("#").finished;
+}, "commit() during event dispatch");
+
+promise_test(async t => {
+ let navigate_event;
+ navigation.onnavigate = t.step_func(e => {
+ e.intercept({ commit: "after-transition" });
+ navigate_event = e;
+ });
+ await navigation.navigate("#").finished;
+ assert_throws_dom("InvalidStateError", () => navigate_event.commit());
+}, "commit() after finish");
+
+promise_test(async t => {
+ navigation.onnavigate = t.step_func(e => {
+ e.intercept({
+ handler: t.step_func(() => {
+ e.commit();
+ assert_throws_dom("InvalidStateError", () => e.commit());
+ }),
+ commit: "after-transition"
+ });
+ });
+ await navigation.navigate("#").finished;
+}, "commit() twice");
+
+promise_test(async t => {
+ // We need to grab an NavigationDestination to construct the event.
+ navigation.onnavigate = t.step_func(e => {
+ const event = new NavigateEvent("navigate", {
+ destination: e.destination,
+ signal: (new AbortController()).signal
+ });
+
+ assert_throws_dom("SecurityError", () => event.commit());
+ });
+ await navigation.navigate("#").finished;
+}, "commit() on synthetic NavigateEvent");
+
+promise_test(async t => {
+ let i = document.createElement("iframe");
+ i.src = "about:blank";
+ document.body.appendChild(i);
+ i.contentWindow.navigation.onnavigate = t.step_func(e => {
+ e.intercept({
+ handler: t.step_func(() => {
+ let iframe_constructor = i.contentWindow.DOMException;
+ i.remove();
+ assert_throws_dom("InvalidStateError", iframe_constructor, () => e.commit());
+ }),
+ commit: "after-transition"
+ });
+ });
+ i.contentWindow.navigation.navigate("#");
+}, "commit() in detached iframe");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/commit-behavior/multiple-intercept.html b/testing/web-platform/tests/navigation-api/commit-behavior/multiple-intercept.html
new file mode 100644
index 0000000000..848af6a65d
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/commit-behavior/multiple-intercept.html
@@ -0,0 +1,83 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<script>
+let i = 0;
+async function urlDidChangeImmediately(listener, expected) {
+ navigation.addEventListener("navigate", listener, { once: true });
+ let expected_hash = "#" + ++i;
+ assert_not_equals(location.hash, expected_hash);
+ let finished = navigation.navigate(expected_hash).finished;
+ assert_equals(location.hash === expected_hash, expected);
+ await finished;
+ assert_equals(location.hash, expected_hash);
+}
+
+async function testUrlDidChangeImmediately(listener) {
+ await urlDidChangeImmediately(listener, true);
+}
+
+async function testUrlDidNotChangeImmediately(listener) {
+ await urlDidChangeImmediately(listener, false);
+}
+
+promise_test(async t => {
+ await testUrlDidNotChangeImmediately(e => {
+ e.intercept({ commit: "after-transition" });
+ e.intercept({ commit: "after-transition" });
+ });
+}, "after-transition + after-transition");
+
+promise_test(async t => {
+ await testUrlDidNotChangeImmediately(e => {
+ e.intercept({ commit: "after-transition" });
+ e.intercept();
+ });
+}, "after-transition + (not provided)");
+
+promise_test(async t => {
+ await testUrlDidChangeImmediately(e => {
+ e.intercept({ commit: "after-transition" });
+ e.intercept({ commit: "immediate" });
+ });
+}, "after-transition + immediate");
+
+promise_test(async t => {
+ await testUrlDidNotChangeImmediately(e => {
+ e.intercept({ commit: "immediate" });
+ e.intercept({ commit: "after-transition" });
+ });
+}, "immediate + after-transition");
+
+promise_test(async t => {
+ await testUrlDidChangeImmediately(e => {
+ e.intercept({ commit: "immediate" });
+ e.intercept();
+ });
+}, "immediate + (not provided)");
+
+promise_test(async t => {
+ await testUrlDidChangeImmediately(e => {
+ e.intercept({ commit: "immediate" });
+ e.intercept({ commit: "immediate" });
+ });
+}, "immediate + immediate");
+
+promise_test(async t => {
+ await testUrlDidNotChangeImmediately(e => {
+ e.intercept();
+ e.intercept({ commit: "after-transition" });
+ });
+}, "(not provided) + after-transition");
+
+promise_test(async t => {
+ await testUrlDidChangeImmediately(e => {
+ e.intercept();
+ e.intercept({ commit: "immediate" });
+ });
+}, "(not provided) + immediate");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/commit-behavior/resources/after-transition-commit-helpers.js b/testing/web-platform/tests/navigation-api/commit-behavior/resources/after-transition-commit-helpers.js
new file mode 100644
index 0000000000..664e8d7280
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/commit-behavior/resources/after-transition-commit-helpers.js
@@ -0,0 +1,90 @@
+window.testAfterTransitionCommit = async (t, navigationType, mode, destinationIndex = 0) => {
+ let startHash = location.hash;
+ let destinationHash;
+ const err = new Error("boo!");
+
+ let popstate_fired = false;
+ window.addEventListener("popstate", () => popstate_fired = true, { once : true });
+ let navigatesuccess_fired = false;
+ navigation.addEventListener("navigatesuccess", () => navigatesuccess_fired = true, { once : true });
+ let navigateerror_fired = false;
+ navigation.addEventListener("navigateerror", () => navigateerror_fired = true, { once : true });
+
+ // mode-specific logic for the navigate event handler
+ let navigate_helpers = {
+ rejectBeforeCommit : async (e) => {
+ return Promise.reject(err);
+ },
+ rejectAfterCommit : async (e) => {
+ e.commit();
+ assert_equals(location.hash, destinationHash, "hash after commit");
+ assert_true(popstate_fired, "popstate fired after commit");
+ await new Promise(resolve => t.step_timeout(resolve, 0));
+ return Promise.reject(err);
+ },
+ successExplicitCommit : async (e) => {
+ e.commit();
+ assert_equals(location.hash, destinationHash, "hash after commit");
+ assert_true(popstate_fired, "popstate fired after commit");
+ return new Promise(resolve => t.step_timeout(resolve, 0));
+ },
+ successNoExplicitCommit : async (e) => {
+ assert_equals(location.hash, startHash, "start has after first async step");
+ assert_false(popstate_fired, "popstate fired after first async step");
+ await new Promise(resolve => t.step_timeout(resolve, 0));
+ assert_equals(location.hash, startHash, "start has after second async step");
+ assert_false(popstate_fired, "popstate fired after second async step");
+ return new Promise(resolve => t.step_timeout(resolve, 0));
+ }
+ }
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ commit: "after-transition",
+ handler: t.step_func(async () => {
+ assert_equals(e.navigationType, navigationType);
+ assert_equals(location.hash, startHash, "start hash");
+ assert_false(popstate_fired, "popstate fired at handler start");
+
+ await new Promise(resolve => t.step_timeout(resolve, 0));
+ assert_equals(location.hash, startHash, "hash after first async step");
+ assert_false(popstate_fired, "popstate fired after first async step");
+
+ return navigate_helpers[mode](e);
+ })});
+ }, { once: true });
+
+ let promises;
+ if (navigationType === "push" || navigationType === "replace") {
+ destinationHash = (startHash === "" ? "#" : startHash) + "a";
+ promises = navigation.navigate(destinationHash, { history: navigationType });
+ } else if (navigationType === "reload") {
+ destinationHash = startHash;
+ promises = navigation.reload();
+ } else if (navigationType === "traverse") {
+ let destinationEntry = navigation.entries()[destinationIndex];
+ destinationHash = new URL(destinationEntry.url).hash;
+ promises = navigation.traverseTo(destinationEntry.key);
+ }
+
+ if (mode === "rejectBeforeCommit") {
+ await assertBothRejectExactly(t, promises, err);
+ assert_equals(location.hash, startHash, "hash after promise resolution");
+ assert_false(popstate_fired, "popstate fired after promise resolution");
+ assert_false(navigatesuccess_fired, "navigatesuccess fired");
+ assert_true(navigateerror_fired, "navigateerror fired");
+ } else if (mode === "rejectAfterCommit") {
+ await promises.committed;
+ await assertCommittedFulfillsFinishedRejectsExactly(t, promises, navigation.currentEntry, err);
+ assert_equals(location.hash, destinationHash, "hash after promise resolution");
+ assert_true(popstate_fired, "popstate fired after promise resolution");
+ assert_false(navigatesuccess_fired, "navigatesuccess fired");
+ assert_true(navigateerror_fired, "navigateerror fired");
+ } else {
+ await promises.committed;
+ await assertBothFulfill(t, promises, navigation.currentEntry);
+ assert_equals(location.hash, destinationHash, "hash after promise resolution");
+ assert_true(popstate_fired, "popstate fired after promise resolution");
+ assert_true(navigatesuccess_fired, "navigatesuccess fired");
+ assert_false(navigateerror_fired, "navigateerror fired");
+ }
+}
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/anchor-click.html b/testing/web-platform/tests/navigation-api/currententrychange-event/anchor-click.html
new file mode 100644
index 0000000000..e0bf91166a
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/anchor-click.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<a id="a" href="#foo"></a>
+<script>
+test(t => {
+ let oncurrententrychange_called = false;
+ navigation.oncurrententrychange = t.step_func(e => {
+ oncurrententrychange_called = true;
+ assert_equals(e.from, navigation.entries()[0]);
+ assert_equals(e.from.index, 0);
+ assert_equals(e.navigationType, "push");
+ assert_equals(navigation.currentEntry.index, 1);
+ });
+ a.click();
+ assert_true(oncurrententrychange_called);
+}, "currententrychange fires for link click");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/constructor.html b/testing/web-platform/tests/navigation-api/currententrychange-event/constructor.html
new file mode 100644
index 0000000000..b09e68e1c7
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/constructor.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new NavigationCurrentEntryChangeEvent("currententrychange");
+ });
+}, "can't bypass required members by omitting the dictionary entirely");
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new NavigationCurrentEntryChangeEvent("currententrychange", {
+ navigationType: "push"
+ });
+ });
+}, "from is required");
+
+test(() => {
+ const event = new NavigationCurrentEntryChangeEvent("currententrychange", {
+ navigationType: "replace",
+ from: navigation.currentEntry
+ });
+ assert_equals(event.navigationType, "replace");
+ assert_equals(event.from, navigation.currentEntry);
+}, "all properties are reflected back");
+
+test(t => {
+ const event = new NavigationCurrentEntryChangeEvent("currententrychange", { from: navigation.currentEntry });
+ assert_equals(event.navigationType, null);
+}, "defaults are as expected");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/history-back-same-doc.html b/testing/web-platform/tests/navigation-api/currententrychange-event/history-back-same-doc.html
new file mode 100644
index 0000000000..768805b752
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/history-back-same-doc.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));
+ await navigation.navigate("#foo");
+ assert_equals(navigation.entries().length, 2);
+
+ let oncurrententrychange_called = false;
+ navigation.oncurrententrychange = t.step_func(e => {
+ oncurrententrychange_called = true;
+ assert_equals(e.from, navigation.entries()[1]);
+ assert_equals(e.navigationType, "traverse");
+ assert_equals(navigation.currentEntry.index, 0);
+ });
+ history.back();
+ assert_false(oncurrententrychange_called);
+ await new Promise(resolve => window.onpopstate = resolve);
+ assert_true(oncurrententrychange_called);
+}, "currententrychange fires for history.back()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/history-pushState.html b/testing/web-platform/tests/navigation-api/currententrychange-event/history-pushState.html
new file mode 100644
index 0000000000..1a17a7cec0
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/history-pushState.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(t => {
+ let oncurrententrychange_called = false;
+ navigation.oncurrententrychange = t.step_func(e => {
+ oncurrententrychange_called = true;
+ assert_equals(e.from, navigation.entries()[0]);
+ assert_equals(e.navigationType, "push");
+ assert_equals(navigation.currentEntry.index, 1);
+ });
+ history.pushState(1, "", "#1");
+ assert_true(oncurrententrychange_called);
+}, "currententrychange fires for history.pushState()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/history-replaceState.html b/testing/web-platform/tests/navigation-api/currententrychange-event/history-replaceState.html
new file mode 100644
index 0000000000..e8ae8ee57a
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/history-replaceState.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(t => {
+ let oncurrententrychange_called = false;
+ let original_currentEntry = navigation.currentEntry;
+ navigation.oncurrententrychange = t.step_func(e => {
+ oncurrententrychange_called = true;
+ assert_equals(e.from, original_currentEntry);
+ assert_equals(e.from.index, -1);
+ assert_equals(e.navigationType, "replace");
+ assert_equals(navigation.currentEntry.index, 0);
+ });
+ history.replaceState(1, "", "#1");
+ assert_true(oncurrententrychange_called);
+}, "currententrychange fires for history.replaceState()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/location-api.html b/testing/web-platform/tests/navigation-api/currententrychange-event/location-api.html
new file mode 100644
index 0000000000..88ebd985a1
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/location-api.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(t => {
+ let oncurrententrychange_called = false;
+ let original_entry = navigation.currentEntry;
+ navigation.oncurrententrychange = t.step_func(e => {
+ oncurrententrychange_called = true;
+ assert_equals(e.from, original_entry);
+ assert_equals(e.from.index, -1);
+ assert_equals(e.navigationType, "replace");
+ assert_equals(navigation.currentEntry.index, 0);
+ });
+ location.hash = "#foo";
+ assert_true(oncurrententrychange_called);
+}, "currententrychange fires for location API navigations");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/navigate-from-initial-about-blank-same-doc-popup.html b/testing/web-platform/tests/navigation-api/currententrychange-event/navigate-from-initial-about-blank-same-doc-popup.html
new file mode 100644
index 0000000000..2399fb2ef9
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/navigate-from-initial-about-blank-same-doc-popup.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(async t => {
+ const w = window.open("about:blank#1");
+ w.navigation.oncurrententrychange = t.unreached_func("currententrychange should not fire");
+
+ assert_equals(w.location.href, "about:blank#1");
+ w.location.href = "about:blank#2";
+ assert_equals(w.location.href, "about:blank#2");
+
+ await new Promise(resolve => t.step_timeout(resolve, 10));
+}, "currententrychange does not fire when navigating away from the initial about:blank (popup window)");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/navigate-from-initial-about-blank-same-doc.html b/testing/web-platform/tests/navigation-api/currententrychange-event/navigate-from-initial-about-blank-same-doc.html
new file mode 100644
index 0000000000..f0fa9cdf57
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/navigate-from-initial-about-blank-same-doc.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i"></iframe>
+<script>
+promise_test(async t => {
+ i.contentWindow.navigation.oncurrententrychange = t.unreached_func("currententrychange should not fire");
+ i.contentWindow.location.href = "about:blank#1";
+ i.contentWindow.location.href = "about:blank#2";
+ await new Promise(resolve => t.step_timeout(resolve, 10));
+}, "currententrychange does not fire when navigating away from the initial about:blank");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/navigate-from-initial-about-blank.html b/testing/web-platform/tests/navigation-api/currententrychange-event/navigate-from-initial-about-blank.html
new file mode 100644
index 0000000000..56eaa1af26
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/navigate-from-initial-about-blank.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i"></iframe>
+<script>
+promise_test(async t => {
+ i.contentWindow.navigation.oncurrententrychange = t.unreached_func("currententrychange should not fire");
+ i.contentWindow.navigation.navigate("/common/blank.html");
+ await new Promise(resolve => t.step_timeout(resolve, 10));
+}, "currententrychange does not fire when navigating away from the initial about:blank");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-back-forward-cross-doc.html b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-back-forward-cross-doc.html
new file mode 100644
index 0000000000..7416caa94b
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-back-forward-cross-doc.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+ i.contentWindow.navigation.navigate("/common/blank.html?1");
+ await new Promise(resolve => i.onload = resolve);
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+
+ i.contentWindow.navigation.oncurrententrychange = t.unreached_func("currententrychange should not fire for cross-document navigations");
+ i.contentWindow.navigation.back();
+ await new Promise(resolve => i.onload = resolve);
+
+ i.contentWindow.navigation.oncurrententrychange = t.unreached_func("currententrychange should not fire for cross-document navigations");
+ i.contentWindow.navigation.forward();
+ await new Promise(resolve => i.onload = resolve);
+}, "currententrychange does not fire for cross-document navigation.back() and navigation.forward()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-back-forward-same-doc.html b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-back-forward-same-doc.html
new file mode 100644
index 0000000000..8182673aa5
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-back-forward-same-doc.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));
+ await navigation.navigate("#foo").committed;
+ assert_equals(navigation.entries().length, 2);
+
+ let oncurrententrychange_back_called = false;
+ let back_committed = false;
+ navigation.oncurrententrychange = t.step_func(e => {
+ oncurrententrychange_back_called = true;
+ assert_equals(e.from, navigation.entries()[1]);
+ assert_equals(e.navigationType, "traverse");
+ assert_equals(navigation.currentEntry.index, 0);
+ assert_false(back_committed);
+ });
+ let back_result = navigation.back();
+ assert_false(oncurrententrychange_back_called);
+ await back_result.committed.then(() => back_committed = true);
+ assert_true(oncurrententrychange_back_called);
+
+ let oncurrententrychange_forward_called = false;
+ let forward_committed = false;
+ navigation.oncurrententrychange = t.step_func(e => {
+ oncurrententrychange_forward_called = true;
+ assert_equals(e.from, navigation.entries()[0]);
+ assert_equals(e.navigationType, "traverse");
+ assert_equals(navigation.currentEntry.index, 1);
+ assert_false(forward_committed);
+ });
+ let forward_result = navigation.forward();
+ assert_false(oncurrententrychange_forward_called);
+ await forward_result.committed.then(() => forward_committed = true);
+ assert_true(oncurrententrychange_forward_called);
+}, "currententrychange fires for same-document navigation.back() and navigation.forward()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-cross-doc.html b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-cross-doc.html
new file mode 100644
index 0000000000..81a4e239ba
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-cross-doc.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+ i.contentWindow.navigation.oncurrententrychange = t.unreached_func("currententrychange should not fire for cross-document navigations");
+ i.contentWindow.navigation.navigate("/common/blank.html?1");
+ await new Promise(resolve => i.onload = resolve);
+}, "currententrychange does not fire for cross-document navigation.navigate()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-intercept.html b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-intercept.html
new file mode 100644
index 0000000000..af0fe104f9
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-intercept.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ let oncurrententrychange_called = false;
+ i.contentWindow.navigation.oncurrententrychange = t.step_func(e => {
+ oncurrententrychange_called = true;
+ assert_equals(e.from, i.contentWindow.navigation.entries()[0]);
+ assert_equals(e.navigationType, "push");
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+ });
+ i.contentWindow.navigation.onnavigate = e => e.intercept();
+ let result = i.contentWindow.navigation.navigate("/common/blank.html?1");
+ assert_true(oncurrententrychange_called);
+ await result.committed;
+}, "currententrychange fires for navigation.navigate() intercepted by intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-preventDefault.html b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-preventDefault.html
new file mode 100644
index 0000000000..34b98353ba
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-preventDefault.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(async t => {
+ navigation.oncurrententrychange = t.unreached_func("currententrychange should not fire");
+ navigation.onnavigate = e => e.preventDefault();
+ await promise_rejects_dom(t, "AbortError", navigation.navigate("#foo").committed);
+}, "currententrychange does not fire when onnavigate preventDefault() is called");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-replace-cross-doc.html b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-replace-cross-doc.html
new file mode 100644
index 0000000000..ab762c04bc
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-replace-cross-doc.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+ i.contentWindow.navigation.oncurrententrychange = t.unreached_func("currententrychange should not fire for cross-document navigations");
+ i.contentWindow.navigation.navigate("/common/blank.html?1", { history: "replace" });
+ await new Promise(resolve => i.onload = resolve);
+}, "currententrychange does not fire for cross-document navigation.navigate() with replace");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-replace-intercept.html b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-replace-intercept.html
new file mode 100644
index 0000000000..33209202d8
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-replace-intercept.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ let oncurrententrychange_called = false;
+ let original_entry = i.contentWindow.navigation.currentEntry;
+ i.contentWindow.navigation.oncurrententrychange = t.step_func(e => {
+ oncurrententrychange_called = true;
+ assert_equals(e.from, original_entry);
+ assert_equals(e.from.index, -1);
+ assert_equals(e.navigationType, "replace");
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 0);
+ });
+ i.contentWindow.navigation.onnavigate = e => e.intercept();
+ let result = i.contentWindow.navigation.navigate("/common/blank.html?1", { history: "replace" });
+ assert_true(oncurrententrychange_called);
+ await result.committed;
+}, "currententrychange fires for navigation.navigate() with replace intercepted by intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-replace-same-doc.html b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-replace-same-doc.html
new file mode 100644
index 0000000000..f993597305
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-replace-same-doc.html
@@ -0,0 +1,23 @@
+<!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));
+
+ let oncurrententrychange_called = false;
+ let original_entry = navigation.currentEntry;
+ navigation.oncurrententrychange = t.step_func(e => {
+ oncurrententrychange_called = true;
+ assert_equals(e.from, original_entry);
+ assert_equals(e.from.index, -1);
+ assert_equals(e.navigationType, "replace");
+ assert_equals(navigation.currentEntry.index, 0);
+ });
+ let result = navigation.navigate("#foo", { history: "replace" });
+ assert_true(oncurrententrychange_called);
+ await result.committed;
+}, "currententrychange fires for navigation.navigate() with replace");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-same-doc.html b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-same-doc.html
new file mode 100644
index 0000000000..87fc28d174
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-navigate-same-doc.html
@@ -0,0 +1,21 @@
+<!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));
+
+ let oncurrententrychange_called = false;
+ navigation.oncurrententrychange = t.step_func(e => {
+ oncurrententrychange_called = true;
+ assert_equals(e.from, navigation.entries()[0]);
+ assert_equals(e.navigationType, "push");
+ assert_equals(navigation.currentEntry.index, 1);
+ });
+ let result = navigation.navigate("#foo");
+ assert_true(oncurrententrychange_called);
+ await result.committed;
+}, "currententrychange fires for navigation.navigate()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-reload-cross-doc.html b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-reload-cross-doc.html
new file mode 100644
index 0000000000..e590cab382
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-reload-cross-doc.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+ i.contentWindow.navigation.oncurrententrychange = t.unreached_func("currententrychange should not fire for cross-document navigations");
+ i.contentWindow.navigation.reload();
+ await new Promise(resolve => i.onload = resolve);
+}, "currententrychange does not fire for cross-document navigation.reload()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-reload-intercept.html b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-reload-intercept.html
new file mode 100644
index 0000000000..275e23363c
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-reload-intercept.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ let oncurrententrychange_called = false;
+ i.contentWindow.navigation.oncurrententrychange = t.step_func(e => {
+ oncurrententrychange_called = true;
+ assert_equals(e.from, i.contentWindow.navigation.currentEntry);
+ assert_equals(e.navigationType, "reload");
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 0);
+ });
+ i.contentWindow.navigation.onnavigate = e => e.intercept();
+ let result = i.contentWindow.navigation.reload();
+ assert_true(oncurrententrychange_called);
+ await result.committed;
+}, "currententrychange fires for navigation.reload() intercepted by intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-updateCurrentEntry.html b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-updateCurrentEntry.html
new file mode 100644
index 0000000000..4423b2bc90
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/navigation-updateCurrentEntry.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(t => {
+ let oncurrententrychange_count = 0;
+ navigation.oncurrententrychange = t.step_func(e => {
+ oncurrententrychange_count++;
+ assert_equals(e.from, navigation.currentEntry);
+ assert_equals(e.navigationType, null);
+ assert_equals(navigation.currentEntry.getState(), "newState");
+ });
+ navigation.updateCurrentEntry({ state: "newState" });
+ assert_equals(oncurrententrychange_count, 1);
+
+ // "Updating" the state to the current state should still fire currententrychange.
+ navigation.updateCurrentEntry({ state: navigation.currentEntry.getState() });
+ assert_equals(oncurrententrychange_count, 2);
+}, "currententrychange fires for navigation.updateCurrentEntry()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/not-on-load.html b/testing/web-platform/tests/navigation-api/currententrychange-event/not-on-load.html
new file mode 100644
index 0000000000..8cc1688913
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/not-on-load.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<!-- Per https://github.com/WICG/navigation-api/issues/31, make sure nothing happens on page loads -->
+
+<script>
+async_test(t => {
+ navigation.onnavigate = t.unreached_func("navigate must not fire");
+ navigation.oncurrententrychange = t.unreached_func("currententrychange must not fire");
+
+ // pageshow is the latest event in the normal document loading cycle.
+ // Ensure nothing happens even 10 ms afterward.
+ window.addEventListener("pageshow", () => t.step_timeout(() => {
+ t.done();
+ }, 10));
+}, "No navigation API events happen on initial page load");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/currententrychange-event/properties.html b/testing/web-platform/tests/navigation-api/currententrychange-event/properties.html
new file mode 100644
index 0000000000..e862543bcc
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/currententrychange-event/properties.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ navigation.oncurrententrychange = t.step_func_done(e => {
+ assert_equals(e.constructor, NavigationCurrentEntryChangeEvent);
+ assert_false(e.bubbles);
+ assert_false(e.cancelable);
+ assert_true(e.isTrusted);
+ });
+ location.href = "#1";
+}, "NavigationCurrentEntryChangeEvent's properties");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/focus-reset/autofocus.html b/testing/web-platform/tests/navigation-api/focus-reset/autofocus.html
new file mode 100644
index 0000000000..6044447367
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/focus-reset/autofocus.html
@@ -0,0 +1,185 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<button autofocus id="initialAutofocusTarget">Initial autofocus target</button>
+
+<script type="module">
+promise_setup(async () => {
+ // Get the overall autofocus processed flag to flip to true, so that
+ // we only test the navigation API-specific stuff.
+ await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
+ assert_equals(document.activeElement, initialAutofocusTarget, "Non-navigation API autofocus was processed");
+ initialAutofocusTarget.remove();
+ assert_equals(document.activeElement, document.body);
+});
+
+promise_test(async t => {
+ const decoy = createAndAppend(t);
+ const autofocusTarget = createAndAppend(t, { autofocus: true });
+
+ assert_equals(document.activeElement, document.body, "Start on body");
+ decoy.focus();
+ assert_equals(document.activeElement, decoy, "focus() worked");
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept();
+ }, { once: true });
+
+ const { committed, finished } = navigation.navigate("#1");
+
+ await committed;
+ assert_equals(document.activeElement, decoy, "Focus stays on the non-autofocused button during the transition");
+
+ await finished;
+ assert_equals(document.activeElement, autofocusTarget, "Focus moves to the autofocused button after the transition");
+}, "An element with autofocus, present before navigation, gets focused");
+
+promise_test(async t => {
+ const autofocusTarget = createAndAppend(t, { autofocus: true });
+ const decoy = createAndAppend(t, { autofocus: true });
+
+ assert_equals(document.activeElement, document.body, "Start on body");
+ decoy.focus();
+ assert_equals(document.activeElement, decoy, "focus() worked");
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept();
+ }, { once: true });
+
+ const { committed, finished } = navigation.navigate("#1");
+
+ await committed;
+ assert_equals(document.activeElement, decoy, "Focus stays on the initially-focused button during the transition");
+
+ await finished;
+ assert_equals(document.activeElement, autofocusTarget, "Focus moves to the first autofocused button after the transition");
+}, "Two elements with autofocus, present before navigation; the first gets focused");
+
+promise_test(async t => {
+ const decoy = createAndAppend(t);
+ const autofocusTarget = createAndAppend(t, { autofocus: true });
+
+ assert_equals(document.activeElement, document.body, "Start on body");
+ decoy.focus();
+ assert_equals(document.activeElement, decoy, "focus() worked");
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept();
+ }, { once: true });
+
+ const { committed, finished } = navigation.navigate("#1");
+
+ await committed;
+ assert_equals(document.activeElement, decoy, "Focus stays on the non-autofocused button during the transition");
+
+ autofocusTarget.disabled = true;
+
+ await finished;
+ assert_equals(document.activeElement, document.body, "Focus gets reset after the transition");
+}, "An element with autofocus, present before navigation but disabled before finished, does not get focused");
+
+promise_test(async t => {
+ const decoy = createAndAppend(t);
+ const autofocusTarget = createAndAppend(t, { autofocus: true });
+
+ assert_equals(document.activeElement, document.body, "Start on body");
+ decoy.focus();
+ assert_equals(document.activeElement, decoy, "focus() worked");
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept();
+ }, { once: true });
+
+ const { committed, finished } = navigation.navigate("#1");
+
+ await committed;
+ assert_equals(document.activeElement, decoy, "Focus stays on the non-autofocused button during the transition");
+
+ autofocusTarget.autofocus = false;
+
+ await finished;
+ assert_equals(document.activeElement, document.body, "Focus gets reset after the transition");
+}, "An element with autofocus, present before navigation but with its autofocus attribute removed before finished, does not get focused");
+
+promise_test(async t => {
+ const decoy = createAndAppend(t, { autofocus: true });
+ const autofocusTarget = createAndAppend(t, { autofocus: true });
+
+ assert_equals(document.activeElement, document.body, "Start on body");
+ decoy.focus();
+ assert_equals(document.activeElement, decoy, "focus() worked");
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept();
+ }, { once: true });
+
+ const { committed, finished } = navigation.navigate("#1");
+
+ await committed;
+ assert_equals(document.activeElement, decoy, "Focus stays on the initially-focused button during the transition");
+
+ decoy.disabled = true;
+ assert_equals(document.activeElement, document.body, "Disabling the initially-focused button temporarily resets focus to the body");
+
+ await finished;
+ assert_equals(document.activeElement, autofocusTarget, "Focus moves to the second autofocused button after the transition");
+}, "Two elements with autofocus, present before navigation, but the first gets disabled; the second gets focused");
+
+promise_test(async t => {
+ const decoy = createAndAppend(t);
+
+ assert_equals(document.activeElement, document.body, "Start on body");
+ decoy.focus();
+ assert_equals(document.activeElement, decoy, "focus() worked");
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept();
+ }, { once: true });
+
+ const { committed, finished } = navigation.navigate("#1");
+
+ await committed;
+ assert_equals(document.activeElement, decoy, "Focus stays on the non-autofocused button during the transition");
+
+ const autofocusTarget = createAndAppend(t, { autofocus: true });
+
+ await finished;
+ assert_equals(document.activeElement, autofocusTarget, "Focus moves to the autofocused button after the transition");
+}, "An element with autofocus, introduced between committed and finished, gets focused");
+
+promise_test(async t => {
+ const decoy = createAndAppend(t);
+
+ assert_equals(document.activeElement, document.body, "Start on body");
+ decoy.focus();
+ assert_equals(document.activeElement, decoy, "focus() worked");
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept();
+ }, { once: true });
+
+ const { committed, finished } = navigation.navigate("#1");
+
+ await committed;
+ assert_equals(document.activeElement, decoy, "Focus stays on the non-autofocused button during the transition");
+
+ await finished;
+ assert_equals(document.activeElement, document.body, "Focus gets reset after the transition");
+
+ const autofocusTarget = createAndAppend(t, { autofocus: true });
+
+ await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
+ assert_equals(document.activeElement, document.body, "Focus stays reset two animation frames after the transition");
+}, "An element with autofocus, introduced after finished, does not get focused");
+
+function createAndAppend(t, props) {
+ const element = document.createElement("button");
+ Object.assign(element, props);
+
+ document.body.append(element);
+ t.add_cleanup(() => { element.remove(); });
+
+ return element;
+}
+</script>
diff --git a/testing/web-platform/tests/navigation-api/focus-reset/basic.html b/testing/web-platform/tests/navigation-api/focus-reset/basic.html
new file mode 100644
index 0000000000..f5a30972b0
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/focus-reset/basic.html
@@ -0,0 +1,63 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<script type="module">
+import { testFocusWasReset, testFocusWasNotReset } from "./resources/helpers.mjs";
+
+test(() => {
+ let throwAssertionHappened = false;
+
+ navigation.addEventListener("navigate", e => {
+ assert_throws_js(TypeError, () => {
+ e.intercept({ focusReset: "invalid" });
+ });
+ throwAssertionHappened = true;
+ }, { once: true });
+
+ navigation.navigate("#1");
+ assert_true(throwAssertionHappened);
+}, "Invalid values for focusReset throw");
+
+testFocusWasNotReset(() => {
+ // Intentionally left blank.
+}, "Does not reset the focus when no navigate handler is present");
+
+testFocusWasReset(t => {
+ navigation.addEventListener("navigate", e => {
+ e.intercept();
+ }, { once: true });
+}, "Resets the focus when no focusReset option is provided");
+
+testFocusWasReset(t => {
+ navigation.addEventListener("navigate", e => {
+ e.intercept();
+ }, { once: true });
+}, "Resets the focus when focusReset is explicitly set to undefined");
+
+testFocusWasReset(t => {
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler: () => new Promise(r => t.step_timeout(r, 5)) });
+ }, { once: true });
+}, "Resets the focus when no focusReset option is provided (nontrivial fulfilled promise)");
+
+testFocusWasReset(t => {
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler: () => Promise.reject() });
+ }, { once: true });
+}, "Resets the focus when no focusReset option is provided (rejected promise)");
+
+testFocusWasReset(t => {
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ focusReset: "after-transition" });
+ }, { once: true });
+}, "Resets the focus when focusReset is explicitly set to 'after-transition'");
+
+testFocusWasNotReset(t => {
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ focusReset: "manual" });
+ });
+}, "Does not reset the focus when focusReset is set to 'manual'");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/focus-reset/change-focus-again-in-blur-during-intercept.html b/testing/web-platform/tests/navigation-api/focus-reset/change-focus-again-in-blur-during-intercept.html
new file mode 100644
index 0000000000..a7339c9788
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/focus-reset/change-focus-again-in-blur-during-intercept.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+promise_test(async t => {
+ let intercept_resolve;
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler: () => new Promise(resolve => intercept_resolve = resolve),
+ focusReset: "after-transition" });
+ }, { once: true });
+
+ const button = document.body.appendChild(document.createElement("button"));
+ const button2 = document.body.appendChild(document.createElement("button"));
+ button2.tabIndex = 0;
+ t.add_cleanup(() => {
+ button.remove();
+ button2.remove();
+ });
+
+ assert_equals(document.activeElement, document.body, "Start on body");
+ button.focus();
+ assert_equals(document.activeElement, button, "focus() worked");
+
+ const finished = navigation.navigate("#1").finished;
+ button.onblur = () => button2.focus();
+ button.blur();
+ assert_equals(document.activeElement, button2, "focus() in blur worked");
+
+ intercept_resolve();
+ await finished;
+ assert_equals(document.activeElement, button2, "Focus was not reset after the transition");
+}, "");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/focus-reset/change-focus-back-to-origial-during-intercept.html b/testing/web-platform/tests/navigation-api/focus-reset/change-focus-back-to-origial-during-intercept.html
new file mode 100644
index 0000000000..4e5b9dfb6a
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/focus-reset/change-focus-back-to-origial-during-intercept.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+promise_test(async t => {
+ let intercept_resolve;
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler: () => new Promise(resolve => intercept_resolve = resolve),
+ focusReset: "after-transition" });
+ }, { once: true });
+
+ const button = document.body.appendChild(document.createElement("button"));
+ const button2 = document.body.appendChild(document.createElement("button"));
+ button2.tabIndex = 0;
+ t.add_cleanup(() => {
+ button.remove();
+ button2.remove();
+ });
+
+ assert_equals(document.activeElement, document.body, "Start on body");
+ button.focus();
+ assert_equals(document.activeElement, button, "focus() worked");
+
+ const finished = navigation.navigate("#1").finished;
+ button2.focus();
+ assert_equals(document.activeElement, button2, "focus() worked");
+ button.focus();
+ assert_equals(document.activeElement, button, "focus() worked");
+
+ intercept_resolve();
+ await finished;
+ assert_equals(document.activeElement, button, "Focus was not reset after the transition");
+}, "");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/focus-reset/change-focus-during-intercept.html b/testing/web-platform/tests/navigation-api/focus-reset/change-focus-during-intercept.html
new file mode 100644
index 0000000000..0593231a39
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/focus-reset/change-focus-during-intercept.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+promise_test(async t => {
+ let intercept_resolve;
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler: () => new Promise(resolve => intercept_resolve = resolve),
+ focusReset: "after-transition" });
+ }, { once: true });
+
+ const button = document.body.appendChild(document.createElement("button"));
+ const button2 = document.body.appendChild(document.createElement("button"));
+ button2.tabIndex = 0;
+ t.add_cleanup(() => {
+ button.remove();
+ button2.remove();
+ });
+
+ assert_equals(document.activeElement, document.body, "Start on body");
+ button.focus();
+ assert_equals(document.activeElement, button, "focus() worked");
+
+ const finished = navigation.navigate("#1").finished;
+ button2.focus();
+ assert_equals(document.activeElement, button2, "focus() worked");
+
+ intercept_resolve();
+ await finished;
+ assert_equals(document.activeElement, button2, "Focus was not reset after the transition");
+}, "");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/focus-reset/change-focus-then-remove-during-intercept.html b/testing/web-platform/tests/navigation-api/focus-reset/change-focus-then-remove-during-intercept.html
new file mode 100644
index 0000000000..a5d8062ce0
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/focus-reset/change-focus-then-remove-during-intercept.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+promise_test(async t => {
+ let intercept_resolve;
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler: () => new Promise(resolve => intercept_resolve = resolve),
+ focusReset: "after-transition" });
+ }, { once: true });
+
+ const button = document.body.appendChild(document.createElement("button"));
+ const button2 = document.body.appendChild(document.createElement("button"));
+ button2.tabIndex = 0;
+ t.add_cleanup(() => {
+ button.remove();
+ button2.remove();
+ });
+
+ assert_equals(document.activeElement, document.body, "Start on body");
+ button.focus();
+ assert_equals(document.activeElement, button, "focus() worked");
+
+ const finished = navigation.navigate("#1").finished;
+
+ let onfocus_called = false;
+ document.body.onfocus = onfocus_called = true;
+ button.remove();
+ assert_equals(document.activeElement, document.body, "Removing the element reset focus");
+ assert_true(onfocus_called);
+
+ document.body.onfocus = t.unreached_func("onfocus shouldn't fire a second time due to focus reset");
+ intercept_resolve();
+ await finished;
+ assert_equals(document.activeElement, document.body, "Focus remains on document.body after promise fulfills");
+ await new Promise(resolve => t.step_timeout(resolve, 10));
+}, "");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/focus-reset/focus-reset-timing.html b/testing/web-platform/tests/navigation-api/focus-reset/focus-reset-timing.html
new file mode 100644
index 0000000000..df9f03afe9
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/focus-reset/focus-reset-timing.html
@@ -0,0 +1,61 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+promise_test(async t => {
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ focusReset: "after-transition" });
+ }, { once: true });
+
+ const button = document.body.appendChild(document.createElement("button"));
+ const button2 = document.body.appendChild(document.createElement("button"));
+ button2.tabIndex = 0;
+ t.add_cleanup(() => {
+ button.remove();
+ button2.remove();
+ });
+
+ assert_equals(document.activeElement, document.body, "Start on body");
+ button.focus();
+ assert_equals(document.activeElement, button, "focus() worked");
+
+ let navigatesuccess_called = false;
+ navigation.onnavigatesuccess = t.step_func(() => {
+ navigatesuccess_called = true;
+ assert_equals(document.activeElement, document.body, "Focus must be reset before navigatesuccess");
+ });
+
+ await navigation.navigate("#1").finished;
+ assert_true(navigatesuccess_called);
+}, "Focus should be reset before navigatesuccess");
+
+promise_test(async t => {
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler: () => Promise.reject(),
+ focusReset: "after-transition" });
+ }, { once: true });
+
+ const button = document.body.appendChild(document.createElement("button"));
+ const button2 = document.body.appendChild(document.createElement("button"));
+ button2.tabIndex = 0;
+ t.add_cleanup(() => {
+ button.remove();
+ button2.remove();
+ });
+
+ assert_equals(document.activeElement, document.body, "Start on body");
+ button.focus();
+ assert_equals(document.activeElement, button, "focus() worked");
+
+ let navigateerror_called = false;
+ navigation.onnavigateerror = t.step_func(() => {
+ navigateerror_called = true;
+ assert_equals(document.activeElement, document.body, "Focus must be reset before navigateerror");
+ });
+
+ await promise_rejects_exactly(t, undefined, navigation.navigate("#2").finished);
+ assert_true(navigateerror_called);
+}, "Focus should be reset before navigateerror");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/focus-reset/multiple-intercept.html b/testing/web-platform/tests/navigation-api/focus-reset/multiple-intercept.html
new file mode 100644
index 0000000000..75e38c98a4
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/focus-reset/multiple-intercept.html
@@ -0,0 +1,69 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<script type="module">
+import { testFocusWasReset, testFocusWasNotReset } from "./resources/helpers.mjs";
+
+testFocusWasReset(t => {
+ navigation.addEventListener("navigate", e => {
+ e.intercept();
+ }, { once: true });
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ focusReset: "after-transition" });
+ }, { once: true });
+}, "(not provided) + after-transition");
+
+testFocusWasNotReset(t => {
+ navigation.addEventListener("navigate", e => {
+ e.intercept();
+ }, { once: true });
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ focusReset: "manual" });
+ }, { once: true });
+}, "(not provided) + manual");
+
+testFocusWasNotReset(t => {
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ focusReset: "after-transition" });
+ }, { once: true });
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ focusReset: "manual" });
+ }, { once: true });
+}, "after-transition + manual");
+
+testFocusWasReset(t => {
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ focusReset: "after-transition" });
+ }, { once: true });
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept();
+ }, { once: true });
+}, "after-transition + (not provided)");
+
+testFocusWasReset(t => {
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ focusReset: "manual" });
+ }, { once: true });
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ focusReset: "after-transition" });
+ }, { once: true });
+}, "manual + after-transition");
+
+testFocusWasNotReset(t => {
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ focusReset: "manual" });
+ }, { once: true });
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept();
+ }, { once: true });
+}, "manual + (not provided)");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/focus-reset/resources/helpers.mjs b/testing/web-platform/tests/navigation-api/focus-reset/resources/helpers.mjs
new file mode 100644
index 0000000000..0a8a0439e1
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/focus-reset/resources/helpers.mjs
@@ -0,0 +1,73 @@
+// Usage note: if you use these more than once in a given file, be sure to
+// clean up any navigate event listeners, e.g. by using { once: true }, between
+// tests.
+
+const TAB_KEY = "\uE004";
+
+export function testFocusWasReset(setupFunc, description) {
+ promise_test(async t => {
+ setupFunc(t);
+
+ const button = document.body.appendChild(document.createElement("button"));
+ const button2 = document.body.appendChild(document.createElement("button"));
+ button2.tabIndex = 0;
+ t.add_cleanup(() => {
+ button.remove();
+ button2.remove();
+ });
+
+ assert_equals(document.activeElement, document.body, "Start on body");
+ button.focus();
+ assert_equals(document.activeElement, button, "focus() worked");
+
+ const { committed, finished } = navigation.navigate("#" + location.hash.substring(1) + "1");
+
+ await committed;
+ assert_equals(document.activeElement, button, "Focus stays on the button during the transition");
+
+ await finished.catch(() => {});
+ assert_equals(document.activeElement, document.body, "Focus reset after the transition");
+
+ button2.onfocus = t.unreached_func("button2 must not be focused after pressing Tab");
+ const focusPromise = waitForFocus(t, button);
+ await test_driver.send_keys(document.body, TAB_KEY);
+ await focusPromise;
+ }, description);
+}
+
+export function testFocusWasNotReset(setupFunc, description) {
+ promise_test(async t => {
+ setupFunc(t);
+
+ const button = document.body.appendChild(document.createElement("button"));
+ const button2 = document.body.appendChild(document.createElement("button"));
+ button2.tabIndex = 0;
+ t.add_cleanup(() => {
+ button.remove();
+ button2.remove();
+ });
+
+ assert_equals(document.activeElement, document.body, "Start on body");
+ button.focus();
+ assert_equals(document.activeElement, button, "focus() worked");
+
+ const { committed, finished } = navigation.navigate("#" + location.hash.substring(1) + "1");
+
+ await committed;
+ assert_equals(document.activeElement, button, "Focus stays on the button during the transition");
+
+ await finished.catch(() => {});
+ assert_equals(document.activeElement, button, "Focus stays on the button after the transition");
+
+ button.onfocus = t.unreached_func("button must not be focused after pressing Tab");
+ const focusPromise = waitForFocus(t, button2);
+ await test_driver.send_keys(document.body, TAB_KEY);
+ await focusPromise;
+ }, description);
+}
+
+function waitForFocus(t, target) {
+ return new Promise(resolve => {
+ target.addEventListener("focus", () => resolve(), { once: true });
+ });
+}
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-origin-traversal-does-not-fire-navigate.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-origin-traversal-does-not-fire-navigate.html
new file mode 100644
index 0000000000..36491be22c
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-origin-traversal-does-not-fire-navigate.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../navigation-methods/return-value/resources/helpers.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+promise_test(async t => {
+ // Open a cross-origin window.
+ let w = window.open(get_host_info().HTTP_REMOTE_ORIGIN + "/navigation-api/navigate-event/resources/opener-postMessage-onload.html");
+ await new Promise(resolve => window.onmessage = resolve);
+
+ // Navigate the opened window to this origin.
+ w.location = get_host_info().ORIGIN + "/navigation-api/navigate-event/resources/opener-postMessage-onload.html";
+ await new Promise(resolve => window.onmessage = resolve);
+ assert_equals(w.navigation.entries().length, 1);
+ assert_equals(w.navigation.currentEntry.index, 0);
+
+ // Go back. This should not fire a navigate event, because the traversal is
+ // cross-origin. Note that history.back() must be used instead of
+ // navigation.back() because navigation.back() can never go cross-origin.
+ w.navigation.onnavigate = t.unreached_func("navigate should not fire");
+ w.history.back();
+ await new Promise(resolve => window.onmessage = resolve);
+}, "A cross-origin traversal does not fire the navigate event");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-crossdocument-crossorigin-sameorigindomain.sub.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-crossdocument-crossorigin-sameorigindomain.sub.html
new file mode 100644
index 0000000000..004e1a75e3
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-crossdocument-crossorigin-sameorigindomain.sub.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+
+<script>
+document.domain = "{{host}}";
+async_test(t => {
+ const url = new URL("resources/document-domain-setter.sub.html", location.href);
+ url.hostname = "{{domains[www1]}}";
+ const iframe = document.createElement("iframe");
+ iframe.name = "windowname";
+ iframe.src = url;
+ document.body.append(iframe);
+
+ url.search = "?foo";
+ const link = document.createElement("a");
+ link.href = url;
+ link.target = iframe.name;
+ document.body.append(link);
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => {
+ iframe.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push", "navigationType");
+ assert_true(e.cancelable, "cancelable");
+ assert_true(e.canIntercept, "canIntercept");
+ assert_false(e.userInitiated, "userInitiated");
+ assert_false(e.hashChange, "hashChange");
+ assert_equals(e.formData, null, "formData");
+ assert_equals(e.destination.url, link.href, "destination.url");
+ assert_false(e.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.destination.key, "", "destination.key");
+ assert_equals(e.destination.id, "", "destination.id");
+ assert_equals(e.destination.index, -1, "destination.index");
+ });
+
+ link.click();
+ });
+}, "clicking on an <a> element that navigates cross-document targeting a same-origin-domain (but cross-origin) window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-crossdocument-crossorigin.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-crossdocument-crossorigin.html
new file mode 100644
index 0000000000..2f40238912
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-crossdocument-crossorigin.html
@@ -0,0 +1,30 @@
+<!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 url = new URL("resources/cross-origin-iframe-helper.html", location.href);
+ url.hostname = get_host_info().REMOTE_HOST;
+ const iframe = document.createElement("iframe");
+ iframe.src = url;
+ iframe.name = "windowname";
+ document.body.append(iframe);
+
+ url.search = "?postMessage-top-when-done";
+ const link = document.createElement("a");
+ link.href = url;
+ link.target = iframe.name;
+ document.body.append(link);
+
+ window.onmessage = t.step_func_done(e => {
+ // If we hit onnavigate in the target window, we'll get a different message, and fail.
+ assert_equals(e.data, "DONE");
+ });
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => link.click());
+}, "clicking on an <a> element that navigates cross-document targeting a cross-origin window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-crossdocument-sameorigin.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-crossdocument-sameorigin.html
new file mode 100644
index 0000000000..683875d006
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-crossdocument-sameorigin.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<a id="link" href="/common/blank.html?foo" target="windowname">Click me</a>
+<iframe id="i" name="windowname" src="/common/blank.html"></iframe>
+
+<script>
+async_test(t => {
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => {
+ i.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push", "navigationType");
+ assert_true(e.cancelable, "cancelable");
+ assert_true(e.canIntercept, "canIntercept");
+ assert_false(e.userInitiated, "userInitiated");
+ assert_false(e.hashChange, "hashChange");
+ assert_equals(e.formData, null, "formData");
+ assert_equals(e.destination.url, link.href, "destination.url");
+ assert_false(e.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.destination.key, "", "destination.key");
+ assert_equals(e.destination.id, "", "destination.id");
+ assert_equals(e.destination.index, -1, "destination.index");
+ });
+
+ link.click();
+ });
+}, "clicking on an <a> element that navigates cross-document targeting a same-origin window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-samedocument-crossorigin-sameorigindomain.sub.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-samedocument-crossorigin-sameorigindomain.sub.html
new file mode 100644
index 0000000000..60d1653a3a
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-samedocument-crossorigin-sameorigindomain.sub.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+
+<script>
+document.domain = "{{host}}";
+async_test(t => {
+ const url = new URL("resources/document-domain-setter.sub.html", location.href);
+ url.hostname = "{{domains[www1]}}";
+ const iframe = document.createElement("iframe");
+ iframe.name = "windowname";
+ iframe.src = url;
+ document.body.append(iframe);
+
+ url.hash = "#foo";
+ const link = document.createElement("a");
+ link.href = url;
+ link.target = iframe.name;
+ document.body.append(link);
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => {
+ iframe.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push", "navigationType");
+ assert_true(e.cancelable, "cancelable");
+ assert_true(e.canIntercept, "canIntercept");
+ assert_false(e.userInitiated, "userInitiated");
+ assert_true(e.hashChange, "hashChange");
+ assert_equals(e.formData, null, "formData");
+ assert_equals(e.destination.url, link.href, "destination.url");
+ assert_true(e.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.destination.key, "", "destination.key");
+ assert_equals(e.destination.id, "", "destination.id");
+ assert_equals(e.destination.index, -1, "destination.index");
+ });
+
+ link.click();
+ });
+}, "clicking on an <a> element that navigates same-document targeting a same-origin-domain (but cross-origin) window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-samedocument-crossorigin.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-samedocument-crossorigin.html
new file mode 100644
index 0000000000..ee01e7f1f2
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-samedocument-crossorigin.html
@@ -0,0 +1,39 @@
+<!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 url = new URL("resources/cross-origin-iframe-helper.html", location.href);
+ url.hostname = get_host_info().REMOTE_HOST;
+ const iframe = document.createElement("iframe")
+ iframe.src = url;
+ iframe.name = "windowname";
+ document.body.append(iframe);
+
+ url.hash = "#foo";
+ const link = document.createElement("a");
+ link.href = url;
+ link.target = iframe.name;
+ document.body.append(link);
+
+ window.onmessage = t.step_func_done(e => {
+ assert_equals(e.data.navigationType, "push", "navigationType");
+ assert_true(e.data.cancelable, "cancelable");
+ assert_true(e.data.canIntercept, "canIntercept");
+ assert_false(e.data.userInitiated, "userInitiated");
+ assert_true(e.data.hashChange, "hashChange");
+ assert_equals(e.data.formData, null, "formData");
+ assert_equals(e.data.destination.url, link.href, "destination.url");
+ assert_true(e.data.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.data.destination.key, "", "destination.key");
+ assert_equals(e.data.destination.id, "", "destination.id");
+ assert_equals(e.data.destination.index, -1, "destination.index");
+ });
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => link.click());
+}, "clicking on an <a> element that navigates same-document targeting a cross-origin window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-samedocument-sameorigin.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-samedocument-sameorigin.html
new file mode 100644
index 0000000000..eaa7b52345
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/click-samedocument-sameorigin.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<a id="link" href="/common/blank.html#foo" target="windowname">Click me</a>
+<iframe id="i" name="windowname" src="/common/blank.html"></iframe>
+
+<script>
+async_test(t => {
+ i.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push", "navigationType");
+ assert_true(e.cancelable, "cancelable");
+ assert_true(e.canIntercept, "canIntercept");
+ assert_false(e.userInitiated, "userInitiated");
+ assert_true(e.hashChange, "hashChange");
+ assert_equals(e.formData, null, "formData");
+ assert_equals(e.destination.url, link.href, "destination.url");
+ assert_true(e.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.destination.key, "", "destination.key");
+ assert_equals(e.destination.id, "", "destination.id");
+ assert_equals(e.destination.index, -1, "destination.index");
+ });
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => link.click());
+}, "clicking on an <a> element that navigates same-document targeting a same-origin window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-crossdocument-crossorigin-sameorigindomain.sub.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-crossdocument-crossorigin-sameorigindomain.sub.html
new file mode 100644
index 0000000000..cee7d95b9e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-crossdocument-crossorigin-sameorigindomain.sub.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+
+<script>
+document.domain = "{{host}}";
+async_test(t => {
+ const url = new URL("resources/document-domain-setter.sub.html", location.href);
+ url.hostname = "{{domains[www1]}}";
+ const iframe = document.createElement("iframe");
+ iframe.src = url;
+ document.body.append(iframe);
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => {
+ iframe.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push", "navigationType");
+ assert_true(e.cancelable, "cancelable");
+ assert_true(e.canIntercept, "canIntercept");
+ assert_false(e.userInitiated, "userInitiated");
+ assert_false(e.hashChange, "hashChange");
+ assert_equals(e.formData, null, "formData");
+ assert_equals(e.destination.url, iframe.src + "?foo", "destination.url");
+ assert_false(e.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.destination.key, "", "destination.key");
+ assert_equals(e.destination.id, "", "destination.id");
+ assert_equals(e.destination.index, -1, "destination.index");
+ });
+
+ iframe.contentWindow.location.href = url + "?foo";
+ });
+}, "using location.href to navigate cross-document targeting a same-origin-domain (but cross-origin) window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-crossdocument-crossorigin.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-crossdocument-crossorigin.html
new file mode 100644
index 0000000000..79df86fdc9
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-crossdocument-crossorigin.html
@@ -0,0 +1,23 @@
+<!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 iframeURL = new URL("resources/cross-origin-iframe-helper.html", location.href);
+ iframeURL.hostname = get_host_info().REMOTE_HOST;
+ const iframe = document.createElement("iframe");
+ iframe.src = iframeURL;
+ document.body.append(iframe);
+
+ window.onmessage = t.step_func_done(e => {
+ // If we hit onnavigate in the target window, we'll get a different message, and fail.
+ assert_equals(e.data, "DONE");
+ });
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => iframe.contentWindow.location.href = iframeURL + "?postMessage-top-when-done");
+}, "using location.href to navigate cross-document targeting a cross-origin window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-crossdocument-sameorigin.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-crossdocument-sameorigin.html
new file mode 100644
index 0000000000..d0dad46b72
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-crossdocument-sameorigin.html
@@ -0,0 +1,26 @@
+<!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 => {
+ i.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push", "navigationType");
+ assert_true(e.cancelable, "cancelable");
+ assert_true(e.canIntercept, "canIntercept");
+ assert_false(e.userInitiated, "userInitiated");
+ assert_false(e.hashChange, "hashChange");
+ assert_equals(e.formData, null, "formData");
+ assert_equals(e.destination.url, i.src + "?foo", "destination.url");
+ assert_false(e.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.destination.key, "", "destination.key");
+ assert_equals(e.destination.id, "", "destination.id");
+ assert_equals(e.destination.index, -1, "destination.index");
+ });
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => i.contentWindow.location.href = "/common/blank.html?foo");
+}, "using location.href to navigate cross-document targeting a same-origin window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-samedocument-crossorigin-sameorigindomain.sub.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-samedocument-crossorigin-sameorigindomain.sub.html
new file mode 100644
index 0000000000..156d1cb3cf
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-samedocument-crossorigin-sameorigindomain.sub.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+
+<script>
+document.domain = "{{host}}";
+async_test(t => {
+ const url = new URL("resources/document-domain-setter.sub.html", location.href);
+ url.hostname = "{{domains[www1]}}";
+ const iframe = document.createElement("iframe");
+ iframe.src = url;
+ document.body.append(iframe);
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => {
+ iframe.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push", "navigationType");
+ assert_true(e.cancelable, "cancelable");
+ assert_true(e.canIntercept, "canIntercept");
+ assert_false(e.userInitiated, "userInitiated");
+ assert_true(e.hashChange, "hashChange");
+ assert_equals(e.formData, null, "formData");
+ assert_equals(e.destination.url, iframe.src + "#foo", "destination.url");
+ assert_true(e.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.destination.key, "", "destination.key");
+ assert_equals(e.destination.id, "", "destination.id");
+ assert_equals(e.destination.index, -1, "destination.index");
+ });
+
+ iframe.contentWindow.location.href = url + "#foo";
+ });
+}, "using location.href to navigate same-document targeting a same-origin-domain (but cross-origin) window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-samedocument-crossorigin.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-samedocument-crossorigin.html
new file mode 100644
index 0000000000..6d73262dea
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-samedocument-crossorigin.html
@@ -0,0 +1,32 @@
+<!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 iframeURL = new URL("resources/cross-origin-iframe-helper.html", location.href);
+ iframeURL.hostname = get_host_info().REMOTE_HOST;
+ const iframe = document.createElement("iframe")
+ iframe.src = iframeURL;
+ document.body.append(iframe);
+
+ window.onmessage = t.step_func_done(e => {
+ assert_equals(e.data.navigationType, "push", "navigationType");
+ assert_true(e.data.cancelable, "cancelable");
+ assert_true(e.data.canIntercept, "canIntercept");
+ assert_false(e.data.userInitiated, "userInitiated");
+ assert_true(e.data.hashChange, "hashChange");
+ assert_equals(e.data.formData, null, "formData");
+ assert_equals(e.data.destination.url, iframe.src + "#foo", "destination.url");
+ assert_true(e.data.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.data.destination.key, "", "destination.key");
+ assert_equals(e.data.destination.id, "", "destination.id");
+ assert_equals(e.data.destination.index, -1, "destination.index");
+ });
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => iframe.contentWindow.location.href = iframeURL + "#foo");
+}, "using location.href to navigate same-document targeting a cross-origin window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-samedocument-sameorigin.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-samedocument-sameorigin.html
new file mode 100644
index 0000000000..6516a60239
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/location-samedocument-sameorigin.html
@@ -0,0 +1,26 @@
+<!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 => {
+ i.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push", "navigationType");
+ assert_true(e.cancelable, "cancelable");
+ assert_true(e.canIntercept, "canIntercept");
+ assert_false(e.userInitiated, "userInitiated");
+ assert_true(e.hashChange, "hashChange");
+ assert_equals(e.formData, null, "formData");
+ assert_equals(e.destination.url, i.src + "#foo", "destination.url");
+ assert_true(e.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.destination.key, "", "destination.key");
+ assert_equals(e.destination.id, "", "destination.id");
+ assert_equals(e.destination.index, -1, "destination.index");
+ });
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => i.contentWindow.location.href = "/common/blank.html#foo");
+}, "using location.href to navigate same-document targeting a same-origin window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-crossdocument-crossorigin-sameorigindomain.sub.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-crossdocument-crossorigin-sameorigindomain.sub.html
new file mode 100644
index 0000000000..1c411d9866
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-crossdocument-crossorigin-sameorigindomain.sub.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+
+<script>
+document.domain = "{{host}}";
+async_test(t => {
+ const url = new URL("resources/document-domain-setter.sub.html", location.href);
+ url.hostname = "{{domains[www1]}}";
+ const iframe = document.createElement("iframe");
+ iframe.name = "windowname";
+ iframe.src = url;
+ document.body.append(iframe);
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => {
+ iframe.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push", "navigationType");
+ assert_true(e.cancelable, "cancelable");
+ assert_true(e.canIntercept, "canIntercept");
+ assert_false(e.userInitiated, "userInitiated");
+ assert_false(e.hashChange, "hashChange");
+ assert_equals(e.formData, null, "formData");
+ assert_equals(e.destination.url, url + "?foo", "destination.url");
+ assert_false(e.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.destination.key, "", "destination.key");
+ assert_equals(e.destination.id, "", "destination.id");
+ assert_equals(e.destination.index, -1, "destination.index");
+ });
+
+ window.open(url + "?foo", iframe.name);
+ });
+}, "using window.open() to navigate cross-document targeting a same-origin-domain (but cross-origin) window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-crossdocument-crossorigin.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-crossdocument-crossorigin.html
new file mode 100644
index 0000000000..73ede89cbf
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-crossdocument-crossorigin.html
@@ -0,0 +1,24 @@
+<!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 iframeURL = new URL("resources/cross-origin-iframe-helper.html", location.href);
+ iframeURL.hostname = get_host_info().REMOTE_HOST;
+ const iframe = document.createElement("iframe");
+ iframe.src = iframeURL;
+ iframe.name = "windowname";
+ document.body.append(iframe);
+
+ window.onmessage = t.step_func_done(e => {
+ // If we hit onnavigate in the target window, we'll get a different message, and fail.
+ assert_equals(e.data, "DONE");
+ });
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => window.open(iframeURL + "?postMessage-top-when-done", "windowname"));
+}, "using window.open() to navigate cross-document targeting a cross-origin window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-crossdocument-sameorigin.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-crossdocument-sameorigin.html
new file mode 100644
index 0000000000..c91689341e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-crossdocument-sameorigin.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe id="i" name="windowname" src="/common/blank.html"></iframe>
+
+<script>
+async_test(t => {
+ i.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push", "navigationType");
+ assert_true(e.cancelable, "cancelable");
+ assert_true(e.canIntercept, "canIntercept");
+ assert_false(e.userInitiated, "userInitiated");
+ assert_false(e.hashChange, "hashChange");
+ assert_equals(e.formData, null, "formData");
+ assert_equals(e.destination.url, i.src + "?foo", "destination.url");
+ assert_false(e.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.destination.key, "", "destination.key");
+ assert_equals(e.destination.id, "", "destination.id");
+ assert_equals(e.destination.index, -1, "destination.index");
+ });
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => window.open("/common/blank.html?foo", "windowname"));
+}, "using window.open() to navigate cross-document targeting a same-origin window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-samedocument-crossorigin-sameorigindomain.sub.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-samedocument-crossorigin-sameorigindomain.sub.html
new file mode 100644
index 0000000000..74a6cae246
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-samedocument-crossorigin-sameorigindomain.sub.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+
+<script>
+document.domain = "{{host}}";
+async_test(t => {
+ const url = new URL("resources/document-domain-setter.sub.html", location.href);
+ url.hostname = "{{domains[www1]}}";
+ const iframe = document.createElement("iframe");
+ iframe.name = "windowname";
+ iframe.src = url;
+ document.body.append(iframe);
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => {
+ iframe.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push", "navigationType");
+ assert_true(e.cancelable, "cancelable");
+ assert_true(e.canIntercept, "canIntercept");
+ assert_false(e.userInitiated, "userInitiated");
+ assert_true(e.hashChange, "hashChange");
+ assert_equals(e.formData, null, "formData");
+ assert_equals(e.destination.url, url + "#foo", "destination.url");
+ assert_true(e.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.destination.key, "", "destination.key");
+ assert_equals(e.destination.id, "", "destination.id");
+ assert_equals(e.destination.index, -1, "destination.index");
+ });
+
+ window.open(url + "#foo", iframe.name);
+ });
+}, "using window.open() to navigate same-document targeting a same-origin-domain (but cross-origin) window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-samedocument-crossorigin.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-samedocument-crossorigin.html
new file mode 100644
index 0000000000..9b31744ba3
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-samedocument-crossorigin.html
@@ -0,0 +1,33 @@
+<!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 iframeURL = new URL("resources/cross-origin-iframe-helper.html", location.href);
+ iframeURL.hostname = get_host_info().REMOTE_HOST;
+ const iframe = document.createElement("iframe")
+ iframe.src = iframeURL;
+ iframe.name = "windowname";
+ document.body.append(iframe);
+
+ window.onmessage = t.step_func_done(e => {
+ assert_equals(e.data.navigationType, "push", "navigationType");
+ assert_true(e.data.cancelable, "cancelable");
+ assert_true(e.data.canIntercept, "canIntercept");
+ assert_false(e.data.userInitiated, "userInitiated");
+ assert_true(e.data.hashChange, "hashChange");
+ assert_equals(e.data.formData, null, "formData");
+ assert_equals(e.data.destination.url, iframe.src + "#foo", "destination.url");
+ assert_true(e.data.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.data.destination.key, "", "destination.key");
+ assert_equals(e.data.destination.id, "", "destination.id");
+ assert_equals(e.data.destination.index, -1, "destination.index");
+ });
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => window.open(iframeURL + "#foo", "windowname"));
+}, "using window.open() to navigate same-document targeting a cross-origin window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-samedocument-sameorigin.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-samedocument-sameorigin.html
new file mode 100644
index 0000000000..c357072e09
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/open-samedocument-sameorigin.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe id="i" name="windowname" src="/common/blank.html"></iframe>
+
+<script>
+async_test(t => {
+ i.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push", "navigationType");
+ assert_true(e.cancelable, "cancelable");
+ assert_true(e.canIntercept, "canIntercept");
+ assert_false(e.userInitiated, "userInitiated");
+ assert_true(e.hashChange, "hashChange");
+ assert_equals(e.formData, null, "formData");
+ assert_equals(e.destination.url, i.src + "#foo", "destination.url");
+ assert_true(e.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.destination.key, "", "destination.key");
+ assert_equals(e.destination.id, "", "destination.id");
+ assert_equals(e.destination.index, -1, "destination.index");
+ });
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => window.open("/common/blank.html#foo", "windowname"));
+}, "using window.open() to navigate same-document targeting a same-origin window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/resources/cross-origin-iframe-helper.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/resources/cross-origin-iframe-helper.html
new file mode 100644
index 0000000000..3393a2ecaa
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/resources/cross-origin-iframe-helper.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<script>
+navigation.onnavigate = e => {
+ const message = {
+ navigationType: e.navigationType,
+ cancelable: e.cancelable,
+ canIntercept: e.canIntercept,
+ userInitiated: e.userInitiated,
+ hashChange: e.hashChange,
+ formData: e.formData,
+ destination: {
+ url: e.destination.url,
+ sameDocument: e.destination.sameDocument,
+ key: e.destination.key,
+ id: e.destination.id,
+ index: e.destination.index
+ }
+ };
+ e.preventDefault();
+ top.postMessage(message, "*");
+};
+
+window.onload = () => {
+ if (location.href.includes("postMessage-top-when-done")) {
+ top.postMessage("DONE", "*");
+ }
+};
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/resources/document-domain-setter.sub.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/resources/document-domain-setter.sub.html
new file mode 100644
index 0000000000..abe32ad8fb
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/resources/document-domain-setter.sub.html
@@ -0,0 +1,3 @@
+<script>
+document.domain = "{{host}}";
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-crossdocument-crossorigin-sameorigindomain.sub.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-crossdocument-crossorigin-sameorigindomain.sub.html
new file mode 100644
index 0000000000..ffdaac7ccd
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-crossdocument-crossorigin-sameorigindomain.sub.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+
+<script>
+document.domain = "{{host}}";
+async_test(t => {
+ const url = new URL("resources/document-domain-setter.sub.html?start", location.href);
+ url.hostname = "{{domains[www1]}}";
+ const iframe = document.createElement("iframe");
+ iframe.name = "windowname";
+ iframe.src = url;
+ document.body.append(iframe);
+
+ url.search = ""; // setting to "?" actually erases it anyway
+ const form = document.createElement("form");
+ form.action = url + "?";
+ form.target = iframe.name;
+ document.body.append(form);
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => {
+ iframe.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push", "navigationType");
+ assert_true(e.cancelable, "cancelable");
+ assert_true(e.canIntercept, "canIntercept");
+ assert_false(e.userInitiated, "userInitiated");
+ assert_false(e.hashChange, "hashChange");
+ assert_equals(e.formData, null, "formData");
+ assert_equals(e.destination.url, form.action, "destination.url");
+ assert_false(e.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.destination.key, "", "destination.key");
+ assert_equals(e.destination.id, "", "destination.id");
+ assert_equals(e.destination.index, -1, "destination.index");
+ });
+
+ form.submit();
+ });
+}, "submitting a <form> element that navigates cross-document targeting a same-origin-domain (but cross-origin) window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-crossdocument-crossorigin.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-crossdocument-crossorigin.html
new file mode 100644
index 0000000000..007f58b1fb
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-crossdocument-crossorigin.html
@@ -0,0 +1,30 @@
+<!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 url = new URL("resources/cross-origin-iframe-helper.html", location.href);
+ url.hostname = get_host_info().REMOTE_HOST;
+ const iframe = document.createElement("iframe")
+ iframe.src = url;
+ iframe.name = "windowname";
+ document.body.append(iframe);
+
+ const form = document.createElement("form");
+ form.action = url;
+ form.target = iframe.name;
+ form.innerHTML = `<input type="hidden" name="postMessage-top-when-done" value="yes">`;
+ document.body.append(form);
+
+ window.onmessage = t.step_func_done(e => {
+ // If we hit onnavigate in the target window, we'll get a different message, and fail.
+ assert_equals(e.data, "DONE");
+ });
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => form.submit());
+}, "submitting a <form> element that navigates cross-document targeting a cross-origin window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-crossdocument-sameorigin.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-crossdocument-sameorigin.html
new file mode 100644
index 0000000000..b54a5a440b
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-crossdocument-sameorigin.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<form id="form" action="/common/blank.html?" target="windowname"></form>
+<iframe id="i" name="windowname" src="/common/blank.html?start"></iframe>
+
+<script>
+async_test(t => {
+ i.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push", "navigationType");
+ assert_true(e.cancelable, "cancelable");
+ assert_true(e.canIntercept, "canIntercept");
+ assert_false(e.userInitiated, "userInitiated");
+ assert_false(e.hashChange, "hashChange");
+ assert_equals(e.formData, null, "formData");
+ assert_equals(e.destination.url, form.action, "destination.url");
+ assert_false(e.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.destination.key, "", "destination.key");
+ assert_equals(e.destination.id, "", "destination.id");
+ assert_equals(e.destination.index, -1, "destination.index");
+ });
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => form.submit());
+}, "submitting a <form> element that navigates cross-document targeting a same-origin window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-samedocument-crossorigin-sameorigindomain.sub.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-samedocument-crossorigin-sameorigindomain.sub.html
new file mode 100644
index 0000000000..e9ab17243e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-samedocument-crossorigin-sameorigindomain.sub.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+
+<script>
+document.domain = "{{host}}";
+async_test(t => {
+ const url = new URL("resources/document-domain-setter.sub.html?", location.href);
+ url.hostname = "{{domains[www1]}}";
+ const iframe = document.createElement("iframe");
+ iframe.name = "windowname";
+ iframe.src = url;
+ document.body.append(iframe);
+
+ url.hash = "#foo";
+ const form = document.createElement("form");
+ form.action = url;
+ form.target = iframe.name;
+ document.body.append(form);
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => {
+ iframe.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push", "navigationType");
+ assert_true(e.cancelable, "cancelable");
+ assert_true(e.canIntercept, "canIntercept");
+ assert_false(e.userInitiated, "userInitiated");
+ assert_true(e.hashChange, "hashChange");
+ assert_equals(e.formData, null, "formData");
+ assert_equals(e.destination.url, form.action, "destination.url");
+ assert_true(e.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.destination.key, "", "destination.key");
+ assert_equals(e.destination.id, "", "destination.id");
+ assert_equals(e.destination.index, -1, "destination.index");
+ });
+
+ form.submit();
+ });
+}, "submitting a <form> element that navigates cross-document targeting a same-origin-domain (but cross-origin) window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-samedocument-crossorigin.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-samedocument-crossorigin.html
new file mode 100644
index 0000000000..69b12f2795
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-samedocument-crossorigin.html
@@ -0,0 +1,39 @@
+<!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 url = new URL("resources/cross-origin-iframe-helper.html?", location.href);
+ url.hostname = get_host_info().REMOTE_HOST;
+ const iframe = document.createElement("iframe")
+ iframe.src = url;
+ iframe.name = "windowname";
+ document.body.append(iframe);
+
+ url.hash = "#foo";
+ const form = document.createElement("form");
+ form.action = url;
+ form.target = iframe.name;
+ document.body.append(form);
+
+ window.onmessage = t.step_func_done(e => {
+ assert_equals(e.data.navigationType, "push", "navigationType");
+ assert_true(e.data.cancelable, "cancelable");
+ assert_true(e.data.canIntercept, "canIntercept");
+ assert_false(e.data.userInitiated, "userInitiated");
+ assert_true(e.data.hashChange, "hashChange");
+ assert_equals(e.data.formData, null, "formData");
+ assert_equals(e.data.destination.url, form.action, "destination.url");
+ assert_true(e.data.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.data.destination.key, "", "destination.key");
+ assert_equals(e.data.destination.id, "", "destination.id");
+ assert_equals(e.data.destination.index, -1, "destination.index");
+ });
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => form.submit());
+}, "submitting a <form> element that navigates same-document targeting a cross-origin window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-samedocument-sameorigin.html b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-samedocument-sameorigin.html
new file mode 100644
index 0000000000..8a0e1f1fb6
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/cross-window/submit-samedocument-sameorigin.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<form id="form" action="/common/blank.html?#foo" target="windowname"></form>
+<iframe id="i" name="windowname" src="/common/blank.html?"></iframe>
+
+<script>
+async_test(t => {
+ i.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push", "navigationType");
+ assert_true(e.cancelable, "cancelable");
+ assert_true(e.canIntercept, "canIntercept");
+ assert_false(e.userInitiated, "userInitiated");
+ assert_true(e.hashChange, "hashChange");
+ assert_equals(e.formData, null, "formData");
+ assert_equals(e.destination.url, form.action, "destination.url");
+ assert_true(e.destination.sameDocument, "destination.sameDocument");
+ assert_equals(e.destination.key, "", "destination.key");
+ assert_equals(e.destination.id, "", "destination.id");
+ assert_equals(e.destination.index, -1, "destination.index");
+ });
+
+ navigation.onnavigate = t.unreached_func("onnavigate must not fire in the source window");
+ window.onload = t.step_func(() => form.submit());
+}, "submitting a <form> element that navigates same-document targeting a same-origin window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/defaultPrevented-navigation-preempted.html b/testing/web-platform/tests/navigation-api/navigate-event/defaultPrevented-navigation-preempted.html
new file mode 100644
index 0000000000..1df4f56c5e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/defaultPrevented-navigation-preempted.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(() => {
+ let navigateEvent;
+ let navigateError_called = false;
+ navigation.onnavigate = e => navigateEvent = e;
+ navigation.onnavigateerror = () => navigateError_called = true;
+
+ navigation.navigate("#1");
+ assert_false(navigateEvent.defaultPrevented);
+ assert_false(navigateError_called);
+
+ navigation.navigate("#2");
+ assert_false(navigateEvent.defaultPrevented);
+ assert_true(navigateError_called);
+}, "navigateEvent.defaultPrevented isn't affected when the navigation is preempted after dispatch");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/defaultPrevented-window-stop-after-dispatch.html b/testing/web-platform/tests/navigation-api/navigate-event/defaultPrevented-window-stop-after-dispatch.html
new file mode 100644
index 0000000000..da5de10ddd
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/defaultPrevented-window-stop-after-dispatch.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ window.onload = t.step_func_done(() => {
+ let navigateEvent;
+ navigation.onnavigate = e => navigateEvent = e;
+ navigation.navigate("?1");
+ assert_false(navigateEvent.defaultPrevented);
+
+ window.stop();
+ assert_false(navigateEvent.defaultPrevented);
+ });
+}, "window.stop() doesn't affect navigateEvent.defaultPrevented after dispatch");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/event-constructor.html b/testing/web-platform/tests/navigation-api/navigate-event/event-constructor.html
new file mode 100644
index 0000000000..065884240e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/event-constructor.html
@@ -0,0 +1,96 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new NavigateEvent("navigate");
+ });
+}, "can't bypass required members by omitting the dictionary entirely");
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new NavigateEvent("navigate", {
+ navigationType: "push",
+ canIntercept: false,
+ userInitiated: false,
+ hashChange: false,
+ signal: (new AbortController()).signal,
+ formData: null,
+ downloadRequest: null,
+ info: null
+ });
+ });
+}, "destination is required");
+
+async_test(t => {
+ // We need to grab an NavigationDestination.
+ navigation.onnavigate = t.step_func_done(e => {
+ assert_throws_js(TypeError, () => {
+ new NavigateEvent("navigate", {
+ navigationType: "push",
+ destination: e.destination,
+ canIntercept: false,
+ userInitiated: false,
+ hashChange: false,
+ formData: null,
+ downloadRequest: null,
+ info: null
+ });
+ });
+ });
+ history.pushState(1, null, "#1");
+}, "signal is required");
+
+async_test(t => {
+ // We need to grab an NavigationDestination.
+ navigation.onnavigate = t.step_func_done(e => {
+ const info = { some: "object with identity" };
+ const formData = new FormData();
+ const signal = (new AbortController()).signal;
+ const downloadRequest = "abc";
+
+ const event = new NavigateEvent("navigate", {
+ navigationType: "replace",
+ destination: e.destination,
+ canIntercept: true,
+ userInitiated: true,
+ hashChange: true,
+ signal,
+ formData,
+ downloadRequest,
+ info
+ });
+
+ assert_equals(event.navigationType, "replace");
+ assert_equals(event.destination, e.destination);
+ assert_equals(event.canIntercept, true);
+ assert_equals(event.userInitiated, true);
+ assert_equals(event.hashChange, true);
+ assert_equals(event.signal, signal);
+ assert_equals(event.formData, formData);
+ assert_equals(event.downloadRequest, downloadRequest);
+ assert_equals(event.info, info);
+ });
+ history.pushState(2, null, "#2");
+}, "all properties are reflected back");
+
+async_test(t => {
+ // We need to grab an NavigationDestination.
+ navigation.onnavigate = t.step_func_done(e => {
+ const event = new NavigateEvent("navigate", {
+ destination: e.destination,
+ signal: (new AbortController()).signal
+ });
+
+ assert_equals(event.navigationType, "push");
+ assert_equals(event.canIntercept, false);
+ assert_equals(event.userInitiated, false);
+ assert_equals(event.hashChange, false);
+ assert_equals(event.formData, null);
+ assert_equals(event.downloadRequest, null);
+ assert_equals(event.info, undefined);
+ });
+ history.pushState(3, null, "#3");
+}, "defaults are as expected");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-after-dispatch.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-after-dispatch.html
new file mode 100644
index 0000000000..abb328050d
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-after-dispatch.html
@@ -0,0 +1,16 @@
+<!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 => {
+ navigation.onnavigate = t.step_func(e => {
+ t.step_timeout(t.step_func_done(() => {
+ assert_equals(e.eventPhase, Event.NONE);
+ assert_throws_dom("InvalidStateError", () => e.intercept());
+ }), 0);
+ });
+
+ location.href = "#1";
+}, "event.intercept() throws if used after the dispatch phase");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-and-navigate.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-and-navigate.html
new file mode 100644
index 0000000000..dfbb67b6b6
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-and-navigate.html
@@ -0,0 +1,27 @@
+<!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(r => window.onload = () => t.step_timeout(r, 0));
+ await navigation.navigate("#1").finished;
+
+ navigation.onnavigate = e => e.intercept();
+ navigation.oncurrententrychange = e => {
+ if (e.navigationType == "traverse") {
+ assert_equals(location.hash, "");
+ assert_equals(navigation.currentEntry.index, 0);
+ assert_equals(navigation.entries().length, 2);
+ navigation.navigate("#2");
+ }
+ };
+ let back_result = navigation.back();
+ await back_result.committed;
+ assert_equals(location.hash, "#2");
+ await promise_rejects_dom(t, "AbortError", back_result.finished);
+ assert_equals(navigation.currentEntry.index, 1);
+ assert_equals(navigation.entries().length, 2);
+}, "Using intercept() then navigate() in the ensuing currententrychange should abort the finished promise (but not the committed promise)");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-canceled-event.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-canceled-event.html
new file mode 100644
index 0000000000..d4b9633c1a
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-canceled-event.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+test(t => {
+ let assertionHappened = false;
+ navigation.onnavigate = t.step_func_done(e => {
+ e.preventDefault();
+ assert_throws_dom("InvalidStateError", () => e.intercept());
+ assertionHappened = true;
+ });
+
+ location.href = "#1";
+ assert_true(assertionHappened);
+}, "event.intercept() throws if used on a canceled event");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-cross-document-same-origin.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-cross-document-same-origin.html
new file mode 100644
index 0000000000..0d610cce4f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-cross-document-same-origin.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+async_test(t => {
+ let target_url = location.href + "?1";
+ navigation.onnavigate = t.step_func_done(e => {
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_false(e.hashChange);
+ e.intercept({ handler: async () => {
+ await Promise.resolve();
+ t.step_func_done(() => assert_equals(location.href, target_url));
+ }});
+ });
+
+ window.onload = t.step_func(() => location.href = target_url);
+}, "event.intercept() intercepts a same-origin cross-document navigation");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-cross-origin.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-cross-origin.html
new file mode 100644
index 0000000000..b367df546c
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-cross-origin.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+async_test(t => {
+ navigation.onnavigate = t.step_func_done(e => {
+ assert_true(e.cancelable);
+ assert_false(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_false(e.hashChange);
+ assert_throws_dom("SecurityError", () => e.intercept());
+ e.preventDefault();
+ });
+
+ window.onload = t.step_func(() => location.href = get_host_info().HTTPS_REMOTE_ORIGIN);
+}, "event.intercept() should throw if called for a cross origin navigation");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-detach-multiple.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-detach-multiple.html
new file mode 100644
index 0000000000..5b6a623284
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-detach-multiple.html
@@ -0,0 +1,18 @@
+<!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_func(() => {
+ let second_handler_run = false;
+ i.contentWindow.navigation.onnavigate = e => {
+ e.intercept({ handler() { i.remove(); }});
+ e.intercept({ handler() { second_handler_run = true; }});
+ };
+
+ i.contentWindow.location.href = "#1";
+ t.step_timeout(t.step_func_done(() => assert_true(second_handler_run)), 0);
+ });
+}, "event.intercept() throws if used on an event from a detached iframe");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-detach.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-detach.html
new file mode 100644
index 0000000000..b5ce45aa29
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-detach.html
@@ -0,0 +1,17 @@
+<!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_func(() => {
+ i.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ let iframe_constructor = i.contentWindow.DOMException;
+ i.remove();
+ assert_throws_dom("InvalidStateError", iframe_constructor, () => e.intercept());
+ });
+
+ i.contentWindow.location.href = "#1";
+ });
+}, "event.intercept() throws if used on an event from a detached iframe");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-handler-null-or-undefined.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-handler-null-or-undefined.html
new file mode 100644
index 0000000000..7f5bd6b19f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-handler-null-or-undefined.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ navigation.onnavigate = t.step_func_done(e => {
+ assert_throws_js(TypeError, () => e.intercept({ handler: null }));
+ });
+
+ location.href = "#1";
+ assert_equals(location.hash, "#1");
+}, "event.intercept() should throw if the handler is null");
+
+async_test(t => {
+ navigation.onnavigatesuccess = t.step_func_done(e => {});
+ navigation.onnavigate = e => e.intercept({ handler: undefined });
+
+ location.href = "#2";
+ assert_equals(location.hash, "#2");
+}, "event.intercept() should not throw if the handler is explicit undefined");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-handler-returns-non-promise.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-handler-returns-non-promise.html
new file mode 100644
index 0000000000..96116e9295
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-handler-returns-non-promise.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ navigation.onnavigatesuccess = t.step_func_done(e => {
+ assert_equals(location.hash, "#1");
+ assert_equals(e.constructor, Event);
+ assert_false(e.bubbles);
+ assert_false(e.cancelable);
+ });
+ navigation.onnavigateerror = t.step_func_done(assert_unreached);
+ navigation.onnavigate = e => e.intercept({ handler() { return "123"; }});
+
+ location.href = "#1";
+ assert_equals(location.hash, "#1");
+}, "event.intercept() should resolve immediately if the handler doesn't return a promise");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-handler-throws.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-handler-throws.html
new file mode 100644
index 0000000000..769f675999
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-handler-throws.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ const err = new TypeError("a message");
+ let start_href = location.href;
+
+ navigation.onnavigatesuccess = t.step_func_done(assert_unreached);
+ navigation.onnavigateerror = t.step_func_done(e => {
+ assert_equals(location.hash, "#1");
+ assert_equals(e.constructor, ErrorEvent);
+ assert_true(e.error === err);
+ assert_equals(e.message, "Uncaught TypeError: a message");
+ assert_equals(e.filename, start_href);
+ assert_greater_than(e.colno, 0);
+ assert_greater_than(e.lineno, 0);
+ });
+ navigation.onnavigate = e => {
+ e.intercept({ handler() { throw err; }});
+ };
+
+ location.href = "#1";
+ assert_equals(location.hash, "#1");
+}, "event.intercept() should abort if the handler throws");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-history-pushState.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-history-pushState.html
new file mode 100644
index 0000000000..aad08b742e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-history-pushState.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ let start_length = history.length;
+ navigation.onnavigate = t.step_func(e => {
+ e.intercept({ handler: async () => {
+ await new Promise(r => t.step_timeout(r, 0));
+ t.step_timeout(t.step_func_done(() => {
+ assert_equals(location.hash, "#1");
+ assert_equals(history.state, "update");
+ assert_equals(history.length, start_length + 1);
+ }, 0));
+ }});
+ });
+
+ history.pushState("update", "", "#1");
+ assert_equals(location.hash, "#1");
+ assert_equals(history.state, "update");
+ assert_equals(history.length, start_length + 1);
+}, "event.intercept() should proceed if the given promise resolves");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-history-replaceState.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-history-replaceState.html
new file mode 100644
index 0000000000..16edec8596
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-history-replaceState.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ let start_length = history.length;
+ navigation.onnavigate = t.step_func(e => {
+ e.intercept({ handler: async () => {
+ await new Promise(r => t.step_timeout(r, 0));
+ t.step_timeout(t.step_func_done(() => {
+ assert_equals(location.hash, "#1");
+ assert_equals(history.state, "update");
+ assert_equals(history.length, start_length);
+ }, 0));
+ }});
+ });
+
+ history.replaceState("update", "", "#1");
+ assert_equals(location.hash, "#1");
+ assert_equals(history.state, "update");
+ assert_equals(history.length, start_length);
+}, "event.intercept() should proceed if the given promise resolves");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-multiple-times-reject.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-multiple-times-reject.html
new file mode 100644
index 0000000000..0b0f1f0b8e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-multiple-times-reject.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(async t => {
+ const err = new TypeError("a message");
+ let start_href = location.href;
+
+ let onnavigateerror_called = false;
+ let caught_rejection = false;
+ navigation.onnavigatesuccess = t.step_func(assert_unreached);
+ navigation.onnavigateerror = t.step_func(e => {
+ onnavigateerror_called = true;
+ assert_equals(location.hash, "#1");
+ assert_equals(e.constructor, ErrorEvent);
+ assert_equals(e.error, err);
+ assert_equals(e.message, "Uncaught TypeError: a message");
+ assert_equals(e.filename, start_href);
+ assert_greater_than(e.colno, 0);
+ assert_greater_than(e.lineno, 0);
+ });
+ navigation.onnavigate = t.step_func(e => {
+ e.intercept();
+ e.intercept({ handler: async () => {
+ await new Promise(r => t.step_timeout(r, 1));
+ return Promise.reject(err);
+ }});
+ e.intercept({ handler: () => new Promise(resolve => t.step_timeout(resolve, 1)) });
+ });
+
+ await navigation.navigate("#1").finished.catch(t.step_func(e => {
+ caught_rejection = true;
+ assert_equals(e, err);
+ }));
+ assert_true(onnavigateerror_called);
+ assert_true(caught_rejection);
+}, "event.intercept() is called multiple times and one of the promises rejects");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-multiple-times.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-multiple-times.html
new file mode 100644
index 0000000000..6deaeeb507
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-multiple-times.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(async t => {
+ let p1_resolved = false;
+ let p2_resolved = false;
+ let p3_resolved = false;
+ navigation.onnavigate = t.step_func(e => {
+ e.intercept({ handler: async () => {
+ await Promise.resolve();
+ assert_false(p2_resolved);
+ assert_false(p3_resolved);
+ p1_resolved = true;
+ }});
+ e.intercept({ handler: async () => {
+ await new Promise(resolve => t.step_timeout(resolve, 1));
+ assert_true(p1_resolved);
+ assert_false(p3_resolved);
+ p2_resolved = true;
+ }});
+ e.intercept({ handler: async () => {
+ await new Promise(resolve => t.step_timeout(resolve, 1));
+ assert_true(p1_resolved);
+ assert_true(p2_resolved);
+ p3_resolved = true;
+ }});
+ });
+
+ let promise = navigation.navigate("#1").finished;
+ assert_equals(location.hash, "#1");
+ assert_false(p1_resolved);
+ assert_false(p2_resolved);
+ assert_false(p3_resolved);
+
+ await promise;
+ assert_true(p1_resolved);
+ assert_true(p2_resolved);
+ assert_true(p3_resolved);
+}, "navigation.navigate() returns a finished promise that awaits all promises if event.intercept() is called multiple times");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-navigation-back.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-navigation-back.html
new file mode 100644
index 0000000000..8babf2bcdc
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-navigation-back.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(() => {
+ navigation.navigate("#foo").committed.then(() => {
+ assert_true(navigation.canGoBack);
+ navigation.onnavigate = t.step_func(e => e.intercept());
+ navigation.back().committed.then(t.step_func_done(() => {
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry, navigation.entries()[0]);
+ }));
+ });
+ }, 0);
+}, "event.intercept() can intercept navigation.back()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-on-synthetic-event.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-on-synthetic-event.html
new file mode 100644
index 0000000000..3a4b54de5e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-on-synthetic-event.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ // We need to grab an NavigationDestination to construct the event.
+ navigation.onnavigate = t.step_func_done(e => {
+ const event = new NavigateEvent("navigate", {
+ destination: e.destination,
+ signal: (new AbortController()).signal
+ });
+
+ assert_throws_dom("SecurityError", () => event.intercept());
+ });
+ history.pushState(1, null, "#1");
+}, "event.intercept() throws if invoked on a synthetic event");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-popstate.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-popstate.html
new file mode 100644
index 0000000000..f5f9d82be7
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-popstate.html
@@ -0,0 +1,26 @@
+<!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));
+ history.replaceState({ state: "foo"}, "", "#replace");
+
+ let onpopstate_fired = false;
+ let last_state;
+ window.onpopstate = e => {
+ onpopstate_fired = true;
+ last_state = e.state;
+ }
+ navigation.onnavigate = t.step_func(e => e.intercept());
+
+ await navigation.navigate("#").finished;
+ assert_true(navigation.canGoBack);
+
+ await navigation.back().finished;
+ assert_true(onpopstate_fired);
+ assert_not_equals(last_state, null);
+}, "event.intercept() should provide popstate with a valid state object");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-reject.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-reject.html
new file mode 100644
index 0000000000..4c5ec0163d
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-reject.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ const err = new TypeError("a message");
+ let start_href = location.href;
+
+ navigation.onnavigatesuccess = t.step_func_done(assert_unreached);
+ navigation.onnavigateerror = t.step_func_done(e => {
+ assert_equals(location.hash, "#1");
+ assert_equals(e.constructor, ErrorEvent);
+ assert_true(e.error === err);
+ assert_equals(e.message, "Uncaught TypeError: a message");
+ assert_equals(e.filename, start_href);
+ assert_greater_than(e.colno, 0);
+ assert_greater_than(e.lineno, 0);
+ });
+ navigation.onnavigate = e => {
+ e.intercept({ handler: async () => {
+ await new Promise(r => t.step_timeout(r, 0));
+ return Promise.reject(err);
+ }});
+ };
+
+ location.href = "#1";
+ assert_equals(location.hash, "#1");
+}, "event.intercept() should abort if the given promise rejects");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-resolve.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-resolve.html
new file mode 100644
index 0000000000..b60d89b5df
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-resolve.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ navigation.onnavigatesuccess = t.step_func_done(e => {
+ assert_equals(location.hash, "#1");
+ assert_equals(e.constructor, Event);
+ assert_false(e.bubbles);
+ assert_false(e.cancelable);
+ });
+ navigation.onnavigateerror = t.step_func_done(assert_unreached);
+ navigation.onnavigate = e => {
+ e.intercept({ handler: () => new Promise(resolve => t.step_timeout(resolve, 0)) });
+ };
+
+ location.href = "#1";
+ assert_equals(location.hash, "#1");
+}, "event.intercept() should proceed if the given promise resolves");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/intercept-same-document-history-back.html b/testing/web-platform/tests/navigation-api/navigate-event/intercept-same-document-history-back.html
new file mode 100644
index 0000000000..6faabe1964
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/intercept-same-document-history-back.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(() => {
+ let onnavigate_calls = 0;
+ let onnavigatesuccess_calls = 0;
+ navigation.onnavigate = e => {
+ onnavigate_calls++;
+ e.intercept();
+ }
+ navigation.onnavigatesuccess = t.step_func(e => {
+ onnavigatesuccess_calls++;
+ if (onnavigatesuccess_calls == 3) {
+ assert_equals(navigation.entries().length, 3);
+ assert_equals(navigation.currentEntry.index, 1);
+ assert_equals(onnavigate_calls, 3);
+ history.back();
+ } else if (onnavigatesuccess_calls == 4) {
+ assert_equals(navigation.entries().length, 3);
+ assert_equals(navigation.currentEntry.index, 0);
+ assert_equals(onnavigate_calls, 4);
+ t.done();
+ }
+ });
+
+ navigation.navigate("?foo").finished
+ .then(t.step_func(() => navigation.navigate("?bar").finished))
+ .then(t.step_func(() => {
+ assert_equals(navigation.entries().length, 3);
+ assert_equals(navigation.currentEntry.index, 2);
+ assert_equals(onnavigate_calls, 2);
+ history.back();
+ }));
+ }, 0);
+}, "event.intercept() can intercept same-document history.back()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-cross-origin.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-cross-origin.html
new file mode 100644
index 0000000000..ee09924850
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-cross-origin.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<a id="a" href="https://does-not-exist/foo.html"></a>
+<script>
+async_test(t => {
+ navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push");
+ assert_true(e.cancelable);
+ assert_false(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_false(e.hashChange);
+ assert_equals(e.formData, null);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(e.destination.url, "https://does-not-exist/foo.html");
+ assert_false(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ e.preventDefault();
+ });
+ a.click();
+}, "<a> cross-origin navigate event");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-download-userInitiated.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-download-userInitiated.html
new file mode 100644
index 0000000000..b9506984da
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-download-userInitiated.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<a id="a" href="?1">Click me</a>
+<script>
+async_test(t => {
+ a.download = "";
+ navigation.onnavigate = t.step_func(e => {
+ assert_equals(e.navigationType, "push");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_true(e.userInitiated);
+ assert_false(e.hashChange);
+ assert_equals(e.downloadRequest, "");
+ assert_equals(e.formData, null);
+ assert_equals(new URL(e.destination.url).search, "?1");
+ assert_false(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ e.preventDefault();
+ t.step_timeout(t.step_func_done(() => assert_equals(location.hash, "")), 0);
+ });
+ test_driver.click(a);
+}, "<a download> click fires navigate event");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-download.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-download.html
new file mode 100644
index 0000000000..05fb0ecf2f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-download.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+const tests = [["a", ""], ["a", "filename"], ["area", ""], ["area", "filename"]];
+
+for (const [tag, download_attr] of tests) {
+ async_test(t => {
+ let a = document.createElement(tag);
+ a.href = "foo.html";
+ a.download = download_attr;
+ document.body.appendChild(a);
+ navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_false(e.hashChange);
+ assert_equals(e.downloadRequest, download_attr);
+ assert_equals(e.formData, null);
+ assert_equals(new URL(e.destination.url).pathname,
+ "/navigation-api/navigate-event/foo.html");
+ assert_false(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ e.preventDefault();
+ });
+ a.click();
+ }, `<${tag}> fires navigate and populates downloadRequest with '${download_attr}'`);
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-fragment.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-fragment.html
new file mode 100644
index 0000000000..51221ebcad
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-fragment.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<a id="a" href="#1"></a>
+<script>
+async_test(t => {
+ navigation.onnavigate = t.step_func(e => {
+ assert_equals(e.navigationType, "push");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_true(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(e.formData, null);
+ assert_equals(new URL(e.destination.url).hash, "#1");
+ assert_true(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ e.preventDefault();
+ t.step_timeout(t.step_func_done(() => assert_equals(location.hash, "")), 0);
+ });
+ a.click();
+}, "Fragment <a> click fires navigate event");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-same-origin-cross-document.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-same-origin-cross-document.html
new file mode 100644
index 0000000000..68f5bf0627
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-same-origin-cross-document.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<a id="a" href="foo.html"></a>
+<script>
+async_test(t => {
+ navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_false(e.hashChange);
+ assert_equals(e.formData, null);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(new URL(e.destination.url).pathname,
+ "/navigation-api/navigate-event/foo.html");
+ assert_false(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ e.preventDefault();
+ });
+ a.click();
+}, "<a> cross-document (but same-origin) navigate event");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-userInitiated.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-userInitiated.html
new file mode 100644
index 0000000000..39192c9151
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-userInitiated.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<a id="a" href="#1">Click me</a>
+<script>
+async_test(t => {
+ navigation.onnavigate = t.step_func(e => {
+ assert_equals(e.navigationType, "push");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_true(e.userInitiated);
+ assert_true(e.hashChange);
+ assert_equals(e.formData, null);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(new URL(e.destination.url).hash, "#1");
+ assert_true(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ e.preventDefault();
+ t.step_timeout(t.step_func_done(() => assert_equals(location.hash, "")), 0);
+ });
+
+ test_driver.click(a);
+}, "Fragment <a> click fires navigate event");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-with-target.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-with-target.html
new file mode 100644
index 0000000000..6407b963be
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-anchor-with-target.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe" name="i" src="/common/blank.html"></iframe>
+<a id="a" href="foo.html" target="i"></a>
+<script>
+async_test(t => {
+ window.onload = t.step_func(() => {
+ navigation.onnavigate = t.step_func_done(() => {
+ assert_unreached("onnavigate should not have fired in source window");
+ });
+ iframe.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_false(e.hashChange);
+ assert_equals(e.formData, null);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(new URL(e.destination.url).pathname,
+ "/navigation-api/navigate-event/foo.html");
+ assert_false(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ e.preventDefault();
+ });
+ a.click();
+ });
+}, "<a> with a target fires navigate event in target window but not source");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-after-detach.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-after-detach.html
new file mode 100644
index 0000000000..1dcb6cac43
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-after-detach.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<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));
+
+ let destination_key = i.contentWindow.navigation.currentEntry.key;
+ let destination_id = i.contentWindow.navigation.currentEntry.id;
+ let destination_index = i.contentWindow.navigation.currentEntry.index;
+ await i.contentWindow.navigation.navigate("#1").finished;
+
+ let back_destination;
+ i.contentWindow.navigation.onnavigate = e => back_destination = e.destination;
+ await i.contentWindow.navigation.back().finished;
+
+ // Before detach, key/id/index are valid.
+ assert_equals(back_destination.key, destination_key);
+ assert_equals(back_destination.id, destination_id);
+ assert_equals(back_destination.index, destination_index);
+
+ i.remove();
+
+ // After detach, key/id/index are invalid, but the url is still valid.
+ assert_equals(back_destination.key, "");
+ assert_equals(back_destination.id, "");
+ assert_equals(new URL(back_destination.url).pathname, "/common/blank.html");
+ assert_equals(back_destination.index, -1);
+}, "navigate event destination after iframe detach");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-dynamic-index.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-dynamic-index.html
new file mode 100644
index 0000000000..2e0f1ea497
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-dynamic-index.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../navigation-methods/return-value/resources/helpers.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));
+ await navigation.navigate("#1").finished;
+
+ let back_destination;
+ navigation.addEventListener("navigate", t.step_func(e => {
+ back_destination = e.destination;
+ assert_equals(back_destination.index, 0);
+ }), { once: true });
+ await navigation.back().finished;
+
+ // Disposing the destination entry of back_destination should update
+ // back_destination.index, even though back_destination's navigation has
+ // completed.
+ await navigation.navigate("#clobber_back", { history: "replace" }).finished;
+ assert_equals(back_destination.index, -1);
+
+ navigation.addEventListener("navigate", t.step_func(e => {
+ assert_equals(e.destination.index, 1);
+
+ // Dispose the destination entry and destination.index should update.
+ navigation.navigate("#clobber_forward");
+ assert_equals(e.destination.index, -1);
+ }), { once: true });
+ await assertBothRejectDOM(t, navigation.forward(), "AbortError");
+}, "navigate event destination.index should be dynamic");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-back-forward.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-back-forward.html
new file mode 100644
index 0000000000..c8b1043aba
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-back-forward.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(t.step_func_done(() => {
+ let navState = { statevar: "state" };
+ navigation.navigate("#foo", { history: "replace", state: navState });
+ let target_key = navigation.currentEntry.key;
+ let target_id = navigation.currentEntry.id;
+ navigation.navigate("#bar");
+ navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "traverse");
+ assert_not_equals(e.destination, null);
+ assert_not_equals(e.destination.getState(), undefined);
+ assert_not_equals(e.destination.getState(), e.destination.getState());
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ });
+ navigation.back();
+ }), 0);
+}, "navigate event destination.getState() on back/forward navigation");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-navigate.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-navigate.html
new file mode 100644
index 0000000000..5dac40de56
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-navigate.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(() => {
+ let navState = { statevar: "state" };
+ navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push");
+ assert_not_equals(e.destination, null);
+ assert_not_equals(e.destination.getState(), undefined);
+ assert_equals(e.destination.getState().statevar, "state");
+ assert_not_equals(e.destination.getState(), e.destination.getState());
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ });
+ navigation.navigate("#foo", { state: navState });
+ }, 0);
+}, "navigate event destination.getState() should be the state given to navigate()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-reload.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-reload.html
new file mode 100644
index 0000000000..a180e086a9
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-destination-getState-reload.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(() => {
+ let navState = { statevar: "state" };
+ navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "reload");
+ assert_not_equals(e.destination, null);
+ assert_not_equals(e.destination.getState(), undefined);
+ assert_equals(e.destination.getState().statevar, "state");
+ assert_not_equals(e.destination.getState(), e.destination.getState());
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ e.intercept();
+ });
+ navigation.updateCurrentEntry({ state: navState });
+ location.reload();
+ }, 0);
+}, "navigate event destination.getState() on location.reload()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-get.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-get.html
new file mode 100644
index 0000000000..87a102ddc0
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-get.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<form id="form" action=""></form>
+<script>
+async_test(t => {
+ navigation.onnavigate = t.step_func_done(e => {
+ e.preventDefault();
+
+ assert_equals(e.navigationType, "replace");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_false(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(e.destination.url, location.href + "?");
+ assert_false(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+
+ // Because it's a GET, not a POST
+ assert_equals(e.formData, null);
+ });
+ window.onload = t.step_func(() => form.submit());
+}, "<form> submission with GET method fires navigate event but with formData null");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-reload.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-reload.html
new file mode 100644
index 0000000000..f18a11ebda
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-reload.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe" name="i" src="/common/blank.html"></iframe>
+<form id="form" action="/common/blank.html?1" method="post" target="i"></form>
+<script>
+async_test(t => {
+ window.onload = t.step_func(() => {
+ navigation.onnavigate = t.step_func_done(() => {
+ assert_unreached("onnavigate should not have fired in source window");
+ });
+ iframe.contentWindow.navigation.onnavigate = t.step_func(e => {
+ assert_equals(e.navigationType, "push");
+ assert_not_equals(e.formData, null);
+
+ iframe.onload = t.step_func(() => {
+ iframe.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "reload");
+ assert_equals(e.formData, null);
+ });
+
+ iframe.contentWindow.location.reload();
+ });
+ });
+ form.submit();
+ });
+}, "reloading a page created from form submission results in formData of null, not the original form data");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-traverse.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-traverse.html
new file mode 100644
index 0000000000..d673537503
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-traverse.html
@@ -0,0 +1,44 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe" name="i" src="/common/blank.html"></iframe>
+<form id="form" action="/common/blank.html?1" method="post" target="i"></form>
+<script>
+async_test(t => {
+ window.onload = t.step_func(() => {
+ navigation.onnavigate = t.step_func_done(() => {
+ assert_unreached("onnavigate should not have fired in source window");
+ });
+ iframe.contentWindow.navigation.onnavigate = t.step_func(e => {
+ assert_equals(e.navigationType, "push");
+ assert_not_equals(e.formData, null);
+
+ iframe.onload = t.step_func(() => {
+ // Avoid the replace behavior that occurs if you navigate during the load handler
+ t.step_timeout(() => {
+ iframe.contentWindow.navigation.onnavigate = t.step_func(e => {
+ assert_equals(e.navigationType, "push");
+ assert_equals(e.formData, null);
+ });
+
+ iframe.contentWindow.onhashchange = t.step_func(() => {
+ iframe.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "traverse");
+ assert_equals(e.formData, null);
+ });
+
+ // 3: go back
+ iframe.contentWindow.history.back();
+ });
+
+ // 2: navigate from /common/blank.html?1-with-form-data to /common/blank.html?1#1-with-form-data
+ iframe.contentWindow.location.hash = "#1";
+ }, 0);
+ });
+ });
+
+ // 1: submit the form, navigating from /common/blank.html to /common/blank.html?1-with-form-data
+ form.submit();
+ });
+}, "reloading a page created from form submission results in formData of null, not the original form data");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-userInitiated.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-userInitiated.html
new file mode 100644
index 0000000000..40c5905447
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-userInitiated.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<form id="form" method="post" action="">
+<input id="submit" type="submit" value="Submit">
+</form>
+<script>
+async_test(t => {
+ navigation.onnavigate = t.step_func_done(e => {
+ e.preventDefault();
+
+ assert_equals(e.navigationType, "push");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_true(e.userInitiated);
+ assert_false(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(e.destination.url, location.href);
+ assert_false(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ assert_not_equals(e.formData, null);
+ });
+ window.onload = t.step_func(() => test_driver.click(submit));
+}, "<form> submission fires navigate event");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-with-target.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-with-target.html
new file mode 100644
index 0000000000..f6fe05c938
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form-with-target.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe" name="i" src="/common/blank.html"></iframe>
+<form id="form" method="post" action="foo.html" target="i"></form>
+<script>
+async_test(t => {
+ window.onload = t.step_func(() => {
+ navigation.onnavigate = t.unreached_func("onnavigate should not have fired in source window");
+
+ iframe.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_false(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(new URL(e.destination.url).pathname,
+ "/navigation-api/navigate-event/foo.html");
+ assert_false(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ assert_not_equals(e.formData, null);
+ });
+ form.submit();
+ });
+}, "<form> submission with a target fires navigate event in target window but not source");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-form.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form.html
new file mode 100644
index 0000000000..c57d72c3de
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-form.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<form id="form" method="post" action=""></form>
+<script>
+async_test(t => {
+ navigation.onnavigate = t.step_func_done(e => {
+ e.preventDefault();
+
+ assert_equals(e.navigationType, "replace");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_false(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(e.destination.url, location.href);
+ assert_false(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ assert_not_equals(e.formData, null);
+ });
+ window.onload = t.step_func(() => form.submit());
+}, "<form> submission fires navigate event");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-after-fragment.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-after-fragment.html
new file mode 100644
index 0000000000..57a30c85b8
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-after-fragment.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ window.onload = () => t.step_timeout(() => {
+ let start_length = history.length;
+ let target_key = navigation.currentEntry.key;
+ let target_id = navigation.currentEntry.id;
+ location.hash = "#1";
+ assert_equals(history.length, start_length + 1);
+
+ navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "traverse");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_true(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(new URL(e.destination.url).hash, "");
+ assert_true(e.destination.sameDocument);
+ assert_equals(e.destination.key, target_key);
+ assert_equals(e.destination.id, target_id);
+ assert_equals(e.destination.index, 0);
+ assert_equals(e.formData, null);
+ });
+
+ history.back();
+ }, 0);
+}, "history.back() fires the navigate event and sets hashChange when reversing a fragment navigation");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-after-pushState.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-after-pushState.html
new file mode 100644
index 0000000000..bf2e6e4e30
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-after-pushState.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ window.onload = () => t.step_timeout(() => {
+ let start_length = history.length;
+ let target_key = navigation.currentEntry.key;
+ let target_id = navigation.currentEntry.id;
+ history.pushState(1, "", "pushState.html");
+ assert_equals(history.length, start_length + 1);
+
+ navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "traverse");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_false(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(new URL(e.destination.url).hash, "");
+ assert_true(e.destination.sameDocument);
+ assert_equals(e.destination.key, target_key);
+ assert_equals(e.destination.id, target_id);
+ assert_equals(e.destination.index, 0);
+ assert_equals(e.formData, null);
+ });
+
+ history.back();
+ }, 0);
+}, "history.back() fires the navigate event when reversing a pushState()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-cross-document.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-cross-document.html
new file mode 100644
index 0000000000..cd7be6e9ad
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-cross-document.html
@@ -0,0 +1,32 @@
+<!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_func(() => {
+ let target_key = i.contentWindow.navigation.currentEntry.key;
+ let target_id = i.contentWindow.navigation.currentEntry.id;
+ i.contentWindow.navigation.navigate("?foo");
+ i.onload = t.step_func(() => {
+ i.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "traverse");
+ assert_false(e.cancelable);
+ assert_false(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_false(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(new URL(e.destination.url).pathname, "/common/blank.html");
+ assert_false(e.destination.sameDocument);
+ assert_equals(e.destination.key, target_key);
+ assert_equals(e.destination.id, target_id);
+ assert_equals(e.destination.index, 0);
+ assert_equals(e.formData, null);
+ assert_equals(e.info, undefined);
+ });
+ assert_true(i.contentWindow.navigation.canGoBack);
+ i.contentWindow.history.back();
+ })
+ });
+}, "navigate event for history.back() - cross-document");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-noop.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-noop.html
new file mode 100644
index 0000000000..10763c93fd
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-back-noop.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<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));
+
+ await i.contentWindow.navigation.navigate("#").finished;
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+ assert_equals(navigation.entries().length, 1);
+ assert_equals(navigation.currentEntry.index, 0);
+ assert_equals(history.length, 2);
+
+ i.remove();
+ assert_equals(navigation.entries().length, 1);
+ assert_equals(navigation.currentEntry.index, 0);
+ assert_equals(history.length, 2);
+
+ // back() here should do nothing. The iframe that would have navigated has
+ // been removed. No navigate event should be fired.
+ navigation.onnavigate = t.unreached_func("navigate must not fire");
+ navigation.oncurrententrychange = t.unreached_func("currententrychange must not fire");
+ history.back();
+
+ // Give time for the navigation to proceed.
+ await new Promise(resolve => t.step_timeout(resolve, 20));
+}, "history.back() does not fire a navigate event when there's nothing to navigate");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-go-0.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-go-0.html
new file mode 100644
index 0000000000..b1f41425b6
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-go-0.html
@@ -0,0 +1,27 @@
+<!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_func(() => {
+ i.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "reload");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_false(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(new URL(e.destination.url).pathname, "/common/blank.html");
+ assert_false(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ assert_equals(e.formData, null);
+ e.preventDefault();
+ });
+
+ i.contentWindow.history.go(0);
+ });
+}, "history.go(0) fires the navigate event");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-pushState.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-pushState.html
new file mode 100644
index 0000000000..266309a79e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-pushState.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ let start_length = history.length;
+ navigation.onnavigate = t.step_func(e => {
+ assert_equals(e.navigationType, "push");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_false(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(new URL(e.destination.url).hash, "#1");
+ assert_true(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ assert_equals(e.formData, null);
+ e.preventDefault();
+ t.step_timeout(t.step_func_done(() => {
+ assert_equals(location.hash, "");
+ assert_equals(history.state, null);
+ assert_equals(history.length, start_length);
+ }), 0);
+ });
+ history.pushState(1, null, "#1");
+}, "history.pushState() fires the navigate event");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-replaceState.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-replaceState.html
new file mode 100644
index 0000000000..ea6d3df455
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-history-replaceState.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ let start_length = history.length;
+ navigation.onnavigate = t.step_func(e => {
+ assert_equals(e.navigationType, "replace");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_false(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(new URL(e.destination.url).hash, "#1");
+ assert_true(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ assert_equals(e.formData, null);
+ e.preventDefault();
+ t.step_timeout(t.step_func_done(() => {
+ assert_equals(location.hash, "");
+ assert_equals(history.state, null);
+ assert_equals(history.length, start_length);
+ }), 0);
+ });
+ history.replaceState(1, null, "#1");
+}, "history.replaceState() fires the navigate event");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-iframe-location.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-iframe-location.html
new file mode 100644
index 0000000000..25d51476f3
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-iframe-location.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe" src="/common/blank.html"></iframe>
+<script>
+async_test(t => {
+ window.onload = t.step_func(() => {
+ navigation.onnavigate = t.step_func_done(() => {
+ assert_unreached("onnavigate should not have fired in source window");
+ });
+ iframe.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_true(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(new URL(e.destination.url).hash, "#1");
+ assert_true(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ assert_equals(e.formData, null);
+ e.preventDefault();
+ t.step_timeout(t.step_func_done(() => assert_equals(location.hash, "")), 0);
+ });
+ iframe.contentWindow.location.hash = "#1";
+ });
+}, "location API on another window fires navigate event in the target window only");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-location.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-location.html
new file mode 100644
index 0000000000..a4d0c60776
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-location.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<form id="form" action=""></form>
+<script>
+async_test(t => {
+ navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "replace");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_true(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(new URL(e.destination.url).hash, "#1");
+ assert_true(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ assert_equals(e.formData, null);
+ });
+ location.href = "#1";
+}, "location API fires navigate event");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-meta-refresh.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-meta-refresh.html
new file mode 100644
index 0000000000..9fa59b29f2
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-meta-refresh.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<head>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="resources/meta-refresh.html"></iframe>
+</head>
+<script>
+async_test(t => {
+ i.onload = t.step_func(() => {
+ i.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "reload");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_false(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(e.destination.url, i.contentWindow.location.href);
+ assert_false(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ assert_equals(e.formData, null);
+ e.preventDefault();
+ });
+ });
+}, "<meta> refresh fires navigate event");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-cross-document.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-cross-document.html
new file mode 100644
index 0000000000..2e1adbeee9
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-cross-document.html
@@ -0,0 +1,35 @@
+<!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_func(() => {
+ let target_key = i.contentWindow.navigation.currentEntry.key;
+ let target_id = i.contentWindow.navigation.currentEntry.id;
+ i.contentWindow.navigation.navigate("?foo");
+ i.onload = t.step_func(() => {
+ let beforeunload_called = false;
+ i.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_true(beforeunload_called);
+ assert_equals(e.navigationType, "traverse");
+ assert_false(e.cancelable);
+ assert_false(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_false(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(new URL(e.destination.url).pathname, "/common/blank.html");
+ assert_false(e.destination.sameDocument);
+ assert_equals(e.destination.key, target_key);
+ assert_equals(e.destination.id, target_id);
+ assert_equals(e.destination.index, 0);
+ assert_equals(e.formData, null);
+ assert_equals(e.info, "hi");
+ });
+ i.contentWindow.onbeforeunload = () => beforeunload_called = true;
+ assert_true(i.contentWindow.navigation.canGoBack);
+ i.contentWindow.navigation.back({ info: "hi" });
+ })
+ });
+}, "navigate event for navigation.back() - cross-document");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-same-document-in-iframe.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-same-document-in-iframe.html
new file mode 100644
index 0000000000..cebd2f3693
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-same-document-in-iframe.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<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));
+
+ let target_key = i.contentWindow.navigation.currentEntry.key;
+ let target_id = i.contentWindow.navigation.currentEntry.id;
+ await i.contentWindow.navigation.navigate("#").finished;
+ assert_true(i.contentWindow.navigation.canGoBack);
+
+ i.contentWindow.navigation.onnavigate = e => {
+ assert_equals(e.navigationType, "traverse");
+ assert_false(e.cancelable, "traversals in iframes should never be cancelable");
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_true(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(new URL(e.destination.url).hash, "");
+ assert_true(e.destination.sameDocument);
+ assert_equals(e.destination.key, target_key);
+ assert_equals(e.destination.id, target_id);
+ assert_equals(e.destination.index, 0);
+ assert_equals(e.formData, null);
+ assert_equals(e.info, "hi");
+ }
+ await i.contentWindow.navigation.back({ info: "hi" }).finished;
+}, "navigate event for navigation.back() - same-document in an iframe");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-same-document.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-same-document.html
new file mode 100644
index 0000000000..431d38449c
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-back-same-document.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(() => {
+ let target_key = navigation.currentEntry.key;
+ let target_id = navigation.currentEntry.id;
+ navigation.navigate("#foo").committed.then(t.step_func(() => {
+ navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "traverse");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_true(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(new URL(e.destination.url).hash, "");
+ assert_true(e.destination.sameDocument);
+ assert_equals(e.destination.key, target_key);
+ assert_equals(e.destination.id, target_id);
+ assert_equals(e.destination.index, 0);
+ assert_equals(e.formData, null);
+ assert_equals(e.info, "hi");
+ });
+ assert_true(navigation.canGoBack);
+ navigation.back({ info: "hi" });
+ }));
+ }, 0);
+}, "navigate event for navigation.back() - same-document");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-navigate.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-navigate.html
new file mode 100644
index 0000000000..ffc8ea867f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-navigation-navigate.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "replace");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_true(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(new URL(e.destination.url).hash, "#foo");
+ assert_true(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ assert_equals(e.formData, null);
+ });
+ navigation.navigate("#foo");
+}, "navigate event for navigation.navigate()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-to-javascript.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-to-javascript.html
new file mode 100644
index 0000000000..78f490d87b
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-to-javascript.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe" name="i" src="/common/blank.html"></iframe>
+
+<script>
+async_test(t => {
+ window.onload = t.step_func(() => {
+ navigation.onnavigate = t.unreached_func("onnavigate should not have fired in source window");
+
+ iframe.contentWindow.navigation.onnavigate = t.unreached_func("onnavigate should not have fired in iframe window");
+
+ iframe.contentWindow.location.href = "javascript:'foo'";
+
+ iframe.onload = () => t.done();
+ });
+}, "navigate event does not fire for javascript: URL navigations");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-to-srcdoc.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-to-srcdoc.html
new file mode 100644
index 0000000000..8bbb66a31f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-to-srcdoc.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe" name="i" src="/common/blank.html"></iframe>
+
+<script>
+async_test(t => {
+ window.onload = t.step_func(() => {
+ navigation.onnavigate = t.unreached_func("onnavigate should not have fired in source window");
+
+ iframe.contentWindow.navigation.onnavigate = t.step_func(e => {
+ assert_equals(e.navigationType, "push");
+ assert_true(e.cancelable, "cancelable");
+ assert_false(e.canIntercept, "canIntercept");
+ assert_false(e.userInitiated, "userInitiated");
+ assert_false(e.hashChange, "hashChange");
+ assert_equals(e.downloadRequest, null);
+ assert_equals(e.destination.url, "about:srcdoc");
+ assert_false(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ assert_equals(e.formData, null);
+ e.preventDefault();
+
+ // Make sure it doesn't navigate anyway.
+ iframe.onload = t.unreached_func("Must not load the srcdoc document");
+ t.step_timeout(() => t.done(), 10);
+ });
+
+ iframe.srcdoc = "srcdoc contents";
+ });
+}, "navigate event fires appropriately (and can be canceled) for adding the srcdoc attribute");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-window-open-self.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-window-open-self.html
new file mode 100644
index 0000000000..a6e443fd5a
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-window-open-self.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_true(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(new URL(e.destination.url).hash, "#1");
+ assert_true(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ assert_equals(e.formData, null);
+ e.preventDefault();
+ });
+ window.onload = t.step_func(() => window.open("#1", "_self"));
+}, "window.open() fires navigate event when targeting self");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigate-window-open.html b/testing/web-platform/tests/navigation-api/navigate-event/navigate-window-open.html
new file mode 100644
index 0000000000..1fe2402bc9
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigate-window-open.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe" name="i" src="/common/blank.html"></iframe>
+<script>
+async_test(t => {
+ window.onload = t.step_func(() => {
+ navigation.onnavigate = t.step_func_done(() => {
+ assert_unreached("onnavigate should not have fired in source window");
+ });
+ iframe.contentWindow.navigation.onnavigate = t.step_func_done(e => {
+ assert_equals(e.navigationType, "push");
+ assert_true(e.cancelable);
+ assert_true(e.canIntercept);
+ assert_false(e.userInitiated);
+ assert_true(e.hashChange);
+ assert_equals(e.downloadRequest, null);
+ assert_equals(new URL(e.destination.url).hash, "#1");
+ assert_true(e.destination.sameDocument);
+ assert_equals(e.destination.key, "");
+ assert_equals(e.destination.id, "");
+ assert_equals(e.destination.index, -1);
+ assert_equals(e.formData, null);
+ e.preventDefault();
+ });
+
+ window.open("/common/blank.html#1", "i");
+ });
+}, "window.open() fires navigate event in target window but not source");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigatesuccess-cross-document.html b/testing/web-platform/tests/navigation-api/navigate-event/navigatesuccess-cross-document.html
new file mode 100644
index 0000000000..1d528c1f5f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigatesuccess-cross-document.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="resources/navigatesuccess-cross-document-helper.html"></iframe>
+<script>
+async_test(t => {
+ // The iframe will post a message if it receives a navigatesuccess.
+ window.onmessage = t.unreached_func("navigatesuccess received");
+ window.onload = t.step_func(() => {
+ i.contentWindow.location.search = "?1";
+ i.onload = t.step_func_done(() => assert_equals(i.contentWindow.location.search, "?1"));
+ });
+}, "navigatesuccess does not fire for a cross-document navigation");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigatesuccess-same-document.html b/testing/web-platform/tests/navigation-api/navigate-event/navigatesuccess-same-document.html
new file mode 100644
index 0000000000..6007170ec1
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigatesuccess-same-document.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<a id="a" href="#1"></a>
+<script>
+async_test(t => {
+ navigation.onnavigatesuccess = t.step_func_done(() => assert_equals(location.hash, "#1"));
+ a.click();
+}, "navigatesuccess fires for a same-document navigation");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigation-back-cross-document-preventDefault.html b/testing/web-platform/tests/navigation-api/navigate-event/navigation-back-cross-document-preventDefault.html
new file mode 100644
index 0000000000..0b5b750876
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigation-back-cross-document-preventDefault.html
@@ -0,0 +1,33 @@
+
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../navigation-methods/return-value/resources/helpers.js"></script>
+<script src="/common/get-host-info.sub.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.
+ let w = window.open("resources/opener-postMessage-onload.html");
+ await new Promise(resolve => window.onmessage = resolve);
+ // Navigate to a url that will notify us when the navigation is complete.
+ w.navigation.navigate("opener-postMessage-onload.html?1");
+
+ await new Promise(resolve => window.onmessage = resolve);
+ assert_equals(w.navigation.entries().length, 2);
+ assert_equals(w.navigation.currentEntry.index, 1);
+ let navigate_called = false;
+ w.navigation.onnavigate = t.step_func(e => {
+ navigate_called = true;
+ assert_false(e.destination.sameDocument);
+ assert_false(e.cancelable);
+ // Should do nothing.
+ e.preventDefault();
+ });
+ w.navigation.back();
+ await new Promise(resolve => window.onmessage = resolve);
+ assert_equals(w.navigation.currentEntry.index, 0);
+ assert_true(navigate_called);
+}, "navigation.back() cross-document cannot be cancelled with the navigate event");
+</script>
+
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigation-back-same-document-preventDefault.html b/testing/web-platform/tests/navigation-api/navigate-event/navigation-back-same-document-preventDefault.html
new file mode 100644
index 0000000000..7edb188823
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigation-back-same-document-preventDefault.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../navigation-methods/return-value/resources/helpers.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));
+
+ await navigation.navigate("#").finished;
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry.index, 1);
+
+ navigation.onnavigate = e => e.preventDefault();
+
+ navigation.onnavigateerror = t.step_func(e => {
+ assert_equals(e.constructor, ErrorEvent);
+ assert_equals(e.filename, location.href);
+ navigateerror_called = true;
+ });
+ await assertBothRejectDOM(t, navigation.back(), "AbortError");
+ assert_equals(navigation.currentEntry.index, 1);
+ assert_true(navigateerror_called);
+}, "navigation.back() same-document preventDefault");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigation-traverseTo-in-iframe-same-document-preventDefault.html b/testing/web-platform/tests/navigation-api/navigate-event/navigation-traverseTo-in-iframe-same-document-preventDefault.html
new file mode 100644
index 0000000000..463746e8f7
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigation-traverseTo-in-iframe-same-document-preventDefault.html
@@ -0,0 +1,43 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../navigation-methods/return-value/resources/helpers.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<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));
+
+ // Navigate the iframe, then the top window, so that when the iframe goes back
+ // to its initial entry, the top window navigates as well.
+ await i.contentWindow.navigation.navigate("#").finished;
+ await navigation.navigate("#").finished;
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry.index, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+
+ // Ensure the top window, which is allowed to cancel the traversal, does so.
+ navigation.onnavigate = e => e.preventDefault();
+
+ let top_navigateerror_fired = false;
+ navigation.onnavigateerror = t.step_func(e => {
+ assert_equals(e.constructor, ErrorEvent);
+ assert_equals(e.filename, location.href);
+ top_navigateerror_fired = true;
+ });
+
+ i.contentWindow.navigation.onnavigate = t.unreached_func("navigate event should not fire in the iframe, because the traversal was cancelled in the top window");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("navigateerror event should not fire in the iframe, because the navigate event was not fired");
+
+ // When the top window blocks the traversal, it should be blocked in the
+ // iframe as well, and the traversal promises in the iframe should be rejected.
+ const iWindow = i.contentWindow;
+ const iDOMException = iWindow.DOMException;
+ await assertBothRejectDOM(t, i.contentWindow.navigation.traverseTo(i.contentWindow.navigation.entries()[0].key), "AbortError", iWindow, iDOMException);
+ assert_true(top_navigateerror_fired);
+ assert_equals(navigation.currentEntry.index, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+}, "navigation.traverseTo() in an iframe with same-document preventDefault in its parent");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigation-traverseTo-navigates-top-and-same-doc-child-and-cross-doc-child.html b/testing/web-platform/tests/navigation-api/navigate-event/navigation-traverseTo-navigates-top-and-same-doc-child-and-cross-doc-child.html
new file mode 100644
index 0000000000..31cb54fca2
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigation-traverseTo-navigates-top-and-same-doc-child-and-cross-doc-child.html
@@ -0,0 +1,49 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i1" src="/common/blank.html"></iframe>
+<iframe id="i2" src="/common/blank.html"></iframe>
+<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));
+ await navigation.navigate("#").finished;
+ await i1.contentWindow.navigation.navigate("#").finished;
+ i2.contentWindow.navigation.navigate("?");
+ await new Promise(resolve => i2.onload = () => t.step_timeout(resolve, 0));
+
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(i1.contentWindow.navigation.entries().length, 2);
+ assert_equals(i2.contentWindow.navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry.index, 1);
+ assert_equals(i1.contentWindow.navigation.currentEntry.index, 1);
+ assert_equals(i2.contentWindow.navigation.currentEntry.index, 1);
+
+ let navigate_event_count = 0;
+ navigation.onnavigate = t.step_func(e => {
+ assert_equals(navigate_event_count, 0);
+ navigate_event_count++;
+ assert_true(e.cancelable);
+ });
+ i1.contentWindow.navigation.onnavigate = t.step_func(e => {
+ assert_true(navigate_event_count > 0);
+ navigate_event_count++;
+ assert_false(e.cancelable);
+ });
+ i2.contentWindow.navigation.onnavigate = t.step_func(e => {
+ assert_true(navigate_event_count > 0);
+ navigate_event_count++;
+ assert_false(e.cancelable);
+ });
+
+ await navigation.traverseTo(navigation.entries()[0].key).finished;
+ // The top window will finish quickly, becuase it is same-document traversal.
+ // i2 will be slower because it is cross-document, so wait for its onload.
+ await new Promise(resolve => i2.onload = () => t.step_timeout(resolve, 0));
+ assert_equals(navigate_event_count, 3);
+ assert_equals(navigation.currentEntry.index, 0);
+ assert_equals(i1.contentWindow.navigation.currentEntry.index, 0);
+ assert_equals(i2.contentWindow.navigation.currentEntry.index, 0);
+}, "navigation.traverseTo() can navigate 3 frames of different types with correct navigate event cancelable values");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigation-traverseTo-same-document-preventDefault-multiple-windows.html b/testing/web-platform/tests/navigation-api/navigate-event/navigation-traverseTo-same-document-preventDefault-multiple-windows.html
new file mode 100644
index 0000000000..9bb64fb7cc
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigation-traverseTo-same-document-preventDefault-multiple-windows.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<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));
+ await navigation.navigate("#").finished;
+ await i.contentWindow.navigation.navigate("#").finished;
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry.index, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+
+ navigation.onnavigate = e => e.preventDefault();
+ i.contentWindow.navigation.onnavigate = t.unreached_func("navigate event should not fire in the iframe, because the traversal was cancelled in the top window");
+ await promise_rejects_dom(t, "AbortError", navigation.traverseTo(navigation.entries()[0].key).finished);
+ assert_equals(navigation.currentEntry.index, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+}, "navigation.traverseTo() - if a top window cancels the traversal, any iframes should not fire navigate");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/navigation-traverseTo-top-cancels-cross-document-child.html b/testing/web-platform/tests/navigation-api/navigate-event/navigation-traverseTo-top-cancels-cross-document-child.html
new file mode 100644
index 0000000000..11f07afefc
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/navigation-traverseTo-top-cancels-cross-document-child.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../navigation-methods/return-value/resources/helpers.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<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));
+ await navigation.navigate("#").finished;
+ i.contentWindow.navigation.navigate("?");
+ await new Promise(resolve => i.onload = () => t.step_timeout(resolve, 0));
+
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry.index, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+
+ navigation.onnavigate = t.step_func(e => e.preventDefault());
+ i.contentWindow.navigation.onnavigate = t.unreached_func("navigation should be cancelled before iframe fires navigate event");
+ await assertBothRejectDOM(t, navigation.traverseTo(navigation.entries()[0].key), "AbortError");
+ // Give the iframe time to navigate in case it was incorrectly permitted.
+ await new Promise(resolve => t.step_timeout(resolve, 50));
+}, "navigate.traverseTo() cancelled by top frame cancels cross-document iframe");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/replaceState-in-unload-then-remove-iframe.html b/testing/web-platform/tests/navigation-api/navigate-event/replaceState-in-unload-then-remove-iframe.html
new file mode 100644
index 0000000000..e97b72b157
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/replaceState-in-unload-then-remove-iframe.html
@@ -0,0 +1,16 @@
+<!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_func(() => {
+ i.contentWindow.onunload = t.step_func(() => {
+ i.contentWindow.history.replaceState(null, "", "#");
+ i.remove();
+ t.step_timeout(t.step_func_done(), 0);
+ });
+ i.contentWindow.location = "/common/blank.html?1";
+ });
+}, "reacting to the navigate event doesn't crash when replaceState is called in onunload");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/replaceState-inside-back-handler.html b/testing/web-platform/tests/navigation-api/navigate-event/replaceState-inside-back-handler.html
new file mode 100644
index 0000000000..29409b5e2b
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/replaceState-inside-back-handler.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../navigation-methods/return-value/resources/helpers.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));
+
+ await navigation.navigate("#push").finished;
+ navigation.onnavigate = () => history.replaceState(null, "", "#");
+ await assertBothRejectDOM(t, navigation.back(), "AbortError");
+}, "replaceState inside a navigate event for navigation.back()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/resources/meta-refresh.html b/testing/web-platform/tests/navigation-api/navigate-event/resources/meta-refresh.html
new file mode 100644
index 0000000000..fd453e663f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/resources/meta-refresh.html
@@ -0,0 +1,4 @@
+<head>
+<meta http-equiv="refresh" content="0"></meta>
+</head>
+<body></body>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/resources/navigatesuccess-cross-document-helper.html b/testing/web-platform/tests/navigation-api/navigate-event/resources/navigatesuccess-cross-document-helper.html
new file mode 100644
index 0000000000..aabc5015a9
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/resources/navigatesuccess-cross-document-helper.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<head>
+<script>
+navigation.onnavigatesuccess = () => top.postMessage("navigatesuccess received");
+</script>
+</head>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/resources/opener-postMessage-onload.html b/testing/web-platform/tests/navigation-api/navigate-event/resources/opener-postMessage-onload.html
new file mode 100644
index 0000000000..97e1d82058
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/resources/opener-postMessage-onload.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<head>
+<script>
+window.onload = () => opener.postMessage("onload", "*");
+</script>
+</head>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/same-url-replace-cross-document.html b/testing/web-platform/tests/navigation-api/navigate-event/same-url-replace-cross-document.html
new file mode 100644
index 0000000000..0a976cd51f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/same-url-replace-cross-document.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+promise_test(async t => {
+ // Wait for after the load event so that we are definitely testing the
+ // same URL as the cause of the rejections.
+ await new Promise(resolve => window.onload = () => t.step_timeout(resolve, 0));
+ assert_equals(navigation.entries().length, 1);
+
+ navigation.onnavigate = t.step_func(e => {
+ e.intercept();
+ assert_equals(e.navigationType, "replace");
+ });
+ navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ await navigation.navigate(location.href).finished;
+ assert_equals(navigation.entries().length, 1);
+ assert_equals(navigation.currentEntry.index, 0);
+}, "navigate() to the current URL cross document should replace");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/same-url-replace-same-document.html b/testing/web-platform/tests/navigation-api/navigate-event/same-url-replace-same-document.html
new file mode 100644
index 0000000000..839c687934
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/same-url-replace-same-document.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+promise_test(async t => {
+ // Wait for after the load event so that we are definitely testing the
+ // same URL as the cause of the rejections.
+ await new Promise(resolve => window.onload = () => t.step_timeout(resolve, 0));
+ assert_equals(navigation.entries().length, 1);
+ await navigation.navigate("#").finished;
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry.index, 1);
+
+ navigation.onnavigate = t.step_func(e => {
+ assert_equals(e.navigationType, "replace");
+ });
+ await navigation.navigate(location.href).finished;
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry.index, 1);
+}, "navigate() to the current URL same document should replace");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/signal-abort-detach-in-onnavigate.html b/testing/web-platform/tests/navigation-api/navigate-event/signal-abort-detach-in-onnavigate.html
new file mode 100644
index 0000000000..467ea88899
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/signal-abort-detach-in-onnavigate.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+promise_test(async t => {
+ await new Promise(resolve => i.onload = resolve);
+ let iframe_constructor = i.contentWindow.DOMException;
+ let iframe_typeerror = i.contentWindow.TypeError;
+ let abort_signal;
+ let onabort_called = false;
+ i.contentWindow.navigation.onnavigate = t.step_func(e => {
+ abort_signal = e.signal;
+ abort_signal.onabort = () => onabort_called = true;
+ i.remove();
+ });
+ await promise_rejects_dom(t, 'AbortError', iframe_constructor, i.contentWindow.navigation.navigate("#1").committed);
+ assert_true(abort_signal.aborted);
+ assert_true(onabort_called);
+}, "window detach inside a navigate event signals event.signal");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/signal-abort-intercept.html b/testing/web-platform/tests/navigation-api/navigate-event/signal-abort-intercept.html
new file mode 100644
index 0000000000..1e92d8e445
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/signal-abort-intercept.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(async t => {
+ let abort_signal;
+ let onabort_called = false;
+ navigation.onnavigate = t.step_func(e => {
+ abort_signal = e.signal;
+ abort_signal.onabort = () => onabort_called = true;
+ e.intercept();
+ });
+
+ await navigation.navigate("?1").finished;
+ assert_false(abort_signal.aborted);
+ assert_false(onabort_called);
+}, "event.intercept() does not signal event.signal");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/signal-abort-preventDefault.html b/testing/web-platform/tests/navigation-api/navigate-event/signal-abort-preventDefault.html
new file mode 100644
index 0000000000..60fed90ce6
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/signal-abort-preventDefault.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(async t => {
+ let abort_signal;
+ let events = [];
+ navigation.onnavigateerror = () => events.push("onnavigateerror");
+ navigation.onnavigate = t.step_func(e => {
+ abort_signal = e.signal;
+ abort_signal.onabort = () => events.push("onabort");
+ e.preventDefault();
+ });
+
+ await promise_rejects_dom(t, 'AbortError', navigation.navigate("?1").committed);
+ assert_true(abort_signal.aborted);
+ assert_array_equals(events, ["onabort", "onnavigateerror"]);
+}, "event.preventDefault() signals event.signal");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/signal-abort-window-stop-after-intercept.html b/testing/web-platform/tests/navigation-api/navigate-event/signal-abort-window-stop-after-intercept.html
new file mode 100644
index 0000000000..51ba7753a8
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/signal-abort-window-stop-after-intercept.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ window.onload = t.step_func(() => {
+ let start_url = location.href;
+ let abort_signal;
+ let onabort_called = false;
+ let navigateErrorException;
+ navigation.onnavigateerror = t.step_func(e => {
+ assert_equals(e.constructor, ErrorEvent);
+ navigateErrorException = e.error;
+ assert_equals(e.filename, start_url);
+ assert_greater_than(e.lineno, 0);
+ assert_greater_than(e.colno, 0);
+ });
+ navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess");
+ navigation.onnavigate = t.step_func(e => {
+ abort_signal = e.signal;
+ abort_signal.onabort = () => onabort_called = true;
+ e.intercept({ handler: () => new Promise(resolve => t.step_timeout(resolve, 0)) });
+ });
+ let result = navigation.navigate("?1");
+ window.stop();
+ assert_true(abort_signal.aborted);
+ assert_true(onabort_called);
+
+ result.committed.then(() => {
+ return promise_rejects_dom(t, 'AbortError', result.finished);
+ }).then(() => {
+ return result.finished.catch(e => assert_equals(e, navigateErrorException));
+ }).then(() => {
+ // Complete the test asynchronously to ensure that onnavigatesuccess
+ // didn't fire on a microtask.
+ t.step_timeout(t.step_func_done(() => {}), 5);
+ });
+ });
+}, "window.stop() cancels the navigate event's intercept() and signals event.signal");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/signal-abort-window-stop-in-onnavigate.html b/testing/web-platform/tests/navigation-api/navigate-event/signal-abort-window-stop-in-onnavigate.html
new file mode 100644
index 0000000000..1b406c42d3
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/signal-abort-window-stop-in-onnavigate.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ window.onload = t.step_func_done(() => {
+ let abort_signal;
+ let onabort_called = false;
+ let canceled_in_second_handler = false;
+ navigation.addEventListener("navigate", t.step_func(e => {
+ abort_signal = e.signal;
+ abort_signal.onabort = () => onabort_called = true;
+ window.stop();
+ }));
+ navigation.addEventListener("navigate", t.step_func(e => {
+ canceled_in_second_handler = e.defaultPrevented;
+ }));
+ navigation.navigate("?1");
+ assert_true(abort_signal.aborted);
+ assert_true(onabort_called);
+ assert_true(canceled_in_second_handler);
+ });
+}, "window.stop() signals event.signal inside a navigate event handler");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigate-event/signal-abort-window-stop.html b/testing/web-platform/tests/navigation-api/navigate-event/signal-abort-window-stop.html
new file mode 100644
index 0000000000..43e005e8b4
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigate-event/signal-abort-window-stop.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ window.onload = t.step_func(() => {
+ let abort_signal;
+ let onabort_called = false;
+ navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess");
+ navigation.onnavigate = t.step_func(e => {
+ abort_signal = e.signal;
+ abort_signal.onabort = () => onabort_called = true;
+ });
+ navigation.navigate("?1");
+ window.stop();
+ assert_true(abort_signal.aborted);
+ assert_true(onabort_called);
+ // Complete the test asynchronously to ensure that onnavigatesuccess
+ // didn't fire on a microtask.
+ t.step_timeout(t.step_func_done(() => {}), 0);
+ });
+}, "window.stop() signals event.signal");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/after-detach.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/after-detach.html
new file mode 100644
index 0000000000..c4ecfec44d
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/after-detach.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<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));
+
+ let i_navigation = i.contentWindow.navigation;
+
+ await i_navigation.navigate("#1").finished;
+ await i_navigation.navigate("#2").finished;
+ await i_navigation.back().finished;
+
+ assert_not_equals(i_navigation, null);
+ assert_not_equals(i_navigation.currentEntry, null);
+ assert_equals(i_navigation.entries().length, 3);
+ assert_true(i_navigation.canGoBack, "canGoBack");
+ assert_true(i_navigation.canGoForward, "canGoForward");
+
+ i.remove();
+
+ assert_equals(i_navigation.currentEntry, null);
+ assert_equals(i_navigation.entries().length, 0);
+ assert_false(i_navigation.canGoBack);
+ assert_false(i_navigation.canGoForward);
+}, "navigation.currentEntry/entries()/canGoBack/canGoForward after iframe removal");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/current-basic.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/current-basic.html
new file mode 100644
index 0000000000..78bbbb0560
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/current-basic.html
@@ -0,0 +1,107 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/is_uuid.js"></script>
+<script>
+test(() => {
+ let first_entry = navigation.currentEntry;
+ assert_not_equals(first_entry, null);
+ assert_not_equals(first_entry.key, null);
+ assert_true(isUUID(first_entry.key));
+ assert_not_equals(first_entry.id, null);
+ assert_true(isUUID(first_entry.id));
+ assert_equals(first_entry.url, location.href);
+ assert_true(first_entry.sameDocument);
+ assert_equals(navigation.entries().length, 1);
+ assert_equals(first_entry, navigation.entries()[0]);
+
+ history.replaceState(2, "", "#2");
+ let second_entry = navigation.currentEntry;
+ assert_not_equals(second_entry, first_entry);
+ assert_equals(second_entry.key, first_entry.key);
+ assert_true(isUUID(second_entry.key));
+ assert_not_equals(second_entry.id, first_entry.id);
+ assert_true(isUUID(second_entry.id));
+ assert_equals(second_entry.url, location.href);
+ assert_true(second_entry.sameDocument);
+ assert_equals(navigation.entries().length, 1);
+ assert_equals(second_entry, navigation.entries()[0]);
+
+ history.pushState(3, "", "#3");
+ let third_entry = navigation.currentEntry;
+ assert_not_equals(third_entry, second_entry);
+ assert_not_equals(third_entry.key, second_entry.key);
+ assert_true(isUUID(third_entry.key));
+ assert_not_equals(third_entry.id, second_entry.id);
+ assert_true(isUUID(third_entry.id));
+ assert_equals(third_entry.url, location.href);
+ assert_true(third_entry.sameDocument);
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(third_entry, navigation.entries()[1]);
+
+ history.pushState(4, "");
+ let fourth_entry = navigation.currentEntry;
+ assert_not_equals(fourth_entry, third_entry);
+ assert_not_equals(fourth_entry.key, third_entry.key);
+ assert_true(isUUID(fourth_entry.key));
+ assert_not_equals(fourth_entry.id, third_entry.id);
+ assert_true(isUUID(fourth_entry.id));
+ assert_equals(fourth_entry.url, third_entry.url);
+ assert_true(fourth_entry.sameDocument);
+ assert_equals(navigation.entries().length, 3);
+ assert_equals(fourth_entry, navigation.entries()[2]);
+
+ history.replaceState(5, "");
+ let fifth_entry = navigation.currentEntry;
+ assert_not_equals(fifth_entry, fourth_entry);
+ assert_equals(fifth_entry.key, fourth_entry.key);
+ assert_true(isUUID(fifth_entry.key));
+ assert_not_equals(fifth_entry.id, fourth_entry.id);
+ assert_true(isUUID(fifth_entry.id));
+ assert_equals(fifth_entry.url, fourth_entry.url);
+ assert_true(fifth_entry.sameDocument);
+ assert_equals(navigation.entries().length, 3);
+ assert_equals(fifth_entry, navigation.entries()[2]);
+
+ history.pushState(5, "");
+ let fifth_entry_after_push = navigation.currentEntry;
+ assert_not_equals(fifth_entry_after_push, fifth_entry);
+ assert_not_equals(fifth_entry_after_push.key, fifth_entry.key);
+ assert_true(isUUID(fifth_entry_after_push.key));
+ assert_not_equals(fifth_entry_after_push.id, fifth_entry.id);
+ assert_true(isUUID(fifth_entry_after_push.id));
+ assert_equals(fifth_entry_after_push.url, fifth_entry.url);
+ assert_true(fifth_entry_after_push.sameDocument);
+ assert_equals(navigation.entries().length, 4);
+ assert_equals(fifth_entry_after_push, navigation.entries()[3]);
+
+ history.replaceState(5, "");
+ let fifth_entry_after_replace = navigation.currentEntry;
+ assert_not_equals(fifth_entry_after_replace, fifth_entry_after_push);
+ assert_equals(fifth_entry_after_replace.key, fifth_entry_after_push.key);
+ assert_true(isUUID(fifth_entry_after_replace.key));
+ assert_not_equals(fifth_entry_after_replace.id, fifth_entry_after_push.id);
+ assert_true(isUUID(fifth_entry_after_replace.id));
+ assert_equals(fifth_entry_after_replace.url, fifth_entry_after_push.url);
+ assert_true(fifth_entry_after_replace.sameDocument);
+ assert_equals(navigation.entries().length, 4);
+ assert_equals(fifth_entry_after_replace, navigation.entries()[3]);
+
+ location.hash = "6";
+ let sixth_entry = navigation.currentEntry;
+ assert_not_equals(sixth_entry, fifth_entry_after_replace);
+ assert_equals(sixth_entry.key, fifth_entry_after_replace.key);
+ assert_true(isUUID(sixth_entry.key));
+ assert_not_equals(sixth_entry.id, fifth_entry_after_replace.id);
+ assert_true(isUUID(sixth_entry.id));
+ assert_not_equals(sixth_entry.url, fifth_entry_after_replace.url);
+ assert_true(sixth_entry.sameDocument);
+ assert_equals(navigation.entries().length, 4);
+ assert_equals(sixth_entry, navigation.entries()[3]);
+
+ navigation.entries().forEach(entry => {
+ assert_true(isUUID(entry.id));
+ assert_true(isUUID(entry.key));
+ });
+}, "Basic tests for navigation.currentEntry");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-across-origins.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-across-origins.html
new file mode 100644
index 0000000000..447273bff2
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-across-origins.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/is_uuid.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+async_test(t => {
+ window.onload = t.step_func(() => {
+ let start_key = i.contentWindow.navigation.currentEntry.key;
+ let start_id = i.contentWindow.navigation.currentEntry.id;
+
+ let cross_origin_url = new URL("resources/post-entries-length-to-top.html", location.href);
+ cross_origin_url.hostname = get_host_info().REMOTE_HOST;
+ i.contentWindow.location.assign(cross_origin_url.href);
+
+ window.onmessage = t.step_func(e => {
+ assert_equals(e.data, 1);
+
+ i.src = "/common/blank.html?2";
+ i.onload = t.step_func_done(() => {
+ let entries = i.contentWindow.navigation.entries();
+ assert_equals(entries.length, 1);
+ assert_equals(new URL(entries[0].url).search, "?2");
+ assert_not_equals(entries[0].key, start_key);
+ assert_not_equals(entries[0].id, start_id);
+ assert_true(isUUID(entries[0].key));
+ assert_true(isUUID(entries[0].id));
+ });
+ });
+ });
+}, "navigation.entries() should only contain entries that are both same-origin and contiguous");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-bfcache-in-iframe.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-bfcache-in-iframe.html
new file mode 100644
index 0000000000..b54a749950
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-bfcache-in-iframe.html
@@ -0,0 +1,36 @@
+<!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="/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js"></script>
+<script src="resources/is_uuid.js"></script>
+
+<script>
+// This test ensures that navigation.entries() in an iframe is properly updated
+// when a page is restored from bfcache.
+// First, create an iframe and do a fragment navigation in it, so that its
+// navigation.entries().length == 2. Then go back, so that entries()[0] is
+// current. Finally, navigate the main window (which should clobber the
+// the iframe's entries()[1]), and come back via bfcache. If the iframe's
+// entries() were updated, then its entries().length should have been reduced
+// to 1.
+runBfcacheTest({
+ targetOrigin: originSameOrigin,
+ funcBeforeNavigation: async () => {
+ window.events = [];
+ let i = document.createElement("iframe");
+ i.src = "/common/blank.html";
+ document.body.appendChild(i);
+ await new Promise(resolve => i.onload = () => setTimeout(resolve, 0));
+ await i.contentWindow.navigation.navigate("#foo");
+ await i.contentWindow.navigation.back();
+ window.frames[0].navigation.entries()[1].ondispose = () => events.push("dispose");
+ window.frames[0].onpageshow = () => events.push("pageshow");
+ },
+ async funcAfterAssertion(pageA, pageB) {
+ assert_equals(await pageA.execute_script(() => window.frames[0].navigation.entries().length), 1);
+ assert_array_equals(await pageA.execute_script(() => window.events), ["pageshow", "dispose"]);
+ }
+}, "entries() in an iframe must be updated after navigating back to a bfcached page");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-bfcache.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-bfcache.html
new file mode 100644
index 0000000000..ef93d1e27e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-bfcache.html
@@ -0,0 +1,60 @@
+<!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="/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js"></script>
+<script src="resources/is_uuid.js"></script>
+
+<script>
+// This test ensures that navigation.entries() is properly updated when a page
+// is restored from bfcache. Before navigating away and back, entries() contains
+// a single entry representing this document. When restored from bfcache,
+// entries() should now have two entries: [0] should still be this document, but
+// [1] should represent the document that we navigated to and back from
+// (assuming that document is same-origin to this one).
+runBfcacheTest({
+ targetOrigin: originSameOrigin,
+ funcBeforeNavigation: () => {
+ window.originalEntry0 = navigation.entries()[0];
+ },
+ async funcAfterAssertion(pageA, pageB) {
+ const entryData = await pageA.execute_script(() => {
+ return navigation.entries().map(e => ({
+ url: e.url,
+ key: e.key,
+ id: e.id,
+ index: e.index,
+ sameDocument: e.sameDocument
+ }));
+ });
+
+ assert_equals(entryData.length, 2);
+
+ // Ensure that [1] has the proper url, and otherwise is initialized as
+ // a cross-document NavigationHistoryEntry ought to be.
+ assert_equals(entryData[0].url, pageA.url);
+ assert_equals(entryData[1].url, pageB.url);
+
+ assert_true(isUUID(entryData[0].key));
+ assert_true(isUUID(entryData[1].key));
+ assert_not_equals(entryData[0].key, entryData[1].key);
+
+ assert_true(isUUID(entryData[0].id));
+ assert_true(isUUID(entryData[1].id));
+ assert_not_equals(entryData[0].id, entryData[1].id);
+
+ assert_equals(entryData[0].index, 0);
+ assert_equals(entryData[1].index, 1);
+
+ assert_true(entryData[0].sameDocument);
+ assert_false(entryData[1].sameDocument);
+
+ const currentIsZero = await pageA.execute_script(() => navigation.currentEntry === navigation.entries()[0]);
+ assert_true(currentIsZero);
+
+ const zeroIsSameAsOriginal = await pageA.execute_script(() => navigation.currentEntry === window.originalEntry0);
+ assert_true(zeroIsSameAsOriginal);
+ }
+}, "entries() must contain the forward-history page after navigating back to a bfcached page");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-blank-navigation-from-cross-origin.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-blank-navigation-from-cross-origin.html
new file mode 100644
index 0000000000..d527637ed3
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-blank-navigation-from-cross-origin.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>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = () => t.step_timeout(resolve, 0));
+
+ let i = document.createElement("iframe");
+ i.src = get_host_info().HTTP_ORIGIN_WITH_DIFFERENT_PORT + "/common/blank.html";
+ document.body.appendChild(i);
+ await new Promise(resolve => i.onload = () => t.step_timeout(resolve, 0));
+
+ i.contentWindow.location = "about:blank";
+ await new Promise(resolve => i.onload = resolve);
+ let entries = i.contentWindow.navigation.entries();
+ assert_equals(entries.length, 1);
+ assert_equals(entries[0].url, "about:blank");
+}, "entries() should not be leaked from cross-origin when navigating to about:blank");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-blank-navigation.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-blank-navigation.html
new file mode 100644
index 0000000000..f54ae06e14
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-blank-navigation.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/is_uuid.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+async_test(t => {
+ window.onload = t.step_func(() => {
+ i.onload = t.step_func_done(() => {
+ let entries = i.contentWindow.navigation.entries();
+ assert_equals(entries.length, 2);
+ assert_not_equals(entries[1].key, entries[0].key);
+ assert_not_equals(entries[1].url, entries[0].url);
+ assert_equals(entries[1].url, "about:blank");
+ assert_not_equals(entries[1].id, entries[0].id);
+
+ assert_true(isUUID(entries[0].key));
+ assert_true(isUUID(entries[0].id));
+ assert_true(isUUID(entries[1].key));
+ assert_true(isUUID(entries[1].id));
+ });
+ i.src = "about:blank";
+ });
+}, "entries() after navigation to about:blank");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-blob-navigation.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-blob-navigation.html
new file mode 100644
index 0000000000..67611f4d44
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-blob-navigation.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/is_uuid.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+async_test(t => {
+ window.onload = t.step_func(() => {
+ i.onload = t.step_func_done(() => {
+ let entries = i.contentWindow.navigation.entries();
+ assert_equals(entries.length, 2);
+ assert_not_equals(entries[1].key, entries[0].key);
+ assert_not_equals(entries[1].url, entries[0].url);
+ assert_equals(new URL(entries[1].url).protocol, "blob:");
+ assert_not_equals(entries[1].id, entries[0].id);
+
+ assert_true(isUUID(entries[0].key));
+ assert_true(isUUID(entries[0].id));
+ assert_true(isUUID(entries[1].key));
+ assert_true(isUUID(entries[1].id));
+ });
+ i.src = URL.createObjectURL(new Blob(["<body></body>"]));
+ });
+}, "entries() after navigation to a blob: URL");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-cross-document-forward-pruning.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-cross-document-forward-pruning.html
new file mode 100644
index 0000000000..ddd1ad571d
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-cross-document-forward-pruning.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/is_uuid.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<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(r => window.onload = () => t.step_timeout(r, 0));
+
+ i.contentWindow.location.search = "?2";
+ await new Promise(r => i.onload = () => t.step_timeout(r, 0));
+
+ i.contentWindow.location.search = "?3";
+ await new Promise(r => i.onload = () => t.step_timeout(r, 0));
+
+ i.contentWindow.history.back();
+ await new Promise(r => i.onload = () => t.step_timeout(r, 0));
+
+ i.contentWindow.history.back();
+ await new Promise(r => i.onload = () => t.step_timeout(r, 0));
+
+ i.contentWindow.location.search = "?fork";
+ await new Promise(r => i.onload = () => t.step_timeout(r, 0));
+
+ const entries = i.contentWindow.navigation.entries();
+ const searches = entries.map(e => (new URL(e.url)).search);
+ assert_array_equals(searches, ["", "?fork"]);
+
+ assert_equals(i.contentWindow.navigation.entries().at(-1), i.contentWindow.navigation.currentEntry);
+}, "navigation.entries() behavior after forward-pruning due to cross-document navs");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-javascript-url-navigation.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-javascript-url-navigation.html
new file mode 100644
index 0000000000..c5ef7f33e4
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-javascript-url-navigation.html
@@ -0,0 +1,38 @@
+<!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_func(() => {
+ let start_key = i.contentWindow.navigation.currentEntry.key;
+ let start_url = i.contentWindow.navigation.currentEntry.url;
+ let start_id = i.contentWindow.navigation.currentEntry.id;
+ let did_js_url_nav = false;
+ i.onload = t.step_func(() => {
+ if (!did_js_url_nav) {
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ before_js_nav_key = i.contentWindow.navigation.currentEntry.key;
+ before_js_nav_url = i.contentWindow.navigation.currentEntry.url;
+ before_js_nav_id = i.contentWindow.navigation.currentEntry.id;
+ i.src = "javascript:'new content'";
+ did_js_url_nav = true;
+ } else {
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ let first_entry = i.contentWindow.navigation.entries()[0];
+ let js_url_entry = i.contentWindow.navigation.entries()[1];
+ assert_equals(first_entry.key, start_key);
+ assert_equals(first_entry.url, start_url);
+ assert_equals(first_entry.id, start_id);
+
+ assert_equals(js_url_entry, i.contentWindow.navigation.currentEntry);
+ assert_equals(js_url_entry.key, before_js_nav_key);
+ assert_equals(js_url_entry.url, before_js_nav_url);
+ assert_not_equals(js_url_entry.id, before_js_nav_id);
+ t.done();
+ }
+ });
+ i.contentWindow.navigation.navigate("?1");
+ });
+}, "entries() after navigation to a javascript: URL");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-navigations-in-multiple-windows.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-navigations-in-multiple-windows.html
new file mode 100644
index 0000000000..d1d4d86eb3
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-navigations-in-multiple-windows.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/is_uuid.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+async_test(t => {
+ // The navigations in each window should have only added an navigation to
+ // their own window.
+ function assertExpectedEntries(entries, expected_url) {
+ assert_equals(entries.length, 2);
+ assert_not_equals(entries[1].key, entries[0].key);
+ assert_not_equals(entries[1].url, entries[0].url);
+ assert_not_equals(entries[1].id, entries[0].id);
+ assert_true(isUUID(entries[0].key));
+ assert_true(isUUID(entries[0].id));
+ assert_true(isUUID(entries[1].key));
+ assert_true(isUUID(entries[1].id));
+
+ assert_equals(entries[1].url, expected_url);
+ }
+
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(t.step_func(() => {
+ location.hash = "#1";
+ i.onload = t.step_func_done(() => {
+ assertExpectedEntries(navigation.entries(), location.href);
+ assertExpectedEntries(i.contentWindow.navigation.entries(), i.contentWindow.location.href);
+ });
+ i.contentWindow.location = "/common/blank.html?2";
+ }), 0);
+}, "navigation.entries() behavior when multiple windows navigate.");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-srcdoc-navigation.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-srcdoc-navigation.html
new file mode 100644
index 0000000000..b80e8aa0e2
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-after-srcdoc-navigation.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/is_uuid.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+async_test(t => {
+ window.onload = t.step_func(() => {
+ i.onload = t.step_func_done(() => {
+ let entries = i.contentWindow.navigation.entries();
+ assert_equals(entries.length, 2);
+ assert_not_equals(entries[1].key, entries[0].key);
+ assert_not_equals(entries[1].url, entries[0].url);
+ assert_equals(entries[1].url, "about:srcdoc");
+ assert_not_equals(entries[1].id, entries[0].id);
+
+ assert_true(isUUID(entries[0].key));
+ assert_true(isUUID(entries[0].id));
+ assert_true(isUUID(entries[1].key));
+ assert_true(isUUID(entries[1].id));
+ });
+ i.srcdoc = "new";
+ });
+}, "entries() after setting a srcdoc attribute");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-array-equality.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-array-equality.html
new file mode 100644
index 0000000000..98efb6b20c
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-array-equality.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(() => {
+ assert_not_equals(navigation.entries(), navigation.entries());
+}, "navigation.entries() should not return an identical object on repeated invocations");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-in-new-javascript-url-iframe.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-in-new-javascript-url-iframe.html
new file mode 100644
index 0000000000..6f217f5e3c
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-in-new-javascript-url-iframe.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="javascript:'foo'"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ assert_not_equals(i.contentWindow.navigation.currentEntry, null);
+ assert_array_equals(i.contentWindow.navigation.entries(), [i.contentWindow.navigation.currentEntry]);
+
+ assert_equals(i.contentWindow.navigation.currentEntry.url, "about:blank");
+}, "entries() and currentEntry should be set in a new javascript: URL iframe");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-in-new-srcdoc-iframe.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-in-new-srcdoc-iframe.html
new file mode 100644
index 0000000000..a7e0f88d37
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-in-new-srcdoc-iframe.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" srcdoc="new"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ assert_not_equals(i.contentWindow.navigation.currentEntry, null);
+ assert_array_equals(i.contentWindow.navigation.entries(), [i.contentWindow.navigation.currentEntry]);
+
+ assert_equals(i.contentWindow.navigation.currentEntry.url, "about:srcdoc");
+}, "entries() and currentEntry should be set in a new srcdoc iframe");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-when-inactive.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-when-inactive.html
new file mode 100644
index 0000000000..c70b6d8bf8
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/entries-when-inactive.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ const first_entry = navigation.entries()[0];
+ history.pushState(1, "", "#1");
+ assert_equals(navigation.entries()[0], first_entry);
+ history.back();
+ window.onpopstate = t.step_func_done(() => {
+ const second_entry = navigation.entries()[1];
+ history.replaceState(0, "", "#0");
+ assert_equals(navigation.entries()[1], second_entry);
+ });
+}, "A non-active entry in navigation.entries() should not be modified when a different entry is modified");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/entry-after-detach.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/entry-after-detach.html
new file mode 100644
index 0000000000..69c52d1409
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/entry-after-detach.html
@@ -0,0 +1,20 @@
+<!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_func_done(() => {
+ let i_entry = i.contentWindow.navigation.currentEntry;
+ assert_true(i_entry.sameDocument);
+ assert_not_equals(i_entry.url, null);
+ assert_not_equals(i_entry.key, "");
+ assert_not_equals(i_entry.id, "");
+ i.remove();
+ assert_false(i_entry.sameDocument);
+ assert_equals(i_entry.url, null);
+ assert_equals(i_entry.key, "");
+ assert_equals(i_entry.id, "");
+ });
+}, "NavigationHistoryEntry properties after detach");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/index-not-in-entries.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/index-not-in-entries.html
new file mode 100644
index 0000000000..a16d130ba1
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/index-not-in-entries.html
@@ -0,0 +1,24 @@
+<!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 => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(t.step_func_done(() => {
+ // Remove the entry by replacing it.
+ let replaced_entry = navigation.currentEntry;
+ assert_equals(replaced_entry.index, 0);
+ navigation.navigate("#0", { history: "replace" });
+ assert_equals(replaced_entry.index, -1);
+
+ // Remove the entry by detaching its window.
+ let iframe_entry = i.contentWindow.navigation.currentEntry;
+ assert_equals(iframe_entry.index, 0);
+ i.remove();
+ assert_equals(iframe_entry.index, -1);
+ t.done();
+ }), 0);
+}, "entry.index should return -1 when not in navigation.entries()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-back-cross-document.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-back-cross-document.html
new file mode 100644
index 0000000000..2dd58c03e9
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-back-cross-document.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="resources/key-navigate-back-cross-document-helper.html"></iframe>
+<script>
+async_test(t => {
+ window.finish = t.step_func_done((end_key, end_id) => {
+ assert_equals(window.start_key, end_key);
+ assert_equals(window.start_id, end_id);
+ // The new history entry in the iframe should not add any entries to
+ // this window's navigation.
+ assert_equals(navigation.entries().length, 1);
+ });
+}, "NavigationHistoryEntry's key and id on cross-document back navigation");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-back-same-document.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-back-same-document.html
new file mode 100644
index 0000000000..858b5fd2c8
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-back-same-document.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ let key = navigation.currentEntry.key;
+ let id = navigation.currentEntry.id;
+ assert_equals(navigation.entries().length, 1);
+
+ history.pushState("hash", "", "#hash");
+ assert_not_equals(key, navigation.currentEntry.key);
+ assert_not_equals(id, navigation.currentEntry.id);
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry.index, 1);
+
+ window.onpopstate = t.step_func_done(() => {
+ assert_equals(key, navigation.currentEntry.key);
+ assert_equals(id, navigation.currentEntry.id);
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry.index, 0);
+ });
+ history.back();
+}, "NavigationHistoryEntry's key and id on same-document back navigation");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-location-reload-intercept.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-location-reload-intercept.html
new file mode 100644
index 0000000000..62ce097439
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-location-reload-intercept.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+promise_test(async t => {
+ await new Promise(resolve => { window.onload = resolve; });
+
+
+ const original = i.contentWindow.navigation.currentEntry;
+ const { key, id } = original;
+
+ i.contentWindow.navigation.addEventListener("navigate", e => e.intercept());
+
+ i.onload = t.unreached_func("the iframe must not reload");
+
+ await i.contentWindow.location.reload();
+
+ assert_equals(i.contentWindow.navigation.currentEntry, original);
+ assert_equals(i.contentWindow.navigation.currentEntry.key, key);
+ assert_equals(i.contentWindow.navigation.currentEntry.id, id);
+}, "NavigationHistoryEntry's key and id after location.reload() intercepted by intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-location-reload.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-location-reload.html
new file mode 100644
index 0000000000..f950e2f3a2
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-location-reload.html
@@ -0,0 +1,18 @@
+<!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_func(() => {
+ let key = i.contentWindow.navigation.currentEntry.key;
+ let id = i.contentWindow.navigation.currentEntry.id;
+ i.contentWindow.location.reload();
+ i.onload = t.step_func_done(() => {
+ assert_equals(key, i.contentWindow.navigation.currentEntry.key);
+ assert_equals(id, i.contentWindow.navigation.currentEntry.id);
+ assert_equals(navigation.entries().length, 1);
+ });
+ });
+}, "NavigationHistoryEntry's key and id after location.reload()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-location-replace-cross-origin.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-location-replace-cross-origin.html
new file mode 100644
index 0000000000..65aff4a3ab
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-location-replace-cross-origin.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+async_test(t => {
+ window.onload = t.step_func(() => {
+ let key_before_replace = i.contentWindow.navigation.currentEntry.key;
+ window.onmessage = t.step_func_done(e => assert_not_equals(key_before_replace, e.data));
+
+ let cross_origin_url = new URL("resources/post-key-to-top.html", location.href);
+ cross_origin_url.hostname = get_host_info().REMOTE_HOST;
+ i.contentWindow.location.replace(cross_origin_url.href);
+ });
+}, "NavigationHistoryEntry's key and id after location.replace()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-location-replace.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-location-replace.html
new file mode 100644
index 0000000000..a58772a7f4
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/key-id-location-replace.html
@@ -0,0 +1,18 @@
+<!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_func(() => {
+ let key = i.contentWindow.navigation.currentEntry.key;
+ let id = i.contentWindow.navigation.currentEntry.id;
+ i.contentWindow.location.replace("/common/blank.html?query");
+ i.onload = t.step_func_done(() => {
+ assert_equals(key, i.contentWindow.navigation.currentEntry.key);
+ assert_not_equals(id, i.contentWindow.navigation.currentEntry.id);
+ assert_equals(navigation.entries().length, 1);
+ });
+ });
+}, "NavigationHistoryEntry's key and id after location.replace()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/no-referrer-dynamic-url-censored.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/no-referrer-dynamic-url-censored.html
new file mode 100644
index 0000000000..7a5544c419
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/no-referrer-dynamic-url-censored.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<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(r => window.onload = () => t.step_timeout(r, 0));
+
+ // The entry for the first document has a visible url.
+ i.contentWindow.navigation.navigate("/common/blank.html?2");
+ await new Promise(r => i.onload = () => t.step_timeout(r, 0));
+ assert_not_equals(i.contentWindow.navigation.entries()[0].url, null);
+
+ // Apply no-referrer, the url should now be censored when no longer on that document.
+ i.contentWindow.navigation.back();
+ await new Promise(r => i.onload = () => t.step_timeout(r, 0));
+ i.contentDocument.head.innerHTML = `<meta name="referrer" content="no-referrer">`;
+ assert_not_equals(i.contentWindow.navigation.entries()[0].url, null);
+ i.contentWindow.navigation.forward();
+ await new Promise(r => i.onload = () => t.step_timeout(r, 0));
+ assert_equals(i.contentWindow.navigation.entries()[0].url, null);
+
+ // Overwrite the referrer policy, the url should be visible again.
+ i.contentWindow.navigation.back();
+ await new Promise(r => i.onload = () => t.step_timeout(r, 0));
+ i.contentDocument.head.innerHTML = `<meta name="referrer" content="same-origin">`;
+ i.contentWindow.navigation.forward();
+ await new Promise(r => i.onload = () => t.step_timeout(r, 0));
+ assert_not_equals(i.contentWindow.navigation.entries()[0].url, null);
+}, "The url of a document is censored by a no-referrer policy dynamically");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/no-referrer-from-meta-url-censored.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/no-referrer-from-meta-url-censored.html
new file mode 100644
index 0000000000..fc563f509e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/no-referrer-from-meta-url-censored.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="resources/no-referrer-meta.html"></iframe>
+<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(r => window.onload = () => t.step_timeout(r, 0));
+
+ await i.contentWindow.navigation.navigate("#hash");
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+
+ // The entries for no-referrer.html should have the url censored.
+ i.contentWindow.navigation.navigate("/common/blank.html");
+ await new Promise(r => i.onload = () => t.step_timeout(r, 0));
+ assert_equals(i.contentWindow.navigation.entries().length, 3);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 2);
+ assert_equals(i.contentWindow.navigation.entries()[0].url, null);
+ assert_equals(i.contentWindow.navigation.entries()[1].url, null);
+
+ // Navigating back to no-referrer.html should uncensor the urls.
+ i.contentWindow.navigation.back();
+ await new Promise(r => i.onload = () => t.step_timeout(r, 0));
+ assert_equals(i.contentWindow.navigation.entries().length, 3);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+ assert_equals(new URL(i.contentWindow.navigation.entries()[0].url).pathname,
+ "/navigation-api/navigation-history-entry/resources/no-referrer-meta.html");
+ assert_equals(new URL(i.contentWindow.navigation.entries()[1].url).pathname,
+ "/navigation-api/navigation-history-entry/resources/no-referrer-meta.html");
+}, "The url of a document with no-referrer referrer meta tag is censored in NavigationHistoryEntry");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/no-referrer-url-censored.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/no-referrer-url-censored.html
new file mode 100644
index 0000000000..e7eb1afc7d
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/no-referrer-url-censored.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="resources/no-referrer.html"></iframe>
+<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(r => window.onload = () => t.step_timeout(r, 0));
+
+ await i.contentWindow.navigation.navigate("#hash");
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+
+ // The entries for no-referrer.html should have the url censored.
+ i.contentWindow.navigation.navigate("/common/blank.html");
+ await new Promise(r => i.onload = () => t.step_timeout(r, 0));
+ assert_equals(i.contentWindow.navigation.entries().length, 3);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 2);
+ assert_equals(i.contentWindow.navigation.entries()[0].url, null);
+ assert_equals(i.contentWindow.navigation.entries()[1].url, null);
+
+ // Navigating back to no-referrer.html should uncensor the urls.
+ i.contentWindow.navigation.back();
+ await new Promise(r => i.onload = () => t.step_timeout(r, 0));
+ assert_equals(i.contentWindow.navigation.entries().length, 3);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+ assert_equals(new URL(i.contentWindow.navigation.entries()[0].url).pathname,
+ "/navigation-api/navigation-history-entry/resources/no-referrer.html");
+ assert_equals(new URL(i.contentWindow.navigation.entries()[1].url).pathname,
+ "/navigation-api/navigation-history-entry/resources/no-referrer.html");
+}, "The url of a document with no-referrer referrer policy is censored in NavigationHistoryEntry");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/opaque-origin-data-url.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/opaque-origin-data-url.html
new file mode 100644
index 0000000000..65123fa60b
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/opaque-origin-data-url.html
@@ -0,0 +1,17 @@
+<!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_func(() => {
+ window.onmessage = t.step_func_done(e => {
+ assert_equals(e.data.length, 0);
+ assert_true(e.data.currentIsNull);
+ });
+ i.src = "data:text/html,<script>top.postMessage({ length: navigation.entries().length, " +
+ "currentIsNull: navigation.currentEntry === null}, '*')</sc" +
+ "ript>";
+ });
+}, "entries() and currentEntry after navigation to a data: URL (which has an opaque origin)");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/opaque-origin.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/opaque-origin.html
new file mode 100644
index 0000000000..898ca27e4f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/opaque-origin.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe id="i" sandbox="allow-scripts" src="resources/opaque-origin-page.html"></iframe>
+
+<script>
+fetch_tests_from_window(i.contentWindow);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/is_uuid.js b/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/is_uuid.js
new file mode 100644
index 0000000000..3b855c01b0
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/is_uuid.js
@@ -0,0 +1,3 @@
+function isUUID(key) {
+ return /[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}/.test(key);
+}
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/key-navigate-back-cross-document-helper.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/key-navigate-back-cross-document-helper.html
new file mode 100644
index 0000000000..79f3c3da0a
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/key-navigate-back-cross-document-helper.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script>
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+window.onload = () => step_timeout(() => {
+ if (location.search == "?go-back") {
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry.index, 1);
+ // Step 2: Navigate back.
+ history.back();
+ return;
+ }
+ if (top.start_key) {
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry.index, 0);
+ // Step 3: Notify parent, which will ensure the same key is used after back navigation.
+ top.finish(navigation.currentEntry.key, navigation.currentEntry.id);
+ return;
+ }
+ // Step 1: Record initial key and navigate.
+ assert_equals(navigation.entries().length, 1);
+ top.start_key = navigation.currentEntry.key;
+ top.start_id = navigation.currentEntry.id;
+ location.search = "go-back";
+}, 0);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/no-referrer-meta.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/no-referrer-meta.html
new file mode 100644
index 0000000000..bd5ec391cc
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/no-referrer-meta.html
@@ -0,0 +1,2 @@
+<meta name="referrer" content="no-referrer">
+<body></body>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/no-referrer.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/no-referrer.html
new file mode 100644
index 0000000000..c8b7661f42
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/no-referrer.html
@@ -0,0 +1 @@
+<body></body>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/no-referrer.html.headers b/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/no-referrer.html.headers
new file mode 100644
index 0000000000..7ffbf17d6b
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/no-referrer.html.headers
@@ -0,0 +1 @@
+Referrer-Policy: no-referrer
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/opaque-origin-page.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/opaque-origin-page.html
new file mode 100644
index 0000000000..98e2c1b317
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/opaque-origin-page.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<!-- Put this page in a sandbox to give it an opaque origin -->
+
+<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";
+ await new Promise(resolve => window.onhashchange = resolve);
+ location.hash = "#2";
+ await new Promise(resolve => window.onhashchange = resolve);
+ history.back();
+ await new Promise(resolve => window.onhashchange = resolve);
+
+ assert_equals(location.hash, "#1");
+
+ assert_equals(navigation.currentEntry, null);
+ assert_equals(navigation.entries().length, 0);
+ assert_false(navigation.canGoBack);
+ assert_false(navigation.canGoForward);
+}, "navigation.currentEntry/entries()/canGoBack/canGoForward in an opaque origin iframe");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/post-entries-length-to-top.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/post-entries-length-to-top.html
new file mode 100644
index 0000000000..c8fe005d8e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/post-entries-length-to-top.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script>
+// Wait for after the load event so that the navigation doesn't get converted
+// into a replace navigation.
+window.onload = () => step_timeout(() => top.postMessage(navigation.entries().length, "*"), 0);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/post-key-to-top.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/post-key-to-top.html
new file mode 100644
index 0000000000..285f345dc1
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/resources/post-key-to-top.html
@@ -0,0 +1,3 @@
+<script>
+top.postMessage(navigation.currentEntry.key, "*");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/sameDocument-after-fragment-navigate.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/sameDocument-after-fragment-navigate.html
new file mode 100644
index 0000000000..a197f825d8
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/sameDocument-after-fragment-navigate.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(t.step_func_done(() => {
+ let entry1 = navigation.currentEntry;
+ assert_true(entry1.sameDocument);
+
+ location = "#hash";
+ let entry2 = navigation.currentEntry;
+ assert_not_equals(entry1, entry2);
+ assert_true(entry1.sameDocument);
+
+ history.pushState("push", "", "#push");
+ let entry3 = navigation.currentEntry;
+ assert_not_equals(entry1, entry3);
+ assert_not_equals(entry2, entry3);
+ assert_true(entry1.sameDocument);
+ assert_true(entry2.sameDocument);
+
+ assert_equals(navigation.entries().length, 3);
+ assert_equals(navigation.entries()[0], entry1);
+ assert_equals(navigation.entries()[1], entry2);
+ assert_equals(navigation.entries()[2], entry3);
+ }), 0);
+}, "entry.sameDocument after same-document navigations");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/sameDocument-after-navigate-restore.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/sameDocument-after-navigate-restore.html
new file mode 100644
index 0000000000..fd21bc222f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/sameDocument-after-navigate-restore.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+ await i.contentWindow.navigation.navigate("#foo");
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_true(i.contentWindow.navigation.entries()[0].sameDocument);
+
+ i.contentWindow.navigation.navigate("/common/blank.html?bar");
+ await new Promise(resolve => i.onload = resolve);
+ assert_equals(i.contentWindow.navigation.entries().length, 3);
+ assert_false(i.contentWindow.navigation.entries()[0].sameDocument);
+ assert_false(i.contentWindow.navigation.entries()[1].sameDocument);
+
+ i.contentWindow.navigation.back();
+ await new Promise(resolve => i.onload = resolve);
+ assert_equals(i.contentWindow.navigation.entries().length, 3);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+ assert_true(i.contentWindow.navigation.entries()[0].sameDocument);
+ assert_false(i.contentWindow.navigation.entries()[2].sameDocument);
+}, "entry.sameDocument is properly restored after cross-document back");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/sameDocument-after-navigate.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/sameDocument-after-navigate.html
new file mode 100644
index 0000000000..bfcb7c6599
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/sameDocument-after-navigate.html
@@ -0,0 +1,14 @@
+<!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_func(() => {
+ let i_entry = i.contentWindow.navigation.currentEntry;
+ assert_true(i_entry.sameDocument);
+ i.onload = t.step_func_done(() => assert_false(i_entry.sameDocument));
+ i.contentWindow.location = "about:blank";
+ });
+}, "entry.sameDocument after cross-document navigation");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-history-entry/state-after-navigate-restore.html b/testing/web-platform/tests/navigation-api/navigation-history-entry/state-after-navigate-restore.html
new file mode 100644
index 0000000000..097b1d5079
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-history-entry/state-after-navigate-restore.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+ await i.contentWindow.navigation.navigate("#start", { history: "replace", state: "someState" });
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ assert_equals(i.contentWindow.navigation.entries()[0].getState(), "someState");
+
+ await i.contentWindow.navigation.navigate("#foo");
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.entries()[0].getState(), "someState");
+
+ i.contentWindow.navigation.navigate("/common/blank.html?bar");
+ await new Promise(resolve => i.onload = resolve);
+ assert_equals(i.contentWindow.navigation.entries().length, 3);
+ assert_equals(i.contentWindow.navigation.entries()[0].getState(), "someState");
+
+ i.contentWindow.navigation.back();
+ await new Promise(resolve => i.onload = resolve);
+ assert_equals(i.contentWindow.navigation.entries().length, 3);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+ assert_equals(i.contentWindow.navigation.entries()[0].getState(), "someState");
+}, "entry.getState() is properly restored after cross-document back");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/back-forward-multiple-frames.html b/testing/web-platform/tests/navigation-api/navigation-methods/back-forward-multiple-frames.html
new file mode 100644
index 0000000000..738bfd37dc
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/back-forward-multiple-frames.html
@@ -0,0 +1,74 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<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));
+ // Step 1
+ assert_equals(navigation.entries().length, 1, "step 1 outer entries() length");
+ assert_equals(i.contentWindow.navigation.entries().length, 1, "step 1 iframe entries() length");
+ await navigation.navigate("#top").committed;
+ // Step 2: iframe at initial entry, top on second entry
+ assert_equals(navigation.entries().length, 2, "step 2 outer entries() length");
+ assert_equals(i.contentWindow.navigation.entries().length, 1, "step 2 iframe entries() length");
+ await i.contentWindow.navigation.navigate("#iframe").committed;
+
+ // Step 3: Both windows on second entry.
+ assert_equals(navigation.entries().length, 2, "step 3 outer entries() length");
+ assert_equals(i.contentWindow.navigation.entries().length, 2, "step 3 iframe entries() length");
+ assert_equals(navigation.currentEntry.index, 1, "step 3 outer index");
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1, "step 1 iframe index");
+
+ // NOTE: the order of navigation in the two windows is not guaranteed; we need to wait for both.
+
+ // Going back in the iframe should go 3->2 (navigating iframe only)
+ await Promise.all([
+ i.contentWindow.navigation.back().committed,
+ new Promise(resolve => i.contentWindow.onpopstate = resolve)
+ ]);
+ assert_equals(navigation.currentEntry.index, 1, "after iframe back() outer index");
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 0, "after iframe back() iframe index");
+
+ // Going forward in iframe should go 2->3
+ await Promise.all([
+ i.contentWindow.navigation.forward().commited,
+ new Promise(resolve => i.contentWindow.onpopstate = resolve)
+ ]);
+ assert_equals(navigation.currentEntry.index, 1, "after iframe forward() outer index");
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1, "after iframe forward() iframe index");
+
+ // Going back in top should go 3->1 (navigating both windows).
+ await Promise.all([
+ navigation.back().commited,
+ new Promise(resolve => i.contentWindow.onpopstate = resolve)
+ ]);
+ assert_equals(navigation.currentEntry.index, 0, "after outer back() outer index");
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 0, "after outer back() iframe index");
+
+ // Next two should not navigate the iframe
+ i.contentWindow.onpopstate = t.unreached_func("popstate must not be called");
+
+ // Going forward in top should go 1->2 (navigating top only)
+ await navigation.forward().committed;
+ await new Promise(resolve => t.step_timeout(resolve, 0));
+ assert_equals(navigation.currentEntry.index, 1, "after outer forward() outer index");
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 0, "after outer forward() iframe index");
+
+ // Going back in top should go 2->1
+ await navigation.back().committed;
+ await new Promise(resolve => t.step_timeout(resolve, 0));
+ assert_equals(navigation.currentEntry.index, 0, "after outer second back() outer index");
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 0, "after outer second back() iframe index");
+
+ // Going forward in iframe should go 1->3 (navigating both windows)
+ await Promise.all([
+ i.contentWindow.navigation.forward().commited,
+ new Promise(resolve => i.contentWindow.onpopstate = resolve)
+ ]);
+ assert_equals(navigation.currentEntry.index, 1, "after iframe second forward() outer index");
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1, "after iframe second forward() iframe index");
+}, "navigation.back() and navigation.forward() can navigate multiple frames");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/disambigaute-back.html b/testing/web-platform/tests/navigation-api/navigation-methods/disambigaute-back.html
new file mode 100644
index 0000000000..d44d435896
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/disambigaute-back.html
@@ -0,0 +1,32 @@
+<!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 => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(() => {
+ assert_equals(navigation.entries().length, 1);
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ navigation.navigate("#top");
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ i.contentWindow.navigation.navigate("#1");
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry.index, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+ assert_true(navigation.canGoBack);
+ assert_true(i.contentWindow.navigation.canGoBack);
+
+ // There are 2 joint session history entries containing the iframe's
+ // previous key. Navigate to the nearest one (which navigates the iframe
+ // but not the top window).
+ i.contentWindow.navigation.back().committed.then(t.step_func_done(() => {
+ assert_equals(navigation.currentEntry.index, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 0);
+ }));
+ }, 0);
+}, "navigation.back() goes to the nearest back entry");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/disambigaute-forward.html b/testing/web-platform/tests/navigation-api/navigation-methods/disambigaute-forward.html
new file mode 100644
index 0000000000..e252e30994
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/disambigaute-forward.html
@@ -0,0 +1,40 @@
+<!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 => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(() => {
+ assert_equals(navigation.entries().length, 1);
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ navigation.navigate("#top");
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ i.contentWindow.navigation.navigate("#1");
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry.index, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+ assert_true(navigation.canGoBack);
+ assert_true(i.contentWindow.navigation.canGoBack);
+
+ i.contentWindow.navigation.back().committed.then(t.step_func(() => {
+ assert_equals(navigation.currentEntry.index, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 0);
+ navigation.back().committed.then(t.step_func(() => {
+ assert_equals(navigation.currentEntry.index, 0);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 0);
+ // There are 2 joint session history entries containing the top window's
+ // final key. Navigate to the nearest one (which navigates only the
+ // top window).
+ navigation.forward().committed.then(t.step_func_done(() => {
+ assert_equals(navigation.currentEntry.index, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 0);
+ }));
+ }));
+ }));
+ }, 0);
+}, "navigation.forward() goes to the nearest forward entry");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/disambigaute-traverseTo-back-multiple.html b/testing/web-platform/tests/navigation-api/navigation-methods/disambigaute-traverseTo-back-multiple.html
new file mode 100644
index 0000000000..03a8eb10ed
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/disambigaute-traverseTo-back-multiple.html
@@ -0,0 +1,35 @@
+<!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 => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(() => {
+ assert_equals(navigation.entries().length, 1);
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ navigation.navigate("#top");
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+
+ let iframe_initial_key = i.contentWindow.navigation.currentEntry.key;
+ i.contentWindow.navigation.navigate("#1");
+ i.contentWindow.navigation.navigate("#2");
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.entries().length, 3);
+ assert_equals(navigation.currentEntry.index, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 2);
+ assert_true(navigation.canGoBack);
+ assert_true(i.contentWindow.navigation.canGoBack);
+
+ // There are 2 joint session history entries containing the iframe's
+ // initial key. Navigate to the nearest one (which navigates the iframe
+ // but not the top window).
+ i.contentWindow.navigation.traverseTo(iframe_initial_key).committed.then(t.step_func_done(() => {
+ assert_equals(navigation.currentEntry.index, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 0);
+ }));
+ }, 0);
+}, "navigation.traverseTo() goes to the nearest entry when going back");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/disambigaute-traverseTo-forward-multiple.html b/testing/web-platform/tests/navigation-api/navigation-methods/disambigaute-traverseTo-forward-multiple.html
new file mode 100644
index 0000000000..f8e78c4b1d
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/disambigaute-traverseTo-forward-multiple.html
@@ -0,0 +1,39 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<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));
+ assert_equals(navigation.entries().length, 1);
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ let initial_key = navigation.currentEntry.key;
+ await navigation.navigate("#top1").committed;
+ await navigation.navigate("#top2").committed;
+ assert_equals(navigation.entries().length, 3);
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ await i.contentWindow.navigation.navigate("#1").committed;
+ assert_equals(navigation.entries().length, 3);
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry.index, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+ assert_true(navigation.canGoBack);
+ assert_true(i.contentWindow.navigation.canGoBack);
+ let final_key = navigation.currentEntry.key;
+
+ await i.contentWindow.navigation.back().committed;
+ assert_equals(navigation.currentEntry.index, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 0);
+ await navigation.traverseTo(initial_key).committed;
+ assert_equals(navigation.currentEntry.index, 0);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 0);
+ // There are 2 joint session history entries containing the top window's
+ // final key. Navigate to the nearest one (which navigates only the
+ // top window).
+ await navigation.traverseTo(final_key).committed;
+ assert_equals(navigation.currentEntry.index, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 0);
+}, "navigation.traverseTo() goes to the nearest entry when going forward");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/forward-to-pruned-entry.html b/testing/web-platform/tests/navigation-api/navigation-methods/forward-to-pruned-entry.html
new file mode 100644
index 0000000000..05f70c5c2d
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/forward-to-pruned-entry.html
@@ -0,0 +1,33 @@
+<!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));
+ await navigation.navigate("#foo").finished;
+ assert_equals(navigation.entries().length, 2);
+ await navigation.back().finished;
+ assert_equals(navigation.currentEntry.index, 0);
+
+ // Traverse forward then immediately do a same-document push. This will
+ // truncate the back forward list, and by the time the traverse commits, the
+ // destination key will no longer be present in navigation.entries(). The
+ // traverse should abort. Because the traverse aborts before the navigate
+ // event fires, the navigateerror event should not fire.
+ navigation.onnavigateerror = t.unreached_func("navigateerror should not fire");
+ let forward_value = navigation.forward();
+ await navigation.navigate("#clobber").finished;
+ assert_equals(navigation.currentEntry.index, 1);
+ await promise_rejects_dom(t, "AbortError", forward_value.committed);
+ await promise_rejects_dom(t, "AbortError", forward_value.finished);
+
+ // This leaves navigation.entries() in a consistent state where traversing
+ // back and forward still works.
+ await navigation.back().finished;
+ assert_equals(navigation.currentEntry.index, 0);
+ await navigation.forward().finished;
+ assert_equals(navigation.currentEntry.index, 1);
+}, "If forward pruning clobbers the target of a traverse, abort");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/navigate-base-url.html b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-base-url.html
new file mode 100644
index 0000000000..00fefa6608
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-base-url.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="resources/page-with-base-url-common.html"></iframe>
+<script>
+async_test(t => {
+ window.onload = t.step_func(() => {
+ i.contentWindow.navigation.navigate("blank.html");
+ i.onload = t.step_func_done(() => {
+ const iframeURL = new URL(i.contentWindow.navigation.currentEntry.url);
+ assert_equals(iframeURL.pathname, "/common/blank.html");
+ });
+ });
+}, "navigate() must resolve URLs relative to navigation object's base URL");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/navigate-from-initial-about-blank-gc.html b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-from-initial-about-blank-gc.html
new file mode 100644
index 0000000000..d35121ec81
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-from-initial-about-blank-gc.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/gc.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<!--
+ Regression test for https://bugs.chromium.org/p/chromium/issues/detail?id=1289864.
+-->
+
+<script>
+promise_test(t => {
+ i.contentWindow.navigation.navigate("/common/blank.html?1");
+
+ return garbageCollect();
+}, `navigate() from <iframe> with src="" but still on initial about:blank doesn't cause a crash on GC`);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/navigate-from-initial-about-blank-src.html b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-from-initial-about-blank-src.html
new file mode 100644
index 0000000000..8e54943669
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-from-initial-about-blank-src.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<!--
+ Sort of a regression test for https://bugs.chromium.org/p/chromium/issues/detail?id=1289864,
+ but since that is GC-dependent this will probably not fail in codebases that exhibit that bug.
+ So it's really just adding some extra general coverage for navigation.navigate().
+-->
+
+<script>
+async_test(t => {
+ i.contentWindow.navigation.navigate("/common/blank.html?1");
+ i.onload = t.step_func_done(() => {
+ const iframeURL = new URL(i.contentWindow.navigation.currentEntry.url);
+ assert_equals(iframeURL.pathname, "/common/blank.html");
+ assert_equals(iframeURL.search, "?1");
+ });
+}, `navigate() from <iframe> with src="" but still on initial about:blank works`);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/navigate-from-initial-about-blank.html b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-from-initial-about-blank.html
new file mode 100644
index 0000000000..00b2216166
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-from-initial-about-blank.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i"></iframe>
+
+<!--
+ Sort of a regression test for https://bugs.chromium.org/p/chromium/issues/detail?id=1289864,
+ but since that is GC-dependent this will probably not fail in codebases that exhibit that bug.
+ So it's really just adding some extra general coverage for navigation.navigate().
+-->
+
+
+<script>
+async_test(t => {
+ i.contentWindow.navigation.navigate("/common/blank.html?1");
+ i.onload = t.step_func_done(() => {
+ const iframeURL = new URL(i.contentWindow.navigation.currentEntry.url);
+ assert_equals(iframeURL.pathname, "/common/blank.html");
+ assert_equals(iframeURL.search, "?1");
+ });
+}, `navigate() from <iframe> still on initial about:blank works`);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-push-not-loaded.html b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-push-not-loaded.html
new file mode 100644
index 0000000000..c1ecdaeeaf
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-push-not-loaded.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+promise_test(async t => {
+ // Purposefully do not wait until after the load event (unlike some sibling tests).
+ assert_equals(navigation.entries().length, 1);
+ assert_equals(document.readyState, "loading", "Document must not have loaded yet");
+
+ let navigateEventType;
+ navigation.onnavigate = e => navigateEventType = e.navigationType;
+
+ await navigation.navigate("#1", { history: "push" }).finished;
+ assert_equals(navigateEventType, "push");
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry.index, 1);
+}, "navigate() with history: 'push' in a document that has not yet had its load event");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-push-same-url-cross-document.html b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-push-same-url-cross-document.html
new file mode 100644
index 0000000000..2d07bdcede
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-push-same-url-cross-document.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+promise_test(async t => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation due to onload not having completed.
+ await new Promise(resolve => window.onload = () => t.step_timeout(resolve, 0));
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ let navigateEventType;
+ i.contentWindow.navigation.onnavigate = e => navigateEventType = e.navigationType;
+
+ i.contentWindow.navigation.navigate(i.contentWindow.location, { history: "push" });
+ await new Promise(resolve => i.onload = resolve);
+ assert_equals(navigateEventType, "push");
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+}, "navigate() to the current URL with history: 'push' and allow it to go cross document");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-push-same-url.html b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-push-same-url.html
new file mode 100644
index 0000000000..339e8c3056
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-push-same-url.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.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 due to onload not having completed.
+ await new Promise(resolve => window.onload = () => t.step_timeout(resolve, 0));
+ assert_equals(navigation.entries().length, 1);
+
+
+ let navigateEventType;
+ navigation.onnavigate = e => {
+ navigateEventType = e.navigationType;
+ e.intercept();
+ }
+ navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ await navigation.navigate(location.href, { history: "push" }).finished;
+ assert_equals(navigateEventType, "push");
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry.index, 1);
+}, "navigate() to the current URL with history: 'push' and intercept so it remains same-document");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-state-replace.html b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-state-replace.html
new file mode 100644
index 0000000000..bf0af521da
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-state-replace.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(async () => {
+ history.replaceState("state1", "", "#1");
+ assert_equals(history.state, "state1");
+
+ let onnavigate_called = false;
+ navigation.onnavigate = () => onnavigate_called = true;
+ await navigation.navigate("#2", { history: "replace" }).committed;
+ assert_equals(location.hash, "#2");
+ assert_true(onnavigate_called);
+ assert_equals(history.state, null);
+}, "history.state should be nulled by navigate()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-state.html b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-state.html
new file mode 100644
index 0000000000..0541636de5
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-history-state.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(async () => {
+ history.replaceState("state1", "", "#1");
+ assert_equals(history.state, "state1");
+
+ let onnavigate_called = false;
+ navigation.onnavigate = () => onnavigate_called = true;
+ await navigation.navigate("#2").committed;
+ assert_equals(location.hash, "#2");
+ assert_true(onnavigate_called);
+ assert_equals(history.state, null);
+}, "history.state should be nulled by navigate()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/navigate-info-and-state.html b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-info-and-state.html
new file mode 100644
index 0000000000..828d7daea5
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-info-and-state.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(async t => {
+ let navInfo = { nav : "info" };
+ let navState = { statevar: "state" };
+ let onnavigated_called = false;
+ navigation.onnavigate = t.step_func(e => {
+ onnavigated_called = true;
+ assert_equals(e.info, navInfo)
+ });
+ await navigation.navigate("#1", { info: navInfo, state: navState }).committed;
+ assert_true(onnavigated_called);
+ assert_not_equals(navigation.currentEntry.getState(), navState);
+ assert_equals(navigation.currentEntry.getState().statevar, navState.statevar);
+}, "navigate() with info and state");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/navigate-intercept-history-state.html b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-intercept-history-state.html
new file mode 100644
index 0000000000..7faf6ffb5a
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-intercept-history-state.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(async () => {
+ history.replaceState("state1", "", "#1");
+ assert_equals(history.state, "state1");
+
+ navigation.onnavigate = e => e.intercept({ handler: () => Promise.resolve("r") });
+ await navigation.navigate("#2").committed;
+ assert_equals(location.hash, "#2");
+ assert_equals(history.state, null);
+}, "history.story should be nulled by navigate() handled by intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/navigate-relative-url.html b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-relative-url.html
new file mode 100644
index 0000000000..cc95d5e003
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-relative-url.html
@@ -0,0 +1,16 @@
+<!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_func(() => {
+ i.contentWindow.navigation.navigate("?1");
+ i.onload = t.step_func_done(() => {
+ let iframe_url = new URL(i.contentWindow.navigation.currentEntry.url);
+ assert_equals(iframe_url.search, "?1");
+ assert_equals(iframe_url.pathname, "/common/blank.html");
+ });
+ });
+}, "navigate() should resolve urls relative to navigation object, not the caller");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/navigate-replace-cross-document.html b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-replace-cross-document.html
new file mode 100644
index 0000000000..7f016babe6
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-replace-cross-document.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 => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation (which would defeat the point of the test).
+ window.onload = () => t.step_timeout(() => {
+ let start_history_length = history.length;
+ let start_entry_top = navigation.currentEntry;
+ let start_entry_iframe_id = i.contentWindow.navigation.currentEntry.id;
+ let start_entry_iframe_key = i.contentWindow.navigation.currentEntry.key;
+
+ i.contentWindow.navigation.navigate("?1", { history: "replace" });
+ i.onload = t.step_func_done(() => {
+ assert_equals(history.length, start_history_length);
+
+ assert_equals(navigation.entries().length, 1);
+ assert_equals(navigation.currentEntry, start_entry_top);
+
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ let iframe_entry = i.contentWindow.navigation.currentEntry;
+ assert_not_equals(start_entry_iframe_id, iframe_entry.id);
+ assert_equals(start_entry_iframe_key, iframe_entry.key);
+ });
+ }, 0);
+}, "navigate() with history: 'replace' option");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/navigate-replace-same-document.html b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-replace-same-document.html
new file mode 100644
index 0000000000..0d8493b788
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-replace-same-document.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(t.step_func_done(async () => {
+ let start_history_length = history.length;
+ let key1 = navigation.currentEntry.key;
+ await navigation.navigate("#1").committed;
+ let key2 = navigation.currentEntry.key;
+ assert_not_equals(key1, key2);
+ await navigation.navigate("#2", { history: "replace" }).committed;
+ let key3 = navigation.currentEntry.key;
+ assert_equals(key2, key3);
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(history.length, start_history_length + 1);
+ }), 0);
+}, "navigate() with history: 'replace' option");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/navigate-same-document.html b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-same-document.html
new file mode 100644
index 0000000000..9ffd8248f8
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-same-document.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="d"></div>
+<script>
+promise_test(async () => {
+ let onnavigate_called = false;
+ navigation.onnavigate = () => onnavigate_called = true;
+ await navigation.navigate("#d").committed;
+ assert_equals(location.hash, "#d");
+ assert_true(onnavigate_called);
+ assert_equals(document.querySelector(":target"), d);
+}, "navigate() navigates same-document and fires onnavigate (async)");
+
+test(() => {
+ let onnavigate_called = false;
+ navigation.onnavigate = () => onnavigate_called = true;
+ navigation.navigate("#d");
+ assert_equals(location.hash, "#d");
+ assert_true(onnavigate_called);
+ assert_equals(document.querySelector(":target"), d);
+}, "navigate() navigates same-document and fires onnavigate (sync)");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/navigate-state-repeated-await.html b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-state-repeated-await.html
new file mode 100644
index 0000000000..539b3dc1da
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-state-repeated-await.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(async t => {
+ navigation.onnavigate = e => e.intercept();
+
+ await navigation.navigate('/foo', {state: {foo: 1}}).committed;
+ assert_equals(navigation.currentEntry.getState().foo, 1);
+ await navigation.navigate('/foo', {state: {foo: 2}}).committed;
+ assert_equals(navigation.currentEntry.getState().foo, 2);
+}, "navigate() with state should work correctly when called repeatedly - with awaits");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/navigate-state-repeated.html b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-state-repeated.html
new file mode 100644
index 0000000000..227bbb0cfc
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/navigate-state-repeated.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(async t => {
+ navigation.onnavigate = e => e.intercept();
+
+ let result1 = navigation.navigate('/foo', {state: {foo: 1}});
+ assert_equals(navigation.currentEntry.getState().foo, 1);
+
+ // Don't let the harness fail because of the "AbortError" DOMException that is caused by interrupting the navigation.
+ result1.finished.catch(() => {});
+
+ let result2 = navigation.navigate('/foo', {state: {foo: 2}});
+ assert_equals(navigation.currentEntry.getState().foo, 2);
+
+ await result1.committed;
+ await result2.committed;
+}, "navigate() with state should work correctly when called repeatedly");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/reload-base-url.html b/testing/web-platform/tests/navigation-api/navigation-methods/reload-base-url.html
new file mode 100644
index 0000000000..1e8d3b90c3
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/reload-base-url.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="resources/page-with-base-url-common.html"></iframe>
+<script>
+async_test(t => {
+ window.onload = t.step_func(() => {
+ const startingURL = new URL(i.src);
+ i.contentWindow.navigation.reload();
+ i.onload = t.step_func_done(() => {
+ const iframeURL = new URL(i.contentWindow.navigation.currentEntry.url);
+ assert_equals(iframeURL.pathname, startingURL.pathname);
+ });
+ });
+}, "reload() must ignore the Navigation object's base URL");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/reload-info.html b/testing/web-platform/tests/navigation-api/navigation-methods/reload-info.html
new file mode 100644
index 0000000000..637f6976ad
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/reload-info.html
@@ -0,0 +1,47 @@
+<!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_func(() => {
+ const navState = { key: "value" };
+ const navInfo = { infoKey: "infoValue" };
+ i.contentWindow.navigation.navigate("#1", { state: navState }).committed.then(t.step_func(() => {
+ // Make sure that state setting worked
+ assert_equals(i.contentWindow.navigation.currentEntry.getState().key, "value");
+ assert_not_equals(i.contentWindow.navigation.currentEntry.getState(), navState);
+
+ let start_url = i.contentWindow.location.href;
+ let start_key = i.contentWindow.navigation.currentEntry.key;
+ let start_id = i.contentWindow.navigation.currentEntry.id;
+ let start_state = i.contentWindow.navigation.currentEntry.getState();
+ let onnavigate_called = false;
+ let promise_settled = false;
+ i.contentWindow.navigation.onnavigate = t.step_func(e => {
+ onnavigate_called = true;
+ assert_equals(e.info, navInfo);
+ assert_equals(e.navigationType, "reload");
+ assert_equals(e.destination.getState().key, "value");
+ assert_not_equals(e.destination.getState(), start_state);
+ });
+ i.contentWindow.navigation.reload({ info: navInfo }).committed.finally(() => {
+ promise_settled = true;
+ });
+ i.onload = t.step_func(() => {
+ assert_true(onnavigate_called);
+ assert_equals(i.contentWindow.location.href, start_url);
+ assert_equals(i.contentWindow.navigation.currentEntry.key, start_key);
+ assert_equals(i.contentWindow.navigation.currentEntry.id, start_id);
+ assert_equals(i.contentWindow.navigation.currentEntry.getState().key, "value");
+ assert_not_equals(i.contentWindow.navigation.currentEntry.getState(), start_state);
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+
+ t.step_timeout(t.step_func_done(() => {
+ assert_equals(promise_settled, false);
+ }), 0);
+ });
+ }));
+ });
+}, "reload() variant with only info");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/reload-navigation-timing.html b/testing/web-platform/tests/navigation-api/navigation-methods/reload-navigation-timing.html
new file mode 100644
index 0000000000..ce03e7a7ca
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/reload-navigation-timing.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+ i.contentWindow.navigation.reload();
+ await new Promise(resolve => i.onload = resolve);
+
+ const entries = i.contentWindow.performance.getEntriesByType("navigation");
+ assert_equals(entries.length, 1);
+ assert_equals(entries[0].type, "reload");
+}, `reload() appears as a reload to navigation timing APIs`);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/reload-no-args.html b/testing/web-platform/tests/navigation-api/navigation-methods/reload-no-args.html
new file mode 100644
index 0000000000..c94eae0b93
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/reload-no-args.html
@@ -0,0 +1,46 @@
+<!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_func(() => {
+ const navState = { key: "value" };
+ i.contentWindow.navigation.navigate("#1", { state: navState }).committed.then(t.step_func(() => {
+ // Make sure that state setting worked
+ assert_equals(i.contentWindow.navigation.currentEntry.getState().key, "value");
+ assert_not_equals(i.contentWindow.navigation.currentEntry.getState(), navState);
+
+ let start_url = i.contentWindow.location.href;
+ let start_key = i.contentWindow.navigation.currentEntry.key;
+ let start_id = i.contentWindow.navigation.currentEntry.id;
+ let start_state = i.contentWindow.navigation.currentEntry.getState();
+ let onnavigate_called = false;
+ let promise_settled = false;
+ i.contentWindow.navigation.onnavigate = t.step_func(e => {
+ onnavigate_called = true;
+ assert_equals(e.info, undefined);
+ assert_equals(e.navigationType, "reload");
+ assert_equals(e.destination.getState().key, "value");
+ assert_not_equals(e.destination.getState(), start_state);
+ });
+ i.contentWindow.navigation.reload().committed.finally(() => {
+ promise_settled = true;
+ });
+ i.onload = t.step_func(() => {
+ assert_true(onnavigate_called);
+ assert_equals(i.contentWindow.location.href, start_url);
+ assert_equals(i.contentWindow.navigation.currentEntry.key, start_key);
+ assert_equals(i.contentWindow.navigation.currentEntry.id, start_id);
+ assert_equals(i.contentWindow.navigation.currentEntry.getState().key, "value");
+ assert_not_equals(i.contentWindow.navigation.currentEntry.getState(), start_state);
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+
+ t.step_timeout(t.step_func_done(() => {
+ assert_equals(promise_settled, false);
+ }), 0);
+ });
+ }));
+ });
+}, "reload() variant with no state or info");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/reload-service-worker-fetch-event.html b/testing/web-platform/tests/navigation-api/navigation-methods/reload-service-worker-fetch-event.html
new file mode 100644
index 0000000000..67535c70bf
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/reload-service-worker-fetch-event.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+
+<!-- Keep an eye on https://github.com/whatwg/fetch/issues/1280 -->
+
+<script>
+const worker = "resources/fetch-event-test-worker.js";
+
+promise_test(async t => {
+ const scope = "resources/service-worker-page.html?reload-service-worker-fetch-event";
+
+ const reg = await service_worker_unregister_and_register(t, worker, scope);
+
+ await wait_for_state(t, reg.installing, "activated");
+ const frame = await with_iframe(scope);
+ assert_equals(frame.contentDocument.body.textContent, "method = GET, isReloadNavigation = false");
+
+ frame.contentWindow.navigation.reload();
+ await new Promise(resolve => frame.addEventListener("load", resolve, { once: true }));
+ assert_equals(frame.contentDocument.body.textContent, "method = GET, isReloadNavigation = true");
+
+ frame.remove();
+ await reg.unregister();
+}, "reload() appears as a reload to service worker fetch event handlers");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/reload-state-and-info.html b/testing/web-platform/tests/navigation-api/navigation-methods/reload-state-and-info.html
new file mode 100644
index 0000000000..1ba26ae3da
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/reload-state-and-info.html
@@ -0,0 +1,40 @@
+<!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_func(() => {
+ const navState1 = { key: "value" };
+ const navState2 = { key2: "value2" };
+ const navInfo = { infoKey: "infoValue" };
+ i.contentWindow.navigation.navigate("#1", { state: navState1 }).committed.then(t.step_func(() => {
+ // Make sure that state setting worked
+ assert_equals(i.contentWindow.navigation.currentEntry.getState().key, "value", "initial state setup");
+ assert_not_equals(i.contentWindow.navigation.currentEntry.getState(), navState1);
+
+ let start_url = i.contentWindow.location.href;
+ let start_key = i.contentWindow.navigation.currentEntry.key;
+ let start_id = i.contentWindow.navigation.currentEntry.id;
+ let onnavigate_called = false;
+ let promise_settled = false;
+ i.contentWindow.navigation.onnavigate = t.step_func(e => {
+ e.intercept();
+ onnavigate_called = true;
+ assert_equals(e.info, navInfo);
+ assert_equals(e.navigationType, "reload");
+ assert_equals(e.destination.getState().key2, "value2", "navigate event for the reload()");
+ assert_not_equals(e.destination.getState(), navState2);
+ });
+ i.contentWindow.navigation.reload({ info: navInfo, state: navState2 }).committed.then(t.step_func_done(() => {
+ assert_true(onnavigate_called);
+ assert_equals(i.contentWindow.location.href, start_url);
+ assert_equals(i.contentWindow.navigation.currentEntry.key, start_key);
+ assert_equals(i.contentWindow.navigation.currentEntry.id, start_id);
+ assert_equals(i.contentWindow.navigation.currentEntry.getState().key2, "value2", "currentEntry.getState() after the reload");
+ assert_not_equals(i.contentWindow.navigation.currentEntry.getState(), navState2);
+ }));
+ }));
+ });
+}, "reload() variant with info and new state");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/reload-state-undefined.html b/testing/web-platform/tests/navigation-api/navigation-methods/reload-state-undefined.html
new file mode 100644
index 0000000000..b8b34650ab
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/reload-state-undefined.html
@@ -0,0 +1,39 @@
+<!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_func(() => {
+ const navState = { key: "value" };
+ const navInfo = { infoKey: "infoValue" };
+ i.contentWindow.navigation.navigate("#1", { state: navState }).committed.then(t.step_func(() => {
+ // Make sure that state setting worked
+ assert_equals(i.contentWindow.navigation.currentEntry.getState().key, "value");
+ assert_not_equals(i.contentWindow.navigation.currentEntry.getState(), navState);
+
+ let start_url = i.contentWindow.location.href;
+ let start_key = i.contentWindow.navigation.currentEntry.key;
+ let start_id = i.contentWindow.navigation.currentEntry.id;
+ let onnavigate_called = false;
+ let promise_settled = false;
+ i.contentWindow.navigation.onnavigate = t.step_func(e => {
+ e.intercept();
+ onnavigate_called = true;
+ assert_equals(e.info, navInfo);
+ assert_equals(e.navigationType, "reload");
+ assert_equals(e.destination.getState().key, "value", "destination.getState()");
+ assert_not_equals(e.destination.getState(), navState);
+ });
+ i.contentWindow.navigation.reload({ info: navInfo, state: undefined }).committed.then(t.step_func_done(() => {
+ assert_true(onnavigate_called);
+ assert_equals(i.contentWindow.location.href, start_url);
+ assert_equals(i.contentWindow.navigation.currentEntry.key, start_key);
+ assert_equals(i.contentWindow.navigation.currentEntry.id, start_id);
+ assert_equals(i.contentWindow.navigation.currentEntry.getState().key, "value", "destination.getState()");
+ assert_not_equals(i.contentWindow.navigation.currentEntry.getState(), navState);
+ }));
+ }));
+ });
+}, "reload() variant with info and state: undefined counts the same as not present (because of Web IDL dictionary semantics), so preserves the state");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/resources/fetch-event-test-worker.js b/testing/web-platform/tests/navigation-api/navigation-methods/resources/fetch-event-test-worker.js
new file mode 100644
index 0000000000..98b7dd0fb5
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/resources/fetch-event-test-worker.js
@@ -0,0 +1,7 @@
+self.addEventListener('fetch', function(event) {
+ const request = event.request;
+ const body =
+ `method = ${request.method}, ` +
+ `isReloadNavigation = ${request.isReloadNavigation}`;
+ event.transitionWhile(new Response(body));
+});
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/resources/navigate-parent.html b/testing/web-platform/tests/navigation-api/navigation-methods/resources/navigate-parent.html
new file mode 100644
index 0000000000..8584bb5774
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/resources/navigate-parent.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<script>
+window.navigateParent = () => {
+ return parent.navigation.navigate("#2");
+};
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/resources/navigate-sibling.html b/testing/web-platform/tests/navigation-api/navigation-methods/resources/navigate-sibling.html
new file mode 100644
index 0000000000..29a5ee7390
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/resources/navigate-sibling.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<script>
+window.navigateSibling = () => {
+ return parent.frames[0].navigation.navigate("/common/blank.html?2");
+};
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/resources/navigation-back.html b/testing/web-platform/tests/navigation-api/navigation-methods/resources/navigation-back.html
new file mode 100644
index 0000000000..a1441801ac
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/resources/navigation-back.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<script>
+window.doNavigationBack = () => {
+ return navigation.back();
+};
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/resources/page-with-base-url-common.html b/testing/web-platform/tests/navigation-api/navigation-methods/resources/page-with-base-url-common.html
new file mode 100644
index 0000000000..8d9fedcc2b
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/resources/page-with-base-url-common.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<base href="/common/">
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/resources/service-worker-page.html b/testing/web-platform/tests/navigation-api/navigation-methods/resources/service-worker-page.html
new file mode 100644
index 0000000000..a10ff35dce
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/resources/service-worker-page.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<p>We can't use /common/blank.html because of scope restrictions.
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/resources/slow-no-store.py b/testing/web-platform/tests/navigation-api/navigation-methods/resources/slow-no-store.py
new file mode 100644
index 0000000000..48e5fc0266
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/resources/slow-no-store.py
@@ -0,0 +1,6 @@
+import time
+
+def main(request, response):
+ # Sleep for 1sec
+ time.sleep(1)
+ response.headers.set(b"Cache-Control", b"no-cache, no-store, must-revalidate");
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-204-205-download.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-204-205-download.html
new file mode 100644
index 0000000000..5bedbf21e8
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-204-205-download.html
@@ -0,0 +1,52 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<script src="/common/utils.js"></script>
+
+<body>
+<script>
+const tests = [
+ ["204s", "204"],
+ ["205s", "205"],
+ ["Content-Disposition: attachment responses", "download"]
+];
+
+for (const [description, action] of tests) {
+ promise_test(async t => {
+ const id = token();
+
+ const i = document.createElement("iframe");
+ i.src = `resources/204-205-download-on-second-visit.py?id=${id}`;
+ document.body.append(i);
+ await new Promise(r => i.onload = r);
+
+ // Configure it to return a 204 on the next visit
+ await fetch(i.src + `&action=${action}`, { method: "POST" });
+
+ // Now navigate elsewhere
+ i.contentWindow.location.href = "/common/blank.html";
+ await new Promise(r => i.onload = r);
+
+ // Now try going back. It should do nothing (and not tell us about the result).
+
+ const indexBefore = i.contentWindow.navigation.currentEntry.index;
+
+ // One might be surprised that navigate does not fire. (It does fire for the
+ // corresponding tests of navigation.navigate(), i.e., this is
+ // traversal-specific behavior.) See https://github.com/WICG/navigation-api/issues/207
+ // for some discussion.
+ i.contentWindow.navigation.onnavigate = t.unreached_func("onnavigate should not be called");
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ const result = i.contentWindow.navigation.back();
+
+ assertNeverSettles(t, result, i.contentWindow);
+
+ await new Promise(resolve => t.step_timeout(resolve, 50));
+ assert_equals(i.contentWindow.navigation.currentEntry.index, indexBefore);
+ assert_equals(i.contentWindow.navigation.transition, null);
+ }, `back() promises to ${description} never settle`);
+}
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-already-detached.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-already-detached.html
new file mode 100644
index 0000000000..f9ff04f923
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-already-detached.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<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));
+
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ let key = i.contentWindow.navigation.currentEntry.key;
+
+ i.contentWindow.navigation.navigate("?1");
+ await new Promise(resolve => i.onload = resolve);
+
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry, i.contentWindow.navigation.entries()[1]);
+
+ const iWindow = i.contentWindow;
+ const iDOMException = iWindow.DOMException;
+
+ i.remove();
+
+ await assertBothRejectDOM(t, iWindow.navigation.back(), "InvalidStateError", iWindow, iDOMException);
+}, "back() in a detached window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-beforeunload.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-beforeunload.html
new file mode 100644
index 0000000000..82c1f589cc
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-beforeunload.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<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));
+
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ let key = i.contentWindow.navigation.currentEntry.key;
+
+ i.contentWindow.navigation.navigate("?1");
+ await new Promise(resolve => i.onload = resolve);
+
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry, i.contentWindow.navigation.entries()[1]);
+
+ let navigateEventCount = 0;
+ i.contentWindow.navigation.onnavigate = () => navigateEventCount++;
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ let assertionPromise;
+ // The iframe does not have sticky activation, so per
+ // https://html.spec.whatwg.org/#prompt-to-unload-a-document, no prompt is
+ // shown and the navigation will proceed.
+ i.contentWindow.onbeforeunload = t.step_func(() => {
+ assertionPromise = assertBothRejectDOM(t, i.contentWindow.navigation.back(), "InvalidStateError", i.contentWindow);
+ });
+ i.contentWindow.navigation.navigate("?1");
+
+ assert_not_equals(assertionPromise, undefined);
+ await assertionPromise;
+
+ assert_equals(navigateEventCount, 1);
+}, "back() inside onbeforeunload");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-forward-initial-about-blank.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-forward-initial-about-blank.html
new file mode 100644
index 0000000000..dfdb6611ab
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-forward-initial-about-blank.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+promise_test(async t => {
+ let i = document.createElement("iframe");
+ document.body.append(i);
+
+ i.contentWindow.navigation.onnavigate = t.unreached_func("onnavigate should not be called");
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ // Since there's no way to do a non-replacement navigation on the initial
+ // about:blank, there's no way to actually get in a situation where we're on
+ // about:blank but there's something else backward/forward in the history
+ // list. So this test will almost certainly pass just because there's nothing
+ // to go back/forward to. Oh well; it's still reasonable coverage.
+
+ await assertBothRejectDOM(t, i.contentWindow.navigation.back(), "InvalidStateError", i.contentWindow);
+ await assertBothRejectDOM(t, i.contentWindow.navigation.forward(), "InvalidStateError", i.contentWindow);
+}, "back() and forward() in initial about:blank document");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-forward-opaque-origin.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-forward-opaque-origin.html
new file mode 100644
index 0000000000..59849e961d
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-forward-opaque-origin.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe id="i" sandbox="allow-scripts" src="resources/back-forward-opaque-origin-page.html"></iframe>
+
+<script>
+fetch_tests_from_window(i.contentWindow);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-forward-out-of-bounds.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-forward-out-of-bounds.html
new file mode 100644
index 0000000000..015c090bf9
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-forward-out-of-bounds.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+promise_test(async t => {
+ assert_equals(navigation.entries().length, 1);
+ assert_equals(navigation.entries()[0], navigation.currentEntry);
+ assert_false(navigation.canGoBack);
+ assert_false(navigation.canGoForward);
+
+ await assertBothRejectDOM(t, navigation.back(), "InvalidStateError");
+ await assertBothRejectDOM(t, navigation.forward(), "InvalidStateError");
+}, "back() and forward() out of bounds");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-intercept-rejected.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-intercept-rejected.html
new file mode 100644
index 0000000000..9d4238087f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-intercept-rejected.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.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.href = "#1";
+
+ assert_equals(navigation.entries().length, 2);
+ const [entry0, entry1] = navigation.entries();
+ assert_equals((new URL(entry0.url)).hash, "");
+ assert_equals((new URL(entry1.url)).hash, "#1");
+
+ const err = new Error("boo!");
+ const promise = Promise.reject(err);
+ promise.catch(() => {}); // prevent unhandled rejection testharness.js errors
+ navigation.onnavigate = e => e.intercept({ handler: () => promise });
+
+ const result = navigation.back();
+ await assertCommittedFulfillsFinishedRejectsExactly(t, result, entry0, err);
+ assert_equals(navigation.currentEntry, entry0);
+}, "back() promise rejection with rejected intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-intercept.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-intercept.html
new file mode 100644
index 0000000000..2654045c2b
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back-intercept.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.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.href = "#1";
+
+ assert_equals(navigation.entries().length, 2);
+ const [entry0, entry1] = navigation.entries();
+ assert_equals((new URL(entry0.url)).hash, "");
+ assert_equals((new URL(entry1.url)).hash, "#1");
+
+ navigation.onnavigate = e => e.intercept({ handler: () => Promise.resolve({ abc: "def" }) });
+
+ const result = navigation.back();
+ await assertBothFulfill(t, result, entry0);
+ assert_equals(navigation.currentEntry, entry0);
+}, "back() and intercept() with a fulfilled promise");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back.html
new file mode 100644
index 0000000000..a2b13db901
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/back.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.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.href = "#1";
+
+ assert_equals(navigation.entries().length, 2);
+ const [entry0, entry1] = navigation.entries();
+ assert_equals((new URL(entry0.url)).hash, "");
+ assert_equals((new URL(entry1.url)).hash, "#1");
+
+ const result = navigation.back();
+ await assertBothFulfill(t, result, entry0);
+ assert_equals(navigation.currentEntry, entry0);
+}, "back() promises");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward-already-detached.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward-already-detached.html
new file mode 100644
index 0000000000..4dfa74d9f9
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward-already-detached.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<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));
+
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ let key = i.contentWindow.navigation.currentEntry.key;
+
+ i.contentWindow.navigation.navigate("?1");
+ await new Promise(resolve => i.onload = resolve);
+
+ i.contentWindow.navigation.back();
+ await new Promise(resolve => i.onload = resolve);
+
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry, i.contentWindow.navigation.entries()[0]);
+
+ const iWindow = i.contentWindow;
+ const iDOMException = iWindow.DOMException;
+
+ i.remove();
+
+ await assertBothRejectDOM(t, iWindow.navigation.forward(), "InvalidStateError", iWindow, iDOMException);
+}, "forward() in a detached window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward-beforeunload.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward-beforeunload.html
new file mode 100644
index 0000000000..87fa4baa93
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward-beforeunload.html
@@ -0,0 +1,45 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<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));
+
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ let key = i.contentWindow.navigation.currentEntry.key;
+
+ i.contentWindow.navigation.navigate("?1");
+ await new Promise(resolve => i.onload = resolve);
+
+ i.contentWindow.navigation.back();
+ await new Promise(resolve => i.onload = resolve);
+
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry, i.contentWindow.navigation.entries()[0]);
+
+ let navigateEventCount = 0;
+ i.contentWindow.navigation.onnavigate = () => navigateEventCount++;
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ let assertionPromise;
+ // The iframe does not have sticky activation, so per
+ // https://html.spec.whatwg.org/#prompt-to-unload-a-document, no prompt is
+ // shown and the navigation will proceed.
+ i.contentWindow.onbeforeunload = t.step_func(() => {
+ assertionPromise = assertBothRejectDOM(t, i.contentWindow.navigation.forward(), "InvalidStateError", i.contentWindow);
+ });
+ i.contentWindow.navigation.navigate("?1");
+
+ assert_not_equals(assertionPromise, undefined);
+ await assertionPromise;
+
+ assert_equals(navigateEventCount, 1);
+}, "forward() inside onbeforeunload");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward-intercept-rejected.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward-intercept-rejected.html
new file mode 100644
index 0000000000..9e86dcca19
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward-intercept-rejected.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.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.href = "#1";
+ await navigation.back().committed;
+
+ assert_equals(navigation.entries().length, 2);
+ const [entry0, entry1] = navigation.entries();
+ assert_equals((new URL(entry0.url)).hash, "");
+ assert_equals((new URL(entry1.url)).hash, "#1");
+
+ const err = new Error("boo!");
+ const promise = Promise.reject(err);
+ promise.catch(() => {}); // prevent unhandled rejection testharness.js errors
+ navigation.onnavigate = e => e.intercept({ handler: () => promise });
+
+ const result = navigation.forward();
+ await assertCommittedFulfillsFinishedRejectsExactly(t, result, entry1, err);
+ assert_equals(navigation.currentEntry, entry1);
+}, "forward() promise rejection with rejected intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward-intercept.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward-intercept.html
new file mode 100644
index 0000000000..ce4ab32170
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward-intercept.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.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.href = "#1";
+
+ assert_equals(navigation.entries().length, 2);
+ const [entry0, entry1] = navigation.entries();
+ assert_equals((new URL(entry0.url)).hash, "");
+ assert_equals((new URL(entry1.url)).hash, "#1");
+
+ await navigation.back().committed;
+
+ const promise = Promise.resolve({ abc: "def" });
+ navigation.onnavigate = e => e.intercept({ handler: () => promise });
+
+ const result = navigation.forward();
+ await assertBothFulfill(t, result, entry1);
+ assert_equals(navigation.currentEntry, entry1);
+}, "forward() and intercept() with a fulfilled promise");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward.html
new file mode 100644
index 0000000000..36bdba8daa
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/forward.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.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.href = "#1";
+ await navigation.back().committed;
+
+ assert_equals(navigation.entries().length, 2);
+ const [entry0, entry1] = navigation.entries();
+ assert_equals((new URL(entry0.url)).hash, "");
+ assert_equals((new URL(entry1.url)).hash, "#1");
+
+ const result = navigation.forward();
+ await assertBothFulfill(t, result, entry1);
+ assert_equals(navigation.currentEntry, entry1);
+}, "forward() promises");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-204-205-download.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-204-205-download.html
new file mode 100644
index 0000000000..f1e89b6940
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-204-205-download.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+const tests = [
+ ["204s", "/common/blank.html?pipe=status(204)"],
+ ["205s", "/common/blank.html?pipe=status(205)"],
+ ["Content-Disposition: attachment responses", "/common/blank.html?pipe=header(Content-Disposition,attachment)"]
+];
+
+for (const [description, url] of tests) {
+ promise_test(async t => {
+ const i = document.createElement("iframe");
+ i.src = "/common/blank.html";
+ document.body.append(i);
+ await new Promise(resolve => i.onload = resolve);
+
+ // This seems to be important? Without it the (outer) window load event
+ // doesn't fire, and the test harness hangs forever. This is probably a
+ // Chromium bug, but maybe a testharness bug, especially since explicit_done
+ // doesn't seem to help?
+ t.add_cleanup(() => i.remove());
+
+ let navigateCount = 0;
+ i.contentWindow.navigation.onnavigate = () => { ++navigateCount; };
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ const result = i.contentWindow.navigation.navigate(url);
+
+ assert_equals(navigateCount, 1);
+ assertNeverSettles(t, result, i.contentWindow);
+
+ await new Promise(resolve => t.step_timeout(resolve, 50));
+ assert_equals(i.contentWindow.location.href, i.src);
+ assert_equals(i.contentWindow.navigation.transition, null);
+ }, `navigate() promises to ${description} never settle`);
+}
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-already-detached.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-already-detached.html
new file mode 100644
index 0000000000..33cdd6922d
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-already-detached.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ const iWindow = i.contentWindow;
+ const iDOMException = iWindow.DOMException;
+
+ i.remove();
+
+ await assertBothRejectDOM(t, iWindow.navigation.navigate("?1"), "InvalidStateError", iWindow, iDOMException);
+}, "navigate() in a detached window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-beforeunload.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-beforeunload.html
new file mode 100644
index 0000000000..f0a9069677
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-beforeunload.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ let navigateEventCount = 0;
+ i.contentWindow.navigation.onnavigate = () => navigateEventCount++;
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ let assertionPromise;
+ // The iframe does not have sticky activation, so per
+ // https://html.spec.whatwg.org/#prompt-to-unload-a-document, no prompt is
+ // shown and the navigation will proceed.
+ i.contentWindow.onbeforeunload = t.step_func(() => {
+ assertionPromise = assertBothRejectDOM(t, i.contentWindow.navigation.navigate("#"), "InvalidStateError", i.contentWindow);
+ });
+ i.contentWindow.navigation.navigate("?1");
+
+ assert_not_equals(assertionPromise, undefined);
+ await assertionPromise;
+
+ assert_equals(navigateEventCount, 1);
+}, "navigate() inside onbeforeunload");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-cross-document.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-cross-document.html
new file mode 100644
index 0000000000..b83b1e941f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-cross-document.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ const result = i.contentWindow.navigation.navigate("?1");
+ assertNeverSettles(t, result, i.contentWindow);
+
+ await new Promise(resolve => i.onload = () => t.step_timeout(resolve, 0));
+}, "cross-document navigate() promises never settle");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-detach-in-onnavigate.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-detach-in-onnavigate.html
new file mode 100644
index 0000000000..0e80e1b130
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-detach-in-onnavigate.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ const iWindow = i.contentWindow;
+ const iDOMException = iWindow.DOMException;
+ i.contentWindow.navigation.onnavigate = () => i.remove();
+
+ await assertBothRejectDOM(t, i.contentWindow.navigation.navigate("#1"), "AbortError", iWindow, iDOMException);
+}, "navigate() promise rejections when detaching an iframe inside onnavigate");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-detach-in-serialization.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-detach-in-serialization.html
new file mode 100644
index 0000000000..14086c9f01
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-detach-in-serialization.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ const iWindow = i.contentWindow;
+ const iDOMException = iWindow.DOMException;
+
+ const trappedState = {
+ get prop() {
+ i.remove();
+ return "whatever";
+ }
+ };
+
+ await assertBothRejectDOM(t, i.contentWindow.navigation.navigate("#1", { state: trappedState }), "InvalidStateError", iWindow, iDOMException);
+}, "navigate() promise rejections when detaching an iframe inside state serialization");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-file-url.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-file-url.html
new file mode 100644
index 0000000000..138f6ffc12
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-file-url.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+promise_test(async t => {
+ await assertBothRejectDOM(t, navigation.navigate("file://"), "AbortError");
+}, "navigate() to a file: URL");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-initial-about-blank-cross-document.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-initial-about-blank-cross-document.html
new file mode 100644
index 0000000000..8ec76b11dd
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-initial-about-blank-cross-document.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+promise_test(async t => {
+ let i = document.createElement("iframe");
+ document.body.append(i);
+
+ i.contentWindow.navigation.onnavigate = t.unreached_func("onnavigate should not be called");
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ // Cannot just use "/common/blank.html?1" since it doesn't resolve relative to about:blank.
+ const result = i.contentWindow.navigation.navigate(new URL("/common/blank.html?1", location.href).href);
+ assertNeverSettles(t, result, i.contentWindow);
+ await new Promise(resolve => t.step_timeout(resolve, 10));
+}, "navigate() in initial about:blank document (cross-document)");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-initial-about-blank.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-initial-about-blank.html
new file mode 100644
index 0000000000..9734a09d01
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-initial-about-blank.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+promise_test(async t => {
+ let i = document.createElement("iframe");
+ document.body.append(i);
+
+ i.contentWindow.navigation.onnavigate = t.unreached_func("onnavigate should not be called");
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ const result = i.contentWindow.navigation.navigate("#1");
+ assertNeverSettles(t, result, i.contentWindow);
+ await new Promise(resolve => t.step_timeout(resolve, 10));
+}, "navigate() in initial about:blank document");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-intercept-interrupted.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-intercept-interrupted.html
new file mode 100644
index 0000000000..36403c892c
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-intercept-interrupted.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.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));
+
+ navigation.addEventListener("navigate", e => e.intercept());
+
+ const result1 = navigation.navigate("#1");
+ const result2 = navigation.navigate("#2");
+
+ assert_equals(navigation.entries().length, 3);
+ assert_array_equals(navigation.entries().map(e => (new URL(e.url)).hash), ["", "#1", "#2"]);
+
+ await assertCommittedFulfillsFinishedRejectsDOM(t, result1, navigation.entries()[1], "AbortError");
+ await assertBothFulfill(t, result2, navigation.currentEntry);
+}, "interrupted navigate() promises with intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-intercept-rejected.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-intercept-rejected.html
new file mode 100644
index 0000000000..79c8bd803e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-intercept-rejected.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+promise_test(async t => {
+ const err = new Error("boo!");
+ const promise = Promise.reject(err);
+ promise.catch(() => {}); // prevent unhandled rejection testharness.js errors
+ navigation.onnavigate = e => e.intercept({ handler: () => promise });
+
+ const result = navigation.navigate("#1");
+
+ await assertCommittedFulfillsFinishedRejectsExactly(t, result, navigation.currentEntry, err);
+}, "navigate() and intercept() with a rejected promise");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-intercept.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-intercept.html
new file mode 100644
index 0000000000..a9946c71ff
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-intercept.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+promise_test(async t => {
+ navigation.onnavigate = e => e.intercept({ handler: () => Promise.resolve({ abc: 'def' }) });
+
+ const result = navigation.navigate("#1");
+
+ await assertBothFulfill(t, result, navigation.currentEntry);
+ assert_equals((new URL(navigation.currentEntry.url)).hash, "#1");
+}, "navigate() and intercept() with a fulfilled promise");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-interrupted-within-onnavigate.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-interrupted-within-onnavigate.html
new file mode 100644
index 0000000000..3db02c6931
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-interrupted-within-onnavigate.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.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));
+
+ let result2;
+ navigation.onnavigate = t.step_func(e => {
+ if (e.info == 1) {
+ result2 = navigation.navigate("#2", { info: 2 });
+ assert_true(e.defaultPrevented);
+ }
+ });
+
+ const result1 = navigation.navigate("#1", { info: 1 });
+
+ assert_equals(navigation.entries().length, 2);
+ assert_array_equals(navigation.entries().map(e => (new URL(e.url)).hash), ["", "#2"]);
+
+ await assertBothRejectDOM(t, result1, "AbortError");
+ await assertBothFulfill(t, result2, navigation.currentEntry);
+}, "if navigate() is called inside onnavigate, the previous navigation and navigate event are cancelled");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-interrupted.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-interrupted.html
new file mode 100644
index 0000000000..2c928254e0
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-interrupted.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.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));
+
+ const result1 = navigation.navigate("#1");
+ const result2 = navigation.navigate("#2");
+
+ assert_equals(navigation.entries().length, 3);
+ assert_array_equals(navigation.entries().map(e => (new URL(e.url)).hash), ["", "#1", "#2"]);
+
+ await assertCommittedFulfillsFinishedRejectsDOM(t, result1, navigation.entries()[1], "AbortError");
+ await assertBothFulfill(t, result2, navigation.currentEntry);
+}, "interrupted navigate() promises");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-invalid-url.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-invalid-url.html
new file mode 100644
index 0000000000..5b5b442c91
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-invalid-url.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+promise_test(async t => {
+ await assertBothRejectDOM(t, navigation.navigate("https://example.com\u0000mozilla.org"), "SyntaxError");
+}, "navigate() with an invalid URL");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-opaque-origin.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-opaque-origin.html
new file mode 100644
index 0000000000..51500eb8d8
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-opaque-origin.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe id="i" sandbox="allow-scripts" src="resources/navigate-opaque-origin-page.html"></iframe>
+
+<script>
+fetch_tests_from_window(i.contentWindow);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-preventDefault.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-preventDefault.html
new file mode 100644
index 0000000000..6257c5a03d
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-preventDefault.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+promise_test(async t => {
+ navigation.onnavigate = e => e.preventDefault();
+
+ await assertBothRejectDOM(t, navigation.navigate("#"), "AbortError");
+}, "navigate() when the onnavigate handler calls preventDefault()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-push-initial-about-blank.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-push-initial-about-blank.html
new file mode 100644
index 0000000000..9d47c8d0b5
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-push-initial-about-blank.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<iframe id="i"></iframe>
+<script>
+promise_test(async t => {
+ // Wait for after the load event so that we are definitely testing the initial
+ // about:blank-ness as the cause of the rejections.
+ await new Promise(resolve => window.onload = () => t.step_timeout(resolve, 0));
+
+ i.contentWindow.navigation.onnavigate = t.unreached_func("onnavigate should not be called");
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ const result = i.contentWindow.navigation.navigate("#1", { history: "push" });
+ await assertBothRejectDOM(t, result, "NotSupportedError", i.contentWindow);
+}, "navigate() with history: 'push' in initial about:blank document");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-push-javascript-url.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-push-javascript-url.html
new file mode 100644
index 0000000000..e41e06a78d
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-push-javascript-url.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+promise_test(async t => {
+ // Wait for after the load event so that we are definitely testing the
+ // javascript: URL as the cause of the rejections.
+ await new Promise(resolve => window.onload = () => t.step_timeout(resolve, 0));
+
+ navigation.onnavigate = t.unreached_func("onnavigate should not be called");
+ navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ const result = navigation.navigate("javascript:'foo'", { history: "push" });
+ await assertBothRejectDOM(t, result, "NotSupportedError");
+}, "navigate() to a javascript: URL");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-beforeunload-unserializablestate.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-beforeunload-unserializablestate.html
new file mode 100644
index 0000000000..878280a56e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-beforeunload-unserializablestate.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ let navigateEventCount = 0;
+ i.contentWindow.navigation.onnavigate = () => navigateEventCount++;
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func('onnavigatesuccess should not be called');
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func('onnavigateerror should not be called');
+
+ let assertionPromise;
+ // The iframe does not have sticky activation, so per
+ // https://html.spec.whatwg.org/#prompt-to-unload-a-document, no prompt is
+ // shown and the navigation will proceed.
+ i.contentWindow.onbeforeunload = t.step_func(() => {
+ assertionPromise = assertBothRejectDOM(t, i.contentWindow.navigation.navigate("/common/blank.html?1", { state: document.body }), "DataCloneError", i.contentWindow);
+ });
+ i.contentWindow.navigation.navigate("?1");
+
+ assert_not_equals(assertionPromise, undefined);
+ await assertionPromise;
+
+ assert_equals(navigateEventCount, 1);
+}, `navigate() with an unserializable state inside onbeforeunload "DataCloneError", not "InvalidStateError"`);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-detached-unserializablestate.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-detached-unserializablestate.html
new file mode 100644
index 0000000000..2c6f9a28d4
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-detached-unserializablestate.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+ const iWindow = i.contentWindow;
+ const iDOMException = iWindow.DOMException;
+
+ i.remove();
+
+ iWindow.navigation.onnavigate = t.unreached_func("onnavigate");
+
+ await assertBothRejectDOM(t, iWindow.navigation.navigate("https://example.com/", { state: document.body }), "DataCloneError", iWindow, iDOMException);
+}, `navigate() with unserializable state in a detached iframe throws "DataCloneError", not "InvalidStateError"`);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-invalidurl-beforeunload.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-invalidurl-beforeunload.html
new file mode 100644
index 0000000000..4873e85a2f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-invalidurl-beforeunload.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ let navigateEventCount = 0;
+ i.contentWindow.navigation.onnavigate = () => navigateEventCount++;
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ let assertionPromise;
+ // The iframe does not have sticky activation, so per
+ // https://html.spec.whatwg.org/#prompt-to-unload-a-document, no prompt is
+ // shown and the navigation will proceed.
+ i.contentWindow.onbeforeunload = t.step_func(() => {
+ assertionPromise = assertBothRejectDOM(t, i.contentWindow.navigation.navigate("https://example.com\u0000mozilla.org"), "SyntaxError", i.contentWindow);
+ });
+ i.contentWindow.navigation.navigate("?1");
+
+ assert_not_equals(assertionPromise, undefined);
+ await assertionPromise;
+
+ assert_equals(navigateEventCount, 1);
+}, `navigate() with an invalid URL inside onbeforeunload throws "SyntaxError", not "InvalidStateError"`);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-invalidurl-detached.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-invalidurl-detached.html
new file mode 100644
index 0000000000..2250a54114
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-invalidurl-detached.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+ const iWindow = i.contentWindow;
+ const iDOMException = iWindow.DOMException;
+
+ i.remove();
+
+ iWindow.navigation.onnavigate = t.unreached_func("onnavigate");
+
+ await assertBothRejectDOM(t, iWindow.navigation.navigate("https://example.com\u0000mozilla.org"), "SyntaxError", iWindow, iDOMException);
+}, `navigate() with an invalid URL in a detached iframe throws "SyntaxError", not "InvalidStateError"`);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-invalidurl-unload.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-invalidurl-unload.html
new file mode 100644
index 0000000000..d778dd662c
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-invalidurl-unload.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ let navigateEventCount = 0;
+ i.contentWindow.navigation.onnavigate = () => navigateEventCount++;
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ i.contentWindow.navigation.navigate("?1");
+
+ await new Promise(resolve => {
+ i.contentWindow.onunload = t.step_func(async () => {
+ await assertBothRejectDOM(t, i.contentWindow.navigation.navigate("https://example.com\u0000mozilla.org"), "SyntaxError", i.contentWindow);
+ assert_equals(navigateEventCount, 1);
+ resolve();
+ });
+ });
+}, `navigate() with an invalid URL inside onunload throws "SyntaxError", not "InvalidStateError"`);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-invalidurl-unserializablestate.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-invalidurl-unserializablestate.html
new file mode 100644
index 0000000000..07e194ca41
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-invalidurl-unserializablestate.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+promise_test(async t => {
+ navigation.onnavigate = t.unreached_func("onnavigate");
+
+ await assertBothRejectDOM(t, navigation.navigate("https://example.com\u0000mozilla.org", { state: document.body }), "SyntaxError");
+}, `navigate() with an invalid URL and unserializable state throws "SyntaxError", not "DataCloneError"`);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-unload-unserializablestate.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-unload-unserializablestate.html
new file mode 100644
index 0000000000..417dd27174
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-rejection-order-unload-unserializablestate.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ let navigateEventCount = 0;
+ i.contentWindow.navigation.onnavigate = () => navigateEventCount++;
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ i.contentWindow.navigation.navigate("?1");
+
+ await new Promise(resolve => {
+ i.contentWindow.onunload = t.step_func(async () => {
+ await assertBothRejectDOM(t, i.contentWindow.navigation.navigate("?2", { state: document.body }), "DataCloneError", i.contentWindow);
+ assert_equals(navigateEventCount, 1);
+ resolve();
+ });
+ });
+}, `navigate() with an unserializable state inside onunload throws "DataCloneError", not "InvalidStateError"`);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-unload.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-unload.html
new file mode 100644
index 0000000000..fbc1fde6e9
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-unload.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ let navigateEventCount = 0;
+ i.contentWindow.navigation.onnavigate = () => navigateEventCount++;
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ i.contentWindow.navigation.navigate("?1");
+
+ await new Promise(resolve => {
+ i.contentWindow.onunload = t.step_func(async () => {
+ await assertBothRejectDOM(t, i.contentWindow.navigation.navigate("?2"), "InvalidStateError", i.contentWindow);
+ assert_equals(navigateEventCount, 1);
+ resolve();
+ });
+ });
+}, `navigate() inside onunload`);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-unserializable-state.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-unserializable-state.html
new file mode 100644
index 0000000000..36464ec3c5
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate-unserializable-state.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+promise_test(async t => {
+ await assertBothRejectDOM(t, navigation.navigate("#1", { state: new WritableStream() }), "DataCloneError");
+ assert_equals(navigation.currentEntry.getState(), undefined);
+ assert_equals(location.hash, "");
+}, "navigate() with an unserializable state (WritableStream)");
+
+promise_test(async t => {
+ // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()`
+ const buffer = new WebAssembly.Memory({ shared:true, initial:1, maximum:1 }).buffer;
+
+ await assertBothRejectDOM(t, navigation.navigate("#2", { state: buffer }), "DataCloneError");
+ assert_equals(navigation.currentEntry.getState(), undefined);
+ assert_equals(location.hash, "");
+}, "navigate() with an unserializable state (SharedArrayBuffer)");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate.html
new file mode 100644
index 0000000000..34ff84f0e9
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/navigate.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+promise_test(async t => {
+ const result = navigation.navigate("#1");
+ await assertBothFulfill(t, result, navigation.currentEntry);
+ assert_equals((new URL(navigation.currentEntry.url)).hash, "#1");
+}, "navigate() promises");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-already-detached.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-already-detached.html
new file mode 100644
index 0000000000..9b0780c4a9
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-already-detached.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ const iWindow = i.contentWindow;
+ const iDOMException = iWindow.DOMException;
+
+ i.remove();
+
+ await assertBothRejectDOM(t, iWindow.navigation.reload(), "InvalidStateError", iWindow, iDOMException);
+}, "reload() in a detached window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-beforeunload.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-beforeunload.html
new file mode 100644
index 0000000000..05338acc5e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-beforeunload.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ let navigateEventCount = 0;
+ i.contentWindow.navigation.onnavigate = () => navigateEventCount++;
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ let assertionPromise;
+ // The iframe does not have sticky activation, so per
+ // https://html.spec.whatwg.org/#prompt-to-unload-a-document, no prompt is
+ // shown and the navigation will proceed.
+ i.contentWindow.onbeforeunload = t.step_func(() => {
+ assertionPromise = assertBothRejectDOM(t, i.contentWindow.navigation.reload(), "InvalidStateError", i.contentWindow);
+ });
+ i.contentWindow.navigation.navigate("?1");
+
+ assert_not_equals(assertionPromise, undefined);
+ await assertionPromise;
+
+ assert_equals(navigateEventCount, 1);
+}, "reload() inside onbeforeunload");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-detach-in-onnavigate.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-detach-in-onnavigate.html
new file mode 100644
index 0000000000..621bb598f6
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-detach-in-onnavigate.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ const iWindow = i.contentWindow;
+ const iDOMException = iWindow.DOMException;
+ i.contentWindow.navigation.onnavigate = () => i.remove();
+
+ await assertBothRejectDOM(t, i.contentWindow.navigation.reload(), "AbortError", iWindow, iDOMException);
+}, "reload() promise rejections when detaching an iframe inside onnavigate");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-detach-in-serialization.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-detach-in-serialization.html
new file mode 100644
index 0000000000..d958d9cc84
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-detach-in-serialization.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ const iWindow = i.contentWindow;
+ const iDOMException = iWindow.DOMException;
+
+ const trappedState = {
+ get prop() {
+ i.remove();
+ return "whatever";
+ }
+ };
+
+ await assertBothRejectDOM(t, i.contentWindow.navigation.reload({ state: trappedState }), "InvalidStateError", iWindow, iDOMException);
+}, "reload() promise rejections when detaching an iframe inside state serialization");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-initial-about-blank.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-initial-about-blank.html
new file mode 100644
index 0000000000..9e717fee48
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-initial-about-blank.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<body>
+<script>
+promise_test(async t => {
+ let i = document.createElement("iframe");
+ document.body.append(i);
+
+ i.contentWindow.navigation.onnavigate = t.unreached_func("onnavigate should not be called");
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ const result = i.contentWindow.navigation.reload();
+ assertNeverSettles(t, result, i.contentWindow);
+ await new Promise(resolve => t.step_timeout(resolve, 10));
+}, "reload() in initial about:blank document");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-intercept-rejected.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-intercept-rejected.html
new file mode 100644
index 0000000000..93cd75c2b9
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-intercept-rejected.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+promise_test(async t => {
+ const err = new Error("boo!");
+ const promise = Promise.reject(err);
+ promise.catch(() => {}); // prevent unhandled rejection testharness.js errors
+ navigation.onnavigate = e => e.intercept({ handler: () => promise });
+
+ const result = navigation.reload();
+
+ await assertCommittedFulfillsFinishedRejectsExactly(t, result, navigation.currentEntry, err);
+}, "reload() and intercept() with a rejected promise");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-intercept.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-intercept.html
new file mode 100644
index 0000000000..92f1636a6e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-intercept.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+promise_test(async t => {
+ navigation.onnavigate = e => e.intercept({ handler: () => Promise.resolve({ abc: 'def' }) });
+
+ const result = navigation.reload();
+
+ await assertBothFulfill(t, result, navigation.currentEntry);
+}, "reload() and intercept() with a fulfilled promise");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-preventDefault.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-preventDefault.html
new file mode 100644
index 0000000000..747044b60f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-preventDefault.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+promise_test(async t => {
+ navigation.onnavigate = e => e.preventDefault();
+
+ await assertBothRejectDOM(t, navigation.reload(), "AbortError");
+}, "reload() when the onnavigate handler calls preventDefault()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-rejection-order-beforeunload-unserializablestate.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-rejection-order-beforeunload-unserializablestate.html
new file mode 100644
index 0000000000..950d665218
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-rejection-order-beforeunload-unserializablestate.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ let navigateEventCount = 0;
+ i.contentWindow.navigation.onnavigate = () => navigateEventCount++;
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func('onnavigatesuccess should not be called');
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func('onnavigateerror should not be called');
+
+ let assertionPromise;
+ // The iframe does not have sticky activation, so per
+ // https://html.spec.whatwg.org/#prompt-to-unload-a-document, no prompt is
+ // shown and the navigation will proceed.
+ i.contentWindow.onbeforeunload = t.step_func(() => {
+ assertionPromise = assertBothRejectDOM(t, i.contentWindow.navigation.reload({ state: document.body }), "DataCloneError", i.contentWindow);
+ });
+ i.contentWindow.navigation.navigate("?1");
+
+ assert_not_equals(assertionPromise, undefined);
+ await assertionPromise;
+
+ assert_equals(navigateEventCount, 1);
+}, `reload() with an unserializable state inside onbeforeunload throws "DataCloneError", not "InvalidStateError"`);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-rejection-order-detached-unserializablestate.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-rejection-order-detached-unserializablestate.html
new file mode 100644
index 0000000000..9206f18e6c
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-rejection-order-detached-unserializablestate.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+ const iWindow = i.contentWindow;
+ const iDOMException = iWindow.DOMException;
+
+ i.remove();
+
+ iWindow.navigation.onnavigate = t.unreached_func("onnavigate");
+
+ await assertBothRejectDOM(t, iWindow.navigation.reload({ state: document.body }), "DataCloneError", iWindow, iDOMException);
+}, `reload() with unserializable state in a detached iframe throws "DataCloneError", not "InvalidStateError"`);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-rejection-order-unload-unserializablestate.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-rejection-order-unload-unserializablestate.html
new file mode 100644
index 0000000000..9e7edcbba4
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-rejection-order-unload-unserializablestate.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ let navigateEventCount = 0;
+ i.contentWindow.navigation.onnavigate = () => navigateEventCount++;
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ i.contentWindow.navigation.navigate("?1");
+
+ let assertionPromise;
+ await new Promise(resolve => {
+ i.contentWindow.onunload = t.step_func(() => {
+ assertionPromise = assertBothRejectDOM(t, i.contentWindow.navigation.reload({ state: document.body }), "DataCloneError", i.contentWindow);
+ assert_equals(navigateEventCount, 1);
+ resolve();
+ });
+ });
+
+ assert_not_equals(assertionPromise, undefined);
+ await assertionPromise;
+
+}, `reload() with an unserializable state inside onunload throws "DataCloneError", not "InvalidStateError"`);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-unload.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-unload.html
new file mode 100644
index 0000000000..1906659d1d
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-unload.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ let navigateEventCount = 0;
+ i.contentWindow.navigation.onnavigate = () => navigateEventCount++;
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ i.contentWindow.navigation.navigate("?1");
+
+ await new Promise(resolve => {
+ i.contentWindow.onunload = t.step_func(async () => {
+ await assertBothRejectDOM(t, i.contentWindow.navigation.reload(), "InvalidStateError", i.contentWindow);
+ assert_equals(navigateEventCount, 1);
+ resolve();
+ });
+ });
+}, `reload() inside onunload`);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-unserializable-state.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-unserializable-state.html
new file mode 100644
index 0000000000..76fa870558
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload-unserializable-state.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+promise_test(async t => {
+ await assertBothRejectDOM(t, navigation.reload({ state: new WritableStream() }), "DataCloneError");
+ assert_equals(navigation.currentEntry.getState(), undefined);
+ assert_equals(location.hash, "");
+}, "reload() with an unserializable state (WritableStream)");
+
+promise_test(async t => {
+ // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()`
+ const buffer = new WebAssembly.Memory({ shared:true, initial:1, maximum:1 }).buffer;
+
+ await assertBothRejectDOM(t, navigation.reload({ state: buffer }), "DataCloneError");
+ assert_equals(navigation.currentEntry.getState(), undefined);
+ assert_equals(location.hash, "");
+}, "reload() with an unserializable state (SharedArrayBuffer)");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload.html
new file mode 100644
index 0000000000..388f0d94b5
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/reload.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ const result = i.contentWindow.navigation.reload();
+ assertNeverSettles(t, result, i.contentWindow);
+
+ await new Promise(resolve => i.onload = () => t.step_timeout(resolve, 0));
+}, "reload() promises never settle (without intercept())");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/resources/204-205-download-on-second-visit.py b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/resources/204-205-download-on-second-visit.py
new file mode 100644
index 0000000000..c18b0dec3d
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/resources/204-205-download-on-second-visit.py
@@ -0,0 +1,24 @@
+def main(request, response):
+ key = request.GET[b"id"]
+
+ # If hit with a POST with ?action=X, store X in the stash
+ if request.method == "POST":
+ action = request.GET[b"action"]
+ request.server.stash.put(key, action)
+
+ return (204, [], "")
+
+ # If hit with a GET, either return a normal initial page, or the abnormal requested response
+ elif request.method == "GET":
+ action = request.server.stash.take(key)
+
+ if action is None:
+ return (200, [("Content-Type", "text/html"), ("Cache-Control", "no-store")], "initial page")
+ if action == b"204":
+ return (204, [], "")
+ if action == b"205":
+ return (205, [], "")
+ if action == b"download":
+ return (200, [("Content-Type", "text/plain"), ("Content-Disposition", "attachment")], "some text to download")
+
+ return (400, [], "")
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/resources/back-forward-opaque-origin-page.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/resources/back-forward-opaque-origin-page.html
new file mode 100644
index 0000000000..ec63363952
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/resources/back-forward-opaque-origin-page.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="helpers.js"></script>
+<!-- Put this page in a sandbox to give it an opaque origin -->
+
+<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));
+
+ navigation.onnavigate = t.unreached_func("onnavigate should not be called");
+ navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ location.hash = "#1";
+ await new Promise(resolve => window.onhashchange = resolve);
+ location.hash = "#2";
+ await new Promise(resolve => window.onhashchange = resolve);
+ history.back();
+ await new Promise(resolve => window.onhashchange = resolve);
+
+ assert_equals(location.hash, "#1");
+
+ await assertBothRejectDOM(t, navigation.back(), "InvalidStateError");
+ await assertBothRejectDOM(t, navigation.forward(), "InvalidStateError");
+}, "navigation.back()/forward() in an opaque origin iframe");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/resources/helpers.js b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/resources/helpers.js
new file mode 100644
index 0000000000..63d706ed28
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/resources/helpers.js
@@ -0,0 +1,127 @@
+window.assertReturnValue = (result, w = window) => {
+ assert_equals(Object.getPrototypeOf(result), w.Object.prototype, "result object must be from the right realm");
+ assert_array_equals(Reflect.ownKeys(result), ["committed", "finished"]);
+ assert_true(result.committed instanceof w.Promise);
+ assert_true(result.finished instanceof w.Promise);
+ assert_not_equals(result.committed, result.finished);
+};
+
+window.assertNeverSettles = (t, result, w = window) => {
+ assertReturnValue(result, w);
+ result.committed.then(
+ t.unreached_func("committed must not fulfill"),
+ t.unreached_func("committed must not reject")
+ );
+
+ result.finished.then(
+ t.unreached_func("finished must not fulfill"),
+ t.unreached_func("finished must not reject")
+ );
+};
+
+window.assertBothFulfillEntryNotAvailable = async (t, result, w = window) => {
+ assertReturnValue(result, w);
+
+ // Don't use await here so that we can catch out-of-order settlements.
+ let committedValue;
+ result.committed.then(
+ t.step_func(v => { committedValue = v;}),
+ t.unreached_func("committed must not reject")
+ );
+
+ const finishedValue = await result.finished;
+
+ assert_not_equals(committedValue, undefined, "committed must fulfill before finished");
+ assert_equals(finishedValue, committedValue, "committed and finished must fulfill with the same value");
+ assert_true(finishedValue instanceof w.NavigationHistoryEntry, "fulfillment value must be a NavigationHistoryEntry");
+};
+
+window.assertBothFulfill = async (t, result, expected, w = window) => {
+ assertReturnValue(result, w);
+
+ // Don't use await here so that we can catch out-of-order settlements.
+ let committedValue;
+ result.committed.then(
+ t.step_func(v => { committedValue = v; }),
+ t.unreached_func("committed must not reject")
+ );
+
+ const finishedValue = await result.finished;
+
+ assert_not_equals(committedValue, undefined, "committed must fulfill before finished");
+ assert_equals(finishedValue, committedValue, "committed and finished must fulfill with the same value");
+ assert_true(finishedValue instanceof w.NavigationHistoryEntry, "fulfillment value must be a NavigationHistoryEntry");
+ assert_equals(finishedValue, expected);
+};
+
+window.assertCommittedFulfillsFinishedRejectsExactly = async (t, result, expectedEntry, expectedRejection, w = window) => {
+ assertReturnValue(result, w);
+
+ // Don't use await here so that we can catch out-of-order settlements.
+ let committedValue;
+ result.committed.then(
+ t.step_func(v => { committedValue = v; }),
+ t.unreached_func("committed must not reject")
+ );
+
+ await promise_rejects_exactly(t, expectedRejection, result.finished);
+
+ assert_not_equals(committedValue, undefined, "committed must fulfill before finished rejects");
+ assert_true(committedValue instanceof w.NavigationHistoryEntry, "fulfillment value must be a NavigationHistoryEntry");
+ assert_equals(committedValue, expectedEntry);
+};
+
+window.assertCommittedFulfillsFinishedRejectsDOM = async (t, result, expectedEntry, expectedDOMExceptionCode, w = window, domExceptionConstructor = w.DOMException, navigationHistoryEntryConstuctor = w.NavigationHistoryEntry) => {
+ assertReturnValue(result, w);
+
+ let committedValue;
+ result.committed.then(
+ t.step_func(v => { committedValue = v; }),
+ t.unreached_func("committed must not reject")
+ );
+
+ await promise_rejects_dom(t, expectedDOMExceptionCode, domExceptionConstructor, result.finished);
+
+ assert_not_equals(committedValue, undefined, "committed must fulfill before finished rejects");
+ assert_true(committedValue instanceof navigationHistoryEntryConstuctor, "fulfillment value must be an NavigationHistoryEntry");
+ assert_equals(committedValue, expectedEntry);
+};
+
+window.assertBothRejectExactly = async (t, result, expectedRejection, w = window) => {
+ assertReturnValue(result, w);
+
+ let committedReason, finishedReason;
+ await Promise.all([
+ result.committed.then(
+ t.unreached_func("committed must not fulfill"),
+ t.step_func(r => { committedReason = r; })
+ ),
+ result.finished.then(
+ t.unreached_func("finished must not fulfill"),
+ t.step_func(r => { finishedReason = r; })
+ )
+ ]);
+
+ assert_equals(committedReason, finishedReason, "committed and finished must reject with the same value");
+ assert_equals(expectedRejection, committedReason);
+};
+
+window.assertBothRejectDOM = async (t, result, expectedDOMExceptionCode, w = window, domExceptionConstructor = w.DOMException) => {
+ assertReturnValue(result, w);
+
+ // Don't use await here so that we can catch out-of-order settlements.
+ let committedReason, finishedReason;
+ await Promise.all([
+ result.committed.then(
+ t.unreached_func("committed must not fulfill"),
+ t.step_func(r => { committedReason = r; })
+ ),
+ result.finished.then(
+ t.unreached_func("finished must not fulfill"),
+ t.step_func(r => { finishedReason = r; })
+ )
+ ]);
+
+ assert_equals(committedReason, finishedReason, "committed and finished must reject with the same value");
+ assert_throws_dom(expectedDOMExceptionCode, domExceptionConstructor, () => { throw committedReason; });
+};
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/resources/navigate-opaque-origin-page.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/resources/navigate-opaque-origin-page.html
new file mode 100644
index 0000000000..831eefdb61
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/resources/navigate-opaque-origin-page.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="helpers.js"></script>
+<!-- Put this page in a sandbox to give it an opaque origin -->
+
+<script>
+promise_test(async t => {
+ navigation.onnavigate = t.unreached_func("onnavigate should not be called");
+ navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ const result = navigation.navigate("#1");
+ assertNeverSettles(t, result);
+ await new Promise(resolve => t.step_timeout(resolve, 10));
+}, "navigation.navigate() in an opaque origin iframe");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-already-detached.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-already-detached.html
new file mode 100644
index 0000000000..b974393df6
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-already-detached.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ const iWindow = i.contentWindow;
+ const iDOMException = iWindow.DOMException;
+ const key = iWindow.navigation.currentEntry.key;
+
+ i.remove();
+
+ await assertBothRejectDOM(t, iWindow.navigation.traverseTo(key), "InvalidStateError", iWindow, iDOMException);
+}, "traverseTo() in a detached window");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-beforeunload.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-beforeunload.html
new file mode 100644
index 0000000000..3b2722235e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-beforeunload.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ let navigateEventCount = 0;
+ i.contentWindow.navigation.onnavigate = () => navigateEventCount++;
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("onnavigatesuccess should not be called");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("onnavigateerror should not be called");
+
+ let assertionPromise;
+ // The iframe does not have sticky activation, so per
+ // https://html.spec.whatwg.org/#prompt-to-unload-a-document, no prompt is
+ // shown and the navigation will proceed.
+ i.contentWindow.onbeforeunload = t.step_func(() => {
+ assertionPromise = assertBothRejectDOM(t, i.contentWindow.navigation.traverseTo(i.contentWindow.navigation.currentEntry.key), "InvalidStateError", i.contentWindow);
+ });
+ i.contentWindow.navigation.navigate("?1");
+
+ assert_not_equals(assertionPromise, undefined);
+ await assertionPromise;
+
+ assert_equals(navigateEventCount, 1);
+}, "traverseTo() inside onbeforeunload");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-cross-document-preventDefault.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-cross-document-preventDefault.html
new file mode 100644
index 0000000000..09c91ee647
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-cross-document-preventDefault.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<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));
+
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ let key = i.contentWindow.navigation.currentEntry.key;
+
+ i.contentWindow.navigation.navigate("?1");
+ await new Promise(resolve => i.onload = resolve);
+
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry, i.contentWindow.navigation.entries()[1]);
+
+ // This will be a noop, because navigate events are uncancelable for traversals.
+ i.contentWindow.navigation.onnavigate = e => e.preventDefault();
+
+ assertNeverSettles(t, i.contentWindow.navigation.traverseTo(key), i.contentWindow);
+ await new Promise(resolve => i.onload = () => t.step_timeout(resolve, 0));
+}, "traverseTo() promise never settle when preventDefault()ing the navigate event (cross-document)");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-current.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-current.html
new file mode 100644
index 0000000000..212fe992cf
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-current.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+promise_test(async t => {
+ assert_equals(navigation.entries().length, 1);
+ const entry = navigation.currentEntry;
+
+ const result = navigation.traverseTo(navigation.currentEntry.key);
+ await assertBothFulfill(t, result, entry);
+
+ assert_equals(navigation.entries().length, 1);
+ assert_equals(navigation.currentEntry, entry);
+}, "traverseTo() with current key");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-detach-cross-document-before-navigate-event.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-detach-cross-document-before-navigate-event.html
new file mode 100644
index 0000000000..ed319f6223
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-detach-cross-document-before-navigate-event.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<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));
+
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ let key = i.contentWindow.navigation.currentEntry.key;
+
+ i.contentWindow.navigation.navigate("?1");
+ await new Promise(resolve => i.onload = resolve);
+
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry, i.contentWindow.navigation.entries()[1]);
+
+ const iWindow = i.contentWindow;
+ const iDOMException = iWindow.DOMException;
+ i.contentWindow.navigation.onnavigate = t.unreached_func("navigate should not fire");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("navigateerror should not fire");
+
+ let promises = i.contentWindow.navigation.traverseTo(key);
+ i.remove();
+ await assertBothRejectDOM(t, promises, "AbortError", iWindow, iDOMException);
+}, "traverseTo() promise rejections when detaching an iframe before onnavigate (cross-document)");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-detach-cross-document.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-detach-cross-document.html
new file mode 100644
index 0000000000..8784313b70
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-detach-cross-document.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<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));
+
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ let key = i.contentWindow.navigation.currentEntry.key;
+
+ i.contentWindow.navigation.navigate("?1");
+ await new Promise(resolve => i.onload = resolve);
+
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry, i.contentWindow.navigation.entries()[1]);
+
+ const iWindow = i.contentWindow;
+ const iDOMException = iWindow.DOMException;
+ i.contentWindow.navigation.onnavigate = () => i.remove();
+
+ await assertBothRejectDOM(t, i.contentWindow.navigation.traverseTo(key), "AbortError", iWindow, iDOMException);
+}, "traverseTo() promise rejections when detaching an iframe inside onnavigate (cross-document)");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-detach-same-document-before-navigate-event.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-detach-same-document-before-navigate-event.html
new file mode 100644
index 0000000000..592ac6f199
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-detach-same-document-before-navigate-event.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<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));
+
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ let key = i.contentWindow.navigation.currentEntry.key;
+
+ await i.contentWindow.navigation.navigate("#1").committed;
+
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry, i.contentWindow.navigation.entries()[1]);
+
+ const iWindow = i.contentWindow;
+ const iDOMException = iWindow.DOMException;
+ i.contentWindow.navigation.onnavigate = t.unreached_func("navigate should not fire");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("navigateerror should not fire");
+
+ let promises = i.contentWindow.navigation.traverseTo(key);
+ i.remove();
+ await assertBothRejectDOM(t, promises, "AbortError", iWindow, iDOMException);
+}, "traverseTo() promise rejections when detaching an iframe before onnavigate (same-document)");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-detach-same-document.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-detach-same-document.html
new file mode 100644
index 0000000000..b0308b8df8
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-detach-same-document.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<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));
+
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ let key = i.contentWindow.navigation.currentEntry.key;
+
+ await i.contentWindow.navigation.navigate("#1").committed;
+
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry, i.contentWindow.navigation.entries()[1]);
+
+ const iWindow = i.contentWindow;
+ const iDOMException = iWindow.DOMException;
+ i.contentWindow.navigation.onnavigate = () => i.remove();
+
+ await assertBothRejectDOM(t, i.contentWindow.navigation.traverseTo(key), "AbortError", iWindow, iDOMException);
+}, "traverseTo() promise rejections when detaching an iframe inside onnavigate (same-document)");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-intercept-rejected.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-intercept-rejected.html
new file mode 100644
index 0000000000..f934eae5d4
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-intercept-rejected.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.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));
+
+ const key0 = navigation.currentEntry.key;
+
+ location.href = "#1";
+
+ assert_equals(navigation.entries().length, 2);
+ const [entry0, entry1] = navigation.entries();
+ assert_equals((new URL(entry0.url)).hash, "");
+ assert_equals((new URL(entry1.url)).hash, "#1");
+
+ const err = new Error("boo!");
+ const promise = Promise.reject(err);
+ promise.catch(() => {}); // prevent unhandled rejection testharness.js errors
+ navigation.onnavigate = e => e.intercept({ handler: () => promise });
+
+ const result = navigation.traverseTo(key0);
+ await assertCommittedFulfillsFinishedRejectsExactly(t, result, entry0, err);
+ assert_equals(navigation.currentEntry, entry0);
+}, "traverseTo() promise rejection with rejected intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-intercept.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-intercept.html
new file mode 100644
index 0000000000..a4cb4e711d
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-intercept.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.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));
+
+ const key0 = navigation.currentEntry.key;
+
+ location.href = "#1";
+
+ assert_equals(navigation.entries().length, 2);
+ const [entry0, entry1] = navigation.entries();
+ assert_equals((new URL(entry0.url)).hash, "");
+ assert_equals((new URL(entry1.url)).hash, "#1");
+
+ navigation.onnavigate = e => e.intercept({ handler: () => Promise.resolve({ abc: "def" }) });
+
+ const result = navigation.traverseTo(key0);
+ await assertBothFulfill(t, result, entry0);
+ assert_equals(navigation.currentEntry, entry0);
+}, "traverseTo() and intercept() with a fulfilled promise");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-invalid-key.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-invalid-key.html
new file mode 100644
index 0000000000..42be40bfa7
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-invalid-key.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+promise_test(async t => {
+ await assertBothRejectDOM(t, navigation.traverseTo("not a real key"), "InvalidStateError");
+}, "traverseTo() with invalid key");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-repeated.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-repeated.html
new file mode 100644
index 0000000000..d1754d6729
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo-repeated.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.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));
+
+ const key = navigation.currentEntry.key;
+ const entry = navigation.currentEntry;
+ await navigation.navigate("#1").committed;
+
+ const result1 = navigation.traverseTo(key);
+ const result2 = navigation.traverseTo(key);
+
+ await assertBothFulfill(t, result1, entry);
+ assert_not_equals(result1, result2);
+ assert_equals(result2.committed, result1.committed, "committed promises must be equal");
+ assert_equals(result2.finished, result1.finished, "finished promises must be equal");
+}, "Repeated navigation.traverseTo() with the same key");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo.html b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo.html
new file mode 100644
index 0000000000..8e00f5ba91
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/return-value/traverseTo.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.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));
+
+ const key0 = navigation.currentEntry.key;
+
+ location.href = "#1";
+
+ assert_equals(navigation.entries().length, 2);
+ const [entry0, entry1] = navigation.entries();
+ assert_equals((new URL(entry0.url)).hash, "");
+ assert_equals((new URL(entry1.url)).hash, "#1");
+
+ const result = navigation.traverseTo(key0);
+ await assertBothFulfill(t, result, entry0);
+ assert_equals(navigation.currentEntry, entry0);
+}, "traverseTo() promises");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/sandboxing-back-parent.html b/testing/web-platform/tests/navigation-api/navigation-methods/sandboxing-back-parent.html
new file mode 100644
index 0000000000..6eee3f917c
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/sandboxing-back-parent.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="return-value/resources/helpers.js"></script>
+<iframe id="i" src="/common/blank.html?startI" sandbox="allow-scripts allow-same-origin"></iframe>
+
+<script>
+// Intended setup:
+// Step 0:
+// - Parent: (current URL)
+// - i: /common/blank.html?startI
+// Step 1:
+// - Parent: (current URL)
+// - i: resources/navigation-back.html
+// Step 2:
+// - Parent: (current URL)#end
+// - i: resources/navigation-back.html
+//
+// Then, calling navigation.back() in i will take is from step 2 to step 0, which would navigate the parent.
+// That is not allowed, so the call to back() must reject.
+
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ i.contentWindow.location.href = new URL("resources/navigation-back.html", location.href);
+ await new Promise(resolve => i.onload = resolve);
+
+ location.hash = "#end";
+ await new Promise(resolve => window.onhashchange = resolve);
+
+ navigation.onnavigate = t.unreached_func("navigate must not fire");
+ navigation.onnavigateerror = t.unreached_func("navigateerror must not fire");
+ window.onpopstate = t.unreached_func("popstate must not fire");
+ window.onhashchange = t.unreached_func("hashchange must not fire");
+
+ await assertBothRejectDOM(t, i.contentWindow.doNavigationBack(), "SecurityError", i.contentWindow);
+}, "A sandboxed iframe cannot navigate its parent via its own navigation object by using back()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/sandboxing-back-sibling.html b/testing/web-platform/tests/navigation-api/navigation-methods/sandboxing-back-sibling.html
new file mode 100644
index 0000000000..97c03ea0c2
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/sandboxing-back-sibling.html
@@ -0,0 +1,44 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="return-value/resources/helpers.js"></script>
+<iframe id="i" src="/common/blank.html?startI" sandbox="allow-same-origin"></iframe>
+<iframe id="i2" src="/common/blank.html?startI2" sandbox="allow-scripts allow-same-origin"></iframe>
+
+<script>
+// Intended setup:
+// Step 0:
+// - Parent: (current URL)
+// - i: /common/blank.html?startI
+// - i2: /common/blank.html?startI2
+// Step 1:
+// - Parent: (current URL)
+// - i: /common/blank.html?startI
+// - i2: resources/navigation-back.html
+// Step 2:
+// - Parent: (current URL)
+// - i: /common/blank.html?endI
+// - i2: resources/navigation-back.html
+//
+// Then, calling navigation.back() in i2 will take is from step 2 to step 0, which would navigate i.
+// That is not allowed, so the call to back() must reject.
+
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ i2.contentWindow.location.href = new URL("resources/navigation-back.html", location.href);
+ await new Promise(resolve => i2.onload = resolve);
+
+ i.contentWindow.location.href = "/common/blank.html?endI";
+ await new Promise(resolve => i.onload = resolve);
+
+ i.contentWindow.navigation.onnavigate = t.unreached_func("navigate must not fire");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("navigateerror must not fire");
+ i.contentWindow.onbeforeunload = t.unreached_func("beforeunload must not fire");
+ i.contentWindow.onunload = t.unreached_func("unload must not fire");
+ i.contentWindow.onpagehide = t.unreached_func("pagehide must not fire");
+ i.contentWindow.onpopstate = t.unreached_func("popstate must not fire");
+
+ await assertBothRejectDOM(t, i2.contentWindow.doNavigationBack(), "SecurityError", i2.contentWindow);
+}, "A sandboxed iframe cannot navigate its sibling via its own navigation object by using back()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/sandboxing-navigate-parent.html b/testing/web-platform/tests/navigation-api/navigation-methods/sandboxing-navigate-parent.html
new file mode 100644
index 0000000000..b7af32443f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/sandboxing-navigate-parent.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="resources/navigate-parent.html" sandbox="allow-scripts allow-same-origin"></iframe>
+
+<script>
+async_test(t => {
+ window.onload = t.step_func_done(() => {
+ i.contentWindow.navigateParent();
+
+ const destinationURL = (new URL("#2", location.href)).href;
+ assert_equals(location.href, destinationURL);
+ assert_equals(navigation.currentEntry.url, destinationURL);
+ });
+}, "A sandboxed iframe can use its sibling's navigation object to call navigate(), as long as allow-same-origin is present");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/sandboxing-navigate-sibling.html b/testing/web-platform/tests/navigation-api/navigation-methods/sandboxing-navigate-sibling.html
new file mode 100644
index 0000000000..c8e76fc4a6
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/sandboxing-navigate-sibling.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html" sandbox="allow-same-origin"></iframe>
+<iframe id="i2" src="resources/navigate-sibling.html" sandbox="allow-scripts allow-same-origin"></iframe>
+
+<script>
+async_test(t => {
+ window.onload = t.step_func(() => {
+ i2.contentWindow.navigateSibling();
+
+ i.onload = t.step_func_done(() => {
+ const destinationURL = (new URL("/common/blank.html?2", location.href)).href;
+ assert_equals(i.contentWindow.location.href, destinationURL);
+ assert_equals(i.contentWindow.navigation.currentEntry.url, destinationURL);
+ });
+ });
+}, "A sandboxed iframe can use its parent's navigation object to call navigate(), as long as allow-same-origin is present");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-after-adding-iframe.html b/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-after-adding-iframe.html
new file mode 100644
index 0000000000..5ab2820551
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-after-adding-iframe.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+async_test(t => {
+ let start_length = history.length;
+
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(() => {
+ navigation.navigate("#top");
+ assert_equals(navigation.entries().length, 2);
+
+ let i = document.createElement("iframe");
+ i.src = "/common/blank.html";
+ document.body.appendChild(i);
+ i.onload = () => t.step_timeout(() => {
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ i.contentWindow.navigation.navigate("#fragment");
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ i.contentWindow.navigation.back().committed.then(t.step_func(() => {
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 0);
+ return i.contentWindow.navigation.forward().committed;
+ })).then(t.step_func_done(() => {
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+ }));
+ }, 0);
+ }, 0);
+}, "navigation.traverseTo() should work in an iframe that is not present in all history entries");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-after-data-url.html b/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-after-data-url.html
new file mode 100644
index 0000000000..0b21a741d3
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-after-data-url.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.src = "data:text/html,";
+ document.body.appendChild(i);
+ let start_length = history.length;
+
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(() => {
+ i.contentWindow.location = "/common/blank.html";
+ i.onload = () => t.step_timeout(() => {
+ assert_equals(history.length, start_length + 1);
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ i.contentWindow.navigation.navigate("#fragment");
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ i.contentWindow.navigation.back().committed.then(t.step_func(() => {
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 0);
+ return i.contentWindow.navigation.forward().committed;
+ })).then(t.step_func_done(() => {
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+ }));
+ }, 0);
+ }, 0);
+}, "navigation.traverseTo() should work in an iframe that started at a data: url");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-cross-document.html b/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-cross-document.html
new file mode 100644
index 0000000000..2540639bad
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-cross-document.html
@@ -0,0 +1,41 @@
+<!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 => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(() => {
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ let next_step = "goto";
+ let key = i.contentWindow.navigation.currentEntry.key;
+ i.contentWindow.navigation.navigate("?1");
+ i.onload = t.step_func(() => {
+ if (next_step == "goto") {
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry, i.contentWindow.navigation.entries()[1]);
+ i.contentWindow.navigation.traverseTo(key);
+ next_step = "forward";
+ } else if (next_step == "forward") {
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(key, i.contentWindow.navigation.currentEntry.key);
+ assert_equals(i.contentWindow.navigation.currentEntry, i.contentWindow.navigation.entries()[0]);
+ assert_true(i.contentWindow.navigation.canGoForward);
+ i.contentWindow.navigation.forward();
+ next_step = "back";
+ } else if (next_step == "back") {
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry, i.contentWindow.navigation.entries()[1]);
+ assert_true(i.contentWindow.navigation.canGoBack);
+ i.contentWindow.navigation.back();
+ next_step = "finish";
+ } else if (next_step == "finish") {
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry, i.contentWindow.navigation.entries()[0]);
+ t.done();
+ }
+ });
+ }, 0);
+}, "cross-document traverseTo(), back(), and forward()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-detach-between-navigate-and-navigatesuccess.html b/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-detach-between-navigate-and-navigatesuccess.html
new file mode 100644
index 0000000000..43bde9a103
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-detach-between-navigate-and-navigatesuccess.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="return-value/resources/helpers.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+async_test(t => {
+ window.onload = t.step_func(() => {
+ const iWindow = i.contentWindow;
+ const iDOMException = iWindow.DOMException;
+ const iNavigationHistoryEntry = iWindow.NavigationHistoryEntry;
+
+ i.contentWindow.navigation.navigate("#1").finished.then(t.step_func(() => {
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ let key = i.contentWindow.navigation.entries()[0].key;
+
+ let onnavigateerror_called = false;
+ let result;
+
+ i.contentWindow.navigation.onnavigate = t.step_func(e => {
+ // 1. The intercept handler runs.
+ // 2. "t.step_timeout(handlerRunResolve, 0)" executes handlerRunResolve in a macro task.
+ // 3. In the next microtask, the iframe is removed.
+ // 4. "t.step_timeout(resolve, 5)" executes and the intercept handler promise resolves.
+ let handlerRunResolve;
+ new Promise(r => handlerRunResolve = r).then(() => i.remove());
+ e.intercept({ handler() {
+ t.step_timeout(handlerRunResolve, 0);
+ return new Promise(resolve => t.step_timeout(resolve, 5));
+ }});
+ });
+
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("navigatesuccess must not fire");
+
+ i.contentWindow.navigation.onnavigateerror = t.step_func(e => {
+ assert_false(onnavigateerror_called);
+ onnavigateerror_called = true;
+ assert_equals(e.filename, location.href);
+ assert_greater_than(e.lineno, 0);
+ assert_greater_than(e.colno, 0);
+
+ assertCommittedFulfillsFinishedRejectsExactly(t, result, iWindow.navigation.currentEntry, e.error, iWindow, iDOMException, iNavigationHistoryEntry).then(
+ () => t.done(),
+ t.step_func(err => { throw err; })
+ );
+ });
+
+ result = i.contentWindow.navigation.traverseTo(key);
+ }));
+ });
+}, "Detach a window between when a traverseTo() fires navigate and navigatesuccess");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-multiple-steps.html b/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-multiple-steps.html
new file mode 100644
index 0000000000..059eb214a8
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-multiple-steps.html
@@ -0,0 +1,25 @@
+<!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));
+ assert_equals(navigation.entries().length, 1);
+ let key0 = navigation.currentEntry.key;
+ await navigation.navigate("#1").committed;
+ await navigation.navigate("#2").committed;
+ let key2 = navigation.currentEntry.key;
+ assert_equals(navigation.entries().length, 3);
+
+ await navigation.traverseTo(key0).committed;
+ assert_equals(navigation.entries().length, 3);
+ assert_equals(navigation.currentEntry, navigation.entries()[0]);
+ assert_equals(key0, navigation.currentEntry.key);
+ await navigation.traverseTo(key2).committed;
+ assert_equals(navigation.entries().length, 3);
+ assert_equals(navigation.currentEntry, navigation.entries()[2]);
+ assert_equals(key2, navigation.currentEntry.key);
+}, "goto() can precisely traverse multiple steps in the joint session history");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-navigates-multiple-iframes.html b/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-navigates-multiple-iframes.html
new file mode 100644
index 0000000000..3d6adb5341
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-navigates-multiple-iframes.html
@@ -0,0 +1,45 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<iframe id="i1" src="/common/blank.html"></iframe>
+<iframe id="i2" src="resources/slow-no-store.py"></iframe>
+<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));
+
+ i1.src = "/common/blank.html?navigated";
+ await new Promise(resolve => i1.onload = resolve);
+ i2.src = "/common/blank.html?navigated";
+ await new Promise(resolve => i2.onload = resolve);
+ assert_equals(navigation.entries().length, 1);
+ assert_equals(i1.contentWindow.navigation.entries().length, 2);
+ assert_equals(i2.contentWindow.navigation.entries().length, 2);
+ assert_equals(i1.contentWindow.navigation.currentEntry.index, 1);
+ assert_equals(i2.contentWindow.navigation.currentEntry.index, 1);
+
+ function collectKeysAndIds(win) {
+ return win.navigation.entries().map(e => [e.key, e.id]).flat();
+ }
+ let i1_keys_and_ids_before_back = collectKeysAndIds(i1.contentWindow);
+ let i2_keys_and_ids_before_back = collectKeysAndIds(i2.contentWindow);
+
+ // Go back to a point that requires both frames to navigate. Because i2 is
+ // going back to a slow, un-cached document, i1 will likely complete before
+ // the server sends the response for i2. This combination of a slow and fast
+ // traversal is less common than the case where multiple iframes navigate at
+ // similar speeds, and caused a bug in chromium.
+ i1.contentWindow.navigation.traverseTo(i1.contentWindow.navigation.entries()[0].key);
+ await Promise.all(
+ [ new Promise(resolve => i1.onload = resolve),
+ new Promise(resolve => i2.onload = resolve) ]);
+ assert_equals(i1.contentWindow.navigation.currentEntry.index, 0);
+ assert_equals(i2.contentWindow.navigation.currentEntry.index, 0);
+
+ assert_array_equals(i1_keys_and_ids_before_back, collectKeysAndIds(i1.contentWindow));
+ assert_array_equals(i2_keys_and_ids_before_back, collectKeysAndIds(i2.contentWindow));
+}, "entries() should be correct after a traversal that navigates multiple browsing contexts");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-same-document.html b/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-same-document.html
new file mode 100644
index 0000000000..1a32bce99c
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-same-document.html
@@ -0,0 +1,43 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(() => {
+ assert_equals(navigation.entries().length, 1);
+
+ let onnavigate_count = 0;
+ navigation.onnavigate = () => onnavigate_count++;
+
+ let key = navigation.currentEntry.key;
+ navigation.navigate("#").committed
+ .then(t.step_func(() => {
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry, navigation.entries()[1]);
+ assert_not_equals(key, navigation.currentEntry.key);
+ return navigation.traverseTo(key).committed;
+ }))
+ .then(t.step_func(() => {
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry, navigation.entries()[0]);
+ assert_equals(key, navigation.currentEntry.key);
+ assert_true(navigation.canGoForward);
+ return navigation.forward().committed;
+ }))
+ .then(t.step_func(() => {
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry, navigation.entries()[1]);
+ assert_true(navigation.canGoBack);
+ return navigation.back().committed;
+ }))
+ .then(t.step_func_done(() => {
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry, navigation.entries()[0]);
+ assert_equals(key, navigation.currentEntry.key);
+ assert_equals(onnavigate_count, 4);
+ }));
+ }, 0);
+}, "same-document navigate.traverseTo(), back(), and forward()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-with-cross-origin-in-history.html b/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-with-cross-origin-in-history.html
new file mode 100644
index 0000000000..a6b7745584
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/navigation-methods/traverseTo-with-cross-origin-in-history.html
@@ -0,0 +1,37 @@
+<!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 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);
+
+ let start_length = history.length;
+
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(() => {
+ i.contentWindow.location = "/common/blank.html";
+ i.onload = () => t.step_timeout(() => {
+ assert_equals(history.length, start_length + 1);
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ i.contentWindow.navigation.navigate("#fragment");
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ i.contentWindow.navigation.back().committed.then(t.step_func(() => {
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 0);
+ return i.contentWindow.navigation.forward().committed;
+ })).then(t.step_func_done(() => {
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+ }));
+ }, 0);
+ }, 0);
+}, "navigation.traverseTo() should work in an iframe that has navigated across origins");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/README.md b/testing/web-platform/tests/navigation-api/ordering-and-transition/README.md
new file mode 100644
index 0000000000..628b22ec48
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/README.md
@@ -0,0 +1,26 @@
+# Navigation API ordering/transition tests
+
+These are meant to test the ordering between various events and promises, as
+well as in some cases how the `navigation.transition` values changes.
+
+Some of them use the `Recorder` framework in `resources/helpers.mjs`, and others
+test tricky cases (e.g. reentrancy) in a more ad-hoc way.
+
+<https://github.com/WICG/navigation-api/#complete-event-sequence> is a useful
+reference for the intent of these tests.
+
+Note:
+
+* Variants specifically exist for `currententrychange` because an event listener
+ existing for `currententrychange` causes code to run, and thus microtasks to run,
+ at a very specific point in the navigation-commit lifecycle. We want to test
+ that it doesn't impact the ordering.
+* Similarly we test that `intercept()` does not change
+ the ordering compared to no `intercept()` call, for same-document
+ navigations, by trying to ensure most variants have appropriate
+ `intercept()` counterparts with similar orderings.
+
+TODOs:
+
+* Also test `popstate` and `hashchange` once
+ <https://github.com/whatwg/html/issues/1792> is fixed.
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/anchor-download-intercept-reject.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/anchor-download-intercept-reject.html
new file mode 100644
index 0000000000..4869cedd25
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/anchor-download-intercept-reject.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+
+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));
+
+ const from = navigation.currentEntry;
+ const expectedError = new Error("boo");
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "transition.finished rejected"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler() {
+ recorder.record("handler run");
+ return Promise.reject(expectedError);
+ }});
+ });
+
+ let a = document.createElement("a");
+ a.href = "/common/blank.html#1";
+ a.download = "";
+ document.body.appendChild(a);
+ a.click();
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["currententrychange", "#1", { from, navigationType: "push" }],
+ ["handler run", "#1", { from, navigationType: "push" }],
+ ["promise microtask", "#1", { from, navigationType: "push" }],
+ ["navigateerror", "#1", { from, navigationType: "push" }],
+ ["transition.finished rejected", "#1", null],
+ ]);
+
+ recorder.assertErrorsAre(expectedError);
+}, "event and promise ordering for <a download> intercepted by passing a rejected promise to intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/anchor-download-intercept.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/anchor-download-intercept.html
new file mode 100644
index 0000000000..23326c827b
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/anchor-download-intercept.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+
+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));
+
+ const from = navigation.currentEntry;
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "transition.finished fulfilled"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler() { recorder.record("handler run"); } });
+ });
+
+ let a = document.createElement("a");
+ a.href = "/common/blank.html#1";
+ a.download = "";
+ document.body.appendChild(a);
+ a.click();
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["currententrychange", "#1", { from, navigationType: "push" }],
+ ["handler run", "#1", { from, navigationType: "push" }],
+ ["promise microtask", "#1", { from, navigationType: "push" }],
+ ["navigatesuccess", "#1", { from, navigationType: "push" }],
+ ["transition.finished fulfilled", "#1", null],
+ ]);
+}, "event and promise ordering for <a download> intercepted by intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/anchor-download.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/anchor-download.html
new file mode 100644
index 0000000000..c6af13c3e0
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/anchor-download.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="/common/blank.html"></iframe>
+<script>
+promise_test(async t => {
+ await new Promise(resolve => window.onload = resolve);
+
+ let navigate_called = false;
+ i.contentWindow.navigation.onnavigate = () => navigate_called = true;
+ navigation.onnavigate = t.unreached_func("navigate must not fire");
+
+ let a = i.contentDocument.createElement("a");
+ a.href = "?1";
+ a.download = "";
+ i.contentDocument.body.appendChild(a);
+ a.click();
+
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("navigatesuccess must not fire");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("navigateerror must not fire");
+ await new Promise(resolve => t.step_timeout(resolve, 20));
+ assert_true(navigate_called);
+}, "<a download> fires navigate, but not navigatesuccess or navigateerror when not intercepted by intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/back-same-document-intercept-reject.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/back-same-document-intercept-reject.html
new file mode 100644
index 0000000000..c0d4f55027
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/back-same-document-intercept-reject.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+
+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));
+ await navigation.navigate("#1").finished;
+
+ const from = navigation.currentEntry;
+ const expectedError = new Error("boo");
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "transition.finished rejected"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler() {
+ recorder.record("handler run");
+ return Promise.reject(expectedError);
+ }});
+ });
+
+ const result = navigation.back();
+ recorder.setUpResultListeners(result);
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["promise microtask", "#1", null],
+ ["navigate", "#1", null],
+ ["currententrychange", "", { from, navigationType: "traverse" }],
+ ["handler run", "", { from, navigationType: "traverse" }],
+ ["committed fulfilled", "", { from, navigationType: "traverse" }],
+ ["navigateerror", "", { from, navigationType: "traverse" }],
+ ["finished rejected", "", null],
+ ["transition.finished rejected", "", null]
+ ]);
+
+ recorder.assertErrorsAre(expectedError);
+}, "event and promise ordering for same-document navigation.back() intercepted by passing a rejected promise to intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/back-same-document-intercept.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/back-same-document-intercept.html
new file mode 100644
index 0000000000..7bd248bc9c
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/back-same-document-intercept.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+
+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));
+ await navigation.navigate("#1").finished;
+
+ const from = navigation.currentEntry;
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "transition.finished fulfilled"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler() { recorder.record("handler run"); } });
+ });
+
+ const result = navigation.back();
+ recorder.setUpResultListeners(result);
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["promise microtask", "#1", null],
+ ["navigate", "#1", null],
+ ["currententrychange", "", { from, navigationType: "traverse" }],
+ ["handler run", "", { from, navigationType: "traverse" }],
+ ["committed fulfilled", "", { from, navigationType: "traverse" }],
+ ["navigatesuccess", "", { from, navigationType: "traverse" }],
+ ["finished fulfilled", "", null],
+ ["transition.finished fulfilled", "", null]
+ ]);
+}, "event and promise ordering for same-document navigation.back() intercepted by intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/back-same-document.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/back-same-document.html
new file mode 100644
index 0000000000..76c3e99311
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/back-same-document.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+
+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));
+ await navigation.navigate("#1").finished;
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "finished fulfilled"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ const result = navigation.back();
+ recorder.setUpResultListeners(result);
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["promise microtask", "#1", null],
+ ["navigate", "#1", null],
+ ["currententrychange", "", null],
+ ["committed fulfilled", "", null],
+ ["navigatesuccess", "", null],
+ ["finished fulfilled", "", null],
+ ]);
+}, "event and promise ordering for same-document navigation.back()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/currententrychange-before-popstate-intercept.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/currententrychange-before-popstate-intercept.html
new file mode 100644
index 0000000000..c51c7c444c
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/currententrychange-before-popstate-intercept.html
@@ -0,0 +1,48 @@
+<!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));
+ await navigation.navigate("#foo").committed;
+ assert_equals(navigation.entries().length, 2);
+
+ navigation.onnavigate = e => e.intercept();
+
+ let oncurrententrychange_back_called = false;
+ let onpopstate_back_called = false;
+ window.onpopstate = t.step_func(e => {
+ onpopstate_back_called = true;
+ assert_true(oncurrententrychange_back_called);
+ });
+ navigation.oncurrententrychange = t.step_func(e => {
+ oncurrententrychange_back_called = true;
+ assert_false(onpopstate_back_called);
+ });
+ let back_result = navigation.back();
+ assert_false(oncurrententrychange_back_called);
+ assert_false(onpopstate_back_called);
+ await back_result.finished;
+ assert_true(oncurrententrychange_back_called);
+ assert_true(onpopstate_back_called);
+
+ let oncurrententrychange_forward_called = false;
+ let onpopstate_forward_called = false;
+ window.onpopstate = t.step_func(e => {
+ onpopstate_forward_called = true;
+ assert_true(oncurrententrychange_forward_called);
+ });
+ navigation.oncurrententrychange = t.step_func(e => {
+ oncurrententrychange_forward_called = true;
+ assert_false(onpopstate_forward_called);
+ });
+ let forward_result = navigation.forward();
+ assert_false(oncurrententrychange_forward_called);
+ assert_false(onpopstate_forward_called);
+ await forward_result.finished;
+ assert_true(oncurrententrychange_back_called);
+ assert_true(onpopstate_forward_called);
+}, "currententrychange fires before popstate for navigation.back() and navigation.forward()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/currententrychange-dispose-ordering.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/currententrychange-dispose-ordering.html
new file mode 100644
index 0000000000..4ca9ba2980
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/currententrychange-dispose-ordering.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(t => {
+ let oncurrententrychange_called = false;
+ let ondispose_called = false;
+
+ let original_entry = navigation.currentEntry;
+ original_entry.ondispose = t.step_func(() => {
+ assert_true(oncurrententrychange_called);
+ ondispose_called = true;
+ });
+
+ navigation.oncurrententrychange = t.step_func(e => {
+ oncurrententrychange_called = true;
+ assert_equals(e.from, original_entry);
+ assert_equals(e.from.index, -1);
+ assert_equals(e.navigationType, "replace");
+ assert_equals(navigation.currentEntry.index, 0);
+ });
+ navigation.navigate("#foo", { history: "replace" });
+ assert_true(oncurrententrychange_called);
+ assert_true(ondispose_called);
+}, "Ordering between Navigation currententrychange and NavigationHistoryEntry dispose events");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/intercept-async.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/intercept-async.html
new file mode 100644
index 0000000000..f2ca096950
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/intercept-async.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+
+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));
+
+ const from = navigation.currentEntry;
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "transition.finished fulfilled"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ async handler() {
+ recorder.record("handler sync");
+ await Promise.resolve();
+ recorder.record("handler after microtask");
+ await new Promise(r => t.step_timeout(r, 0));
+ recorder.record("handler after setTimeout");
+ } });
+ });
+
+ const result = navigation.navigate("#1");
+ recorder.setUpResultListeners(result);
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["currententrychange", "#1", { from, navigationType: "push" }],
+ ["handler sync", "#1", { from, navigationType: "push" }],
+ ["handler after microtask", "#1", { from, navigationType: "push" }],
+ ["committed fulfilled", "#1", { from, navigationType: "push" }],
+ ["promise microtask", "#1", { from, navigationType: "push" }],
+ ["handler after setTimeout", "#1", { from, navigationType: "push" }],
+ ["navigatesuccess", "#1", { from, navigationType: "push" }],
+ ["finished fulfilled", "#1", null],
+ ["transition.finished fulfilled", "#1", null],
+ ]);
+}, "ordering when intercept() handler has sync and async blocks");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-canceled.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-canceled.html
new file mode 100644
index 0000000000..eef10cd173
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-canceled.html
@@ -0,0 +1,39 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script type="module">
+import { Recorder } from "./resources/helpers.mjs";
+
+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));
+
+ const recorder = new Recorder({
+ finalExpectedEvent: "promise microtask"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", t.step_func(e => {
+ e.preventDefault();
+ }));
+
+ location.href = "/common/blank.html#1";
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["AbortSignal abort", "", null],
+ ["navigateerror", "", null],
+ ["promise microtask", "", null]
+ ]);
+
+ recorder.assertErrorsAreAbortErrors();
+}, "event and promise ordering for the location.href setter where the navigate event is canceled");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-double-intercept.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-double-intercept.html
new file mode 100644
index 0000000000..36ae5ce4ab
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-double-intercept.html
@@ -0,0 +1,61 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+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));
+
+ const fromStart = navigation.currentEntry;
+ let fromHash1;
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "transition.finished fulfilled"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler() {
+ recorder.record("handler run");
+ return new Promise(r => t.step_timeout(r, 1));
+ }});
+
+ if (location.hash === "#1") {
+ fromHash1 = navigation.currentEntry;
+ }
+ });
+
+ location.href = "/common/blank.html#1";
+ location.href = "/common/blank.html#2";
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["currententrychange", "#1", { from: fromStart, navigationType: "push" }],
+ ["handler run", "#1", { from: fromStart, navigationType: "push" }],
+ ["AbortSignal abort", "#1", { from: fromStart, navigationType: "push" }],
+ ["navigateerror", "#1", { from: fromStart, navigationType: "push" }],
+
+ ["navigate", "#1", null],
+ ["currententrychange", "#2", { from: fromHash1, navigationType: "push" }],
+ ["handler run", "#2", { from: fromHash1, navigationType: "push" }],
+ ["transition.finished rejected", "#2", { from: fromHash1, navigationType: "push" }],
+ ["promise microtask", "#2", { from: fromHash1, navigationType: "push" }],
+ ["navigatesuccess", "#2", { from: fromHash1, navigationType: "push" }],
+ ["transition.finished fulfilled", "#2", null]
+ ]);
+
+ recorder.assertErrorsAreAbortErrors();
+}, "event and promise ordering when location.href is set repeatedly and handled by intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-intercept-reentrant.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-intercept-reentrant.html
new file mode 100644
index 0000000000..3fabf52bfb
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-intercept-reentrant.html
@@ -0,0 +1,60 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+
+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));
+
+ const from = navigation.currentEntry;
+ let firstNavigate = true;
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "transition.finished fulfilled"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler() {
+ recorder.record("handler run");
+ return new Promise(resolve => t.step_timeout(resolve, 2));
+ }});
+
+ if (firstNavigate) {
+ firstNavigate = false;
+
+ location.href = "/common/blank.html#2";
+ }
+ });
+
+ location.href = "/common/blank.html#1";
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["AbortSignal abort", "", null],
+ ["navigateerror", "", null],
+
+ ["navigate", "", null],
+ ["currententrychange", "#2", { from, navigationType: "push" }],
+ ["handler run", "#2", { from, navigationType: "push" }],
+ ["promise microtask", "#2", { from, navigationType: "push" }],
+ ["navigatesuccess", "#2", { from, navigationType: "push" }],
+ ["transition.finished fulfilled", "#2", null]
+ ]);
+
+ recorder.assertErrorsAreAbortErrors();
+}, "event and promise ordering for the location.href setter intercepted by intercept() where we set location.href again inside the navigate handler");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-intercept-reject.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-intercept-reject.html
new file mode 100644
index 0000000000..b4aeb726b3
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-intercept-reject.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+
+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));
+
+ const from = navigation.currentEntry;
+ const expectedError = new Error("boo");
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "transition.finished rejected"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler() {
+ recorder.record("handler run");
+ return Promise.reject(expectedError);
+ }});
+ });
+
+ location.href = "/common/blank.html#1";
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["currententrychange", "#1", { from, navigationType: "push" }],
+ ["handler run", "#1", { from, navigationType: "push" }],
+ ["promise microtask", "#1", { from, navigationType: "push" }],
+ ["navigateerror", "#1", { from, navigationType: "push" }],
+ ["transition.finished rejected", "#1", null],
+ ]);
+
+ recorder.assertErrorsAre(expectedError);
+}, "event and promise ordering for location.href setter intercepted by passing a rejected promise to intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-intercept.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-intercept.html
new file mode 100644
index 0000000000..bb861d37ae
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/location-href-intercept.html
@@ -0,0 +1,44 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+
+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));
+
+ const from = navigation.currentEntry;
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "transition.finished fulfilled"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler() { recorder.record("handler run"); } });
+ });
+
+ location.href = "/common/blank.html#1";
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["currententrychange", "#1", { from, navigationType: "push" }],
+ ["handler run", "#1", { from, navigationType: "push" }],
+ ["promise microtask", "#1", { from, navigationType: "push" }],
+ ["navigatesuccess", "#1", { from, navigationType: "push" }],
+ ["transition.finished fulfilled", "#1", null],
+ ]);
+}, "event and promise ordering for location.href setter intercepted by intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-204-205-download-then-same-document.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-204-205-download-then-same-document.html
new file mode 100644
index 0000000000..b7b6283fa7
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-204-205-download-then-same-document.html
@@ -0,0 +1,66 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script type="module">
+import { Recorder } from "./resources/helpers.mjs";
+
+const tests = [
+ ["204s", "/common/blank.html?pipe=status(204)"],
+ ["205s", "/common/blank.html?pipe=status(205)"],
+ ["Content-Disposition: attachment responses", "/common/blank.html?pipe=header(Content-Disposition,attachment)"]
+];
+
+for (const [description, url] of tests) {
+ promise_test(async t => {
+ const i = document.createElement("iframe");
+ i.src = "/common/blank.html";
+ document.body.append(i);
+ await new Promise(resolve => i.onload = () => t.step_timeout(resolve, 0));
+
+ const fromStart = i.contentWindow.navigation.currentEntry;
+
+ const recorder = new Recorder({
+ window: i.contentWindow,
+ finalExpectedEvent: "finished fulfilled 2"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ const result1 = i.contentWindow.navigation.navigate(url);
+ recorder.setUpResultListeners(result1, " 1");
+
+ // Give the server time to send the response. This is not strictly
+ // necessary (the expectations are the same either way) but it's better
+ // coverage if the server is done responding by this time; it guarantees
+ // we're hitting the code path for "got a 204/etc. and ignored it" instead
+ // of "didn't get a response yet".
+ await new Promise(resolve => t.step_timeout(resolve, 50));
+
+ const result2 = i.contentWindow.navigation.navigate("#1");
+ recorder.setUpResultListeners(result2, " 2");
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["AbortSignal abort", "", null],
+ ["navigateerror", "", null],
+
+ ["navigate", "", null],
+ ["currententrychange", "#1", null],
+ ["committed rejected 1", "#1", null],
+ ["finished rejected 1", "#1", null],
+ ["committed fulfilled 2", "#1", null],
+ ["promise microtask", "#1", null],
+ ["navigatesuccess", "#1", null],
+ ["finished fulfilled 2", "#1", null]
+ ]);
+
+ recorder.assertErrorsAreAbortErrors();
+ }, `event and promise ordering when navigate() is to a ${description} and then to a same-document navigation`);
+}
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-canceled.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-canceled.html
new file mode 100644
index 0000000000..2604a60e37
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-canceled.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script type="module">
+import { Recorder } from "./resources/helpers.mjs";
+
+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));
+
+ const recorder = new Recorder({
+ finalExpectedEvent: "finished rejected"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", t.step_func(e => {
+ e.preventDefault();
+ }));
+
+ const result = navigation.navigate("/common/blank.html#1");
+ recorder.setUpResultListeners(result);
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["AbortSignal abort", "", null],
+ ["navigateerror", "", null],
+ ["committed rejected", "", null],
+ ["finished rejected", "", null],
+ ["promise microtask", "", null]
+ ]);
+
+ recorder.assertErrorsAreAbortErrors();
+}, "event and promise ordering for navigation.navigate() where the navigate event is canceled");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-commit-after-transition-intercept.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-commit-after-transition-intercept.html
new file mode 100644
index 0000000000..70a840e692
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-commit-after-transition-intercept.html
@@ -0,0 +1,60 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+
+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));
+
+ const from = navigation.currentEntry;
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "transition.finished fulfilled"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ commit: "after-transition",
+ async handler() {
+ recorder.record("handler start");
+ await new Promise(r => t.step_timeout(r, 0));
+ recorder.record("handler async step 1a");
+ e.commit();
+ recorder.record("handler async step 1b");
+ await new Promise(r => t.step_timeout(r, 0));
+ recorder.record("handler async step 2");
+ }
+ });
+ });
+
+ const result = navigation.navigate("#1");
+ recorder.setUpResultListeners(result);
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["handler start", "", { from, navigationType: "push" }],
+ ["promise microtask", "", { from, navigationType: "push" }],
+ ["handler async step 1a", "", { from, navigationType: "push" }],
+ ["currententrychange", "#1", { from, navigationType: "push" }],
+ ["handler async step 1b", "#1", { from, navigationType: "push" }],
+ ["committed fulfilled", "#1", { from, navigationType: "push" }],
+ ["handler async step 2", "#1", { from, navigationType: "push" }],
+ ["navigatesuccess", "#1", { from, navigationType: "push" }],
+ ["finished fulfilled", "#1", null],
+ ["transition.finished fulfilled", "#1", null],
+ ]);
+}, "event and promise ordering for same-document navigation.navigate() intercepted by intercept() with { commit: 'after-transition' }");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-cross-document-double.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-cross-document-double.html
new file mode 100644
index 0000000000..262809a0ad
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-cross-document-double.html
@@ -0,0 +1,52 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i"></iframe>
+
+
+<script type="module">
+import { Recorder } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ await new Promise(resolve => {
+ i.src = "/common/blank.html";
+ i.onload = () => t.step_timeout(resolve, 0)
+ });
+
+ const fromStart = i.contentWindow.navigation.currentEntry;
+
+ const recorder = new Recorder({
+ window: i.contentWindow,
+ finalExpectedEvent: "promise microtask"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ // Use https://web-platform-tests.org/writing-tests/server-pipes.html to make
+ // sure the response doesn't come back quickly, since once the response comes
+ // back the page would be unloaded and that would break our test.
+ const result1 = i.contentWindow.navigation.navigate("?pipe=trickle(d100)");
+ recorder.setUpResultListeners(result1, " 1");
+
+ const result2 = i.contentWindow.navigation.navigate("?2");
+ recorder.setUpResultListeners(result2, " 2");
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["AbortSignal abort", "", null],
+ ["navigateerror", "", null],
+
+ ["navigate", "", null],
+ ["committed rejected 1", "", null],
+ ["finished rejected 1", "", null],
+ ["promise microtask", "", null]
+ ]);
+
+ recorder.assertErrorsAreAbortErrors();
+}, "event and promise ordering when navigate() is called to a cross-document destination, interrupting another navigate() to a cross-document destination");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-cross-document-event-order.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-cross-document-event-order.html
new file mode 100644
index 0000000000..34a9b79fb5
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-cross-document-event-order.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="i" src="resources/notify-top-early.html"></iframe>
+<script>
+async_test(t => {
+ let events = [];
+ function finish() {
+ assert_array_equals(events, ["onnavigate", "readystateinteractive", "domcontentloaded", "readystatecomplete", "onload", "onpageshow"]);
+ t.done();
+ };
+
+ window.onload = t.step_func(() => {
+ window.childStarted = () => {
+ i.contentWindow.navigation.onnavigatesuccess = () => events.push("onnavigatesuccess");
+ i.contentWindow.navigation.onnavigateerror = () => events.push("onnavigateerror");
+ i.contentWindow.onpageshow = () => events.push("onpageshow");
+ i.contentWindow.onhashchange = () => events.push("onhashchange");
+ i.contentWindow.onpopstate = () => events.push("onpopstate");
+ i.onload = t.step_func(() => {
+ events.push("onload");
+ t.step_timeout(finish, 0);
+ });
+ i.contentDocument.addEventListener("DOMContentLoaded", () => events.push("domcontentloaded"));
+ i.contentDocument.onreadystatechange = () => events.push("readystate" + i.contentDocument.readyState);
+ };
+ i.contentWindow.navigation.onnavigate = () => events.push("onnavigate");
+ i.contentWindow.navigation.navigate("?1").committed.then(
+ () => events.push("promisefulfilled"), () => events.push("promiserejected"));
+ });
+}, "navigate() event ordering for cross-document navigation");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-double-intercept.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-double-intercept.html
new file mode 100644
index 0000000000..6ce67b9af6
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-double-intercept.html
@@ -0,0 +1,68 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+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));
+
+ const fromStart = navigation.currentEntry;
+ let fromHash1;
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "transition.finished fulfilled"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler() {
+ recorder.record("handler run");
+ return new Promise(r => t.step_timeout(r, 1));
+ }});
+
+ if (location.hash === "#1") {
+ fromHash1 = navigation.currentEntry;
+ }
+ });
+
+ const result1 = navigation.navigate("/common/blank.html#1");
+ recorder.setUpResultListeners(result1, " 1");
+
+ const result2 = navigation.navigate("/common/blank.html#2");
+ recorder.setUpResultListeners(result2, " 2");
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["currententrychange", "#1", { from: fromStart, navigationType: "push" }],
+ ["handler run", "#1", { from: fromStart, navigationType: "push" }],
+ ["AbortSignal abort", "#1", { from: fromStart, navigationType: "push" }],
+ ["navigateerror", "#1", { from: fromStart, navigationType: "push" }],
+
+ ["navigate", "#1", null],
+ ["currententrychange", "#2", { from: fromHash1, navigationType: "push" }],
+ ["handler run", "#2", { from: fromHash1, navigationType: "push" }],
+ ["committed fulfilled 1", "#2", { from: fromHash1, navigationType: "push" }],
+ ["finished rejected 1", "#2", { from: fromHash1, navigationType: "push" }],
+ ["transition.finished rejected", "#2", { from: fromHash1, navigationType: "push" }],
+ ["committed fulfilled 2", "#2", { from: fromHash1, navigationType: "push" }],
+ ["promise microtask", "#2", { from: fromHash1, navigationType: "push" }],
+ ["navigatesuccess", "#2", { from: fromHash1, navigationType: "push" }],
+ ["finished fulfilled 2", "#2", null],
+ ["transition.finished fulfilled", "#2", null]
+ ]);
+
+ recorder.assertErrorsAreAbortErrors();
+}, "event and promise ordering when navigate() is called repeatedly and handled by intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-in-transition-finished.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-in-transition-finished.html
new file mode 100644
index 0000000000..9251cfe049
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-in-transition-finished.html
@@ -0,0 +1,69 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+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));
+
+ const fromStart = navigation.currentEntry;
+ let fromHash1;
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "transition.finished fulfilled",
+ finalExpectedEventCount: 2
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigatesuccess", t.step_func(() => {
+ if (location.hash === "#1") {
+ navigation.transition.finished.then(() => {
+ const result2 = navigation.navigate("/common/blank.html#2");
+ recorder.setUpResultListeners(result2, " 2");
+ });
+ }
+ }));
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler() { recorder.record("handler run"); } });
+
+ if (location.hash === "#1") {
+ fromHash1 = navigation.currentEntry;
+ }
+ });
+
+ const result1 = navigation.navigate("/common/blank.html#1");
+ recorder.setUpResultListeners(result1, " 1");
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["currententrychange", "#1", { from: fromStart, navigationType: "push" }],
+ ["handler run", "#1", { from: fromStart, navigationType: "push" }],
+ ["committed fulfilled 1", "#1", { from: fromStart, navigationType: "push" }],
+ ["promise microtask", "#1", { from: fromStart, navigationType: "push" }],
+ ["navigatesuccess", "#1", { from: fromStart, navigationType: "push" }],
+ ["finished fulfilled 1", "#1", null],
+ ["transition.finished fulfilled", "#1", null],
+
+ ["navigate", "#1", null],
+ ["currententrychange", "#2", { from: fromHash1, navigationType: "push" }],
+ ["handler run", "#2", { from: fromHash1, navigationType: "push" }],
+ ["committed fulfilled 2", "#2", { from: fromHash1, navigationType: "push" }],
+ ["navigatesuccess", "#2", { from: fromHash1, navigationType: "push" }],
+ ["finished fulfilled 2", "#2", null],
+ ["transition.finished fulfilled", "#2", null]
+ ]);
+}, "event and promise ordering when navigate() is called inside the transition.finished promise handler");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-intercept-stop.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-intercept-stop.html
new file mode 100644
index 0000000000..5d126a8c2f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-intercept-stop.html
@@ -0,0 +1,52 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+
+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));
+
+ const from = navigation.currentEntry;
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "transition.finished rejected"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler() { recorder.record("handler run"); } });
+ });
+
+ const result = navigation.navigate("/common/blank.html#1");
+ recorder.setUpResultListeners(result);
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ window.stop();
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["currententrychange", "#1", { from, navigationType: "push" }],
+ ["handler run", "#1", { from, navigationType: "push" }],
+ ["AbortSignal abort", "#1", { from, navigationType: "push" }],
+ ["navigateerror", "#1", { from, navigationType: "push" }],
+ ["committed fulfilled", "#1", null],
+ ["promise microtask", "#1", null],
+ ["finished rejected", "#1", null],
+ ["transition.finished rejected", "#1", null],
+ ]);
+
+ recorder.assertErrorsAreAbortErrors();
+}, "event and promise ordering for navigation.navigate() intercepted by intercept() but then stopped using window.stop()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-intercept.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-intercept.html
new file mode 100644
index 0000000000..f76f20bf85
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-intercept.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+
+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));
+
+ const from = navigation.currentEntry;
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "transition.finished fulfilled"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler() { recorder.record("handler run"); } });
+ });
+
+ const result = navigation.navigate("#1");
+ recorder.setUpResultListeners(result);
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["currententrychange", "#1", { from, navigationType: "push" }],
+ ["handler run", "#1", { from, navigationType: "push" }],
+ ["committed fulfilled", "#1", { from, navigationType: "push" }],
+ ["promise microtask", "#1", { from, navigationType: "push" }],
+ ["navigatesuccess", "#1", { from, navigationType: "push" }],
+ ["finished fulfilled", "#1", null],
+ ["transition.finished fulfilled", "#1", null],
+ ]);
+}, "event and promise ordering for same-document navigation.navigate() intercepted by intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-same-document-intercept-reentrant.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-same-document-intercept-reentrant.html
new file mode 100644
index 0000000000..86f4e06731
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-same-document-intercept-reentrant.html
@@ -0,0 +1,66 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+
+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));
+
+ const from = navigation.currentEntry;
+ let firstNavigate = true;
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "transition.finished fulfilled"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler() {
+ recorder.record("handler run");
+ return new Promise(r => t.step_timeout(r, 2));
+ }});
+
+ if (firstNavigate) {
+ firstNavigate = false;
+
+ const result2 = navigation.navigate("#2");
+ recorder.setUpResultListeners(result2, " 2");
+ }
+ });
+
+ const result1 = navigation.navigate("#1");
+ recorder.setUpResultListeners(result1, " 1");
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["AbortSignal abort", "", null],
+ ["navigateerror", "", null],
+
+ ["navigate", "", null],
+ ["currententrychange", "#2", { from, navigationType: "push" }],
+ ["handler run", "#2", { from, navigationType: "push" }],
+ ["committed fulfilled 2", "#2", { from, navigationType: "push" }],
+ ["committed rejected 1", "#2", { from, navigationType: "push" }],
+ ["finished rejected 1", "#2", { from, navigationType: "push" }],
+ ["promise microtask", "#2", { from, navigationType: "push" }],
+ ["navigatesuccess", "#2", { from, navigationType: "push" }],
+ ["finished fulfilled 2", "#2", null],
+ ["transition.finished fulfilled", "#2", null]
+ ]);
+
+ recorder.assertErrorsAreAbortErrors();
+}, "event and promise ordering for same-document navigation.navigate() inside the navigate handler");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-same-document-intercept-reject.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-same-document-intercept-reject.html
new file mode 100644
index 0000000000..88c668e0e9
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-same-document-intercept-reject.html
@@ -0,0 +1,53 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+
+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));
+
+ const from = navigation.currentEntry;
+ const expectedError = new Error("boo");
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "transition.finished rejected"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler() {
+ recorder.record("handler run");
+ return Promise.reject(expectedError);
+ }});
+ });
+
+ const result = navigation.navigate("#1");
+ recorder.setUpResultListeners(result);
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["currententrychange", "#1", { from, navigationType: "push" }],
+ ["handler run", "#1", { from, navigationType: "push" }],
+ ["committed fulfilled", "#1", { from, navigationType: "push" }],
+ ["promise microtask", "#1", { from, navigationType: "push" }],
+ ["navigateerror", "#1", { from, navigationType: "push" }],
+ ["finished rejected", "#1", null],
+ ["transition.finished rejected", "#1", null],
+ ]);
+
+ recorder.assertErrorsAre(expectedError);
+}, "event and promise ordering for same-document navigation.navigate() intercepted by passing a rejected promise to intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-same-document.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-same-document.html
new file mode 100644
index 0000000000..589e1105e6
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/navigate-same-document.html
@@ -0,0 +1,39 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+
+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));
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "finished fulfilled"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ const result = navigation.navigate("#1");
+ recorder.setUpResultListeners(result);
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["currententrychange", "#1", null],
+ ["committed fulfilled", "#1", null],
+ ["promise microtask", "#1", null],
+ ["navigatesuccess", "#1", null],
+ ["finished fulfilled", "#1", null],
+ ]);
+}, "event and promise ordering for same-document navigation.navigate()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/reload-canceled.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/reload-canceled.html
new file mode 100644
index 0000000000..3e9e24e777
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/reload-canceled.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script type="module">
+import { Recorder } from "./resources/helpers.mjs";
+
+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));
+
+ const recorder = new Recorder({
+ finalExpectedEvent: "finished rejected"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", t.step_func(e => {
+ e.preventDefault();
+ }));
+
+ const result = navigation.reload();
+ recorder.setUpResultListeners(result);
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["AbortSignal abort", "", null],
+ ["navigateerror", "", null],
+ ["committed rejected", "", null],
+ ["finished rejected", "", null],
+ ["promise microtask", "", null]
+ ]);
+
+ recorder.assertErrorsAreAbortErrors();
+}, "event and promise ordering for navigation.reload() where the navigate event is canceled");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/reload-intercept-reject.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/reload-intercept-reject.html
new file mode 100644
index 0000000000..334d2108ec
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/reload-intercept-reject.html
@@ -0,0 +1,53 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+
+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));
+
+ const from = navigation.currentEntry;
+ const expectedError = new Error("boo");
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "transition.finished rejected"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler() {
+ recorder.record("handler run");
+ return Promise.reject(expectedError);
+ }});
+ });
+
+ const result = navigation.reload();
+ recorder.setUpResultListeners(result);
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["currententrychange", "", { from, navigationType: "reload" }],
+ ["handler run", "", { from, navigationType: "reload" }],
+ ["committed fulfilled", "", { from, navigationType: "reload" }],
+ ["promise microtask", "", { from, navigationType: "reload" }],
+ ["navigateerror", "", { from, navigationType: "reload" }],
+ ["finished rejected", "", null],
+ ["transition.finished rejected", "", null],
+ ]);
+
+ recorder.assertErrorsAre(expectedError);
+}, "event and promise ordering for navigation.reload() intercepted by intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/reload-intercept.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/reload-intercept.html
new file mode 100644
index 0000000000..8d4160a4d9
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/reload-intercept.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta name="variant" content="">
+<meta name="variant" content="?currententrychange">
+
+<script type="module">
+import { Recorder, hasVariant } from "./resources/helpers.mjs";
+
+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));
+
+ const from = navigation.currentEntry;
+
+ const recorder = new Recorder({
+ skipCurrentChange: !hasVariant("currententrychange"),
+ finalExpectedEvent: "transition.finished fulfilled"
+ });
+
+ recorder.setUpNavigationAPIListeners();
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler() { recorder.record("handler run"); } });
+ });
+
+ const result = navigation.reload();
+ recorder.setUpResultListeners(result);
+
+ Promise.resolve().then(() => recorder.record("promise microtask"));
+
+ await recorder.readyToAssert;
+
+ recorder.assert([
+ /* event name, location.hash value, navigation.transition properties */
+ ["navigate", "", null],
+ ["currententrychange", "", { from, navigationType: "reload" }],
+ ["handler run", "", { from, navigationType: "reload" }],
+ ["committed fulfilled", "", { from, navigationType: "reload" }],
+ ["promise microtask", "", { from, navigationType: "reload" }],
+ ["navigatesuccess", "", { from, navigationType: "reload" }],
+ ["finished fulfilled", "", null],
+ ["transition.finished fulfilled", "", null],
+ ]);
+}, "event and promise ordering for navigation.reload() intercepted by intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/resources/helpers.mjs b/testing/web-platform/tests/navigation-api/ordering-and-transition/resources/helpers.mjs
new file mode 100644
index 0000000000..341befc105
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/resources/helpers.mjs
@@ -0,0 +1,193 @@
+const variants = new Set((new URLSearchParams(location.search)).keys());
+
+export function hasVariant(name) {
+ return variants.has(name);
+}
+
+export class Recorder {
+ #events = [];
+ #errors = [];
+ #navigationAPI;
+ #domExceptionConstructor;
+ #location;
+ #skipCurrentChange;
+ #finalExpectedEvent;
+ #finalExpectedEventCount;
+ #currentFinalEventCount = 0;
+
+ #readyToAssertResolve;
+ #readyToAssertPromise = new Promise(resolve => { this.#readyToAssertResolve = resolve; });
+
+ constructor({ window = self, skipCurrentChange = false, finalExpectedEvent, finalExpectedEventCount = 1 }) {
+ assert_equals(typeof finalExpectedEvent, "string", "Must pass a string for finalExpectedEvent");
+
+ this.#navigationAPI = window.navigation;
+ this.#domExceptionConstructor = window.DOMException;
+ this.#location = window.location;
+
+ this.#skipCurrentChange = skipCurrentChange;
+ this.#finalExpectedEvent = finalExpectedEvent;
+ this.#finalExpectedEventCount = finalExpectedEventCount;
+ }
+
+ setUpNavigationAPIListeners() {
+ this.#navigationAPI.addEventListener("navigate", e => {
+ this.record("navigate");
+
+ e.signal.addEventListener("abort", () => {
+ this.recordWithError("AbortSignal abort", e.signal.reason);
+ });
+ });
+
+ this.#navigationAPI.addEventListener("navigateerror", e => {
+ this.recordWithError("navigateerror", e.error);
+
+ this.#navigationAPI.transition?.finished.then(
+ () => this.record("transition.finished fulfilled"),
+ err => this.recordWithError("transition.finished rejected", err)
+ );
+ });
+
+ this.#navigationAPI.addEventListener("navigatesuccess", () => {
+ this.record("navigatesuccess");
+
+ this.#navigationAPI.transition?.finished.then(
+ () => this.record("transition.finished fulfilled"),
+ err => this.recordWithError("transition.finished rejected", err)
+ );
+ });
+
+ if (!this.#skipCurrentChange) {
+ this.#navigationAPI.addEventListener("currententrychange", () => this.record("currententrychange"));
+ }
+ }
+
+ setUpResultListeners(result, suffix = "") {
+ result.committed.then(
+ () => this.record(`committed fulfilled${suffix}`),
+ err => this.recordWithError(`committed rejected${suffix}`, err)
+ );
+
+ result.finished.then(
+ () => this.record(`finished fulfilled${suffix}`),
+ err => this.recordWithError(`finished rejected${suffix}`, err)
+ );
+ }
+
+ record(name) {
+ const transitionProps = this.#navigationAPI.transition === null ? null : {
+ from: this.#navigationAPI.transition.from,
+ navigationType: this.#navigationAPI.transition.navigationType
+ };
+
+ this.#events.push({ name, location: this.#location.hash, transitionProps });
+
+ if (name === this.#finalExpectedEvent && ++this.#currentFinalEventCount === this.#finalExpectedEventCount) {
+ this.#readyToAssertResolve();
+ }
+ }
+
+ recordWithError(name, errorObject) {
+ this.record(name);
+ this.#errors.push({ name, errorObject });
+ }
+
+ get readyToAssert() {
+ return this.#readyToAssertPromise;
+ }
+
+ // Usage:
+ // recorder.assert([
+ // /* event name, location.hash value, navigation.transition properties */
+ // ["currententrychange", "", null],
+ // ["committed fulfilled", "#1", { from, navigationType }],
+ // ...
+ // ]);
+ //
+ // The array format is to avoid repitition at the call site, but I recommend
+ // you document it like above.
+ //
+ // This will automatically also assert that any error objects recorded are
+ // equal to each other. Use the other assert functions to check the actual
+ // contents of the error objects.
+ assert(expectedAsArray) {
+ if (this.#skipCurrentChange) {
+ expectedAsArray = expectedAsArray.filter(expected => expected[0] !== "currententrychange");
+ }
+
+ // Doing this up front gives nicer error messages because
+ // assert_array_equals is nice.
+ const recordedNames = this.#events.map(e => e.name);
+ const expectedNames = expectedAsArray.map(e => e[0]);
+ assert_array_equals(recordedNames, expectedNames);
+
+ for (let i = 0; i < expectedAsArray.length; ++i) {
+ const recorded = this.#events[i];
+ const expected = expectedAsArray[i];
+
+ assert_equals(
+ recorded.location,
+ expected[1],
+ `event ${i} (${recorded.name}): location.hash value`
+ );
+
+ if (expected[2] === null) {
+ assert_equals(
+ recorded.transitionProps,
+ null,
+ `event ${i} (${recorded.name}): navigation.transition expected to be null`
+ );
+ } else {
+ assert_not_equals(
+ recorded.transitionProps,
+ null,
+ `event ${i} (${recorded.name}): navigation.transition expected not to be null`
+ );
+ assert_equals(
+ recorded.transitionProps.from,
+ expected[2].from,
+ `event ${i} (${recorded.name}): navigation.transition.from`
+ );
+ assert_equals(
+ recorded.transitionProps.navigationType,
+ expected[2].navigationType,
+ `event ${i} (${recorded.name}): navigation.transition.navigationType`
+ );
+ }
+ }
+
+ if (this.#errors.length > 1) {
+ for (let i = 1; i < this.#errors.length; ++i) {
+ assert_equals(
+ this.#errors[i].errorObject,
+ this.#errors[0].errorObject,
+ `error objects must match: error object for ${this.#errors[i].name} did not match the one for ${this.#errors[0].name}`
+ );
+ }
+ }
+ }
+
+ assertErrorsAreAbortErrors() {
+ assert_greater_than(
+ this.#errors.length,
+ 0,
+ "No errors were recorded but assertErrorsAreAbortErrors() was called"
+ );
+
+ // Assume assert() has been called so all error objects are the same.
+ const { errorObject } = this.#errors[0];
+ assert_throws_dom("AbortError", this.#domExceptionConstructor, () => { throw errorObject; });
+ }
+
+ assertErrorsAre(expectedErrorObject) {
+ assert_greater_than(
+ this.#errors.length,
+ 0,
+ "No errors were recorded but assertErrorsAre() was called"
+ );
+
+ // Assume assert() has been called so all error objects are the same.
+ const { errorObject } = this.#errors[0];
+ assert_equals(errorObject, expectedErrorObject);
+ }
+}
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/resources/notify-top-early.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/resources/notify-top-early.html
new file mode 100644
index 0000000000..0dd796f609
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/resources/notify-top-early.html
@@ -0,0 +1,6 @@
+<head>
+<script>
+if (top.childStarted)
+ top.childStarted();
+</script>
+</head>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/transition-cross-document.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/transition-cross-document.html
new file mode 100644
index 0000000000..4a14a1083d
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/transition-cross-document.html
@@ -0,0 +1,44 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<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(r => window.onload = () => t.step_timeout(r, 0));
+
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("navigatesuccess must not fire");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("navigateerror must not fire");
+
+ assert_equals(i.contentWindow.navigation.transition, null);
+ i.contentWindow.navigation.reload();
+ assert_equals(i.contentWindow.navigation.transition, null);
+
+ await new Promise(r => i.onload = () => t.step_timeout(r, 0));
+}, "cross-document reload() must leave transition null");
+
+promise_test(async t => {
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("navigatesuccess must not fire");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("navigateerror must not fire");
+
+ assert_equals(i.contentWindow.navigation.transition, null);
+ i.contentWindow.navigation.navigate("?1");
+ assert_equals(i.contentWindow.navigation.transition, null);
+
+ await new Promise(r => i.onload = () => t.step_timeout(r, 0));
+}, "cross-document navigate() must leave transition null");
+
+promise_test(async t => {
+ i.contentWindow.navigation.onnavigatesuccess = t.unreached_func("navigatesuccess must not fire");
+ i.contentWindow.navigation.onnavigateerror = t.unreached_func("navigateerror must not fire");
+
+ assert_equals(i.contentWindow.navigation.transition, null);
+ i.contentWindow.navigation.back();
+ assert_equals(i.contentWindow.navigation.transition, null);
+
+ await new Promise(r => i.onload = r);
+}, "cross-document back() must leave transition null");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/transition-finished-mark-as-handled.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/transition-finished-mark-as-handled.html
new file mode 100644
index 0000000000..fa38b82216
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/transition-finished-mark-as-handled.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+async_test(t => {
+ navigation.addEventListener("navigate", e => {
+ e.intercept({ handler: () => Promise.reject(new Error("oh no!")) });
+ });
+
+ window.onunhandledrejection = t.unreached_func("unhandledrejection must not fire");
+
+ location.href = "?1";
+
+ // Make sure to trigger the getter to ensure the promise materializes!
+ navigation.transition.finished;
+
+ t.step_timeout(() => t.done(), 10);
+}, "navigation.transition.finished must not trigger unhandled rejections");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/ordering-and-transition/transition-realms-and-identity.html b/testing/web-platform/tests/navigation-api/ordering-and-transition/transition-realms-and-identity.html
new file mode 100644
index 0000000000..f1b3ead980
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/ordering-and-transition/transition-realms-and-identity.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe id="i" src="/common/blank.html"></iframe>
+
+<script>
+promise_test(async () => {
+ await new Promise(resolve => window.onload = resolve);
+
+ i.contentWindow.navigation.addEventListener("navigate", e => {
+ e.intercept();
+ });
+
+ const returnValueFinished1 = i.contentWindow.navigation.navigate("?1").finished;
+ const transition1 = i.contentWindow.navigation.transition;
+ const transitionFinished1 = transition1.finished;
+
+ assert_true(returnValueFinished1 instanceof i.contentWindow.Promise);
+ assert_true(transition1 instanceof i.contentWindow.NavigationTransition);
+ assert_true(transitionFinished1 instanceof i.contentWindow.Promise);
+
+ assert_not_equals(returnValueFinished1, transitionFinished1);
+
+ // Ensure the getters aren't generating new objects each time.
+ assert_equals(i.contentWindow.navigation.transition, transition1);
+ assert_equals(i.contentWindow.navigation.transition.finished, transitionFinished1);
+
+ assert_equals(await transitionFinished1, undefined);
+
+ // Ensure stuff does change after another navigation.
+ const committed2 = i.contentWindow.navigation.navigate("?2").committed;
+ const transition2 = i.contentWindow.navigation.transition;
+ const transitionFinished2 = transition2.finished;
+
+ assert_not_equals(transition2, transition1);
+ assert_not_equals(transitionFinished2, transitionFinished1);
+
+ await committed2;
+}, "Realm and identity of the navigation.transition object and its finished promise");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/per-entry-events/dispose-after-bfcache.html b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-after-bfcache.html
new file mode 100644
index 0000000000..7d3ef4f81e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-after-bfcache.html
@@ -0,0 +1,32 @@
+<!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="/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js"></script>
+<script src="resources/is_uuid.js"></script>
+
+<script>
+// This test:
+// * Does a fragment navigation, then goes back (same-document).
+// * Navigates away, then back via bfcache.
+// When navigating away, navigation.entries()[1] will be overwritten.
+// When returning after bfcache restore, navigation.entries()[1] should represent
+// pageB, and the original navigation.entries()[1] should have been disposed.
+runBfcacheTest({
+ targetOrigin: originSameOrigin,
+ funcBeforeNavigation: async () => {
+ window.events = [];
+ await navigation.navigate("#1");
+ await navigation.back();
+ window.originalEntry1 = navigation.entries()[1];
+ window.originalEntry1.ondispose = () => events.push("dispose");
+ window.onpageshow = () => events.push("pageshow");
+ },
+ async funcAfterAssertion(pageA, pageB) {
+ assert_equals(await pageA.execute_script(() => navigation.entries().length), 2);
+ assert_false(await pageA.execute_script(() => window.originalEntry1 === navigation.entries[1]));
+ assert_array_equals(await pageA.execute_script(() => window.events), ["pageshow", "dispose"]);
+ }
+}, "entries() must contain the forward-history page after navigating back to a bfcached page");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/per-entry-events/dispose-cross-document.html b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-cross-document.html
new file mode 100644
index 0000000000..67f54da48d
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-cross-document.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe" src="/common/blank.html"></iframe>
+<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(r => window.onload = () => t.step_timeout(r, 0));
+
+ iframe.contentWindow.location.search = "?1";
+ await new Promise(r => iframe.onload = () => t.step_timeout(r, 0));
+
+ const keyFor1 = iframe.contentWindow.navigation.currentEntry.key;
+
+ iframe.contentWindow.location.search = "?2";
+ await new Promise(r => iframe.onload = () => t.step_timeout(r, 0));
+ iframe.contentWindow.location.search = "?3";
+ await new Promise(r => iframe.onload = () => t.step_timeout(r, 0));
+
+ iframe.contentWindow.navigation.traverseTo(keyFor1);
+ await new Promise(r => iframe.onload = () => t.step_timeout(r, 0));
+
+ assert_equals((new URL(iframe.contentWindow.location.href)).search, "?1");
+
+ assert_equals(iframe.contentWindow.navigation.entries().length, 4);
+ const [, entry2, entry3] = iframe.contentWindow.navigation.entries();
+
+ entry2.ondispose = t.unreached_func("entry2 dispose must not fire");
+ entry2.addEventListener("dispose", t.unreached_func("entry3 dispose must not fire"));
+
+ iframe.contentWindow.navigation.navigate("/common/blank.html?fork");
+ await new Promise(r => iframe.onload = r);
+
+ // Test passes if we reached this point with no dispose events firing.
+}, "No dispose events are fired due to cross-document forward pruning");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/per-entry-events/dispose-for-full-session-history.tentative.html b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-for-full-session-history.tentative.html
new file mode 100644
index 0000000000..9bed225cf3
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-for-full-session-history.tentative.html
@@ -0,0 +1,20 @@
+<!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(r => window.onload = () => t.step_timeout(r, 0));
+ assert_equals(navigation.entries().length, 1);
+
+ let dispose_promise = new Promise(r => navigation.entries()[0].ondispose = r);
+
+ // There is no spec for the maximum number of joint session history entries
+ // (hence this is a .tentative.html test). However, all(?) browsers have a
+ // settled on a maximum of 50.
+ for (let i = 0; i < 50; i++)
+ await navigation.navigate("#" + i).finished;
+ await dispose_promise;
+}, "Dispose should fire when an entry is removed from session history due to too many entries");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/per-entry-events/dispose-for-navigation-in-child.html b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-for-navigation-in-child.html
new file mode 100644
index 0000000000..2482877085
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-for-navigation-in-child.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe" src="/common/blank.html"></iframe>
+<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(r => window.onload = () => t.step_timeout(r, 0));
+
+ await iframe.contentWindow.navigation.navigate("#a");
+ await navigation.navigate("#1").finished;
+ await navigation.navigate("#2").finished;
+ await navigation.navigate("#3").finished;
+ await iframe.contentWindow.navigation.back().finished;
+ assert_equals(navigation.entries().length, 4);
+ assert_equals(iframe.contentWindow.navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry.index, 0);
+ assert_equals(iframe.contentWindow.navigation.currentEntry.index, 0);
+
+ let top_entry_0_before = navigation.entries()[0];
+ let dispose_promises = [];
+ for (let i = 1; i < navigation.entries().length; i++) {
+ dispose_promises.push(new Promise(r => navigation.entries()[i].ondispose = r))
+ }
+
+ // This push navigation should truncate in the top window.
+ await iframe.contentWindow.navigation.navigate("#b").finished;
+
+ // entries() should be updated in both frames, and all forward entries in the
+ // top window should have dispose events, even though that wasn't the window
+ // that navigated.
+ await Promise.all(dispose_promises);
+ assert_equals(navigation.entries().length, 1);
+ assert_equals(iframe.contentWindow.navigation.entries().length, 2);
+ assert_equals(navigation.currentEntry.index, 0);
+ assert_equals(iframe.contentWindow.navigation.currentEntry.index, 1);
+ assert_equals(navigation.entries()[0], top_entry_0_before);
+
+}, "Dispose events should fire when entries are removed by a navigation in a different frame");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-intercept.html b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-intercept.html
new file mode 100644
index 0000000000..44aa096aaf
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-intercept.html
@@ -0,0 +1,71 @@
+<!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(r => window.onload = () => t.step_timeout(r, 0));
+
+ location.hash = "#1";
+ location.hash = "#2";
+ location.hash = "#3";
+
+ assert_equals(navigation.entries().length, 4);
+ const [entry0, entry1, entry2, entry3] = navigation.entries();
+ assert_equals((new URL(entry2.url)).hash, "#2");
+ assert_equals((new URL(entry3.url)).hash, "#3");
+
+ let dispose2Called = false;
+ entry2.ondispose = t.step_func(e => {
+ dispose2Called = true;
+
+ assert_equals(e.constructor, Event);
+ assert_equals(e.bubbles, false);
+ assert_equals(e.cancelable, false);
+ assert_equals(e.composed, false);
+
+ assert_array_equals(
+ navigation.entries(),
+ [entry0, entry1, navigation.currentEntry],
+ "entries() is updated during dispose for entry 2");
+ assert_not_equals(navigation.currentEntry, entry1, "current entry must be updated during dispose for entry 3");
+ assert_true(navigation.canGoBack, "canGoBack is still true during dispose for entry 2");
+ assert_false(navigation.canGoForward, "canGoForward is still false during beforedispose for entry 2");
+ assert_equals(navigation.transition.navigationType, "push", "transition navigationType during dispose for entry 2");
+ assert_equals(navigation.transition.from, entry1, "transition from during dispose for entry 2");
+ assert_equals(location.hash, "#fork", "location.hash is updated during dispose for entry 2");
+ });
+
+ entry3.addEventListener("dispose", t.step_func_done(e => {
+ assert_true(dispose2Called, "dispose for entry 2 must have happened before entry 3");
+
+ assert_array_equals(
+ navigation.entries(),
+ [entry0, entry1, navigation.currentEntry],
+ "entries() is updated during dispose for entry 3");
+ assert_not_equals(navigation.currentEntry, entry1, "current entry must be updated during dispose for entry 3");
+ assert_true(navigation.canGoBack, "canGoBack is still true during dispose for entry 3");
+ assert_false(navigation.canGoForward, "canGoForward is still false during beforedispose for entry 3");
+ assert_equals(navigation.transition.navigationType, "push", "transition navigationType during dispose for entry 3");
+ assert_equals(navigation.transition.from, entry1, "transition from during dispose for entry 3");
+ assert_equals(location.hash, "#fork", "location.hash is updated during dispose for entry 3");
+ }));
+
+ await navigation.traverseTo(entry1.key).committed;
+
+ navigation.addEventListener("navigate", e => {
+ e.intercept();
+ });
+
+ navigation.navigate("#fork");
+
+ assert_equals(navigation.entries().length, 3);
+ const [finalEntry0, finalEntry1, finalEntry2] = navigation.entries();
+ assert_equals(finalEntry0, entry0);
+ assert_equals(finalEntry1, entry1);
+ assert_not_equals(finalEntry2, entry2);
+ assert_equals(navigation.currentEntry, finalEntry2);
+ assert_equals((new URL(finalEntry2.url)).hash, "#fork");
+}, "dispose events when forward-pruning same-document entries");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-navigate-during.html b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-navigate-during.html
new file mode 100644
index 0000000000..59d9b3cce3
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-navigate-during.html
@@ -0,0 +1,50 @@
+<!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(r => window.onload = () => t.step_timeout(r, 0));
+
+ location.hash = "#1";
+ location.hash = "#2";
+ location.hash = "#3";
+
+ assert_equals(navigation.entries().length, 4);
+ const [entry0, entry1, entry2, entry3] = navigation.entries();
+ assert_equals((new URL(entry2.url)).hash, "#2");
+ assert_equals((new URL(entry3.url)).hash, "#3");
+
+ let dispose3Called = 0;
+ let spoonPromise;
+ entry3.addEventListener("dispose", t.step_func(e => {
+ ++dispose3Called;
+
+ spoonPromise = navigation.navigate("#spoon").committed;
+ }));
+
+ await navigation.traverseTo(entry1.key).committed;
+
+ const forkPromise = navigation.navigate("#fork").committed;
+ assert_equals(dispose3Called, 1, "dispose for entry 3 must happen exactly once (first check)")
+
+ // The navigation to #fork will *not* be finished by the time the navigation
+ // to #spoon kicks off, so the finished promise will reject. But, the
+ // committed promise should still fulfill, since as we see below, #fork ends
+ // up in the history list.
+ await Promise.all([forkPromise, spoonPromise]);
+
+ assert_equals(dispose3Called, 1, "dispose for entry 3 must happen exactly once (final check)");
+
+ assert_equals(navigation.entries().length, 4);
+ const [finalEntry0, finalEntry1, finalEntry2, finalEntry3] = navigation.entries();
+ assert_equals(finalEntry0, entry0);
+ assert_equals(finalEntry1, entry1);
+ assert_not_equals(finalEntry2, entry2);
+ assert_not_equals(finalEntry3, entry3);
+ assert_equals(navigation.currentEntry, finalEntry3);
+ assert_equals((new URL(finalEntry2.url)).hash, "#fork");
+ assert_equals((new URL(finalEntry3.url)).hash, "#spoon");
+}, "navigate() during a same-document-navigation-initiated dispose works (since it's after the previous navigation)");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-reload-with-intercept.html b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-reload-with-intercept.html
new file mode 100644
index 0000000000..1e083714f6
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-reload-with-intercept.html
@@ -0,0 +1,17 @@
+<!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(r => window.onload = () => t.step_timeout(r, 0));
+
+ navigation.currentEntry.ondispose = t.unreached_func("dispose must not happen for reloads");
+
+ navigation.addEventListener("navigate", e => e.intercept());
+
+ await navigation.reload().finished;
+}, "dispose events are not fired when doing a same-document reload using navigation.reload() and intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-replace-with-intercept.html b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-replace-with-intercept.html
new file mode 100644
index 0000000000..4e492e30ae
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-replace-with-intercept.html
@@ -0,0 +1,36 @@
+<!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 we're definitely testing intentional,
+ // navigate()-caused replacement and not the replacement that happens
+ // automatically before the load event completes.
+ await new Promise(r => window.onload = () => t.step_timeout(r, 0));
+
+ const entriesBefore = navigation.entries();
+ const currentBefore = navigation.currentEntry;
+
+ let disposeCalled = false;
+ navigation.currentEntry.ondispose = t.step_func(e => {
+ disposeCalled = true;
+
+ assert_equals(e.constructor, Event);
+ assert_equals(e.bubbles, false);
+ assert_equals(e.cancelable, false);
+ assert_equals(e.composed, false);
+
+ assert_not_equals(navigation.currentEntry, currentBefore);
+ assert_array_equals(navigation.entries(), [navigation.currentEntry]);
+ assert_equals((new URL(navigation.currentEntry.url)).search, "?replacement");
+ assert_equals(navigation.transition.navigationType, "replace");
+ assert_equals(navigation.transition.from, entriesBefore[0]);
+ assert_equals(location.search, "?replacement");
+ });
+
+ navigation.addEventListener("navigate", e => e.intercept());
+
+ navigation.navigate("?replacement", { history: "replace" });
+ assert_true(disposeCalled);
+}, "dispose events when doing a same-document replace using navigation.navigate() and intercept()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-replaceState.html b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-replaceState.html
new file mode 100644
index 0000000000..a6197260a2
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document-replaceState.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(t => {
+ const entriesBefore = navigation.entries();
+ const currentBefore = navigation.currentEntry;
+
+ let disposeCalled = false;
+ navigation.currentEntry.ondispose = t.step_func(e => {
+ disposeCalled = true;
+
+ assert_equals(e.constructor, Event);
+ assert_equals(e.bubbles, false);
+ assert_equals(e.cancelable, false);
+ assert_equals(e.composed, false);
+
+ assert_not_equals(navigation.currentEntry, currentBefore);
+ assert_array_equals(navigation.entries(), [navigation.currentEntry]);
+ assert_equals((new URL(navigation.currentEntry.url)).search, "?replacement");
+ assert_equals(navigation.transition, null);
+ assert_equals(location.search, "?replacement");
+ });
+
+ history.replaceState(null, "", "?replacement");
+ assert_true(disposeCalled);
+}, "dispose events when doing a same-document replace using history.replaceState()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document.html b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document.html
new file mode 100644
index 0000000000..27806ce3c8
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-same-document.html
@@ -0,0 +1,65 @@
+<!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(r => window.onload = () => t.step_timeout(r, 0));
+
+ location.hash = "#1";
+ location.hash = "#2";
+ location.hash = "#3";
+
+ assert_equals(navigation.entries().length, 4);
+ const [entry0, entry1, entry2, entry3] = navigation.entries();
+ assert_equals((new URL(entry2.url)).hash, "#2");
+ assert_equals((new URL(entry3.url)).hash, "#3");
+
+ let dispose2Called = false;
+ entry2.ondispose = t.step_func(e => {
+ dispose2Called = true;
+
+ assert_equals(e.constructor, Event);
+ assert_equals(e.bubbles, false);
+ assert_equals(e.cancelable, false);
+ assert_equals(e.composed, false);
+
+ assert_array_equals(
+ navigation.entries(),
+ [entry0, entry1, navigation.currentEntry],
+ "entries() is updated during dispose for entry 2");
+ assert_not_equals(navigation.currentEntry, entry1, "current entry must be updated during dispose for entry 3");
+ assert_true(navigation.canGoBack, "canGoBack is still true during dispose for entry 2");
+ assert_false(navigation.canGoForward, "canGoForward is still false during beforedispose for entry 2");
+ assert_equals(navigation.transition, null, "transition during dispose for entry 2");
+ assert_equals(location.hash, "#fork", "location.hash is updated during dispose for entry 2");
+ });
+
+ entry3.addEventListener("dispose", t.step_func_done(e => {
+ assert_true(dispose2Called, "dispose for entry 2 must have happened before entry 3");
+
+ assert_array_equals(
+ navigation.entries(),
+ [entry0, entry1, navigation.currentEntry],
+ "entries() is updated during dispose for entry 3");
+ assert_not_equals(navigation.currentEntry, entry1, "current entry must be updated during dispose for entry 3");
+ assert_true(navigation.canGoBack, "canGoBack is still true during dispose for entry 3");
+ assert_false(navigation.canGoForward, "canGoForward is still false during beforedispose for entry 3");
+ assert_equals(navigation.transition, null, "transition during dispose for entry 2");
+ assert_equals(location.hash, "#fork", "location.hash is updated during dispose for entry 3");
+ }));
+
+ await navigation.traverseTo(entry1.key).committed;
+
+ navigation.navigate("#fork");
+
+ assert_equals(navigation.entries().length, 3);
+ const [finalEntry0, finalEntry1, finalEntry2] = navigation.entries();
+ assert_equals(finalEntry0, entry0);
+ assert_equals(finalEntry1, entry1);
+ assert_not_equals(finalEntry2, entry2);
+ assert_equals(navigation.currentEntry, finalEntry2);
+ assert_equals((new URL(finalEntry2.url)).hash, "#fork");
+}, "dispose events when forward-pruning same-document entries");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/per-entry-events/dispose-skip-current-on-truncate.html b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-skip-current-on-truncate.html
new file mode 100644
index 0000000000..56ec4d301f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/per-entry-events/dispose-skip-current-on-truncate.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(r => window.onload = () => t.step_timeout(r, 0));
+ await navigation.navigate("#1").finished;
+ assert_equals(navigation.entries().length, 2);
+
+ let iframe = document.createElement("iframe");
+ iframe.src = "/common/blank.html";
+ document.body.appendChild(iframe);
+ await new Promise(r => iframe.onload = () => t.step_timeout(r, 0));
+ assert_equals(iframe.contentWindow.navigation.entries().length, 1);
+
+ // Go back to before the iframe was added. The iframe will still be in the
+ // document, but we will be at a joint session history entry that was created
+ // before the iframe was added to the document.
+ await navigation.back().finished;
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(iframe.contentWindow.navigation.entries().length, 1);
+ assert_equals(navigation.currentEntry.index, 0);
+ assert_equals(iframe.contentWindow.navigation.currentEntry.index, 0);
+
+ // A push navigation in the top window will truncate-then-push the joint
+ // session history. This should dispose the forward entry in the top window,
+ // but should not interfere with the currentEntry in the iframe.
+ let dispose_promise = new Promise(r => navigation.entries()[1].ondispose = r);
+ iframe.contentWindow.navigation.currentEntry.ondispose = t.unreached_func("iframe entry must not be disposed");
+ await navigation.navigate("#b").finished;
+
+ await dispose_promise;
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(iframe.contentWindow.navigation.entries().length, 1);
+ assert_equals(navigation.currentEntry.index, 1);
+ assert_equals(iframe.contentWindow.navigation.currentEntry.index, 0);
+}, "Removing a currentEntry from the joint session history shouldn't dispose it");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-basic.html b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-basic.html
new file mode 100644
index 0000000000..8ee4cc395b
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-basic.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+ assert_equals(window.scrollY, 0);
+ await navigation.navigate("#frag").finished;
+ assert_not_equals(window.scrollY, 0);
+ navigation.onnavigate = e => e.intercept({ scroll: "after-transition" });
+ await navigation.back().finished;
+ assert_equals(window.scrollY, 0);
+}, "scroll: after-transition should scroll when back completes");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-change-history-scroll-restoration-during-promise.html b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-change-history-scroll-restoration-during-promise.html
new file mode 100644
index 0000000000..6ededdeebf
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-change-history-scroll-restoration-during-promise.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+ assert_equals(window.scrollY, 0);
+
+ history.scrollRestoration = "manual";
+ await navigation.navigate("#frag").finished;
+ assert_not_equals(window.scrollY, 0);
+
+ let intercept_resolve;
+ navigation.onnavigate = e => {
+ e.intercept({ handler: () => new Promise(r => intercept_resolve = r),
+ scroll: "after-transition" });
+ };
+
+ let back_promises = navigation.back();
+ await back_promises.committed;
+ history.scrollRestoration = "auto";
+ intercept_resolve();
+ await back_promises.finished;
+ assert_equals(window.scrollY, 0);
+}, "scroll: after-transition should ignore history.scrollRestoration even if it changes in the middle of the navigation");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-explicit-scroll.html b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-explicit-scroll.html
new file mode 100644
index 0000000000..4b7d075474
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-explicit-scroll.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+ assert_equals(window.scrollY, 0);
+ await navigation.navigate("#frag").finished;
+ assert_not_equals(window.scrollY, 0);
+ navigation.onnavigate = t.step_func(e => {
+ e.intercept({
+ scroll: "after-transition",
+ handler: t.step_func(() => {
+ assert_not_equals(window.scrollY, 0);
+ e.scroll();
+ assert_equals(window.scrollY, 0);
+ })
+ });
+ });
+ await navigation.back().finished;
+ assert_equals(window.scrollY, 0);
+}, "scroll: scroll() should preempt after-transition");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-intercept-handler-modifies.html b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-intercept-handler-modifies.html
new file mode 100644
index 0000000000..78d1692104
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-intercept-handler-modifies.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div id="main" style="height: 1000px; width: 1000px;"></div>
+<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));
+ assert_equals(window.scrollY, 0);
+ window.scrollTo(0, 100);
+ assert_equals(window.scrollY, 100);
+ navigation.onnavigate = e => e.intercept(
+ { scroll: "after-transition",
+ handler: async () => {
+ if (e.navigationType == "push") {
+ // Inserting this <div> should scroll *after* the scroll position
+ // is saved, so that it doesn't break scroll restoration when going
+ // back.
+ let div = document.createElement("div");
+ div.style = "height: 1000px; width: 1000px;";
+ document.body.insertBefore(div, main);
+ }
+ }
+ }
+ );
+ await navigation.navigate("?go").finished;
+ await navigation.back().finished;
+ assert_equals(window.scrollY, 100);
+}, "scroll: state should be saved before intercept handlers run");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-push.html b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-push.html
new file mode 100644
index 0000000000..f3ee1827bc
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-push.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+ assert_equals(window.scrollY, 0);
+
+ let intercept_resolve;
+ let navigate_event;
+ navigation.onnavigate = e => {
+ navigate_event = e;
+ e.intercept({ handler: () => new Promise(r => intercept_resolve = r),
+ scroll: "after-transition" });
+ };
+ let promises = navigation.navigate("#frag");
+ await promises.committed;
+ assert_equals(window.scrollY, 0);
+ intercept_resolve();
+ await promises.finished;
+ assert_not_equals(window.scrollY, 0);
+}, "scroll: after-transition should work on a push navigation");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-reject.html b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-reject.html
new file mode 100644
index 0000000000..5880dbb331
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-reject.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+ assert_equals(window.scrollY, 0);
+ await navigation.navigate("#frag").finished;
+ assert_not_equals(window.scrollY, 0);
+ navigation.onnavigate =
+ e => e.intercept({ handler: () => Promise.reject(),
+ scroll: "after-transition" });
+
+ await promise_rejects_exactly(t, undefined, navigation.back().finished);
+ assert_not_equals(window.scrollY, 0);
+}, "scroll: after-transition should not scroll when the intercept() handler rejects");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-reload.html b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-reload.html
new file mode 100644
index 0000000000..badb7e7f41
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-reload.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div id="buffer" style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<div style="height: 1000px; width: 1000px;"></div>
+<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));
+ assert_equals(window.scrollY, 0);
+ await navigation.navigate("#frag").finished;
+ // Scroll down 10px from #frag
+ window.scrollBy(0, 10);
+ let scrollY_frag_plus_10 = window.scrollY;
+
+ let intercept_resolve;
+ let navigate_event;
+ navigation.onnavigate = e => {
+ navigate_event = e;
+ e.intercept({ handler: () => new Promise(r => intercept_resolve = r),
+ scroll: "after-transition" });
+ };
+ let reload_promises = navigation.reload();
+ await reload_promises.committed;
+
+ // removing the <div id="buffer"> should scroll up 1000px.
+ assert_equals(window.scrollY, scrollY_frag_plus_10);
+ buffer.remove();
+ let scrollY_after_buffer_remove = window.scrollY;
+ assert_equals(scrollY_after_buffer_remove, scrollY_frag_plus_10 - 1000);
+
+ // Finishing should scroll to #frag, which is 10px up from the current location
+ intercept_resolve();
+ await reload_promises.finished;
+ assert_equals(window.scrollY, scrollY_after_buffer_remove - 10);
+}, "scroll: after-transition should work on a reload navigation");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-replace.html b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-replace.html
new file mode 100644
index 0000000000..48f153b99e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-replace.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+ assert_equals(window.scrollY, 0);
+
+ let intercept_resolve;
+ let navigate_event;
+ navigation.onnavigate = e => {
+ navigate_event = e;
+ e.intercept({ handler: () => new Promise(r => intercept_resolve = r),
+ scroll: "after-transition" });
+ };
+ let promises = navigation.navigate("#frag", { history: "replace" });
+ await promises.committed;
+ assert_equals(window.scrollY, 0);
+ intercept_resolve();
+ await promises.finished;
+ assert_not_equals(window.scrollY, 0);
+}, "scroll: after-transition should work on a replace navigation");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-timing.html b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-timing.html
new file mode 100644
index 0000000000..88ba82102a
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-timing.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+ assert_equals(window.scrollY, 0);
+ await navigation.navigate("#frag").finished;
+ assert_not_equals(window.scrollY, 0);
+
+ window.onpopstate = t.step_func(() => assert_not_equals(window.scrollY, 0));
+ window.onhashchange = t.step_func(() => assert_not_equals(window.scrollY, 0));
+
+ // The scroll restore should take place before navigatesuccess fires.
+ let navigatesuccess_called = false;
+ navigation.onnavigatesuccess = t.step_func(() => {
+ navigatesuccess_called = true;
+ assert_equals(window.scrollY, 0);
+ });
+
+ let back_promises = navigation.back();
+ navigation.onnavigate = t.step_func(e => {
+ e.intercept({ scroll: "after-transition",
+ handler: async () => {
+ // The ordering here should be:
+ // * intercept() is called
+ // * back_promises.committed is resolved
+ // * this handler runs.
+ // If this handler incorrectly blocks back_promises.committed,
+ // this test will hang.
+ await back_promises.committed;
+ assert_not_equals(window.scrollY, 0);
+ }
+ });
+ assert_not_equals(window.scrollY, 0);
+ });
+
+ await back_promises.finished;
+ assert_equals(window.scrollY, 0);
+ assert_true(navigatesuccess_called);
+}, "scroll: after-transition should scroll when back completes, just before navigatesuccess");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-with-history-scroll-restoration-manual.html b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-with-history-scroll-restoration-manual.html
new file mode 100644
index 0000000000..bc2ce230ce
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/after-transition-with-history-scroll-restoration-manual.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+ assert_equals(window.scrollY, 0);
+
+ history.scrollRestoration = "manual";
+ await navigation.navigate("#frag").finished;
+ assert_not_equals(window.scrollY, 0);
+
+ navigation.onnavigate = e => e.intercept({ scroll: "after-transition" });
+ await navigation.back().finished;
+ assert_equals(window.scrollY, 0);
+}, "scroll: after-transition should ignore history.scrollRestoration");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/manual-basic.html b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-basic.html
new file mode 100644
index 0000000000..e6ae29ad86
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-basic.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+ assert_equals(window.scrollY, 0);
+ await navigation.navigate("#frag").finished;
+ assert_not_equals(window.scrollY, 0);
+ navigation.onnavigate = e => e.intercept({ scroll: "manual" });
+ await navigation.back().finished;
+ assert_not_equals(window.scrollY, 0);
+}, "scroll: manual should prevent auto scroll on back.");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/manual-immediate-scroll.html b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-immediate-scroll.html
new file mode 100644
index 0000000000..bafcf6b256
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-immediate-scroll.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+ assert_equals(window.scrollY, 0);
+ await navigation.navigate("#frag").finished;
+ assert_not_equals(window.scrollY, 0);
+ navigation.onnavigate = e => {
+ e.intercept({ scroll: "manual" });
+ assert_throws_dom("InvalidStateError", () => e.scroll());
+ assert_not_equals(window.scrollY, 0);
+ }
+ await navigation.back().finished;
+ assert_not_equals(window.scrollY, 0);
+}, "scroll: scroll() should not work inside a navigate event handler");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-after-dispatch.html b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-after-dispatch.html
new file mode 100644
index 0000000000..8b4a58c7a2
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-after-dispatch.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+ assert_equals(window.scrollY, 0);
+ await navigation.navigate("#frag").finished;
+ assert_not_equals(window.scrollY, 0);
+
+ let intercept_resolve;
+ let navigate_event;
+ navigation.onnavigate = e => {
+ navigate_event = e;
+ e.intercept({ handler: () => new Promise(r => intercept_resolve = r),
+ scroll: "manual" });
+ };
+ let back_promises = navigation.back();
+ await back_promises.committed;
+ assert_not_equals(window.scrollY, 0);
+ navigate_event.scroll();
+ assert_equals(window.scrollY, 0);
+ intercept_resolve();
+ await back_promises.finished;
+ assert_equals(window.scrollY, 0);
+}, "scroll: scroll() should work after a navigate event dispatch");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-after-resolve.html b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-after-resolve.html
new file mode 100644
index 0000000000..244c93af90
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-after-resolve.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+ assert_equals(window.scrollY, 0);
+ await navigation.navigate("#frag").finished;
+ assert_not_equals(window.scrollY, 0);
+
+ let navigate_event;
+ navigation.onnavigate = e => {
+ navigate_event = e;
+ e.intercept({ scroll: "manual" });
+ };
+ await navigation.back().finished;
+ assert_not_equals(window.scrollY, 0);
+ assert_throws_dom("InvalidStateError", () => navigate_event.scroll());
+ assert_not_equals(window.scrollY, 0);
+}, "scroll: scroll() should throw after a navigation finished promise fulfills");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-before-after-transition-commit.html b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-before-after-transition-commit.html
new file mode 100644
index 0000000000..3b32e72bb1
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-before-after-transition-commit.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+ assert_equals(window.scrollY, 0);
+ await navigation.navigate("#frag").finished;
+ assert_not_equals(window.scrollY, 0);
+ navigation.onnavigate = e => {
+ e.intercept({
+ scroll: "manual",
+ commit: "after-transition",
+ handler: t.step_func(() => {
+ assert_throws_dom("InvalidStateError", () => e.scroll());
+ assert_not_equals(window.scrollY, 0);
+ e.commit();
+ e.scroll();
+ assert_equals(window.scrollY, 0);
+ })
+ });
+ }
+ await navigation.back().finished;
+ assert_equals(window.scrollY, 0);
+}, "scroll: scroll() before commit()");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-fragment-does-not-exist.html b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-fragment-does-not-exist.html
new file mode 100644
index 0000000000..1ca582787e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-fragment-does-not-exist.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+ await navigation.navigate("#frag").finished;
+ assert_not_equals(window.scrollY, 0);
+
+ let intercept_resolve;
+ let navigate_event;
+ navigation.onnavigate = e => {
+ navigate_event = e;
+ e.intercept({ handler: () => new Promise(r => intercept_resolve = r),
+ scroll: "manual" });
+ };
+ let promises = navigation.navigate("#does-not-exist");
+ await promises.committed;
+ assert_not_equals(window.scrollY, 0);
+ navigate_event.scroll();
+ assert_not_equals(window.scrollY, 0);
+ intercept_resolve();
+ await promises.finished;
+ assert_not_equals(window.scrollY, 0);
+}, "scroll: scroll() should do nothing when the fragment does not exist");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-push.html b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-push.html
new file mode 100644
index 0000000000..3c29365178
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-push.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+ assert_equals(window.scrollY, 0);
+
+ let intercept_resolve;
+ let navigate_event;
+ navigation.onnavigate = e => {
+ navigate_event = e;
+ e.intercept({ handler: () => new Promise(r => intercept_resolve = r),
+ scroll: "manual" });
+ };
+ let promises = navigation.navigate("#frag");
+ await promises.committed;
+ assert_equals(window.scrollY, 0);
+ navigate_event.scroll();
+ assert_not_equals(window.scrollY, 0);
+ intercept_resolve();
+ await promises.finished;
+ assert_not_equals(window.scrollY, 0);
+}, "scroll: scroll() should work on a push navigation");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-reload.html b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-reload.html
new file mode 100644
index 0000000000..6dd3fd8a38
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-reload.html
@@ -0,0 +1,45 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div id="buffer" style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<div style="height: 1000px; width: 1000px;"></div>
+<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));
+ assert_equals(window.scrollY, 0);
+ await navigation.navigate("#frag").finished;
+ // Scroll down 10px from #frag
+ window.scrollBy(0, 10);
+ let scrollY_frag_plus_10 = window.scrollY;
+
+ let intercept_resolve;
+ let navigate_event;
+ navigation.onnavigate = e => {
+ navigate_event = e;
+ e.intercept({ handler: () => new Promise(r => intercept_resolve = r),
+ scroll: "manual" });
+ };
+ let reload_promises = navigation.reload();
+ await reload_promises.committed;
+
+ // removing the <div id="buffer"> should scroll up 1000px.
+ assert_equals(window.scrollY, scrollY_frag_plus_10);
+ buffer.remove();
+ let scrollY_after_buffer_remove = window.scrollY;
+ assert_equals(scrollY_after_buffer_remove, scrollY_frag_plus_10 - 1000);
+
+ // scroll() should scroll to #frag, which is 10px up from the current location
+ navigate_event.scroll();
+ assert_equals(window.scrollY, scrollY_after_buffer_remove - 10);
+
+ // Finishing should not scroll.
+ intercept_resolve();
+ await reload_promises.finished;
+ assert_equals(window.scrollY, scrollY_after_buffer_remove - 10);
+}, "scroll: scroll() should work on a reload navigation");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-repeated.html b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-repeated.html
new file mode 100644
index 0000000000..1239146088
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-repeated.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+ assert_equals(window.scrollY, 0);
+ await navigation.navigate("#frag").finished;
+ assert_not_equals(window.scrollY, 0);
+ navigation.onnavigate = e => {
+ e.intercept({
+ scroll: "manual",
+ handler: t.step_func(() => {
+ e.scroll();
+ assert_equals(window.scrollY, 0);
+ assert_throws_dom("InvalidStateError", () => e.scroll());
+ })
+ });
+ };
+ await navigation.back().finished;
+ assert_equals(window.scrollY, 0);
+}, "scroll: scroll() should throw if called a second time");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-replace.html b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-replace.html
new file mode 100644
index 0000000000..db580229a8
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-replace.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+ assert_equals(window.scrollY, 0);
+
+ let intercept_resolve;
+ let navigate_event;
+ navigation.onnavigate = e => {
+ navigate_event = e;
+ e.intercept({ handler: () => new Promise(r => intercept_resolve = r),
+ scroll: "manual" });
+ };
+ let promises = navigation.navigate("#frag", { history: "replace" });
+ await promises.committed;
+ assert_equals(window.scrollY, 0);
+ navigate_event.scroll();
+ assert_not_equals(window.scrollY, 0);
+ intercept_resolve();
+ await promises.finished;
+ assert_not_equals(window.scrollY, 0);
+}, "scroll: scroll() should work on a replace navigation");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-resets-when-no-fragment.html b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-resets-when-no-fragment.html
new file mode 100644
index 0000000000..5401976f64
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-resets-when-no-fragment.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+ let start_url = location.href;
+ await navigation.navigate("#frag").finished;
+ assert_not_equals(window.scrollY, 0);
+
+ let intercept_resolve;
+ let navigate_event;
+ navigation.onnavigate = e => {
+ navigate_event = e;
+ e.intercept({ handler: () => new Promise(r => intercept_resolve = r),
+ scroll: "manual" });
+ };
+ let promises = navigation.navigate(start_url);
+ await promises.committed;
+ assert_not_equals(window.scrollY, 0);
+ navigate_event.scroll();
+ assert_equals(window.scrollY, 0);
+ intercept_resolve();
+ await promises.finished;
+ assert_equals(window.scrollY, 0);
+}, "scroll: scroll() should reset scroll position when the destination url contains no fragment");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/scroll-after-preventDefault.html b/testing/web-platform/tests/navigation-api/scroll-behavior/scroll-after-preventDefault.html
new file mode 100644
index 0000000000..d83d341feb
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/scroll-after-preventDefault.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+
+ navigation.addEventListener("navigate", t.step_func(e => {
+ e.intercept();
+ e.preventDefault();
+ assert_throws_dom("InvalidStateError", () => e.scroll());
+ }), { once : true });
+ await promise_rejects_dom(t, "AbortError", navigation.navigate("#frag").finished);
+}, "scroll: scroll() should throw after preventDefault");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/scroll-on-synthetic-event.html b/testing/web-platform/tests/navigation-api/scroll-behavior/scroll-on-synthetic-event.html
new file mode 100644
index 0000000000..7efc2d1d98
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/scroll-on-synthetic-event.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+promise_test(async t => {
+ // We need to grab an NavigationDestination to construct the event.
+ navigation.onnavigate = t.step_func_done(e => {
+ const event = new NavigateEvent("navigate", {
+ destination: e.destination,
+ signal: (new AbortController()).signal
+ });
+
+ assert_throws_dom("SecurityError", () => event.scroll());
+ });
+ history.pushState(1, null, "#1");
+}, "scroll: scroll() should throw if invoked on a synthetic event.");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/scroll-without-intercept.html b/testing/web-platform/tests/navigation-api/scroll-behavior/scroll-without-intercept.html
new file mode 100644
index 0000000000..b3958352c7
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/scroll-behavior/scroll-without-intercept.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div style="height: 1000px; width: 1000px;"></div>
+<div id="frag"></div>
+<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));
+
+ navigation.addEventListener("navigate", t.step_func(e => {
+ assert_throws_dom("InvalidStateError", () => e.scroll());
+ }), { once : true });
+ await navigation.navigate("#frag").finished;
+}, "scroll: scroll() should throw for non-intercept");
+</script>
+</body>
diff --git a/testing/web-platform/tests/navigation-api/state/cross-document-away-and-back.html b/testing/web-platform/tests/navigation-api/state/cross-document-away-and-back.html
new file mode 100644
index 0000000000..cccaebdf7b
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/state/cross-document-away-and-back.html
@@ -0,0 +1,31 @@
+<!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_func(() => {
+ i.contentWindow.navigation.navigate("#", { history: "replace", state: { data : "value" } });
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.getState().data, "value");
+
+ let navigated_back = false;
+ i.contentWindow.navigation.navigate("?1");
+ i.onload = t.step_func(() => {
+ if (navigated_back) {
+ let back_entry = i.contentWindow.navigation.currentEntry;
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(back_entry.index, 0);
+ assert_equals(back_entry.getState().data, "value");
+ t.done();
+ } else {
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry, i.contentWindow.navigation.entries()[1]);
+ assert_equals(i.contentWindow.navigation.currentEntry.getState(), undefined);
+ history.back();
+ navigated_back = true;
+ }
+ });
+ });
+}, "entry.getState() behavior after navigating away and back");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/state/cross-document-getState-undefined.html b/testing/web-platform/tests/navigation-api/state/cross-document-getState-undefined.html
new file mode 100644
index 0000000000..cbd5cdd52e
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/state/cross-document-getState-undefined.html
@@ -0,0 +1,17 @@
+<!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_func(() => {
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ i.contentWindow.location.href = "?1";
+ i.onload = t.step_func_done(() => {
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+ assert_equals(i.contentWindow.navigation.entries()[0].getState(), undefined);
+ });
+ });
+}, "Default behavior for entry.getState() for a non-current cross-document entry");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/state/cross-document-getState.html b/testing/web-platform/tests/navigation-api/state/cross-document-getState.html
new file mode 100644
index 0000000000..aedbc471e0
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/state/cross-document-getState.html
@@ -0,0 +1,19 @@
+<!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_func(() => {
+ i.contentWindow.navigation.updateCurrentEntry({ state: { data: "value" } });
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ i.contentWindow.location.href = "?1";
+ i.onload = t.step_func_done(() => {
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+ assert_not_equals(i.contentWindow.navigation.entries()[0].getState(), undefined);
+ assert_equals(i.contentWindow.navigation.entries()[0].getState().data, "value");
+ });
+ });
+}, "entry.getState() still works for a non-current cross-document entry");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/state/cross-document-location-api.html b/testing/web-platform/tests/navigation-api/state/cross-document-location-api.html
new file mode 100644
index 0000000000..395d95c6fb
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/state/cross-document-location-api.html
@@ -0,0 +1,20 @@
+<!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_func(() => {
+ i.contentWindow.navigation.navigate("#", { history: "replace", state: { data: "value" } });
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.getState().data, "value");
+
+ i.contentWindow.location.href = "?1";
+ i.onload = t.step_func_done(() => {
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.getState(), undefined);
+ });
+ });
+}, "entry.getState() behavior after cross-document location API navigation");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/state/history-pushState.html b/testing/web-platform/tests/navigation-api/state/history-pushState.html
new file mode 100644
index 0000000000..7d3c79ba6c
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/state/history-pushState.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(() => {
+ navigation.navigate("#", { history: "replace", state: { data: "value" } });
+ assert_equals(navigation.currentEntry.getState().data, "value");
+ history.pushState(1, "", "#push");
+ assert_equals(navigation.currentEntry.getState(), undefined);
+}, "entry.getState() after history.pushState()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/state/history-replaceState.html b/testing/web-platform/tests/navigation-api/state/history-replaceState.html
new file mode 100644
index 0000000000..bdf3561639
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/state/history-replaceState.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(() => {
+ navigation.navigate("#", { history: "replace", state: { data: "value" } });
+ assert_equals(navigation.currentEntry.getState().data, "value");
+ history.replaceState(1, "", "#replace");
+ assert_equals(navigation.currentEntry.getState(), undefined);
+}, "entry.getState() after history.replaceState()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/state/location-reload.html b/testing/web-platform/tests/navigation-api/state/location-reload.html
new file mode 100644
index 0000000000..bf1bc105fb
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/state/location-reload.html
@@ -0,0 +1,19 @@
+<!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_func(() => {
+ i.contentWindow.navigation.navigate("#", { history: "replace", state: { data: "value" } });
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.getState().data, "value");
+
+ i.contentWindow.location.reload();
+ i.onload = t.step_func_done(() => {
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.getState().data, "value");
+ });
+ });
+}, "entry.getState() after location.reload()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/state/same-document-away-and-back-location-api.html b/testing/web-platform/tests/navigation-api/state/same-document-away-and-back-location-api.html
new file mode 100644
index 0000000000..d161df8b52
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/state/same-document-away-and-back-location-api.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(async () => {
+ let entry0 = navigation.currentEntry;
+
+ let navState = { statevar: "state" };
+ await navigation.navigate("#1", { state: navState });
+ let entry1 = navigation.currentEntry;
+
+ location.href = "#2";
+ let entry2 = navigation.currentEntry;
+
+ assert_equals(navigation.entries().length, 3);
+ assert_equals(entry0, navigation.entries()[0]);
+ assert_equals(entry1, navigation.entries()[1]);
+ assert_equals(entry2, navigation.entries()[2]);
+
+ assert_equals(entry0.getState(), undefined);
+
+ let state1 = entry1.getState();
+ assert_not_equals(state1, undefined);
+ assert_not_equals(state1, navState);
+ assert_equals(state1.statevar, "state");
+
+ let state2 = entry2.getState();
+ assert_not_equals(state2, undefined);
+ assert_not_equals(state2, navState);
+ assert_equals(state2.statevar, "state");
+
+ history.back();
+ window.onpopstate = t.step_func_done(() => {
+ assert_equals(navigation.entries().length, 3);
+ let back_entry = navigation.currentEntry;
+ assert_equals(back_entry, navigation.entries()[1]);
+ let back_state = back_entry.getState();
+ assert_not_equals(back_state, state1);
+ assert_not_equals(back_state, state2);
+ assert_equals(back_state.statevar, "state");
+ });
+ }, 0);
+}, "entry.getState() behavior after navigating away using a fragment change, then back");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/state/same-document-away-and-back-navigation-api.html b/testing/web-platform/tests/navigation-api/state/same-document-away-and-back-navigation-api.html
new file mode 100644
index 0000000000..220908e4c4
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/state/same-document-away-and-back-navigation-api.html
@@ -0,0 +1,53 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(async () => {
+ let entry0 = navigation.currentEntry;
+
+ let navState = { statevar: "state" };
+ await navigation.navigate("#1", { state: navState }).committed;
+ let entry1 = navigation.currentEntry;
+
+ navState.startvar2 = "otherstate";
+ await navigation.navigate("#2", { state: navState }).committed;
+ let entry2 = navigation.currentEntry;
+
+ t.step_func(() => {
+ assert_equals(navigation.entries().length, 3);
+ assert_equals(entry0, navigation.entries()[0]);
+ assert_equals(entry1, navigation.entries()[1]);
+ assert_equals(entry2, navigation.entries()[2]);
+
+ assert_equals(entry0.getState(), undefined);
+
+ let state1 = entry1.getState();
+ assert_not_equals(state1, undefined);
+ assert_not_equals(state1, navState);
+ assert_equals(state1.statevar, "state");
+ assert_equals(state1.startvar2, undefined);
+
+ let state2 = entry2.getState();
+ assert_not_equals(state2, undefined);
+ assert_not_equals(state2, navState);
+ assert_equals(state2.statevar, "state");
+ assert_equals(state2.startvar2, "otherstate");
+
+ navigation.back();
+ window.onpopstate = t.step_func_done(() => {
+ assert_equals(navigation.entries().length, 3);
+ let back_entry = navigation.currentEntry;
+ assert_equals(back_entry, navigation.entries()[1]);
+ let back_state = back_entry.getState();
+ assert_not_equals(back_state, state1);
+ assert_not_equals(back_state, state2);
+ assert_equals(back_state.statevar, "state");
+ assert_equals(back_state.startvar2, undefined);
+ });
+ })();
+ }, 0);
+}, "entry.getState() behavior after navigating away using the navigation API, then back");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/basic.html b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/basic.html
new file mode 100644
index 0000000000..b4a49e5bf9
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/basic.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+async_test(t => {
+ navigation.onnavigate = t.unreached_func("navigate must not fire");
+ navigation.onnavigatesuccess = t.unreached_func("navigatesuccess must not fire");
+ navigation.onnavigateerror = t.unreached_func("navigateerror must not fire");
+
+ assert_equals(navigation.currentEntry.getState(), undefined, "Navigation API state starts out as undefined");
+ assert_equals(history.state, null, "history.state starts out as null");
+
+ const newState = { key: "value" };
+
+ navigation.updateCurrentEntry({ state: newState });
+
+ assert_equals(navigation.currentEntry.getState().key, "value");
+ assert_not_equals(navigation.currentEntry.getState(), newState);
+ assert_equals(history.state, null);
+
+ // Wait a tick to make sure no events fire asynchronously.
+ t.step_timeout(() => t.done(), 0);
+}, "updateCurrentEntry() works as expected");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/cross-document-away-and-back.html b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/cross-document-away-and-back.html
new file mode 100644
index 0000000000..c37d5e979a
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/cross-document-away-and-back.html
@@ -0,0 +1,31 @@
+<!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_func(() => {
+ i.contentWindow.navigation.updateCurrentEntry({ state: { data: "value" } });
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.getState().data, "value");
+
+ let navigated_back = false;
+ i.contentWindow.navigation.navigate("?1");
+ i.onload = t.step_func(() => {
+ if (navigated_back) {
+ let back_entry = i.contentWindow.navigation.currentEntry;
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(back_entry.index, 0);
+ assert_equals(back_entry.getState().data, "value");
+ t.done();
+ } else {
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry, i.contentWindow.navigation.entries()[1]);
+ assert_equals(i.contentWindow.navigation.currentEntry.getState(), undefined);
+ history.back();
+ navigated_back = true;
+ }
+ });
+ });
+}, "entry.getState() behavior after navigating away and back");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/cross-document-location-api.html b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/cross-document-location-api.html
new file mode 100644
index 0000000000..26191fb876
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/cross-document-location-api.html
@@ -0,0 +1,20 @@
+<!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_func(() => {
+ i.contentWindow.navigation.updateCurrentEntry({ state: { data: "value" } });
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.getState().data, "value");
+
+ i.contentWindow.location.href = "?1";
+ i.onload = t.step_func_done(() => {
+ assert_equals(i.contentWindow.navigation.entries().length, 2);
+ assert_equals(i.contentWindow.navigation.currentEntry.index, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.getState(), undefined);
+ });
+ });
+}, "entry.getState() behavior after cross-document location API navigation");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/exception-order-initial-about-blank-unserializablestate.html b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/exception-order-initial-about-blank-unserializablestate.html
new file mode 100644
index 0000000000..010632a40f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/exception-order-initial-about-blank-unserializablestate.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe"></iframe>
+
+<script>
+test(() => {
+ assert_throws_dom("InvalidStateError", iframe.contentWindow.DOMException, () => {
+ iframe.contentWindow.navigation.updateCurrentEntry({ state: document.body });
+ });
+ assert_equals(navigation.currentEntry.getState(), undefined);
+}, `updateCurrentEntry() with unserializable state on the initial about:blank must throw an "InvalidStateError", not a "DataCloneError"`);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/exception-order-not-fully-active-unserializablestate.html b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/exception-order-not-fully-active-unserializablestate.html
new file mode 100644
index 0000000000..1e1c1e2bae
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/exception-order-not-fully-active-unserializablestate.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe" src="/common/blank.html"></iframe>
+
+<script>
+async_test(t => {
+ window.onload = t.step_func_done(() => {
+ const wNavigation = iframe.contentWindow.navigation;
+ const wDOMException = iframe.contentWindow.DOMException;
+
+ iframe.remove();
+
+ assert_throws_dom("InvalidStateError", wDOMException, () => {
+ wNavigation.updateCurrentEntry({ state: document.body });
+ });
+ assert_equals(navigation.currentEntry.getState(), undefined);
+ });
+}, `updateCurrentEntry() with unserializable state while not fully active must throw an "InvalidStateError", not a "DataCloneError"`);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/history-pushState.html b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/history-pushState.html
new file mode 100644
index 0000000000..852294c64f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/history-pushState.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(() => {
+ navigation.updateCurrentEntry({ state : { data : "value" } });
+ assert_equals(navigation.currentEntry.getState().data, "value");
+ history.pushState(1, "", "#push");
+ assert_equals(navigation.currentEntry.getState(), undefined);
+}, "entry.getState() after history.pushState()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/history-replaceState.html b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/history-replaceState.html
new file mode 100644
index 0000000000..3eb91a9a80
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/history-replaceState.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(() => {
+ navigation.updateCurrentEntry({ state : { data : "value" } });
+ assert_equals(navigation.currentEntry.getState().data, "value");
+ history.replaceState(1, "", "#replace");
+ assert_equals(navigation.currentEntry.getState(), undefined);
+}, "entry.getState() after history.replaceState()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/initial-about-blank.html b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/initial-about-blank.html
new file mode 100644
index 0000000000..c28137c082
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/initial-about-blank.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe"></iframe>
+
+<script>
+test(() => {
+ assert_throws_dom("InvalidStateError", iframe.contentWindow.DOMException, () => {
+ iframe.contentWindow.navigation.updateCurrentEntry({ state: 1 });
+ });
+ assert_equals(navigation.currentEntry.getState(), undefined);
+}, "updateCurrentEntry() must throw if the document is still on the initial about:blank");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/location-reload.html b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/location-reload.html
new file mode 100644
index 0000000000..8589eeb694
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/location-reload.html
@@ -0,0 +1,19 @@
+<!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_func(() => {
+ i.contentWindow.navigation.updateCurrentEntry({ state: { data: "value" } });
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.getState().data, "value");
+
+ i.contentWindow.location.reload();
+ i.onload = t.step_func_done(() => {
+ assert_equals(i.contentWindow.navigation.entries().length, 1);
+ assert_equals(i.contentWindow.navigation.currentEntry.getState().data, "value");
+ });
+ });
+}, "entry.getState() after location.reload()");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/no-args.html b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/no-args.html
new file mode 100644
index 0000000000..3fd011e3d3
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/no-args.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+test(() => {
+ assert_throws_js(TypeError, () => {
+ navigation.updateCurrentEntry();
+ }, "no args");
+
+ assert_throws_js(TypeError, () => {
+ navigation.updateCurrentEntry({});
+ }, "empty dictionary");
+}, "updateCurrentEntry() must throw if state is not given");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/not-fully-active.html b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/not-fully-active.html
new file mode 100644
index 0000000000..fce5e72c8d
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/not-fully-active.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe" src="/common/blank.html"></iframe>
+
+<script>
+async_test(t => {
+ window.onload = t.step_func_done(() => {
+ const wNavigation = iframe.contentWindow.navigation;
+ const wDOMException = iframe.contentWindow.DOMException;
+
+ iframe.remove();
+
+ assert_throws_dom("InvalidStateError", wDOMException, () => {
+ wNavigation.updateCurrentEntry({ state: 1 });
+ });
+ assert_equals(navigation.currentEntry.getState(), undefined);
+ });
+}, "updateCurrentEntry() must throw if the document is not fully active");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/opaque-origin.html b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/opaque-origin.html
new file mode 100644
index 0000000000..898ca27e4f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/opaque-origin.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe id="i" sandbox="allow-scripts" src="resources/opaque-origin-page.html"></iframe>
+
+<script>
+fetch_tests_from_window(i.contentWindow);
+</script>
diff --git a/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/resources/opaque-origin-page.html b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/resources/opaque-origin-page.html
new file mode 100644
index 0000000000..59931458a6
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/resources/opaque-origin-page.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<!-- Put this page in a sandbox to give it an opaque origin -->
+
+<script>
+test(t => {
+ assert_throws_dom("InvalidStateError", () => {
+ navigation.updateCurrentEntry({ state: 1 });
+ });
+}, "navigation.updateCurrentEntry() in an opaque origin iframe");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/same-document-away-and-back-location-api.html b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/same-document-away-and-back-location-api.html
new file mode 100644
index 0000000000..47b1904f4f
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/same-document-away-and-back-location-api.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ window.onload = () => t.step_timeout(async () => {
+ let entry0 = navigation.currentEntry;
+
+ let navState = { statevar: "state" };
+ navigation.updateCurrentEntry({ state: navState });
+ assert_equals(navigation.currentEntry, entry0);
+
+ location.href = "#2";
+ let entry1 = navigation.currentEntry;
+
+ assert_equals(navigation.entries().length, 2);
+ assert_equals(entry0, navigation.entries()[0]);
+ assert_equals(entry1, navigation.entries()[1]);
+
+ assert_equals(entry0.getState().statevar, "state");
+ assert_not_equals(entry0.getState(), navState);
+
+ assert_equals(entry1.getState().statevar, "state");
+ assert_not_equals(entry1.getState(), navState);
+ assert_not_equals(entry1.getState(), entry0.getState());
+
+ history.back();
+ window.onpopstate = t.step_func_done(() => {
+ assert_equals(navigation.entries().length, 2);
+ let back_entry = navigation.currentEntry;
+ assert_equals(back_entry, entry0);
+ let back_state = back_entry.getState();
+ assert_not_equals(back_state, navState);
+ assert_equals(back_state.statevar, "state");
+ });
+ }, 0);
+}, "entry.getState() behavior after navigating away using the location API, then back");
+</script>
diff --git a/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/unserializable.html b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/unserializable.html
new file mode 100644
index 0000000000..596ab16d62
--- /dev/null
+++ b/testing/web-platform/tests/navigation-api/updateCurrentEntry-method/unserializable.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe" src="/common/blank.html"></iframe>
+
+<script>
+setup({ explicit_done: true });
+
+window.onload = () => {
+ test(() => {
+ assert_throws_dom("DataCloneError", iframe.contentWindow.DOMException, () => {
+ iframe.contentWindow.navigation.updateCurrentEntry({ state: new WritableStream() });
+ });
+ assert_equals(navigation.currentEntry.getState(), undefined);
+ }, "updateCurrentEntry() must throw if state is unserializable (WritableStream)");
+
+ test(() => {
+ // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()`
+ const buffer = new WebAssembly.Memory({ shared:true, initial:1, maximum:1 }).buffer;
+
+ assert_throws_dom("DataCloneError", iframe.contentWindow.DOMException, () => {
+ iframe.contentWindow.navigation.updateCurrentEntry({ state: buffer });
+ });
+ assert_equals(navigation.currentEntry.getState(), undefined);
+ }, "updateCurrentEntry() must throw if state is unserializable (SharedArrayBuffer)");
+
+ done();
+};
+</script>