tor-browser

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

browser_dbg-features-source-text-content.js (18682B)


      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 focus on asserting the source content displayed in CodeMirror
      7 * when we open a source from the SourceTree (or by any other means).
      8 *
      9 * The source content is being fetched from the server only on-demand.
     10 * The main shortcoming is about sources being GC-ed. This only happens
     11 * when we open the debugger on an already loaded page.
     12 * When we (re)load a page while the debugger is opened, sources are never GC-ed.
     13 * There are also specifics related to HTML page having inline scripts.
     14 * Also, as this data is fetched on-demand, there is a loading prompt
     15 * being displayed while the source is being fetched from the server.
     16 */
     17 
     18 "use strict";
     19 
     20 const httpServer = createTestHTTPServer();
     21 const BASE_URL = `http://localhost:${httpServer.identity.primaryPort}/`;
     22 const loadCounts = {};
     23 
     24 /**
     25 * Simple tests, asserting that we correctly display source text content in CodeMirror
     26 */
     27 const NAMED_EVAL_CONTENT = `function namedEval() {}; console.log('eval script'); //# sourceURL=named-eval.js`;
     28 const NEW_FUNCTION_CONTENT =
     29  "console.log('new function'); //# sourceURL=new-function.js";
     30 const INDEX_PAGE_CONTENT = `<!DOCTYPE html>
     31    <html>
     32      <head>
     33        <script type="text/javascript" src="/normal-script.js"></script>
     34        <script type="text/javascript" src="/slow-loading-script.js"></script>
     35        <script type="text/javascript" src="/http-error-script.js"></script>
     36        <script type="text/javascript" src="/same-url.js"></script>
     37        <script>
     38          console.log("inline script");
     39        </script>
     40        <script>
     41          console.log("second inline script");
     42          this.evaled1 = eval("${NAMED_EVAL_CONTENT}");
     43 
     44          // Load same-url.js in various different ways
     45          this.evaled2 = eval("function sameUrlEval() {}; //# sourceURL=same-url.js");
     46 
     47          const script = document.createElement("script");
     48          script.src = "same-url.js";
     49          document.documentElement.append(script);
     50 
     51          // Ensure loading the same-url.js file *after*
     52          // the iframe is done loading. So that we have a deterministic load order.
     53          window.onload = () => {
     54            this.worker = new Worker("same-url.js");
     55            this.worker.postMessage("foo");
     56          };
     57 
     58          this.newFunction = new Function("${NEW_FUNCTION_CONTENT}");
     59 
     60          // This method will be called via invokeInTab.
     61          let inFunctionIncrement = 1;
     62          function breakInNewFunction() {
     63            new Function("a\\n", "b" +inFunctionIncrement++, "debugger;")();
     64          }
     65        </script>
     66      </head>
     67      <body>
     68        <iframe src="iframe.html"></iframe>
     69      </body>
     70    </html>`;
     71 const IFRAME_CONTENT = `<!DOCTYPE html>
     72    <html>
     73      <head>
     74        <script type="text/javascript" src="/same-url.js"></script>
     75      </head>
     76      <body>
     77        <script>
     78          console.log("First inline script");
     79        </script>
     80        <script>
     81          console.log("Second inline script");
     82        </script>
     83      </body>
     84    </html>`;
     85 
     86 httpServer.registerPathHandler("/index.html", (request, response) => {
     87  loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
     88  response.setStatusLine(request.httpVersion, 200, "OK");
     89  response.write(INDEX_PAGE_CONTENT);
     90 });
     91 
     92 httpServer.registerPathHandler("/normal-script.js", (request, response) => {
     93  loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
     94  response.setHeader("Content-Type", "application/javascript");
     95  response.write(`console.log("normal script")`);
     96 });
     97 httpServer.registerPathHandler(
     98  "/slow-loading-script.js",
     99  (request, response) => {
    100    loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
    101    response.processAsync();
    102    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    103    setTimeout(function () {
    104      response.setHeader("Content-Type", "application/javascript");
    105      response.write(`console.log("slow loading script")`);
    106      response.finish();
    107    }, 1000);
    108  }
    109 );
    110 httpServer.registerPathHandler("/http-error-script.js", (request, response) => {
    111  loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
    112  response.setStatusLine(request.httpVersion, 404, "Not found");
    113  response.write(`console.log("http error")`);
    114 });
    115 httpServer.registerPathHandler("/same-url.js", (request, response) => {
    116  loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
    117  const sameUrlLoadCount = loadCounts[request.path];
    118  // Prevents gecko from cache this request in order to force fetching
    119  // a new, distinct content for each usage of this URL
    120  response.setHeader("Cache-Control", "no-store");
    121  response.setHeader("Content-Type", "application/javascript");
    122  response.write(`console.log("same url #${sameUrlLoadCount}")`);
    123 });
    124 httpServer.registerPathHandler("/iframe.html", (request, response) => {
    125  loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
    126  response.setHeader("Content-Type", "text/html");
    127  response.write(IFRAME_CONTENT);
    128 });
    129 add_task(async function testSourceTextContent() {
    130  const dbg = await initDebuggerWithAbsoluteURL("about:blank");
    131 
    132  const waitForSources = [
    133    "index.html",
    134    "normal-script.js",
    135    "slow-loading-script.js",
    136    "same-url.js",
    137    "new-function.js",
    138    "iframe.html",
    139  ];
    140 
    141  // Load the document *once* the debugger is opened
    142  // in order to avoid having any source being GC-ed.
    143  await navigateToAbsoluteURL(dbg, BASE_URL + "index.html", ...waitForSources);
    144 
    145  await selectSourceFromSourceTreeWithIndex(
    146    dbg,
    147    "new-function.js",
    148    5,
    149    "Select `new-function.js`"
    150  );
    151  is(
    152    getEditorContent(dbg),
    153    `function anonymous(\n) {\n${NEW_FUNCTION_CONTENT}\n}`
    154  );
    155 
    156  await selectSourceFromSourceTreeWithIndex(
    157    dbg,
    158    "normal-script.js",
    159    6,
    160    "Select `normal-script.js`"
    161  );
    162  is(getEditorContent(dbg), `console.log("normal script")`);
    163 
    164  await selectSourceFromSourceTreeWithIndex(
    165    dbg,
    166    "slow-loading-script.js",
    167    8,
    168    "Select `slow-loading-script.js`"
    169  );
    170  is(getEditorContent(dbg), `console.log("slow loading script")`);
    171 
    172  await selectSourceFromSourceTreeWithIndex(
    173    dbg,
    174    "index.html",
    175    3,
    176    "Select `index.html`"
    177  );
    178  is(getEditorContent(dbg), INDEX_PAGE_CONTENT);
    179 
    180  await selectSourceFromSourceTreeWithIndex(
    181    dbg,
    182    "named-eval.js",
    183    4,
    184    "Select `named-eval.js`"
    185  );
    186  is(getEditorContent(dbg), NAMED_EVAL_CONTENT);
    187 
    188  await selectSourceFromSourceTreeWithIndex(
    189    dbg,
    190    "same-url.js",
    191    7,
    192    "Select `same-url.js` in the Main Thread"
    193  );
    194 
    195  is(
    196    getEditorContent(dbg),
    197    `console.log("same url #1")`,
    198    "We get an arbitrary content for same-url, the first loaded one"
    199  );
    200 
    201  const sameUrlSource = findSource(dbg, "same-url.js");
    202  const sourceActors = dbg.selectors.getSourceActorsForSource(sameUrlSource.id);
    203 
    204  const mainThread = dbg.selectors
    205    .getAllThreads()
    206    .find(thread => thread.name == "Main Thread");
    207 
    208  is(
    209    sourceActors.filter(actor => actor.thread == mainThread.actor).length,
    210    3,
    211    "same-url.js is loaded 3 times in the main thread"
    212  );
    213 
    214  info(`Close the same-url.js from Main Thread`);
    215  await closeTab(dbg, "same-url.js");
    216 
    217  info("Click on the iframe tree node to show sources in the iframe");
    218  await clickElement(dbg, "sourceDirectoryLabel", 9);
    219  await waitForSourcesInSourceTree(
    220    dbg,
    221    [
    222      "index.html",
    223      "named-eval.js",
    224      "normal-script.js",
    225      "slow-loading-script.js",
    226      "same-url.js",
    227      "iframe.html",
    228      "same-url.js",
    229      "new-function.js",
    230    ],
    231    {
    232      noExpand: true,
    233    }
    234  );
    235 
    236  await selectSourceFromSourceTreeWithIndex(
    237    dbg,
    238    "same-url.js",
    239    12,
    240    "Select `same-url.js` in the iframe"
    241  );
    242 
    243  is(
    244    getEditorContent(dbg),
    245    `console.log("same url #3")`,
    246    "We get the expected content for same-url.js in the iframe"
    247  );
    248 
    249  const iframeThread = dbg.selectors
    250    .getAllThreads()
    251    .find(thread => thread.name == `${BASE_URL}iframe.html`);
    252  is(
    253    sourceActors.filter(actor => actor.thread == iframeThread.actor).length,
    254    1,
    255    "same-url.js is loaded one time in the iframe thread"
    256  );
    257 
    258  info(`Close the same-url.js from the iframe`);
    259  await closeTab(dbg, "same-url.js");
    260 
    261  info("Click on the worker tree node to show sources in the worker");
    262 
    263  await clickElement(dbg, "sourceDirectoryLabel", 13);
    264 
    265  const workerSources = [
    266    "index.html",
    267    "named-eval.js",
    268    "normal-script.js",
    269    "slow-loading-script.js",
    270    "same-url.js",
    271    "iframe.html",
    272    "same-url.js",
    273    "new-function.js",
    274    "same-url.js",
    275  ];
    276 
    277  await waitForSourcesInSourceTree(dbg, workerSources, {
    278    noExpand: true,
    279  });
    280 
    281  await selectSourceFromSourceTreeWithIndex(
    282    dbg,
    283    "same-url.js",
    284    15,
    285    "Select `same-url.js` in the worker"
    286  );
    287 
    288  is(
    289    getEditorContent(dbg),
    290    `console.log("same url #4")`,
    291    "We get the expected content for same-url.js worker"
    292  );
    293 
    294  const workerThread = dbg.selectors
    295    .getAllThreads()
    296    .find(thread => thread.url == `${BASE_URL}same-url.js`);
    297 
    298  is(
    299    sourceActors.filter(actor => actor.thread == workerThread.actor).length,
    300    1,
    301    "same-url.js is loaded one time in the worker thread"
    302  );
    303 
    304  await selectSource(dbg, "iframe.html");
    305  is(getEditorContent(dbg), IFRAME_CONTENT);
    306 
    307  ok(
    308    !sourceExists(dbg, "http-error-script.js"),
    309    "scripts with HTTP error code do not appear in the source list"
    310  );
    311 
    312  info(
    313    "Verify that breaking in a source without url displays the right content"
    314  );
    315  let onNewSource = waitForDispatch(dbg.store, "ADD_SOURCES");
    316  invokeInTab("breakInNewFunction");
    317  await waitForPaused(dbg);
    318  let { sources } = await onNewSource;
    319  is(sources.length, 1, "Got a unique source related to new Function source");
    320  let newFunctionSource = sources[0];
    321  // We acknowledge the function header as well as the new line in the first argument
    322  await assertPausedAtSourceAndLine(dbg, newFunctionSource.id, 4, 0);
    323  is(getEditorContent(dbg), "function anonymous(a\n,b1\n) {\ndebugger;\n}");
    324  await resume(dbg);
    325 
    326  info(
    327    "Break a second time in a source without url to verify we display the right content"
    328  );
    329  onNewSource = waitForDispatch(dbg.store, "ADD_SOURCES");
    330  invokeInTab("breakInNewFunction");
    331  await waitForPaused(dbg);
    332  ({ sources } = await onNewSource);
    333  is(sources.length, 1, "Got a unique source related to new Function source");
    334  newFunctionSource = sources[0];
    335  // We acknowledge the function header as well as the new line in the first argument
    336  await assertPausedAtSourceAndLine(dbg, newFunctionSource.id, 4, 0);
    337  is(getEditorContent(dbg), "function anonymous(a\n,b2\n) {\ndebugger;\n}");
    338  await resume(dbg);
    339 
    340  // As we are loading the page while the debugger is already opened,
    341  // none of the resources are loaded twice.
    342  is(loadCounts["/index.html"], 1, "We loaded index.html only once");
    343  is(
    344    loadCounts["/normal-script.js"],
    345    1,
    346    "We loaded normal-script.js only once"
    347  );
    348  is(
    349    loadCounts["/slow-loading-script.js"],
    350    1,
    351    "We loaded slow-loading-script.js only once"
    352  );
    353  is(
    354    loadCounts["/same-url.js"],
    355    4,
    356    "We loaded same-url.js in 4 distinct ways (the named eval doesn't count)"
    357  );
    358  // For some reason external to the debugger, we issue two requests to scripts having http error codes.
    359  // These two requests are done before opening the debugger.
    360  is(
    361    loadCounts["/http-error-script.js"],
    362    2,
    363    "We loaded http-error-script.js twice, only before the debugger is opened"
    364  );
    365 
    366  await reload(dbg, "index.html", ...waitForSources);
    367 
    368  // Verify that iframe source content is fetch correctly after reload
    369  await selectSource(dbg, "iframe.html");
    370  is(getEditorContent(dbg), IFRAME_CONTENT);
    371 });
    372 
    373 /**
    374 * In this test, we force a GC before loading DevTools.
    375 * So that Spidermonkey will no longer have access to the sources
    376 * and another request should be issues to load the source text content.
    377 */
    378 const GARBAGED_PAGE_CONTENT = `<!DOCTYPE html>
    379    <html>
    380      <head>
    381        <script type="text/javascript" src="/garbaged-script.js"></script>
    382        <script>
    383          console.log("garbaged inline script");
    384        </script>
    385      </head>
    386    </html>`;
    387 
    388 httpServer.registerPathHandler(
    389  "/garbaged-collected.html",
    390  (request, response) => {
    391    loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
    392    response.setStatusLine(request.httpVersion, 200, "OK");
    393    response.write(GARBAGED_PAGE_CONTENT);
    394  }
    395 );
    396 
    397 httpServer.registerPathHandler("/garbaged-script.js", (request, response) => {
    398  loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
    399  response.setHeader("Content-Type", "application/javascript");
    400  response.write(`console.log("garbaged script ${loadCounts[request.path]}")`);
    401 });
    402 add_task(async function testGarbageCollectedSourceTextContent() {
    403  const tab = await addTab(BASE_URL + "garbaged-collected.html");
    404  is(
    405    loadCounts["/garbaged-collected.html"],
    406    1,
    407    "The HTML page is loaded once before opening the DevTools"
    408  );
    409  is(
    410    loadCounts["/garbaged-script.js"],
    411    1,
    412    "The script is loaded once before opening the DevTools"
    413  );
    414 
    415  // Force freeing both the HTML page and script in memory
    416  // so that the debugger has to fetch source content from http cache.
    417  await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
    418    Cu.forceGC();
    419  });
    420 
    421  const toolbox = await openToolboxForTab(tab, "jsdebugger");
    422  const dbg = createDebuggerContext(toolbox);
    423  await waitForSources(dbg, "garbaged-collected.html", "garbaged-script.js");
    424 
    425  await selectSource(dbg, "garbaged-script.js");
    426  // XXX Bug 1758454 - Source content of GC-ed script can be wrong!
    427  // Even if we have to issue a new HTTP request for this source,
    428  // we should be using HTTP cache and retrieve the first served version which
    429  // is the one that actually runs in the page!
    430  // We should be displaying `console.log("garbaged script 1")`,
    431  // but instead, a new HTTP request is dispatched and we get a new content.
    432  is(getEditorContent(dbg), `console.log("garbaged script 2")`);
    433 
    434  await selectSource(dbg, "garbaged-collected.html");
    435  is(getEditorContent(dbg), GARBAGED_PAGE_CONTENT);
    436 
    437  is(
    438    loadCounts["/garbaged-collected.html"],
    439    2,
    440    "We loaded the html page once as we haven't tried to display it in the debugger (2)"
    441  );
    442  is(
    443    loadCounts["/garbaged-script.js"],
    444    2,
    445    "We loaded the garbaged script twice as we lost its content"
    446  );
    447 });
    448 
    449 /**
    450 * Test failures when trying to open the source text content.
    451 *
    452 * In this test we load an html page
    453 * - with inline source (so that it shows up in the debugger)
    454 * - it first loads fine so that it shows up
    455 * - initDebuggerWithAbsoluteURL will first load the document before the debugger
    456 * - so the debugger will have to fetch the html page content via a network request
    457 * - the test page will return a connection reset error on the second load attempt
    458 */
    459 let loadCount = 0;
    460 httpServer.registerPathHandler(
    461  "/200-then-connection-reset.html",
    462  (request, response) => {
    463    loadCount++;
    464    if (loadCount > 1) {
    465      response.seizePower();
    466      response.bodyOutPutStream.close();
    467      response.finish();
    468      return;
    469    }
    470    response.setStatusLine(request.httpVersion, 200, "OK");
    471    response.write(`<!DOCTYPE html><script>console.log("200 page");</script>`);
    472  }
    473 );
    474 add_task(async function testFailingHtmlSource() {
    475  info("Test failure in retrieving html page sources");
    476 
    477  // initDebuggerWithAbsoluteURL will first load the document once before the debugger,
    478  // then the debugger will have to fetch the html page content via a network request
    479  // therefore the test page will encounter a connection reset error on the second load attempt
    480  const dbg = await initDebuggerWithAbsoluteURL(
    481    BASE_URL + "200-then-connection-reset.html",
    482    "200-then-connection-reset.html"
    483  );
    484 
    485  // We can't select the HTML page as its source content isn't fetched
    486  // (waitForSelectedSource doesn't resolve)
    487  // Note that it is important to load the page *before* opening the page
    488  // so that the thread actor has to request the page content and will fail
    489  const source = findSource(dbg, "200-then-connection-reset.html");
    490  await dbg.actions.selectLocation(createLocation({ source }), {
    491    keepContext: false,
    492  });
    493 
    494  ok(
    495    getEditorContent(dbg).includes("Could not load the source"),
    496    "Display failure error"
    497  );
    498 });
    499 
    500 /**
    501 * In this test we try to reproduce the "Loading..." message.
    502 * This may happen when opening an HTML source that was loaded *before*
    503 * opening DevTools. The thread actor will have to issue a new HTTP request
    504 * to load the source content.
    505 */
    506 let loadCount2 = 0;
    507 let slowLoadingPageResolution = null;
    508 httpServer.registerPathHandler(
    509  "/slow-loading-page.html",
    510  (request, response) => {
    511    loadCount2++;
    512    if (loadCount2 > 1) {
    513      response.processAsync();
    514      slowLoadingPageResolution = function () {
    515        response.write(
    516          `<!DOCTYPE html><script>console.log("slow-loading-page:second-load");</script>`
    517        );
    518        response.finish();
    519      };
    520      return;
    521    }
    522    response.write(
    523      `<!DOCTYPE html><script>console.log("slow-loading-page:first-load");</script>`
    524    );
    525  }
    526 );
    527 add_task(async function testLoadingHtmlSource() {
    528  info("Test loading progress of html page sources");
    529  const dbg = await initDebuggerWithAbsoluteURL(
    530    BASE_URL + "slow-loading-page.html",
    531    "slow-loading-page.html"
    532  );
    533 
    534  const onSelected = selectSource(dbg, "slow-loading-page.html");
    535  await waitFor(
    536    () => getEditorContent(dbg) == DEBUGGER_L10N.getStr("loadingText"),
    537    "Wait for the source to be displayed as loading"
    538  );
    539 
    540  info("Wait for a second HTTP request to be made for the html page");
    541  await waitFor(
    542    () => slowLoadingPageResolution,
    543    "Wait for the html page to be queried a second time"
    544  );
    545  is(
    546    getEditorContent(dbg),
    547    DEBUGGER_L10N.getStr("loadingText"),
    548    "The source is still loading until we release the network request"
    549  );
    550 
    551  slowLoadingPageResolution();
    552  info("Wait for the source to be fully selected and loaded");
    553  await onSelected;
    554 
    555  // Note that, even if the thread actor triggers a new HTTP request,
    556  // it will use the HTTP cache and retrieve the first request content.
    557  // This is actually relevant as that's the source that actually runs in the page!
    558  //
    559  // XXX Bug 1758458 - the source content is wrong.
    560  // We should be seeing the whole HTML page content,
    561  // whereas we only see the inline source text content.
    562  is(getEditorContent(dbg), `console.log("slow-loading-page:first-load");`);
    563 });