tor-browser

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

browser_dbg-features-source-tree.js (18380B)


      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 /**
      6 * This test focuses on the SourceTree component, where we display all debuggable sources.
      7 *
      8 * The first two tests expand the tree via manual DOM events (first with clicks and second with keys).
      9 * `waitForSourcesInSourceTree()` is a key assertion method. Passing `{noExpand: true}`
     10 * is important to avoid automatically expand the source tree.
     11 *
     12 * The following tests depend on auto-expand and only assert all the sources possibly displayed
     13 */
     14 
     15 "use strict";
     16 
     17 const testServer = createVersionizedHttpTestServer(
     18  "../examples/sourcemaps-reload-uncompressed"
     19 );
     20 const TEST_URL = testServer.urlFor("index.html");
     21 
     22 /**
     23 * This test opens the SourceTree manually via click events on the nested source,
     24 * and then adds a source dynamically and asserts it is visible.
     25 */
     26 add_task(async function testSimpleSourcesWithManualClickExpand() {
     27  const dbg = await initDebugger(
     28    "doc-sources.html",
     29    "simple1.js",
     30    "simple2.js",
     31    "nested-source.js",
     32    "long.js"
     33  );
     34 
     35  // Expand nodes and make sure more sources appear.
     36  is(
     37    getSourceTreeLabel(dbg, 1),
     38    "Main Thread",
     39    "Main thread is labeled properly"
     40  );
     41  info("Before interacting with the source tree, no source are displayed");
     42  await waitForSourcesInSourceTree(dbg, [], { noExpand: true });
     43  await clickElement(dbg, "sourceDirectoryLabel", 3);
     44  info(
     45    "After clicking on the directory, all sources but the nested ones are displayed"
     46  );
     47  await waitForSourcesInSourceTree(
     48    dbg,
     49    ["doc-sources.html", "simple1.js", "simple2.js", "long.js"],
     50    { noExpand: true }
     51  );
     52 
     53  await clickElement(dbg, "sourceDirectoryLabel", 4);
     54  info(
     55    "After clicking on the nested directory, the nested source is also displayed"
     56  );
     57  await waitForSourcesInSourceTree(
     58    dbg,
     59    [
     60      "doc-sources.html",
     61      "simple1.js",
     62      "simple2.js",
     63      "long.js",
     64      "nested-source.js",
     65    ],
     66    { noExpand: true }
     67  );
     68 
     69  const selected = waitForDispatch(dbg.store, "SET_SELECTED_LOCATION");
     70  await clickElement(dbg, "sourceNode", 5);
     71  await selected;
     72  await waitForSelectedSource(dbg, "nested-source.js");
     73 
     74  // Ensure the source file clicked is now focused
     75  await waitForElementWithSelector(dbg, ".sources-list .focused");
     76 
     77  const selectedSource = dbg.selectors.getSelectedSource().url;
     78  ok(selectedSource.includes("nested-source.js"), "nested-source is selected");
     79  await assertNodeIsFocused(dbg, 5);
     80 
     81  // Make sure new sources appear in the list.
     82  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
     83    const script = content.document.createElement("script");
     84    script.src = "math.min.js";
     85    content.document.body.appendChild(script);
     86  });
     87 
     88  info("After adding math.min.js, we got a new source displayed");
     89  await waitForSourcesInSourceTree(
     90    dbg,
     91    [
     92      "doc-sources.html",
     93      "simple1.js",
     94      "simple2.js",
     95      "long.js",
     96      "nested-source.js",
     97      "math.min.js",
     98    ],
     99    { noExpand: true }
    100  );
    101  is(
    102    getSourceNodeLabel(dbg, 8),
    103    "math.min.js",
    104    "math.min.js - The dynamic script exists"
    105  );
    106 
    107  info("Assert that nested-source.js is still the selected source");
    108  await assertNodeIsFocused(dbg, 5);
    109 
    110  info("Test the copy to clipboard context menu");
    111  const mathMinTreeNode = findSourceNodeWithText(dbg, "math.min.js");
    112  await triggerSourceTreeContextMenu(
    113    dbg,
    114    mathMinTreeNode,
    115    "#node-menu-copy-source"
    116  );
    117  const clipboardData = SpecialPowers.getClipboardData("text/plain");
    118  is(
    119    clipboardData,
    120    EXAMPLE_URL + "math.min.js",
    121    "The clipboard content is the selected source URL"
    122  );
    123 
    124  info("Test the download file context menu");
    125  // Before trigerring the menu, mock the file picker
    126  const MockFilePicker = SpecialPowers.MockFilePicker;
    127  MockFilePicker.init(window.browsingContext);
    128  const nsiFile = new FileUtils.File(
    129    PathUtils.join(PathUtils.tempDir, `export_source_content_${Date.now()}.log`)
    130  );
    131  MockFilePicker.setFiles([nsiFile]);
    132  const path = nsiFile.path;
    133 
    134  await triggerSourceTreeContextMenu(
    135    dbg,
    136    mathMinTreeNode,
    137    "#node-menu-download-file"
    138  );
    139 
    140  info("Wait for the downloaded file to be fully saved to disk");
    141  await BrowserTestUtils.waitForCondition(() => IOUtils.exists(path));
    142  await BrowserTestUtils.waitForCondition(async () => {
    143    const { size } = await IOUtils.stat(path);
    144    return size > 0;
    145  });
    146  const buffer = await IOUtils.read(path);
    147  const savedFileContent = new TextDecoder().decode(buffer);
    148 
    149  const mathMinRequest = await fetch(EXAMPLE_URL + "math.min.js");
    150  const mathMinContent = await mathMinRequest.text();
    151 
    152  is(
    153    savedFileContent,
    154    mathMinContent,
    155    "The downloaded file has the expected content"
    156  );
    157 
    158  dbg.toolbox.closeToolbox();
    159 });
    160 
    161 /**
    162 * Test keyboard arrow behaviour on the SourceTree with a nested folder
    163 * that we manually expand/collapse via arrow keys.
    164 */
    165 add_task(async function testSimpleSourcesWithManualKeyShortcutsExpand() {
    166  const dbg = await initDebugger(
    167    "doc-sources.html",
    168    "simple1.js",
    169    "simple2.js",
    170    "nested-source.js",
    171    "long.js"
    172  );
    173 
    174  // Before clicking on the source label, no source is displayed
    175  await waitForSourcesInSourceTree(dbg, [], { noExpand: true });
    176  await clickElement(dbg, "sourceDirectoryLabel", 3);
    177  // Right after, all sources, but the nested one are displayed
    178  await waitForSourcesInSourceTree(
    179    dbg,
    180    ["doc-sources.html", "simple1.js", "simple2.js", "long.js"],
    181    { noExpand: true }
    182  );
    183 
    184  // Right key on open dir
    185  await pressKey(dbg, "Right");
    186  await assertNodeIsFocused(dbg, 3);
    187 
    188  // Right key on closed dir
    189  await pressKey(dbg, "Right");
    190  await assertNodeIsFocused(dbg, 4);
    191 
    192  // Left key on a open dir
    193  await pressKey(dbg, "Left");
    194  await assertNodeIsFocused(dbg, 4);
    195 
    196  // Down key on a closed dir
    197  await pressKey(dbg, "Down");
    198  await assertNodeIsFocused(dbg, 4);
    199 
    200  // Right key on a source
    201  // We are focused on the nested source and up to this point we still display only the 4 initial sources
    202  await waitForSourcesInSourceTree(
    203    dbg,
    204    ["doc-sources.html", "simple1.js", "simple2.js", "long.js"],
    205    { noExpand: true }
    206  );
    207  await pressKey(dbg, "Right");
    208  await assertNodeIsFocused(dbg, 4);
    209  // Now, the nested source is also displayed
    210  await waitForSourcesInSourceTree(
    211    dbg,
    212    [
    213      "doc-sources.html",
    214      "simple1.js",
    215      "simple2.js",
    216      "long.js",
    217      "nested-source.js",
    218    ],
    219    { noExpand: true }
    220  );
    221 
    222  // Down key on a source
    223  await pressKey(dbg, "Down");
    224  await assertNodeIsFocused(dbg, 5);
    225 
    226  // Go to bottom of tree and press down key
    227  await pressKey(dbg, "Down");
    228  await pressKey(dbg, "Down");
    229  await assertNodeIsFocused(dbg, 6);
    230 
    231  // Up key on a source
    232  await pressKey(dbg, "Up");
    233  await assertNodeIsFocused(dbg, 5);
    234 
    235  // Left key on a source
    236  await pressKey(dbg, "Left");
    237  await assertNodeIsFocused(dbg, 4);
    238 
    239  // Left key on a closed dir
    240  // We are about to close the nested folder, the nested source is about to disappear
    241  await waitForSourcesInSourceTree(
    242    dbg,
    243    [
    244      "doc-sources.html",
    245      "simple1.js",
    246      "simple2.js",
    247      "long.js",
    248      "nested-source.js",
    249    ],
    250    { noExpand: true }
    251  );
    252  await pressKey(dbg, "Left");
    253  // And it disappeared
    254  await waitForSourcesInSourceTree(
    255    dbg,
    256    ["doc-sources.html", "simple1.js", "simple2.js", "long.js"],
    257    { noExpand: true }
    258  );
    259  await pressKey(dbg, "Left");
    260  await assertNodeIsFocused(dbg, 3);
    261 
    262  // Up Key at the top of the source tree
    263  await pressKey(dbg, "Up");
    264  await assertNodeIsFocused(dbg, 2);
    265  dbg.toolbox.closeToolbox();
    266 });
    267 
    268 /**
    269 * Tests that the source tree works with all the various types of sources
    270 * coming from the integration test page.
    271 *
    272 * Also assert a few extra things on sources with query strings:
    273 *  - they can be pretty printed,
    274 *  - quick open matches them,
    275 *  - you can set breakpoint on them.
    276 */
    277 add_task(async function testSourceTreeOnTheIntegrationTestPage() {
    278  // We open against a blank page and only then navigate to the test page
    279  // so that sources aren't GC-ed before opening the debugger.
    280  // When we (re)load a page while the debugger is opened, the debugger
    281  // will force all sources to be held in memory.
    282  const dbg = await initDebuggerWithAbsoluteURL("about:blank");
    283 
    284  await navigateToAbsoluteURL(
    285    dbg,
    286    TEST_URL,
    287    "index.html",
    288    "script.js",
    289    "test-functions.js",
    290    "query.js?x=1",
    291    "query.js?x=2",
    292    "query2.js?y=3",
    293    "bundle.js",
    294    "original.js",
    295    "replaced-bundle.js",
    296    "removed-original.js",
    297    "named-eval.js"
    298  );
    299 
    300  info("Verify source tree content");
    301  await waitForSourcesInSourceTree(dbg, INTEGRATION_TEST_PAGE_SOURCES);
    302 
    303  info("Verify Thread Source Items");
    304  const mainThreadItem = findSourceTreeThreadByName(dbg, "Main Thread");
    305  ok(mainThreadItem, "Found the thread item for the main thread");
    306  ok(
    307    mainThreadItem.querySelector("span.dbg-img-window"),
    308    "The thread has the window icon"
    309  );
    310 
    311  info(
    312    "Assert the number of sources and source actors for the same-url.sjs sources"
    313  );
    314  const sameUrlSource = findSource(dbg, "same-url.sjs");
    315  ok(sameUrlSource, "Found same-url.js in the main thread");
    316 
    317  const sourceActors = dbg.selectors.getSourceActorsForSource(sameUrlSource.id);
    318 
    319  const mainThread = dbg.selectors
    320    .getAllThreads()
    321    .find(thread => thread.name == "Main Thread");
    322 
    323  const expectedSameUrlSources = 3;
    324  is(
    325    sourceActors.filter(actor => actor.thread == mainThread.actor).length,
    326    expectedSameUrlSources,
    327    `same-url.js is loaded ${expectedSameUrlSources} times in the main thread`
    328  );
    329 
    330  const iframeThread = dbg.selectors
    331    .getAllThreads()
    332    .find(thread => thread.name == testServer.urlFor("iframe.html"));
    333 
    334  is(
    335    sourceActors.filter(actor => actor.thread == iframeThread.actor).length,
    336    1,
    337    "same-url.js is loaded one time in the iframe thread"
    338  );
    339 
    340  const workerThread = dbg.selectors
    341    .getAllThreads()
    342    .find(thread => thread.url == testServer.urlFor("same-url.sjs"));
    343 
    344  is(
    345    sourceActors.filter(actor => actor.thread == workerThread.actor).length,
    346    1,
    347    "same-url.js is loaded one time in the worker thread"
    348  );
    349 
    350  const workerThreadItem = findSourceTreeThreadByName(dbg, "same-url.sjs");
    351  ok(workerThreadItem, "Found the thread item for the worker");
    352  ok(
    353    workerThreadItem.querySelector("span.dbg-img-worker"),
    354    "The thread has the worker icon"
    355  );
    356 
    357  info("Verify source icons");
    358  assertSourceIcon(dbg, "index.html", "file");
    359  assertSourceIcon(dbg, "script.js", "javascript");
    360  assertSourceIcon(dbg, "query.js?x=1", "javascript");
    361  assertSourceIcon(dbg, "original.js", "javascript");
    362  // Framework icons are only displayed when we parse the source,
    363  // which happens when we select the source
    364  assertSourceIcon(dbg, "react-component-module.js", "javascript");
    365  const onResumed = SpecialPowers.spawn(
    366    gBrowser.selectedBrowser,
    367    [],
    368    function () {
    369      content.eval("pauseInReact()");
    370    }
    371  );
    372  await waitForPaused(dbg);
    373  assertSourceIcon(dbg, "react-component-module.js", "javascript");
    374  await resume(dbg);
    375  await onResumed;
    376 
    377  info("Select an original source while bundle should be opened by default");
    378  // The setting should be ignored when selecting a source from the source tree
    379  await dbg.actions.setDefaultSelectedLocation(false);
    380  await selectSourceFromSourceTree(dbg, "original.js");
    381  assertTextContentOnLine(dbg, 1, `window.bar = function bar() {`);
    382 
    383  info("Verify blackbox source icon");
    384  await selectSourceFromSourceTree(dbg, "script.js");
    385  await clickElement(dbg, "blackbox");
    386  await waitForDispatch(dbg.store, "BLACKBOX_WHOLE_SOURCES");
    387  assertSourceIcon(dbg, "script.js", "blackBox");
    388  await clickElement(dbg, "blackbox");
    389  await waitForDispatch(dbg.store, "UNBLACKBOX_WHOLE_SOURCES");
    390  assertSourceIcon(dbg, "script.js", "javascript");
    391 
    392  info("Assert the content of the named eval");
    393  await selectSourceFromSourceTree(dbg, "named-eval.js");
    394  assertTextContentOnLine(dbg, 3, `console.log("named-eval");`);
    395 
    396  info("Assert that nameless eval don't show up in the source tree");
    397  invokeInTab("breakInEval");
    398  await waitForPaused(dbg);
    399  await waitForSourcesInSourceTree(dbg, INTEGRATION_TEST_PAGE_SOURCES);
    400  await resume(dbg);
    401 
    402  info("Assert the content of sources with query string");
    403  await selectSourceFromSourceTree(dbg, "query.js?x=1");
    404  const tab = findElement(dbg, "activeTab");
    405  is(tab.innerText, "query.js?x=1", "Tab label is query.js?x=1");
    406  assertTextContentOnLine(
    407    dbg,
    408    1,
    409    `function query() {console.log("query x=1");}`
    410  );
    411  await addBreakpoint(dbg, "query.js?x=1", 1);
    412  assertBreakpointHeading(dbg, "query.js?x=1", 0);
    413 
    414  // pretty print the source and check the tab text
    415  await togglePrettyPrint(dbg);
    416 
    417  const prettyTab = findElement(dbg, "activeTab");
    418  is(prettyTab.innerText, "query.js?x=1", "Tab label is query.js?x=1");
    419  assertBreakpointHeading(dbg, "query.js?x=1", 0);
    420  assertTextContentOnLine(dbg, 1, `function query() {`);
    421  // Note the replacements of " by ' here:
    422  assertTextContentOnLine(dbg, 2, `console.log('query x=1');`);
    423 
    424  // assert quick open works with queries
    425  pressKey(dbg, "quickOpen");
    426  type(dbg, "query.js?x");
    427 
    428  // There can be intermediate updates in the results,
    429  // so wait for the final expected value
    430  await waitFor(async () => {
    431    const resultItem = findElement(dbg, "resultItems");
    432    if (!resultItem) {
    433      return false;
    434    }
    435    return resultItem.innerText.includes("query.js?x=1");
    436  }, "Results include the source with the query string");
    437  dbg.toolbox.closeToolbox();
    438 });
    439 
    440 /**
    441 * Verify that Web Extension content scripts appear only when
    442 * devtools.chrome.enabled is set to true and that they get
    443 * automatically re-selected on page reload.
    444 */
    445 add_task(async function testSourceTreeWithWebExtensionContentScript() {
    446  const extension = await installAndStartContentScriptExtension();
    447 
    448  // Ensure that the setting to show content script is off before running the test
    449  await pushPref("devtools.debugger.show-content-scripts", false);
    450  let dbg = await initDebugger("doc-content-script-sources.html");
    451  // Let some time for unexpected source to appear
    452  await wait(1000);
    453  // There is no content script, but still html pages inline sources
    454  await waitForSourcesInSourceTree(dbg, [
    455    "doc-content-script-sources.html",
    456    "doc-strict.html",
    457  ]);
    458  await dbg.toolbox.closeToolbox();
    459 
    460  const toolbox = await openToolboxForTab(gBrowser.selectedTab, "jsdebugger");
    461  dbg = createDebuggerContext(toolbox);
    462 
    463  info("Enable the content script setting");
    464  await toggleSourcesTreeSettingsMenuItem(dbg, {
    465    className: ".debugger-settings-menu-item-show-content-scripts",
    466    isChecked: false,
    467  });
    468 
    469  await waitForSourcesInSourceTree(dbg, [
    470    "doc-content-script-sources.html",
    471    "doc-strict.html",
    472    "content_script.js",
    473  ]);
    474  await selectSourceFromSourceTree(dbg, "content_script.js");
    475  ok(
    476    findElementWithSelector(dbg, ".sources-list .focused"),
    477    "Source is focused"
    478  );
    479 
    480  // Note that the thread item also contains the extension name
    481  const contentScriptGroupItem = findSourceTreeGroupByName(
    482    dbg,
    483    "Test content script extension"
    484  );
    485  ok(contentScriptGroupItem, "Found the group item for the content script");
    486  ok(
    487    contentScriptGroupItem.querySelector("span.dbg-img-extension"),
    488    "The group has the extension icon"
    489  );
    490  assertSourceIcon(dbg, "content_script.js", "javascript");
    491 
    492  for (let i = 1; i < 3; i++) {
    493    info(
    494      `Reloading tab (${i} time), the content script should always be reselected`
    495    );
    496    gBrowser.reloadTab(gBrowser.selectedTab);
    497    await waitForSelectedSource(dbg, "content_script.js");
    498    ok(
    499      findElementWithSelector(dbg, ".sources-list .focused"),
    500      "Source is focused"
    501    );
    502  }
    503  await dbg.toolbox.closeToolbox();
    504 
    505  await extension.unload();
    506 });
    507 
    508 add_task(async function testSourceTreeWithEncodedPaths() {
    509  const httpServer = createTestHTTPServer();
    510  httpServer.registerContentType("html", "text/html");
    511  httpServer.registerContentType("js", "application/javascript");
    512 
    513  httpServer.registerPathHandler("/index.html", function (request, response) {
    514    response.setStatusLine(request.httpVersion, 200, "OK");
    515    response.write(`<!DOCTYPE html>
    516    <html>
    517      <head>
    518      <script src="/my folder/my file.js"></script>
    519      <script src="/malformedUri.js?%"></script>
    520      </head>
    521      <body>
    522      <h1>Encoded scripts paths</h1>
    523      </body>
    524    `);
    525  });
    526  httpServer.registerPathHandler(
    527    encodeURI("/my folder/my file.js"),
    528    function (request, response) {
    529      response.setStatusLine(request.httpVersion, 200, "OK");
    530      response.setHeader("Content-Type", "application/javascript", false);
    531      response.write(`const x = 42`);
    532    }
    533  );
    534  httpServer.registerPathHandler(
    535    "/malformedUri.js",
    536    function (request, response) {
    537      response.setStatusLine(request.httpVersion, 200, "OK");
    538      response.setHeader("Content-Type", "application/javascript", false);
    539      response.write(`const y = "malformed"`);
    540    }
    541  );
    542  const port = httpServer.identity.primaryPort;
    543 
    544  const dbg = await initDebuggerWithAbsoluteURL(
    545    `http://localhost:${port}/index.html`,
    546    "my file.js"
    547  );
    548 
    549  await waitForSourcesInSourceTree(dbg, ["my file.js", "malformedUri.js?%"]);
    550  ok(
    551    true,
    552    "source name are decoded in the tree, and malformed uri source are displayed"
    553  );
    554  is(
    555    // We don't have any specific class on the folder item, so let's target the folder
    556    // icon next sibling, which is the directory label.
    557    findElementWithSelector(
    558      dbg,
    559      ".sources-panel .node .dbg-img-folder + .label"
    560    ).innerText,
    561    "my folder",
    562    "folder name is decoded in the tree"
    563  );
    564 });
    565 
    566 /**
    567 * Assert the location displayed in the breakpoint list, in the right sidebar.
    568 *
    569 * @param {object} dbg
    570 * @param {string} label
    571 *        The expected displayed location
    572 * @param {number} index
    573 *        The position of the breakpoint in the list to verify
    574 */
    575 function assertBreakpointHeading(dbg, label, index) {
    576  const breakpointHeading = findAllElements(dbg, "breakpointHeadings")[index]
    577    .innerText;
    578  is(breakpointHeading, label, `Breakpoint heading is ${label}`);
    579 }