summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals')
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/README.md11
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/anchor-fragment-history-back-on-click.html40
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-nav.html30
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-traversal.html38
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-nav.html44
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-traversal.html37
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-stop.html21
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-nav.html77
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-traversal.html160
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-nav.html66
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-traversal.html94
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-stop.html42
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/forward-to-pruned-entry.html24
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/nav-cancelation-1.html62
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/nav-cancelation-2.sub.html178
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/helpers.mjs52
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/nav-cancelation-2-helper.html18
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/slow.py7
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-cross-document-nav.html41
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-cross-document-traversal.html70
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-same-document-nav.html61
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-same-document-traversal.html55
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-stop.html28
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-cross-document-nav.html42
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-cross-document-traversal.html88
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-nav.html57
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-hashchange.html148
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-pushstate.html137
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-stop.html37
29 files changed, 1765 insertions, 0 deletions
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/README.md b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/README.md
new file mode 100644
index 0000000000..cc313a155a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/README.md
@@ -0,0 +1,11 @@
+# Overlapping navigation and traversal tests
+
+These tests follow the behavior outlined in the
+[session history rewrite](https://github.com/whatwg/html/pull/6315).
+
+<https://github.com/whatwg/html/issues/6927> discusses these results.
+
+We are not yet 100% sure on this behavior, especially for overlapping
+traversal cases where the spec is complex and some of the tests don't
+seem to match any browser. Please feel free to discuss on the spec
+issue.
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/anchor-fragment-history-back-on-click.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/anchor-fragment-history-back-on-click.html
new file mode 100644
index 0000000000..a081bec514
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/anchor-fragment-history-back-on-click.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(async t => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ await new Promise(resolve => window.onload = () => t.step_timeout(resolve, 0));
+
+ location.hash = "#1";
+ assert_equals(location.hash, "#1");
+ location.hash = "#2";
+ assert_equals(location.hash, "#2");
+
+ let anchor = document.createElement("a");
+ anchor.href = "#3";
+ anchor.onclick = () => {
+ history.back();
+ };
+
+ let navigations = [];
+ let navigationsPromise = new Promise(resolve => {
+ onpopstate = () => {
+ navigations.push(location.hash);
+ if (navigations.length === 2) {
+ resolve();
+ }
+ }
+ });
+
+ anchor.click();
+ await navigationsPromise;
+
+ // We were on #2 when history.back() was called so we should be on #1 now.
+ assert_equals(location.hash, "#1");
+
+ // While the history navigation back to "#1" was pending, we should have navigated to "#3".
+ assert_array_equals(navigations, ["#3", "#1"]);
+}, "Anchor with a fragment href and a click handler that navigates back");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-nav.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-nav.html
new file mode 100644
index 0000000000..99d9a8fbb1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-nav.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cross-document navigation after a cross-document navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ According to the spec, the navigate algorithm synchronously cancels ongoing
+ non-mature navigations.
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad, waitForPotentialNetworkLoads } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.location.search = "?1";
+ iframe.contentWindow.location.search = "?2";
+ assert_equals(iframe.contentWindow.location.search, "");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?2");
+
+ iframe.onload = t.unreached_func("second load event");
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.search, "?2");
+}, "cross-document navigation then cross-document navigation");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-traversal.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-traversal.html
new file mode 100644
index 0000000000..341f66a996
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-traversal.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cross-document traversal during cross-document navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ According to the spec, "apply the history step" will set the ongoing
+ navigation to "traversal", canceling any non-mature navigations.
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad, delay } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.location.search = "?3";
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1", "must go back one step eventually");
+}, "cross-document navigations are stopped by cross-document back()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-nav.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-nav.html
new file mode 100644
index 0000000000..99525cb3ed
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-nav.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cross-document navigation after a same-document navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ According to the spec, the "URL and history update steps" (used by
+ pushState()) and the fragment navigation steps, do *not* modify the ongoing
+ navigation, i.e. do not cancel any navigations.
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.location.search = "?1";
+ iframe.contentWindow.location.hash = "#2";
+
+ assert_equals(iframe.contentWindow.location.search, "");
+ assert_equals(iframe.contentWindow.location.hash, "#2");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1");
+ assert_equals(iframe.contentWindow.location.hash, "");
+}, "cross-document navigation then fragment navigation");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.location.search = "?1";
+ iframe.contentWindow.history.pushState(null, "", "/2");
+
+ assert_equals(iframe.contentWindow.location.search, "");
+ assert_equals(iframe.contentWindow.location.pathname, "/2");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1");
+ assert_equals(iframe.contentWindow.location.pathname, "/common/blank.html");
+}, "cross-document navigation then pushState()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-traversal.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-traversal.html
new file mode 100644
index 0000000000..2ff91be7e1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-traversal.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Same-document traversal during cross-document navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ According to the spec, "apply the history step" will set the ongoing
+ navigation to "traversal", canceling any navigation that is still processing
+ in parallel and hasn't yet reached "apply the history step".
+-->
+
+<body>
+<script type="module">
+import { createIframe, delay } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.location.hash = "#1";
+ await delay(t, 0);
+ iframe.contentWindow.location.hash = "#2";
+ await delay(t, 0);
+
+ iframe.contentWindow.location.search = "?1";
+ iframe.contentWindow.onload = t.unreached_func("load event fired");
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.search, "", "must not go back synchronously (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go back synchronously (hash)");
+
+ // Does go back eventually, and only one step
+ await t.step_wait(() => iframe.contentWindow.location.hash === "#1" && iframe.contentWindow.location.search === "");
+}, "cross-document navigations are stopped by same-document back()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-stop.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-stop.html
new file mode 100644
index 0000000000..0803d6c8d1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-stop.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Stop during cross-document navigations</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script type="module">
+import { createIframe, waitForPotentialNetworkLoads } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.location.search = "?1";
+ iframe.contentWindow.onload = t.unreached_func("load event fired");
+ iframe.contentWindow.stop();
+
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.search, "");
+}, "cross-document navigations are stopped by stop()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-nav.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-nav.html
new file mode 100644
index 0000000000..5141259d08
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-nav.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cross-document navigations during cross-document traversals</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ According to the spec, if ongoing navigation is "traversal", the navigation
+ fails and nothing happens.
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad, delay, waitForPotentialNetworkLoads } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+ const slowURL = (new URL("resources/slow.py", location.href)).href;
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.href = slowURL;
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.href = "/common/blank.html?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously");
+
+ iframe.contentWindow.location.href = "/common/blank.html?3";
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not navigate synchronously");
+
+ // We end up at slow.py and never at /common/blank.html?3
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.href, slowURL, "first load after the nav");
+
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.href, slowURL, "must stay on slow.py");
+}, "slow cross-document traversal and then fast cross-document navigation: traversal wins and nav is ignored");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+ const slowURL = (new URL("resources/slow.py", location.href)).href;
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously");
+
+ iframe.contentWindow.location.href = slowURL;
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not navigate synchronously");
+
+ // We end up at ?1 and never at slowURL
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1", "first load after the nav");
+
+ // The long timeout is because slow.py would take 2 seconds, if it did load.
+ await delay(t, 3000);
+ assert_equals(iframe.contentWindow.location.search, "?1", "must stay on ?1");
+}, "fast cross-document traversal and then slow cross-document navigation: traversal wins and nav is ignored");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-traversal.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-traversal.html
new file mode 100644
index 0000000000..97907df23d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-traversal.html
@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cross-document traversals during cross-document traversals</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ In the spec, all traversals are queued, and that includes computing what
+ "back" and "forward" mean, based on the "current session history step". The
+ "current session history step" is updated at the end of "apply the history
+ step", at which point the queued steps in "traverse history by a delta" get to
+ run and compute what is back/forward. So the basic structure is:
+
+ - back(), back(): go back once, then again.
+ - back(), forward(): go back once, then go forward.
+
+ However, note that these observable effects (e.g., actually loading an
+ intermediate document) are done via queued tasks. Those tasks will end up not
+ running, once we switch the active document due to the second traversal. So
+ the end observable result looks like:
+
+ - back(), back(): go back -2.
+ - back(), forward(): go nowhere.
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad, delay, waitForPotentialNetworkLoads } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?3";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.history.back();
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ assert_equals(iframe.contentWindow.location.search, "?2", "we made our way to ?2 for setup");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go forward synchronously");
+
+ iframe.onload = t.unreached_func("second load event");
+
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.search, "?2", "must stay on ?2");
+}, "cross-document traversals in opposite directions: the result is going nowhere");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go forward synchronously");
+
+ iframe.onload = t.unreached_func("second load event");
+
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.search, "?2", "must stay on ?2");
+}, "cross-document traversals in opposite directions, second traversal invalid at queuing time but valid at the time it is run: the result is going nowhere");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?3";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.search, "?3", "must not go back synchronously (1)");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.search, "?3", "must not go back synchronously (2)");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1", "first load event must be going back");
+
+ iframe.onload = t.unreached_func("second load event");
+
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.search, "?1", "must stay on ?1");
+}, "cross-document traversals in the same (back) direction: the result is going -2 with only one load event");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?3";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.history.back();
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ assert_equals(iframe.contentWindow.location.search, "?2", "we made our way to ?2 for setup");
+ iframe.contentWindow.history.back();
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ assert_equals(iframe.contentWindow.location.search, "?1", "we made our way to ?1 for setup");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.search, "?1", "must not go forward synchronously (1)");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.search, "?1", "must not go forward synchronously (2)");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?3", "first load event must be going forward");
+
+ iframe.onload = t.unreached_func("second load event");
+
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.search, "?3", "must stay on ?3");
+}, "cross-document traversals in the same (forward) direction: the result is going +2 with only one load event");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-nav.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-nav.html
new file mode 100644
index 0000000000..df6258f9b3
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-nav.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Same-document navigations during cross-document traversals</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ The spec explicitly covers this case, with a Jake diagram:
+ https://whatpr.org/html/6315/browsing-the-web.html#example-sync-navigation-steps-queue-jumping-basic
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad, delay } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously");
+
+ iframe.contentWindow.location.hash = "#3";
+ assert_equals(iframe.contentWindow.location.search, "?2");
+ assert_equals(iframe.contentWindow.location.hash, "#3");
+
+ // Eventually ends up on ?1
+ await t.step_wait(() => iframe.contentWindow.location.search === "?1" && iframe.contentWindow.location.hash === "");
+}, "same-document traversals + fragment navigations");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously");
+
+ iframe.contentWindow.history.pushState(null, "", "?3");
+ assert_equals(iframe.contentWindow.location.search, "?3");
+
+ // Eventually ends up on ?1
+ await t.step_wait(() => iframe.contentWindow.location.search === "?1");
+}, "same-document traversals + pushState()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-traversal.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-traversal.html
new file mode 100644
index 0000000000..3c37c46b64
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-traversal.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Same-document traversals during cross-document traversals</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ This case is not significantly different from
+ cross-document-traversal-cross-document-traversal.html.
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad, waitForHashchange, delay, waitForPotentialNetworkLoads } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.hash = "#2";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.location.search = "?3";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.search, "?3", "must not go back synchronously 1 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go back synchronously 1 (hash)");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.search, "?3", "must not go back synchronously 2 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go back synchronously 2 (hash)");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1", "first load event must be going back (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "first load event must be going back (hash)");
+
+ iframe.contentWindow.onhashchange = t.unreached_func("hashchange event");
+ iframe.onload = t.unreached_func("second load event");
+
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.search, "?1", "must stay on ?1 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "must stay on ?1 (hash)");
+}, "traversals in the same (back) direction: coalesced");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.hash = "#3";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.history.back();
+ await waitForHashchange(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.hash, "", "we made our way to ?2 for setup");
+ iframe.contentWindow.history.back();
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ assert_equals(iframe.contentWindow.location.search, "?1", "we made our way to ?1 for setup");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.search, "?1", "must not go forward synchronously 1 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "must not go forward synchronously 1 (hash)");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.search, "?1", "must not go forward synchronously 2 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "must not go forward synchronously 2 (hash)");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?2", "first load event must be going forward (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#3", "first load event must be going forward (hash)");
+
+ iframe.contentWindow.onhashchange = t.unreached_func("hashchange event");
+ iframe.onload = t.unreached_func("second load event");
+
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.search, "?2", "must stay on ?2");
+ assert_equals(iframe.contentWindow.location.hash, "#3", "must stay on ?2");
+}, "traversals in the same (forward) direction: coalesced");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-stop.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-stop.html
new file mode 100644
index 0000000000..6202eb9226
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-stop.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Stop during cross-document traversals</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ The spec says that stop() must not stop traverals.
+
+ (Note: the spec also says the UI "stop" button must not stop traversals, but
+ that does not match browsers. See https://github.com/whatwg/html/issues/6905.
+ But that is not what's under test here.)
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad, delay } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously");
+
+ window.stop();
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1", "must go back eventually");
+}, "cross-document traversals are not stopped by stop()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/forward-to-pruned-entry.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/forward-to-pruned-entry.html
new file mode 100644
index 0000000000..8e1c349e21
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/forward-to-pruned-entry.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(async t => {
+ // Wait for after the load event so that the navigation doesn't get converted
+ // into a replace navigation.
+ await new Promise(resolve => window.onload = () => t.step_timeout(resolve, 0));
+ location.hash = "#1";
+ location.hash = "#2";
+ history.go(-2);
+ await new Promise(r => window.onpopstate = r);
+
+ // Traverse forward then immediately do a same-document push. This will
+ // truncate the back forward list.
+ history.forward();
+ location.hash = "#clobber";
+
+ // history.forward() should be aborted.
+ window.onpopstate = t.unreached_func("history.forward() should have been cancelled");
+ await new Promise(r => t.step_timeout(r, 20));
+ assert_equals(location.hash, "#clobber");
+}, "If forward pruning clobbers the target of a traverse, abort");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/nav-cancelation-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/nav-cancelation-1.html
new file mode 100644
index 0000000000..b52fa04977
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/nav-cancelation-1.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Parent main frame cancels a same-origin child whose navigation is pending</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ This test asserts that a parent canceling a same-origin child's cross-origin
+ navigation does not result in load events firing synchronously in the parent
+-->
+
+<body>
+
+<iframe src=resources/slow.py></iframe>
+
+<script>
+promise_test(async t => {
+ let window_load_fired = false;
+ let iframe_load_fired = false;
+ const iframe = document.querySelector('iframe');
+
+ const window_load_promise = new Promise(resolve => {
+ window.onload = () => {
+ window_load_fired = true;
+ resolve();
+ }
+ });
+
+ const iframe_onload_promise = new Promise(resolve => {
+ iframe.onload = () => {
+ iframe_load_fired = true;
+ resolve();
+ }
+ });
+
+ // While the child navigation is in-flight, cancel it and record when the
+ // parent `load` event fires.
+ window.frames[0].location.href = "resources/slow.py?different";
+
+ // Synchronously after cancelation, no load events should have been fired.
+ assert_false(window_load_fired,
+ "Parent's load event does not synchronously fire after cancelation");
+ assert_false(iframe_load_fired,
+ "<iframe> load event does not synchronously fire after cancelation");
+
+ // Load events did not fire in a microtask after cancelation.
+ await Promise.resolve();
+ assert_false(window_load_fired,
+ "Parent's load event does not fire in the microtask after cancelation");
+ assert_false(iframe_load_fired,
+ "<iframe> load event does not fire in the microtask after cancelation");
+
+ // Canceling the navigation should unblock the parent's load event, but the
+ // new iframe navigation should still be pending, and the iframe load event
+ // shouldn't fire until *that one* is complete.
+ await window_load_promise;
+ assert_true(window_load_fired,
+ "Parent's load event fires asynchronously after child navigation cancelation");
+ assert_false(iframe_load_fired,
+ "<iframe> load event does not fire until subsequent navigation is complete");
+}, "parent cancels a pending navigation in a same-origin child");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/nav-cancelation-2.sub.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/nav-cancelation-2.sub.html
new file mode 100644
index 0000000000..c081513b7c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/nav-cancelation-2.sub.html
@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Grandparent main frame cancels a navigation in a cross-origin grandchild</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ This test asserts that an ancestor canceling a cross-origin descendant's
+ ongoing navigation does not result in load events firing in the ancestor
+ synchronously.
+
+ The reason this test uses a grandparent/grandchild pair to represent the
+ ancestor/descendant, instead of a parent/child pair, is because if a child
+ frame is blocking its parent window's load event, that means the child frame
+ navigation is being made from the initial about:blank Document to some
+ resource, and the initial about:blank child is synchronously scriptable from
+ the parent since they share the same window agent. This test is trying to
+ capture the scenario where the descendant document (that owns the ongoing
+ navigation) is hosted/scheduled on a different agent than the ancestor
+ document that cancels the descendant's ongoing navigation. The only way to do
+ this is to have a grandparent frame load a cross-origin child, whose document
+ itself loads a child frame that has a very slow ongoing navigation. That way
+ the grandparent can reach the grandchild via `window.frames[0].frames[0]`,
+ which is a proxy to the document living in a different agent.
+-->
+
+<body>
+
+<iframe src="http://{{domains[www1]}}:{{ports[http][0]}}/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/nav-cancelation-2-helper.html"></iframe>
+
+<script>
+promise_test(async t => {
+ let window_load_fired = false;
+ let iframe_load_fired = false;
+ let grandchild_iframe_load_fired = false;
+ const iframe = document.querySelector('iframe');
+
+ const window_load_promise = new Promise(resolve => {
+ window.onload = () => {
+ window_load_fired = true;
+ resolve();
+ }
+ });
+
+ const iframe_onload_promise = new Promise(resolve => {
+ iframe.onload = () => {
+ iframe_load_fired = true;
+ resolve();
+ }
+ });
+
+ // Let the grandchild frame get registered in window.frames.
+ await new Promise((resolve, reject) => {
+ window.addEventListener('message', e => {
+ if (e.data != "grandchild frame created") {
+ reject(Error(`Expected 'grandchild frame created', but got ${e.data}`));
+ }
+
+ resolve();
+ }, {once: true});
+ });
+
+ // Set up a message handler to listen to the grandchild's iframe element's
+ // load event being fired. We should never get this message, and we assert
+ // this below. If we ever get this message, that means one of two things:
+ // 1.) The grandparent (this document)'s load event was blocked on the
+ // completion of its grandchild's subsequent navigation (after
+ // cancelation)
+ // 2.) After the grandchild's navigation was canceled, its <iframe>'s load
+ // event was fired before its subsequent navigation completed
+ // Both of these are wrong.
+ addEventListener('message', e => {
+ assert_equals(e.data, "grandchild frame loaded",
+ `Expected 'grandchild frame loaded', but got ${e.data}`);
+ grandchild_iframe_load_fired = true;
+ });
+
+ // While the grandchild navigation is in-flight, cancel it and record when the
+ // our `load` event fires. The second navigation is a slow resource so that
+ // the speed of the network doesn't cause the grandchild load event to fire
+ // early and confuse the grandparent when running the assertions below. We're
+ // trying to clearly separate out when the grandparent load event fires vs
+ // when the grandchild load event fires.
+ window.frames[0].frames[0].location.href = "resources/slow.py?different";
+
+ // Synchronously after cancelation, no load events should have been fired.
+ assert_false(window_load_fired,
+ "Grandparent's load event does not synchronously fire after grandchild " +
+ "navigation cancelation");
+ assert_false(iframe_load_fired,
+ "<iframe> load event does not synchronously fire after grandchild " +
+ "navigation cancelation");
+ assert_false(grandchild_iframe_load_fired,
+ "Grandchild <iframe>'s load event does not synchronously fire upon " +
+ "navigation cancelation");
+
+ // Load events did not fire in a microtask after cancelation.
+ await Promise.resolve();
+ assert_false(window_load_fired,
+ "Grandparent's load event does not fire in the microtask after " +
+ "navigation canceled");
+ assert_false(iframe_load_fired,
+ "<iframe> load event does not fire in the microtask after navigation " +
+ "canceled");
+ assert_false(grandchild_iframe_load_fired,
+ "Grandchild <iframe> load event does not fire in the microtask after " +
+ "navigation canceled");
+
+ // Canceling the navigation should however, asynchronously unblock, in this
+ // order:
+ // 1.) Our child window's load event, captured by our `iframe`'s load event
+ // 2.) Our window load event
+ // On the other hand, the grandchild navigation should still be ongoing, so
+ // inside our child's document, the nested <iframe> representing our
+ // grandchild should not have had its load event fired yet.
+ await iframe_onload_promise;
+ assert_true(iframe_load_fired);
+ assert_false(window_load_fired,
+ "Grandparent's load event does not fire before its child iframe's load " +
+ "event");
+ assert_false(grandchild_iframe_load_fired,
+ "Grandchild <iframe>'s load event does not fire before its parent's load " +
+ "event and grandparent's load event");
+
+ // We want to assert that the grandparent is not (incorrectly) blocked on its
+ // grandchild's second navigation from completing. One sign that it was
+ // incorrectly blocked on its grandchild's second navigation is if the
+ // grandparent receives a message (saying that the grandchild <iframe>
+ // element's load event fired) before the grandparent's load event fires.
+ //
+ // This indicates a weird state where the grandparent's immediate child fired
+ // its load event in response to navigation cancelation (see the assertions
+ // above), but the grandparent itself is still blocked on the grandchild
+ // loading. If this is the case, the the postMessage() (that sets
+ // `grandchild_iframe_load_fired = true`) is received by the grandparent just
+ // before the grandparent's load event is unblocked and fired. Therefore we
+ // can detect this situation by checking `grandchild_iframe_load_fired`.
+ await window_load_promise;
+ assert_true(iframe_load_fired);
+ assert_true(window_load_fired,
+ "Grandparent's load event fires asynchronously after grandchild " +
+ "navigation cancelation");
+ assert_false(grandchild_iframe_load_fired,
+ "Grandchild <iframe> load event doesn't fire before grandparent's " +
+ "load event");
+
+ // Verify that the grandchild <iframe>'s load event does not fire within one
+ // task of the grandchild's load event from being fired. This is to further
+ // verify that the grandparent's load event is not tied to its grandchild's
+ // second navigation.
+ //
+ // If for example, the grandparent's load event *is* blocked on the
+ // grandchild's second navigation from finishing, it is still possible for the
+ // grandparent's load event to fire. For example, Chromium has a bug where if
+ // both are true:
+ // 1.) The grandparent frame is in the same process as the grandchild frame
+ // 2.) The grandparent frame's load event is blocked on its grandchild's
+ // second navigation
+ //
+ // ...then the following will happen:
+ // 1.) The grandchild's load event will fire, triggering a postMessage() to
+ // the grandparent frame. This queues a task to run the grandparent's
+ // message handler.
+ // 2.) The grandparent's load event will *immediately* fire, and the
+ // postMessage() will fire a single task later since it is queued.
+ //
+ // Therefore, we assert that `grandchild_iframe_load_fired` is not true up to
+ // a single task after the grandparent's load event fires.
+ await new Promise(resolve => {
+ t.step_timeout(resolve, 0);
+ });
+
+ assert_false(grandchild_iframe_load_fired,
+ "Grandchild <iframe>'s load event does not fire at least one task " +
+ "after the grandparent's window load event fires. It should only fire " +
+ "when its subsequent navigation is complete");
+}, "grandparent cancels a pending navigation in a cross-origin grandchild");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/helpers.mjs b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/helpers.mjs
new file mode 100644
index 0000000000..7938497920
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/helpers.mjs
@@ -0,0 +1,52 @@
+export function createIframe(t) {
+ return new Promise((resolve, reject) => {
+ const iframe = document.createElement("iframe");
+ iframe.onload = () => resolve(iframe);
+ iframe.onerror = () => reject(new Error("Could not load iframe"));
+ iframe.src = "/common/blank.html";
+
+ t.add_cleanup(() => iframe.remove());
+ document.body.append(iframe);
+ });
+}
+
+export function delay(t, ms) {
+ return new Promise(resolve => t.step_timeout(resolve, ms));
+}
+
+export function waitForLoad(obj) {
+ return new Promise(resolve => {
+ obj.addEventListener("load", resolve, { once: true });
+ });
+}
+
+export function waitForHashchange(obj) {
+ return new Promise(resolve => {
+ obj.addEventListener("hashchange", resolve, { once: true });
+ });
+}
+
+export function waitForPopstate(obj) {
+ return new Promise(resolve => {
+ obj.addEventListener("popstate", resolve, { once: true });
+ });
+}
+
+// This is used when we want to end the test by asserting some load doesn't
+// happen, but we're not sure how long to wait. We could just wait a long-ish
+// time (e.g. a second), but that makes the tests slow. Instead, assume that
+// network loads take roughly the same time. Then, you can use this function to
+// wait a small multiple of the duration of a separate iframe load; this should
+// be long enough to catch any problems.
+export async function waitForPotentialNetworkLoads(t) {
+ const before = performance.now();
+
+ // Sometimes we're doing something, like a traversal, which cancels our first
+ // attempt at iframe loading. In that case we bail out after 100 ms and try
+ // again. (Better ideas welcome...)
+ await Promise.race([createIframe(t), delay(t, 100)]);
+ await createIframe(t);
+
+ const after = performance.now();
+ await delay(t, after - before);
+}
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/nav-cancelation-2-helper.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/nav-cancelation-2-helper.html
new file mode 100644
index 0000000000..a0b4acda2e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/nav-cancelation-2-helper.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Page with child frame that navigates slowly</title>
+
+<!--
+ This file is used by `../nav-cancelation-2.sub.html`. The iframe below is its
+ grandchild iframe, and whenever its load event fires we report this up to our
+ parent Document.
+-->
+<iframe src="slow.py"></iframe>
+
+<script>
+ window.parent.postMessage("grandchild frame created", "*");
+ const iframe = document.querySelector('iframe');
+ iframe.onload = e => {
+ window.parent.postMessage("grandchild frame loaded", "*");
+ };
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/slow.py b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/slow.py
new file mode 100644
index 0000000000..5ee32a60ba
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/slow.py
@@ -0,0 +1,7 @@
+# Like /common/slow.py except with text/html content-type so that it won't
+# trigger strange parts of the <iframe> navigate algorithm.
+import time
+
+def main(request, response):
+ time.sleep(2)
+ return 200, [["Content-Type", "text/html"]], b''
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-cross-document-nav.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-cross-document-nav.html
new file mode 100644
index 0000000000..8082e9bbe0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-cross-document-nav.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cross-document navigation after a same-document navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ These tests are kind of silly since it's hard to imagine any other result:
+ same-document navigations are always synchronous so of course the
+ same-document navigation will succeed, followed by the cross-document one.
+
+ Nevertheless they're nice as a basis from which to write corresponding app
+ history tests, where the consequences aren't as obvious.
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.location.hash = "#1";
+ assert_equals(iframe.contentWindow.location.hash, "#1");
+
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?2");
+}, "fragment navigation then cross-document navigation");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.history.pushState(null, "", "?1");
+ assert_equals(iframe.contentWindow.location.search, "?1");
+
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?2");
+}, "pushState() then cross-document navigation");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-cross-document-traversal.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-cross-document-traversal.html
new file mode 100644
index 0000000000..fc6f92e819
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-cross-document-traversal.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Traversal after a same-document navigations</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ These tests are kind of silly since it's hard to imagine any other result:
+ same-document navigations are always synchronous so of course back() won't
+ cancel them.
+
+ Nevertheless they're nice as a basis from which to write corresponding app
+ history tests, where the consequences aren't as obvious.
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad, delay } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.location.hash = "#3";
+ iframe.contentWindow.history.go(-2);
+
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#3", "must not go back synchronously (hash)");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1", "must go back eventually (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "must go back eventually (hash)");
+}, "fragment navigation then go(-2)");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+
+ iframe.contentWindow.history.pushState(null, "", "/3");
+ iframe.contentWindow.history.go(-2);
+
+ assert_equals(iframe.contentWindow.location.search, "", "must not go back synchronously (search)");
+ assert_equals(iframe.contentWindow.location.pathname, "/3", "must not go back synchronously (pathname)");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1", "must go back eventually (search)");
+ assert_equals(iframe.contentWindow.location.pathname, "/common/blank.html", "must go back eventually (pathname)");
+
+}, "pushState then go(-2)");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-same-document-nav.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-same-document-nav.html
new file mode 100644
index 0000000000..2d8961d6e4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-same-document-nav.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Same-document navigation after a same-document navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ These tests are kind of silly since it's hard to imagine any other result:
+ same-document navigations are always synchronous so of course two in a row
+ will succeed.
+
+ Nevertheless they're nice as a basis from which to write corresponding app
+ history tests, where the consequences aren't as obvious.
+-->
+
+<body>
+<script type="module">
+import { createIframe } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.location.hash = "#1";
+ assert_equals(iframe.contentWindow.location.hash, "#1");
+
+ iframe.contentWindow.location.hash = "#2";
+ assert_equals(iframe.contentWindow.location.hash, "#2");
+}, "fragment navigation then fragment navigation");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.history.pushState(null, "", "?1");
+ assert_equals(iframe.contentWindow.location.search, "?1");
+
+ iframe.contentWindow.history.pushState(null, "", "?2");
+ assert_equals(iframe.contentWindow.location.search, "?2");
+}, "pushState() then pushState()");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.history.pushState(null, "", "?1");
+ assert_equals(iframe.contentWindow.location.search, "?1");
+
+ iframe.contentWindow.location.hash = "#2";
+ assert_equals(iframe.contentWindow.location.search, "?1");
+ assert_equals(iframe.contentWindow.location.hash, "#2");
+}, "pushState() then fragment navigation");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.location.hash = "#1";
+ assert_equals(iframe.contentWindow.location.hash, "#1");
+
+ iframe.contentWindow.history.pushState(null, "", "?2");
+ assert_equals(iframe.contentWindow.location.search, "?2");
+ assert_equals(iframe.contentWindow.location.hash, "");
+}, "fragment navigation then pushState()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-same-document-traversal.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-same-document-traversal.html
new file mode 100644
index 0000000000..a112143837
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-same-document-traversal.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Same-document traversal after a same-document navigations</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ These tests are kind of silly since it's hard to imagine any other result:
+ same-document navigations are always synchronous so of course back() won't
+ cancel them.
+
+ Nevertheless they're nice as a basis from which to write corresponding app
+ history tests, where the consequences aren't as obvious.
+-->
+
+<body>
+<script type="module">
+import { createIframe, delay } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.location.hash = "#1";
+ await delay(t, 0);
+ iframe.contentWindow.location.hash = "#2";
+ await delay(t, 0);
+
+ iframe.contentWindow.location.hash = "#3";
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.hash, "#3", "must not go back synchronously");
+
+ // Does go back eventually, and only one step
+ await t.step_wait(() => iframe.contentWindow.location.hash === "#2");
+}, "fragment navigation then back()");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.history.pushState(null, "", "?1");
+ await delay(t, 0);
+ iframe.contentWindow.history.pushState(null, "", "?2");
+ await delay(t, 0);
+
+ iframe.contentWindow.history.pushState(null, "", "?3");
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.search, "?3", "must not go back synchronously");
+
+ // Does go back eventually, and only one step
+ await t.step_wait(() => iframe.contentWindow.location.search === "?2");
+}, "pushState then back()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-stop.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-stop.html
new file mode 100644
index 0000000000..a9036209a5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-nav-stop.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Stop after a same-document navigations</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script type="module">
+import { createIframe } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.location.hash = "#1";
+ iframe.contentWindow.stop();
+
+ assert_equals(iframe.contentWindow.location.hash, "#1");
+}, "fragment navigations are not stopped by stop()");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ iframe.contentWindow.history.pushState(null, "", "?1");
+ iframe.contentWindow.stop();
+
+ assert_equals(iframe.contentWindow.location.search, "?1");
+}, "pushState() navigations are not stopped by stop()");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-cross-document-nav.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-cross-document-nav.html
new file mode 100644
index 0000000000..37960c3c54
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-cross-document-nav.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cross-document navigations during same-document traversals</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ The spec says that navigations are ignored if there is an ongoing traversal.
+-->
+
+<body>
+<script type="module">
+import { createIframe, delay, waitForPotentialNetworkLoads } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.location.hash = "#1";
+ await delay(t, 0);
+ iframe.contentWindow.location.hash = "#2";
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go back synchronously");
+
+ iframe.contentWindow.location.search = "?1";
+ assert_equals(iframe.contentWindow.location.search, "", "must not navigate synchronously (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not navigate synchronously (hash)");
+
+ // Eventually ends up on #1.
+ await t.step_wait(() => iframe.contentWindow.location.hash === "#1", "traversal");
+
+ // Never loads a different document.
+ iframe.onload = t.unreached_func("load event");
+ await waitForPotentialNetworkLoads(t);
+ assert_equals(iframe.contentWindow.location.search, "", "must stay on #2 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must stay on #2 (hash)");
+}, "same-document traversals are not canceled by cross-document navigations");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-cross-document-traversal.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-cross-document-traversal.html
new file mode 100644
index 0000000000..a48f4d484f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-cross-document-traversal.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cross-document traversals during same-document traversals</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ Compare this to cross-document-traversal-cross-document-traversal.html. Since
+ there are no network loads for the first traversal here, it does observably go
+ through. So we end up with both traversals before observable in sequence.
+-->
+
+<body>
+<script type="module">
+import { createIframe, waitForLoad, waitForHashchange, delay } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.search = "?2";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.hash = "#3";
+ await waitForHashchange(iframe.contentWindow);
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously 1 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#3", "must not go back synchronously 1 (hash)");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.search, "?2", "must not go back synchronously 1 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#3", "must not go back synchronously 1 (hash)");
+
+ await waitForHashchange(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.search, "?2", "first hashchange event must be going back (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "first hashchange event must be going back (hash)");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?1", "first load event must be going back (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "first load event must be going back (hash)");
+}, "traversals in the same (back) direction: queued up");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ // Extra delay()s are necessary because if we navigate "inside" the load
+ // handler (i.e. in a promise reaction for the load handler) then it will
+ // be a replace navigation.
+ iframe.contentWindow.location.search = "?1";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.location.hash = "#2";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.location.search = "?3";
+ await waitForLoad(iframe);
+ await delay(t, 0);
+ iframe.contentWindow.history.back();
+ await waitForLoad(iframe);
+ iframe.contentWindow.history.back();
+ await waitForHashchange(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.search, "?1", "we made our way to ?1 for setup (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "we made our way to ?1 for setup (search)");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.search, "?1", "must not go forward synchronously 1 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "must not go forward synchronously 1 (hash)");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.search, "?1", "must not go forward synchronously 2 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "", "must not go forward synchronously 2 (hash)");
+
+ await waitForHashchange(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.search, "?1", "first hashchange event must be going forward (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#2", "first hashchange event must be going forward (hash)");
+
+ await waitForLoad(iframe);
+ assert_equals(iframe.contentWindow.location.search, "?3", "first load event must be going forward (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#2", "first load event must be going forward (hash)");
+}, "traversals in the same (forward) direction: queued up");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-nav.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-nav.html
new file mode 100644
index 0000000000..5094651ab5
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-nav.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Same-document navigations during same-document traversals</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ Per spec, same-document navigations ignore the "ongoing navigation" flag and
+ just happen synchronously, then queue onto the session history traversal queue
+ to update the source of truth. However, the traversal was queued first, so it
+ will ignore that update when calculating its endpoint.
+-->
+
+<body>
+<script type="module">
+import { createIframe, delay } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.location.hash = "#1";
+ await delay(t, 0);
+ iframe.contentWindow.location.hash = "#2";
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go back synchronously");
+
+ iframe.contentWindow.location.hash = "#3";
+ assert_equals(iframe.contentWindow.location.hash, "#3");
+
+ // Eventually ends up on #1
+ await t.step_wait(() => iframe.contentWindow.location.hash === "#1");
+}, "same-document traversals are not canceled by fragment navigations and calculate their endpoint based on the original placement");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.history.pushState(null, "", "/1");
+ await delay(t, 0);
+ iframe.contentWindow.history.pushState(null, "", "/2");
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "must not go back synchronously");
+
+ iframe.contentWindow.history.pushState(null, "", "/3");
+ assert_equals(iframe.contentWindow.location.pathname, "/3");
+
+ // Eventually ends up on /1
+ await t.step_wait(() => iframe.contentWindow.location.pathname === "/1");
+}, "same-document traversals are not canceled by pushState() and calculate their endpoint based on the original placement");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-hashchange.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-hashchange.html
new file mode 100644
index 0000000000..df5ea5caab
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-hashchange.html
@@ -0,0 +1,148 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Same-document traversals during same-document traversals (using fragment navigations)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ Compare this to cross-document-traversal-cross-document-traversal.html. Since
+ there are no network loads or document unloads to cancel tasks, both
+ traversals should observably go through. Target step calculation for the
+ second traversal should take place after the first traversal is finished. So
+ we end up with both traversals observable in sequence.
+-->
+
+<body>
+<script type="module">
+import { createIframe, delay, waitForHashchange } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+ const baseURL = iframe.contentWindow.location.href;
+
+ // Setup
+ iframe.contentWindow.location.hash = "#1";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.location.hash = "#2";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.location.hash = "#3";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.history.back();
+ await waitForHashchange(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.hash, "#2", "we made our way to #2 for setup");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go back synchronously");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go forward synchronously");
+
+ const event1 = await waitForHashchange(iframe.contentWindow);
+ assert_equals(event1.oldURL, baseURL + "#2", "oldURL 1");
+ assert_equals(event1.newURL, baseURL + "#1", "newURL 1");
+ // Cannot test iframe.contentWindow.location.hash since the second history
+ // traversal task is racing with the fire an event task, so we don't know
+ // which will happen first.
+
+ const event2 = await waitForHashchange(iframe.contentWindow);
+ assert_equals(event2.oldURL, baseURL + "#1", "oldURL 2");
+ assert_equals(event2.newURL, baseURL + "#2", "newURL 2");
+ assert_equals(iframe.contentWindow.location.hash, "#2");
+}, "same-document traversals in opposite directions: queued up");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+ const baseURL = iframe.contentWindow.location.href;
+
+ // Setup
+ iframe.contentWindow.location.hash = "#1";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.location.hash = "#2";
+ await waitForHashchange(iframe.contentWindow);
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go back synchronously");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go forward synchronously");
+
+ const event1 = await waitForHashchange(iframe.contentWindow);
+ assert_equals(event1.oldURL, baseURL + "#2", "oldURL 1");
+ assert_equals(event1.newURL, baseURL + "#1", "newURL 1");
+ // Cannot test iframe.contentWindow.location.hash since the second history
+ // traversal task is racing with the fire an event task, so we don't know
+ // which will happen first.
+
+ const event2 = await waitForHashchange(iframe.contentWindow);
+ assert_equals(event2.oldURL, baseURL + "#1", "oldURL 2");
+ assert_equals(event2.newURL, baseURL + "#2", "newURL 2");
+ assert_equals(iframe.contentWindow.location.hash, "#2");
+}, "same-document traversals in opposite directions, second traversal invalid at queuing time: queued up");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+ const baseURL = iframe.contentWindow.location.href;
+
+ // Setup
+ iframe.contentWindow.location.hash = "#1";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.location.hash = "#2";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.location.hash = "#3";
+ await waitForHashchange(iframe.contentWindow);
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.hash, "#3", "must not go back synchronously (1)");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.hash, "#3", "must not go back synchronously (2)");
+
+ const event1 = await waitForHashchange(iframe.contentWindow);
+ assert_equals(event1.oldURL, baseURL + "#3", "oldURL 1");
+ assert_equals(event1.newURL, baseURL + "#2", "newURL 1");
+ // Cannot test iframe.contentWindow.location.hash since the second history
+ // traversal task is racing with the fire an event task, so we don't know
+ // which will happen first.
+
+ const event2 = await waitForHashchange(iframe.contentWindow);
+ assert_equals(event2.oldURL, baseURL + "#2", "oldURL 2");
+ assert_equals(event2.newURL, baseURL + "#1", "newURL 2");
+ assert_equals(iframe.contentWindow.location.hash, "#1");
+}, "same-document traversals in the same (back) direction: queue up");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+ const baseURL = iframe.contentWindow.location.href;
+
+ // Setup
+ iframe.contentWindow.location.hash = "#1";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.location.hash = "#2";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.location.hash = "#3";
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.history.back();
+ await waitForHashchange(iframe.contentWindow);
+ iframe.contentWindow.history.back();
+ await waitForHashchange(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.hash, "#1", "we made our way to #1 for setup");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.hash, "#1", "must not go forward synchronously (1)");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.hash, "#1", "must not go forward synchronously (2)");
+
+ const event1 = await waitForHashchange(iframe.contentWindow);
+ assert_equals(event1.oldURL, baseURL + "#1", "oldURL 1");
+ assert_equals(event1.newURL, baseURL + "#2", "newURL 1");
+ // Cannot test iframe.contentWindow.location.hash since the second history
+ // traversal task is racing with the fire an event task, so we don't know
+ // which will happen first.
+
+ const event2 = await waitForHashchange(iframe.contentWindow);
+ assert_equals(event2.oldURL, baseURL + "#2", "oldURL 2");
+ assert_equals(event2.newURL, baseURL + "#3", "newURL 2");
+ assert_equals(iframe.contentWindow.location.hash, "#3");
+}, "same-document traversals in the same (forward) direction: queue up");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-pushstate.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-pushstate.html
new file mode 100644
index 0000000000..47c7d6e3dc
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-pushstate.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Same-document traversals during same-document traversals (using pushState())</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ Compare this to cross-document-traversal-cross-document-traversal.html. Since
+ there are no network loads or document unloads to cancel tasks, both
+ traversals should observably go through. Target step calculation for the
+ second traversal should take place after the first traversal is finished. So
+ we end up with both traversals observable in sequence.
+-->
+
+<body>
+<script type="module">
+import { createIframe, delay, waitForPopstate } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.history.pushState(1, "", "/1");
+ assert_equals(iframe.contentWindow.location.pathname, "/1", "setup /1");
+ iframe.contentWindow.history.pushState(2, "", "/2");
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "setup /2");
+ iframe.contentWindow.history.pushState(3, "", "/3");
+ assert_equals(iframe.contentWindow.location.pathname, "/3", "setup /3");
+ iframe.contentWindow.history.back();
+ await waitForPopstate(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "we made our way to /2 for setup");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "must not go back synchronously");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "must not go forward synchronously");
+
+ const event1 = await waitForPopstate(iframe.contentWindow);
+ assert_equals(event1.state, 1, "state 1");
+ // Cannot test iframe.contentWindow.location.pathname since the second history
+ // traversal task is racing with the fire an event task, so we don't know
+ // which will happen first.
+
+ const event2 = await waitForPopstate(iframe.contentWindow);
+ assert_equals(event2.state, 2, "state 2");
+ assert_equals(iframe.contentWindow.location.pathname, "/2");
+}, "same-document traversals in opposite directions: queued up");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.history.pushState(1, "", "/1");
+ assert_equals(iframe.contentWindow.location.pathname, "/1", "setup /1");
+ iframe.contentWindow.history.pushState(2, "", "/2");
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "we made our way to /2 for setup");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "must not go back synchronously");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "must not go forward synchronously");
+
+ const event1 = await waitForPopstate(iframe.contentWindow);
+ assert_equals(event1.state, 1, "state 1");
+ // Cannot test iframe.contentWindow.location.pathname since the second history
+ // traversal task is racing with the fire an event task, so we don't know
+ // which will happen first.
+
+ const event2 = await waitForPopstate(iframe.contentWindow);
+ assert_equals(event2.state, 2, "state 2");
+ assert_equals(iframe.contentWindow.location.pathname, "/2");
+}, "same-document traversals in opposite directions, second traversal invalid at queuing time: queued up");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.history.pushState(1, "", "/1");
+ assert_equals(iframe.contentWindow.location.pathname, "/1", "setup /1");
+ iframe.contentWindow.history.pushState(2, "", "/2");
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "setup /2");
+ iframe.contentWindow.history.pushState(3, "", "/3");
+ assert_equals(iframe.contentWindow.location.pathname, "/3", "we made our way to /3 for setup");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.pathname, "/3", "must not go back synchronously (1)");
+
+ iframe.contentWindow.history.back();
+ assert_equals(iframe.contentWindow.location.pathname, "/3", "must not go back synchronously (2)");
+
+ const event1 = await waitForPopstate(iframe.contentWindow);
+ assert_equals(event1.state, 2, "state 1");
+ // Cannot test iframe.contentWindow.location.pathname since the second history
+ // traversal task is racing with the fire an event task, so we don't know
+ // which will happen first.
+
+ const event2 = await waitForPopstate(iframe.contentWindow);
+ assert_equals(event2.state, 1, "state 2");
+ assert_equals(iframe.contentWindow.location.pathname, "/1");
+}, "same-document traversals in the same (back) direction: queue up");
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.history.pushState(1, "", "/1");
+ assert_equals(iframe.contentWindow.location.pathname, "/1", "setup /1");
+ iframe.contentWindow.history.pushState(2, "", "/2");
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "setup /2");
+ iframe.contentWindow.history.pushState(3, "", "/3");
+ assert_equals(iframe.contentWindow.location.pathname, "/3", "setup /3");
+ iframe.contentWindow.history.back();
+ await waitForPopstate(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.pathname, "/2", "setup /2 again");
+ iframe.contentWindow.history.back();
+ await waitForPopstate(iframe.contentWindow);
+ assert_equals(iframe.contentWindow.location.pathname, "/1", "we made our way to /1 for setup");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.pathname, "/1", "must not go forward synchronously (1)");
+
+ iframe.contentWindow.history.forward();
+ assert_equals(iframe.contentWindow.location.pathname, "/1", "must not go forward synchronously (2)");
+
+ const event1 = await waitForPopstate(iframe.contentWindow);
+ assert_equals(event1.state, 2, "state 1");
+ // Cannot test iframe.contentWindow.location.pathname since the second history
+ // traversal task is racing with the fire an event task, so we don't know
+ // which will happen first.
+
+ const event2 = await waitForPopstate(iframe.contentWindow);
+ assert_equals(event2.state, 3, "state 2");
+ assert_equals(iframe.contentWindow.location.pathname, "/3");
+}, "same-document traversals in the same (forward) direction: queue up");
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-stop.html b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-stop.html
new file mode 100644
index 0000000000..2f0570380a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-stop.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Stop during same-document traversals</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!--
+ The spec says that stop() must not stop traverals.
+
+ (Note: the spec also says the UI "stop" button must not stop traversals, but
+ that does not match browsers. See https://github.com/whatwg/html/issues/6905.
+ But that is not what's under test here.)
+-->
+
+<body>
+<script type="module">
+import { createIframe, delay } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+
+ // Setup
+ iframe.contentWindow.location.hash = "#1";
+ await delay(t, 0);
+ iframe.contentWindow.location.hash = "#2";
+ await delay(t, 0);
+
+ iframe.contentWindow.history.back();
+
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must not go back synchronously");
+
+ window.stop();
+
+ // Does go back eventually
+ await t.step_wait(() => iframe.contentWindow.location.hash === "#1");
+}, "same-document traversals are not stopped by stop()");
+</script>