summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/extensions/test/browser/browser_ext_compose_attachments.js
diff options
context:
space:
mode:
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.js2268
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();
+});