diff --git a/common/PrefixedLocalStorage.js b/common/PrefixedLocalStorage.js
index 2f4e7b6a055caa..24f19e1d2fceb3 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 00000000000000..91585d94339c60
--- /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 00000000000000..39a0a73b68cd61
--- /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 00000000000000..8384893f3c7ac4
--- /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 00000000000000..ddb670a4b2a92f
--- /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 f85b19fd9bd90c..f54808ce35592d 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();