зеркало из https://github.com/mozilla/gecko-dev.git
305 строки
10 KiB
JavaScript
305 строки
10 KiB
JavaScript
/* 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.loadURI(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");
|
|
}
|
|
}
|