summaryrefslogtreecommitdiffstats
path: root/dom/serviceworkers/test/browser_download_canceled.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/serviceworkers/test/browser_download_canceled.js')
-rw-r--r--dom/serviceworkers/test/browser_download_canceled.js174
1 files changed, 174 insertions, 0 deletions
diff --git a/dom/serviceworkers/test/browser_download_canceled.js b/dom/serviceworkers/test/browser_download_canceled.js
new file mode 100644
index 0000000000..2deb8389ef
--- /dev/null
+++ b/dom/serviceworkers/test/browser_download_canceled.js
@@ -0,0 +1,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();
+});