summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache/resources')
-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
11 files changed, 664 insertions, 0 deletions
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');
+ }
+ });
+ }
+};