diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/html/browsers/browsing-the-web/back-forward-cache | |
parent | Initial commit. (diff) | |
download | firefox-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')
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> |