diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /testing/web-platform/tests/navigation-api/ordering-and-transition | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/navigation-api/ordering-and-transition')
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> |