const HELPER_PAGE_URL = "http://example.com/browser/dom/tests/browser/page_localstorage_e10s.html"; const HELPER_PAGE_ORIGIN = "http://example.com/"; let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); Services.scriptloader.loadSubScript( testDir + "/helper_localStorage_e10s.js", this ); /** * 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. */ /* import-globals-from helper_localStorage_e10s.js */ 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() { let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( HELPER_PAGE_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) { let storageExists = await SpecialPowers.spawn( knownTab.tab.linkedBrowser, [HELPER_PAGE_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"); } } // We spin up a ton of child processes. requestLongerTimeout(4); /** * Verify the basics of our multi-e10s localStorage support. We are focused on * whitebox testing two things. When this is being written, broadcast filtering * is not in place, but the test is intended to attempt to verify that its * implementation does not break things. * * 1) That pages see the same localStorage state in a timely fashion when * engaging in non-conflicting operations. We are not testing races or * conflict resolution; the spec does not cover that. * * 2) That there are no edge-cases related to when the Storage instance is * created for the page or the StorageCache for the origin. (StorageCache is * what actually backs the Storage binding exposed to the page.) This * matters because the following reasons can exist for them to be created: * - Preload, on the basis of knowing the origin uses localStorage. The * interesting edge case is when we have the same origin open in different * processes and the origin starts using localStorage when it did not * before. Preload will not have instantiated bindings, which could impact * correctness. * - The page accessing localStorage for read or write purposes. This is the * obvious, boring one. * - The page adding a "storage" listener. This is less obvious and * interacts with the preload edge-case mentioned above. The page needs to * hear "storage" events even if the page has not touched localStorage * itself and its origin had nothing stored in localStorage when the page * was created. * * We use the same simple child page in all tabs that: * - can be instructed to listen for and record "storage" events * - can be instructed to issue a series of localStorage writes * - can be instructed to return the current entire localStorage contents * * We open the 5 following tabs: * - Open a "writer" tab that does not listen for "storage" events and will * issue only writes. * - Open a "listener" tab instructed to listen for "storage" events * immediately. We expect it to capture all events. * - Open an "reader" tab that does not listen for "storage" events and will * only issue reads when instructed. * - Open a "lateWriteThenListen" tab that initially does nothing. We will * later tell it to issue a write and then listen for events to make sure it * captures the later events. * - Open "lateOpenSeesPreload" tab after we've done everything and ensure that * it preloads/precaches the data without us having touched localStorage or * added an event listener. */ add_task(async function() { await SpecialPowers.pushPrefEnv({ set: [ // Stop the preallocated process manager from speculatively creating // processes. Our test explicitly asserts on whether preload happened or // not for each tab's process. This information is loaded and latched by // the StorageDBParent constructor which the child process's // LocalStorageManager() constructor causes to be created via a call to // LocalStorageCache::StartDatabase(). Although the service is lazily // created and should not have been created prior to our opening the tab, // it's safest to ensure the process simply didn't exist before we ask for // it. // // This is done in conjunction with our use of forceNewProcess when // opening tabs. There would be no point if we weren't also requesting a // new process. ["dom.ipc.processPrelaunch.enabled", false], // Enable LocalStorage's testing API so we can explicitly trigger a flush // when needed. ["dom.storage.testing", true], ], }); // Ensure that there is no localstorage data or potential false positives for // localstorage preloads by forcing the origin to be cleared prior to the // start of our test. await clearOriginStorageEnsuringNoPreload(); // Make sure mOriginsHavingData gets updated. await triggerAndWaitForLocalStorageFlush(); // - Open tabs. Don't configure any of them yet. const knownTabs = new KnownTabs(); const writerTab = await openTestTabInOwnProcess( HELPER_PAGE_URL, "writer", knownTabs ); const listenerTab = await openTestTabInOwnProcess( HELPER_PAGE_URL, "listener", knownTabs ); const readerTab = await openTestTabInOwnProcess( HELPER_PAGE_URL, "reader", knownTabs ); const lateWriteThenListenTab = await openTestTabInOwnProcess( HELPER_PAGE_URL, "lateWriteThenListen", knownTabs ); // Sanity check that preloading did not occur in the tabs. await verifyTabPreload(writerTab, false); await verifyTabPreload(listenerTab, false); await verifyTabPreload(readerTab, false); // - Configure the tabs. const initialSentinel = "initial"; const noSentinelCheck = null; await recordTabStorageEvents(listenerTab, initialSentinel); // - Issue the initial batch of writes and verify. info("initial writes"); const initialWriteMutations = [ // [key (null=clear), newValue (null=delete), oldValue (verification)] ["getsCleared", "1", null], ["alsoGetsCleared", "2", null], [null, null, null], ["stays", "3", null], ["clobbered", "pre", null], ["getsDeletedLater", "4", null], ["getsDeletedImmediately", "5", null], ["getsDeletedImmediately", null, "5"], ["alsoStays", "6", null], ["getsDeletedLater", null, "4"], ["clobbered", "post", "pre"], ]; const initialWriteState = { stays: "3", clobbered: "post", alsoStays: "6", }; await mutateTabStorage(writerTab, initialWriteMutations, initialSentinel); // We expect the writer tab to have the correct state because it just did the // writes. We do not perform a sentinel-check because the writes should be // locally available and consistent. await verifyTabStorageState(writerTab, initialWriteState, noSentinelCheck); // We expect the listener tab to have heard all events despite preload not // having occurred and despite not issuing any reads or writes itself. We // intentionally check the events before the state because we're most // interested in adding the listener having had a side-effect of subscribing // to changes for the process. // // We ensure it had a chance to hear all of the events because we told // recordTabStorageEvents to listen for the given sentinel. The state check // then does not need to do a sentinel check. await verifyTabStorageEvents( listenerTab, initialWriteMutations, initialSentinel ); await verifyTabStorageState(listenerTab, initialWriteState, noSentinelCheck); // We expect the reader tab to retrieve the current localStorage state from // the database. Because of the above checks, we are confident that the // writes have hit PBackground and therefore that the (synchronous) state // retrieval contains all the data we need. No sentinel-check is required. await verifyTabStorageState(readerTab, initialWriteState, noSentinelCheck); // - Issue second set of writes from lateWriteThenListen // This tests that our new tab that begins by issuing only writes is building // on top of the existing state (although we don't verify that until after the // next set of mutations). We also verify that the initial "writerTab" that // was our first tab and started with only writes sees the writes, even though // it did not add an event listener. info("late writes"); const lateWriteSentinel = "lateWrite"; const lateWriteMutations = [ ["lateStays", "10", null], ["lateClobbered", "latePre", null], ["lateDeleted", "11", null], ["lateClobbered", "lastPost", "latePre"], ["lateDeleted", null, "11"], ]; const lateWriteState = Object.assign({}, initialWriteState, { lateStays: "10", lateClobbered: "lastPost", }); await recordTabStorageEvents(listenerTab, lateWriteSentinel); await mutateTabStorage( lateWriteThenListenTab, lateWriteMutations, lateWriteSentinel ); // Verify the writer tab saw the writes. It has to wait for the sentinel to // appear before checking. await verifyTabStorageState(writerTab, lateWriteState, lateWriteSentinel); // Wait for the sentinel event before checking the events and then the state. await verifyTabStorageEvents( listenerTab, lateWriteMutations, lateWriteSentinel ); await verifyTabStorageState(listenerTab, lateWriteState, noSentinelCheck); // We need to wait for the sentinel to show up for the reader. await verifyTabStorageState(readerTab, lateWriteState, lateWriteSentinel); // - Issue last set of writes from writerTab. info("last set of writes"); const lastWriteSentinel = "lastWrite"; const lastWriteMutations = [ ["lastStays", "20", null], ["lastDeleted", "21", null], ["lastClobbered", "lastPre", null], ["lastClobbered", "lastPost", "lastPre"], ["lastDeleted", null, "21"], ]; const lastWriteState = Object.assign({}, lateWriteState, { lastStays: "20", lastClobbered: "lastPost", }); await recordTabStorageEvents(listenerTab, lastWriteSentinel); await recordTabStorageEvents(lateWriteThenListenTab, lastWriteSentinel); await mutateTabStorage(writerTab, lastWriteMutations, lastWriteSentinel); // The writer performed the writes, no need to wait for the sentinel. await verifyTabStorageState(writerTab, lastWriteState, noSentinelCheck); // Wait for the sentinel event to be received, then check. await verifyTabStorageEvents( listenerTab, lastWriteMutations, lastWriteSentinel ); await verifyTabStorageState(listenerTab, lastWriteState, noSentinelCheck); // We need to wait for the sentinel to show up for the reader. await verifyTabStorageState(readerTab, lastWriteState, lastWriteSentinel); // Wait for the sentinel event to be received, then check. await verifyTabStorageEvents( lateWriteThenListenTab, lastWriteMutations, lastWriteSentinel ); await verifyTabStorageState( lateWriteThenListenTab, lastWriteState, noSentinelCheck ); // - Force a LocalStorage DB flush so mOriginsHavingData is updated. // mOriginsHavingData is only updated when the storage thread runs its // accumulated operations during the flush. If we don't initiate and ensure // that a flush has occurred before moving on to the next step, // mOriginsHavingData may not include our origin when it's sent down to the // child process. info("flush to make preload check work"); await triggerAndWaitForLocalStorageFlush(); // - Open a fresh tab and make sure it sees the precache/preload info("late open preload check"); const lateOpenSeesPreload = await openTestTabInOwnProcess( HELPER_PAGE_URL, "lateOpenSeesPreload", knownTabs ); await verifyTabPreload(lateOpenSeesPreload, true); // - Clean up. await cleanupTabs(knownTabs); clearOriginStorageEnsuringNoPreload(); });