From 467bf8a8ea6396cc72cc38f6f20d038fcb09a8ef Mon Sep 17 00:00:00 2001 From: Hiroshige Hayashizaki Date: Mon, 24 May 2021 16:31:13 -0700 Subject: [PATCH] [BFCache] Add basic event tests + helpers for BFCache WPT back-forward-cache/resources/helper.sub.js: Helper for A->B->A navigation scenarios. BFCache state is detected by observing `pageshow` events. We might want to use more explicit APIs like `isPreviousPageInBFCache` discussed at https://github.com/web-platform-tests/wpt/issues/16359 in the future / in more complicated scenarios, but so far `pageshow`-based detection seems to work without hurting ergonomics. back-forward-cache/resources/events.html: The file that loads `helper.sub.js` and contains test logic and assertions. We navigate to `back-forward-cache/resources/back.html` and then back-navigate to `back-forward-cache/resources/events.html`. In the case of BFCache is not used, two async_test objects are created: - The first one during the initial navigation, which never completes, and - The second one during the back navigation, which will execute test assertions and then complete. This doesn't affect the behavior (the first one seems just ignored), but might look awkward. back-forward-cache/events.html: The main test Document that opens `back-forward-cache/resources/events.html` using `window.open()` with 'noopener' option. This is because (at least) Chromium requires top-level navigations to trigger BFCache and thus `back-forward-cache/resources/events.html` is navigated away during the test, but the WPT test infrastructure doesn't support navigating the main test Document. testharness.js: `fetch_tests_from_prefixed_local_storage()` is introduced to communicate test results from `back-forward-cache/resources/events.html` in a similar way to `fetch_tests_from_window()`. PrefixedLocalStorage.js: Some basic utility methods for `helper.sub.js` are added. Bug: 1107415 Change-Id: I034f9f5376dc3f9f32ca0b936dbd06e458c9160b --- common/PrefixedLocalStorage.js | 18 ++++ .../back-forward-cache/events.html | 12 +++ .../back-forward-cache/resources/back.html | 4 + .../back-forward-cache/resources/events.html | 26 ++++++ .../resources/helper.sub.js | 89 +++++++++++++++++++ resources/testharness.js | 41 +++++++++ 6 files changed, 190 insertions(+) create mode 100644 html/browsers/browsing-the-web/back-forward-cache/events.html create mode 100644 html/browsers/browsing-the-web/back-forward-cache/resources/back.html create mode 100644 html/browsers/browsing-the-web/back-forward-cache/resources/events.html create mode 100644 html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js diff --git a/common/PrefixedLocalStorage.js b/common/PrefixedLocalStorage.js index 2f4e7b6a055caa3..24f19e1d2fceb3f 100644 --- a/common/PrefixedLocalStorage.js +++ b/common/PrefixedLocalStorage.js @@ -48,6 +48,24 @@ PrefixedLocalStorage.prototype.setItem = function (baseKey, value) { localStorage.setItem(this.prefixedKey(baseKey), value); }; +PrefixedLocalStorage.prototype.getItem = function (baseKey) { + return localStorage.getItem(this.prefixedKey(baseKey)); +}; + +PrefixedLocalStorage.prototype.pushItem = function (baseKey, value) { + const array = this.getPushedItems(baseKey); + array.push(value); + this.setItem(baseKey, JSON.stringify(array)); +}; + +PrefixedLocalStorage.prototype.getPushedItems = function (baseKey) { + const value = this.getItem(baseKey); + if (!value) { + return []; + } + return JSON.parse(value); +}; + /** * Listen for `storage` events pertaining to a particular key, * prefixed with this object's prefix. Ignore when value is being set to null diff --git a/html/browsers/browsing-the-web/back-forward-cache/events.html b/html/browsers/browsing-the-web/back-forward-cache/events.html new file mode 100644 index 000000000000000..91585d94339c607 --- /dev/null +++ b/html/browsers/browsing-the-web/back-forward-cache/events.html @@ -0,0 +1,12 @@ + + + + +Events fired during BFCached back navigation (cross-site) + diff --git a/html/browsers/browsing-the-web/back-forward-cache/resources/back.html b/html/browsers/browsing-the-web/back-forward-cache/resources/back.html new file mode 100644 index 000000000000000..39a0a73b68cd611 --- /dev/null +++ b/html/browsers/browsing-the-web/back-forward-cache/resources/back.html @@ -0,0 +1,4 @@ + + diff --git a/html/browsers/browsing-the-web/back-forward-cache/resources/events.html b/html/browsers/browsing-the-web/back-forward-cache/resources/events.html new file mode 100644 index 000000000000000..8384893f3c7ac49 --- /dev/null +++ b/html/browsers/browsing-the-web/back-forward-cache/resources/events.html @@ -0,0 +1,26 @@ + + + + + diff --git a/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js b/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js new file mode 100644 index 000000000000000..ddb670a4b2a92f2 --- /dev/null +++ b/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js @@ -0,0 +1,89 @@ +// A helper script for simple A->B->A navigation scenarios like: +// 1. Initial navigation to `A.html`. +// 2. Navigation to `B.html`. +// 3. Back navigation to `A.html`, assuming `A.html` is (or is not) in BFCache. + +// This script is loaded from `A.html`. + +// `A.html` should be opened using `PrefixedLocalStorage.url()`, because +// `/common/PrefixedLocalStorage.js` is used to save states across navigations. + +window.prefixedLocalStorage = new PrefixedLocalStorageResource({ + close_on_cleanup: true +}); + +// Starts an A->B->A navigation test. This should be called on `A.html`. +// `onStart()` is called on the initial navigation, which is expected to +// initiate a navigation to a page (`B.html`) that will eventually back +// navigate to `A.html`. +// `onBackNavigated(isBFCached, observedEvents)` is called on back navigation. +// - `isBFCached` indicates whether the back navigation is from BFCache or not, +// based on events fired. +// - `observedEvents` is an array of event labels fired on `A.html`, +// if `startRecordingEvents()` is called. +function runTest(test, onStart, onBackNavigated) { + window.addEventListener('load', () => { + if (prefixedLocalStorage.getItem('state') === null) { + // Initial navigation. + prefixedLocalStorage.setItem('state', 'started'); + + // Call `onStart()` (and thus starting navigation) after this document + // is fully loaded. + // `step_timeout()` is used here because starting the navigation + // synchronously inside the window load event handler seems to + // cause back navigation to this page to fail on Firefox. + test.step_timeout(() => { + window.addEventListener('pageshow', (() => { + // Back navigation, from BFCache. + test.step( + onBackNavigated, + undefined, + true, + prefixedLocalStorage.getPushedItems('observedEvents')); + })); + test.step(onStart); + }, 0); + } else { + // Back navigation, not from BFCache. + test.step( + onBackNavigated, + undefined, + false, + prefixedLocalStorage.getPushedItems('observedEvents')); + } + }); +} + +// Records events fired on `window` and `document`, with names listed in +// `eventNames`. +// The recorded events are stored in localStorage and used later in the +// runTest() callback. +function startRecordingEvents(eventNames) { + window.testObservedEvents = []; + for (const eventName of eventNames) { + window.addEventListener(eventName, event => { + let result = eventName; + if (event.persisted) { + result += '.persisted'; + } + if (eventName === 'visibilitychange') { + result += '.' + document.visibilityState; + } + prefixedLocalStorage.pushItem('observedEvents', 'window.' + result); + }); + document.addEventListener(eventName, () => { + let result = eventName; + if (eventName === 'visibilitychange') { + result += '.' + document.visibilityState; + } + prefixedLocalStorage.pushItem('observedEvents', 'document.' + result); + }); + } +} + +const origin = + 'http://{{hosts[alt][www]}}:{{ports[http][0]}}'; // cross-site + +const backUrl = + origin + + '/html/browsers/browsing-the-web/back-forward-cache/resources/back.html'; diff --git a/resources/testharness.js b/resources/testharness.js index f85b19fd9bd90c3..f54808ce35592d8 100644 --- a/resources/testharness.js +++ b/resources/testharness.js @@ -130,6 +130,10 @@ w.postMessage(message_arg, "*"); } }); + if (window.prefixedLocalStorage) { + window.prefixedLocalStorage.setItem('dispatched_messages.' + Math.random(), + JSON.stringify(message_arg)); + } }; WindowTestEnvironment.prototype._forEach_windows = function(callback) { @@ -3168,6 +3172,25 @@ ); }; + /* + * Constructs a RemoteContext that tracks tests from prefixed local storage. + * Test results from a window where + * - `window.prefixedLocalStorage` is defined using `/common/PrefixedLocalStorage.js` + * like `window.prefixedLocalStorage = new PrefixedLocalStorageResource();` and + * - testharness.js is loaded + * are received via `prefixedLocalStorage`. + */ + Tests.prototype.create_remote_prefixed_local_storage = function(prefixedLocalStorage) { + const channel = new MessageChannel(); + // This receives the random keys prefixed by 'dispatched_messages' sent by the remote + // window's `WindowTestEnvironment.prototype._dispatch`. + prefixedLocalStorage.onSet('dispatched_messages', e => { + channel.port1.postMessage(JSON.parse(e.newValue)); + }); + channel.port2.start(); + return new RemoteContext(null, channel.port2); + }; + Tests.prototype.fetch_tests_from_worker = function(worker) { if (this.phase >= this.phases.COMPLETE) { return; @@ -3196,6 +3219,24 @@ } expose(fetch_tests_from_window, 'fetch_tests_from_window'); + + Tests.prototype.fetch_tests_from_prefixed_local_storage = function(prefixedLocalStorage) { + if (this.phase >= this.phases.COMPLETE) { + return; + } + + var remoteContext = this.create_remote_prefixed_local_storage(prefixedLocalStorage); + this.pending_remotes.push(remoteContext); + return remoteContext.done.then(() => { + prefixedLocalStorage.cleanup(); + }); + }; + + function fetch_tests_from_prefixed_local_storage(prefixedLocalStorage) { + return tests.fetch_tests_from_prefixed_local_storage(prefixedLocalStorage); + } + expose(fetch_tests_from_prefixed_local_storage, 'fetch_tests_from_prefixed_local_storage'); + function timeout() { if (tests.timeout_length === null) { tests.timeout();