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 });