browser_target_command_frames.js (20583B)
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 around frames 7 8 const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document.html"; 9 const IFRAME_URL = URL_ROOT_ORG_SSL + "fission_iframe.html"; 10 const SECOND_PAGE_URL = "https://example.org/document-builder.sjs?html=org"; 11 12 const PID_REGEXP = /^\d+$/; 13 14 add_task(async function () { 15 // Disable bfcache for Fission for now. 16 // If Fission is disabled, the pref is no-op. 17 await SpecialPowers.pushPrefEnv({ 18 set: [["fission.bfcacheInParent", false]], 19 }); 20 21 // Enabled fission prefs 22 await pushPref("devtools.browsertoolbox.scope", "everything"); 23 // Disable the preloaded process as it gets created lazily and may interfere 24 // with process count assertions 25 await pushPref("dom.ipc.processPrelaunch.enabled", false); 26 // This preference helps destroying the content process when we close the tab 27 await pushPref("dom.ipc.keepProcessesAlive.web", 1); 28 29 // Test fetching the frames from the main process descriptor 30 await testBrowserFrames(); 31 32 // Test fetching the frames from a tab descriptor 33 await testTabFrames(); 34 35 // Test what happens with documents running in the parent process 36 await testOpeningOnParentProcessDocument(); 37 await testNavigationToParentProcessDocument(); 38 39 // Test what happens with about:blank documents 40 await testOpeningOnAboutBlankDocument(); 41 await testNavigationToAboutBlankDocument(); 42 43 await testNestedIframes(); 44 }); 45 46 async function testOpeningOnParentProcessDocument() { 47 info("Test opening against a parent process document"); 48 const tab = await addTab("about:robots"); 49 is( 50 tab.linkedBrowser.browsingContext.currentWindowGlobal.osPid, 51 -1, 52 "The tab is loaded in the parent process" 53 ); 54 55 const commands = await CommandsFactory.forTab(tab); 56 const targetCommand = commands.targetCommand; 57 await targetCommand.startListening(); 58 59 const frames = await targetCommand.getAllTargets([targetCommand.TYPES.FRAME]); 60 is(frames.length, 1); 61 is(frames[0].url, "about:robots", "target url is correct"); 62 is( 63 frames[0], 64 targetCommand.targetFront, 65 "the target is the current top level one" 66 ); 67 68 await commands.destroy(); 69 } 70 71 async function testNavigationToParentProcessDocument() { 72 info("Test navigating to parent process document"); 73 const firstLocation = "data:text/html,foo"; 74 const secondLocation = "about:robots"; 75 76 const tab = await addTab(firstLocation); 77 const commands = await CommandsFactory.forTab(tab); 78 const targetCommand = commands.targetCommand; 79 // When the first top level target is created from the server, 80 // `startListening` emits a spurious switched-target event 81 // which isn't necessarily emited before it resolves. 82 // So ensure waiting for it, otherwise we may resolve too eagerly 83 // in our expected listener. 84 const onSwitchedTarget1 = targetCommand.once("switched-target"); 85 await targetCommand.startListening(); 86 info("wait for first top level target"); 87 await onSwitchedTarget1; 88 89 const firstTarget = targetCommand.targetFront; 90 is(firstTarget.url, firstLocation, "first target url is correct"); 91 92 info("Navigate to a parent process page"); 93 const onSwitchedTarget = targetCommand.once("switched-target"); 94 const browser = tab.linkedBrowser; 95 const onLoaded = BrowserTestUtils.browserLoaded(browser); 96 BrowserTestUtils.startLoadingURIString(browser, secondLocation); 97 await onLoaded; 98 is( 99 browser.browsingContext.currentWindowGlobal.osPid, 100 -1, 101 "The tab is loaded in the parent process" 102 ); 103 104 await onSwitchedTarget; 105 isnot(targetCommand.targetFront, firstTarget, "got a new target"); 106 107 // Check that calling getAllTargets([frame]) return the same target instances 108 const frames = await targetCommand.getAllTargets([targetCommand.TYPES.FRAME]); 109 is(frames.length, 1); 110 is(frames[0].url, secondLocation, "second target url is correct"); 111 is( 112 frames[0], 113 targetCommand.targetFront, 114 "second target is the current top level one" 115 ); 116 117 await commands.destroy(); 118 } 119 120 async function testOpeningOnAboutBlankDocument() { 121 info("Test opening against about:blank document"); 122 const tab = await addTab("about:blank"); 123 124 const commands = await CommandsFactory.forTab(tab); 125 const targetCommand = commands.targetCommand; 126 await targetCommand.startListening(); 127 128 const frames = await targetCommand.getAllTargets([targetCommand.TYPES.FRAME]); 129 is(frames.length, 1); 130 is(frames[0].url, "about:blank", "target url is correct"); 131 is( 132 frames[0], 133 targetCommand.targetFront, 134 "the target is the current top level one" 135 ); 136 137 await commands.destroy(); 138 } 139 140 async function testNavigationToAboutBlankDocument() { 141 info("Test navigating to about:blank"); 142 const firstLocation = "data:text/html,foo"; 143 const secondLocation = "about:blank"; 144 145 const tab = await addTab(firstLocation); 146 const commands = await CommandsFactory.forTab(tab); 147 const targetCommand = commands.targetCommand; 148 // When the first top level target is created from the server, 149 // `startListening` emits a spurious switched-target event 150 // which isn't necessarily emited before it resolves. 151 // So ensure waiting for it, otherwise we may resolve too eagerly 152 // in our expected listener. 153 const onSwitchedTarget1 = targetCommand.once("switched-target"); 154 await targetCommand.startListening(); 155 info("wait for first top level target"); 156 await onSwitchedTarget1; 157 158 const firstTarget = targetCommand.targetFront; 159 is(firstTarget.url, firstLocation, "first target url is correct"); 160 161 info("Navigate to about:blank page"); 162 const onSwitchedTarget = targetCommand.once("switched-target"); 163 const browser = tab.linkedBrowser; 164 const onLoaded = BrowserTestUtils.browserLoaded(browser, { 165 wantLoad: secondLocation, 166 }); 167 BrowserTestUtils.startLoadingURIString(browser, secondLocation); 168 await onLoaded; 169 170 await onSwitchedTarget; 171 isnot(targetCommand.targetFront, firstTarget, "got a new target"); 172 173 // Check that calling getAllTargets([frame]) return the same target instances 174 const frames = await targetCommand.getAllTargets([targetCommand.TYPES.FRAME]); 175 is(frames.length, 1); 176 is(frames[0].url, secondLocation, "second target url is correct"); 177 is( 178 frames[0], 179 targetCommand.targetFront, 180 "second target is the current top level one" 181 ); 182 183 await commands.destroy(); 184 } 185 186 async function testBrowserFrames() { 187 info("Test TargetCommand against frames via the parent process target"); 188 189 const aboutBlankTab = await addTab("about:blank"); 190 191 const commands = await CommandsFactory.forMainProcess(); 192 const targetCommand = commands.targetCommand; 193 const { TYPES } = targetCommand; 194 await targetCommand.startListening(); 195 196 async function getAllFrameTargets() { 197 const targets = await targetCommand.getAllTargets([TYPES.FRAME]); 198 199 // Some extensions may be running and lead to the creation 200 // of some unexpected addon targets. 201 return targets.filter(t => !t.addonId); 202 } 203 204 // Very naive sanity check against getAllTargets([frame]) 205 const frames = await getAllFrameTargets(); 206 207 const hasBrowserDocument = frames.find( 208 frameTarget => frameTarget.url == window.location.href 209 ); 210 ok(hasBrowserDocument, "retrieve the target for the browser document"); 211 212 const hasAboutBlankDocument = frames.find( 213 frameTarget => 214 frameTarget.browsingContextID == 215 aboutBlankTab.linkedBrowser.browsingContext.id 216 ); 217 ok(hasAboutBlankDocument, "retrieve the target for the about:blank tab"); 218 219 // Check that calling getAllTargets([frame]) return the same target instances 220 const frames2 = await getAllFrameTargets(); 221 222 is(frames2.length, frames.length, "retrieved the same number of frames"); 223 224 function sortFronts(f1, f2) { 225 return f1.actorID < f2.actorID; 226 } 227 frames.sort(sortFronts); 228 frames2.sort(sortFronts); 229 for (let i = 0; i < frames.length; i++) { 230 is(frames[i], frames2[i], `frame ${i} targets are the same`); 231 } 232 233 // Assert that watchTargets will call the create callback for all existing frames 234 const targets = []; 235 const topLevelTarget = targetCommand.targetFront; 236 237 const noParentTarget = await topLevelTarget.getParentTarget(); 238 is(noParentTarget, null, "The top level target has no parent target"); 239 240 const onAvailable = ({ targetFront }) => { 241 is( 242 targetFront.targetType, 243 TYPES.FRAME, 244 "We are only notified about frame targets" 245 ); 246 ok( 247 targetFront == topLevelTarget 248 ? targetFront.isTopLevel 249 : !targetFront.isTopLevel, 250 "isTopLevel property is correct" 251 ); 252 ok( 253 PID_REGEXP.test(targetFront.processID), 254 `Target has processID of expected shape (${targetFront.processID})` 255 ); 256 if (!targetFront.addonId) { 257 targets.push(targetFront); 258 } 259 }; 260 await targetCommand.watchTargets({ types: [TYPES.FRAME], onAvailable }); 261 262 is( 263 targets.length, 264 frames.length, 265 "retrieved the same number of frames via watchTargets" 266 ); 267 268 frames.sort(sortFronts); 269 targets.sort(sortFronts); 270 for (let i = 0; i < frames.length; i++) { 271 is( 272 frames[i], 273 targets[i], 274 `frame ${i} targets are the same via watchTargets` 275 ); 276 } 277 278 async function addTabAndAssertNewTarget(url) { 279 const previousTargetCount = targets.length; 280 const tab = await addTab(url); 281 await waitFor( 282 () => targets.length == previousTargetCount + 1, 283 "Wait for all expected targets after tab opening" 284 ); 285 is( 286 targets.length, 287 previousTargetCount + 1, 288 "Opening a tab reported a new frame" 289 ); 290 const newTabTarget = targets.at(-1); 291 is(newTabTarget.url, url, "This frame target is about the new tab"); 292 // Internaly, the tab, which uses a <browser type='content'> element is considered detached from their owner document 293 // and so the target is having a null parentInnerWindowId. But the framework will attach all non-top-level targets 294 // as children of the top level. 295 const tabParentTarget = await newTabTarget.getParentTarget(); 296 is( 297 tabParentTarget, 298 targetCommand.targetFront, 299 "tab's WindowGlobal/BrowsingContext is detached and has no parent, but we report them as children of the top level target" 300 ); 301 302 const frames3 = await getAllFrameTargets(); 303 const hasTabDocument = frames3.find(target => target.url == url); 304 ok(hasTabDocument, "retrieve the target for tab via getAllTargets"); 305 306 return tab; 307 } 308 309 info("Open a tab loaded in content process"); 310 await addTabAndAssertNewTarget("data:text/html,content-process-page"); 311 312 info("Open a tab loaded in the parent process"); 313 const parentProcessTab = await addTabAndAssertNewTarget("about:robots"); 314 is( 315 parentProcessTab.linkedBrowser.browsingContext.currentWindowGlobal.osPid, 316 -1, 317 "The tab is loaded in the parent process" 318 ); 319 320 info("Open a new content window via window.open"); 321 info("First open a tab on .org domain"); 322 const tabUrl = "https://example.org/document-builder.sjs?html=org"; 323 await addTabAndAssertNewTarget(tabUrl); 324 const previousTargetCount = targets.length; 325 326 info("Then open a popup on .com domain"); 327 const popupUrl = "https://example.com/document-builder.sjs?html=com"; 328 const onPopupOpened = BrowserTestUtils.waitForNewTab(gBrowser, popupUrl); 329 await SpecialPowers.spawn(gBrowser.selectedBrowser, [popupUrl], async url => { 330 content.window.open(url, "_blank"); 331 }); 332 await onPopupOpened; 333 334 await waitFor( 335 () => targets.length == previousTargetCount + 1, 336 "Wait for all expected targets after window.open()" 337 ); 338 is( 339 targets.length, 340 previousTargetCount + 1, 341 "Opening a new content window reported a new frame" 342 ); 343 is( 344 targets.at(-1).url, 345 popupUrl, 346 "This frame target is about the new content window" 347 ); 348 349 // About:blank are a bit special because we ignore a transcient about:blank 350 // document when navigating to another process. But we should not ignore 351 // tabs, loading a real, final about:blank document. 352 info("Open a tab with about:blank"); 353 await addTabAndAssertNewTarget("about:blank"); 354 355 // Until we start spawning target for all WindowGlobals, 356 // including the one running in the same process as their parent, 357 // we won't create dedicated target for new top level windows. 358 // Instead, these document will be debugged via the ParentProcessTargetActor. 359 info("Open a top level chrome window"); 360 const expectedTargets = targets.length; 361 const chromeWindow = Services.ww.openWindow( 362 null, 363 "about:robots", 364 "_blank", 365 "chrome", 366 null 367 ); 368 await wait(250); 369 is( 370 targets.length, 371 expectedTargets, 372 "New top level window shouldn't spawn new target" 373 ); 374 chromeWindow.close(); 375 376 targetCommand.unwatchTargets({ types: [TYPES.FRAME], onAvailable }); 377 378 targetCommand.destroy(); 379 await waitForAllTargetsToBeAttached(targetCommand); 380 381 await commands.destroy(); 382 } 383 384 async function testTabFrames() { 385 info("Test TargetCommand against frames via a tab target"); 386 387 // Create a TargetCommand for a given test tab 388 const tab = await addTab(FISSION_TEST_URL); 389 const commands = await CommandsFactory.forTab(tab); 390 const targetCommand = commands.targetCommand; 391 const { TYPES } = targetCommand; 392 393 await targetCommand.startListening(); 394 395 // Check that calling getAllTargets([frame]) return the same target instances 396 const frames = await targetCommand.getAllTargets([TYPES.FRAME]); 397 // When fission is enabled, we also get the remote example.org iframe. 398 const expectedFramesCount = 2; 399 is( 400 frames.length, 401 expectedFramesCount, 402 "retrieved the expected number of targets" 403 ); 404 405 // Assert that watchTargets will call the create callback for all existing frames 406 const targets = []; 407 const destroyedTargets = []; 408 const topLevelTarget = targetCommand.targetFront; 409 const onAvailable = ({ targetFront, isTargetSwitching }) => { 410 is( 411 targetFront.targetType, 412 TYPES.FRAME, 413 "We are only notified about frame targets" 414 ); 415 ok( 416 PID_REGEXP.test(targetFront.processID), 417 `Target has processID of expected shape (${targetFront.processID})` 418 ); 419 targets.push({ targetFront, isTargetSwitching }); 420 }; 421 const onDestroyed = ({ targetFront, isTargetSwitching }) => { 422 is( 423 targetFront.targetType, 424 TYPES.FRAME, 425 "We are only notified about frame targets" 426 ); 427 ok( 428 targetFront == topLevelTarget 429 ? targetFront.isTopLevel 430 : !targetFront.isTopLevel, 431 "isTopLevel property is correct" 432 ); 433 destroyedTargets.push({ targetFront, isTargetSwitching }); 434 }; 435 await targetCommand.watchTargets({ 436 types: [TYPES.FRAME], 437 onAvailable, 438 onDestroyed, 439 }); 440 is( 441 targets.length, 442 frames.length, 443 "retrieved the same number of frames via watchTargets" 444 ); 445 is(destroyedTargets.length, 0, "Should be no destroyed target initialy"); 446 447 for (const frame of frames) { 448 ok( 449 targets.find(({ targetFront }) => targetFront === frame), 450 "frame " + frame.actorID + " target is the same via watchTargets" 451 ); 452 } 453 is( 454 targets[0].targetFront.url, 455 FISSION_TEST_URL, 456 "First target should be the top document one" 457 ); 458 is( 459 targets[0].targetFront.isTopLevel, 460 true, 461 "First target is a top level one" 462 ); 463 is( 464 !targets[0].isTargetSwitching, 465 true, 466 "First target is not considered as a target switching" 467 ); 468 const noParentTarget = await targets[0].targetFront.getParentTarget(); 469 is(noParentTarget, null, "The top level target has no parent target"); 470 471 is( 472 targets[1].targetFront.url, 473 IFRAME_URL, 474 "Second target should be the iframe one" 475 ); 476 is(!targets[1].targetFront.isTopLevel, true, "Iframe target isn't top level"); 477 is(!targets[1].isTargetSwitching, true, "Iframe target isn't a target swich"); 478 const parentTarget = await targets[1].targetFront.getParentTarget(); 479 is( 480 parentTarget, 481 targets[0].targetFront, 482 "The parent target for the iframe is the top level target" 483 ); 484 485 // Before navigating to another process, ensure cleaning up everything from the first page 486 await waitForAllTargetsToBeAttached(targetCommand); 487 await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { 488 // registrationPromise is set by the test page. 489 const registration = await content.wrappedJSObject.registrationPromise; 490 registration.unregister(); 491 }); 492 493 info("Navigate to another domain and process (if fission is enabled)"); 494 // When a new target will be created, we need to wait until it's fully processed 495 // to avoid pending promises. 496 const onNewTargetProcessed = targetCommand.once("processed-available-target"); 497 498 const browser = tab.linkedBrowser; 499 const onLoaded = BrowserTestUtils.browserLoaded(browser); 500 BrowserTestUtils.startLoadingURIString(browser, SECOND_PAGE_URL); 501 await onLoaded; 502 503 const afterNavigationFramesCount = 3; 504 await waitFor( 505 () => targets.length == afterNavigationFramesCount, 506 "Wait for all expected targets after navigation" 507 ); 508 is( 509 targets.length, 510 afterNavigationFramesCount, 511 "retrieved all targets after navigation" 512 ); 513 // As targetFront.url isn't reliable and might be about:blank, 514 // try to assert that we got the right target via other means. 515 // outerWindowID should change when navigating to another process, 516 // while it would stay equal for in-process navigations. 517 is( 518 targets[2].targetFront.outerWindowID, 519 browser.outerWindowID, 520 "The new target should be the newly loaded document" 521 ); 522 is( 523 targets[2].isTargetSwitching, 524 true, 525 "and should be flagged as a target switching" 526 ); 527 528 is( 529 destroyedTargets.length, 530 2, 531 "The two existing targets should be destroyed" 532 ); 533 const iframeDestroyedTarget = destroyedTargets.find( 534 target => target.targetFront === targets[1].targetFront 535 ); 536 ok( 537 iframeDestroyedTarget, 538 "Received the destroyed target notification for the iframe" 539 ); 540 is( 541 iframeDestroyedTarget.isTargetSwitching, 542 false, 543 "the target destruction is not flagged as target switching for iframes" 544 ); 545 const topLevelDestroyedTarget = destroyedTargets.find( 546 target => target.targetFront === targets[0].targetFront 547 ); 548 ok( 549 topLevelDestroyedTarget, 550 "Received the destroyed target notification for the top level frame" 551 ); 552 is( 553 topLevelDestroyedTarget.isTargetSwitching, 554 true, 555 "the target destruction is flagged as target switching" 556 ); 557 558 await onNewTargetProcessed; 559 560 targetCommand.unwatchTargets({ types: [TYPES.FRAME], onAvailable }); 561 562 targetCommand.destroy(); 563 564 BrowserTestUtils.removeTab(tab); 565 566 await commands.destroy(); 567 } 568 569 async function testNestedIframes() { 570 info("Test TargetCommand against nested frames"); 571 572 const nestedIframeUrl = `https://example.com/document-builder.sjs?html=${encodeURIComponent( 573 "<title>second</title><h3>second level iframe</h3>" 574 )}&delay=500`; 575 576 // addTab will wait for this specific url so need to use URL to serialize correctly 577 const testUrl = new URL(`data:text/html;charset=utf-8, 578 <h1>Top-level</h1> 579 <iframe id=first-level 580 src='data:text/html;charset=utf-8,${encodeURIComponent( 581 `<title>first</title><h2>first level iframe</h2><iframe id=second-level src="${nestedIframeUrl}"></iframe>` 582 )}' 583 ></iframe>`).href; 584 585 // Create a TargetCommand for a given test tab 586 const tab = await addTab(testUrl); 587 const commands = await CommandsFactory.forTab(tab); 588 const targetCommand = commands.targetCommand; 589 const { TYPES } = targetCommand; 590 591 await targetCommand.startListening(); 592 593 // Check that calling getAllTargets([frame]) return the same target instances 594 const frames = await targetCommand.getAllTargets([TYPES.FRAME]); 595 596 is(frames[0], targetCommand.targetFront, "First target is the top level one"); 597 const topParent = await frames[0].getParentTarget(); 598 is(topParent, null, "Top level target has no parent"); 599 600 const firstIframeTarget = frames.find(target => target.title == "first"); 601 ok(firstIframeTarget, "Got the target for the first level iframe"); 602 const firstParent = await firstIframeTarget.getParentTarget(); 603 is( 604 firstParent, 605 targetCommand.targetFront, 606 "First level has top level target as parent" 607 ); 608 609 const secondIframeTarget = frames.find(target => target.title == "second"); 610 ok(secondIframeTarget, "Got the target for the second level iframe"); 611 const secondParent = await secondIframeTarget.getParentTarget(); 612 is( 613 secondParent, 614 firstIframeTarget, 615 "Second level has the first level target as parent" 616 ); 617 618 await commands.destroy(); 619 }