Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* Any copyright is dedicated to the Public Domain.
let { nsUnknownContentTypeDialog } = ChromeUtils.importESModule(
"resource://gre/modules/HelperAppDlg.sys.mjs"
);
let MockFilePicker = SpecialPowers.MockFilePicker;
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [
["browser.download.useDownloadDir", false],
["browser.download.lastDir", ""],
["browser.download.lastDir.savePerSite", true],
],
});
MockFilePicker.init();
registerCleanupFunction(() => MockFilePicker.cleanup());
});
function createSaveDirectory() {
let directory = Services.dirsvc.get("TmpD", Ci.nsIFile);
directory.append("download-save-directory");
directory.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o700);
registerCleanupFunction(() => directory.remove(true));
return directory;
}
function fileInDirectory(directory, name) {
let file = directory.clone();
file.append(name);
return file;
}
async function openSaveWindow(isPrivate = false) {
let win = await BrowserTestUtils.openNewBrowserWindow({
private: isPrivate,
});
registerCleanupFunction(async () => {
if (!win.closed) {
await BrowserTestUtils.closeWindow(win);
}
});
return win;
}
async function rememberDirectory(win, source, directory) {
let lastDir = new DownloadLastDir(win);
lastDir.setFile(source, directory);
registerCleanupFunction(() => lastDir.setFile(source, null));
Assert.equal((await lastDir.getFileAsync(source)).path, directory.path);
}
function promptToSave(win, source, name) {
let { promise, resolve } = Promise.withResolvers();
let calls = 0;
let launcher = {
source,
MIMEInfo: { description: "Text", primaryExtension: "txt" },
saveDestinationAvailable(file) {
Assert.equal(++calls, 1, "The download receives exactly one result");
resolve(file);
},
};
new nsUnknownContentTypeDialog().promptForSaveToFileAsync(
launcher,
win.docShell.QueryInterface(Ci.nsIInterfaceRequestor),
name,
".txt",
true
);
return {
result: promise,
get calls() {
return calls;
},
};
}
async function checkDirectoryReuse(isPrivate) {
let win = await openSaveWindow(isPrivate);
let initialDir = createSaveDirectory();
let firstDir = createSaveDirectory();
let secondDir = createSaveDirectory();
let source = Services.io.newURI("https://directory.example.com/file.txt");
await rememberDirectory(win, source, initialDir);
let lastDirPref = Services.prefs.getStringPref("browser.download.lastDir");
let expected = [initialDir, firstDir, secondDir];
let selected = [firstDir, secondDir, secondDir];
let shown = 0;
MockFilePicker.showCallback = picker => {
Assert.equal(
picker.displayDirectory.path,
expected[shown].path,
"The next picker uses the directory selected by the preceding save"
);
MockFilePicker.setFiles([
fileInDirectory(selected[shown], picker.defaultString),
]);
++shown;
return MockFilePicker.returnOK;
};
let requests = ["first.txt", "second.txt", "third.txt"].map(name =>
promptToSave(win, source, name)
);
let files = await Promise.all(requests.map(request => request.result));
Assert.equal(shown, 3, "Every download gets a picker");
Assert.deepEqual(
files.map(file => file.parent.path),
selected.map(directory => directory.path),
"Every file uses its selected directory"
);
if (isPrivate) {
Assert.equal(
Services.prefs.getStringPref("browser.download.lastDir"),
lastDirPref,
"Private saves do not change the persisted directory"
);
}
await BrowserTestUtils.closeWindow(win);
}
add_task(async function test_directory_reuse() {
await checkDirectoryReuse(false);
});
add_task(async function test_private_directory_reuse() {
await checkDirectoryReuse(true);
});
add_task(async function test_site_directories_remain_separate() {
let win = await openSaveWindow();
let firstDir = createSaveDirectory();
let otherDir = createSaveDirectory();
let chosenDir = createSaveDirectory();
let firstSource = Services.io.newURI("https://first.example.com/file.txt");
let otherSource = Services.io.newURI("https://other.example.com/file.txt");
await rememberDirectory(win, firstSource, firstDir);
await rememberDirectory(win, otherSource, otherDir);
let expected = [firstDir, otherDir];
let shown = 0;
MockFilePicker.showCallback = picker => {
Assert.equal(picker.displayDirectory.path, expected[shown].path);
++shown;
MockFilePicker.setFiles([fileInDirectory(chosenDir, picker.defaultString)]);
return MockFilePicker.returnOK;
};
let first = promptToSave(win, firstSource, "first.txt");
let other = promptToSave(win, otherSource, "other.txt");
await Promise.all([first.result, other.result]);
Assert.equal(shown, 2);
await BrowserTestUtils.closeWindow(win);
});
add_task(async function test_other_windows_remain_independent() {
let firstWindow = await openSaveWindow();
let otherWindow = await openSaveWindow();
let source = Services.io.newURI("https://independent.example.com/file.txt");
let firstShown = Promise.withResolvers();
let finishFirst = Promise.withResolvers();
registerCleanupFunction(() =>
finishFirst.resolve(MockFilePicker.returnCancel)
);
MockFilePicker.showCallback = async picker => {
if (picker.defaultString == "first.txt") {
firstShown.resolve();
return finishFirst.promise;
}
return MockFilePicker.returnCancel;
};
let first = promptToSave(firstWindow, source, "first.txt");
await firstShown.promise;
let other = promptToSave(otherWindow, source, "other.txt");
Assert.equal(
await other.result,
null,
"Another window can finish independently"
);
Assert.equal(first.calls, 0, "The first window is still waiting");
finishFirst.resolve(MockFilePicker.returnCancel);
await first.result;
await BrowserTestUtils.closeWindow(firstWindow);
await BrowserTestUtils.closeWindow(otherWindow);
});
add_task(async function test_close_window_cancels_active_and_queued_requests() {
let win = await openSaveWindow();
let source = Services.io.newURI("https://closed.example.com/file.txt");
let firstShown = Promise.withResolvers();
let finishPicker = Promise.withResolvers();
let pickerFinished = Promise.withResolvers();
MockFilePicker.setFiles([]);
registerCleanupFunction(() =>
finishPicker.resolve(MockFilePicker.returnCancel)
);
let shown = 0;
MockFilePicker.showCallback = () => {
++shown;
firstShown.resolve();
return finishPicker.promise;
};
let requests = ["first.txt", "second.txt", "third.txt"].map(name =>
promptToSave(win, source, name)
);
await firstShown.promise;
await BrowserTestUtils.closeWindow(win);
Assert.deepEqual(
await Promise.all(requests.map(request => request.result)),
[null, null, null],
"Closing the window cancels every request before the native callback arrives"
);
Assert.equal(shown, 1, "Queued pickers are not opened after window closure");
MockFilePicker.afterOpenCallback = () => pickerFinished.resolve();
finishPicker.resolve(MockFilePicker.returnCancel);
await pickerFinished.promise;
MockFilePicker.afterOpenCallback = null;
Assert.deepEqual(
requests.map(request => request.calls),
[1, 1, 1],
"The late native callback does not complete any download twice"
);
});
add_task(async function test_close_window_during_directory_lookup() {
let win = await openSaveWindow();
let source = Services.io.newURI("https://lookup.example.com/file.txt");
let lookupStarted = Promise.withResolvers();
let finishLookup = Promise.withResolvers();
let originalGetFileAsync = DownloadLastDir.prototype.getFileAsync;
DownloadLastDir.prototype.getFileAsync = () => {
lookupStarted.resolve();
return finishLookup.promise;
};
registerCleanupFunction(() => {
DownloadLastDir.prototype.getFileAsync = originalGetFileAsync;
finishLookup.resolve(null);
});
let shown = 0;
MockFilePicker.setFiles([]);
MockFilePicker.showCallback = () => {
++shown;
return MockFilePicker.returnCancel;
};
try {
let request = promptToSave(win, source, "lookup.txt");
await lookupStarted.promise;
await BrowserTestUtils.closeWindow(win);
Assert.equal(
await request.result,
null,
"Window closure cancels a request while its directory lookup is pending"
);
} finally {
DownloadLastDir.prototype.getFileAsync = originalGetFileAsync;
finishLookup.resolve(null);
}
await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
Assert.equal(shown, 0, "Finishing the lookup does not open a picker");
});
add_task(async function test_location_only_downloads() {
let { HttpServer } = ChromeUtils.importESModule(
);
let { Downloads } = ChromeUtils.importESModule(
"resource://gre/modules/Downloads.sys.mjs"
);
let server = new HttpServer();
server.registerPathHandler("/download", (request, response) => {
response.setHeader("Content-Type", "application/x-save-directory-test");
response.setHeader(
"Content-Disposition",
"attachment; filename=download.txt"
);
response.write("Download directory test");
});
server.start(-1);
registerCleanupFunction(() => new Promise(resolve => server.stop(resolve)));
let source = Services.io.newURI(
`http://localhost:${server.identity.primaryPort}/download`
);
let mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
let handlerService = Cc[
"@mozilla.org/uriloader/handler-service;1"
].getService(Ci.nsIHandlerService);
let handler = mimeService.getFromTypeAndExtension(
"application/x-save-directory-test",
"txt"
);
handler.preferredAction = Ci.nsIHandlerInfo.saveToDisk;
handler.alwaysAskBeforeHandling = false;
handlerService.store(handler);
registerCleanupFunction(() => handlerService.remove(handler));
let win = await openSaveWindow();
let initialDir = createSaveDirectory();
let chosenDir = createSaveDirectory();
await rememberDirectory(win, source, initialDir);
let shown = 0;
MockFilePicker.showCallback = picker => {
Assert.equal(
picker.displayDirectory.path,
shown ? chosenDir.path : initialDir.path,
"Location-only downloads reuse the directory selected by the previous save"
);
++shown;
MockFilePicker.setFiles([
fileInDirectory(chosenDir, `location-only-${shown}.txt`),
]);
return MockFilePicker.returnOK;
};
let list = await Downloads.getList(Downloads.ALL);
let completed = Promise.withResolvers();
let downloads = new Set();
let view = {
onDownloadChanged(download) {
if (download.source.url != source.spec) {
return;
}
downloads.add(download);
if (downloads.size == 3 && [...downloads].every(item => item.succeeded)) {
completed.resolve();
}
},
};
list.addView(view);
registerCleanupFunction(async () => {
list.removeView(view);
for (let download of downloads) {
await download.finalize(true);
await list.remove(download);
}
});
for (let i = 0; i < 3; ++i) {
win.gBrowser.addTab(source.spec, {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
}
await completed.promise;
Assert.equal(shown, 3, "All three downloads show a location picker");
Assert.ok(
[...downloads].every(download =>
download.target.path.startsWith(chosenDir.path)
),
"All three downloads finish in the selected directory"
);
await BrowserTestUtils.closeWindow(win);
});