From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- dom/tests/browser/helper_localStorage.js | 302 +++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 dom/tests/browser/helper_localStorage.js (limited to 'dom/tests/browser/helper_localStorage.js') 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"); + } +} -- cgit v1.2.3