summaryrefslogtreecommitdiffstats
path: root/dom/tests/browser/helper_localStorage.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/tests/browser/helper_localStorage.js')
-rw-r--r--dom/tests/browser/helper_localStorage.js302
1 files changed, 302 insertions, 0 deletions
diff --git a/dom/tests/browser/helper_localStorage.js b/dom/tests/browser/helper_localStorage.js
new file mode 100644
index 0000000000..386d2d8b4e
--- /dev/null
+++ b/dom/tests/browser/helper_localStorage.js
@@ -0,0 +1,302 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Simple tab wrapper abstracting our messaging mechanism;
+class KnownTab {
+ constructor(name, tab) {
+ this.name = name;
+ this.tab = tab;
+ }
+
+ cleanup() {
+ this.tab = null;
+ }
+}
+
+// Simple data structure class to help us track opened tabs and their pids.
+class KnownTabs {
+ constructor() {
+ this.byPid = new Map();
+ this.byName = new Map();
+ }
+
+ cleanup() {
+ for (let key of this.byPid.keys()) {
+ this.byPid[key] = null;
+ }
+ this.byPid = null;
+ this.byName = null;
+ }
+}
+
+/**
+ * Open our helper page in a tab in its own content process, asserting that it
+ * really is in its own process. We initially load and wait for about:blank to
+ * load, and only then loadURI to our actual page. This is to ensure that
+ * LocalStorageManager has had an opportunity to be created and populate
+ * mOriginsHavingData.
+ *
+ * (nsGlobalWindow will reliably create LocalStorageManager as a side-effect of
+ * the unconditional call to nsGlobalWindow::PreloadLocalStorage. This will
+ * reliably create the StorageDBChild instance, and its corresponding
+ * StorageDBParent will send the set of origins when it is constructed.)
+ */
+async function openTestTab(
+ helperPageUrl,
+ name,
+ knownTabs,
+ shouldLoadInNewProcess
+) {
+ let realUrl = helperPageUrl + "?" + encodeURIComponent(name);
+ // Load and wait for about:blank.
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:blank",
+ forceNewProcess: true,
+ });
+ ok(!knownTabs.byName.has(name), "tab needs its own name: " + name);
+
+ let knownTab = new KnownTab(name, tab);
+ knownTabs.byName.set(name, knownTab);
+
+ // Now trigger the actual load of our page.
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, realUrl);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ let pid = tab.linkedBrowser.frameLoader.remoteTab.osPid;
+ if (shouldLoadInNewProcess) {
+ ok(
+ !knownTabs.byPid.has(pid),
+ "tab should be loaded in new process, pid: " + pid
+ );
+ } else {
+ ok(
+ knownTabs.byPid.has(pid),
+ "tab should be loaded in the same process, new pid: " + pid
+ );
+ }
+
+ if (knownTabs.byPid.has(pid)) {
+ knownTabs.byPid.get(pid).set(name, knownTab);
+ } else {
+ let pidMap = new Map();
+ pidMap.set(name, knownTab);
+ knownTabs.byPid.set(pid, pidMap);
+ }
+
+ return knownTab;
+}
+
+/**
+ * Close all the tabs we opened.
+ */
+async function cleanupTabs(knownTabs) {
+ for (let knownTab of knownTabs.byName.values()) {
+ BrowserTestUtils.removeTab(knownTab.tab);
+ knownTab.cleanup();
+ }
+ knownTabs.cleanup();
+}
+
+/**
+ * Wait for a LocalStorage flush to occur. This notification can occur as a
+ * result of any of:
+ * - The normal, hardcoded 5-second flush timer.
+ * - InsertDBOp seeing a preload op for an origin with outstanding changes.
+ * - Us generating a "domstorage-test-flush-force" observer notification.
+ */
+function waitForLocalStorageFlush() {
+ if (Services.domStorageManager.nextGenLocalStorageEnabled) {
+ return new Promise(resolve => executeSoon(resolve));
+ }
+
+ return new Promise(function (resolve) {
+ let observer = {
+ observe() {
+ SpecialPowers.removeObserver(observer, "domstorage-test-flushed");
+ resolve();
+ },
+ };
+ SpecialPowers.addObserver(observer, "domstorage-test-flushed");
+ });
+}
+
+/**
+ * Trigger and wait for a flush. This is only necessary for forcing
+ * mOriginsHavingData to be updated. Normal operations exposed to content know
+ * to automatically flush when necessary for correctness.
+ *
+ * The notification we're waiting for to verify flushing is fundamentally
+ * ambiguous (see waitForLocalStorageFlush), so we actually trigger the flush
+ * twice and wait twice. In the event there was a race, there will be 3 flush
+ * notifications, but correctness is guaranteed after the second notification.
+ */
+function triggerAndWaitForLocalStorageFlush() {
+ if (Services.domStorageManager.nextGenLocalStorageEnabled) {
+ return new Promise(resolve => executeSoon(resolve));
+ }
+
+ SpecialPowers.notifyObservers(null, "domstorage-test-flush-force");
+ // This first wait is ambiguous...
+ return waitForLocalStorageFlush().then(function () {
+ // So issue a second flush and wait for that.
+ SpecialPowers.notifyObservers(null, "domstorage-test-flush-force");
+ return waitForLocalStorageFlush();
+ });
+}
+
+/**
+ * Clear the origin's storage so that "OriginsHavingData" will return false for
+ * our origin. Note that this is only the case for AsyncClear() which is
+ * explicitly issued against a cache, or AsyncClearAll() which we can trigger
+ * by wiping all storage. However, the more targeted domain clearings that
+ * we can trigger via observer, AsyncClearMatchingOrigin and
+ * AsyncClearMatchingOriginAttributes will not clear the hashtable entry for
+ * the origin.
+ *
+ * So we explicitly access the cache here in the parent for the origin and issue
+ * an explicit clear. Clearing all storage might be a little easier but seems
+ * like asking for intermittent failures.
+ */
+function clearOriginStorageEnsuringNoPreload(origin) {
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
+
+ if (Services.domStorageManager.nextGenLocalStorageEnabled) {
+ let request = Services.qms.clearStoragesForPrincipal(
+ principal,
+ "default",
+ "ls"
+ );
+ let promise = new Promise(resolve => {
+ request.callback = () => {
+ resolve();
+ };
+ });
+ return promise;
+ }
+
+ // We want to use createStorage to force the cache to be created so we can
+ // issue the clear. It's possible for getStorage to return false but for the
+ // origin preload hash to still have our origin in it.
+ let storage = Services.domStorageManager.createStorage(
+ null,
+ principal,
+ principal,
+ ""
+ );
+ storage.clear();
+
+ // We also need to trigger a flush os that mOriginsHavingData gets updated.
+ // The inherent flush race is fine here because
+ return triggerAndWaitForLocalStorageFlush();
+}
+
+async function verifyTabPreload(knownTab, expectStorageExists, origin) {
+ let storageExists = await SpecialPowers.spawn(
+ knownTab.tab.linkedBrowser,
+ [origin],
+ function (origin) {
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
+ if (Services.domStorageManager.nextGenLocalStorageEnabled) {
+ return Services.domStorageManager.isPreloaded(principal);
+ }
+ return !!Services.domStorageManager.getStorage(
+ null,
+ principal,
+ principal
+ );
+ }
+ );
+ is(storageExists, expectStorageExists, "Storage existence === preload");
+}
+
+/**
+ * Instruct the given tab to execute the given series of mutations. For
+ * simplicity, the mutations representation matches the expected events rep.
+ */
+async function mutateTabStorage(knownTab, mutations, sentinelValue) {
+ await SpecialPowers.spawn(
+ knownTab.tab.linkedBrowser,
+ [{ mutations, sentinelValue }],
+ function (args) {
+ return content.wrappedJSObject.mutateStorage(Cu.cloneInto(args, content));
+ }
+ );
+}
+
+/**
+ * Instruct the given tab to add a "storage" event listener and record all
+ * received events. verifyTabStorageEvents is the corresponding method to
+ * check and assert the recorded events.
+ */
+async function recordTabStorageEvents(knownTab, sentinelValue) {
+ await SpecialPowers.spawn(
+ knownTab.tab.linkedBrowser,
+ [sentinelValue],
+ function (sentinelValue) {
+ return content.wrappedJSObject.listenForStorageEvents(sentinelValue);
+ }
+ );
+}
+
+/**
+ * Retrieve the current localStorage contents perceived by the tab and assert
+ * that they match the provided expected state.
+ *
+ * If maybeSentinel is non-null, it's assumed to be a string that identifies the
+ * value we should be waiting for the sentinel key to take on. This is
+ * necessary because we cannot make any assumptions about when state will be
+ * propagated to the given process. See the comments in
+ * page_localstorage_e10s.js for more context. In general, a sentinel value is
+ * required for correctness unless the process in question is the one where the
+ * writes were performed or verifyTabStorageEvents was used.
+ */
+async function verifyTabStorageState(knownTab, expectedState, maybeSentinel) {
+ let actualState = await SpecialPowers.spawn(
+ knownTab.tab.linkedBrowser,
+ [maybeSentinel],
+ function (maybeSentinel) {
+ return content.wrappedJSObject.getStorageState(maybeSentinel);
+ }
+ );
+
+ for (let [expectedKey, expectedValue] of Object.entries(expectedState)) {
+ ok(actualState.hasOwnProperty(expectedKey), "key present: " + expectedKey);
+ is(actualState[expectedKey], expectedValue, "value correct");
+ }
+ for (let actualKey of Object.keys(actualState)) {
+ if (!expectedState.hasOwnProperty(actualKey)) {
+ ok(false, "actual state has key it shouldn't have: " + actualKey);
+ }
+ }
+}
+
+/**
+ * Retrieve and clear the storage events recorded by the tab and assert that
+ * they match the provided expected events. For simplicity, the expected events
+ * representation is the same as that used by mutateTabStorage.
+ *
+ * Note that by convention for test readability we are passed a 3rd argument of
+ * the sentinel value, but we don't actually care what it is.
+ */
+async function verifyTabStorageEvents(knownTab, expectedEvents) {
+ let actualEvents = await SpecialPowers.spawn(
+ knownTab.tab.linkedBrowser,
+ [],
+ function () {
+ return content.wrappedJSObject.returnAndClearStorageEvents();
+ }
+ );
+
+ is(actualEvents.length, expectedEvents.length, "right number of events");
+ for (let i = 0; i < actualEvents.length; i++) {
+ let [actualKey, actualNewValue, actualOldValue] = actualEvents[i];
+ let [expectedKey, expectedNewValue, expectedOldValue] = expectedEvents[i];
+ is(actualKey, expectedKey, "keys match");
+ is(actualNewValue, expectedNewValue, "new values match");
+ is(actualOldValue, expectedOldValue, "old values match");
+ }
+}