/* 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 */ const { Downloads } = ChromeUtils.importESModule( "resource://gre/modules/Downloads.sys.mjs" ); /** * 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(); });