summaryrefslogtreecommitdiffstats
path: root/dom/serviceworkers/test/browser_download_canceled.js
blob: 2deb8389efa2e21f9478b2db437370ca1ad04ab3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

/*
 * Test cancellation of a download in order to test edge-cases related to
 * channel diversion.  Channel diversion occurs in cases of file (and PSM cert)
 * downloads where we realize in the child that we really want to consume the
 * channel data in the parent.  For data "sourced" by the parent, like network
 * data, data streaming to the child is suspended and the parent waits for the
 * child to send back the data it already received, then the channel is resumed.
 * For data generated by the child, such as (the current, to be mooted by
 * parent-intercept) child-side intercept, the data (currently) stream is
 * continually pumped up to the parent.
 *
 * In particular, we want to reproduce the circumstances of Bug 1418795 where
 * the child-side input-stream pump attempts to send data to the parent process
 * but the parent has canceled the channel and so the IPC Actor has been torn
 * down.  Diversion begins once the nsURILoader receives the OnStartRequest
 * notification with the headers, so there are two ways to produce
 */

/**
 * Clear the downloads list so other tests don't see our byproducts.
 */
async function clearDownloads() {
  const downloads = await Downloads.getList(Downloads.ALL);
  downloads.removeFinished();
}

/**
 * Returns a Promise that will be resolved once the download dialog shows up and
 * we have clicked the given button.
 */
function promiseClickDownloadDialogButton(buttonAction) {
  const uri = "chrome://mozapps/content/downloads/unknownContentType.xhtml";
  return BrowserTestUtils.promiseAlertDialogOpen(buttonAction, uri, {
    async callback(win) {
      // nsHelperAppDlg.js currently uses an eval-based setTimeout(0) to invoke
      // its postShowCallback that results in a misleading error to the console
      // if we close the dialog before it gets a chance to run.  Just a
      // setTimeout is not sufficient because it appears we get our "load"
      // listener before the document's, so we use TestUtils.waitForTick() to
      // defer until after its load handler runs, then use setTimeout(0) to end
      // up after its eval.
      await TestUtils.waitForTick();

      await new Promise(resolve => setTimeout(resolve, 0));

      const button = win.document
        .getElementById("unknownContentType")
        .getButton(buttonAction);
      button.disabled = false;
      info(`clicking ${buttonAction} button`);
      button.click();
    },
  });
}

async function performCanceledDownload(tab, path) {
  // If we're going to show a modal dialog for this download, then we should
  // use it to cancel the download. If not, then we have to let the download
  // start and then call into the downloads API ourselves to cancel it.
  // We use this promise to signal the cancel being complete in either case.
  let cancelledDownload;

  if (
    Services.prefs.getBoolPref(
      "browser.download.always_ask_before_handling_new_types",
      false
    )
  ) {
    // Start waiting for the download dialog before triggering the download.
    cancelledDownload = promiseClickDownloadDialogButton("cancel");
    // Wait for the cancelation to have been triggered.
    info("waiting for download popup");
  } else {
    let downloadView;
    cancelledDownload = new Promise(resolve => {
      downloadView = {
        onDownloadAdded(aDownload) {
          aDownload.cancel();
          resolve();
        },
      };
    });
    const downloadList = await Downloads.getList(Downloads.ALL);
    await downloadList.addView(downloadView);
  }

  // Trigger the download.
  info(`triggering download of "${path}"`);
  /* eslint-disable no-shadow */
  await SpecialPowers.spawn(tab.linkedBrowser, [path], function (path) {
    // Put a Promise in place that we can wait on for stream closure.
    content.wrappedJSObject.trackStreamClosure(path);
    // Create the link and trigger the download.
    const link = content.document.createElement("a");
    link.href = path;
    link.download = path;
    content.document.body.appendChild(link);
    link.click();
  });
  /* eslint-enable no-shadow */

  // Wait for the download to cancel.
  await cancelledDownload;
  info("cancelled download");

  // Wait for confirmation that the stream stopped.
  info(`wait for the ${path} stream to close.`);
  /* eslint-disable no-shadow */
  const why = await SpecialPowers.spawn(
    tab.linkedBrowser,
    [path],
    function (path) {
      return content.wrappedJSObject.streamClosed[path].promise;
    }
  );
  /* eslint-enable no-shadow */
  is(why.why, "canceled", "Ensure the stream canceled instead of timing out.");
  // Note that for the "sw-stream-download" case, we end up with a bogus
  // reason of "'close' may only be called on a stream in the 'readable' state."
  // Since we aren't actually invoking close(), I'm assuming this is an
  // implementation bug that will be corrected in the web platform tests.
  info(`Cancellation reason: ${why.message} after ${why.ticks} ticks`);
}

const gTestRoot = getRootDirectory(gTestPath).replace(
  "chrome://mochitests/content/",
  "http://mochi.test:8888/"
);

const PAGE_URL = `${gTestRoot}download_canceled/page_download_canceled.html`;

add_task(async function interruptedDownloads() {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["dom.serviceWorkers.enabled", true],
      ["dom.serviceWorkers.exemptFromPerDomainMax", true],
      ["dom.serviceWorkers.testing.enabled", true],
    ],
  });

  // Open the tab
  const tab = await BrowserTestUtils.openNewForegroundTab({
    gBrowser,
    opening: PAGE_URL,
  });

  // Wait for it to become controlled.  Check that it was a promise that
  // resolved as expected rather than undefined by checking the return value.
  const controlled = await SpecialPowers.spawn(
    tab.linkedBrowser,
    [],
    function () {
      // This is a promise set up by the page during load, and we are post-load.
      return content.wrappedJSObject.controlled;
    }
  );
  is(controlled, "controlled", "page became controlled");

  // Download a pass-through fetch stream.
  await performCanceledDownload(tab, "sw-passthrough-download");

  // Download a SW-generated stream
  await performCanceledDownload(tab, "sw-stream-download");

  // Cleanup
  await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
    return content.wrappedJSObject.registration.unregister();
  });
  BrowserTestUtils.removeTab(tab);
  await clearDownloads();
});