browser_storage_dynamic_windows.js (11901B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 Services.scriptloader.loadSubScript( 7 "chrome://mochitests/content/browser/devtools/server/tests/browser/storage-helpers.js", 8 this 9 ); 10 11 // beforeReload references an object representing the initialized state of the 12 // storage actor. 13 const beforeReload = { 14 cookies: { 15 "http://test1.example.org": ["c1", "cs2", "c3", "uc1"], 16 "http://sectest1.example.org": ["uc1", "cs2"], 17 }, 18 "indexed-db": { 19 "http://test1.example.org": [ 20 JSON.stringify(["idb1", "obj1"]), 21 JSON.stringify(["idb1", "obj2"]), 22 JSON.stringify(["idb2", "obj3"]), 23 ], 24 "http://sectest1.example.org": [], 25 }, 26 "local-storage": { 27 "http://test1.example.org": ["ls1", "ls2"], 28 "http://sectest1.example.org": ["iframe-u-ls1"], 29 }, 30 "session-storage": { 31 "http://test1.example.org": ["ss1"], 32 "http://sectest1.example.org": ["iframe-u-ss1", "iframe-u-ss2"], 33 }, 34 }; 35 36 // afterIframeAdded references the items added when an iframe containing storage 37 // items is added to the page. 38 const afterIframeAdded = { 39 cookies: { 40 "https://sectest1.example.org": [ 41 getCookieId("cs2", ".example.org", "/"), 42 getCookieId( 43 "sc1", 44 "sectest1.example.org", 45 "/browser/devtools/server/tests/browser" 46 ), 47 ], 48 "http://sectest1.example.org": [ 49 getCookieId( 50 "sc1", 51 "sectest1.example.org", 52 "/browser/devtools/server/tests/browser" 53 ), 54 ], 55 }, 56 "indexed-db": { 57 // empty because indexed db creation happens after the page load, so at 58 // the time of window-ready, there was no indexed db present. 59 "https://sectest1.example.org": [], 60 }, 61 "local-storage": { 62 "https://sectest1.example.org": ["iframe-s-ls1"], 63 }, 64 "session-storage": { 65 "https://sectest1.example.org": ["iframe-s-ss1"], 66 }, 67 }; 68 69 // afterIframeRemoved references the items deleted when an iframe containing 70 // storage items is removed from the page. 71 const afterIframeRemoved = { 72 cookies: { 73 "http://sectest1.example.org": [], 74 }, 75 "indexed-db": { 76 "http://sectest1.example.org": [], 77 }, 78 "local-storage": { 79 "http://sectest1.example.org": [], 80 }, 81 "session-storage": { 82 "http://sectest1.example.org": [], 83 }, 84 }; 85 86 add_task(async function () { 87 const { commands } = await openTabAndSetupStorage( 88 MAIN_DOMAIN + "storage-dynamic-windows.html" 89 ); 90 91 const { resourceCommand } = commands; 92 const { TYPES } = resourceCommand; 93 const allResources = {}; 94 const onAvailable = resources => { 95 for (const resource of resources) { 96 is( 97 resource.targetFront.targetType, 98 commands.targetCommand.TYPES.FRAME, 99 "Each storage resource has a valid 'targetFront' attribute" 100 ); 101 // Because we have iframes, we have distinct targets, each spawning their own storage resource 102 if (allResources[resource.resourceType]) { 103 allResources[resource.resourceType].push(resource); 104 } else { 105 allResources[resource.resourceType] = [resource]; 106 } 107 } 108 }; 109 const parentProcessStorages = [TYPES.COOKIE, TYPES.INDEXED_DB]; 110 const contentProcessStorages = [TYPES.LOCAL_STORAGE, TYPES.SESSION_STORAGE]; 111 const allStorages = [...parentProcessStorages, ...contentProcessStorages]; 112 await resourceCommand.watchResources(allStorages, { onAvailable }); 113 is( 114 Object.keys(allStorages).length, 115 allStorages.length, 116 "Got all the storage resources" 117 ); 118 119 // Do a copy of all the initial storages as test function may spawn new resources for the same 120 // type and override the initial ones. 121 // We do not call unwatchResources as it would clear its cache and next call 122 // to watchResources with ignoreExistingResources would break and reprocess all resources again. 123 const initialResources = Object.assign({}, allResources); 124 125 testWindowsBeforeReload(initialResources); 126 127 await testAddIframe(commands, initialResources, { 128 contentProcessStorages, 129 parentProcessStorages, 130 }); 131 132 await testRemoveIframe(commands, initialResources, { 133 contentProcessStorages, 134 parentProcessStorages, 135 }); 136 137 await clearStorage(); 138 139 // Forcing GC/CC to get rid of docshells and windows created by this test. 140 forceCollections(); 141 await commands.destroy(); 142 forceCollections(); 143 }); 144 145 function testWindowsBeforeReload(resources) { 146 for (const storageType in beforeReload) { 147 ok(resources[storageType], `${storageType} storage actor is present`); 148 149 const hosts = {}; 150 for (const resource of resources[storageType]) { 151 for (const [hostType, hostValues] of Object.entries(resource.hosts)) { 152 if (!hosts[hostType]) { 153 hosts[hostType] = []; 154 } 155 156 hosts[hostType].push(hostValues); 157 } 158 } 159 160 // If this test is run with chrome debugging enabled we get an extra 161 // key for "chrome". We don't want the test to fail in this case, so 162 // ignore it. 163 if (storageType == "indexedDB") { 164 delete hosts.chrome; 165 } 166 167 is( 168 Object.keys(hosts).length, 169 Object.keys(beforeReload[storageType]).length, 170 `Number of hosts for ${storageType} match` 171 ); 172 for (const host in beforeReload[storageType]) { 173 ok(hosts[host], `Host ${host} is present`); 174 } 175 } 176 } 177 178 /** 179 * Wait for new storage resources to be created of the given types. 180 */ 181 async function waitForNewResourcesAndUpdates(commands, resourceTypes) { 182 // When fission is off, we don't expect any new resource 183 if (resourceTypes.length === 0) { 184 return { newResources: [], updates: [] }; 185 } 186 const { resourceCommand } = commands; 187 let resolve; 188 const promise = new Promise(r => (resolve = r)); 189 const allResources = {}; 190 const allUpdates = {}; 191 const onAvailable = resources => { 192 for (const resource of resources) { 193 if (resource.resourceType in allResources) { 194 ok(false, `Got multiple ${resource.resourceTypes} resources`); 195 } 196 allResources[resource.resourceType] = resource; 197 ok(true, `Got resource for ${resource.resourceType}`); 198 199 // Stop watching for resources when we got them all 200 if (Object.keys(allResources).length == resourceTypes.length) { 201 resourceCommand.unwatchResources(resourceTypes, { 202 onAvailable, 203 }); 204 } 205 206 // But also listen for updates on each new resource 207 resource.once("single-store-update").then(update => { 208 ok(true, `Got updates for ${resource.resourceType}`); 209 allUpdates[resource.resourceType] = update; 210 211 // Resolve only once we got all the updates, for all the resources 212 if (Object.keys(allUpdates).length == resourceTypes.length) { 213 resolve({ newResources: allResources, updates: allUpdates }); 214 } 215 }); 216 } 217 }; 218 await resourceCommand.watchResources(resourceTypes, { 219 onAvailable, 220 ignoreExistingResources: true, 221 }); 222 return promise; 223 } 224 225 /** 226 * Wait for single-store-update events on all the given storage resources. 227 */ 228 function waitForResourceUpdates(resources, resourceTypes) { 229 const allUpdates = {}; 230 const promises = []; 231 for (const type of resourceTypes) { 232 // Resolves once any of the many resources for the given storage type updates 233 const promise = Promise.any( 234 resources[type].map(resource => resource.once("single-store-update")) 235 ); 236 promise.then(update => { 237 ok(true, `Got updates for ${type}`); 238 allUpdates[type] = update; 239 }); 240 promises.push(promise); 241 } 242 return Promise.all(promises).then(() => allUpdates); 243 } 244 245 async function testAddIframe( 246 commands, 247 resources, 248 { contentProcessStorages, parentProcessStorages } 249 ) { 250 info("Testing if new iframe addition works properly"); 251 252 // Expect: 253 // - new resources alongside single-store-update events for content process storages 254 // - only single-store-update events for previous resources for parent process storages 255 const onResources = waitForNewResourcesAndUpdates( 256 commands, 257 contentProcessStorages 258 ); 259 const onUpdates = waitForResourceUpdates(resources, parentProcessStorages); 260 261 await SpecialPowers.spawn( 262 gBrowser.selectedBrowser, 263 [ALT_DOMAIN_SECURED], 264 secured => { 265 const doc = content.document; 266 267 const iframe = doc.createElement("iframe"); 268 iframe.src = secured + "storage-secured-iframe.html"; 269 270 doc.querySelector("body").appendChild(iframe); 271 } 272 ); 273 274 info("Wait for all resources"); 275 const { newResources, updates } = await onResources; 276 info("Wait for all updates"); 277 const previousResourceUpdates = await onUpdates; 278 279 for (const resourceType of contentProcessStorages) { 280 const resource = newResources[resourceType]; 281 const expected = afterIframeAdded[resourceType]; 282 // The resource only comes with hosts, without any values. 283 // Each host will be an empty array. 284 Assert.deepEqual( 285 Object.keys(resource.hosts), 286 Object.keys(expected), 287 `List of hosts for resource ${resourceType} is correct` 288 ); 289 for (const host in resource.hosts) { 290 is( 291 resource.hosts[host].length, 292 0, 293 "For new resources, each host has no value and is an empty array" 294 ); 295 } 296 const update = updates[resourceType]; 297 const storageKey = resourceTypeToStorageKey(resourceType); 298 Assert.deepEqual( 299 update.added[storageKey], 300 expected, 301 "We get an update after the resource, with the host values" 302 ); 303 } 304 305 for (const resourceType of parentProcessStorages) { 306 const expected = afterIframeAdded[resourceType]; 307 const update = previousResourceUpdates[resourceType]; 308 const storageKey = resourceTypeToStorageKey(resourceType); 309 Assert.deepEqual( 310 update.added[storageKey], 311 expected, 312 `We get an update after the resource ${resourceType}, with the host values` 313 ); 314 } 315 316 return newResources; 317 } 318 319 async function testRemoveIframe( 320 commands, 321 resources, 322 { contentProcessStorages, parentProcessStorages } 323 ) { 324 info("Testing if iframe removal works properly"); 325 326 // We only get updates for parent process storages, content process storage 327 // resources are wiped via their related target destruction. 328 const onUpdates = waitForResourceUpdates(resources, parentProcessStorages); 329 330 await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { 331 for (const iframe of content.document.querySelectorAll("iframe")) { 332 if (iframe.src.startsWith("http:")) { 333 iframe.remove(); 334 break; 335 } 336 } 337 }); 338 339 info("Wait for all updates"); 340 const previousResourceUpdates = await onUpdates; 341 342 for (const resourceType of parentProcessStorages) { 343 const expected = afterIframeRemoved[resourceType]; 344 const update = previousResourceUpdates[resourceType]; 345 const storageKey = resourceTypeToStorageKey(resourceType); 346 Assert.deepEqual( 347 update.deleted[storageKey], 348 expected, 349 `We get an update after the resource ${resourceType}, with the host values` 350 ); 351 } 352 353 // The iframe target is destroyed, which ends up destroying related resources 354 const destroyedResourceTypes = []; 355 for (const storageType in resources) { 356 for (const resource of resources[storageType]) { 357 if (resource.isDestroyed()) { 358 destroyedResourceTypes.push(resource.resourceType); 359 } 360 } 361 } 362 Assert.deepEqual( 363 destroyedResourceTypes.sort(), 364 contentProcessStorages.sort(), 365 "Content process storage resources have been destroyed [local and session storages]" 366 ); 367 } 368 369 /** 370 * single-store-update emits objects using attributes with old "storage key" namings, 371 * which is different from resource type namings. 372 */ 373 function resourceTypeToStorageKey(resourceType) { 374 if (resourceType == "local-storage") { 375 return "localStorage"; 376 } 377 if (resourceType == "session-storage") { 378 return "sessionStorage"; 379 } 380 if (resourceType == "indexed-db") { 381 return "indexedDB"; 382 } 383 return resourceType; 384 }