tor-browser

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

browser_download_canceled.js (6523B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 /*
      5 * Test cancellation of a download in order to test edge-cases related to
      6 * channel diversion.  Channel diversion occurs in cases of file (and PSM cert)
      7 * downloads where we realize in the child that we really want to consume the
      8 * channel data in the parent.  For data "sourced" by the parent, like network
      9 * data, data streaming to the child is suspended and the parent waits for the
     10 * child to send back the data it already received, then the channel is resumed.
     11 * For data generated by the child, such as (the current, to be mooted by
     12 * parent-intercept) child-side intercept, the data (currently) stream is
     13 * continually pumped up to the parent.
     14 *
     15 * In particular, we want to reproduce the circumstances of Bug 1418795 where
     16 * the child-side input-stream pump attempts to send data to the parent process
     17 * but the parent has canceled the channel and so the IPC Actor has been torn
     18 * down.  Diversion begins once the nsURILoader receives the OnStartRequest
     19 * notification with the headers, so there are two ways to produce
     20 */
     21 
     22 /**
     23 * Clear the downloads list so other tests don't see our byproducts.
     24 */
     25 async function clearDownloads() {
     26  const downloads = await Downloads.getList(Downloads.ALL);
     27  downloads.removeFinished();
     28 }
     29 
     30 /**
     31 * Returns a Promise that will be resolved once the download dialog shows up and
     32 * we have clicked the given button.
     33 */
     34 function promiseClickDownloadDialogButton(buttonAction) {
     35  const uri = "chrome://mozapps/content/downloads/unknownContentType.xhtml";
     36  return BrowserTestUtils.promiseAlertDialogOpen(buttonAction, uri, {
     37    async callback(win) {
     38      // nsHelperAppDlg.js currently uses an eval-based setTimeout(0) to invoke
     39      // its postShowCallback that results in a misleading error to the console
     40      // if we close the dialog before it gets a chance to run.  Just a
     41      // setTimeout is not sufficient because it appears we get our "load"
     42      // listener before the document's, so we use TestUtils.waitForTick() to
     43      // defer until after its load handler runs, then use setTimeout(0) to end
     44      // up after its eval.
     45      await TestUtils.waitForTick();
     46 
     47      await new Promise(resolve => setTimeout(resolve, 0));
     48 
     49      const button = win.document
     50        .getElementById("unknownContentType")
     51        .getButton(buttonAction);
     52      button.disabled = false;
     53      info(`clicking ${buttonAction} button`);
     54      button.click();
     55    },
     56  });
     57 }
     58 
     59 async function performCanceledDownload(tab, path) {
     60  // If we're going to show a modal dialog for this download, then we should
     61  // use it to cancel the download. If not, then we have to let the download
     62  // start and then call into the downloads API ourselves to cancel it.
     63  // We use this promise to signal the cancel being complete in either case.
     64  let cancelledDownload;
     65 
     66  if (
     67    Services.prefs.getBoolPref(
     68      "browser.download.always_ask_before_handling_new_types",
     69      false
     70    )
     71  ) {
     72    // Start waiting for the download dialog before triggering the download.
     73    cancelledDownload = promiseClickDownloadDialogButton("cancel");
     74    // Wait for the cancelation to have been triggered.
     75    info("waiting for download popup");
     76  } else {
     77    let downloadView;
     78    cancelledDownload = new Promise(resolve => {
     79      downloadView = {
     80        onDownloadAdded(aDownload) {
     81          aDownload.cancel();
     82          resolve();
     83        },
     84      };
     85    });
     86    const downloadList = await Downloads.getList(Downloads.ALL);
     87    await downloadList.addView(downloadView);
     88  }
     89 
     90  // Trigger the download.
     91  info(`triggering download of "${path}"`);
     92  /* eslint-disable no-shadow */
     93  await SpecialPowers.spawn(tab.linkedBrowser, [path], function (path) {
     94    // Put a Promise in place that we can wait on for stream closure.
     95    content.wrappedJSObject.trackStreamClosure(path);
     96    // Create the link and trigger the download.
     97    const link = content.document.createElement("a");
     98    link.href = path;
     99    link.download = path;
    100    content.document.body.appendChild(link);
    101    link.click();
    102  });
    103  /* eslint-enable no-shadow */
    104 
    105  // Wait for the download to cancel.
    106  await cancelledDownload;
    107  info("cancelled download");
    108 
    109  // Wait for confirmation that the stream stopped.
    110  info(`wait for the ${path} stream to close.`);
    111  /* eslint-disable no-shadow */
    112  const why = await SpecialPowers.spawn(
    113    tab.linkedBrowser,
    114    [path],
    115    function (path) {
    116      return content.wrappedJSObject.streamClosed[path].promise;
    117    }
    118  );
    119  /* eslint-enable no-shadow */
    120  is(why.why, "canceled", "Ensure the stream canceled instead of timing out.");
    121  // Note that for the "sw-stream-download" case, we end up with a bogus
    122  // reason of "'close' may only be called on a stream in the 'readable' state."
    123  // Since we aren't actually invoking close(), I'm assuming this is an
    124  // implementation bug that will be corrected in the web platform tests.
    125  info(`Cancellation reason: ${why.message} after ${why.ticks} ticks`);
    126 }
    127 
    128 const gTestRoot = getRootDirectory(gTestPath).replace(
    129  "chrome://mochitests/content/",
    130  "http://mochi.test:8888/"
    131 );
    132 
    133 const PAGE_URL = `${gTestRoot}download_canceled/page_download_canceled.html`;
    134 
    135 add_task(async function interruptedDownloads() {
    136  await SpecialPowers.pushPrefEnv({
    137    set: [
    138      ["dom.serviceWorkers.enabled", true],
    139      ["dom.serviceWorkers.exemptFromPerDomainMax", true],
    140      ["dom.serviceWorkers.testing.enabled", true],
    141    ],
    142  });
    143 
    144  // Open the tab
    145  const tab = await BrowserTestUtils.openNewForegroundTab({
    146    gBrowser,
    147    opening: PAGE_URL,
    148  });
    149 
    150  // Wait for it to become controlled.  Check that it was a promise that
    151  // resolved as expected rather than undefined by checking the return value.
    152  const controlled = await SpecialPowers.spawn(
    153    tab.linkedBrowser,
    154    [],
    155    function () {
    156      // This is a promise set up by the page during load, and we are post-load.
    157      return content.wrappedJSObject.controlled;
    158    }
    159  );
    160  is(controlled, "controlled", "page became controlled");
    161 
    162  // Download a pass-through fetch stream.
    163  await performCanceledDownload(tab, "sw-passthrough-download");
    164 
    165  // Download a SW-generated stream
    166  await performCanceledDownload(tab, "sw-stream-download");
    167 
    168  // Cleanup
    169  await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
    170    return content.wrappedJSObject.registration.unregister();
    171  });
    172  BrowserTestUtils.removeTab(tab);
    173  await clearDownloads();
    174 });