tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }