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