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>