diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mail/components/extensions/test/browser/browser_ext_compose_attachments.js | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mail/components/extensions/test/browser/browser_ext_compose_attachments.js')
-rw-r--r-- | comm/mail/components/extensions/test/browser/browser_ext_compose_attachments.js | 2268 |
1 files changed, 2268 insertions, 0 deletions
diff --git a/comm/mail/components/extensions/test/browser/browser_ext_compose_attachments.js b/comm/mail/components/extensions/test/browser/browser_ext_compose_attachments.js new file mode 100644 index 0000000000..2b66b5a200 --- /dev/null +++ b/comm/mail/components/extensions/test/browser/browser_ext_compose_attachments.js @@ -0,0 +1,2268 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var { cloudFileAccounts } = ChromeUtils.import( + "resource:///modules/cloudFileAccounts.jsm" +); + +const { ExtensionUtils } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionUtils.sys.mjs" +); + +var { ExtensionSupport } = ChromeUtils.import( + "resource:///modules/ExtensionSupport.jsm" +); + +let account = createAccount(); +let defaultIdentity = addIdentity(account); + +function findWindow(subject) { + let windows = Array.from(Services.wm.getEnumerator("msgcompose")); + return windows.find(win => { + let composeFields = win.GetComposeDetails(); + return composeFields.subject == subject; + }); +} + +var MockCompleteGenericSendMessage = { + register() { + // For every compose window that opens, replace the function which does the + // actual sending with one that only records when it has been called. + MockCompleteGenericSendMessage._didTryToSendMessage = false; + ExtensionSupport.registerWindowListener("MockCompleteGenericSendMessage", { + chromeURLs: [ + "chrome://messenger/content/messengercompose/messengercompose.xhtml", + ], + onLoadWindow(window) { + window.CompleteGenericSendMessage = function (msgType) { + let items = [...window.gAttachmentBucket.itemChildren]; + for (let item of items) { + if (item.attachment.sendViaCloud && item.cloudFileAccount) { + item.cloudFileAccount.markAsImmutable(item.cloudFileUpload.id); + } + } + Services.obs.notifyObservers( + { + composeWindow: window, + }, + "mail:composeSendProgressStop" + ); + }; + }, + }); + }, + + unregister() { + ExtensionSupport.unregisterWindowListener("MockCompleteGenericSendMessage"); + }, +}; + +add_task(async function test_file_attachments() { + let files = { + "background.js": async () => { + let listener = { + events: [], + currentPromise: null, + + pushEvent(...args) { + browser.test.log(JSON.stringify(args)); + this.events.push(args); + if (this.currentPromise) { + let p = this.currentPromise; + this.currentPromise = null; + p.resolve(); + } + }, + async checkEvent(expectedEvent, ...expectedArgs) { + if (this.events.length == 0) { + await new Promise(resolve => (this.currentPromise = { resolve })); + } + let [actualEvent, ...actualArgs] = this.events.shift(); + browser.test.assertEq(expectedEvent, actualEvent); + browser.test.assertEq(expectedArgs.length, actualArgs.length); + + for (let i = 0; i < expectedArgs.length; i++) { + browser.test.assertEq(typeof expectedArgs[i], typeof actualArgs[i]); + if (typeof expectedArgs[i] == "object") { + for (let key of Object.keys(expectedArgs[i])) { + browser.test.assertEq(expectedArgs[i][key], actualArgs[i][key]); + } + } else { + browser.test.assertEq(expectedArgs[i], actualArgs[i]); + } + } + + return actualArgs; + }, + }; + browser.compose.onAttachmentAdded.addListener((...args) => + listener.pushEvent("onAttachmentAdded", ...args) + ); + browser.compose.onAttachmentRemoved.addListener((...args) => + listener.pushEvent("onAttachmentRemoved", ...args) + ); + + let checkData = async (attachment, size) => { + let data = await browser.compose.getAttachmentFile(attachment.id); + // eslint-disable-next-line mozilla/use-isInstance + browser.test.assertTrue(data instanceof File); + browser.test.assertEq(size, data.size); + }; + + let checkUI = async (composeTab, ...expected) => { + let attachments = await browser.compose.listAttachments(composeTab.id); + browser.test.assertEq(expected.length, attachments.length); + for (let i = 0; i < expected.length; i++) { + browser.test.assertEq(expected[i].id, attachments[i].id); + browser.test.assertEq(expected[i].size, attachments[i].size); + } + let details = await browser.compose.getComposeDetails(composeTab.id); + return window.sendMessage("checkUI", details, expected); + }; + + let createCloudfileAccount = () => { + let addListener = window.waitForEvent("cloudFile.onAccountAdded"); + browser.test.sendMessage("createAccount"); + return addListener; + }; + + let removeCloudfileAccount = id => { + let deleteListener = window.waitForEvent("cloudFile.onAccountDeleted"); + browser.test.sendMessage("removeAccount", id); + return deleteListener; + }; + + let [createdAccount] = await createCloudfileAccount(); + + let file1 = new File(["File number one!"], "file1.txt", { + type: "application/vnd.regify", + }); + let file2 = new File( + ["File number two? Yes, this is number two."], + "file2.txt" + ); + let file3 = new File(["I'm pretending to be file two."], "file3.txt"); + let composeTab = await browser.compose.beginNew({ + subject: "Message #1", + }); + + await checkUI(composeTab); + + // Add an attachment. + + let attachment1 = await browser.compose.addAttachment(composeTab.id, { + file: file1, + }); + browser.test.assertEq("file1.txt", attachment1.name); + browser.test.assertEq(16, attachment1.size); + await checkData(attachment1, file1.size); + + let [, added1] = await listener.checkEvent( + "onAttachmentAdded", + { id: composeTab.id }, + { id: attachment1.id, name: "file1.txt" } + ); + await checkData(added1, file1.size); + + await checkUI(composeTab, { + id: attachment1.id, + name: "file1.txt", + size: file1.size, + contentType: "application/vnd.regify", + }); + + // Add another attachment. + + let attachment2 = await browser.compose.addAttachment(composeTab.id, { + file: file2, + name: "this is file2.txt", + }); + browser.test.assertEq("this is file2.txt", attachment2.name); + browser.test.assertEq(41, attachment2.size); + await checkData(attachment2, file2.size); + + let [, added2] = await listener.checkEvent( + "onAttachmentAdded", + { id: composeTab.id }, + { id: attachment2.id, name: "this is file2.txt" } + ); + await checkData(added2, file2.size); + + await checkUI( + composeTab, + { + id: attachment1.id, + name: "file1.txt", + size: file1.size, + contentType: "application/vnd.regify", + }, + { id: attachment2.id, name: "this is file2.txt", size: file2.size } + ); + + // Change an attachment. + + let changed2 = await browser.compose.updateAttachment( + composeTab.id, + attachment2.id, + { + name: "file2 with a new name.txt", + } + ); + browser.test.assertEq("file2 with a new name.txt", changed2.name); + browser.test.assertEq(41, changed2.size); + await checkData(changed2, file2.size); + + await checkUI( + composeTab, + { + id: attachment1.id, + name: "file1.txt", + size: file1.size, + contentType: "application/vnd.regify", + }, + { + id: attachment2.id, + name: "file2 with a new name.txt", + size: file2.size, + } + ); + + let changed3 = await browser.compose.updateAttachment( + composeTab.id, + attachment2.id, + { file: file3 } + ); + browser.test.assertEq("file2 with a new name.txt", changed3.name); + browser.test.assertEq(30, changed3.size); + await checkData(changed3, file3.size); + + await checkUI( + composeTab, + { + id: attachment1.id, + name: "file1.txt", + size: file1.size, + contentType: "application/vnd.regify", + }, + { + id: attachment2.id, + name: "file2 with a new name.txt", + size: file3.size, + } + ); + + // Remove the first/local attachment. + + await browser.compose.removeAttachment(composeTab.id, attachment1.id); + await listener.checkEvent( + "onAttachmentRemoved", + { id: composeTab.id }, + attachment1.id + ); + + await checkUI(composeTab, { + id: attachment2.id, + name: "file2 with a new name.txt", + size: file3.size, + }); + + // Convert the second attachment to a cloudFile attachment. + + await new Promise(resolve => { + function fileListener(account, fileInfo, tab, relatedFileInfo) { + browser.cloudFile.onFileUpload.removeListener(fileListener); + browser.test.assertEq(1, fileInfo.id); + browser.test.assertEq(undefined, relatedFileInfo); + setTimeout(() => resolve()); + return { + url: "https://cloud.provider.net/1", + templateInfo: { + download_limit: "2", + service_name: "Superior Mochitest Service", + }, + }; + } + + browser.cloudFile.onFileUpload.addListener(fileListener); + // Conversion/upload is not yet supported via WebExt API. + browser.test.sendMessage( + "convertFile", + createdAccount.id, + "file2 with a new name.txt" + ); + }); + + // File retrieved by WebExt API should still be the real file. + await checkData(attachment2, 30); + + // UI should show both file size. + await checkUI(composeTab, { + id: attachment2.id, + name: "file2 with a new name.txt", + size: file3.size, + htmlSize: 4536, + }); + + // Rename the second/cloud attachment. + + await browser.test.assertRejects( + browser.compose.updateAttachment(composeTab.id, attachment2.id, { + name: "cloud file2 with a new name.txt", + }), + "Rename error: Missing cloudFile.onFileRename listener for compose.attachments@mochi.test", + "Provider should reject for missing rename support" + ); + + function cloudFileRenameListener(account, id) { + browser.cloudFile.onFileRename.removeListener(cloudFileRenameListener); + browser.test.assertEq(1, id); + return { url: "https://cloud.provider.net/2" }; + } + browser.cloudFile.onFileRename.addListener(cloudFileRenameListener); + + let changed4 = await browser.compose.updateAttachment( + composeTab.id, + attachment2.id, + { + name: "cloud file2 with a new name.txt", + } + ); + browser.test.assertEq("cloud file2 with a new name.txt", changed4.name); + browser.test.assertEq(30, changed4.size); + + await checkUI(composeTab, { + id: attachment2.id, + name: "cloud file2 with a new name.txt", + size: file3.size, + htmlSize: 4554, + }); + + // File retrieved by WebExt API should still be the real file. + await checkData(changed4, 30); + + // Update the second/cloud attachment. + + await browser.test.assertRejects( + browser.compose.updateAttachment(composeTab.id, attachment2.id, { + file: file2, + }), + "Upload error: Missing cloudFile.onFileUpload listener for compose.attachments@mochi.test (or it is not returning url or aborted)", + "Provider should reject due to upload errors" + ); + + function cloudFileUploadListener( + account, + fileInfo, + tab, + relatedFileInfo + ) { + browser.cloudFile.onFileUpload.removeListener(cloudFileUploadListener); + browser.test.assertEq(3, fileInfo.id); + browser.test.assertEq("cloud file2 with a new name.txt", fileInfo.name); + browser.test.assertEq(1, relatedFileInfo.id); + browser.test.assertEq( + "cloud file2 with a new name.txt", + relatedFileInfo.name + ); + browser.test.assertTrue( + relatedFileInfo.dataChanged, + `data should have changed` + ); + browser.test.assertEq( + "2", + relatedFileInfo.templateInfo.download_limit, + "templateInfo download_limit should be correct" + ); + browser.test.assertEq( + "Superior Mochitest Service", + relatedFileInfo.templateInfo.service_name, + "templateInfo service_name should be correct" + ); + return { url: "https://cloud.provider.net/3" }; + } + browser.cloudFile.onFileUpload.addListener(cloudFileUploadListener); + + let changed5 = await browser.compose.updateAttachment( + composeTab.id, + attachment2.id, + { file: file2 } + ); + + browser.test.assertEq("cloud file2 with a new name.txt", changed5.name); + browser.test.assertEq(41, changed5.size); + await checkData(changed5, file2.size); + + // Remove the second/cloud attachment. + + await browser.compose.removeAttachment(composeTab.id, attachment2.id); + + await listener.checkEvent( + "onAttachmentRemoved", + { id: composeTab.id }, + attachment2.id + ); + + await checkUI(composeTab); + + await browser.tabs.remove(composeTab.id); + browser.test.assertEq(0, listener.events.length); + + await removeCloudfileAccount(createdAccount.id); + browser.test.notifyPass("finished"); + }, + "utils.js": await getUtilsJS(), + }; + + let messenger = Cc["@mozilla.org/messenger;1"].createInstance( + Ci.nsIMessenger + ); + + let extension = ExtensionTestUtils.loadExtension({ + files, + manifest: { + cloud_file: { + name: "mochitest", + management_url: "/content/management.html", + }, + background: { scripts: ["utils.js", "background.js"] }, + permissions: ["compose"], + applications: { gecko: { id: "compose.attachments@mochi.test" } }, + }, + }); + + extension.onMessage("checkUI", (details, expected) => { + let composeWindow = findWindow(details.subject); + let composeDocument = composeWindow.document; + + let bucket = composeDocument.getElementById("attachmentBucket"); + Assert.equal(bucket.itemCount, expected.length); + + let totalSize = 0; + for (let i = 0; i < expected.length; i++) { + let item = bucket.itemChildren[i]; + let { name, size, htmlSize, contentType } = expected[i]; + totalSize += htmlSize ? htmlSize : size; + + let displaySize = messenger.formatFileSize(size); + if (htmlSize) { + displaySize = `${messenger.formatFileSize(htmlSize)} (${displaySize})`; + Assert.equal( + item.cloudHtmlFileSize, + htmlSize, + "htmlSize should be correct." + ); + } + + if (contentType) { + Assert.equal( + item.attachment.contentType, + contentType, + "contentType should be correct." + ); + } + + Assert.equal( + item.querySelector(".attachmentcell-name").textContent + + item.querySelector(".attachmentcell-extension").textContent, + name, + "Displayed name should be correct." + ); + Assert.equal( + item.querySelector(".attachmentcell-size").textContent, + displaySize, + "Displayed size should be correct." + ); + } + + let bucketTotal = composeDocument.getElementById("attachmentBucketSize"); + if (totalSize == 0) { + Assert.equal(bucketTotal.textContent, ""); + } else { + Assert.equal( + bucketTotal.textContent, + messenger.formatFileSize(totalSize), + "Total size should match." + ); + } + + extension.sendMessage(); + }); + + extension.onMessage("createAccount", () => { + cloudFileAccounts.createAccount("ext-compose.attachments@mochi.test"); + }); + + extension.onMessage("removeAccount", id => { + cloudFileAccounts.removeAccount(id); + }); + + extension.onMessage("convertFile", (cloudFileAccountId, attachmentName) => { + let composeWindow = Services.wm.getMostRecentWindow("msgcompose"); + let composeDocument = composeWindow.document; + let bucket = composeDocument.getElementById("attachmentBucket"); + let account = cloudFileAccounts.getAccount(cloudFileAccountId); + + let attachmentItem = bucket.itemChildren.find( + item => item.attachment && item.attachment.name == attachmentName + ); + + composeWindow.convertListItemsToCloudAttachment([attachmentItem], account); + }); + + await extension.startup(); + await extension.awaitFinish("finished"); + await extension.unload(); +}); + +add_task(async function test_compose_attachments() { + let files = { + "background.js": async () => { + let listener = { + events: [], + currentPromise: null, + + pushEvent(...args) { + browser.test.log(JSON.stringify(args)); + this.events.push(args); + if (this.currentPromise) { + let p = this.currentPromise; + this.currentPromise = null; + p.resolve(); + } + }, + async checkEvent(expectedEvent, ...expectedArgs) { + if (this.events.length == 0) { + await new Promise(resolve => (this.currentPromise = { resolve })); + } + let [actualEvent, ...actualArgs] = this.events.shift(); + browser.test.assertEq(expectedEvent, actualEvent); + browser.test.assertEq(expectedArgs.length, actualArgs.length); + + for (let i = 0; i < expectedArgs.length; i++) { + browser.test.assertEq(typeof expectedArgs[i], typeof actualArgs[i]); + if (typeof expectedArgs[i] == "object") { + for (let key of Object.keys(expectedArgs[i])) { + browser.test.assertEq(expectedArgs[i][key], actualArgs[i][key]); + } + } else { + browser.test.assertEq(expectedArgs[i], actualArgs[i]); + } + } + + return actualArgs; + }, + }; + browser.compose.onAttachmentAdded.addListener((...args) => + listener.pushEvent("onAttachmentAdded", ...args) + ); + browser.compose.onAttachmentRemoved.addListener((...args) => + listener.pushEvent("onAttachmentRemoved", ...args) + ); + + let checkData = async (attachment, size) => { + let data = await browser.compose.getAttachmentFile(attachment.id); + browser.test.assertTrue( + // eslint-disable-next-line mozilla/use-isInstance + data instanceof File, + "Returned file obj should be a File instance." + ); + browser.test.assertEq( + size, + data.size, + "Reported size should be correct." + ); + browser.test.assertEq( + attachment.name, + data.name, + "Name of the File object should match the name of the attachment." + ); + }; + + let checkUI = async (composeTab, ...expected) => { + let attachments = await browser.compose.listAttachments(composeTab.id); + browser.test.assertEq( + expected.length, + attachments.length, + "Number of found attachments should be correct." + ); + for (let i = 0; i < expected.length; i++) { + browser.test.assertEq(expected[i].id, attachments[i].id); + browser.test.assertEq(expected[i].size, attachments[i].size); + } + let details = await browser.compose.getComposeDetails(composeTab.id); + return window.sendMessage("checkUI", details, expected); + }; + + let createCloudfileAccount = () => { + let addListener = window.waitForEvent("cloudFile.onAccountAdded"); + browser.test.sendMessage("createAccount"); + return addListener; + }; + + let removeCloudfileAccount = id => { + let deleteListener = window.waitForEvent("cloudFile.onAccountDeleted"); + browser.test.sendMessage("removeAccount", id); + return deleteListener; + }; + + async function cloneAttachment( + attachment, + composeTab, + name = attachment.name + ) { + let clone; + + // If the name is not changed, try to pass in the full object. + if (name == attachment.name) { + clone = await browser.compose.addAttachment( + composeTab.id, + attachment + ); + } else { + clone = await browser.compose.addAttachment(composeTab.id, { + id: attachment.id, + name, + }); + } + + browser.test.assertEq(name, clone.name); + browser.test.assertEq(attachment.size, clone.size); + await checkData(clone, attachment.size); + + let [, added] = await listener.checkEvent( + "onAttachmentAdded", + { id: composeTab.id }, + { id: clone.id, name } + ); + await checkData(added, attachment.size); + return clone; + } + + let [createdAccount] = await createCloudfileAccount(); + + let file1 = new File(["File number one!"], "file1.txt"); + let file2 = new File( + ["File number two? Yes, this is number two."], + "file2.txt" + ); + + // ----------------------------------------------------------------------- + + let composeTab1 = await browser.compose.beginNew({ + subject: "Message #2", + }); + await checkUI(composeTab1); + + // Add an attachment to composeTab1. + + let tab1_attachment1 = await browser.compose.addAttachment( + composeTab1.id, + { + file: file1, + } + ); + browser.test.assertEq("file1.txt", tab1_attachment1.name); + browser.test.assertEq(16, tab1_attachment1.size); + await checkData(tab1_attachment1, file1.size); + + let [, tab1_added1] = await listener.checkEvent( + "onAttachmentAdded", + { id: composeTab1.id }, + { id: tab1_attachment1.id, name: "file1.txt" } + ); + await checkData(tab1_added1, file1.size); + + await checkUI(composeTab1, { + id: tab1_attachment1.id, + name: "file1.txt", + size: file1.size, + }); + + // Add another attachment to composeTab1. + + let tab1_attachment2 = await browser.compose.addAttachment( + composeTab1.id, + { + file: file2, + name: "this is file2.txt", + } + ); + browser.test.assertEq("this is file2.txt", tab1_attachment2.name); + browser.test.assertEq(41, tab1_attachment2.size); + await checkData(tab1_attachment2, file2.size); + + let [, tab1_added2] = await listener.checkEvent( + "onAttachmentAdded", + { id: composeTab1.id }, + { id: tab1_attachment2.id, name: "this is file2.txt" } + ); + await checkData(tab1_added2, file2.size); + + await checkUI( + composeTab1, + { id: tab1_attachment1.id, name: "file1.txt", size: file1.size }, + { id: tab1_attachment2.id, name: "this is file2.txt", size: file2.size } + ); + + // Convert the second attachment to a cloudFile attachment. + + await new Promise(resolve => { + function fileListener(account, fileInfo, tab, relatedFileInfo) { + browser.cloudFile.onFileUpload.removeListener(fileListener); + browser.test.assertEq(1, fileInfo.id); + browser.test.assertEq(undefined, relatedFileInfo); + setTimeout(() => resolve()); + return { url: "https://cloud.provider.net/1" }; + } + + browser.cloudFile.onFileUpload.addListener(fileListener); + // Conversion/upload is not yet supported via WebExt API. + browser.test.sendMessage( + "convertFile", + createdAccount.id, + "this is file2.txt" + ); + }); + + await checkUI( + composeTab1, + { + id: tab1_attachment1.id, + name: "file1.txt", + size: file1.size, + }, + { + id: tab1_attachment2.id, + name: "this is file2.txt", + size: 41, + htmlSize: 4300, + contentLocation: "https://cloud.provider.net/1", + } + ); + + // ----------------------------------------------------------------------- + + // Create a second compose window and clone both attachments from tab1. The + // second one should be cloned as a cloud attachment, having no size and the + // correct contentLocation. Both attachments will be renamed while cloning. + + // The cloud file rename should be handled as a new file upload, because + // the same url is used in tab1. The original attachment should be passed + // as relatedFileInfo. + let tab2_uploadPromise = new Promise(resolve => { + function fileListener(account, fileInfo, tab, relatedFileInfo) { + browser.cloudFile.onFileUpload.removeListener(fileListener); + browser.test.assertEq(2, fileInfo.id); + browser.test.assertEq("this is renamed file2.txt", fileInfo.name); + browser.test.assertEq(1, relatedFileInfo.id); + browser.test.assertEq("this is file2.txt", relatedFileInfo.name); + browser.test.assertFalse( + relatedFileInfo.dataChanged, + `data should not have changed` + ); + setTimeout(() => resolve()); + return { url: "https://cloud.provider.net/2" }; + } + + browser.cloudFile.onFileUpload.addListener(fileListener); + }); + + let composeTab2 = await browser.compose.beginNew({ + subject: "Message #3", + }); + let tab2_attachment1 = await cloneAttachment( + tab1_attachment1, + composeTab2, + "I want to be called file3.txt" + ); + await checkUI(composeTab2, { + id: tab2_attachment1.id, + name: "I want to be called file3.txt", + size: file1.size, + }); + + let tab2_attachment2 = await cloneAttachment( + tab1_attachment2, + composeTab2, + "this is renamed file2.txt" + ); + + await checkUI( + composeTab2, + { + id: tab2_attachment1.id, + name: "I want to be called file3.txt", + size: file1.size, + }, + { + id: tab2_attachment2.id, + name: "this is renamed file2.txt", + size: 41, + htmlSize: 4324, + contentLocation: "https://cloud.provider.net/2", + } + ); + + await tab2_uploadPromise; + + // ----------------------------------------------------------------------- + + // Create a 3rd compose window and clone both attachments from tab1. The + // second one should be cloned as cloud attachment, having no size and the + // correct contentLocation. Files are not renamed this time, so there should + // not be an upload request (which would fail without upload listener), as + // we simply re-attach the cloudFileUpload data. + + let composeTab3 = await browser.compose.beginNew({ + subject: "Message #4", + }); + let tab3_attachment1 = await cloneAttachment( + tab1_attachment1, + composeTab3 + ); + await checkUI(composeTab3, { + id: tab3_attachment1.id, + name: "file1.txt", + size: file1.size, + }); + + let tab3_attachment2 = await cloneAttachment( + tab1_attachment2, + composeTab3 + ); + + await checkUI( + composeTab3, + { + id: tab3_attachment1.id, + name: "file1.txt", + size: file1.size, + }, + { + id: tab3_attachment2.id, + name: "this is file2.txt", + size: 41, + htmlSize: 4300, + contentLocation: "https://cloud.provider.net/1", + } + ); + + // Rename the cloned cloud attachments of tab3. It should trigger a new + // upload, to not invalidate the original url still used in tab1. + + let tab3_uploadPromise = new Promise(resolve => { + function fileListener(account, fileInfo, tab, relatedFileInfo) { + browser.cloudFile.onFileUpload.removeListener(fileListener); + browser.test.assertEq(3, fileInfo.id); + browser.test.assertEq( + "That is going to be interesting.txt", + fileInfo.name + ); + browser.test.assertEq(1, relatedFileInfo.id); + browser.test.assertEq("this is file2.txt", relatedFileInfo.name); + browser.test.assertFalse( + relatedFileInfo.dataChanged, + `data should not have changed` + ); + setTimeout(() => resolve()); + return { url: "https://cloud.provider.net/3" }; + } + + browser.cloudFile.onFileUpload.addListener(fileListener); + }); + + let tab3_changed2 = await browser.compose.updateAttachment( + composeTab3.id, + tab3_attachment2.id, + { + name: "That is going to be interesting.txt", + } + ); + browser.test.assertEq( + "That is going to be interesting.txt", + tab3_changed2.name + ); + browser.test.assertEq(41, tab3_changed2.size); + await checkData(tab3_changed2, file2.size); + + await checkUI( + composeTab3, + { + id: tab3_attachment1.id, + name: "file1.txt", + size: file1.size, + }, + { + id: tab3_attachment2.id, + name: "That is going to be interesting.txt", + size: 41, + htmlSize: 4354, + contentLocation: "https://cloud.provider.net/3", + } + ); + + await tab3_uploadPromise; + + // ----------------------------------------------------------------------- + + // Open a 4th compose window and directly clone attachment1 and attachment2, + // renaming both. This should trigger a new file upload. + + let tab4_uploadPromise = new Promise(resolve => { + function fileListener(account, fileInfo, tab, relatedFileInfo) { + browser.cloudFile.onFileUpload.removeListener(fileListener); + browser.test.assertEq(4, fileInfo.id); + browser.test.assertEq( + "I got renamed too, how crazy is that!.txt", + fileInfo.name + ); + browser.test.assertEq(1, relatedFileInfo.id); + browser.test.assertEq("this is file2.txt", relatedFileInfo.name); + browser.test.assertFalse( + relatedFileInfo.dataChanged, + `data should not have changed` + ); + setTimeout(() => resolve()); + return { url: "https://cloud.provider.net/4" }; + } + + browser.cloudFile.onFileUpload.addListener(fileListener); + }); + + let tab4_details = { subject: "Message #5" }; + tab4_details.attachments = [ + Object.assign({}, tab1_attachment1), + Object.assign({}, tab1_attachment2), + ]; + tab4_details.attachments[0].name = "I got renamed.txt"; + tab4_details.attachments[1].name = + "I got renamed too, how crazy is that!.txt"; + let composeTab4 = await browser.compose.beginNew(tab4_details); + + // In this test we need to manually request the id of the added attachments. + let [tab4_attachment1, tab4_attachment2] = + await browser.compose.listAttachments(composeTab4.id); + + let [, addedReClone1] = await listener.checkEvent( + "onAttachmentAdded", + { id: composeTab4.id }, + { id: tab4_attachment1.id, name: "I got renamed.txt" } + ); + await checkData(addedReClone1, file1.size); + let [, addedReClone2] = await listener.checkEvent( + "onAttachmentAdded", + { id: composeTab4.id }, + { + id: tab4_attachment2.id, + name: "I got renamed too, how crazy is that!.txt", + } + ); + await checkData(addedReClone2, file2.size); + + await checkUI( + composeTab4, + { + id: tab4_attachment1.id, + name: "I got renamed.txt", + size: file1.size, + }, + { + id: tab4_attachment2.id, + name: "I got renamed too, how crazy is that!.txt", + size: 41, + htmlSize: 4372, + contentLocation: "https://cloud.provider.net/4", + } + ); + + await tab4_uploadPromise; + + // ----------------------------------------------------------------------- + + // Open a 5th compose window and directly clone attachment1 and attachment2 + // from tab1. + + let tab5_details = { subject: "Message #6" }; + tab5_details.attachments = [tab1_attachment1, tab1_attachment2]; + let composeTab5 = await browser.compose.beginNew(tab5_details); + + // In this test we need to manually request the id of the added attachments. + let [tab5_attachment1, tab5_attachment2] = + await browser.compose.listAttachments(composeTab5.id); + + await listener.checkEvent( + "onAttachmentAdded", + { id: composeTab5.id }, + { id: tab5_attachment1.id, name: "file1.txt" } + ); + await listener.checkEvent( + "onAttachmentAdded", + { id: composeTab5.id }, + { id: tab5_attachment2.id, name: "this is file2.txt" } + ); + + // Delete the cloud attachment2 in tab1, which should not trigger a cloud + // delete, as the url is still used in tab5. + + function fileListener(account, id, tab) { + browser.test.fail( + `The onFileDeleted listener should not fire for deleting a cloud file which is still used in another tab.` + ); + } + browser.cloudFile.onFileDeleted.addListener(fileListener); + + await browser.compose.removeAttachment( + composeTab1.id, + tab1_attachment2.id + ); + await listener.checkEvent( + "onAttachmentRemoved", + { id: composeTab1.id }, + tab1_attachment2.id + ); + browser.cloudFile.onFileDeleted.removeListener(fileListener); + + // Renaming cloud attachment2 in tab5 should now be a simple rename, as the + // url is not used anywhere anymore. + + let tab5_renamePromise = new Promise(resolve => { + function fileListener() { + browser.cloudFile.onFileRename.removeListener(fileListener); + setTimeout(() => resolve()); + } + browser.cloudFile.onFileRename.addListener(fileListener); + }); + + await browser.compose.updateAttachment( + composeTab5.id, + tab5_attachment2.id, + { + name: "I am the only one left.txt", + } + ); + await tab5_renamePromise; + + // Delete the cloud attachment2 in tab5, which now should trigger a cloud + // delete. + + let tab5_deletePromise = new Promise(resolve => { + function fileListener(account, id, tab) { + browser.cloudFile.onFileDeleted.removeListener(fileListener); + setTimeout(() => resolve(id)); + } + browser.cloudFile.onFileDeleted.addListener(fileListener); + }); + + await browser.compose.removeAttachment( + composeTab5.id, + tab5_attachment2.id + ); + await listener.checkEvent( + "onAttachmentRemoved", + { id: composeTab5.id }, + tab5_attachment2.id + ); + await tab5_deletePromise; + + // Clean up + + await browser.tabs.remove(composeTab5.id); + await browser.tabs.remove(composeTab4.id); + await browser.tabs.remove(composeTab3.id); + await browser.tabs.remove(composeTab2.id); + await browser.tabs.remove(composeTab1.id); + browser.test.assertEq(0, listener.events.length); + + await removeCloudfileAccount(createdAccount.id); + browser.test.notifyPass("finished"); + }, + "utils.js": await getUtilsJS(), + }; + + let messenger = Cc["@mozilla.org/messenger;1"].createInstance( + Ci.nsIMessenger + ); + + let extension = ExtensionTestUtils.loadExtension({ + files, + manifest: { + cloud_file: { + name: "mochitest", + management_url: "/content/management.html", + }, + background: { scripts: ["utils.js", "background.js"] }, + permissions: ["compose"], + applications: { gecko: { id: "compose.attachments@mochi.test" } }, + }, + }); + + extension.onMessage("checkUI", (details, expected) => { + let composeWindow = findWindow(details.subject); + let composeDocument = composeWindow.document; + + let bucket = composeDocument.getElementById("attachmentBucket"); + Assert.equal(bucket.itemCount, expected.length); + + let totalSize = 0; + for (let i = 0; i < expected.length; i++) { + let item = bucket.itemChildren[i]; + let { name, size, htmlSize, contentLocation } = expected[i]; + totalSize += htmlSize ? htmlSize : size; + + let displaySize = messenger.formatFileSize(size); + if (htmlSize) { + displaySize = `${messenger.formatFileSize(htmlSize)} (${displaySize})`; + Assert.equal( + item.cloudHtmlFileSize, + htmlSize, + "htmlSize should be correct." + ); + } + + // size and name are checked against the displayed values, contentLocation + // is checked against the associated attachment. + + if (contentLocation) { + Assert.equal( + item.attachment.contentLocation, + contentLocation, + "contentLocation for cloud files should be correct." + ); + Assert.equal( + item.attachment.sendViaCloud, + true, + "sendViaCloud for cloud files should be correct." + ); + } else { + Assert.equal( + item.attachment.contentLocation, + "", + "contentLocation for cloud files should be correct." + ); + Assert.equal( + item.attachment.sendViaCloud, + false, + "sendViaCloud for cloud files should be correct." + ); + } + + Assert.equal( + item.querySelector(".attachmentcell-name").textContent + + item.querySelector(".attachmentcell-extension").textContent, + name, + "Name should be correct." + ); + Assert.equal( + item.querySelector(".attachmentcell-size").textContent, + displaySize, + "Displayed size should be correct." + ); + } + + let bucketTotal = composeDocument.getElementById("attachmentBucketSize"); + if (totalSize == 0) { + Assert.equal(bucketTotal.textContent, ""); + } else { + Assert.equal( + bucketTotal.textContent, + messenger.formatFileSize(totalSize) + ); + } + + extension.sendMessage(); + }); + + extension.onMessage("createAccount", () => { + cloudFileAccounts.createAccount("ext-compose.attachments@mochi.test"); + }); + + extension.onMessage("removeAccount", id => { + cloudFileAccounts.removeAccount(id); + }); + + extension.onMessage("convertFile", (cloudFileAccountId, attachmentName) => { + let composeWindow = Services.wm.getMostRecentWindow("msgcompose"); + let composeDocument = composeWindow.document; + let bucket = composeDocument.getElementById("attachmentBucket"); + let account = cloudFileAccounts.getAccount(cloudFileAccountId); + + let attachmentItem = bucket.itemChildren.find( + item => item.attachment && item.attachment.name == attachmentName + ); + + composeWindow.convertListItemsToCloudAttachment([attachmentItem], account); + }); + + await extension.startup(); + await extension.awaitFinish("finished"); + await extension.unload(); +}); + +add_task(async function test_compose_attachments_immutable() { + MockCompleteGenericSendMessage.register(); + + let files = { + "background.js": async () => { + let listener = { + events: [], + currentPromise: null, + + pushEvent(...args) { + browser.test.log(JSON.stringify(args)); + this.events.push(args); + if (this.currentPromise) { + let p = this.currentPromise; + this.currentPromise = null; + p.resolve(); + } + }, + async checkEvent(expectedEvent, ...expectedArgs) { + if (this.events.length == 0) { + await new Promise(resolve => (this.currentPromise = { resolve })); + } + let [actualEvent, ...actualArgs] = this.events.shift(); + browser.test.assertEq(expectedEvent, actualEvent); + browser.test.assertEq(expectedArgs.length, actualArgs.length); + + for (let i = 0; i < expectedArgs.length; i++) { + browser.test.assertEq(typeof expectedArgs[i], typeof actualArgs[i]); + if (typeof expectedArgs[i] == "object") { + for (let key of Object.keys(expectedArgs[i])) { + browser.test.assertEq(expectedArgs[i][key], actualArgs[i][key]); + } + } else { + browser.test.assertEq(expectedArgs[i], actualArgs[i]); + } + } + + return actualArgs; + }, + }; + browser.compose.onAttachmentAdded.addListener((...args) => + listener.pushEvent("onAttachmentAdded", ...args) + ); + browser.compose.onAttachmentRemoved.addListener((...args) => + listener.pushEvent("onAttachmentRemoved", ...args) + ); + + let checkData = async (attachment, size) => { + let data = await browser.compose.getAttachmentFile(attachment.id); + browser.test.assertTrue( + // eslint-disable-next-line mozilla/use-isInstance + data instanceof File, + "Returned file obj should be a File instance." + ); + browser.test.assertEq( + size, + data.size, + "Reported size should be correct." + ); + browser.test.assertEq( + attachment.name, + data.name, + "Name of the File object should match the name of the attachment." + ); + }; + + let checkUI = async (composeTab, ...expected) => { + let attachments = await browser.compose.listAttachments(composeTab.id); + browser.test.assertEq( + expected.length, + attachments.length, + "Number of found attachments should be correct." + ); + for (let i = 0; i < expected.length; i++) { + browser.test.assertEq(expected[i].id, attachments[i].id); + browser.test.assertEq(expected[i].size, attachments[i].size); + } + let details = await browser.compose.getComposeDetails(composeTab.id); + return window.sendMessage("checkUI", details, expected); + }; + + let createCloudfileAccount = () => { + let addListener = window.waitForEvent("cloudFile.onAccountAdded"); + browser.test.sendMessage("createAccount"); + return addListener; + }; + + let removeCloudfileAccount = id => { + let deleteListener = window.waitForEvent("cloudFile.onAccountDeleted"); + browser.test.sendMessage("removeAccount", id); + return deleteListener; + }; + + async function cloneAttachment( + attachment, + composeTab, + name = attachment.name + ) { + let clone; + + // If the name is not changed, try to pass in the full object. + if (name == attachment.name) { + clone = await browser.compose.addAttachment( + composeTab.id, + attachment + ); + } else { + clone = await browser.compose.addAttachment(composeTab.id, { + id: attachment.id, + name, + }); + } + + browser.test.assertEq(name, clone.name); + browser.test.assertEq(attachment.size, clone.size); + await checkData(clone, attachment.size); + + let [, added] = await listener.checkEvent( + "onAttachmentAdded", + { id: composeTab.id }, + { id: clone.id, name } + ); + await checkData(added, attachment.size); + return clone; + } + + let [createdAccount] = await createCloudfileAccount(); + + let file1 = new File(["File number one!"], "file1.txt"); + let file2 = new File( + ["File number two? Yes, this is number two."], + "file2.txt" + ); + + // ----------------------------------------------------------------------- + + let composeTab1 = await browser.compose.beginNew({ + to: "user@inter.net", + subject: "Test", + }); + await checkUI(composeTab1); + + // Add an attachment to composeTab1. + + let tab1_attachment1 = await browser.compose.addAttachment( + composeTab1.id, + { + file: file1, + } + ); + browser.test.assertEq("file1.txt", tab1_attachment1.name); + browser.test.assertEq(16, tab1_attachment1.size); + await checkData(tab1_attachment1, file1.size); + + let [, tab1_added1] = await listener.checkEvent( + "onAttachmentAdded", + { id: composeTab1.id }, + { id: tab1_attachment1.id, name: "file1.txt" } + ); + await checkData(tab1_added1, file1.size); + + await checkUI(composeTab1, { + id: tab1_attachment1.id, + name: "file1.txt", + size: file1.size, + }); + + // Add another attachment to composeTab1. + + let tab1_attachment2 = await browser.compose.addAttachment( + composeTab1.id, + { + file: file2, + name: "this is file2.txt", + } + ); + browser.test.assertEq("this is file2.txt", tab1_attachment2.name); + browser.test.assertEq(41, tab1_attachment2.size); + await checkData(tab1_attachment2, file2.size); + + let [, tab1_added2] = await listener.checkEvent( + "onAttachmentAdded", + { id: composeTab1.id }, + { id: tab1_attachment2.id, name: "this is file2.txt" } + ); + await checkData(tab1_added2, file2.size); + + await checkUI( + composeTab1, + { id: tab1_attachment1.id, name: "file1.txt", size: file1.size }, + { id: tab1_attachment2.id, name: "this is file2.txt", size: file2.size } + ); + + // Convert the second attachment to a cloudFile attachment. + + await new Promise(resolve => { + function fileListener(account, fileInfo, tab, relatedFileInfo) { + browser.cloudFile.onFileUpload.removeListener(fileListener); + browser.test.assertEq(1, fileInfo.id); + browser.test.assertEq(undefined, relatedFileInfo); + setTimeout(() => resolve()); + return { url: "https://cloud.provider.net/1" }; + } + + browser.cloudFile.onFileUpload.addListener(fileListener); + // Conversion/upload is not yet supported via WebExt API. + browser.test.sendMessage( + "convertFile", + createdAccount.id, + "this is file2.txt" + ); + }); + + await checkUI( + composeTab1, + { + id: tab1_attachment1.id, + name: "file1.txt", + size: file1.size, + }, + { + id: tab1_attachment2.id, + name: "this is file2.txt", + size: 41, + htmlSize: 4300, + contentLocation: "https://cloud.provider.net/1", + } + ); + + // ----------------------------------------------------------------------- + + // Create a second compose window and clone both attachments from tab1. The + // second one should be cloned as a cloud attachment, having no size and the + // correct contentLocation. + + let composeTab2 = await browser.compose.beginNew({ + subject: "Message #7", + }); + let tab2_attachment1 = await cloneAttachment( + tab1_attachment1, + composeTab2 + ); + await checkUI(composeTab2, { + id: tab2_attachment1.id, + name: "file1.txt", + size: file1.size, + }); + + let tab2_attachment2 = await cloneAttachment( + tab1_attachment2, + composeTab2, + "this is file2.txt" + ); + + await checkUI( + composeTab2, + { + id: tab2_attachment1.id, + name: "file1.txt", + size: file1.size, + }, + { + id: tab2_attachment2.id, + name: "this is file2.txt", + size: 41, + htmlSize: 4300, + contentLocation: "https://cloud.provider.net/1", + } + ); + + // Send the message and have its attachment marked as immutable. + await browser.compose.sendMessage(composeTab1.id, { mode: "sendNow" }); + await browser.tabs.remove(composeTab1.id); + + // Delete the cloud attachment2 in tab2, which should not trigger a cloud + // delete, as the url has been marked as immutable by sending the message + // in tab1. + + function fileListener(account, id, tab) { + browser.test.fail( + `The onFileDeleted listener should not fire for deleting a cloud file marked as immutable.` + ); + } + browser.cloudFile.onFileDeleted.addListener(fileListener); + + await browser.compose.removeAttachment( + composeTab2.id, + tab2_attachment2.id + ); + await listener.checkEvent( + "onAttachmentRemoved", + { id: composeTab2.id }, + tab2_attachment2.id + ); + + // Clean up + + await browser.tabs.remove(composeTab2.id); + browser.test.assertEq(0, listener.events.length); + + await removeCloudfileAccount(createdAccount.id); + browser.test.notifyPass("finished"); + }, + "utils.js": await getUtilsJS(), + }; + + let messenger = Cc["@mozilla.org/messenger;1"].createInstance( + Ci.nsIMessenger + ); + + let extension = ExtensionTestUtils.loadExtension({ + files, + manifest: { + cloud_file: { + name: "mochitest", + management_url: "/content/management.html", + }, + background: { scripts: ["utils.js", "background.js"] }, + permissions: ["compose", "compose.send"], + applications: { gecko: { id: "compose.attachments@mochi.test" } }, + }, + }); + + extension.onMessage("checkUI", (details, expected) => { + let composeWindow = findWindow(details.subject); + let composeDocument = composeWindow.document; + + let bucket = composeDocument.getElementById("attachmentBucket"); + Assert.equal(bucket.itemCount, expected.length); + + let totalSize = 0; + for (let i = 0; i < expected.length; i++) { + let item = bucket.itemChildren[i]; + let { name, size, htmlSize, contentLocation } = expected[i]; + totalSize += htmlSize ? htmlSize : size; + + let displaySize = messenger.formatFileSize(size); + if (htmlSize) { + displaySize = `${messenger.formatFileSize(htmlSize)} (${displaySize})`; + Assert.equal( + item.cloudHtmlFileSize, + htmlSize, + "htmlSize should be correct." + ); + } + + // size and name are checked against the displayed values, contentLocation + // is checked against the associated attachment. + + if (contentLocation) { + Assert.equal( + item.attachment.contentLocation, + contentLocation, + "contentLocation for cloud files should be correct." + ); + Assert.equal( + item.attachment.sendViaCloud, + true, + "sendViaCloud for cloud files should be correct." + ); + } else { + Assert.equal( + item.attachment.contentLocation, + "", + "contentLocation for cloud files should be correct." + ); + Assert.equal( + item.attachment.sendViaCloud, + false, + "sendViaCloud for cloud files should be correct." + ); + } + + Assert.equal( + item.querySelector(".attachmentcell-name").textContent + + item.querySelector(".attachmentcell-extension").textContent, + name, + "Name should be correct." + ); + Assert.equal( + item.querySelector(".attachmentcell-size").textContent, + displaySize, + "Displayed size should be correct." + ); + } + + let bucketTotal = composeDocument.getElementById("attachmentBucketSize"); + if (totalSize == 0) { + Assert.equal(bucketTotal.textContent, ""); + } else { + Assert.equal( + bucketTotal.textContent, + messenger.formatFileSize(totalSize) + ); + } + + extension.sendMessage(); + }); + + extension.onMessage("createAccount", () => { + cloudFileAccounts.createAccount("ext-compose.attachments@mochi.test"); + }); + + extension.onMessage("removeAccount", id => { + cloudFileAccounts.removeAccount(id); + }); + + extension.onMessage("convertFile", (cloudFileAccountId, attachmentName) => { + let composeWindow = Services.wm.getMostRecentWindow("msgcompose"); + let composeDocument = composeWindow.document; + let bucket = composeDocument.getElementById("attachmentBucket"); + let account = cloudFileAccounts.getAccount(cloudFileAccountId); + + let attachmentItem = bucket.itemChildren.find( + item => item.attachment && item.attachment.name == attachmentName + ); + + composeWindow.convertListItemsToCloudAttachment([attachmentItem], account); + }); + + await extension.startup(); + await extension.awaitFinish("finished"); + await extension.unload(); + + MockCompleteGenericSendMessage.unregister(); +}); + +add_task(async function test_compose_attachments_no_reuse() { + let files = { + "background.js": async () => { + let listener = { + events: [], + currentPromise: null, + + pushEvent(...args) { + browser.test.log(JSON.stringify(args)); + this.events.push(args); + if (this.currentPromise) { + let p = this.currentPromise; + this.currentPromise = null; + p.resolve(); + } + }, + async checkEvent(expectedEvent, ...expectedArgs) { + if (this.events.length == 0) { + await new Promise(resolve => (this.currentPromise = { resolve })); + } + let [actualEvent, ...actualArgs] = this.events.shift(); + browser.test.assertEq(expectedEvent, actualEvent); + browser.test.assertEq(expectedArgs.length, actualArgs.length); + + for (let i = 0; i < expectedArgs.length; i++) { + browser.test.assertEq(typeof expectedArgs[i], typeof actualArgs[i]); + if (typeof expectedArgs[i] == "object") { + for (let key of Object.keys(expectedArgs[i])) { + browser.test.assertEq(expectedArgs[i][key], actualArgs[i][key]); + } + } else { + browser.test.assertEq(expectedArgs[i], actualArgs[i]); + } + } + + return actualArgs; + }, + }; + browser.compose.onAttachmentAdded.addListener((...args) => + listener.pushEvent("onAttachmentAdded", ...args) + ); + browser.compose.onAttachmentRemoved.addListener((...args) => + listener.pushEvent("onAttachmentRemoved", ...args) + ); + + let checkData = async (attachment, size) => { + let data = await browser.compose.getAttachmentFile(attachment.id); + browser.test.assertTrue( + // eslint-disable-next-line mozilla/use-isInstance + data instanceof File, + "Returned file obj should be a File instance." + ); + browser.test.assertEq( + size, + data.size, + "Reported size should be correct." + ); + browser.test.assertEq( + attachment.name, + data.name, + "Name of the File object should match the name of the attachment." + ); + }; + + let checkUI = async (composeTab, ...expected) => { + let attachments = await browser.compose.listAttachments(composeTab.id); + browser.test.assertEq( + expected.length, + attachments.length, + "Number of found attachments should be correct." + ); + for (let i = 0; i < expected.length; i++) { + browser.test.assertEq(expected[i].id, attachments[i].id); + browser.test.assertEq(expected[i].size, attachments[i].size); + } + let details = await browser.compose.getComposeDetails(composeTab.id); + return window.sendMessage("checkUI", details, expected); + }; + + let createCloudfileAccount = () => { + let addListener = window.waitForEvent("cloudFile.onAccountAdded"); + browser.test.sendMessage("createAccount"); + return addListener; + }; + + let removeCloudfileAccount = id => { + let deleteListener = window.waitForEvent("cloudFile.onAccountDeleted"); + browser.test.sendMessage("removeAccount", id); + return deleteListener; + }; + + async function cloneAttachment( + attachment, + composeTab, + name = attachment.name + ) { + let clone; + + // If the name is not changed, try to pass in the full object. + if (name == attachment.name) { + clone = await browser.compose.addAttachment( + composeTab.id, + attachment + ); + } else { + clone = await browser.compose.addAttachment(composeTab.id, { + id: attachment.id, + name, + }); + } + + browser.test.assertEq(name, clone.name); + browser.test.assertEq(attachment.size, clone.size); + await checkData(clone, attachment.size); + + let [, added] = await listener.checkEvent( + "onAttachmentAdded", + { id: composeTab.id }, + { id: clone.id, name } + ); + await checkData(added, attachment.size); + return clone; + } + + let [createdAccount] = await createCloudfileAccount(); + + let file1 = new File(["File number one!"], "file1.txt"); + let file2 = new File( + ["File number two? Yes, this is number two."], + "file2.txt" + ); + + // ----------------------------------------------------------------------- + + let composeTab1 = await browser.compose.beginNew({ + subject: "Message #8", + }); + await checkUI(composeTab1); + + // Add an attachment to composeTab1. + + let tab1_attachment1 = await browser.compose.addAttachment( + composeTab1.id, + { + file: file1, + } + ); + browser.test.assertEq("file1.txt", tab1_attachment1.name); + browser.test.assertEq(16, tab1_attachment1.size); + await checkData(tab1_attachment1, file1.size); + + let [, tab1_added1] = await listener.checkEvent( + "onAttachmentAdded", + { id: composeTab1.id }, + { id: tab1_attachment1.id, name: "file1.txt" } + ); + await checkData(tab1_added1, file1.size); + + await checkUI(composeTab1, { + id: tab1_attachment1.id, + name: "file1.txt", + size: file1.size, + }); + + // Add another attachment to composeTab1. + + let tab1_attachment2 = await browser.compose.addAttachment( + composeTab1.id, + { + file: file2, + name: "this is file2.txt", + } + ); + browser.test.assertEq("this is file2.txt", tab1_attachment2.name); + browser.test.assertEq(41, tab1_attachment2.size); + await checkData(tab1_attachment2, file2.size); + + let [, tab1_added2] = await listener.checkEvent( + "onAttachmentAdded", + { id: composeTab1.id }, + { id: tab1_attachment2.id, name: "this is file2.txt" } + ); + await checkData(tab1_added2, file2.size); + + await checkUI( + composeTab1, + { id: tab1_attachment1.id, name: "file1.txt", size: file1.size }, + { id: tab1_attachment2.id, name: "this is file2.txt", size: file2.size } + ); + + // Convert the second attachment to a cloudFile attachment. + + await new Promise(resolve => { + function fileListener(account, fileInfo, tab, relatedFileInfo) { + browser.cloudFile.onFileUpload.removeListener(fileListener); + browser.test.assertEq(1, fileInfo.id); + browser.test.assertEq(undefined, relatedFileInfo); + setTimeout(() => resolve()); + return { url: "https://cloud.provider.net/1" }; + } + + browser.cloudFile.onFileUpload.addListener(fileListener); + // Conversion/upload is not yet supported via WebExt API. + browser.test.sendMessage( + "convertFile", + createdAccount.id, + "this is file2.txt" + ); + }); + + await checkUI( + composeTab1, + { + id: tab1_attachment1.id, + name: "file1.txt", + size: file1.size, + }, + { + id: tab1_attachment2.id, + name: "this is file2.txt", + size: 41, + htmlSize: 4300, + contentLocation: "https://cloud.provider.net/1", + } + ); + + // ----------------------------------------------------------------------- + + // Create a second compose window and clone both attachments from tab1. The + // second one should be cloned as a cloud attachment, having no size and the + // correct contentLocation. + // Attachments are not renamed, but since reuse_uploads is disabled, a new + // upload request must be issued. The original attachment should be passed + // as relatedFileInfo. + let tab2_uploadPromise = new Promise(resolve => { + function fileListener(account, fileInfo, tab, relatedFileInfo) { + browser.cloudFile.onFileUpload.removeListener(fileListener); + browser.test.assertEq(2, fileInfo.id); + browser.test.assertEq("this is file2.txt", fileInfo.name); + browser.test.assertEq(1, relatedFileInfo.id); + browser.test.assertEq("this is file2.txt", relatedFileInfo.name); + browser.test.assertFalse( + relatedFileInfo.dataChanged, + `data should not have changed` + ); + setTimeout(() => resolve()); + return { url: "https://cloud.provider.net/2" }; + } + + browser.cloudFile.onFileUpload.addListener(fileListener); + }); + + let composeTab2 = await browser.compose.beginNew({ + subject: "Message #9", + }); + let tab2_attachment1 = await cloneAttachment( + tab1_attachment1, + composeTab2 + ); + await checkUI(composeTab2, { + id: tab2_attachment1.id, + name: "file1.txt", + size: file1.size, + }); + + let tab2_attachment2 = await cloneAttachment( + tab1_attachment2, + composeTab2, + "this is file2.txt" + ); + + await checkUI( + composeTab2, + { + id: tab2_attachment1.id, + name: "file1.txt", + size: file1.size, + }, + { + id: tab2_attachment2.id, + name: "this is file2.txt", + size: 41, + htmlSize: 4300, + contentLocation: "https://cloud.provider.net/2", + } + ); + + await tab2_uploadPromise; + + await browser.tabs.remove(composeTab2.id); + await browser.tabs.remove(composeTab1.id); + browser.test.assertEq(0, listener.events.length); + + await removeCloudfileAccount(createdAccount.id); + browser.test.notifyPass("finished"); + }, + "utils.js": await getUtilsJS(), + }; + + let messenger = Cc["@mozilla.org/messenger;1"].createInstance( + Ci.nsIMessenger + ); + + let extension = ExtensionTestUtils.loadExtension({ + files, + manifest: { + cloud_file: { + name: "mochitest", + management_url: "/content/management.html", + reuse_uploads: false, + }, + background: { scripts: ["utils.js", "background.js"] }, + permissions: ["compose"], + applications: { gecko: { id: "compose.attachments@mochi.test" } }, + }, + }); + + extension.onMessage("checkUI", (details, expected) => { + let composeWindow = findWindow(details.subject); + let composeDocument = composeWindow.document; + + let bucket = composeDocument.getElementById("attachmentBucket"); + Assert.equal(bucket.itemCount, expected.length); + + let totalSize = 0; + for (let i = 0; i < expected.length; i++) { + let item = bucket.itemChildren[i]; + let { name, size, htmlSize, contentLocation } = expected[i]; + totalSize += htmlSize ? htmlSize : size; + + let displaySize = messenger.formatFileSize(size); + if (htmlSize) { + displaySize = `${messenger.formatFileSize(htmlSize)} (${displaySize})`; + Assert.equal( + item.cloudHtmlFileSize, + htmlSize, + "htmlSize should be correct." + ); + } + + // size and name are checked against the displayed values, contentLocation + // is checked against the associated attachment. + + if (contentLocation) { + Assert.equal( + item.attachment.contentLocation, + contentLocation, + "contentLocation for cloud files should be correct." + ); + Assert.equal( + item.attachment.sendViaCloud, + true, + "sendViaCloud for cloud files should be correct." + ); + } else { + Assert.equal( + item.attachment.contentLocation, + "", + "contentLocation for cloud files should be correct." + ); + Assert.equal( + item.attachment.sendViaCloud, + false, + "sendViaCloud for cloud files should be correct." + ); + } + + Assert.equal( + item.querySelector(".attachmentcell-name").textContent + + item.querySelector(".attachmentcell-extension").textContent, + name, + "Name should be correct." + ); + Assert.equal( + item.querySelector(".attachmentcell-size").textContent, + displaySize, + "Displayed size should be correct." + ); + } + + let bucketTotal = composeDocument.getElementById("attachmentBucketSize"); + if (totalSize == 0) { + Assert.equal(bucketTotal.textContent, ""); + } else { + Assert.equal( + bucketTotal.textContent, + messenger.formatFileSize(totalSize) + ); + } + + extension.sendMessage(); + }); + + extension.onMessage("createAccount", () => { + cloudFileAccounts.createAccount("ext-compose.attachments@mochi.test"); + }); + + extension.onMessage("removeAccount", id => { + cloudFileAccounts.removeAccount(id); + }); + + extension.onMessage("convertFile", (cloudFileAccountId, attachmentName) => { + let composeWindow = Services.wm.getMostRecentWindow("msgcompose"); + let composeDocument = composeWindow.document; + let bucket = composeDocument.getElementById("attachmentBucket"); + let account = cloudFileAccounts.getAccount(cloudFileAccountId); + + let attachmentItem = bucket.itemChildren.find( + item => item.attachment && item.attachment.name == attachmentName + ); + + composeWindow.convertListItemsToCloudAttachment([attachmentItem], account); + }); + + await extension.startup(); + await extension.awaitFinish("finished"); + await extension.unload(); +}); + +add_task(async function test_without_permission() { + let files = { + "background.js": async () => { + // Try to use onAttachmentAdded. + await browser.test.assertThrows( + () => browser.compose.onAttachmentAdded.addListener(), + /browser\.compose\.onAttachmentAdded is undefined/, + "Should reject listener without proper permission" + ); + + // Try to use onAttachmentRemoved. + await browser.test.assertThrows( + () => browser.compose.onAttachmentRemoved.addListener(), + /browser\.compose\.onAttachmentRemoved is undefined/, + "Should reject listener without proper permission" + ); + + // Try to use listAttachments. + await browser.test.assertThrows( + () => browser.compose.listAttachments(), + `browser.compose.listAttachments is not a function`, + "Should reject function without proper permission" + ); + + // Try to use addAttachment. + await browser.test.assertThrows( + () => browser.compose.addAttachment(), + `browser.compose.addAttachment is not a function`, + "Should reject function without proper permission" + ); + + // Try to use updateAttachment. + await browser.test.assertThrows( + () => browser.compose.updateAttachment(), + `browser.compose.updateAttachment is not a function`, + "Should reject function without proper permission" + ); + + // Try to use removeAttachment. + await browser.test.assertThrows( + () => browser.compose.removeAttachment(), + `browser.compose.removeAttachment is not a function`, + "Should reject function without proper permission" + ); + + browser.test.notifyPass("finished"); + }, + "utils.js": await getUtilsJS(), + }; + + let extension = ExtensionTestUtils.loadExtension({ + files, + manifest: { + background: { scripts: ["utils.js", "background.js"] }, + permissions: [], + }, + }); + + await extension.startup(); + await extension.awaitFinish("finished"); + await extension.unload(); +}); + +add_task(async function test_attachment_MV3_event_pages() { + let files = { + "background.js": async () => { + // Whenever the extension starts or wakes up, the eventCounter is reset and + // allows to observe the order of events fired. In case of a wake-up, the + // first observed event is the one that woke up the background. + let eventCounter = 0; + + browser.compose.onAttachmentAdded.addListener(async (tab, attachment) => { + browser.test.sendMessage("attachment added", { + eventCount: ++eventCounter, + attachment, + }); + }); + + browser.compose.onAttachmentRemoved.addListener( + async (tab, attachmentId) => { + browser.test.sendMessage("attachment removed", { + eventCount: ++eventCounter, + attachmentId, + }); + } + ); + + browser.test.sendMessage("background started"); + }, + "utils.js": await getUtilsJS(), + }; + let extension = ExtensionTestUtils.loadExtension({ + files, + manifest: { + manifest_version: 3, + background: { scripts: ["utils.js", "background.js"] }, + permissions: ["accountsRead", "compose", "messagesRead"], + browser_specific_settings: { + gecko: { id: "compose.attachment@mochi.test" }, + }, + }, + }); + + async function addAttachment(ordinal) { + let attachment = Cc[ + "@mozilla.org/messengercompose/attachment;1" + ].createInstance(Ci.nsIMsgAttachment); + attachment.name = `${ordinal}.txt`; + attachment.url = `data:text/plain,I'm the ${ordinal} attachment!`; + attachment.size = attachment.url.length - 16; + + await composeWindow.AddAttachments([attachment]); + return attachment; + } + + async function removeAttachment(attachment) { + let item = + composeWindow.gAttachmentBucket.findItemForAttachment(attachment); + await composeWindow.RemoveAttachments([item]); + } + + function checkPersistentListeners({ primed }) { + // A persistent event is referenced by its moduleName as defined in + // ext-mails.json, not by its actual namespace. + const persistent_events = [ + "compose.onAttachmentAdded", + "compose.onAttachmentRemoved", + ]; + + for (let event of persistent_events) { + let [moduleName, eventName] = event.split("."); + assertPersistentListeners(extension, moduleName, eventName, { + primed, + }); + } + } + + let composeWindow = await openComposeWindow(account); + await focusWindow(composeWindow); + + await extension.startup(); + await extension.awaitMessage("background started"); + // The listeners should be persistent, but not primed. + checkPersistentListeners({ primed: false }); + + // Trigger events without terminating the background first. + + let rawFirstAttachment = await addAttachment("first"); + let addedFirst = await extension.awaitMessage("attachment added"); + Assert.equal( + "first.txt", + rawFirstAttachment.name, + "Created attachment should be correct" + ); + Assert.equal( + "first.txt", + addedFirst.attachment.name, + "Attachment returned by onAttachmentAdded should be correct" + ); + Assert.equal(1, addedFirst.eventCount, "Event counter should be correct"); + + await removeAttachment(rawFirstAttachment); + + let removedFirst = await extension.awaitMessage("attachment removed"); + Assert.equal( + addedFirst.attachment.id, + removedFirst.attachmentId, + "Attachment id returned by onAttachmentRemoved should be correct" + ); + Assert.equal(2, removedFirst.eventCount, "Event counter should be correct"); + + // Terminate background and re-trigger onAttachmentAdded event. + + await extension.terminateBackground({ disableResetIdleForTest: true }); + // The listeners should be primed. + checkPersistentListeners({ primed: true }); + + let rawSecondAttachment = await addAttachment("second"); + let addedSecond = await extension.awaitMessage("attachment added"); + Assert.equal( + "second.txt", + rawSecondAttachment.name, + "Created attachment should be correct" + ); + Assert.equal( + "second.txt", + addedSecond.attachment.name, + "Attachment returned by onAttachmentAdded should be correct" + ); + Assert.equal(1, addedSecond.eventCount, "Event counter should be correct"); + + // The background should have been restarted. + await extension.awaitMessage("background started"); + // The listeners should no longer be primed. + checkPersistentListeners({ primed: false }); + + // Terminate background and re-trigger onAttachmentRemoved event. + + await extension.terminateBackground({ disableResetIdleForTest: true }); + // The listeners should be primed. + checkPersistentListeners({ primed: true }); + + await removeAttachment(rawSecondAttachment); + let removedSecond = await extension.awaitMessage("attachment removed"); + Assert.equal( + addedSecond.attachment.id, + removedSecond.attachmentId, + "Attachment id returned by onAttachmentRemoved should be correct" + ); + Assert.equal(1, removedSecond.eventCount, "Event counter should be correct"); + + // The background should have been restarted. + await extension.awaitMessage("background started"); + // The listeners should no longer be primed. + checkPersistentListeners({ primed: false }); + + await extension.unload(); + composeWindow.close(); +}); |