browser_target_command_bfcache.js (15049B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 // Test the TargetCommand API when bfcache navigations happen 7 8 const TEST_COM_URL = URL_ROOT_SSL + "simple_document.html"; 9 10 add_task(async function () { 11 // Disable the preloaded process as it gets created lazily and may interfere 12 // with process count assertions 13 await pushPref("dom.ipc.processPrelaunch.enabled", false); 14 // This preference helps destroying the content process when we close the tab 15 await pushPref("dom.ipc.keepProcessesAlive.web", 1); 16 17 info("## Test with bfcache in parent DISABLED"); 18 await pushPref("fission.bfcacheInParent", false); 19 await testTopLevelNavigations(false); 20 await testIframeNavigations(false); 21 await testTopLevelNavigationsOnDocumentWithIframe(false); 22 23 // bfcacheInParent only works if sessionHistoryInParent is enable 24 // so only test it if both settings are enabled. 25 // (it looks like sessionHistoryInParent is enabled by default when fission is enabled) 26 if (Services.appinfo.sessionHistoryInParent) { 27 info("## Test with bfcache in parent ENABLED"); 28 await pushPref("fission.bfcacheInParent", true); 29 await testTopLevelNavigations(true); 30 await testIframeNavigations(true); 31 await testTopLevelNavigationsOnDocumentWithIframe(true); 32 } 33 }); 34 35 async function testTopLevelNavigations(bfcacheInParent) { 36 info(" # Test TOP LEVEL navigations"); 37 // Create a TargetCommand for a given test tab 38 const tab = await addTab(TEST_COM_URL); 39 const commands = await CommandsFactory.forTab(tab); 40 const targetCommand = commands.targetCommand; 41 const { TYPES } = targetCommand; 42 43 await targetCommand.startListening(); 44 45 // Assert that watchTargets will call the create callback for all existing frames 46 const targets = []; 47 const onAvailable = async ({ targetFront }) => { 48 is( 49 targetFront.targetType, 50 TYPES.FRAME, 51 "We are only notified about frame targets" 52 ); 53 ok(targetFront.isTopLevel, "all targets of this test are top level"); 54 targets.push(targetFront); 55 }; 56 const destroyedTargets = []; 57 const onDestroyed = async ({ targetFront }) => { 58 is( 59 targetFront.targetType, 60 TYPES.FRAME, 61 "We are only notified about frame targets" 62 ); 63 ok(targetFront.isTopLevel, "all targets of this test are top level"); 64 destroyedTargets.push(targetFront); 65 }; 66 67 await targetCommand.watchTargets({ 68 types: [TYPES.FRAME], 69 onAvailable, 70 onDestroyed, 71 }); 72 is(targets.length, 1, "retrieved only the top level target"); 73 is(targets[0], targetCommand.targetFront, "the target is the top level one"); 74 is( 75 destroyedTargets.length, 76 0, 77 "We get no destruction when calling watchTargets" 78 ); 79 ok( 80 targets[0].targetForm.followWindowGlobalLifeCycle, 81 "the first server side target follows the WindowGlobal lifecycle, when server target switching is enabled" 82 ); 83 84 // Navigate to the same page with query params 85 info("Load the second page"); 86 const secondPageUrl = TEST_COM_URL + "?second-load"; 87 const previousBrowsingContextID = gBrowser.selectedBrowser.browsingContext.id; 88 ok( 89 previousBrowsingContextID, 90 "Fetch the tab's browsing context id before navigation" 91 ); 92 const onLoaded = BrowserTestUtils.browserLoaded( 93 gBrowser.selectedBrowser, 94 false, 95 secondPageUrl 96 ); 97 BrowserTestUtils.startLoadingURIString( 98 gBrowser.selectedBrowser, 99 secondPageUrl 100 ); 101 await onLoaded; 102 103 // Assert BrowsingContext changes as it impact the behavior of targets 104 if (bfcacheInParent) { 105 isnot( 106 previousBrowsingContextID, 107 gBrowser.selectedBrowser.browsingContext.id, 108 "When bfcacheInParent is enabled, same-origin navigations spawn new BrowsingContext" 109 ); 110 } else { 111 is( 112 previousBrowsingContextID, 113 gBrowser.selectedBrowser.browsingContext.id, 114 "When bfcacheInParent is disabled, same-origin navigations re-use the same BrowsingContext" 115 ); 116 } 117 118 // Same-origin navigations also spawn a new top level target 119 await waitFor( 120 () => targets.length == 2, 121 "wait for the next top level target" 122 ); 123 is( 124 targets[1], 125 targetCommand.targetFront, 126 "the second target is the top level one" 127 ); 128 // As targetFront.url isn't reliable and might be about:blank, 129 // try to assert that we got the right target via other means. 130 // outerWindowID should change when navigating to another process, 131 // while it would stay equal for in-process navigations. 132 is( 133 targets[1].outerWindowID, 134 gBrowser.selectedBrowser.outerWindowID, 135 "the second target is for the second page" 136 ); 137 ok( 138 targets[1].targetForm.followWindowGlobalLifeCycle, 139 "the new server side target follows the WindowGlobal lifecycle" 140 ); 141 ok(targets[0].isDestroyed(), "the first target is destroyed"); 142 is(destroyedTargets.length, 1, "We get one target being destroyed..."); 143 is(destroyedTargets[0], targets[0], "...and that's the first one"); 144 145 // Go back to the first page, this should be a bfcache navigation, and, 146 // we should get a new target 147 info("Go back to the first page"); 148 const onPageShow = BrowserTestUtils.waitForContentEvent( 149 gBrowser.selectedBrowser, 150 "pageshow" 151 ); 152 gBrowser.selectedBrowser.goBack(); 153 await onPageShow; 154 155 await waitFor( 156 () => targets.length == 3, 157 "wait for the next top level target" 158 ); 159 is( 160 targets[2], 161 targetCommand.targetFront, 162 "the third target is the top level one" 163 ); 164 // Here as this is revived from cache, the url should always be correct 165 is(targets[2].url, TEST_COM_URL, "the third target is for the first url"); 166 ok( 167 targets[2].targetForm.followWindowGlobalLifeCycle, 168 "the third target for bfcache navigations is following the WindowGlobal lifecycle" 169 ); 170 ok(targets[1].isDestroyed(), "the second target is destroyed"); 171 is( 172 destroyedTargets.length, 173 2, 174 "We get one additional target being destroyed..." 175 ); 176 is(destroyedTargets[1], targets[1], "...and that's the second one"); 177 178 // Wait for full attach in order to having breaking any pending requests 179 // when navigating to another page and switching to new process and target. 180 await waitForAllTargetsToBeAttached(targetCommand); 181 182 // Go forward and resurect the second page, this should also be a bfcache navigation, and, 183 // get a new target. 184 info("Go forward to the second page"); 185 186 // When a new target will be created, we need to wait until it's fully processed 187 // to avoid pending promises. 188 const onNewTargetProcessed = bfcacheInParent 189 ? new Promise(resolve => { 190 targetCommand.on( 191 "processed-available-target", 192 function onProcessedAvailableTarget(targetFront) { 193 if (targetFront === targets[3]) { 194 resolve(); 195 targetCommand.off( 196 "processed-available-target", 197 onProcessedAvailableTarget 198 ); 199 } 200 } 201 ); 202 }) 203 : null; 204 205 gBrowser.selectedBrowser.goForward(); 206 207 await waitFor( 208 () => targets.length == 4, 209 "wait for the next top level target" 210 ); 211 is( 212 targets[3], 213 targetCommand.targetFront, 214 "the 4th target is the top level one" 215 ); 216 // Same here, as the document is revived from the cache, the url should always be correct 217 is(targets[3].url, secondPageUrl, "the 4th target is for the second url"); 218 ok( 219 targets[3].targetForm.followWindowGlobalLifeCycle, 220 "the 4th target for bfcache navigations is following the WindowGlobal lifecycle" 221 ); 222 ok(targets[2].isDestroyed(), "the third target is destroyed"); 223 is( 224 destroyedTargets.length, 225 3, 226 "We get one additional target being destroyed..." 227 ); 228 is(destroyedTargets[2], targets[2], "...and that's the third one"); 229 230 // Wait for full attach in order to having breaking any pending requests 231 // when navigating to another page and switching to new process and target. 232 await waitForAllTargetsToBeAttached(targetCommand); 233 await onNewTargetProcessed; 234 235 await waitForAllTargetsToBeAttached(targetCommand); 236 237 targetCommand.unwatchTargets({ types: [TYPES.FRAME], onAvailable }); 238 239 BrowserTestUtils.removeTab(tab); 240 241 await commands.destroy(); 242 } 243 244 async function testTopLevelNavigationsOnDocumentWithIframe() { 245 info(" # Test TOP LEVEL navigations on document with iframe"); 246 // Create a TargetCommand for a given test tab 247 const tab = 248 await addTab(`https://example.com/document-builder.sjs?id=top&html= 249 <h1>Top level</h1> 250 <iframe src="${encodeURIComponent( 251 "https://example.com/document-builder.sjs?id=iframe&html=<h2>In iframe</h2>" 252 )}"> 253 </iframe>`); 254 const getLocationIdParam = url => 255 new URLSearchParams(new URL(url).search).get("id"); 256 257 const commands = await CommandsFactory.forTab(tab); 258 const targetCommand = commands.targetCommand; 259 const { TYPES } = targetCommand; 260 261 await targetCommand.startListening(); 262 263 // Assert that watchTargets will call the create callback for all existing frames 264 const targets = []; 265 const onAvailable = async ({ targetFront }) => { 266 is( 267 targetFront.targetType, 268 TYPES.FRAME, 269 "We are only notified about frame targets" 270 ); 271 targets.push(targetFront); 272 }; 273 const destroyedTargets = []; 274 const onDestroyed = async ({ targetFront }) => { 275 is( 276 targetFront.targetType, 277 TYPES.FRAME, 278 "We are only notified about frame targets" 279 ); 280 destroyedTargets.push(targetFront); 281 }; 282 283 await targetCommand.watchTargets({ 284 types: [TYPES.FRAME], 285 onAvailable, 286 onDestroyed, 287 }); 288 289 is(targets.length, 2, "retrieved targets for top level and iframe documents"); 290 is(targets[0], targetCommand.targetFront, "the target is the top level one"); 291 is( 292 getLocationIdParam(targets[1].url), 293 "iframe", 294 "the second target is the iframe one" 295 ); 296 297 is( 298 destroyedTargets.length, 299 0, 300 "We get no destruction when calling watchTargets" 301 ); 302 303 info("Navigate to a new page"); 304 let targetCountBeforeNavigation = targets.length; 305 const secondPageUrl = `https://example.com/document-builder.sjs?html=second`; 306 const onLoaded = BrowserTestUtils.browserLoaded( 307 gBrowser.selectedBrowser, 308 false, 309 secondPageUrl 310 ); 311 BrowserTestUtils.startLoadingURIString( 312 gBrowser.selectedBrowser, 313 secondPageUrl 314 ); 315 await onLoaded; 316 317 // Same-origin navigations also spawn a new top level target 318 await waitFor( 319 () => targets.length == targetCountBeforeNavigation + 1, 320 "wait for the next top level target" 321 ); 322 is( 323 targets.at(-1), 324 targetCommand.targetFront, 325 "the new target is the top level one" 326 ); 327 328 ok(targets[0].isDestroyed(), "the first target is destroyed"); 329 ok(targets[1].isDestroyed(), "the second target is destroyed"); 330 is(destroyedTargets.length, 2, "The two targets were destroyed"); 331 332 // Go back to the first page, this should be a bfcache navigation, and, 333 // we should get 2 new targets 334 targetCountBeforeNavigation = targets.length; 335 info("Go back to the first page"); 336 gBrowser.selectedBrowser.goBack(); 337 338 await waitFor( 339 () => targets.length === targetCountBeforeNavigation + 2, 340 "wait for the next targets" 341 ); 342 343 await waitFor(() => targets.at(-2).url && targets.at(-1).url); 344 is( 345 getLocationIdParam(targets.at(-2).url), 346 "top", 347 "the first new target is for the top document…" 348 ); 349 is( 350 getLocationIdParam(targets.at(-1).url), 351 "iframe", 352 "…and the second one is for the iframe" 353 ); 354 355 ok( 356 targets[targetCountBeforeNavigation - 1].isDestroyed(), 357 "the target for the second page is destroyed" 358 ); 359 is( 360 destroyedTargets.length, 361 targetCountBeforeNavigation, 362 "We get one additional target being destroyed…" 363 ); 364 is( 365 destroyedTargets.at(-1), 366 targets[targetCountBeforeNavigation - 1], 367 "…and that's the second page one" 368 ); 369 370 await waitForAllTargetsToBeAttached(targetCommand); 371 372 targetCommand.unwatchTargets({ 373 types: [TYPES.FRAME], 374 onAvailable, 375 onDestroyed, 376 }); 377 378 BrowserTestUtils.removeTab(tab); 379 380 await commands.destroy(); 381 } 382 383 async function testIframeNavigations() { 384 info(" # Test IFRAME navigations"); 385 // Create a TargetCommand for a given test tab 386 const tab = await addTab( 387 `https://example.org/document-builder.sjs?html=<iframe src="${TEST_COM_URL}"></iframe>` 388 ); 389 const commands = await CommandsFactory.forTab(tab); 390 const targetCommand = commands.targetCommand; 391 const { TYPES } = targetCommand; 392 393 await targetCommand.startListening(); 394 395 // Assert that watchTargets will call the create callback for all existing frames 396 const targets = []; 397 const onAvailable = async ({ targetFront }) => { 398 is( 399 targetFront.targetType, 400 TYPES.FRAME, 401 "We are only notified about frame targets" 402 ); 403 targets.push(targetFront); 404 }; 405 await targetCommand.watchTargets({ types: [TYPES.FRAME], onAvailable }); 406 407 is(targets.length, 2, "retrieved the top level and the iframe targets"); 408 is( 409 targets[0], 410 targetCommand.targetFront, 411 "the first target is the top level one" 412 ); 413 is(targets[1].url, TEST_COM_URL, "the second target is the iframe one"); 414 415 // Navigate to the same page with query params 416 info("Load the second page"); 417 const secondPageUrl = TEST_COM_URL + "?second-load"; 418 await SpecialPowers.spawn( 419 gBrowser.selectedBrowser, 420 [secondPageUrl], 421 function (url) { 422 const iframe = content.document.querySelector("iframe"); 423 iframe.src = url; 424 } 425 ); 426 427 await waitFor(() => targets.length == 3, "wait for the next target"); 428 is(targets[2].url, secondPageUrl, "the second target is for the second url"); 429 ok(targets[1].isDestroyed(), "the first target is destroyed"); 430 431 // Go back to the first page, this should be a bfcache navigation, and, 432 // we should get a new target 433 info("Go back to the first page"); 434 const iframeBrowsingContext = await SpecialPowers.spawn( 435 gBrowser.selectedBrowser, 436 [], 437 function () { 438 const iframe = content.document.querySelector("iframe"); 439 return iframe.browsingContext; 440 } 441 ); 442 await SpecialPowers.spawn(iframeBrowsingContext, [], function () { 443 content.history.back(); 444 }); 445 446 await waitFor(() => targets.length == 4, "wait for the next target"); 447 is(targets[3].url, TEST_COM_URL, "the third target is for the first url"); 448 ok(targets[2].isDestroyed(), "the second target is destroyed"); 449 450 // Go forward and resurect the second page, this should also be a bfcache navigation, and, 451 // get a new target. 452 info("Go forward to the second page"); 453 await SpecialPowers.spawn(iframeBrowsingContext, [], function () { 454 content.history.forward(); 455 }); 456 457 await waitFor(() => targets.length == 5, "wait for the next target"); 458 is(targets[4].url, secondPageUrl, "the 4th target is for the second url"); 459 ok(targets[3].isDestroyed(), "the third target is destroyed"); 460 461 targetCommand.unwatchTargets({ types: [TYPES.FRAME], onAvailable }); 462 463 await waitForAllTargetsToBeAttached(targetCommand); 464 465 BrowserTestUtils.removeTab(tab); 466 467 await commands.destroy(); 468 }