summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/navigation-api/ordering-and-transition
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/navigation-api/ordering-and-transition')
-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
35 files changed, 1769 insertions, 0 deletions
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>