summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache')
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/README.md60
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/broadcast-channel.html23
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/dedicated-worker.html25
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-1.html18
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-2.html22
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-cors.html18
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-redirects.html34
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/shared-worker.html25
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/events.html44
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/focus.html73
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/pushstate.https.html63
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/echo-worker.js16
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/event-recorder.js54
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor-pushstate.html13
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor.html5
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor.js64
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js229
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/inflight-fetch-helper.js47
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js137
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/service-worker.js58
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/slow.py13
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/worker-helper.js28
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-client-postmessage.https.html71
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-claim.https.html71
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-matchall.https.html76
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-controlled-after-restore.https.html54
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-unregister.https.html67
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/storage-events.html101
-rw-r--r--testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/timers.html68
29 files changed, 1577 insertions, 0 deletions
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/README.md b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/README.md
new file mode 100644
index 0000000000..afda8f6c8a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/README.md
@@ -0,0 +1,60 @@
+# How to write back-forward cache tests
+
+In the back-forward cache tests, the main test HTML usually:
+
+1. Opens new executor Windows using `window.open()` + `noopener` option,
+ because less isolated Windows (e.g. iframes and `window.open()` without
+ `noopener` option) are often not eligible for back-forward cache (e.g.
+ in Chromium).
+2. Injects scripts to the executor Windows and receives the results via
+ `RemoteContext.execute_script()` by
+ [/common/dispatcher](../../../../common/dispatcher/README.md).
+ Follow the semantics and guideline described there.
+
+Back-forward cache specific helpers are in:
+
+- [resources/executor.html](resources/executor.html):
+ The BFCache-specific executor and contains helpers for executors.
+- [resources/helper.sub.js](resources/helper.sub.js):
+ Helpers for main test HTMLs.
+
+We must ensure that injected scripts are evaluated only after page load
+(more precisely, the first `pageshow` event) and not during navigation,
+to prevent unexpected interference between injected scripts, in-flight fetch
+requests behind `RemoteContext.execute_script()`, navigation and back-forward
+cache. To ensure this,
+
+- Call `await remoteContext.execute_script(waitForPageShow)` before any
+ other scripts are injected to the remote context, and
+- Call `prepareNavigation(callback)` synchronously from the script injected
+ by `RemoteContext.execute_script()`, and trigger navigation on or after the
+ callback is called.
+
+In typical A-B-A scenarios (where we navigate from Page A to Page B and then
+navigate back to Page A, assuming Page A is (or isn't) in BFCache),
+
+- Call `prepareNavigation()` on the executor, and then navigate to B, and then
+ navigate back to Page A.
+- Call `assert_bfcached()` or `assert_not_bfcached()` on the main test HTML, to
+ check the BFCache status. This is important to do to ensure the test would
+ not fail normally and instead result in `PRECONDITION_FAILED` if the page is
+ unexpectedly bfcached/not bfcached.
+- Check other test expectations on the main test HTML,
+
+as in [events.html](./events.html) and `runEventTest()` in
+[resources/helper.sub.js](resources/helper.sub.js).
+
+# Asserting PRECONDITION_FAILED for unexpected BFCache eligibility
+
+Browsers are not actually obliged to put pages in BFCache after navigations, so
+BFCache WPTs shouldn't result in `FAILED` if it expects a certain case to be
+supported by BFCache. But, it is still useful to test those cases in the
+browsers that do support BFCache for that case.
+
+To distinguish genuine failures from just not using BFCache, we use
+`assert_bfcached()` and `assert_not_bfcached()` which result in
+`PRECONDITION_FAILED` rather than `FAILED`. that should be put in the
+expectations for the failing tests (instead of marking it as `FAILED` or
+skipping the test). This means if the test starts passing (e.g. if we start
+BFCaching in the case being tested), we will notice that the output changed from
+`PRECONDITION_FAILED` to `PASS`.
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/broadcast-channel.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/broadcast-channel.html
new file mode 100644
index 0000000000..bc04a5ed7f
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/broadcast-channel.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="../resources/helper.sub.js"></script>
+<script>
+// Check whether the page is BFCached when there are open BroadcastChannels.
+// See https://github.com/whatwg/html/issues/7219 for other related scenarios.
+runEventTest(
+ {funcBeforeNavigation: () => {
+ window.bc = new BroadcastChannel('foo');
+ }},
+ 'Eligibility (BroadcastChannel)');
+
+// Same as above, but the BroadcastChannels are closed in the pagehide event.
+runEventTest(
+ {funcBeforeNavigation: () => {
+ window.bc = new BroadcastChannel('foo');
+ window.addEventListener('pagehide', () => window.bc.close());
+ }},
+ 'Eligibility (BroadcastChannel closed in the pagehide event)');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/dedicated-worker.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/dedicated-worker.html
new file mode 100644
index 0000000000..b08588a8bd
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/dedicated-worker.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="../resources/helper.sub.js"></script>
+<script>
+// Check whether the page is BFCached when there are dedicated workers that are
+// already loaded.
+runBfcacheTest({
+ funcBeforeNavigation: async () => {
+ globalThis.worker = new Worker('../resources/echo-worker.js');
+ // Make sure the worker starts before navigation.
+ await WorkerHelper.pingWorker(globalThis.worker);
+ },
+ funcAfterAssertion: async (pageA) => {
+ // Confirm that the worker is still there.
+ assert_equals(
+ await pageA.execute_script(() => WorkerHelper.pingWorker(globalThis.worker)),
+ 'PASS',
+ 'Worker should still work after restored from BFCache');
+ }
+}, 'Eligibility: dedicated workers');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-1.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-1.html
new file mode 100644
index 0000000000..6a48d0657b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-1.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="../resources/helper.sub.js"></script>
+<script src="../resources/inflight-fetch-helper.js"></script>
+<script>
+// Check whether the page is BFCached when there are in-flight network requests
+// at the time of navigation.
+
+// Successful fetch completion with header received before BFCached.
+runTest(sameOriginUrl + '?delayBeforeBody=2000', false, true,
+ 'Header received before BFCache and body received when in BFCache');
+runTest(sameOriginUrl + '?delayBeforeBody=3500', false, true,
+ 'Header received before BFCache and body received after BFCache');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-2.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-2.html
new file mode 100644
index 0000000000..a767c4fa83
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-2.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="../resources/helper.sub.js"></script>
+<script src="../resources/inflight-fetch-helper.js"></script>
+<script>
+// Check whether the page is BFCached when there are in-flight network requests
+// at the time of navigation.
+
+// Successful fetch completion with header received when in BFCache or after
+// BFCache.
+runTest(sameOriginUrl + '?delayBeforeHeader=2000', false, true,
+ 'Header and body received when in BFCache');
+runTest(sameOriginUrl + '?delayBeforeHeader=2000&delayBeforeBody=1500',
+ false, true,
+ 'Header received when in BFCache and body received after BFCache');
+runTest(sameOriginUrl + '?delayBeforeHeader=3500', false, true,
+ 'Header and body received after BFCache');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-cors.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-cors.html
new file mode 100644
index 0000000000..c04089a5e2
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-cors.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="../resources/helper.sub.js"></script>
+<script src="../resources/inflight-fetch-helper.js"></script>
+<script>
+// Check whether the page is BFCached when there are in-flight network requests
+// at the time of navigation.
+
+// CORS and failing fetch.
+runTest(crossSiteUrl + '?delayBeforeHeader=2000&cors=yes', false, true,
+ 'CORS succeeded when in BFCache');
+runTest(crossSiteUrl + '?delayBeforeHeader=2000', false, false,
+ 'CORS failed when in BFCache');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-redirects.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-redirects.html
new file mode 100644
index 0000000000..b0b49d5f12
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-redirects.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="../resources/helper.sub.js"></script>
+<script src="../resources/inflight-fetch-helper.js"></script>
+<script>
+// Check whether the page is BFCached when there are in-flight network requests
+// at the time of navigation.
+
+// Redirects and CSP.
+runTest(
+ '/common/slow-redirect.py?delay=2&location=' +
+ encodeURIComponent(sameOriginUrl),
+ false, true,
+ 'Redirect header received when in BFCache');
+runTest(
+ '/common/slow-redirect.py?delay=2&location=' +
+ encodeURIComponent(sameOriginUrl),
+ true, true,
+ 'Redirect header received when in BFCache w/ CSP passing');
+runTest(
+ '/common/slow-redirect.py?delay=2&location=' +
+ encodeURIComponent(crossSiteUrl + '?cors=yes'),
+ false, true,
+ 'Cross-origin redirect header received when in BFCache');
+runTest(
+ '/common/slow-redirect.py?delay=2&location=' +
+ encodeURIComponent(crossSiteUrl + '?cors=yes'),
+ true, false,
+ 'Cross-origin redirect header received when in BFCache w/ CSP failing');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/shared-worker.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/shared-worker.html
new file mode 100644
index 0000000000..77139fd08a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/eligibility/shared-worker.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="../resources/helper.sub.js"></script>
+<script>
+// Check whether the page is BFCached when there are shared workers that are
+// already loaded.
+runBfcacheTest({
+ funcBeforeNavigation: async () => {
+ globalThis.worker = new SharedWorker('../resources/echo-worker.js');
+ // Make sure the worker starts before navigation.
+ await WorkerHelper.pingWorker(globalThis.worker);
+ },
+ funcAfterAssertion: async (pageA) => {
+ // Confirm that the worker is still there.
+ assert_equals(
+ await pageA.execute_script(() => WorkerHelper.pingWorker(globalThis.worker)),
+ 'PASS',
+ 'SharedWorker should still work after restored from BFCache');
+ }
+}, 'Eligibility: shared workers');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/events.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/events.html
new file mode 100644
index 0000000000..4b1d3e408e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/events.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/helper.sub.js"></script>
+<script>
+// Basic event tests.
+runEventTest(
+ {targetOrigin: originSameOrigin},
+ 'SameOrigin');
+
+runEventTest(
+ {targetOrigin: originSameSite},
+ 'SameSite');
+
+runEventTest(
+ {},
+ 'CrossSite');
+
+// beforeunload.
+runEventTest({
+ events: ['pagehide', 'pageshow', 'load', 'beforeunload'],
+ expectedEvents: [
+ 'window.load',
+ 'window.pageshow',
+ 'window.beforeunload',
+ 'window.pagehide.persisted',
+ 'window.pageshow.persisted'
+ ]},
+ 'beforeunload');
+
+// unload.
+runEventTest({
+ events: ['pagehide', 'pageshow', 'load', 'unload'],
+ expectedEvents: [
+ 'window.load',
+ 'window.pageshow',
+ 'window.pagehide.persisted',
+ 'window.pageshow.persisted'
+ ]},
+ 'unload');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/focus.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/focus.html
new file mode 100644
index 0000000000..3901a5417d
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/focus.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<meta name="timeout" content="long">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#focused-area-of-the-document">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/helper.sub.js"></script>
+<script>
+// Focus should remain the same and thus blur/focus events shouldn't be fired
+// when page gets into and out of BFCache, as explicitly noted in the spec:
+// https://html.spec.whatwg.org/multipage/interaction.html#focused-area-of-the-document
+// "Even if a document is not fully active and not shown to the user, it can still
+// have a focused area of the document. If a document's fully active state changes,
+// its focused area of the document will stay the same."
+runBfcacheTest({
+ openFunc: (url) => window.open(url + '&events=pagehide,pageshow,load',
+ '_blank', 'noopener'),
+ funcBeforeNavigation: () => {
+ // Create and focus on an <input> before navigation.
+ // Focus/blur events on the <input> are recorded.
+ const textInput = document.createElement('input');
+ textInput.setAttribute('type', 'text');
+ textInput.setAttribute('id', 'toBeFocused');
+ textInput.onfocus = () => {
+ recordEvent('input.focus');
+ };
+ textInput.onblur = () => {
+ recordEvent('input.blur');
+ };
+ document.body.appendChild(textInput);
+ textInput.focus();
+ window.activeElementBeforePageHide = document.activeElement;
+ window.addEventListener('pagehide', () => {
+ window.activeElementOnPageHide = document.activeElement;
+ });
+ },
+ funcAfterAssertion: async (pageA) => {
+ assert_true(
+ await pageA.execute_script(() => {
+ return window.activeElementBeforePageHide ===
+ document.querySelector('#toBeFocused');
+ }),
+ 'activeElement before pagehide');
+
+ assert_true(
+ await pageA.execute_script(() => {
+ return window.activeElementOnPageHide ===
+ document.querySelector('#toBeFocused');
+ }),
+ 'activeElement on pagehide');
+
+ assert_true(
+ await pageA.execute_script(() => {
+ return document.activeElement ===
+ document.querySelector('#toBeFocused');
+ }),
+ 'activeElement after navigation');
+
+ assert_array_equals(
+ await pageA.execute_script(() => getRecordedEvents()),
+ [
+ 'window.load',
+ 'window.pageshow',
+ 'input.focus',
+ 'window.pagehide.persisted',
+ 'window.pageshow.persisted'
+ ],
+ 'blur/focus events should not be fired ' +
+ 'when page gets into and out of BFCache');
+ }
+}, 'Focus should be kept when page gets into and out of BFCache');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/pushstate.https.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/pushstate.https.html
new file mode 100644
index 0000000000..218562254a
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/pushstate.https.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/helper.sub.js"></script>
+<script>
+// Tests what happens when doing history navigation to an entry that's created
+// via pushState, with and without BFCache:
+// 1. Navigate to `urlA`.
+// 2. `pushState(urlPushState)`.
+// 3. Navigate to `urlB`.
+// 4. Do a back navigation.
+// With BFCache, the page loaded at Step 1 is restored from BFCache,
+// and is not reloaded from `urlPushState` nor `urlA`.
+// Without BFCache, a page is loaded from `urlPushState`, not from `urlA`.
+// In both cases, `location` and `history.state` are set to those set by
+// `pushState()` in Step 2.
+// See https://github.com/whatwg/html/issues/6207 for more discussion on the
+// specified, implemented and desired behaviors. While this test contradicts
+// the current spec but matches the desired behavior, and the spec will be
+// fixed as part of https://github.com/whatwg/html/pull/6315.
+for (const bfcacheDisabled of [false, true]) {
+ const pushStateExecutorPath =
+ '/html/browsers/browsing-the-web/back-forward-cache/resources/executor-pushstate.html';
+
+ runBfcacheTest({
+ funcBeforeNavigation: async (bfcacheDisabled, pushStateExecutorPath) => {
+ const urlPushState = new URL(location.href);
+ urlPushState.pathname = pushStateExecutorPath;
+ if (bfcacheDisabled) {
+ await disableBFCache();
+ }
+
+ // `pushState(..., urlPushState)` on `urlA`,
+ history.pushState('blue', '', urlPushState.href);
+ },
+ argsBeforeNavigation: [bfcacheDisabled, pushStateExecutorPath],
+ shouldBeCached: !bfcacheDisabled,
+ funcAfterAssertion: async (pageA) => {
+ // We've navigated to `urlB` and back again
+ // (already done within `runBfcacheTest()`).
+ // After the back navigation, `location` etc. should point to
+ // `urlPushState` and the state that's pushed.
+ const urlPushState = location.origin + pushStateExecutorPath +
+ '?uuid=' + pageA.context_id;
+ assert_equals(await pageA.execute_script(() => location.href),
+ urlPushState, 'url');
+ assert_equals(await pageA.execute_script(() => history.state),
+ 'blue', 'history.state');
+
+ if (bfcacheDisabled) {
+ // When the page is not restored from BFCache, the HTML page is loaded
+ // from `urlPushState` (not from `urlA`).
+ assert_true(await pageA.execute_script(() => isLoadedFromPushState),
+ 'document should be loaded from urlPushState');
+ }
+ }
+ }, 'back navigation to pushState()d page (' +
+ (bfcacheDisabled ? 'not ' : '') + 'in BFCache)');
+}
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/echo-worker.js b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/echo-worker.js
new file mode 100644
index 0000000000..3e3ecb52e9
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/echo-worker.js
@@ -0,0 +1,16 @@
+// On receiving a message from the parent Document, send back a message to the
+// parent Document. This is used to wait for worker initialization and test
+// that this worker is alive and working.
+
+// For dedicated workers.
+self.addEventListener('message', event => {
+ postMessage(event.data);
+});
+
+// For shared workers.
+onconnect = e => {
+ const port = e.ports[0];
+ port.onmessage = event => {
+ port.postMessage(event.data);
+ }
+};
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/event-recorder.js b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/event-recorder.js
new file mode 100644
index 0000000000..469286a399
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/event-recorder.js
@@ -0,0 +1,54 @@
+// Recording events
+
+const params = new URLSearchParams(window.location.search);
+const uuid = params.get('uuid');
+
+// The recorded events are stored in localStorage rather than global variables
+// to catch events fired just before navigating out.
+function getPushedItems(key) {
+ return JSON.parse(localStorage.getItem(key) || '[]');
+}
+
+function pushItem(key, value) {
+ const array = getPushedItems(key);
+ array.push(value);
+ localStorage.setItem(key, JSON.stringify(array));
+}
+
+window.recordEvent = function(eventName) {
+ pushItem(uuid + '.observedEvents', eventName);
+}
+
+window.getRecordedEvents = function() {
+ return getPushedItems(uuid + '.observedEvents');
+}
+
+// Records events fired on `window` and `document`, with names listed in
+// `eventNames`.
+function startRecordingEvents(eventNames) {
+ for (const eventName of eventNames) {
+ window.addEventListener(eventName, event => {
+ let result = eventName;
+ if (event.persisted) {
+ result += '.persisted';
+ }
+ if (eventName === 'visibilitychange') {
+ result += '.' + document.visibilityState;
+ }
+ recordEvent('window.' + result);
+ });
+ document.addEventListener(eventName, () => {
+ let result = eventName;
+ if (eventName === 'visibilitychange') {
+ result += '.' + document.visibilityState;
+ }
+ recordEvent('document.' + result);
+ });
+ }
+}
+
+// When a comma-separated list of event names are given as the `events`
+// parameter in the URL, start record the events of the given names.
+if (params.get('events')) {
+ startRecordingEvents(params.get('events').split(','));
+}
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor-pushstate.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor-pushstate.html
new file mode 100644
index 0000000000..dcf4a798d0
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor-pushstate.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="event-recorder.js" type="module"></script>
+<script src="worker-helper.js" type="module"></script>
+<script type="module">
+// This is mostly the same as `executor.html`, except for
+// `isLoadedFromPushState` is set here, in order to detect whether the page
+// was loaded from `executor.html` or `executor-pushstate.html`.
+// Full executor functionality is still needed to handle remote script
+// execution requests etc.
+window.isLoadedFromPushState = true;
+</script>
+<script src="executor.js" type="module"></script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor.html
new file mode 100644
index 0000000000..2d118bbe2b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor.html
@@ -0,0 +1,5 @@
+<!DOCTYPE HTML>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="event-recorder.js" type="module"></script>
+<script src="worker-helper.js" type="module"></script>
+<script src="executor.js" type="module"></script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor.js b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor.js
new file mode 100644
index 0000000000..67ce068130
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/executor.js
@@ -0,0 +1,64 @@
+const params = new URLSearchParams(window.location.search);
+const uuid = params.get('uuid');
+
+// Executor and BFCache detection
+
+// When navigating out from this page, always call
+// `prepareNavigation(callback)` synchronously from the script injected by
+// `RemoteContext.execute_script()`, and trigger navigation on or after the
+// callback is called.
+// prepareNavigation() suspends task polling and avoid in-flight fetch
+// requests during navigation that might evict the page from BFCache.
+//
+// When we navigate to the page again, task polling is resumed, either
+// - (BFCache cases) when the pageshow event listener added by
+// prepareNavigation() is executed, or
+// - (Non-BFCache cases) when `Executor.execute()` is called again during
+// non-BFCache page loading.
+//
+// In such scenarios, `assert_bfcached()` etc. in `helper.sub.js` can determine
+// whether the page is restored from BFCache or not, by observing
+// - `isPageshowFired`: whether the pageshow event listener added by the
+// prepareNavigation() before navigating out, and
+// - `loadCount`: whether this inline script is evaluated again.
+// - `isPageshowPersisted` is used to assert that `event.persisted` is true
+// when restored from BFCache.
+
+window.isPageshowFired = false;
+window.isPageshowPersisted = null;
+window.loadCount = parseInt(localStorage.getItem(uuid + '.loadCount') || '0') + 1;
+localStorage.setItem(uuid + '.loadCount', loadCount);
+
+window.pageShowPromise = new Promise(resolve =>
+ window.addEventListener('pageshow', resolve, {once: true}));
+
+const executor = new Executor(uuid);
+
+window.prepareNavigation = function(callback) {
+ window.addEventListener(
+ 'pageshow',
+ (event) => {
+ window.isPageshowFired = true;
+ window.isPageshowPersisted = event.persisted;
+ executor.resume();
+ },
+ {once: true});
+ executor.suspend(callback);
+}
+
+// Try to disable BFCache by acquiring and never releasing a Web Lock.
+// This requires HTTPS.
+// Note: This is a workaround depending on non-specified WebLock+BFCache
+// behavior, and doesn't work on Safari. We might want to introduce a
+// test-only BFCache-disabling API instead in the future.
+// https://github.com/web-platform-tests/wpt/issues/16359#issuecomment-795004780
+// https://crbug.com/1298336
+window.disableBFCache = () => {
+ return new Promise(resolve => {
+ // Use page's UUID as a unique lock name.
+ navigator.locks.request(uuid, () => {
+ resolve();
+ return new Promise(() => {});
+ });
+ });
+};
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js
new file mode 100644
index 0000000000..a1d18d108e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js
@@ -0,0 +1,229 @@
+// Helpers called on the main test HTMLs.
+// Functions in `RemoteContext.execute_script()`'s 1st argument are evaluated
+// on the executors (`executor.html`), and helpers available on the executors
+// are defined in `executor.html`.
+
+const originSameOrigin =
+ location.protocol === 'http:' ?
+ 'http://{{host}}:{{ports[http][0]}}' :
+ 'https://{{host}}:{{ports[https][0]}}';
+const originSameSite =
+ location.protocol === 'http:' ?
+ 'http://{{host}}:{{ports[http][1]}}' :
+ 'https://{{host}}:{{ports[https][1]}}';
+const originCrossSite =
+ location.protocol === 'http:' ?
+ 'http://{{hosts[alt][www]}}:{{ports[http][0]}}' :
+ 'https://{{hosts[alt][www]}}:{{ports[https][0]}}';
+
+const executorPath =
+ '/html/browsers/browsing-the-web/back-forward-cache/resources/executor.html?uuid=';
+
+// Asserts that the executor `target` is (or isn't, respectively)
+// restored from BFCache. These should be used in the following fashion:
+// 1. Call prepareNavigation() on the executor `target`.
+// 2. Navigate the executor to another page.
+// 3. Navigate back to the executor `target`.
+// 4. Call assert_bfcached() or assert_not_bfcached() on the main test HTML.
+async function assert_bfcached(target) {
+ const status = await getBFCachedStatus(target);
+ assert_implements_optional(status === 'BFCached',
+ "Could have been BFCached but actually wasn't");
+}
+
+async function assert_not_bfcached(target) {
+ const status = await getBFCachedStatus(target);
+ assert_implements(status !== 'BFCached',
+ 'Should not be BFCached but actually was');
+}
+
+async function getBFCachedStatus(target) {
+ const [loadCount, isPageshowFired, isPageshowPersisted] =
+ await target.execute_script(() => [
+ window.loadCount, window.isPageshowFired, window.isPageshowPersisted]);
+
+ if (loadCount === 1 && isPageshowFired === true &&
+ isPageshowPersisted === true) {
+ return 'BFCached';
+ } else if (loadCount === 2 && isPageshowFired === false) {
+ return 'Not BFCached';
+ } else {
+ // This can occur for example when this is called before first navigating
+ // away (loadCount = 1, isPageshowFired = false), e.g. when
+ // 1. sending a script for navigation and then
+ // 2. calling getBFCachedStatus() without waiting for the completion of
+ // the script on the `target` page.
+ assert_unreached(
+ `Got unexpected BFCache status: loadCount = ${loadCount}, ` +
+ `isPageshowFired = ${isPageshowFired}, ` +
+ `isPageshowPersisted = ${isPageshowPersisted}`);
+ }
+}
+
+// Always call `await remoteContext.execute_script(waitForPageShow);` after
+// triggering to navigation to the page, to wait for pageshow event on the
+// remote context.
+const waitForPageShow = () => window.pageShowPromise;
+
+// Run a test that navigates A->B->A:
+// 1. Page A is opened by `params.openFunc(url)`.
+// 2. `params.funcBeforeNavigation(params.argsBeforeNavigation)` is executed
+// on page A.
+// 3. The window is navigated to page B on `params.targetOrigin`.
+// 4. The window is back navigated to page A (expecting BFCached).
+//
+// Events `params.events` (an array of strings) are observed on page A and
+// `params.expectedEvents` (an array of strings) is expected to be recorded.
+// See `event-recorder.js` for event recording.
+//
+// Parameters can be omitted. See `defaultParams` below for default.
+function runEventTest(params, description) {
+ const defaultParams = {
+ openFunc(url) {
+ window.open(
+ `${url}&events=${this.events.join(',')}`,
+ '_blank',
+ 'noopener'
+ )
+ },
+ events: ['pagehide', 'pageshow', 'load'],
+ expectedEvents: [
+ 'window.load',
+ 'window.pageshow',
+ 'window.pagehide.persisted',
+ 'window.pageshow.persisted'
+ ],
+ async funcAfterAssertion(pageA) {
+ assert_array_equals(
+ await pageA.execute_script(() => getRecordedEvents()),
+ this.expectedEvents);
+ }
+ }
+ // Apply defaults.
+ params = { ...defaultParams, ...params };
+
+ runBfcacheTest(params, description);
+}
+
+async function navigateAndThenBack(pageA, pageB, urlB,
+ funcBeforeBackNavigation,
+ argsBeforeBackNavigation) {
+ await pageA.execute_script(
+ (url) => {
+ prepareNavigation(() => {
+ location.href = url;
+ });
+ },
+ [urlB]
+ );
+
+ await pageB.execute_script(waitForPageShow);
+ if (funcBeforeBackNavigation) {
+ await pageB.execute_script(funcBeforeBackNavigation,
+ argsBeforeBackNavigation);
+ }
+ await pageB.execute_script(
+ () => {
+ prepareNavigation(() => { history.back(); });
+ }
+ );
+
+ await pageA.execute_script(waitForPageShow);
+}
+
+function runBfcacheTest(params, description) {
+ const defaultParams = {
+ openFunc: url => window.open(url, '_blank', 'noopener'),
+ scripts: [],
+ funcBeforeNavigation: () => {},
+ argsBeforeNavigation: [],
+ targetOrigin: originCrossSite,
+ funcBeforeBackNavigation: () => {},
+ argsBeforeBackNavigation: [],
+ shouldBeCached: true,
+ funcAfterAssertion: () => {},
+ }
+ // Apply defaults.
+ params = {...defaultParams, ...params };
+
+ promise_test(async t => {
+ const pageA = new RemoteContext(token());
+ const pageB = new RemoteContext(token());
+
+ const urlA = executorPath + pageA.context_id;
+ const urlB = params.targetOrigin + executorPath + pageB.context_id;
+
+ // So that tests can refer to these URLs for assertions if necessary.
+ pageA.url = originSameOrigin + urlA;
+ pageB.url = urlB;
+
+ params.openFunc(urlA);
+
+ await pageA.execute_script(waitForPageShow);
+
+ for (const src of params.scripts) {
+ await pageA.execute_script((src) => {
+ const script = document.createElement("script");
+ script.src = src;
+ document.head.append(script);
+ return new Promise(resolve => script.onload = resolve);
+ }, [src]);
+ }
+
+ await pageA.execute_script(params.funcBeforeNavigation,
+ params.argsBeforeNavigation);
+ await navigateAndThenBack(pageA, pageB, urlB,
+ params.funcBeforeBackNavigation,
+ params.argsBeforeBackNavigation);
+
+ if (params.shouldBeCached) {
+ await assert_bfcached(pageA);
+ } else {
+ await assert_not_bfcached(pageA);
+ }
+
+ if (params.funcAfterAssertion) {
+ await params.funcAfterAssertion(pageA, pageB, t);
+ }
+ }, description);
+}
+
+// Call clients.claim() on the service worker
+async function claim(t, worker) {
+ const channel = new MessageChannel();
+ const saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS', 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+ worker.postMessage({type: "claim", port: channel.port2}, [channel.port2]);
+ await saw_message;
+}
+
+// Assigns the current client to a local variable on the service worker.
+async function storeClients(t, worker) {
+ const channel = new MessageChannel();
+ const saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS', 'storeClients');
+ resolve();
+ });
+ });
+ worker.postMessage({type: "storeClients", port: channel.port2}, [channel.port2]);
+ await saw_message;
+}
+
+// Call storedClients.postMessage("") on the service worker
+async function postMessageToStoredClients(t, worker) {
+ const channel = new MessageChannel();
+ const saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS', 'postMessageToStoredClients');
+ resolve();
+ });
+ });
+ worker.postMessage({type: "postMessageToStoredClients",
+ port: channel.port2}, [channel.port2]);
+ await saw_message;
+}
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/inflight-fetch-helper.js b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/inflight-fetch-helper.js
new file mode 100644
index 0000000000..7832003b76
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/inflight-fetch-helper.js
@@ -0,0 +1,47 @@
+// Delay after fetch start:
+// - 0.0 seconds: before BFCache
+// - 2.0 seconds: when in BFCache
+// - 3.5 seconds: after restored from BFCache
+function runTest(urlToFetch, hasCSP, shouldSucceed, description) {
+ runBfcacheTest({
+ funcBeforeNavigation: async (urlToFetch, hasCSP) => {
+ if (hasCSP) {
+ // Set CSP.
+ const meta = document.createElement('meta');
+ meta.setAttribute('http-equiv', 'Content-Security-Policy');
+ meta.setAttribute('content', "connect-src 'self'");
+ document.head.appendChild(meta);
+ }
+
+ // Initiate a `fetch()`.
+ window.fetchPromise = fetch(urlToFetch);
+
+ // Wait for 0.5 seconds to receive response headers for the fetch()
+ // before BFCache, if any.
+ await new Promise(resolve => setTimeout(resolve, 500));
+ },
+ argsBeforeNavigation: [urlToFetch, hasCSP],
+ funcBeforeBackNavigation: () => {
+ // Wait for 2 seconds before back navigating to pageA.
+ return new Promise(resolve => setTimeout(resolve, 2000));
+ },
+ funcAfterAssertion: async (pageA, pageB, t) => {
+ // Wait for fetch() completion and check the result.
+ const result = pageA.execute_script(
+ () => window.fetchPromise.then(r => r.text()));
+ if (shouldSucceed) {
+ assert_equals(
+ await result,
+ 'Body',
+ 'Fetch should complete successfully after restored from BFCache');
+ } else {
+ await promise_rejects_js(t, TypeError, result,
+ 'Fetch should fail after restored from BFCache');
+ }
+ }
+ }, 'Eligibility (in-flight fetch): ' + description);
+}
+
+const url = new URL('../resources/slow.py', location);
+const sameOriginUrl = url.href;
+const crossSiteUrl = originCrossSite + url.pathname;
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js
new file mode 100644
index 0000000000..80c164f560
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js
@@ -0,0 +1,137 @@
+// A collection of helper functions that make use of the `remoteContextHelper`
+// to test BFCache support and behavior.
+
+// Call `prepareForBFCache()` before navigating away from the page. This simply
+// sets a variable in window.
+async function prepareForBFCache(remoteContextHelper) {
+ await remoteContextHelper.executeScript(() => {
+ window.beforeBFCache = true;
+ });
+}
+
+// Call `getBeforeCache()` after navigating back to the page. This returns the
+// value in window.
+async function getBeforeBFCache(remoteContextHelper) {
+ return await remoteContextHelper.executeScript(() => {
+ return window.beforeBFCache;
+ });
+}
+
+// If the value in window is set to true, this means that the page was reloaded,
+// i.e., the page was restored from BFCache.
+// Call `prepareForBFCache()` before navigating away to call this function.
+async function assertImplementsBFCacheOptional(remoteContextHelper) {
+ var beforeBFCache = await getBeforeBFCache(remoteContextHelper);
+ assert_implements_optional(beforeBFCache == true, 'BFCache not supported.');
+}
+
+// Subtracts set `b` from set `a` and returns the result.
+function setMinus(a, b) {
+ const minus = new Set();
+ a.forEach(e => {
+ if (!b.has(e)) {
+ minus.add(e);
+ }
+ });
+ return minus;
+}
+
+// Return a sorted Array from the iterable `s`.
+function sorted(s) {
+ return Array.from(s).sort();
+}
+
+// Assert expected reasons and the reported reasons match.
+function matchReasons(expectedNotRestoredReasonsSet, notRestoredReasonsSet) {
+ const missing = setMinus(
+ expectedNotRestoredReasonsSet, notRestoredReasonsSet, 'Missing reasons');
+ const extra = setMinus(
+ notRestoredReasonsSet, expectedNotRestoredReasonsSet, 'Extra reasons');
+ assert_true(missing.size + extra.size == 0, `Expected: ${sorted(expectedNotRestoredReasonsSet)}\n` +
+ `Got: ${sorted(notRestoredReasonsSet)}\n` +
+ `Missing: ${sorted(missing)}\n` +
+ `Extra: ${sorted(extra)}\n`);
+}
+
+// This function takes a set of reasons and extracts reasons out of it and returns a set of strings.
+// For example, if the input is [{"reason": "error-document"}, {"reason": "masked"}],
+// the output is ["error-document", "masked"].
+function extractReason(reasonSet) {
+ let reasonsExtracted = new Set();
+ for (let reason of reasonSet) {
+ reasonsExtracted.add(reason.reason);
+ }
+ return reasonsExtracted;
+}
+
+// A helper function to assert that the page is not restored from BFCache by
+// checking whether the `beforeBFCache` value from `window` is undefined
+// due to page reload.
+// This function also takes an optional `notRestoredReasons` list which
+// indicates the set of expected reasons that make the page not restored.
+// If the reasons list is undefined, the check will be skipped. Otherwise
+// this check will use the `notRestoredReasons` API, to obtain the reasons
+// in a tree structure, and flatten the reasons before making the order-
+// insensitive comparison.
+// If the API is not available, the function will terminate instead of marking
+// the assertion failed.
+// Call `prepareForBFCache()` before navigating away to call this function.
+async function assertNotRestoredFromBFCache(
+ remoteContextHelper, notRestoredReasons) {
+ var beforeBFCache = await getBeforeBFCache(remoteContextHelper);
+ assert_equals(beforeBFCache, undefined, 'document unexpectedly BFCached');
+
+ // The reason is optional, so skip the remaining test if the
+ // `notRestoredReasons` is not set.
+ if (notRestoredReasons === undefined) {
+ return;
+ }
+
+ let isFeatureEnabled = await remoteContextHelper.executeScript(() => {
+ return 'notRestoredReasons' in
+ performance.getEntriesByType('navigation')[0];
+ });
+
+ // Return if the `notRestoredReasons` API is not available.
+ if (!isFeatureEnabled) {
+ return;
+ }
+
+ let result = await remoteContextHelper.executeScript(() => {
+ return performance.getEntriesByType('navigation')[0].notRestoredReasons;
+ });
+
+ let expectedNotRestoredReasonsSet = new Set(notRestoredReasons);
+ let notRestoredReasonsSet = new Set();
+
+ // Flatten the reasons from the main frame and all the child frames.
+ const collectReason = (node) => {
+ for (let reason of node.reasons) {
+ notRestoredReasonsSet.add(reason.reason);
+ }
+ for (let child of node.children) {
+ collectReason(child);
+ }
+ };
+ collectReason(result);
+ matchReasons(expectedNotRestoredReasonsSet, notRestoredReasonsSet);
+}
+
+// A helper function that combines the steps of setting window property,
+// navigating away and back, and making assertion on whether BFCache is
+// supported.
+// This function can be used to check if the current page is eligible for
+// BFCache.
+async function assertBFCacheEligibility(
+ remoteContextHelper, shouldRestoreFromBFCache) {
+ await prepareForBFCache(remoteContextHelper);
+ // Navigate away and back.
+ const newRemoteContextHelper = await remoteContextHelper.navigateToNew();
+ await newRemoteContextHelper.historyBack();
+
+ if (shouldRestoreFromBFCache) {
+ await assertImplementsBFCacheOptional(remoteContextHelper);
+ } else {
+ await assertNotRestoredFromBFCache(remoteContextHelper);
+ }
+}
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/service-worker.js b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/service-worker.js
new file mode 100644
index 0000000000..ab9a3239ea
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/service-worker.js
@@ -0,0 +1,58 @@
+self.addEventListener('message', function(event) {
+ if (event.data.type == "claim") {
+ self.clients.claim()
+ .then(function(result) {
+ if (result !== undefined) {
+ event.data.port.postMessage(
+ 'FAIL: claim() should be resolved with undefined');
+ return;
+ }
+ event.data.port.postMessage('PASS');
+ })
+ .catch(function(error) {
+ event.data.port.postMessage('FAIL: exception: ' + error.name);
+ });
+ } else if (event.data.type == "storeClients") {
+ self.clients.matchAll()
+ .then(function(result) {
+ self.storedClients = result;
+ event.data.port.postMessage("PASS");
+ });
+ } else if (event.data.type == "postMessageToStoredClients") {
+ for (let client of self.storedClients) {
+ client.postMessage("dummyValue");
+ }
+ event.data.port.postMessage("PASS");
+ } else if (event.data.type == 'storeMessagePort') {
+ let isCloseEventFired = false;
+ const port = event.ports[0];
+ port.start();
+ port.onmessage = (event) => {
+ if (event.data == 'Confirm the ports can communicate') {
+ port.postMessage('Receive message');
+ } else if (event.data == 'Ask if the close event was fired') {
+ port.postMessage(isCloseEventFired);
+ }
+ };
+ port.onclose = () => {
+ isCloseEventFired = true;
+ };
+ }
+ });
+
+self.addEventListener('fetch', e => {
+ if (e.request.url.match(/\/is-controlled/)) {
+ e.respondWith(new Response('controlled'));
+ }
+ else if (e.request.url.match(/\/get-clients-matchall/)) {
+ const options = { includeUncontrolled: true, type: 'all' };
+ e.respondWith(
+ self.clients.matchAll(options)
+ .then(clients => {
+ const client_urls = [];
+ clients.forEach(client => client_urls.push(client.url));
+ return new Response(JSON.stringify(client_urls));
+ })
+ );
+ }
+ });
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/slow.py b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/slow.py
new file mode 100644
index 0000000000..01bb3309b1
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/slow.py
@@ -0,0 +1,13 @@
+import time
+
+def main(request, response):
+ delay_before_header = float(request.GET.first(b"delayBeforeHeader", 0)) / 1000
+ delay_before_body = float(request.GET.first(b"delayBeforeBody", 0)) / 1000
+
+ time.sleep(delay_before_header)
+ if b"cors" in request.GET:
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+ response.write_status_headers()
+
+ time.sleep(delay_before_body)
+ response.writer.write_content(b"Body")
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/worker-helper.js b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/worker-helper.js
new file mode 100644
index 0000000000..d5f3a0c814
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources/worker-helper.js
@@ -0,0 +1,28 @@
+// Worker-related helper file to be used from executor.html.
+
+// The class `WorkerHelper` is exposed to `globalThis` because this should be
+// used via `eval()`.
+globalThis.WorkerHelper = class {
+ static pingWorker(worker) {
+ return new Promise((resolve, reject) => {
+ const message = 'message ' + Math.random();
+ const onmessage = e => {
+ if (e.data === message) {
+ resolve('PASS');
+ } else {
+ reject('pingWorker: expected ' + message + ' but got ' + e.data);
+ }
+ };
+ worker.onerror = reject;
+ if (worker instanceof Worker) {
+ worker.addEventListener('message', onmessage, {once: true});
+ worker.postMessage(message);
+ } else if (worker instanceof SharedWorker) {
+ worker.port.onmessage = onmessage;
+ worker.port.postMessage(message);
+ } else {
+ reject('Unexpected worker type');
+ }
+ });
+ }
+};
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-client-postmessage.https.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-client-postmessage.https.html
new file mode 100644
index 0000000000..acc682a073
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-client-postmessage.https.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/helper.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+// When a service worker is unregistered when a controlled page is in BFCache,
+// the page can be still restored from BFCache and remain controlled by the
+// service worker.
+promise_test(async t => {
+ // Register a service worker and make this page controlled.
+ const workerUrl =
+ 'resources/service-worker.js?pipe=header(Service-Worker-Allowed,../)';
+ const registration =
+ await service_worker_unregister_and_register(t, workerUrl, './');
+ t.add_cleanup(_ => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const controllerChanged = new Promise(
+ resolve => navigator.serviceWorker.oncontrollerchange = resolve);
+ await claim(t, registration.active);
+ await controllerChanged;
+
+ const pageA = new RemoteContext(token());
+ const pageB = new RemoteContext(token());
+
+ const urlA = location.origin + executorPath + pageA.context_id;
+ const urlB = originCrossSite + executorPath + pageB.context_id;
+
+ // Open `urlA`.
+ window.open(urlA, '_blank', 'noopener');
+ await pageA.execute_script(waitForPageShow);
+
+ assert_true(
+ await pageA.execute_script(
+ () => (navigator.serviceWorker.controller !== null)),
+ 'pageA should be controlled before navigation');
+
+ await storeClients(t, registration.active);
+
+ // Navigate to `urlB`.
+ await pageA.execute_script(
+ (url) => prepareNavigation(() => {
+ location.href = url;
+ }),
+ [urlB]);
+ await pageB.execute_script(waitForPageShow);
+
+ // Posting a message to a client should evict it from the bfcache.
+ await postMessageToStoredClients(t, registration.active);
+
+ // Back navigate and check whether the page is restored from BFCache.
+ await pageB.execute_script(
+ () => {
+ prepareNavigation(() => { history.back(); });
+ }
+ );
+ await pageA.execute_script(waitForPageShow);
+ await assert_not_bfcached(pageA);
+
+ await pageA.execute_script(() => navigator.serviceWorker.ready);
+
+ assert_true(
+ await pageA.execute_script(
+ () => (navigator.serviceWorker.controller !== null)),
+ 'pageA should be controlled after history navigation');
+
+}, 'Client.postMessage while a controlled page is in BFCache');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-claim.https.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-claim.https.html
new file mode 100644
index 0000000000..d9540c221b
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-claim.https.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/helper.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+// Calling Clients.claim() on the service worker when a controlled page is in
+// BFCache should evict the page from BFCache, as per
+// https://github.com/w3c/ServiceWorker/issues/1038#issuecomment-291028845.
+promise_test(async t => {
+ const pageA = new RemoteContext(token());
+ const pageB = new RemoteContext(token());
+
+ const urlA = location.origin + executorPath + pageA.context_id;
+ const urlB = originCrossSite + executorPath + pageB.context_id;
+
+ window.open(urlA, '_blank', 'noopener');
+ await pageA.execute_script(waitForPageShow);
+
+ // Register a service worker after `pageA` is loaded to make `pageA`
+ // uncontrolled at this time.
+ const workerUrl =
+ 'resources/service-worker.js?pipe=header(Service-Worker-Allowed,../)';
+ const registration =
+ await service_worker_unregister_and_register(t, workerUrl, './');
+ t.add_cleanup(_ => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Navigate to `urlB`.
+ await pageA.execute_script(
+ (url) => {
+ prepareNavigation(() => { location.href = url; });
+ },
+ [urlB]);
+ await pageB.execute_script(waitForPageShow);
+
+ // Call Clients.claim() on the service worker when `pageA` is in BFCache.
+ const controllerChanged = new Promise(
+ resolve => navigator.serviceWorker.oncontrollerchange = resolve);
+ await claim(t, registration.active);
+ await controllerChanged;
+
+ // `pageA` doesn't appear in matchAll().
+ const clients1 = await (await fetch('/get-clients-matchall')).json();
+ assert_true(clients1.indexOf(urlA) < 0,
+ '1: matchAll() before back navigation');
+
+ // Back navigate and check that the page was evicted from BFCache.
+ await pageB.execute_script(
+ () => {
+ prepareNavigation(() => { history.back(); });
+ }
+ );
+ await pageA.execute_script(waitForPageShow);
+ await assert_not_bfcached(pageA);
+
+ // After back navigation, `pageA` appear in matchAll(), because it was newly
+ // loaded and controlled by the service worker.
+ const clients2 = await (await fetch('/get-clients-matchall')).json();
+ const controlled2 = await pageA.execute_script(
+ () => (navigator.serviceWorker.controller !== null));
+ assert_true(clients2.indexOf(urlA) >= 0,
+ '2: matchAll() just after back navigation');
+ assert_true(controlled2,
+ '2: pageA should be controlled just after back navigation');
+
+}, 'Clients.claim() evicts pages that would be affected from BFCache');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-matchall.https.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-matchall.https.html
new file mode 100644
index 0000000000..069529dbe4
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-clients-matchall.https.html
@@ -0,0 +1,76 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/helper.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+promise_test(async t => {
+ // Register a service worker and make this page controlled.
+ const workerUrl =
+ 'resources/service-worker.js?pipe=header(Service-Worker-Allowed,../)';
+ const registration =
+ await service_worker_unregister_and_register(t, workerUrl, './');
+ t.add_cleanup(_ => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const controllerChanged = new Promise(
+ resolve => navigator.serviceWorker.oncontrollerchange = resolve);
+ await claim(t, registration.active);
+ await controllerChanged;
+
+ const pageA = new RemoteContext(token());
+ const pageB = new RemoteContext(token());
+
+ const urlA = location.origin + executorPath + pageA.context_id;
+ const urlB = originCrossSite + executorPath + pageB.context_id;
+
+ // Open `urlA`.
+ window.open(urlA, '_blank', 'noopener');
+ await pageA.execute_script(waitForPageShow);
+
+ // Get Clients.matchAll() and check whether `pageA` is controlled.
+ // Actual `assert_*()` is called after `assert_bfcached()` below.
+ const clients1 = await (await fetch('/get-clients-matchall')).json();
+ const controlled1 = await pageA.execute_script(
+ () => (navigator.serviceWorker.controller !== null));
+
+ // Navigate to `urlB` and get Clients.matchAll() when `urlA` is in BFCache.
+ await pageA.execute_script(
+ (url) => prepareNavigation(() => {
+ location.href = url;
+ }),
+ [urlB]);
+ await pageB.execute_script(waitForPageShow);
+ const clients2 = await (await fetch('/get-clients-matchall')).json();
+
+ // Back navigate and check whether the page is restored from BFCache.
+ await pageB.execute_script(
+ () => {
+ prepareNavigation(() => { history.back(); });
+ }
+ );
+ await pageA.execute_script(waitForPageShow);
+ await assert_bfcached(pageA);
+
+ // Get Clients.matchAll() and check whether `pageA` is controlled.
+ const clients3 = await (await fetch('/get-clients-matchall')).json();
+ const controlled3 = await pageA.execute_script(
+ () => (navigator.serviceWorker.controller !== null));
+
+ // Clients.matchAll() should not list `urlA` when it is in BFCache.
+ assert_true(clients1.indexOf(urlA) >= 0,
+ '1: matchAll() before navigation');
+ assert_true(clients2.indexOf(urlA) < 0,
+ '2: matchAll() before back navigation');
+ assert_true(clients3.indexOf(urlA) >= 0,
+ '3: matchAll() after back navigation');
+
+ // `pageA` should be controlled before/after BFCached.
+ assert_true(controlled1,
+ 'pageA should be controlled before BFCached');
+ assert_true(controlled3,
+ 'pageA should be controlled after restored');
+}, 'Clients.matchAll() should not list pages in BFCache');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-controlled-after-restore.https.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-controlled-after-restore.https.html
new file mode 100644
index 0000000000..a937eb85ac
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-controlled-after-restore.https.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/helper.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+promise_test(async t => {
+ const pageA = new RemoteContext(token());
+ const pageB = new RemoteContext(token());
+
+ const urlA = location.origin + executorPath + pageA.context_id;
+ const urlB = originCrossSite + executorPath + pageB.context_id;
+
+ // Register a service worker.
+ const workerUrl =
+ 'resources/service-worker.js?pipe=header(Service-Worker-Allowed,../)';
+ const registration =
+ await service_worker_unregister_and_register(t, workerUrl, './');
+ t.add_cleanup(_ => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ window.open(urlA, '_blank', 'noopener');
+ await pageA.execute_script(waitForPageShow);
+
+ assert_true(
+ await pageA.execute_script(
+ () => (navigator.serviceWorker.controller !== null)),
+ 'pageA should be controlled before navigation');
+
+ await navigateAndThenBack(pageA, pageB, urlB);
+ await assert_bfcached(pageA);
+
+ assert_true(
+ await pageA.execute_script(
+ () => (navigator.serviceWorker.controller !== null)),
+ 'navigator.serviceWorker.controller should be non-null ' +
+ 'after restored from BFCache');
+
+ const isControlled = await pageA.execute_script(
+ () => fetch('/is-controlled').then(r => r.text()));
+
+ assert_true(
+ await pageA.execute_script(
+ () => (navigator.serviceWorker.controller !== null)),
+ 'navigator.serviceWorker.controller should be non-null ' +
+ 'after restored from BFCache and after fetch');
+
+ assert_equals(isControlled, 'controlled',
+ 'fetch should be intercepted after restored from BFCache');
+}, 'Pages should remain controlled after restored from BFCache');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-unregister.https.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-unregister.https.html
new file mode 100644
index 0000000000..1c3f81153c
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/service-worker-unregister.https.html
@@ -0,0 +1,67 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/helper.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+// When a service worker is unregistered when a controlled page is in BFCache,
+// the page can be still restored from BFCache and remain controlled by the
+// service worker.
+promise_test(async t => {
+ // Register a service worker and make this page controlled.
+ const workerUrl =
+ 'resources/service-worker.js?pipe=header(Service-Worker-Allowed,../)';
+ const registration =
+ await service_worker_unregister_and_register(t, workerUrl, './');
+ t.add_cleanup(_ => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const controllerChanged = new Promise(
+ resolve => navigator.serviceWorker.oncontrollerchange = resolve);
+ await claim(t, registration.active);
+ await controllerChanged;
+
+ const pageA = new RemoteContext(token());
+ const pageB = new RemoteContext(token());
+
+ const urlA = location.origin + executorPath + pageA.context_id;
+ const urlB = originCrossSite + executorPath + pageB.context_id;
+
+ // Open `urlA`.
+ window.open(urlA, '_blank', 'noopener');
+ await pageA.execute_script(waitForPageShow);
+
+ assert_true(
+ await pageA.execute_script(
+ () => (navigator.serviceWorker.controller !== null)),
+ 'pageA should be controlled before navigation');
+
+ // Navigate to `urlB`.
+ await pageA.execute_script(
+ (url) => prepareNavigation(() => {
+ location.href = url;
+ }),
+ [urlB]);
+ await pageB.execute_script(waitForPageShow);
+
+ // Unregister the service worker when the controlled `pageA` is in BFCache.
+ await registration.unregister();
+
+ // Back navigate and check whether the page is restored from BFCache.
+ await pageB.execute_script(
+ () => {
+ prepareNavigation(() => { history.back(); });
+ }
+ );
+ await pageA.execute_script(waitForPageShow);
+ await assert_not_bfcached(pageA);
+
+ assert_true(
+ await pageA.execute_script(
+ () => (navigator.serviceWorker.controller === null)),
+ 'pageA should not be controlled');
+
+}, 'Unregister service worker while a controlled page is in BFCache');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/storage-events.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/storage-events.html
new file mode 100644
index 0000000000..6957496c30
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/storage-events.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/helper.sub.js"></script>
+<script>
+// When localStorage (`key1`) is modified when a page (`pageA`) is in BFCache,
+// storage events should not be fired for the page after becoming active.
+// https://github.com/whatwg/storage/issues/119#issuecomment-1115844532
+promise_test(async t => {
+ const pageA = new RemoteContext(token());
+ const pageB = new RemoteContext(token());
+ const pageC = new RemoteContext(token());
+
+ const urlA = executorPath + pageA.context_id + '&events=pagehide,pageshow,load';
+ const urlB = originCrossSite + executorPath + pageB.context_id;
+ const urlC = executorPath + pageC.context_id + '&events=pagehide,pageshow,load';
+
+ // localStorage key to set while pageA is in BFCache.
+ const key1 = token();
+ // localStorage key to set after pageA is restored from BFCache.
+ const key2 = token();
+
+ const startRecordingStorageEvent = (key1, key2) => {
+ window.key1EventFired = new Promise(resolve => {
+ window.addEventListener('storage', e => {
+ if (e.key === key1) {
+ recordEvent('storage1');
+ resolve();
+ }
+ });
+ });
+ window.key2EventFired = new Promise(resolve => {
+ window.addEventListener('storage', e => {
+ if (e.key === key2) {
+ recordEvent('storage2');
+ resolve();
+ }
+ });
+ });
+ };
+
+ window.open(urlA, '_blank', 'noopener');
+ await pageA.execute_script(waitForPageShow);
+ await pageA.execute_script(startRecordingStorageEvent, [key1, key2]);
+
+ // Window C is an unrelated window kept open without navigation, to confirm
+ // that storage events are fired as expected in non-BFCache-related scenario
+ // and not blocked due to non-BFCache-related reasons.
+ window.open(urlC, '_blank');
+ await pageC.execute_script(waitForPageShow);
+ await pageC.execute_script(startRecordingStorageEvent, [key1, key2]);
+
+ // Navigate A to B.
+ await pageA.execute_script((url) => {
+ prepareNavigation(() => {
+ location.href = url;
+ });
+ }, [urlB]);
+ await pageB.execute_script(waitForPageShow);
+
+ // Update `key1` while pageA is in BFCache.
+ localStorage.setItem(key1, 'value');
+
+ // Wait for a storage event is fired on PageC and a while,
+ // to prevent race conditions between event processing
+ // triggered by `setItem()` and the following operations.
+ await pageC.execute_script(() => window.key1EventFired);
+ await new Promise(resolve => t.step_timeout(resolve, 1000));
+
+ // Back navigate to pageA, to be restored from BFCache.
+ await pageB.execute_script(
+ () => {
+ prepareNavigation(() => { history.back(); });
+ }
+ );
+ await pageA.execute_script(waitForPageShow);
+ await assert_bfcached(pageA);
+
+ // Update `key2` after pageA is restored from BFCache.
+ localStorage.setItem(key2, 'value');
+
+ // Wait for a storage event for `key2` is fired on PageA.
+ await pageA.execute_script(() => window.key2EventFired);
+
+ // Confirm that a storage event for `key1` is not fired on PageA.
+ assert_array_equals(
+ await pageA.execute_script(() => getRecordedEvents()),
+ [
+ 'window.load',
+ 'window.pageshow',
+ 'window.pagehide.persisted',
+ 'window.pageshow.persisted',
+ 'storage2',
+ ],
+ 'pageA should not receive storage events for updates while in BFCache');
+
+}, 'Storage events should not be fired for BFCached pages after becoming active');
+</script>
diff --git a/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/timers.html b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/timers.html
new file mode 100644
index 0000000000..aab650f36e
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/timers.html
@@ -0,0 +1,68 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers:fully-active">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/helper.sub.js"></script>
+<script>
+// Timers should be paused when the Document is not fully active.
+// This test is checking this by measuring the actual elapsed time for a timer
+// started before a page is stored into BFCache, staying for a while in BFCache,
+// and fired after the page is restored from BFCache.
+
+const delayMain = 18000;
+const delayBeforeForwardNavigation = 6000;
+const delayBeforeBackNavigation = 5000;
+// `delayBeforeForwardNavigation` and `delayBeforeBackNavigation` are set
+// sufficiently large in order to distinguish the expected case from other
+// scenarios listed in `funcAfterAssertion()`, and to allow some delays outside
+// timers (e.g. due to communication between Windows). The additional delays
+// can be large (e.g. ~4 seconds), so the delays above should be sufficiently
+// large.
+
+const startTime = performance.now();
+
+runBfcacheTest({
+ funcBeforeNavigation: async (delayMain, delayBeforeForwardNavigation) => {
+ // Set `promiseMainTimer` that is resolved after a timeout of `delayMain`
+ // ms.
+ window.promiseMainTimer = new Promise(resolve => {
+ setTimeout(resolve, delayMain);
+ });
+ // Then navigate to another page after `delayBeforeForwardNavigation` ms.
+ await new Promise(resolve =>
+ setTimeout(resolve, delayBeforeForwardNavigation));
+ },
+ argsBeforeNavigation: [delayMain, delayBeforeForwardNavigation],
+ funcBeforeBackNavigation: async (delayBeforeBackNavigation) => {
+ // Back navigate after `delayBeforeBackNavigation` ms.
+ await new Promise(resolve =>
+ setTimeout(resolve, delayBeforeBackNavigation));
+ },
+ argsBeforeBackNavigation: [delayBeforeBackNavigation],
+ funcAfterAssertion: async (pageA) => {
+ // Wait for `promiseMainTimer` resolution and check its timing.
+ await pageA.execute_script(() => window.promiseMainTimer);
+ const actualDelay = performance.now() - startTime;
+
+ if (actualDelay >= delayMain + delayBeforeBackNavigation +
+ delayBeforeForwardNavigation) {
+ assert_unreached(
+ "The timer is fired too late. " +
+ "Maybe the timer is reset when restored from BFCache and " +
+ "waits from the beginning again");
+ } else if (actualDelay >= delayMain + delayBeforeBackNavigation) {
+ // Expected: The timer is paused when the page is in BFCache.
+ } else if (actualDelay >= delayMain) {
+ assert_unreached(
+ "The timer is fired too early. " +
+ "Maybe the time isn't paused when the page is in BFCache");
+ } else {
+ assert_unreached(
+ "The timer is fired too early, even earlier than delayMain.");
+ }
+ }
+}, 'Timers should be paused when the page is in BFCache');
+</script>