helper_localStorage.js (10196B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 // Simple tab wrapper abstracting our messaging mechanism; 6 class KnownTab { 7 constructor(name, tab) { 8 this.name = name; 9 this.tab = tab; 10 } 11 12 cleanup() { 13 this.tab = null; 14 } 15 } 16 17 // Simple data structure class to help us track opened tabs and their pids. 18 class KnownTabs { 19 constructor() { 20 this.byPid = new Map(); 21 this.byName = new Map(); 22 } 23 24 cleanup() { 25 for (let key of this.byPid.keys()) { 26 this.byPid[key] = null; 27 } 28 this.byPid = null; 29 this.byName = null; 30 } 31 } 32 33 /** 34 * Open our helper page in a tab in its own content process, asserting that it 35 * really is in its own process. We initially load and wait for about:blank to 36 * load, and only then loadURI to our actual page. This is to ensure that 37 * LocalStorageManager has had an opportunity to be created and populate 38 * mOriginsHavingData. 39 * 40 * (nsGlobalWindow will reliably create LocalStorageManager as a side-effect of 41 * the unconditional call to nsGlobalWindow::PreloadLocalStorage. This will 42 * reliably create the StorageDBChild instance, and its corresponding 43 * StorageDBParent will send the set of origins when it is constructed.) 44 */ 45 async function openTestTab( 46 helperPageUrl, 47 name, 48 knownTabs, 49 shouldLoadInNewProcess 50 ) { 51 let realUrl = helperPageUrl + "?" + encodeURIComponent(name); 52 // Load and wait for about:blank. 53 let tab = await BrowserTestUtils.openNewForegroundTab({ 54 gBrowser, 55 opening: "about:blank", 56 forceNewProcess: true, 57 }); 58 ok(!knownTabs.byName.has(name), "tab needs its own name: " + name); 59 60 let knownTab = new KnownTab(name, tab); 61 knownTabs.byName.set(name, knownTab); 62 63 // Now trigger the actual load of our page. 64 BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, realUrl); 65 await BrowserTestUtils.browserLoaded(tab.linkedBrowser); 66 67 let pid = tab.linkedBrowser.frameLoader.remoteTab.osPid; 68 if (shouldLoadInNewProcess) { 69 ok( 70 !knownTabs.byPid.has(pid), 71 "tab should be loaded in new process, pid: " + pid 72 ); 73 } else { 74 ok( 75 knownTabs.byPid.has(pid), 76 "tab should be loaded in the same process, new pid: " + pid 77 ); 78 } 79 80 if (knownTabs.byPid.has(pid)) { 81 knownTabs.byPid.get(pid).set(name, knownTab); 82 } else { 83 let pidMap = new Map(); 84 pidMap.set(name, knownTab); 85 knownTabs.byPid.set(pid, pidMap); 86 } 87 88 return knownTab; 89 } 90 91 /** 92 * Close all the tabs we opened. 93 */ 94 async function cleanupTabs(knownTabs) { 95 for (let knownTab of knownTabs.byName.values()) { 96 BrowserTestUtils.removeTab(knownTab.tab); 97 knownTab.cleanup(); 98 } 99 knownTabs.cleanup(); 100 } 101 102 /** 103 * Wait for a LocalStorage flush to occur. This notification can occur as a 104 * result of any of: 105 * - The normal, hardcoded 5-second flush timer. 106 * - InsertDBOp seeing a preload op for an origin with outstanding changes. 107 * - Us generating a "domstorage-test-flush-force" observer notification. 108 */ 109 function waitForLocalStorageFlush() { 110 if (Services.domStorageManager.nextGenLocalStorageEnabled) { 111 return new Promise(resolve => executeSoon(resolve)); 112 } 113 114 return new Promise(function (resolve) { 115 let observer = { 116 observe() { 117 SpecialPowers.removeObserver(observer, "domstorage-test-flushed"); 118 resolve(); 119 }, 120 }; 121 SpecialPowers.addObserver(observer, "domstorage-test-flushed"); 122 }); 123 } 124 125 /** 126 * Trigger and wait for a flush. This is only necessary for forcing 127 * mOriginsHavingData to be updated. Normal operations exposed to content know 128 * to automatically flush when necessary for correctness. 129 * 130 * The notification we're waiting for to verify flushing is fundamentally 131 * ambiguous (see waitForLocalStorageFlush), so we actually trigger the flush 132 * twice and wait twice. In the event there was a race, there will be 3 flush 133 * notifications, but correctness is guaranteed after the second notification. 134 */ 135 function triggerAndWaitForLocalStorageFlush() { 136 if (Services.domStorageManager.nextGenLocalStorageEnabled) { 137 return new Promise(resolve => executeSoon(resolve)); 138 } 139 140 SpecialPowers.notifyObservers(null, "domstorage-test-flush-force"); 141 // This first wait is ambiguous... 142 return waitForLocalStorageFlush().then(function () { 143 // So issue a second flush and wait for that. 144 SpecialPowers.notifyObservers(null, "domstorage-test-flush-force"); 145 return waitForLocalStorageFlush(); 146 }); 147 } 148 149 /** 150 * Clear the origin's storage so that "OriginsHavingData" will return false for 151 * our origin. Note that this is only the case for AsyncClear() which is 152 * explicitly issued against a cache, or AsyncClearAll() which we can trigger 153 * by wiping all storage. However, the more targeted domain clearings that 154 * we can trigger via observer, AsyncClearMatchingOrigin and 155 * AsyncClearMatchingOriginAttributes will not clear the hashtable entry for 156 * the origin. 157 * 158 * So we explicitly access the cache here in the parent for the origin and issue 159 * an explicit clear. Clearing all storage might be a little easier but seems 160 * like asking for intermittent failures. 161 */ 162 function clearOriginStorageEnsuringNoPreload(origin) { 163 let principal = 164 Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); 165 166 if (Services.domStorageManager.nextGenLocalStorageEnabled) { 167 let request = Services.qms.clearStoragesForClient( 168 principal, 169 "ls", 170 "default" 171 ); 172 let promise = new Promise(resolve => { 173 request.callback = () => { 174 resolve(); 175 }; 176 }); 177 return promise; 178 } 179 180 // We want to use createStorage to force the cache to be created so we can 181 // issue the clear. It's possible for getStorage to return false but for the 182 // origin preload hash to still have our origin in it. 183 let storage = Services.domStorageManager.createStorage( 184 null, 185 principal, 186 principal, 187 "" 188 ); 189 storage.clear(); 190 191 // We also need to trigger a flush os that mOriginsHavingData gets updated. 192 // The inherent flush race is fine here because 193 return triggerAndWaitForLocalStorageFlush(); 194 } 195 196 async function verifyTabPreload(knownTab, expectStorageExists, origin) { 197 let storageExists = await SpecialPowers.spawn( 198 knownTab.tab.linkedBrowser, 199 [origin], 200 function (origin) { 201 let principal = 202 Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); 203 if (Services.domStorageManager.nextGenLocalStorageEnabled) { 204 return Services.domStorageManager.isPreloaded(principal); 205 } 206 return !!Services.domStorageManager.getStorage( 207 null, 208 principal, 209 principal 210 ); 211 } 212 ); 213 is(storageExists, expectStorageExists, "Storage existence === preload"); 214 } 215 216 /** 217 * Instruct the given tab to execute the given series of mutations. For 218 * simplicity, the mutations representation matches the expected events rep. 219 */ 220 async function mutateTabStorage(knownTab, mutations, sentinelValue) { 221 await SpecialPowers.spawn( 222 knownTab.tab.linkedBrowser, 223 [{ mutations, sentinelValue }], 224 function (args) { 225 return content.wrappedJSObject.mutateStorage(Cu.cloneInto(args, content)); 226 } 227 ); 228 } 229 230 /** 231 * Instruct the given tab to add a "storage" event listener and record all 232 * received events. verifyTabStorageEvents is the corresponding method to 233 * check and assert the recorded events. 234 */ 235 async function recordTabStorageEvents(knownTab, sentinelValue) { 236 await SpecialPowers.spawn( 237 knownTab.tab.linkedBrowser, 238 [sentinelValue], 239 function (sentinelValue) { 240 return content.wrappedJSObject.listenForStorageEvents(sentinelValue); 241 } 242 ); 243 } 244 245 /** 246 * Retrieve the current localStorage contents perceived by the tab and assert 247 * that they match the provided expected state. 248 * 249 * If maybeSentinel is non-null, it's assumed to be a string that identifies the 250 * value we should be waiting for the sentinel key to take on. This is 251 * necessary because we cannot make any assumptions about when state will be 252 * propagated to the given process. See the comments in 253 * page_localstorage_e10s.js for more context. In general, a sentinel value is 254 * required for correctness unless the process in question is the one where the 255 * writes were performed or verifyTabStorageEvents was used. 256 */ 257 async function verifyTabStorageState(knownTab, expectedState, maybeSentinel) { 258 let actualState = await SpecialPowers.spawn( 259 knownTab.tab.linkedBrowser, 260 [maybeSentinel], 261 function (maybeSentinel) { 262 return content.wrappedJSObject.getStorageState(maybeSentinel); 263 } 264 ); 265 266 for (let [expectedKey, expectedValue] of Object.entries(expectedState)) { 267 ok(actualState.hasOwnProperty(expectedKey), "key present: " + expectedKey); 268 is(actualState[expectedKey], expectedValue, "value correct"); 269 } 270 for (let actualKey of Object.keys(actualState)) { 271 if (!expectedState.hasOwnProperty(actualKey)) { 272 ok(false, "actual state has key it shouldn't have: " + actualKey); 273 } 274 } 275 } 276 277 /** 278 * Retrieve and clear the storage events recorded by the tab and assert that 279 * they match the provided expected events. For simplicity, the expected events 280 * representation is the same as that used by mutateTabStorage. 281 * 282 * Note that by convention for test readability we are passed a 3rd argument of 283 * the sentinel value, but we don't actually care what it is. 284 */ 285 async function verifyTabStorageEvents(knownTab, expectedEvents) { 286 let actualEvents = await SpecialPowers.spawn( 287 knownTab.tab.linkedBrowser, 288 [], 289 function () { 290 return content.wrappedJSObject.returnAndClearStorageEvents(); 291 } 292 ); 293 294 is(actualEvents.length, expectedEvents.length, "right number of events"); 295 for (let i = 0; i < actualEvents.length; i++) { 296 let [actualKey, actualNewValue, actualOldValue] = actualEvents[i]; 297 let [expectedKey, expectedNewValue, expectedOldValue] = expectedEvents[i]; 298 is(actualKey, expectedKey, "keys match"); 299 is(actualNewValue, expectedNewValue, "new values match"); 300 is(actualOldValue, expectedOldValue, "old values match"); 301 } 302 }