174 lines
6.4 KiB
JavaScript
174 lines
6.4 KiB
JavaScript
/* 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();
|
|
});
|