Tentative WPTs for overlapping navigations/traverals
See https://github.com/whatwg/html/issues/6927.
This will help app history which needs its own versions of these tests.
Change-Id: I84c67c6f23149d9c106f011ec13807d68d749633
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3067883
Reviewed-by: Nate Chapin <japhet@chromium.org>
Commit-Queue: Domenic Denicola <domenic@chromium.org>
Cr-Commit-Position: refs/heads/master@{#909433}
diff --git a/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/README.md b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/README.md
new file mode 100644
index 0000000..02d2e94
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/README.md
@@ -0,0 +1,13 @@
+# Overlapping navigation and traversal tests
+
+While developing [app history](https://wicg.github.io/app-history/), @domenic
+noticed that cancelation of navigations and history traversals is not very
+well-defined in the spec.
+
+On the spec side, this will probably be fixed as part of, or after, the
+[session history rewrite](https://github.com/whatwg/html/pull/6315).
+
+In the meantime, this directory contains tests which try to match most browsers,
+or picks one of the potential behaviors.
+
+<https://github.com/whatwg/html/issues/6927> discusses these results.
diff --git a/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-nav-cross-document-nav.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-nav-cross-document-nav.html
new file mode 100644
index 0000000..99d9a8f
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/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/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-nav-cross-document-traversal.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-nav-cross-document-traversal.html
new file mode 100644
index 0000000..eed07f4
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/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, "traverse the history by a delta" (e.g. history.back())
+ cancels 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 same-document back()");
+</script>
diff --git a/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-nav-same-document-nav.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-nav-same-document-nav.html
new file mode 100644
index 0000000..1b2aeae
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/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, both cancel any ongoing
+ history traversals, but do *not* cancel any ongoing 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/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-nav-same-document-traversal.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-nav-same-document-traversal.html
new file mode 100644
index 0000000..fac6624
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-nav-same-document-traversal.html
@@ -0,0 +1,36 @@
+<!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, "traverse the history by a delta" (e.g. history.back())
+ cancels any non-mature navigations.
+-->
+
+<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/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-nav-stop.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-nav-stop.html
new file mode 100644
index 0000000..0803d6c
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/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/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-traversal-cross-document-nav.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-traversal-cross-document-nav.html
new file mode 100644
index 0000000..05d4705
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-traversal-cross-document-nav.html
@@ -0,0 +1,78 @@
+<!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>
+
+<!--
+ Apparently if a cross-document traversal is in progress, a cross-document
+ navigation just gets ignored. (Instead of, as you might expect, a race.)
+ This does not match the spec. This test instead asserts browser behavior.
+-->
+
+<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/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-traversal-cross-document-traversal.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-traversal-cross-document-traversal.html
new file mode 100644
index 0000000..ec8b374
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-traversal-cross-document-traversal.html
@@ -0,0 +1,169 @@
+<!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. However, what "back" and "forward"
+ mean is computed synchronously. So per spec:
+
+ - back(), back(): go back 1.
+ - back(), forward(): go forward 1 (if you're not starting from the end).
+
+ This is not how browsers behave:
+
+ - Chrome seems to coalesce all traversals and apply them at once, i.e.:
+
+ - back(), back(): go back 2 at once.
+ - back(), forward(): go nowhere.
+
+ - Firefox seems to ignore traverals requests while a traversal is ongoing:
+
+ - back(), back(): go back 1.
+ - back(), forward(): go back 1.
+
+ We assert the Firefox behavior here for now.
+-->
+
+<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");
+
+ 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 opposite directions: the second is ignored");
+
+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");
+
+ 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 opposite directions, second traversal invalid at queuing time: the second is ignored");
+
+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, "?2", "first load event must be going back");
+
+ 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 the same (back) direction: the second is ignored");
+
+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, "?2", "first load event must be going forward");
+
+ 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 the same (forward) direction: the second is ignored");
+</script>
diff --git a/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-traversal-same-document-nav.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-traversal-same-document-nav.html
new file mode 100644
index 0000000..5d3850f
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-traversal-same-document-nav.html
@@ -0,0 +1,69 @@
+<!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 currently says that same-document navigations must stop traverals,
+ but this does not match browsers: https://github.com/whatwg/html/issues/6773.
+ Browsers also disagree on how far back this should take us. This test assumes
+ a behavior similar to Firefox's, although Firefox is inconsistent and only
+ applies this behavior for fragments, not for pushState().
+-->
+
+<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 ?2
+ await t.step_wait(() => iframe.contentWindow.location.search === "?2" && iframe.contentWindow.location.hash === "");
+}, "same-document traversals are not canceled by 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 ?2
+ await t.step_wait(() => iframe.contentWindow.location.search === "?2");
+}, "same-document traversals are not canceled by pushState()");
+</script>
diff --git a/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-traversal-same-document-traversal.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-traversal-same-document-traversal.html
new file mode 100644
index 0000000..0a9154c
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-traversal-same-document-traversal.html
@@ -0,0 +1,108 @@
+<!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>
+
+<!--
+ In the spec, all traversals are queued. However, what "back" and "forward"
+ mean is computed synchronously. So per spec:
+
+ - back(), back(): go back 1.
+
+ This is not how browsers behave:
+
+ - Chrome seems to coalesce all traversals and apply them at once, i.e.:
+
+ - back(), back(): go back 2 at once.
+
+ - Firefox seems to ignore traverals requests while a traversal is ongoing:
+
+ - back(), back(): go back 1.
+
+ We assert the Firefox behavior here for now.
+-->
+
+<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, "#2", "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#2 (search)");
+ assert_equals(iframe.contentWindow.location.hash, "#2", "must stay on ?1#2 (hash)");
+}, "traversals in the same (back) direction: the second is ignored");
+
+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, "", "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, "", "must stay on ?2");
+}, "traversals in the same (forward) direction: the second is ignored");
+</script>
diff --git a/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-traversal-stop.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-traversal-stop.html
new file mode 100644
index 0000000..49d97e7
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/cross-document-traversal-stop.html
@@ -0,0 +1,40 @@
+<!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 currently says that stop() must stop traverals, but this does not
+ match browsers: https://github.com/whatwg/html/issues/6905. This test assumes
+ browser behavior.
+-->
+
+<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/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/resources/helpers.mjs b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/resources/helpers.mjs
new file mode 100644
index 0000000..d2ecb9d
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/resources/helpers.mjs
@@ -0,0 +1,45 @@
+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, so by waiting for 2x the duration
+// of a separate iframe load, we would have caught any problems.
+export async function waitForPotentialNetworkLoads(t) {
+ const before = performance.now();
+ await createIframe(t);
+ const after = performance.now();
+ await delay(t, after - before);
+}
diff --git a/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/resources/slow.py b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/resources/slow.py
new file mode 100644
index 0000000..5ee32a6
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/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/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-nav-cross-document-nav.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-nav-cross-document-nav.html
new file mode 100644
index 0000000..8082e9b
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/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/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-nav-cross-document-traversal.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-nav-cross-document-traversal.html
new file mode 100644
index 0000000..fc6f92e
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/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/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-nav-same-document-nav.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-nav-same-document-nav.html
new file mode 100644
index 0000000..2d8961d
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/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/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-nav-same-document-traversal.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-nav-same-document-traversal.html
new file mode 100644
index 0000000..a112143
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/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/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-nav-stop.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-nav-stop.html
new file mode 100644
index 0000000..a903620
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/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/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-traversal-cross-document-nav.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-traversal-cross-document-nav.html
new file mode 100644
index 0000000..3978a2c
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-traversal-cross-document-nav.html
@@ -0,0 +1,44 @@
+<!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 currently says that cross-document navigations cancel traversals
+ only once they mature. So the traversal and navigation both go through.
+ We use slow.py to ensure the traversal finishes first (although it'd be pretty
+ likely even with /common/blank.html, since same-document traverals should be
+ fast).
+-->
+
+<body>
+<script type="module">
+import { createIframe, delay } from "./resources/helpers.mjs";
+
+promise_test(async t => {
+ const iframe = await createIframe(t);
+ const slowURL = (new URL("resources/slow.py", location.href)).href;
+
+ // 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.href = slowURL;
+ 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 #2
+ await t.step_wait(() => iframe.contentWindow.location.hash === "#2", "traversal");
+
+ // And then slow.py
+ await t.step_wait(() => iframe.contentWindow.location.href === slowURL, "navigation");
+}, "same-document traversals are not canceled by cross-document navigations");
+</script>
diff --git a/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-traversal-cross-document-traversal.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-traversal-cross-document-traversal.html
new file mode 100644
index 0000000..5dfd3e1
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-traversal-cross-document-traversal.html
@@ -0,0 +1,98 @@
+<!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>
+
+<!--
+ In the spec, all traversals are queued. However, what "back" and "forward"
+ mean is computed synchronously. So per spec:
+
+ - back(), back(): go back 1.
+ - back(), forward(): go forward 1 (if you're not starting from the end).
+
+ This is not how browsers behave. Furthermore, this case is apparently unlike
+ other multi-traversal test cases: both Chrome and Firefox queue up these
+ traverals, firing events for each of them. I.e.
+
+ - back(), back(): go back 1, go back 1.
+
+ We assert this shared browser behavior here for now.
+-->
+
+<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: the second is queued up");
+</script>
diff --git a/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-traversal-same-document-nav.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-traversal-same-document-nav.html
new file mode 100644
index 0000000..7142cbb
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-traversal-same-document-nav.html
@@ -0,0 +1,59 @@
+<!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>
+
+<!--
+ The spec currently says that same-document navigations must stop traverals,
+ but this does not match browsers: https://github.com/whatwg/html/issues/6773.
+ There is a separate interop issue about how the traversal vs. the
+ same-document navigation get ordered. This test assumes a behavior similar to
+ Firefox's, although Firefox is inconsistent and only applies this behavior
+ for fragments, not for pushState().
+-->
+
+<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 #2
+ await t.step_wait(() => iframe.contentWindow.location.hash === "#2");
+}, "same-document traversals are not canceled by fragment navigations");
+
+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 /2
+ await t.step_wait(() => iframe.contentWindow.location.pathname === "/2");
+}, "same-document traversals are not canceled by pushState()");
+</script>
diff --git a/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-traversal-same-document-traversal-hashchange.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-traversal-same-document-traversal-hashchange.html
new file mode 100644
index 0000000..43aefa0
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-traversal-same-document-traversal-hashchange.html
@@ -0,0 +1,164 @@
+<!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>
+
+<!--
+ In the spec, all traversals are queued. However, what "back" and "forward"
+ mean is computed synchronously. So per spec:
+
+ - back(), back(): go back 1.
+ - back(), forward(): go forward 1 (if you're not starting from the end).
+
+ (The spec doesn't care about same or cross-document.)
+
+ This is not how browsers behave:
+
+ - Chrome seems to ignore same-document traversal requests if one is already in flight:
+
+ - back(), back(): go back 1.
+ - back(), forward(): go back 1 (regardless of starting at the end or not).
+
+ - Firefox seems to queue up "back" or "forward" commands:
+
+ - back(), back(): go back 1, then go back 1.
+ - back(), forward(): go back 1, then go forward 1 (regardless of starting at the end or not).
+
+ We assert the Firefox behavior here for now.
+-->
+
+<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/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-traversal-same-document-traversal-pushstate.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-traversal-same-document-traversal-pushstate.html
new file mode 100644
index 0000000..0261733
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-traversal-same-document-traversal-pushstate.html
@@ -0,0 +1,153 @@
+<!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>
+
+<!--
+ In the spec, all traversals are queued. However, what "back" and "forward"
+ mean is computed synchronously. So per spec:
+
+ - back(), back(): go back 1.
+ - back(), forward(): go forward 1 (if you're not starting from the end).
+
+ (The spec doesn't care about same or cross-document.)
+
+ This is not how browsers behave:
+
+ - Chrome seems to ignore same-document traversal requests if one is already in flight:
+
+ - back(), back(): go back 1.
+ - back(), forward(): go back 1 (regardless of starting at the end or not).
+
+ - Firefox seems to queue up "back" or "forward" commands:
+
+ - back(), back(): go back 1, then go back 1.
+ - back(), forward(): go back 1, then go forward 1 (regardless of starting at the end or not).
+
+ We assert the Firefox behavior here for now.
+-->
+
+<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/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-traversal-stop.html b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-traversal-stop.html
new file mode 100644
index 0000000..ffa2f14
--- /dev/null
+++ b/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/tentative/same-document-traversal-stop.html
@@ -0,0 +1,35 @@
+<!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 currently says that stop() must stop traverals, but this does not
+ match browsers: https://github.com/whatwg/html/issues/6905. This test assumes
+ browser behavior.
+-->
+
+<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>