summaryrefslogtreecommitdiffstats
path: root/comm/mail/test/browser/cloudfile
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/test/browser/cloudfile')
-rw-r--r--comm/mail/test/browser/cloudfile/browser.ini52
-rw-r--r--comm/mail/test/browser/cloudfile/browser_attachmentErrors.js440
-rw-r--r--comm/mail/test/browser/cloudfile/browser_attachmentItem.js451
-rw-r--r--comm/mail/test/browser/cloudfile/browser_attachmentUrls.js1554
-rw-r--r--comm/mail/test/browser/cloudfile/browser_filelinkTelemetry.js123
-rw-r--r--comm/mail/test/browser/cloudfile/browser_notifications.js565
-rw-r--r--comm/mail/test/browser/cloudfile/data/testFile11
-rw-r--r--comm/mail/test/browser/cloudfile/data/testFile23
-rw-r--r--comm/mail/test/browser/cloudfile/data/testFile33
-rw-r--r--comm/mail/test/browser/cloudfile/data/testFile41
-rw-r--r--comm/mail/test/browser/cloudfile/head.js7
-rw-r--r--comm/mail/test/browser/cloudfile/html/settings-with-link.xhtml9
12 files changed, 3209 insertions, 0 deletions
diff --git a/comm/mail/test/browser/cloudfile/browser.ini b/comm/mail/test/browser/cloudfile/browser.ini
new file mode 100644
index 0000000000..0ea38fdc52
--- /dev/null
+++ b/comm/mail/test/browser/cloudfile/browser.ini
@@ -0,0 +1,52 @@
+[DEFAULT]
+head = head.js
+prefs =
+ mail.account.account1.server=server1
+ mail.account.account2.identities=id1,id2
+ mail.account.account2.server=server2
+ mail.accountmanager.accounts=account1,account2
+ mail.accountmanager.defaultaccount=account2
+ mail.accountmanager.localfoldersserver=server1
+ mail.identity.id1.fullName=Tinderbox
+ mail.identity.id1.htmlSigFormat=false
+ mail.identity.id1.smtpServer=smtp1
+ mail.identity.id1.useremail=tinderbox@foo.invalid
+ mail.identity.id1.valid=true
+ mail.identity.id2.fullName=Tinderboxpushlog
+ mail.identity.id2.htmlSigFormat=true
+ mail.identity.id2.smtpServer=smtp1
+ mail.identity.id2.useremail=tinderboxpushlog@foo.invalid
+ mail.identity.id2.valid=true
+ mail.provider.suppress_dialog_on_startup=true
+ mail.server.server1.type=none
+ mail.server.server1.userName=nobody
+ mail.server.server2.check_new_mail=false
+ mail.server.server2.directory-rel=[ProfD]Mail/tinderbox
+ mail.server.server2.download_on_biff=true
+ mail.server.server2.hostname=tinderbox123
+ mail.server.server2.login_at_startup=false
+ mail.server.server2.name=tinderbox@foo.invalid
+ mail.server.server2.type=pop3
+ mail.server.server2.userName=tinderbox
+ mail.server.server2.whiteListAbURI=
+ mail.shell.checkDefaultClient=false
+ mail.smtp.defaultserver=smtp1
+ mail.smtpserver.smtp1.hostname=tinderbox123
+ mail.smtpserver.smtp1.username=tinderbox
+ mail.smtpservers=smtp1
+ mail.spotlight.firstRunDone=true
+ mail.startup.enabledMailCheckOnce=true
+ mail.winsearch.firstRunDone=true
+ mailnews.start_page.override_url=about:blank
+ mailnews.start_page.url=about:blank
+ datareporting.policy.dataSubmissionPolicyBypassNotification=true
+subsuite = thunderbird
+support-files =
+ data/**
+ html/**
+
+[browser_attachmentItem.js]
+[browser_attachmentUrls.js]
+[browser_attachmentErrors.js]
+[browser_notifications.js]
+[browser_filelinkTelemetry.js]
diff --git a/comm/mail/test/browser/cloudfile/browser_attachmentErrors.js b/comm/mail/test/browser/cloudfile/browser_attachmentErrors.js
new file mode 100644
index 0000000000..e2cf049c1c
--- /dev/null
+++ b/comm/mail/test/browser/cloudfile/browser_attachmentErrors.js
@@ -0,0 +1,440 @@
+/* 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/. */
+
+/**
+ * Tests CloudFile alerts on errors.
+ */
+
+"use strict";
+
+XPCOMUtils.defineLazyGetter(this, "brandShortName", () =>
+ Services.strings
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandShortName")
+);
+
+var { gMockFilePicker, gMockFilePickReg, select_attachments } =
+ ChromeUtils.import("resource://testing-common/mozmill/AttachmentHelpers.jsm");
+var { gMockCloudfileManager, MockCloudfileAccount } = ChromeUtils.import(
+ "resource://testing-common/mozmill/CloudfileHelpers.jsm"
+);
+var {
+ add_cloud_attachments,
+ rename_selected_cloud_attachment,
+ close_compose_window,
+ open_compose_new_mail,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ add_message_to_folder,
+ create_message,
+ FAKE_SERVER_HOSTNAME,
+ get_special_folder,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+var { cloudFileAccounts } = ChromeUtils.import(
+ "resource:///modules/cloudFileAccounts.jsm"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var kHtmlPrefKey = "mail.identity.default.compose_html";
+var kDefaultSigKey = "mail.identity.id1.htmlSigText";
+var kDefaultSig = "This is my signature.\n\nCheck out my website sometime!";
+var kFiles = ["./data/testFile1", "./data/testFile2"];
+
+var gInbox;
+
+function test_expected_included(actual, expected, description) {
+ Assert.equal(
+ actual.length,
+ expected.length,
+ `${description}: correct length`
+ );
+ for (let i = 0; i < expected.length; i++) {
+ for (let item of Object.keys(expected[i])) {
+ Assert.equal(
+ actual[i][item],
+ expected[i][item],
+ `${description}: ${item} exists and is correct`
+ );
+ }
+ }
+}
+
+add_setup(async function () {
+ requestLongerTimeout(3);
+
+ // These prefs can't be set in the manifest as they contain white-space.
+ Services.prefs.setStringPref(
+ "mail.identity.id1.htmlSigText",
+ "Tinderbox is soo 90ies"
+ );
+ Services.prefs.setStringPref(
+ "mail.identity.id2.htmlSigText",
+ "Tinderboxpushlog is the new <b>hotness!</b>"
+ );
+
+ // For replies and forwards, we'll work off a message in the Inbox folder
+ // of the fake "tinderbox" account.
+ let server = MailServices.accounts.findServer(
+ "tinderbox",
+ FAKE_SERVER_HOSTNAME,
+ "pop3"
+ );
+ gInbox = await get_special_folder(Ci.nsMsgFolderFlags.Inbox, false, server);
+ await add_message_to_folder([gInbox], create_message());
+
+ gMockFilePickReg.register();
+ gMockCloudfileManager.register();
+
+ Services.prefs.setBoolPref(kHtmlPrefKey, true);
+
+ // Don't create paragraphs in the test.
+ // The test fails if it encounters paragraphs <p> instead of breaks <br>.
+ Services.prefs.setBoolPref("mail.compose.default_to_paragraph", false);
+});
+
+registerCleanupFunction(function () {
+ gMockCloudfileManager.unregister();
+ gMockFilePickReg.unregister();
+ Services.prefs.clearUserPref(kDefaultSigKey);
+ Services.prefs.clearUserPref(kHtmlPrefKey);
+ Services.prefs.clearUserPref("mail.compose.default_to_paragraph");
+});
+
+/**
+ * Test that we get the correct alert message when the provider reports a custom
+ * error during upload operation.
+ */
+add_task(function test_custom_error_during_upload() {
+ subtest_errors_during_upload({
+ exception: {
+ message: "This is a custom error.",
+ result: cloudFileAccounts.constants.uploadErrWithCustomMessage,
+ },
+ expectedAlerts: [
+ {
+ title: "Uploading testFile1 to providerA Failed",
+ message: "This is a custom error.",
+ },
+ {
+ title: "Uploading testFile2 to providerA Failed",
+ message: "This is a custom error.",
+ },
+ ],
+ });
+});
+
+/**
+ * Test that we get the correct alert message when the provider reports a standard
+ * error during upload operation.
+ */
+add_task(function test_standard_error_during_upload() {
+ subtest_errors_during_upload({
+ exception: {
+ message: "This is a standard error.",
+ result: cloudFileAccounts.constants.uploadErr,
+ },
+ expectedAlerts: [
+ {
+ title: "Upload Error",
+ message: "Unable to upload testFile1 to providerA.",
+ },
+ {
+ title: "Upload Error",
+ message: "Unable to upload testFile2 to providerA.",
+ },
+ ],
+ });
+});
+
+/**
+ * Test that we get the correct alert message when the provider reports a quota
+ * error.
+ */
+add_task(function test_quota_error_during_upload() {
+ subtest_errors_during_upload({
+ exception: {
+ message: "Quota Error.",
+ result: cloudFileAccounts.constants.uploadWouldExceedQuota,
+ },
+ expectedAlerts: [
+ {
+ title: "Quota Error",
+ message:
+ "Uploading testFile1 to providerA would exceed your space quota.",
+ },
+ {
+ title: "Quota Error",
+ message:
+ "Uploading testFile2 to providerA would exceed your space quota.",
+ },
+ ],
+ });
+});
+
+/**
+ * Test that we get the correct alert message when the provider reports a file
+ * size exceeded error.
+ */
+add_task(function test_file_size_error_during_upload() {
+ subtest_errors_during_upload({
+ exception: {
+ message: "File Size Error.",
+ result: cloudFileAccounts.constants.uploadExceedsFileLimit,
+ },
+ expectedAlerts: [
+ {
+ title: "File Size Error",
+ message: "testFile1 exceeds the maximum size for providerA.",
+ },
+ {
+ title: "File Size Error",
+ message: "testFile2 exceeds the maximum size for providerA.",
+ },
+ ],
+ });
+});
+
+/**
+ * Test that we get the connection error in offline mode.
+ */
+add_task(function test_offline_error_during_upload() {
+ subtest_errors_during_upload({
+ toggleOffline: true,
+ expectedAlerts: [
+ {
+ title: "Connection Error",
+ message: `${brandShortName} is offline. Could not connect to providerA.`,
+ },
+ {
+ title: "Connection Error",
+ message: `${brandShortName} is offline. Could not connect to providerA.`,
+ },
+ ],
+ });
+});
+
+/**
+ * Subtest for testing error messages during upload operation.
+ *
+ * @param error - defines the the thrown exception and the expected alert messages
+ * @param error.exception - the exception to be thrown by uploadFile()
+ * @param error.expectedAlerts - array with { title, message } objects for expected
+ * alerts for each uploaded file
+ */
+function subtest_errors_during_upload(error) {
+ gMockFilePicker.returnFiles = collectFiles(kFiles);
+ let provider = new MockCloudfileAccount();
+ let config = {
+ serviceName: "MochiTest A",
+ serviceUrl: "https://www.provider-A.org",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ };
+ if (error.exception) {
+ config.uploadError = error.exception;
+ }
+ provider.init("providerA", config);
+
+ let cw = open_compose_new_mail();
+
+ if (error.toggleOffline) {
+ Services.io.offline = true;
+ }
+ let seenAlerts = add_cloud_attachments(
+ cw,
+ provider,
+ false,
+ error.expectedAlerts.length
+ );
+ if (error.toggleOffline) {
+ Services.io.offline = false;
+ }
+
+ Assert.equal(
+ seenAlerts.length,
+ error.expectedAlerts.length,
+ "Should have seen the correct number of alerts."
+ );
+ for (let i = 0; i < error.expectedAlerts.length; i++) {
+ Assert.equal(
+ error.expectedAlerts[i].title,
+ seenAlerts[i].title,
+ "Alert should have the correct title."
+ );
+ Assert.equal(
+ error.expectedAlerts[i].message,
+ seenAlerts[i].message,
+ "Alert should have the correct message."
+ );
+ }
+ close_compose_window(cw);
+}
+
+/**
+ * Test that we get the correct alert message when the provider does not support
+ * renaming.
+ */
+add_task(function test_nosupport_error_during_rename() {
+ subtest_errors_during_rename({
+ exception: {
+ message: "Rename not supported.",
+ result: cloudFileAccounts.constants.renameNotSupported,
+ },
+ expectedAlerts: [
+ {
+ title: "Rename Error",
+ message: "providerA does not support renaming already uploaded files.",
+ },
+ {
+ title: "Rename Error",
+ message: "providerA does not support renaming already uploaded files.",
+ },
+ ],
+ });
+});
+
+/**
+ * Test that we get the correct alert message when the provider reports a standard
+ * error during rename operation.
+ */
+add_task(function test_standard_error_during_rename() {
+ subtest_errors_during_rename({
+ exception: {
+ message: "Rename error.",
+ result: cloudFileAccounts.constants.renameErr,
+ },
+ expectedAlerts: [
+ {
+ title: "Rename Error",
+ message: "There was a problem renaming testFile1 on providerA.",
+ },
+ {
+ title: "Rename Error",
+ message: "There was a problem renaming testFile2 on providerA.",
+ },
+ ],
+ });
+});
+
+/**
+ * Test that we get the correct alert message when the provider reports a custom
+ * error during rename operation.
+ */
+add_task(function test_custom_error_during_rename() {
+ subtest_errors_during_rename({
+ exception: {
+ message: "This is a custom error.",
+ result: cloudFileAccounts.constants.renameErrWithCustomMessage,
+ },
+ expectedAlerts: [
+ {
+ title: "Renaming testFile1 on providerA Failed",
+ message: "This is a custom error.",
+ },
+ {
+ title: "Renaming testFile2 on providerA Failed",
+ message: "This is a custom error.",
+ },
+ ],
+ });
+});
+
+/**
+ * Test that we get the connection error in offline mode.
+ */
+add_task(function test_offline_error_during_rename() {
+ subtest_errors_during_rename({
+ toggleOffline: true,
+ expectedAlerts: [
+ {
+ title: "Connection Error",
+ message: `${brandShortName} is offline. Could not connect to providerA.`,
+ },
+ {
+ title: "Connection Error",
+ message: `${brandShortName} is offline. Could not connect to providerA.`,
+ },
+ ],
+ });
+});
+
+/**
+ * Subtest for testing error messages during rename operation.
+ *
+ * @param error - defines the the thrown exception and the expected alert messagees
+ * @param error.exception - the exception to be thrown by renameFile()
+ * @param error.expectedAlerts - array with { title, message } objects for each renamed file
+ */
+function subtest_errors_during_rename(error) {
+ gMockFilePicker.returnFiles = collectFiles(kFiles);
+ let provider = new MockCloudfileAccount();
+ let config = {
+ serviceName: "MochiTest A",
+ serviceUrl: "https://www.provider-A.org",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ };
+ if (error.exception) {
+ config.renameError = error.exception;
+ }
+ provider.init("providerA", config);
+
+ let cw = open_compose_new_mail();
+ let uploads = add_cloud_attachments(cw, provider);
+ test_expected_included(
+ uploads,
+ [
+ {
+ url: "https://www.example.com/providerA/testFile1",
+ name: "testFile1",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceName: "MochiTest A",
+ serviceUrl: "https://www.provider-A.org",
+ },
+ {
+ url: "https://www.example.com/providerA/testFile2",
+ name: "testFile2",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceName: "MochiTest A",
+ serviceUrl: "https://www.provider-A.org",
+ },
+ ],
+ `Expected values in uploads array before renaming the files`
+ );
+
+ // Try to rename each Filelink, ensuring that we get the correct alerts.
+ if (error.toggleOffline) {
+ Services.io.offline = true;
+ }
+ let seenAlerts = [];
+ for (let i = 0; i < kFiles.length; ++i) {
+ select_attachments(cw, i);
+ seenAlerts.push(rename_selected_cloud_attachment(cw, "IgnoredNewName"));
+ }
+ if (error.toggleOffline) {
+ Services.io.offline = false;
+ }
+
+ Assert.equal(
+ seenAlerts.length,
+ error.expectedAlerts.length,
+ "Should have seen the correct number of alerts."
+ );
+ for (let i = 0; i < error.expectedAlerts.length; i++) {
+ Assert.equal(
+ error.expectedAlerts[i].title,
+ seenAlerts[i].title,
+ "Alert should have the correct title."
+ );
+ Assert.equal(
+ error.expectedAlerts[i].message,
+ seenAlerts[i].message,
+ "Alert should have the correct message."
+ );
+ }
+ close_compose_window(cw);
+}
diff --git a/comm/mail/test/browser/cloudfile/browser_attachmentItem.js b/comm/mail/test/browser/cloudfile/browser_attachmentItem.js
new file mode 100644
index 0000000000..18c3d92e41
--- /dev/null
+++ b/comm/mail/test/browser/cloudfile/browser_attachmentItem.js
@@ -0,0 +1,451 @@
+/* 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/. */
+
+/**
+ * Tests Filelink attachment item behaviour.
+ */
+
+"use strict";
+
+var utils = ChromeUtils.import("resource://testing-common/mozmill/utils.jsm");
+var { gMockFilePicker, gMockFilePickReg, select_attachments } =
+ ChromeUtils.import("resource://testing-common/mozmill/AttachmentHelpers.jsm");
+var { getFile, gMockCloudfileManager, MockCloudfileAccount } =
+ ChromeUtils.import("resource://testing-common/mozmill/CloudfileHelpers.jsm");
+var {
+ add_cloud_attachments,
+ convert_selected_to_cloud_attachment,
+ close_compose_window,
+ open_compose_new_mail,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var { close_popup, mc } = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+var { cloudFileAccounts } = ChromeUtils.import(
+ "resource:///modules/cloudFileAccounts.jsm"
+);
+
+var kAttachmentItemContextID = "msgComposeAttachmentItemContext";
+
+// Prepare the mock prompt.
+var originalPromptService = Services.prompt;
+var mockPromptService = {
+ alertCount: 0,
+ alert() {
+ this.alertCount++;
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]),
+};
+
+add_setup(function () {
+ Services.prompt = mockPromptService;
+ gMockFilePickReg.register();
+ gMockCloudfileManager.register();
+});
+
+registerCleanupFunction(function () {
+ gMockCloudfileManager.unregister();
+ gMockFilePickReg.unregister();
+ Services.prompt = originalPromptService;
+});
+
+/**
+ * Test that when an upload has been started, we can cancel and restart
+ * the upload, and then cancel again. For this test, we repeat this
+ * 3 times.
+ */
+add_task(async function test_upload_cancel_repeat() {
+ const kFile = "./data/testFile1";
+
+ // Prepare the mock file picker to return our test file.
+ let file = new FileUtils.File(getTestFilePath(kFile));
+ gMockFilePicker.returnFiles = [file];
+
+ let provider = new MockCloudfileAccount();
+ provider.init("someKey");
+ let cw = open_compose_new_mail(mc);
+
+ // We've got a compose window open, and our mock Filelink provider
+ // ready. Let's attach a file...
+ cw.window.AttachFile();
+
+ // Now we override the uploadFile function of the MockCloudfileAccount
+ // so that we're perpetually uploading...
+ let promise;
+ let started;
+ provider.uploadFile = function (window, aFile) {
+ return new Promise((resolve, reject) => {
+ promise = { resolve, reject };
+ started = true;
+ });
+ };
+
+ const kAttempts = 3;
+ for (let i = 0; i < kAttempts; i++) {
+ promise = null;
+ started = false;
+
+ let bucket = cw.window.document.getElementById("attachmentBucket");
+ Assert.equal(
+ bucket.itemCount,
+ 1,
+ "Should find correct number of attachments before converting."
+ );
+
+ // Select the attachment, and choose to convert it to a Filelink
+ select_attachments(cw, 0)[0];
+ cw.window.convertSelectedToCloudAttachment(provider);
+ utils.waitFor(() => started);
+
+ await assert_can_cancel_upload(cw, provider, promise, file);
+ await new Promise(resolve => setTimeout(resolve));
+
+ // A cancelled conversion must not remove the attachment.
+ Assert.equal(
+ bucket.itemCount,
+ 1,
+ "Should find correct number of attachments after converting."
+ );
+ }
+
+ close_compose_window(cw);
+});
+
+/**
+ * Test that we can cancel a whole series of files being uploaded at once.
+ */
+add_task(async function test_upload_multiple_and_cancel() {
+ const kFiles = ["./data/testFile1", "./data/testFile2", "./data/testFile3"];
+
+ // Prepare the mock file picker to return our test file.
+ let files = collectFiles(kFiles);
+ gMockFilePicker.returnFiles = files;
+
+ let provider = new MockCloudfileAccount();
+ provider.init("someKey");
+ let cw = open_compose_new_mail();
+
+ let promises = {};
+ provider.uploadFile = function (window, aFile) {
+ return new Promise((resolve, reject) => {
+ promises[aFile.leafName] = { resolve, reject };
+ });
+ };
+
+ add_cloud_attachments(cw, provider, false);
+
+ let bucket = cw.window.document.getElementById("attachmentBucket");
+ Assert.equal(
+ bucket.itemCount,
+ kFiles.length,
+ "Should find correct number of attachments before uploading."
+ );
+
+ for (let i = files.length - 1; i >= 0; --i) {
+ await assert_can_cancel_upload(
+ cw,
+ provider,
+ promises[files[i].leafName],
+ files[i]
+ );
+ }
+
+ // The cancelled attachment uploads should have been removed.
+ Assert.equal(
+ bucket.itemCount,
+ 0,
+ "Should find correct number of attachments after uploading."
+ );
+
+ close_compose_window(cw);
+});
+
+/**
+ * Helper function that takes an upload in progress, and cancels it,
+ * ensuring that the nsIMsgCloudFileProvider.uploadCanceled status message
+ * is returned to the passed in listener.
+ *
+ * @param aController the compose window controller to use.
+ * @param aProvider a MockCloudfileAccount for which the uploads have already
+ * started.
+ * @param aListener the nsIRequestObserver passed to aProvider's uploadFile
+ * function.
+ * @param aTargetFile the nsIFile to cancel the upload for.
+ */
+async function assert_can_cancel_upload(
+ aController,
+ aProvider,
+ aPromise,
+ aTargetFile
+) {
+ let cancelled = false;
+
+ // Override the provider's cancelFileUpload function. We can do this because
+ // it's assumed that the provider is a MockCloudfileAccount.
+ aProvider.cancelFileUpload = function (window, aFileToCancel) {
+ if (aTargetFile.equals(aFileToCancel)) {
+ aPromise.reject(
+ Components.Exception(
+ "Upload cancelled.",
+ cloudFileAccounts.constants.uploadCancelled
+ )
+ );
+ cancelled = true;
+ }
+ };
+
+ // Retrieve the attachment bucket index for the target file...
+ let index = get_attachmentitem_index_for_file(aController, aTargetFile);
+
+ // Select that attachmentitem in the bucket
+ select_attachments(aController, index)[0];
+
+ // Bring up the context menu, and click cancel.
+ let cmd = aController.window.document.getElementById("cmd_cancelUpload");
+ aController.window.updateAttachmentItems();
+
+ Assert.ok(!cmd.hidden, "cmd_cancelUpload should be shown");
+ Assert.ok(!cmd.disabled, "cmd_cancelUpload should be enabled");
+
+ let attachmentItem =
+ aController.window.document.getElementById("attachmentBucket").selectedItem;
+ let contextMenu = aController.window.document.getElementById(
+ "msgComposeAttachmentItemContext"
+ );
+
+ let popupPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ attachmentItem,
+ { type: "contextmenu", button: 2 },
+ attachmentItem.ownerGlobal
+ );
+ await popupPromise;
+
+ let cancelItem = aController.window.document.getElementById(
+ "composeAttachmentContext_cancelUploadItem"
+ );
+ if (AppConstants.platform == "macosx") {
+ // We need to use click() since the synthesizeMouseAtCenter doesn't work for
+ // context menu items on macos.
+ cancelItem.click();
+ } else {
+ EventUtils.synthesizeMouseAtCenter(cancelItem, {}, cancelItem.ownerGlobal);
+ await new Promise(resolve => setTimeout(resolve));
+ }
+
+ // Close the popup, and wait for the cancellation to be complete.
+ await close_popup(
+ aController,
+ aController.window.document.getElementById(kAttachmentItemContextID)
+ );
+ utils.waitFor(() => cancelled);
+}
+
+/**
+ * A helper function to find the attachment bucket index for a particular
+ * nsIFile. Returns null if no attachmentitem is found.
+ *
+ * @param aController the compose window controller to use.
+ * @param aFile the nsIFile to search for.
+ */
+function get_attachmentitem_index_for_file(aController, aFile) {
+ // Get the fileUrl from the file.
+ let fileUrl = aController.window.FileToAttachment(aFile).url;
+
+ // Get the bucket, and go through each item looking for the matching
+ // attachmentitem.
+ let bucket = aController.window.document.getElementById("attachmentBucket");
+ for (let i = 0; i < bucket.getRowCount(); ++i) {
+ let attachmentitem = bucket.getItemAtIndex(i);
+ if (attachmentitem.attachment.url == fileUrl) {
+ return i;
+ }
+ }
+ return null;
+}
+
+/**
+ * Helper function to start uploads and check number and icon of attachments
+ * after successful or failed uploads.
+ *
+ * @param error - to be returned error by uploadFile in case of failure
+ * @param expectedAttachments - number of expected attachments at the end of the test
+ * @param expectedAlerts - number of expected alerts at the end of the test
+ */
+async function test_upload(cw, error, expectedAttachments, expectedAlerts = 0) {
+ const kFiles = ["./data/testFile1", "./data/testFile2", "./data/testFile3"];
+
+ // Prepare the mock file picker to return our test file.
+ let files = collectFiles(kFiles);
+ gMockFilePicker.returnFiles = files;
+
+ let provider = new MockCloudfileAccount();
+ provider.init("someKey");
+
+ // Override the uploadFile function of the MockCloudfileAccount.
+ let promises = [];
+ provider.uploadFile = function (window, aFile) {
+ return new Promise((resolve, reject) => {
+ promises.push({
+ resolve,
+ reject,
+ upload: {
+ url: `https://example.org/${aFile.leafName}`,
+ size: aFile.fileSize,
+ path: aFile.path,
+ },
+ });
+ });
+ };
+
+ add_cloud_attachments(cw, provider, false);
+ utils.waitFor(() => promises.length == kFiles.length);
+
+ let bucket = cw.window.document.getElementById("attachmentBucket");
+ Assert.equal(
+ bucket.itemCount,
+ kFiles.length,
+ "Should find correct number of attachments before uploading."
+ );
+
+ for (let item of bucket.itemChildren) {
+ is(
+ item.querySelector("img.attachmentcell-icon").src,
+ "chrome://global/skin/icons/loading.png",
+ "CloudFile icon should be the loading spinner."
+ );
+ }
+
+ for (let promise of promises) {
+ if (error) {
+ promise.reject(error);
+ } else {
+ promise.resolve(promise.upload);
+ }
+ }
+ await new Promise(resolve => setTimeout(resolve));
+
+ Assert.equal(
+ bucket.itemCount,
+ expectedAttachments,
+ "Should find correct number of attachments after uploading."
+ );
+ // Check if the spinner is no longer shown, but the expected moz-icon.
+ for (let item of bucket.itemChildren) {
+ ok(
+ item
+ .querySelector("img.attachmentcell-icon")
+ .src.startsWith("moz-icon://testFile"),
+ "CloudFile icon should be correct."
+ );
+ }
+
+ // Check and reset the prompt mock service.
+ is(
+ expectedAlerts,
+ Services.prompt.alertCount,
+ "Number of expected alert prompts should be correct."
+ );
+ Services.prompt.alertCount = 0;
+}
+
+/**
+ * Check if attachment is removed if upload failed.
+ */
+add_task(async function test_error_upload() {
+ let cw = open_compose_new_mail();
+ await test_upload(
+ cw,
+ Components.Exception(
+ "Upload error.",
+ cloudFileAccounts.constants.uploadErr
+ ),
+ 0,
+ 3
+ );
+ close_compose_window(cw);
+});
+
+/**
+ * Check if attachment is not removed if upload is successful.
+ */
+add_task(async function test_successful_upload() {
+ let cw = open_compose_new_mail();
+ await test_upload(cw, null, 3, 0);
+ close_compose_window(cw);
+});
+
+/**
+ * Check if the original cloud attachment is kept, after converting it to another
+ * provider failed.
+ */
+add_task(async function test_error_conversion() {
+ let cw = open_compose_new_mail();
+ let bucket = cw.window.document.getElementById("attachmentBucket");
+
+ // Upload 3 files to the standard provider.
+ await test_upload(cw, null, 3, 0);
+
+ // Define another provider.
+ let providerB = new MockCloudfileAccount();
+ providerB.init("someOtherKey");
+
+ let uploadPromise = null;
+ providerB.uploadFile = function (window, aFile) {
+ return new Promise((resolve, reject) => {
+ uploadPromise = { resolve, reject };
+ });
+ };
+
+ select_attachments(cw, 0);
+ convert_selected_to_cloud_attachment(cw, providerB, false);
+
+ let uploadError = new Promise(resolve => {
+ bucket.addEventListener("attachment-move-failed", resolve, {
+ once: true,
+ });
+ });
+
+ // Reject the upload, causing the conversion to fail.
+ uploadPromise.reject(
+ new Components.Exception(
+ "Upload error.",
+ cloudFileAccounts.constants.uploadErr
+ )
+ );
+ await uploadError;
+
+ // Wait for the showLocalizedCloudFileAlert() to localize the error message.
+ await new Promise(resolve => setTimeout(resolve));
+
+ is(
+ Services.prompt.alertCount,
+ 1,
+ "Number of expected alert prompts should be correct."
+ );
+ Services.prompt.alertCount = 0;
+
+ // Check that we still have the 3 attachments we started with.
+ Assert.equal(
+ bucket.itemCount,
+ 3,
+ "Should find correct number of attachments."
+ );
+ for (let i = 0; i < bucket.itemCount; i++) {
+ let item = bucket.itemChildren[i];
+ Assert.equal(
+ item.attachment.sendViaCloud,
+ true,
+ "Attachment should be a cloud attachment."
+ );
+ Assert.equal(
+ item.attachment.cloudFileAccountKey,
+ "someKey",
+ "Attachment should be hosted by the correct provider."
+ );
+ }
+
+ close_compose_window(cw);
+});
diff --git a/comm/mail/test/browser/cloudfile/browser_attachmentUrls.js b/comm/mail/test/browser/cloudfile/browser_attachmentUrls.js
new file mode 100644
index 0000000000..38604afc6d
--- /dev/null
+++ b/comm/mail/test/browser/cloudfile/browser_attachmentUrls.js
@@ -0,0 +1,1554 @@
+/* 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/. */
+
+/**
+ * Tests Filelink URL insertion behaviours in compose windows.
+ */
+
+"use strict";
+
+var utils = ChromeUtils.import("resource://testing-common/mozmill/utils.jsm");
+var { gMockFilePicker, gMockFilePickReg, select_attachments } =
+ ChromeUtils.import("resource://testing-common/mozmill/AttachmentHelpers.jsm");
+var { gMockCloudfileManager, MockCloudfileAccount } = ChromeUtils.import(
+ "resource://testing-common/mozmill/CloudfileHelpers.jsm"
+);
+var {
+ add_cloud_attachments,
+ convert_selected_to_cloud_attachment,
+ rename_selected_cloud_attachment,
+ assert_previous_text,
+ close_compose_window,
+ get_compose_body,
+ open_compose_new_mail,
+ open_compose_with_forward,
+ open_compose_with_reply,
+ type_in_composer,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var { assert_next_nodes, assert_previous_nodes, wait_for_element } =
+ ChromeUtils.import("resource://testing-common/mozmill/DOMHelpers.jsm");
+var {
+ add_message_to_folder,
+ assert_selected_and_displayed,
+ be_in_folder,
+ create_message,
+ FAKE_SERVER_HOSTNAME,
+ get_special_folder,
+ mc,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var kHtmlPrefKey = "mail.identity.default.compose_html";
+var kReplyOnTopKey = "mail.identity.default.reply_on_top";
+var kReplyOnTop = 1;
+var kReplyOnBottom = 0;
+var kTextNodeType = 3;
+var kSigPrefKey = "mail.identity.id1.htmlSigText";
+var kSigOnReplyKey = "mail.identity.default.sig_on_reply";
+var kSigOnForwardKey = "mail.identity.default.sig_on_fwd";
+var kDefaultSigKey = "mail.identity.id1.htmlSigText";
+var kDefaultSig = "This is my signature.\n\nCheck out my website sometime!";
+var kFiles = ["./data/testFile1", "./data/testFile2"];
+var kLines = ["This is a line of text", "and here's another!"];
+
+const DATA_URLS = {
+ "chrome://messenger/content/extension.svg":
+ "data:image/svg+xml;filename=extension.svg;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBUaGlzIFNvdXJjZSBDb2RlIEZvcm0gaXMgc3ViamVjdCB0byB0aGUgdGVybXMgb2YgdGhlIE1vemlsbGEgUHVibGljCiAgIC0gTGljZW5zZSwgdi4gMi4wLiBJZiBhIGNvcHkgb2YgdGhlIE1QTCB3YXMgbm90IGRpc3RyaWJ1dGVkIHdpdGggdGhpcwogICAtIGZpbGUsIFlvdSBjYW4gb2J0YWluIG9uZSBhdCBodHRwOi8vbW96aWxsYS5vcmcvTVBMLzIuMC8uIC0tPgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiCiAgICAgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiB2aWV3Qm94PSIwIDAgNjQgNjQiPgogIDxkZWZzPgogICAgPHN0eWxlPgogICAgICAuc3R5bGUtcHV6emxlLXBpZWNlIHsKICAgICAgICBmaWxsOiB1cmwoJyNncmFkaWVudC1saW5lYXItcHV6emxlLXBpZWNlJyk7CiAgICAgIH0KICAgIDwvc3R5bGU+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImdyYWRpZW50LWxpbmVhci1wdXp6bGUtcGllY2UiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMCUiIHkyPSIxMDAlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzY2Y2M1MiIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzYwYmY0YyIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogIDwvZGVmcz4KICA8cGF0aCBjbGFzcz0ic3R5bGUtcHV6emxlLXBpZWNlIiBkPSJNNDIsNjJjMi4yLDAsNC0xLjgsNC00bDAtMTQuMmMwLDAsMC40LTMuNywyLjgtMy43YzIuNCwwLDIuMiwzLjksNi43LDMuOWMyLjMsMCw2LjItMS4yLDYuMi04LjIgYzAtNy0zLjktNy45LTYuMi03LjljLTQuNSwwLTQuMywzLjctNi43LDMuN2MtMi40LDAtMi44LTMuOC0yLjgtMy44VjIyYzAtMi4yLTEuOC00LTQtNEgzMS41YzAsMC0zLjQtMC42LTMuNC0zIGMwLTIuNCwzLjgtMi42LDMuOC03LjFjMC0yLjMtMS4zLTUuOS04LjMtNS45cy04LDMuNi04LDUuOWMwLDQuNSwzLjQsNC43LDMuNCw3LjFjMCwyLjQtMy40LDMtMy40LDNINmMtMi4yLDAtNCwxLjgtNCw0bDAsNy44IGMwLDAtMC40LDYsNC40LDZjMy4xLDAsMy4yLTQuMSw3LjMtNC4xYzIsMCw0LDEuOSw0LDZjMCw0LjItMiw2LjMtNCw2LjNjLTQsMC00LjItNC4xLTcuMy00LjFjLTQuOCwwLTQuNCw1LjgtNC40LDUuOEwyLDU4IGMwLDIuMiwxLjgsNCw0LDRIMTljMCwwLDYuMywwLjQsNi4zLTQuNGMwLTMuMS00LTMuNi00LTcuN2MwLTIsMi4yLTQuNSw2LjQtNC41YzQuMiwwLDYuNiwyLjUsNi42LDQuNWMwLDQtMy45LDQuNi0zLjksNy43IGMwLDQuOSw2LjMsNC40LDYuMyw0LjRINDJ6Ii8+Cjwvc3ZnPgo=",
+ "chrome://messenger/skin/icons/globe.svg":
+ "data:image/svg+xml;filename=globe.svg;base64,PCEtLSBUaGlzIFNvdXJjZSBDb2RlIEZvcm0gaXMgc3ViamVjdCB0byB0aGUgdGVybXMgb2YgdGhlIE1vemlsbGEgUHVibGljCiAgIC0gTGljZW5zZSwgdi4gMi4wLiBJZiBhIGNvcHkgb2YgdGhlIE1QTCB3YXMgbm90IGRpc3RyaWJ1dGVkIHdpdGggdGhpcwogICAtIGZpbGUsIFlvdSBjYW4gb2J0YWluIG9uZSBhdCBodHRwOi8vbW96aWxsYS5vcmcvTVBMLzIuMC8uIC0tPgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiPgogIDxwYXRoIGZpbGw9ImNvbnRleHQtZmlsbCIgZD0iTTggMGE4IDggMCAxIDAgOCA4IDguMDA5IDguMDA5IDAgMCAwLTgtOHptNS4xNjMgNC45NThoLTEuNTUyYTcuNyA3LjcgMCAwIDAtMS4wNTEtMi4zNzYgNi4wMyA2LjAzIDAgMCAxIDIuNjAzIDIuMzc2ek0xNCA4YTUuOTYzIDUuOTYzIDAgMCAxLS4zMzUgMS45NThoLTEuODIxQTEyLjMyNyAxMi4zMjcgMCAwIDAgMTIgOGExMi4zMjcgMTIuMzI3IDAgMCAwLS4xNTYtMS45NThoMS44MjFBNS45NjMgNS45NjMgMCAwIDEgMTQgOHptLTYgNmMtMS4wNzUgMC0yLjAzNy0xLjItMi41NjctMi45NThoNS4xMzVDMTAuMDM3IDEyLjggOS4wNzUgMTQgOCAxNHpNNS4xNzQgOS45NThhMTEuMDg0IDExLjA4NCAwIDAgMSAwLTMuOTE2aDUuNjUxQTExLjExNCAxMS4xMTQgMCAwIDEgMTEgOGExMS4xMTQgMTEuMTE0IDAgMCAxLS4xNzQgMS45NTh6TTIgOGE1Ljk2MyA1Ljk2MyAwIDAgMSAuMzM1LTEuOTU4aDEuODIxYTEyLjM2MSAxMi4zNjEgMCAwIDAgMCAzLjkxNkgyLjMzNUE1Ljk2MyA1Ljk2MyAwIDAgMSAyIDh6bTYtNmMxLjA3NSAwIDIuMDM3IDEuMiAyLjU2NyAyLjk1OEg1LjQzM0M1Ljk2MyAzLjIgNi45MjUgMiA4IDJ6bS0yLjU2LjU4MmE3LjcgNy43IDAgMCAwLTEuMDUxIDIuMzc2SDIuODM3QTYuMDMgNi4wMyAwIDAgMSA1LjQ0IDIuNTgyem0tMi42IDguNDZoMS41NDlhNy43IDcuNyAwIDAgMCAxLjA1MSAyLjM3NiA2LjAzIDYuMDMgMCAwIDEtMi42MDMtMi4zNzZ6bTcuNzIzIDIuMzc2YTcuNyA3LjcgMCAwIDAgMS4wNTEtMi4zNzZoMS41NTJhNi4wMyA2LjAzIDAgMCAxLTIuNjA2IDIuMzc2eiI+PC9wYXRoPgo8L3N2Zz4K",
+};
+
+var gInbox;
+
+function test_expected_included(actual, expected, description) {
+ Assert.equal(
+ actual.length,
+ expected.length,
+ `${description}: correct length`
+ );
+
+ for (let i = 0; i < expected.length; i++) {
+ for (let item of Object.keys(expected[i])) {
+ Assert.deepEqual(
+ actual[i][item],
+ expected[i][item],
+ `${description}: ${item} should exist and be correct`
+ );
+ }
+ }
+}
+
+add_setup(async function () {
+ requestLongerTimeout(4);
+
+ // These prefs can't be set in the manifest as they contain white-space.
+ Services.prefs.setStringPref(
+ "mail.identity.id1.htmlSigText",
+ "Tinderbox is soo 90ies"
+ );
+ Services.prefs.setStringPref(
+ "mail.identity.id2.htmlSigText",
+ "Tinderboxpushlog is the new <b>hotness!</b>"
+ );
+
+ // For replies and forwards, we'll work off a message in the Inbox folder
+ // of the fake "tinderbox" account.
+ let server = MailServices.accounts.findServer(
+ "tinderbox",
+ FAKE_SERVER_HOSTNAME,
+ "pop3"
+ );
+ gInbox = await get_special_folder(Ci.nsMsgFolderFlags.Inbox, false, server);
+ await add_message_to_folder([gInbox], create_message());
+
+ gMockFilePickReg.register();
+ gMockCloudfileManager.register();
+
+ Services.prefs.setBoolPref(kHtmlPrefKey, true);
+
+ // Don't create paragraphs in the test.
+ // The test fails if it encounters paragraphs <p> instead of breaks <br>.
+ Services.prefs.setBoolPref("mail.compose.default_to_paragraph", false);
+});
+
+registerCleanupFunction(function () {
+ gMockCloudfileManager.unregister();
+ gMockFilePickReg.unregister();
+ Services.prefs.clearUserPref(kDefaultSigKey);
+ Services.prefs.clearUserPref(kHtmlPrefKey);
+ Services.prefs.clearUserPref("mail.compose.default_to_paragraph");
+});
+
+function setupTest() {
+ // If our signature got accidentally wiped out, let's just put it back.
+ Services.prefs.setCharPref(kDefaultSigKey, kDefaultSig);
+}
+
+/**
+ * Given some compose window controller, wait for some Filelink URLs to be
+ * inserted.
+ *
+ * Note: This function also validates, if the correct items have been added to
+ * the template (serviceUrl, downloadLimit, downloadExpiryDate,
+ * downloadPasswordProtected). There is no dedicated test for the different
+ * conditions, but the tests in this file are using different setups.
+ * See the values in the used provider.init() calls.
+ *
+ * @param aController the controller for a compose window.
+ * @param aNumUrls the number of Filelink URLs that are expected.
+ * @param aUploads an array containing the objects returned by
+ * cloudFileAccounts.uploadFile() for all uploads
+ * @returns an array containing the root containment node, the list node, and
+ * an array of the link URL nodes.
+ */
+function wait_for_attachment_urls(aController, aNumUrls, aUploads = []) {
+ let mailBody = get_compose_body(aController);
+
+ // Wait until we can find the root attachment URL node...
+ let root = wait_for_element(
+ mailBody.parentNode,
+ "body > #cloudAttachmentListRoot"
+ );
+
+ let list = wait_for_element(
+ mailBody,
+ "#cloudAttachmentListRoot > #cloudAttachmentList"
+ );
+
+ let header = wait_for_element(
+ mailBody,
+ "#cloudAttachmentListRoot > #cloudAttachmentListHeader"
+ );
+
+ let footer = wait_for_element(
+ mailBody,
+ "#cloudAttachmentListRoot > #cloudAttachmentListFooter"
+ );
+
+ let urls = null;
+ utils.waitFor(function () {
+ urls = mailBody.querySelectorAll(
+ "#cloudAttachmentList > .cloudAttachmentItem"
+ );
+ return urls != null && urls.length == aNumUrls;
+ });
+
+ Assert.equal(
+ aUploads.length,
+ aNumUrls,
+ "Number of links should match number of linked files."
+ );
+
+ Assert.equal(
+ header.textContent,
+ aNumUrls == 1
+ ? `I’ve linked 1 file to this email:`
+ : `I’ve linked ${aNumUrls} files to this email:`,
+ "Number of links mentioned in header should matches number of linked files."
+ );
+
+ let footerExpected = false;
+ for (let entry of aUploads) {
+ if (!entry.serviceUrl) {
+ continue;
+ }
+
+ footerExpected = true;
+ Assert.ok(
+ footer.innerHTML.includes(entry.serviceUrl),
+ `Footer "${footer.innerHTML}" should include serviceUrl "${entry.serviceUrl}".`
+ );
+ Assert.ok(
+ footer.innerHTML.includes(entry.serviceName),
+ `Footer "${footer.innerHTML}" should include serviceName "${entry.serviceName}".`
+ );
+ }
+ if (footerExpected) {
+ Assert.ok(
+ footer.innerHTML.startsWith("Learn more about"),
+ `Footer "${footer.innerHTML}" should start with "Learn more about "`
+ );
+ } else {
+ Assert.ok(
+ footer.innerHTML == "",
+ `Footer should be empty if no serviceUrl is specified.`
+ );
+ }
+
+ let bucket = aController.window.document.getElementById("attachmentBucket");
+
+ // Check the actual content of the generated cloudAttachmentItems.
+ for (let i = 0; i < urls.length; i++) {
+ if (aController.window.gMsgCompose.composeHTML) {
+ // Test HTML message.
+
+ let paperClipIcon = urls[i].querySelector(".paperClipIcon");
+ Assert.equal(
+ aUploads[i].downloadPasswordProtected
+ ? "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIfSURBVFhH7ZfLK0RRHMfvNd6PMV4Lj5UkO5bslJIdf4ClRw2TlY2yt2EhsZO9DYoFoiSvJBZkI6SsNMyIiLnH93vmXDF5HNe9pHzqM797fufMPb+Zc4Z7jC+QBnvgJryD93AddkH2eUop3IPiHXdgCfSEdLgLOdE+bIFFSl4zZxeRAl2HXzsn2IIZTCTAHPs4hsvhOlxz3rxRtt6GfRyzJlsucw1582zZehv2cUxEtlyGN6afkThuFa7EL7+H0wK03pek4q/xJwtYVv4YumurO+4V/3vgvwAvC5iHTfHL9zFV/Ah7J9tjE9s2r/K3YwWlD8IaREP+ExPCWBDJVl+gM3LEto0nBURHCiuNpBiflvLjqWcufDFfdVbo4ly1PVoC0xrAaz4qnLdiVjk1hVhArvDRFxuSYxQeFSAaGHzCbAuEIsf0URjtsithX3i1Cf18yewKn8kWyOu+OlWXuSpKnBRwpWKxioTXi7BCtr6Ak004BZvhJAwyAUZhb3Q0bwKxXmY+xVzyB8MNOgXwE/NrC0A+clXBDZV7iYkC7GK18AcvTZ0lOFGRE5NDWAtn4A28hdPQEToFcG1Jq4qERXAZ+DCaBXk+cIROAePQgh2whgk30SngAA7CVDgLq6Fr6P4M++Ec5PmPp6BhWAdzIA+m3BOO0C2AJ2GuMyfme0KQp6Ao5EmZf/fLDGFuI2oi+EEcUQm5JDywhpWc2MFGNIwn/WmcKhqF50UAAAAASUVORK5CYII="
+ : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAVFJREFUWIXtl8FKw0AQhj8EbQ/p0Ut8AVEPgYLUB+i5L6J9E0Wtr1HPgl48WU8K1Tfw4LktxUAhHvZfiMXUbdhVhB0Yms78M/NldwkJuFsD6AMjYCYfASfKBbUd4BkoKvxJmiDWKA1/AXrAtrynmIUIshJ9DXgEmt/km8oVwHEIANu8u0LTleYhBMBUzZMVmkSaSQgAe9DW1d3L/wzAqW6jJpQ3+5cA3vbW1Vz3Np6BCBABIkAE+DWAmX7TUixdynm15Wf6jf5fa3Cq60K5qrraNuHrK1kbmJcGWJ8rB9DC4yvaq5odlmK7wBB4lw8Vs9ZRzdgHwLmaXa5RM1DNmQ+AA2ABfACZgz4DctXs+QAAuMLc0dsPEJk0BXDhazjAFnCnxjlmiTuYg5kAR4rl0twCmz4BLMQAs7RVH6kLzJ17H162fczhGmO+mqa6PqXGnn8CxMN0PcC9DrQAAAAASUVORK5CYII=",
+ paperClipIcon.src,
+ "The paperClipIcon should be correct."
+ );
+
+ Assert.equal(
+ urls[i].querySelector(".cloudfile-name").href,
+ aUploads[i].url,
+ "The link attached to the cloudfile name should be correct."
+ );
+
+ let providerIcon = urls[i].querySelector(".cloudfile-service-icon");
+ if (providerIcon) {
+ Assert.equal(
+ DATA_URLS[aUploads[i].serviceIcon] || aUploads[i].serviceIcon,
+ providerIcon.src,
+ "The cloufile service icon should be correct."
+ );
+ }
+
+ let expected = {
+ url: aUploads[i].downloadPasswordProtected
+ ? ".cloudfile-password-protected-link"
+ : ".cloudfile-link",
+ name: ".cloudfile-name",
+ serviceName: ".cloudfile-service-name",
+ downloadLimit: ".cloudfile-download-limit",
+ downloadExpiryDateString: ".cloudfile-expiry-date",
+ };
+
+ for (let [fieldName, id] of Object.entries(expected)) {
+ let element = urls[i].querySelector(id);
+ Assert.ok(
+ !!element == !!aUploads[i][fieldName],
+ `The ${fieldName} should have been correctly added.`
+ );
+ if (aUploads[i][fieldName]) {
+ Assert.equal(
+ element.textContent,
+ `${aUploads[i][fieldName]}`,
+ `The cloudfile ${fieldName} should be correct.`
+ );
+ } else {
+ Assert.equal(
+ element,
+ null,
+ `The cloudfile ${fieldName} should not be present.`
+ );
+ }
+ }
+ } else {
+ // Test plain text message.
+
+ let lines = urls[i].textContent.split("\n");
+ let expected = {
+ url: aUploads[i].downloadPasswordProtected
+ ? ` Password Protected Link: `
+ : ` Link: `,
+ name: ` * `,
+ downloadLimit: ` Download Limit: `,
+ downloadExpiryDateString: ` Expiry Date: `,
+ };
+
+ if (urls[i].serviceUrl) {
+ expected.serviceName = ` CloudFile Service: `;
+ }
+
+ for (let [fieldName, prefix] of Object.entries(expected)) {
+ if (aUploads[i][fieldName]) {
+ let line = `${prefix}${aUploads[i][fieldName]}`;
+ Assert.ok(
+ lines.includes(line),
+ `Line "${line}" should be part of "${lines}".`
+ );
+ } else {
+ !lines.find(
+ line => line.startsWith(prefix),
+ `There should be no line starting with "${prefix}" part of "${lines}".`
+ );
+ }
+ }
+ }
+
+ // Find the bucket entry for this upload.
+ let items = Array.from(
+ bucket.querySelectorAll(".attachmentItem"),
+ item => item
+ ).filter(item => item.attachment.name == aUploads[i].name);
+ Assert.equal(
+ items.length,
+ 1,
+ `Should find one matching bucket entry for ${aUploads[i].serviceName} / ${aUploads[i].name}.`
+ );
+ Assert.equal(
+ items[0].querySelector("img.attachmentcell-icon").src,
+ aUploads[i].serviceIcon,
+ `CloudFile icon should be correct for ${aUploads[i].serviceName} / ${aUploads[i].name}`
+ );
+ }
+
+ return [root, list, urls];
+}
+
+/**
+ * Helper function that sets up the mock file picker for a series of files,
+ * spawns a reply window for the first message in the gInbox, optionally
+ * types some strings into the compose window, and then attaches some
+ * Filelinks.
+ *
+ * @param aText an array of strings to type into the compose window. Each
+ * string is followed by pressing the RETURN key, except for
+ * the final string. Pass an empty array if you don't want
+ * anything typed.
+ * @param aFiles an array of filename strings for files located beneath
+ * the test directory.
+ */
+async function prepare_some_attachments_and_reply(aText, aFiles) {
+ gMockFilePicker.returnFiles = collectFiles(aFiles);
+
+ let provider = new MockCloudfileAccount();
+ provider.init("providerF", {
+ serviceName: "MochiTest F",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceUrl: "https://www.provider-F.org",
+ downloadLimit: 2,
+ });
+
+ await be_in_folder(gInbox);
+ let msg = select_click_row(0);
+ assert_selected_and_displayed(mc, msg);
+
+ let cw = open_compose_with_reply();
+
+ // If we have any typing to do, let's do it.
+ type_in_composer(cw, aText);
+ let uploads = add_cloud_attachments(cw, provider);
+
+ test_expected_included(
+ uploads,
+ [
+ {
+ url: "https://www.example.com/providerF/testFile1",
+ name: "testFile1",
+ serviceName: "MochiTest F",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceUrl: "https://www.provider-F.org",
+ downloadLimit: 2,
+ },
+ {
+ url: "https://www.example.com/providerF/testFile2",
+ name: "testFile2",
+ serviceName: "MochiTest F",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceUrl: "https://www.provider-F.org",
+ downloadLimit: 2,
+ },
+ ],
+ `Expected values in uploads array #11`
+ );
+ let [root] = wait_for_attachment_urls(cw, aFiles.length, uploads);
+
+ return [cw, root];
+}
+
+/**
+ * Helper function that sets up the mock file picker for a series of files,
+ * spawns an inline forward compose window for the first message in the gInbox,
+ * optionally types some strings into the compose window, and then attaches
+ * some Filelinks.
+ *
+ * @param aText an array of strings to type into the compose window. Each
+ * string is followed by pressing the RETURN key, except for
+ * the final string. Pass an empty array if you don't want
+ * anything typed.
+ * @param aFiles an array of filename strings for files located beneath
+ * the test directory.
+ */
+async function prepare_some_attachments_and_forward(aText, aFiles) {
+ gMockFilePicker.returnFiles = collectFiles(aFiles);
+
+ let provider = new MockCloudfileAccount();
+ provider.init("providerG", {
+ serviceName: "MochiTest G",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceUrl: "https://www.provider-G.org",
+ downloadExpiryDate: { timestamp: 1639827408073 },
+ });
+
+ await be_in_folder(gInbox);
+ let msg = select_click_row(0);
+ assert_selected_and_displayed(mc, msg);
+
+ let cw = open_compose_with_forward();
+
+ // Put the selection at the beginning of the document...
+ let editor = cw.window.GetCurrentEditor();
+ editor.beginningOfDocument();
+
+ // Do any necessary typing...
+ type_in_composer(cw, aText);
+ let uploads = add_cloud_attachments(cw, provider);
+ test_expected_included(
+ uploads,
+ [
+ {
+ url: "https://www.example.com/providerG/testFile1",
+ name: "testFile1",
+ serviceName: "MochiTest G",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceUrl: "https://www.provider-G.org",
+ downloadExpiryDate: { timestamp: 1639827408073 },
+ },
+ {
+ url: "https://www.example.com/providerG/testFile2",
+ name: "testFile2",
+ serviceName: "MochiTest G",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceUrl: "https://www.provider-G.org",
+ downloadExpiryDate: { timestamp: 1639827408073 },
+ },
+ ],
+ `Expected values in uploads array #12`
+ );
+
+ // Add the expected time string.
+ let timeString = new Date(1639827408073).toLocaleString(undefined, {
+ day: "2-digit",
+ month: "2-digit",
+ year: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ timeZoneName: "short",
+ });
+ uploads[0].downloadExpiryDateString = timeString;
+ uploads[1].downloadExpiryDateString = timeString;
+ let [root] = wait_for_attachment_urls(cw, aFiles.length, uploads);
+
+ return [cw, root];
+}
+
+/**
+ * Helper function that runs a test function with signature-in-reply and
+ * signature-in-forward enabled, and then runs the test again with those
+ * prefs disabled.
+ *
+ * @param aSpecialTest a test that takes two arguments - the first argument
+ * is the aText array of any text that should be typed,
+ * and the second is a boolean for whether or not the
+ * special test should expect a signature or not.
+ * @param aText any text to be typed into the compose window, passed to
+ * aSpecialTest.
+ */
+async function try_with_and_without_signature_in_reply_or_fwd(
+ aSpecialTest,
+ aText
+) {
+ // By default, we have a signature included in replies, so we'll start
+ // with that.
+ Services.prefs.setBoolPref(kSigOnReplyKey, true);
+ Services.prefs.setBoolPref(kSigOnForwardKey, true);
+ await aSpecialTest(aText, true);
+
+ Services.prefs.setBoolPref(kSigOnReplyKey, false);
+ Services.prefs.setBoolPref(kSigOnForwardKey, false);
+ await aSpecialTest(aText, false);
+}
+
+/**
+ * Helper function that runs a test function without a signature, once
+ * in HTML mode, and again in plaintext mode.
+ *
+ * @param aTest a test that takes no arguments.
+ */
+async function try_without_signature(aTest) {
+ let oldSig = Services.prefs.getCharPref(kSigPrefKey);
+ Services.prefs.setCharPref(kSigPrefKey, "");
+
+ await try_with_plaintext_and_html_mail(aTest);
+ Services.prefs.setCharPref(kSigPrefKey, oldSig);
+}
+
+/**
+ * Helper function that runs a test function for HTML mail composition, and
+ * then again in plaintext mail composition.
+ *
+ * @param aTest a test that takes no arguments.
+ */
+async function try_with_plaintext_and_html_mail(aTest) {
+ await aTest();
+ Services.prefs.setBoolPref(kHtmlPrefKey, false);
+ await aTest();
+ Services.prefs.setBoolPref(kHtmlPrefKey, true);
+}
+
+/**
+ * Test that if we open up a composer and immediately attach a Filelink,
+ * a linebreak is inserted before the containment node in order to allow
+ * the user to write before the attachment URLs. This assumes the user
+ * does not have a signature already inserted into the message body.
+ */
+add_task(async function test_inserts_linebreak_on_empty_compose() {
+ await try_without_signature(subtest_inserts_linebreak_on_empty_compose);
+});
+
+/**
+ * Subtest for test_inserts_linebreak_on_empty_compose - can be executed
+ * on both plaintext and HTML compose windows.
+ */
+function subtest_inserts_linebreak_on_empty_compose() {
+ gMockFilePicker.returnFiles = collectFiles(kFiles);
+ let provider = new MockCloudfileAccount();
+ provider.init("someKey", {
+ downloadPasswordProtected: false,
+ });
+ let cw = open_compose_new_mail();
+ let uploads = add_cloud_attachments(cw, provider);
+ test_expected_included(
+ uploads,
+ [
+ {
+ url: "https://www.example.com/someKey/testFile1",
+ name: "testFile1",
+ serviceName: "default",
+ serviceIcon: "chrome://messenger/content/extension.svg",
+ serviceUrl: "",
+ downloadPasswordProtected: false,
+ },
+ {
+ url: "https://www.example.com/someKey/testFile2",
+ name: "testFile2",
+ serviceName: "default",
+ serviceIcon: "chrome://messenger/content/extension.svg",
+ serviceUrl: "",
+ downloadPasswordProtected: false,
+ },
+ ],
+ `Expected values in uploads array #1`
+ );
+ let [root] = wait_for_attachment_urls(cw, kFiles.length, uploads);
+
+ let br = root.previousSibling;
+ Assert.equal(
+ br.localName,
+ "br",
+ "The attachment URL containment node should be preceded by a linebreak"
+ );
+
+ let mailBody = get_compose_body(cw);
+
+ Assert.equal(
+ mailBody.firstChild,
+ br,
+ "The linebreak should be the first child of the compose body"
+ );
+
+ close_compose_window(cw);
+}
+
+/**
+ * Test that if we open up a composer and immediately attach a Filelink,
+ * a linebreak is inserted before the containment node. This test also
+ * ensures that, with a signature already in the compose window, we don't
+ * accidentally insert the attachment URL containment within the signature
+ * node.
+ */
+add_task(function test_inserts_linebreak_on_empty_compose_with_signature() {
+ gMockFilePicker.returnFiles = collectFiles(kFiles);
+ let provider = new MockCloudfileAccount();
+ provider.init("someKey", {
+ downloadPasswordProtected: true,
+ });
+
+ let cw = open_compose_new_mail();
+ let uploads = add_cloud_attachments(cw, provider);
+ test_expected_included(
+ uploads,
+ [
+ {
+ url: "https://www.example.com/someKey/testFile1",
+ name: "testFile1",
+ serviceName: "default",
+ serviceIcon: "chrome://messenger/content/extension.svg",
+ serviceUrl: "",
+ downloadPasswordProtected: true,
+ },
+ {
+ url: "https://www.example.com/someKey/testFile2",
+ name: "testFile2",
+ serviceName: "default",
+ serviceIcon: "chrome://messenger/content/extension.svg",
+ serviceUrl: "",
+ downloadPasswordProtected: true,
+ },
+ ],
+ `Expected values in uploads array #2`
+ );
+ // wait_for_attachment_urls ensures that the attachment URL containment
+ // node is an immediate child of the body of the message, so if this
+ // succeeds, then we were not in the signature node.
+ let [root] = wait_for_attachment_urls(cw, kFiles.length, uploads);
+
+ let br = assert_previous_nodes("br", root, 1);
+
+ let mailBody = get_compose_body(cw);
+ Assert.equal(
+ mailBody.firstChild,
+ br,
+ "The linebreak should be the first child of the compose body"
+ );
+
+ // Now ensure that the node after the attachments is a br, and following
+ // that is the signature.
+ br = assert_next_nodes("br", root, 1);
+
+ let pre = br.nextSibling;
+ Assert.equal(
+ pre.localName,
+ "pre",
+ "The linebreak should be followed by the signature pre"
+ );
+ Assert.ok(
+ pre.classList.contains("moz-signature"),
+ "The pre should have the moz-signature class"
+ );
+
+ close_compose_window(cw);
+
+ Services.prefs.setBoolPref(kHtmlPrefKey, false);
+
+ // Now let's try with plaintext mail.
+ cw = open_compose_new_mail();
+ uploads = add_cloud_attachments(cw, provider);
+ test_expected_included(
+ uploads,
+ [
+ {
+ url: "https://www.example.com/someKey/testFile1",
+ name: "testFile1",
+ serviceIcon: "chrome://messenger/content/extension.svg",
+ serviceName: "default",
+ serviceUrl: "",
+ downloadPasswordProtected: true,
+ },
+ {
+ url: "https://www.example.com/someKey/testFile2",
+ name: "testFile2",
+ serviceIcon: "chrome://messenger/content/extension.svg",
+ serviceName: "default",
+ serviceUrl: "",
+ downloadPasswordProtected: true,
+ },
+ ],
+ `Expected values in uploads array #3`
+ );
+ [root] = wait_for_attachment_urls(cw, kFiles.length, uploads);
+
+ br = assert_previous_nodes("br", root, 1);
+
+ mailBody = get_compose_body(cw);
+ Assert.equal(
+ mailBody.firstChild,
+ br,
+ "The linebreak should be the first child of the compose body"
+ );
+
+ // Now ensure that the node after the attachments is a br, and following
+ // that is the signature.
+ br = assert_next_nodes("br", root, 1);
+
+ let div = br.nextSibling;
+ Assert.equal(
+ div.localName,
+ "div",
+ "The linebreak should be followed by the signature div"
+ );
+ Assert.ok(
+ div.classList.contains("moz-signature"),
+ "The div should have the moz-signature class"
+ );
+
+ close_compose_window(cw);
+
+ Services.prefs.setBoolPref(kHtmlPrefKey, true);
+});
+
+/**
+ * Tests that removing all Filelinks causes the root node to be removed.
+ */
+add_task(async function test_removing_filelinks_removes_root_node() {
+ await try_with_plaintext_and_html_mail(
+ subtest_removing_filelinks_removes_root_node
+ );
+});
+
+/**
+ * Test for test_removing_filelinks_removes_root_node - can be executed
+ * on both plaintext and HTML compose windows.
+ */
+async function subtest_removing_filelinks_removes_root_node() {
+ let [cw, root] = await prepare_some_attachments_and_reply([], kFiles);
+
+ // Now select the attachments in the attachment bucket, and remove them.
+ select_attachments(cw, 0, 1);
+ cw.window.goDoCommand("cmd_delete");
+
+ // Wait for the root to be removed.
+ let mailBody = get_compose_body(cw);
+ utils.waitFor(function () {
+ let result = mailBody.querySelector(root.id);
+ return result == null;
+ }, "Timed out waiting for attachment container to be removed");
+
+ close_compose_window(cw);
+}
+
+/**
+ * Test that if we write some text in an empty message (no signature),
+ * and the selection is at the end of a line of text, attaching some Filelinks
+ * causes the attachment URL container to be separated from the text by
+ * two br tags.
+ */
+add_task(async function test_adding_filelinks_to_written_message() {
+ await try_without_signature(subtest_adding_filelinks_to_written_message);
+});
+
+/**
+ * Subtest for test_adding_filelinks_to_written_message - generalized for both
+ * HTML and plaintext mail.
+ */
+function subtest_adding_filelinks_to_written_message() {
+ gMockFilePicker.returnFiles = collectFiles(kFiles);
+ let provider = new MockCloudfileAccount();
+ provider.init("someKey");
+ let cw = open_compose_new_mail();
+
+ type_in_composer(cw, kLines);
+ let uploads = add_cloud_attachments(cw, provider);
+ test_expected_included(
+ uploads,
+ [
+ {
+ url: "https://www.example.com/someKey/testFile1",
+ name: "testFile1",
+ serviceName: "default",
+ serviceIcon: "chrome://messenger/content/extension.svg",
+ serviceUrl: "",
+ },
+ {
+ url: "https://www.example.com/someKey/testFile2",
+ name: "testFile2",
+ serviceName: "default",
+ serviceIcon: "chrome://messenger/content/extension.svg",
+ serviceUrl: "",
+ },
+ ],
+ `Expected values in uploads array #4`
+ );
+ let [root] = wait_for_attachment_urls(cw, kFiles.length, uploads);
+
+ let br = root.previousSibling;
+ Assert.equal(
+ br.localName,
+ "br",
+ "The attachment URL containment node should be preceded by a linebreak"
+ );
+ br = br.previousSibling;
+ Assert.equal(
+ br.localName,
+ "br",
+ "The attachment URL containment node should be preceded by " +
+ "two linebreaks"
+ );
+ close_compose_window(cw);
+}
+
+/**
+ * Tests for inserting Filelinks into a reply, when we're configured to
+ * reply above the quote.
+ */
+add_task(async function test_adding_filelinks_to_empty_reply_above() {
+ let oldReplyOnTop = Services.prefs.getIntPref(kReplyOnTopKey);
+ Services.prefs.setIntPref(kReplyOnTopKey, kReplyOnTop);
+
+ await try_with_and_without_signature_in_reply_or_fwd(
+ subtest_adding_filelinks_to_reply_above,
+ []
+ );
+ // Now with HTML mail...
+ Services.prefs.setBoolPref(kHtmlPrefKey, false);
+ await try_with_and_without_signature_in_reply_or_fwd(
+ subtest_adding_filelinks_to_reply_above_plaintext,
+ []
+ );
+
+ Services.prefs.setBoolPref(kHtmlPrefKey, true);
+ Services.prefs.setIntPref(kReplyOnTopKey, oldReplyOnTop);
+});
+
+/**
+ * Tests for inserting Filelinks into a reply, when we're configured to
+ * reply above the quote, after entering some text.
+ */
+add_task(async function test_adding_filelinks_to_nonempty_reply_above() {
+ let oldReplyOnTop = Services.prefs.getIntPref(kReplyOnTopKey);
+ Services.prefs.setIntPref(kReplyOnTopKey, kReplyOnTop);
+
+ await subtest_adding_filelinks_to_reply_above(kLines);
+
+ Services.prefs.setBoolPref(kHtmlPrefKey, false);
+ await subtest_adding_filelinks_to_reply_above_plaintext(kLines);
+ Services.prefs.setBoolPref(kHtmlPrefKey, true);
+
+ Services.prefs.setIntPref(kReplyOnTopKey, oldReplyOnTop);
+});
+
+/**
+ * Subtest for test_adding_filelinks_to_reply_above for the plaintext composer.
+ * Does some special casing for the weird br insertions that happens in
+ * various cases.
+ */
+async function subtest_adding_filelinks_to_reply_above_plaintext(
+ aText,
+ aWithSig
+) {
+ let [cw, root] = await prepare_some_attachments_and_reply(aText, kFiles);
+
+ let br;
+ if (aText.length) {
+ br = assert_next_nodes("br", root, 2);
+ } else {
+ br = assert_next_nodes("br", root, 1);
+ }
+
+ let div = br.nextSibling;
+ Assert.equal(
+ div.localName,
+ "div",
+ "The linebreak should be followed by a div"
+ );
+
+ Assert.ok(div.classList.contains("moz-cite-prefix"));
+
+ if (aText.length) {
+ br = assert_previous_nodes("br", root, 2);
+ } else {
+ br = assert_previous_nodes("br", root, 1);
+ }
+
+ if (aText.length == 0) {
+ // If we didn't type anything, that br should be the first element of the
+ // message body.
+ let msgBody = get_compose_body(cw);
+ Assert.equal(
+ msgBody.firstChild,
+ br,
+ "The linebreak should have been the first element in the " +
+ "message body"
+ );
+ } else {
+ let targetText = aText[aText.length - 1];
+ let textNode = br.previousSibling;
+ Assert.equal(textNode.nodeType, kTextNodeType);
+ Assert.equal(textNode.nodeValue, targetText);
+ }
+
+ close_compose_window(cw);
+}
+
+/**
+ * Subtest for test_adding_filelinks_to_reply_above for the HTML composer.
+ */
+async function subtest_adding_filelinks_to_reply_above(aText) {
+ let [cw, root] = await prepare_some_attachments_and_reply(aText, kFiles);
+
+ // If there's any text written, then there's only a single break between the
+ // end of the text and the reply. Otherwise, there are two breaks.
+ let br =
+ aText.length > 1
+ ? assert_next_nodes("br", root, 2)
+ : assert_next_nodes("br", root, 1);
+
+ // ... which is followed by a div with a class of "moz-cite-prefix".
+ let div = br.nextSibling;
+ Assert.equal(
+ div.localName,
+ "div",
+ "The linebreak should be followed by a div"
+ );
+
+ Assert.ok(div.classList.contains("moz-cite-prefix"));
+
+ close_compose_window(cw);
+}
+
+/**
+ * Tests for inserting Filelinks into a reply, when we're configured to
+ * reply below the quote.
+ */
+add_task(async function test_adding_filelinks_to_empty_reply_below() {
+ let oldReplyOnTop = Services.prefs.getIntPref(kReplyOnTopKey);
+ Services.prefs.setIntPref(kReplyOnTopKey, kReplyOnBottom);
+
+ await try_with_and_without_signature_in_reply_or_fwd(
+ subtest_adding_filelinks_to_reply_below,
+ []
+ );
+ Services.prefs.setBoolPref(kHtmlPrefKey, false);
+ await try_with_and_without_signature_in_reply_or_fwd(
+ subtest_adding_filelinks_to_plaintext_reply_below,
+ []
+ );
+ Services.prefs.setBoolPref(kHtmlPrefKey, true);
+
+ Services.prefs.setIntPref(kReplyOnTopKey, oldReplyOnTop);
+});
+
+/**
+ * Tests for inserting Filelinks into a reply, when we're configured to
+ * reply below the quote, after entering some text.
+ */
+add_task(async function test_adding_filelinks_to_nonempty_reply_below() {
+ let oldReplyOnTop = Services.prefs.getIntPref(kReplyOnTopKey);
+ Services.prefs.setIntPref(kReplyOnTopKey, kReplyOnBottom);
+
+ await try_with_and_without_signature_in_reply_or_fwd(
+ subtest_adding_filelinks_to_reply_below,
+ kLines
+ );
+
+ Services.prefs.setBoolPref(kHtmlPrefKey, false);
+ await try_with_and_without_signature_in_reply_or_fwd(
+ subtest_adding_filelinks_to_plaintext_reply_below,
+ kLines
+ );
+ Services.prefs.setBoolPref(kHtmlPrefKey, true);
+
+ Services.prefs.setIntPref(kReplyOnTopKey, oldReplyOnTop);
+});
+
+/**
+ * Subtest for test_adding_filelinks_to_reply_below for the HTML composer.
+ */
+async function subtest_adding_filelinks_to_reply_below(aText, aWithSig) {
+ let [cw, root] = await prepare_some_attachments_and_reply(aText, kFiles);
+
+ // So, we should have the root, followed by a br
+ let br = root.nextSibling;
+ Assert.equal(
+ br.localName,
+ "br",
+ "The attachment URL containment node should be followed by a br"
+ );
+
+ let blockquote;
+ if (aText.length) {
+ // If there was any text inserted, check for 2 previous br nodes, and then
+ // the inserted text, and then the blockquote.
+ br = assert_previous_nodes("br", root, 2);
+ let textNode = assert_previous_text(br.previousSibling, aText);
+ blockquote = textNode.previousSibling;
+ } else {
+ // If no text was inserted, check for 1 previous br node, and then the
+ // blockquote.
+ br = assert_previous_nodes("br", root, 1);
+ blockquote = br.previousSibling;
+ }
+
+ Assert.equal(
+ blockquote.localName,
+ "blockquote",
+ "The linebreak should be preceded by a blockquote."
+ );
+
+ let prefix = blockquote.previousSibling;
+ Assert.equal(
+ prefix.localName,
+ "div",
+ "The blockquote should be preceded by the prefix div"
+ );
+ Assert.ok(
+ prefix.classList.contains("moz-cite-prefix"),
+ "The prefix should have the moz-cite-prefix class"
+ );
+
+ close_compose_window(cw);
+}
+
+/**
+ * Subtest for test_adding_filelinks_to_reply_below for the plaintext composer.
+ */
+async function subtest_adding_filelinks_to_plaintext_reply_below(
+ aText,
+ aWithSig
+) {
+ let [cw, root] = await prepare_some_attachments_and_reply(aText, kFiles);
+ let br, span;
+
+ assert_next_nodes("br", root, 1);
+
+ if (aText.length) {
+ br = assert_previous_nodes("br", root, 2);
+ // If text was entered, make sure it matches what we expect...
+ let textNode = assert_previous_text(br.previousSibling, aText);
+ // And then grab the span, which should be before the final text node.
+ span = textNode.previousSibling;
+ } else {
+ br = assert_previous_nodes("br", root, 1);
+ // If no text was entered, just grab the last br's previous sibling - that
+ // will be the span.
+ span = br.previousSibling;
+ // Sometimes we need to skip one more linebreak.
+ if (span.localName != "span") {
+ span = span.previousSibling;
+ }
+ }
+
+ Assert.equal(
+ span.localName,
+ "span",
+ "The linebreak should be preceded by a span."
+ );
+
+ let prefix = span.previousSibling;
+ Assert.equal(
+ prefix.localName,
+ "div",
+ "The blockquote should be preceded by the prefix div"
+ );
+ Assert.ok(
+ prefix.classList.contains("moz-cite-prefix"),
+ "The prefix should have the moz-cite-prefix class"
+ );
+
+ close_compose_window(cw);
+}
+
+/**
+ * Tests Filelink insertion on an inline-forward compose window with nothing
+ * typed into it.
+ */
+add_task(async function test_adding_filelinks_to_empty_forward() {
+ Services.prefs.setIntPref(kReplyOnTopKey, kReplyOnTop);
+ await try_with_and_without_signature_in_reply_or_fwd(
+ subtest_adding_filelinks_to_forward,
+ []
+ );
+ Services.prefs.setBoolPref(kHtmlPrefKey, false);
+ await try_with_and_without_signature_in_reply_or_fwd(
+ subtest_adding_filelinks_to_forward,
+ []
+ );
+ Services.prefs.setBoolPref(kHtmlPrefKey, true);
+});
+
+/**
+ * Tests Filelink insertion on an inline-forward compose window with some
+ * text typed into it.
+ */
+add_task(async function test_adding_filelinks_to_forward() {
+ await try_with_and_without_signature_in_reply_or_fwd(
+ subtest_adding_filelinks_to_forward,
+ kLines
+ );
+ Services.prefs.setBoolPref(kHtmlPrefKey, false);
+ await try_with_and_without_signature_in_reply_or_fwd(
+ subtest_adding_filelinks_to_forward,
+ kLines
+ );
+ Services.prefs.setBoolPref(kHtmlPrefKey, true);
+});
+
+/**
+ * Subtest for both test_adding_filelinks_to_empty_forward and
+ * test_adding_filelinks_to_forward - ensures that the inserted Filelinks
+ * are positioned correctly.
+ */
+async function subtest_adding_filelinks_to_forward(aText, aWithSig) {
+ let [cw, root] = await prepare_some_attachments_and_forward(aText, kFiles);
+
+ let br = assert_next_nodes("br", root, 1);
+ let forwardDiv = br.nextSibling;
+ Assert.equal(forwardDiv.localName, "div");
+ Assert.ok(forwardDiv.classList.contains("moz-forward-container"));
+
+ if (aText.length) {
+ // If there was text typed in, it should be separated from the root by two
+ // br's
+ let br = assert_previous_nodes("br", root, 2);
+ assert_previous_text(br.previousSibling, aText);
+ } else {
+ // Otherwise, there's only 1 br, and that br should be the first element
+ // of the message body.
+ let br = assert_previous_nodes("br", root, 1);
+ let mailBody = get_compose_body(cw);
+ Assert.equal(br, mailBody.firstChild);
+ }
+
+ close_compose_window(cw);
+}
+
+/**
+ * Test that if we convert a Filelink from one provider to another, that the
+ * old Filelink is removed, and a new Filelink is added for the new provider.
+ * We test this on both HTML and plaintext mail.
+ */
+add_task(async function test_converting_filelink_updates_urls() {
+ await try_with_plaintext_and_html_mail(
+ subtest_converting_filelink_updates_urls
+ );
+});
+
+/**
+ * Subtest for test_converting_filelink_updates_urls that creates two
+ * storage provider accounts, uploads files to one, converts them to the
+ * other, and ensures that the attachment links in the message body get
+ * get updated.
+ */
+function subtest_converting_filelink_updates_urls() {
+ gMockFilePicker.returnFiles = collectFiles(kFiles);
+ let providerA = new MockCloudfileAccount();
+ let providerB = new MockCloudfileAccount();
+ providerA.init("providerA", {
+ serviceName: "MochiTest A",
+ serviceUrl: "https://www.provider-A.org",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ });
+ providerB.init("providerB", {
+ serviceName: "MochiTest B",
+ serviceUrl: "https://www.provider-B.org",
+ });
+
+ let cw = open_compose_new_mail();
+ let uploads = add_cloud_attachments(cw, providerA);
+ test_expected_included(
+ uploads,
+ [
+ {
+ url: "https://www.example.com/providerA/testFile1",
+ name: "testFile1",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceName: "MochiTest A",
+ serviceUrl: "https://www.provider-A.org",
+ },
+ {
+ url: "https://www.example.com/providerA/testFile2",
+ name: "testFile2",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceName: "MochiTest A",
+ serviceUrl: "https://www.provider-A.org",
+ },
+ ],
+ `Expected values in uploads array #5`
+ );
+ let [, , UrlsA] = wait_for_attachment_urls(cw, kFiles.length, uploads);
+
+ // Convert each Filelink to providerB, ensuring that the URLs are replaced.
+ uploads = [];
+ for (let i = 0; i < kFiles.length; ++i) {
+ select_attachments(cw, i);
+ uploads.push(...convert_selected_to_cloud_attachment(cw, providerB));
+ }
+ test_expected_included(
+ uploads,
+ [
+ {
+ url: "https://www.example.com/providerB/testFile1",
+ name: "testFile1",
+ serviceIcon: "chrome://messenger/content/extension.svg",
+ serviceName: "MochiTest B",
+ serviceUrl: "https://www.provider-B.org",
+ },
+ {
+ url: "https://www.example.com/providerB/testFile2",
+ name: "testFile2",
+ serviceIcon: "chrome://messenger/content/extension.svg",
+ serviceName: "MochiTest B",
+ serviceUrl: "https://www.provider-B.org",
+ },
+ ],
+ `Expected values in uploads array #6`
+ );
+ let [, , UrlsB] = wait_for_attachment_urls(cw, kFiles.length, uploads);
+ Assert.notEqual(UrlsA, UrlsB, "The original URL should have been replaced");
+
+ close_compose_window(cw);
+}
+
+/**
+ * Test that if we rename a Filelink, that the old Filelink is removed, and a
+ * new Filelink is added. We test this on both HTML and plaintext mail.
+ */
+add_task(async function test_renaming_filelink_updates_urls() {
+ await try_with_plaintext_and_html_mail(
+ subtest_renaming_filelink_updates_urls
+ );
+});
+
+/**
+ * Subtest for test_renaming_filelink_updates_urls that uploads a file to a
+ * storage provider account, renames the upload, and ensures that the attachment
+ * links in the message body get get updated.
+ */
+function subtest_renaming_filelink_updates_urls() {
+ gMockFilePicker.returnFiles = collectFiles(kFiles);
+ let provider = new MockCloudfileAccount();
+ provider.init("providerA", {
+ serviceName: "MochiTest A",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceUrl: "https://www.provider-A.org",
+ downloadExpiryDate: {
+ timestamp: 1639827408073,
+ format: { dateStyle: "short" },
+ },
+ });
+
+ let cw = open_compose_new_mail();
+ let uploads = add_cloud_attachments(cw, provider);
+ test_expected_included(
+ uploads,
+ [
+ {
+ url: "https://www.example.com/providerA/testFile1",
+ name: "testFile1",
+ serviceName: "MochiTest A",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceUrl: "https://www.provider-A.org",
+ downloadExpiryDate: {
+ timestamp: 1639827408073,
+ format: { dateStyle: "short" },
+ },
+ },
+ {
+ url: "https://www.example.com/providerA/testFile2",
+ name: "testFile2",
+ serviceName: "MochiTest A",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceUrl: "https://www.provider-A.org",
+ downloadExpiryDate: {
+ timestamp: 1639827408073,
+ format: { dateStyle: "short" },
+ },
+ },
+ ],
+ `Expected values in uploads array before renaming the files`
+ );
+
+ // Add the expected time string.
+ let timeString = new Date(1639827408073).toLocaleString(undefined, {
+ dateStyle: "short",
+ });
+ uploads[0].downloadExpiryDateString = timeString;
+ uploads[1].downloadExpiryDateString = timeString;
+ let [, , Urls1] = wait_for_attachment_urls(cw, kFiles.length, uploads);
+
+ // Rename each Filelink, ensuring that the URLs are replaced.
+ let newNames = ["testFile1Renamed", "testFile2Renamed"];
+ uploads = [];
+ for (let i = 0; i < kFiles.length; ++i) {
+ select_attachments(cw, i);
+ uploads.push(rename_selected_cloud_attachment(cw, newNames[i]));
+ }
+
+ test_expected_included(
+ uploads,
+ [
+ {
+ url: "https://www.example.com/providerA/testFile1Renamed",
+ name: "testFile1Renamed",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceName: "MochiTest A",
+ serviceUrl: "https://www.provider-A.org",
+ downloadExpiryDate: {
+ timestamp: 1639827408073,
+ format: { dateStyle: "short" },
+ },
+ },
+ {
+ url: "https://www.example.com/providerA/testFile2Renamed",
+ name: "testFile2Renamed",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceName: "MochiTest A",
+ serviceUrl: "https://www.provider-A.org",
+ downloadExpiryDate: {
+ timestamp: 1639827408073,
+ format: { dateStyle: "short" },
+ },
+ },
+ ],
+ `Expected values in uploads array after renaming the files`
+ );
+
+ // Add the expected time string.
+ uploads[0].downloadExpiryDateString = timeString;
+ uploads[1].downloadExpiryDateString = timeString;
+ let [, , Urls2] = wait_for_attachment_urls(cw, kFiles.length, uploads);
+ Assert.notEqual(Urls1, Urls2, "The original URL should have been replaced");
+
+ close_compose_window(cw);
+}
+
+/**
+ * Test that if we convert a Filelink to a normal attachment that the
+ * Filelink is removed from the message body.
+ */
+add_task(async function test_converting_filelink_to_normal_removes_url() {
+ await try_with_plaintext_and_html_mail(
+ subtest_converting_filelink_to_normal_removes_url
+ );
+});
+
+/**
+ * Subtest for test_converting_filelink_to_normal_removes_url that adds
+ * some Filelinks to an email, and then converts those Filelinks back into
+ * normal attachments, checking to ensure that the links are removed from
+ * the body of the email.
+ */
+async function subtest_converting_filelink_to_normal_removes_url() {
+ gMockFilePicker.returnFiles = collectFiles(kFiles);
+ let provider = new MockCloudfileAccount();
+ provider.init("providerC", {
+ serviceName: "MochiTest C",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceUrl: "https://www.provider-C.org",
+ });
+
+ let cw = open_compose_new_mail();
+ let uploads = add_cloud_attachments(cw, provider);
+ test_expected_included(
+ uploads,
+ [
+ {
+ url: "https://www.example.com/providerC/testFile1",
+ name: "testFile1",
+ serviceName: "MochiTest C",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceUrl: "https://www.provider-C.org",
+ },
+ {
+ url: "https://www.example.com/providerC/testFile2",
+ name: "testFile2",
+ serviceName: "MochiTest C",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceUrl: "https://www.provider-C.org",
+ },
+ ],
+ `Expected values in uploads array #7`
+ );
+ let [root, list] = wait_for_attachment_urls(cw, kFiles.length, uploads);
+
+ for (let i = 0; i < kFiles.length; ++i) {
+ let [selectedItem] = select_attachments(cw, i);
+ cw.window.convertSelectedToRegularAttachment();
+
+ // Wait until the cloud file entry has been removed.
+ utils.waitFor(function () {
+ let urls = list.querySelectorAll(".cloudAttachmentItem");
+ return urls.length == kFiles.length - (i + 1);
+ });
+
+ // Check that the cloud icon has been removed.
+ Assert.equal(
+ selectedItem.querySelector("img.attachmentcell-icon").src,
+ `moz-icon://${selectedItem.attachment.name}?size=16`,
+ `CloudIcon should be correctly removed for ${selectedItem.attachment.name}`
+ );
+ }
+
+ // At this point, the root should also have been removed.
+ await new Promise(resolve => setTimeout(resolve));
+ let mailBody = get_compose_body(cw);
+ root = mailBody.querySelector("#cloudAttachmentListRoot");
+ if (root) {
+ throw new Error("Should not have found the cloudAttachmentListRoot");
+ }
+
+ close_compose_window(cw);
+}
+
+/**
+ * Tests that if the user manually removes the Filelinks from the message body
+ * that it doesn't break future Filelink insertions. Tests both HTML and
+ * plaintext composers.
+ */
+add_task(async function test_filelinks_work_after_manual_removal() {
+ await try_with_plaintext_and_html_mail(
+ subtest_filelinks_work_after_manual_removal
+ );
+});
+
+/**
+ * Subtest that first adds some Filelinks to the message body, removes them,
+ * and then adds another Filelink ensuring that the new URL is successfully
+ * inserted.
+ */
+function subtest_filelinks_work_after_manual_removal() {
+ // Insert some Filelinks...
+ gMockFilePicker.returnFiles = collectFiles(kFiles);
+ let provider = new MockCloudfileAccount();
+ provider.init("providerD", {
+ serviceName: "MochiTest D",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceUrl: "https://www.provider-D.org",
+ });
+
+ let cw = open_compose_new_mail();
+ let uploads = add_cloud_attachments(cw, provider);
+ test_expected_included(
+ uploads,
+ [
+ {
+ url: "https://www.example.com/providerD/testFile1",
+ name: "testFile1",
+ serviceName: "MochiTest D",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceUrl: "https://www.provider-D.org",
+ },
+ {
+ url: "https://www.example.com/providerD/testFile2",
+ name: "testFile2",
+ serviceName: "MochiTest D",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceUrl: "https://www.provider-D.org",
+ },
+ ],
+ `Expected values in uploads array #8`
+ );
+ let [root] = wait_for_attachment_urls(cw, kFiles.length, uploads);
+
+ // Now remove the root node from the document body
+ root.remove();
+
+ gMockFilePicker.returnFiles = collectFiles(["./data/testFile3"]);
+ uploads = add_cloud_attachments(cw, provider);
+ test_expected_included(
+ uploads,
+ [
+ {
+ url: "https://www.example.com/providerD/testFile3",
+ name: "testFile3",
+ serviceIcon: "chrome://messenger/skin/icons/globe.svg",
+ serviceName: "MochiTest D",
+ serviceUrl: "https://www.provider-D.org",
+ },
+ ],
+ `Expected values in uploads array #9`
+ );
+ [root] = wait_for_attachment_urls(cw, 1, uploads);
+
+ close_compose_window(cw);
+}
+
+/**
+ * Test that if the users selection caret is on a newline when the URL
+ * insertion occurs, that the caret does not move when the insertion is
+ * complete. Tests both HTML and plaintext composers.
+ */
+add_task(async function test_insertion_restores_caret_point() {
+ await try_with_plaintext_and_html_mail(
+ subtest_insertion_restores_caret_point
+ );
+});
+
+/**
+ * Subtest that types some things into the composer, finishes on two
+ * linebreaks, inserts some Filelink URLs, and then types some more,
+ * ensuring that the selection is where we expect it to be.
+ */
+function subtest_insertion_restores_caret_point() {
+ // Insert some Filelinks...
+ gMockFilePicker.returnFiles = collectFiles(kFiles);
+ let provider = new MockCloudfileAccount();
+ provider.init("providerE", {
+ serviceName: "MochiTest E",
+ serviceUrl: "https://www.provider-E.org",
+ });
+
+ let cw = open_compose_new_mail();
+
+ // Put the selection at the beginning of the document...
+ let editor = cw.window.GetCurrentEditor();
+ editor.beginningOfDocument();
+
+ // Do any necessary typing, ending with two linebreaks.
+ type_in_composer(cw, ["Line 1", "Line 2", "", ""]);
+
+ // Attach some Filelinks.
+ let uploads = add_cloud_attachments(cw, provider);
+ test_expected_included(
+ uploads,
+ [
+ {
+ url: "https://www.example.com/providerE/testFile1",
+ name: "testFile1",
+ serviceName: "MochiTest E",
+ serviceIcon: "chrome://messenger/content/extension.svg",
+ serviceUrl: "https://www.provider-E.org",
+ },
+ {
+ url: "https://www.example.com/providerE/testFile2",
+ name: "testFile2",
+ serviceName: "MochiTest E",
+ serviceIcon: "chrome://messenger/content/extension.svg",
+ serviceUrl: "https://www.provider-E.org",
+ },
+ ],
+ `Expected values in uploads array #10`
+ );
+ let [root] = wait_for_attachment_urls(cw, kFiles.length, uploads);
+
+ // Type some text.
+ const kTypedIn = "Test";
+ type_in_composer(cw, [kTypedIn]);
+
+ // That text should be inserted just above the root attachment URL node.
+ let br = assert_previous_nodes("br", root, 1);
+ assert_previous_text(br.previousSibling, [kTypedIn]);
+
+ close_compose_window(cw);
+}
diff --git a/comm/mail/test/browser/cloudfile/browser_filelinkTelemetry.js b/comm/mail/test/browser/cloudfile/browser_filelinkTelemetry.js
new file mode 100644
index 0000000000..100cbdd1c6
--- /dev/null
+++ b/comm/mail/test/browser/cloudfile/browser_filelinkTelemetry.js
@@ -0,0 +1,123 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test telemetry related to filelink.
+ */
+
+let { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+let { gMockFilePicker, gMockFilePickReg } = ChromeUtils.import(
+ "resource://testing-common/mozmill/AttachmentHelpers.jsm"
+);
+let { gMockCloudfileManager } = ChromeUtils.import(
+ "resource://testing-common/mozmill/CloudfileHelpers.jsm"
+);
+let {
+ add_attachments,
+ add_cloud_attachments,
+ close_compose_window,
+ open_compose_new_mail,
+ setup_msg_contents,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+let { mc } = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+let { wait_for_notification_to_stop } = ChromeUtils.import(
+ "resource://testing-common/mozmill/NotificationBoxHelpers.jsm"
+);
+let { cloudFileAccounts } = ChromeUtils.import(
+ "resource:///modules/cloudFileAccounts.jsm"
+);
+
+let cloudType = "default";
+let kInsertNotificationPref =
+ "mail.compose.big_attachments.insert_notification";
+
+let maxSize =
+ Services.prefs.getIntPref("mail.compose.big_attachments.threshold_kb") * 1024;
+
+add_setup(function () {
+ requestLongerTimeout(2);
+
+ gMockCloudfileManager.register(cloudType);
+ gMockFilePickReg.register();
+
+ Services.prefs.setBoolPref(kInsertNotificationPref, true);
+});
+
+registerCleanupFunction(function () {
+ gMockCloudfileManager.unregister(cloudType);
+ gMockFilePickReg.unregister();
+ Services.prefs.clearUserPref(kInsertNotificationPref);
+});
+
+let kBoxId = "compose-notification-bottom";
+let kNotificationValue = "bigAttachment";
+
+/**
+ * Check that we're counting file size uploaded.
+ */
+add_task(async function test_filelink_uploaded_size() {
+ Services.telemetry.clearScalars();
+ let testFile1Size = 495;
+ let testFile2Size = 637;
+ let totalSize = testFile1Size + testFile2Size;
+
+ gMockFilePicker.returnFiles = collectFiles([
+ "./data/testFile1",
+ "./data/testFile2",
+ ]);
+
+ let provider = cloudFileAccounts.getProviderForType(cloudType);
+ let cwc = open_compose_new_mail(mc);
+ let account = cloudFileAccounts.createAccount(cloudType);
+
+ add_cloud_attachments(cwc, account, false);
+ gMockCloudfileManager.resolveUploads();
+ wait_for_notification_to_stop(cwc.window, kBoxId, "bigAttachmentUploading");
+
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ Assert.equal(
+ scalars["tb.filelink.uploaded_size"][provider.displayName],
+ totalSize,
+ "Count of uploaded size must be correct."
+ );
+ close_compose_window(cwc);
+});
+
+/**
+ * Check that we're counting filelink suggestion ignored.
+ */
+add_task(async function test_filelink_ignored() {
+ Services.telemetry.clearScalars();
+
+ let cwc = open_compose_new_mail(mc);
+ setup_msg_contents(
+ cwc,
+ "test@example.org",
+ "Testing ignoring filelink suggestion",
+ "Hello! "
+ );
+
+ // Multiple big attachments should be counted as one ignoring.
+ add_attachments(cwc, "https://www.example.com/1", maxSize);
+ add_attachments(cwc, "https://www.example.com/2", maxSize + 10);
+ add_attachments(cwc, "https://www.example.com/3", maxSize - 1);
+ let aftersend = BrowserTestUtils.waitForEvent(cwc.window, "aftersend");
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("button-send"),
+ {},
+ cwc.window.document.getElementById("button-send").ownerGlobal
+ );
+ await aftersend;
+ let scalars = TelemetryTestUtils.getProcessScalars("parent");
+ Assert.equal(
+ scalars["tb.filelink.ignored"],
+ 1,
+ "Count of ignored times must be correct."
+ );
+ close_compose_window(cwc, true);
+ close_compose_window(cwc);
+});
diff --git a/comm/mail/test/browser/cloudfile/browser_notifications.js b/comm/mail/test/browser/cloudfile/browser_notifications.js
new file mode 100644
index 0000000000..98e0c49c0f
--- /dev/null
+++ b/comm/mail/test/browser/cloudfile/browser_notifications.js
@@ -0,0 +1,565 @@
+/* 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/. */
+
+/**
+ * Tests that the cloudfile notifications work as they should.
+ */
+
+"use strict";
+
+var { gMockFilePicker, gMockFilePickReg, select_attachments } =
+ ChromeUtils.import("resource://testing-common/mozmill/AttachmentHelpers.jsm");
+var { gMockCloudfileManager, MockCloudfileAccount } = ChromeUtils.import(
+ "resource://testing-common/mozmill/CloudfileHelpers.jsm"
+);
+var {
+ add_attachments,
+ add_cloud_attachments,
+ close_compose_window,
+ open_compose_new_mail,
+ delete_attachment,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var { mc } = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var {
+ assert_notification_displayed,
+ close_notification,
+ wait_for_notification_to_show,
+ wait_for_notification_to_stop,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/NotificationBoxHelpers.jsm"
+);
+var { gMockPromptService } = ChromeUtils.import(
+ "resource://testing-common/mozmill/PromptHelpers.jsm"
+);
+
+var maxSize, oldInsertNotificationPref;
+
+var kOfferThreshold = "mail.compose.big_attachments.threshold_kb";
+var kInsertNotificationPref =
+ "mail.compose.big_attachments.insert_notification";
+
+var kBoxId = "compose-notification-bottom";
+
+add_setup(function () {
+ requestLongerTimeout(2);
+
+ gMockCloudfileManager.register();
+ gMockFilePickReg.register();
+
+ maxSize = Services.prefs.getIntPref(kOfferThreshold, 0) * 1024;
+ oldInsertNotificationPref = Services.prefs.getBoolPref(
+ kInsertNotificationPref
+ );
+ Services.prefs.setBoolPref(kInsertNotificationPref, true);
+});
+
+registerCleanupFunction(function () {
+ gMockCloudfileManager.unregister();
+ gMockFilePickReg.unregister();
+ Services.prefs.setBoolPref(
+ kInsertNotificationPref,
+ oldInsertNotificationPref
+ );
+ Services.prefs.setIntPref(kOfferThreshold, maxSize);
+});
+
+/**
+ * A helper function to assert that the Filelink offer notification is
+ * either displayed or not displayed.
+ *
+ * @param aController the controller of the compose window to check.
+ * @param aDisplayed true if the notification should be displayed, false
+ * otherwise.
+ */
+function assert_cloudfile_notification_displayed(aController, aDisplayed) {
+ assert_notification_displayed(
+ aController.window,
+ kBoxId,
+ "bigAttachment",
+ aDisplayed
+ );
+}
+
+/**
+ * A helper function to assert that the Filelink upload notification is
+ * either displayed or not displayed.
+ *
+ * @param aController the controller of the compose window to check.
+ * @param aDisplayed true if the notification should be displayed, false
+ * otherwise.
+ */
+function assert_upload_notification_displayed(aController, aDisplayed) {
+ assert_notification_displayed(
+ aController.window,
+ kBoxId,
+ "bigAttachmentUploading",
+ aDisplayed
+ );
+}
+
+/**
+ * A helper function to assert that the Filelink privacy warning notification
+ * is either displayed or not displayed.
+ *
+ * @param aController the controller of the compose window to check.
+ * @param aDisplayed true if the notification should be displayed, false
+ * otherwise.
+ */
+function assert_privacy_warning_notification_displayed(
+ aController,
+ aDisplayed
+) {
+ assert_notification_displayed(
+ aController.window,
+ kBoxId,
+ "bigAttachmentPrivacyWarning",
+ aDisplayed
+ );
+}
+
+/**
+ * A helper function to close the Filelink upload notification.
+ */
+function close_upload_notification(aController) {
+ close_notification(aController.window, kBoxId, "bigAttachmentUploading");
+}
+
+/**
+ * A helper function to close the Filelink privacy warning notification.
+ */
+function close_privacy_warning_notification(aController) {
+ close_notification(aController.window, kBoxId, "bigAttachmentPrivacyWarning");
+}
+
+add_task(function test_no_notification_for_small_file() {
+ let cwc = open_compose_new_mail(mc);
+ add_attachments(cwc, "https://www.example.com/1", 0);
+ assert_cloudfile_notification_displayed(cwc, false);
+
+ add_attachments(cwc, "https://www.example.com/2", 1);
+ assert_cloudfile_notification_displayed(cwc, false);
+
+ add_attachments(cwc, "https://www.example.com/3", 100);
+ assert_cloudfile_notification_displayed(cwc, false);
+
+ add_attachments(cwc, "https://www.example.com/4", 500);
+ assert_cloudfile_notification_displayed(cwc, false);
+
+ close_compose_window(cwc);
+});
+
+add_task(function test_notification_for_big_files() {
+ let cwc = open_compose_new_mail(mc);
+ add_attachments(cwc, "https://www.example.com/1", maxSize);
+ assert_cloudfile_notification_displayed(cwc, true);
+
+ add_attachments(cwc, "https://www.example.com/2", maxSize + 1000);
+ assert_cloudfile_notification_displayed(cwc, true);
+
+ add_attachments(cwc, "https://www.example.com/3", maxSize + 10000);
+ assert_cloudfile_notification_displayed(cwc, true);
+
+ add_attachments(cwc, "https://www.example.com/4", maxSize + 100000);
+ assert_cloudfile_notification_displayed(cwc, true);
+
+ close_compose_window(cwc);
+});
+
+add_task(function test_graduate_to_notification() {
+ let cwc = open_compose_new_mail(mc);
+ add_attachments(cwc, "https://www.example.com/1", maxSize - 100);
+ assert_cloudfile_notification_displayed(cwc, false);
+
+ add_attachments(cwc, "https://www.example.com/2", maxSize - 25);
+ assert_cloudfile_notification_displayed(cwc, false);
+
+ add_attachments(cwc, "https://www.example.com/3", maxSize);
+ assert_cloudfile_notification_displayed(cwc, true);
+
+ close_compose_window(cwc);
+});
+
+add_task(function test_no_notification_if_disabled() {
+ Services.prefs.setBoolPref("mail.cloud_files.enabled", false);
+ let cwc = open_compose_new_mail(mc);
+
+ add_attachments(cwc, "https://www.example.com/1", maxSize);
+ assert_cloudfile_notification_displayed(cwc, false);
+
+ add_attachments(cwc, "https://www.example.com/2", maxSize + 1000);
+ assert_cloudfile_notification_displayed(cwc, false);
+
+ add_attachments(cwc, "https://www.example.com/3", maxSize + 10000);
+ assert_cloudfile_notification_displayed(cwc, false);
+
+ add_attachments(cwc, "https://www.example.com/4", maxSize + 100000);
+ assert_cloudfile_notification_displayed(cwc, false);
+
+ close_compose_window(cwc);
+ Services.prefs.setBoolPref("mail.cloud_files.enabled", true);
+});
+
+/**
+ * Tests that if we upload a single file, we get the link insertion
+ * notification bar displayed (unless preffed off).
+ */
+add_task(function test_link_insertion_notification_single() {
+ gMockFilePicker.returnFiles = collectFiles(["./data/testFile1"]);
+ let provider = new MockCloudfileAccount();
+ provider.init("aKey");
+
+ let cwc = open_compose_new_mail(mc);
+ add_cloud_attachments(cwc, provider, false);
+
+ assert_upload_notification_displayed(cwc, true);
+ close_upload_notification(cwc);
+ gMockCloudfileManager.resolveUploads();
+
+ Services.prefs.setBoolPref(kInsertNotificationPref, false);
+ gMockFilePicker.returnFiles = collectFiles(["./data/testFile2"]);
+ add_cloud_attachments(cwc, provider, false);
+
+ assert_upload_notification_displayed(cwc, false);
+ Services.prefs.setBoolPref(kInsertNotificationPref, true);
+
+ close_compose_window(cwc);
+ gMockCloudfileManager.resolveUploads();
+});
+
+/**
+ * Tests that if we upload multiple files, we get the link insertion
+ * notification bar displayed (unless preffed off).
+ */
+add_task(function test_link_insertion_notification_multiple() {
+ gMockFilePicker.returnFiles = collectFiles([
+ "./data/testFile1",
+ "./data/testFile2",
+ ]);
+ let provider = new MockCloudfileAccount();
+ provider.init("aKey");
+
+ let cwc = open_compose_new_mail(mc);
+ add_cloud_attachments(cwc, provider, false);
+
+ assert_upload_notification_displayed(cwc, true);
+ close_upload_notification(cwc);
+ gMockCloudfileManager.resolveUploads();
+
+ Services.prefs.setBoolPref(kInsertNotificationPref, false);
+ gMockFilePicker.returnFiles = collectFiles([
+ "./data/testFile3",
+ "./data/testFile4",
+ ]);
+ add_cloud_attachments(cwc, provider, false);
+
+ assert_upload_notification_displayed(cwc, false);
+ Services.prefs.setBoolPref(kInsertNotificationPref, true);
+
+ close_compose_window(cwc);
+ gMockCloudfileManager.resolveUploads();
+});
+
+/**
+ * Tests that the link insertion notification bar goes away even
+ * if we hit an uploading error.
+ */
+add_task(function test_link_insertion_goes_away_on_error() {
+ gMockPromptService.register();
+ gMockPromptService.returnValue = false;
+ gMockFilePicker.returnFiles = collectFiles([
+ "./data/testFile1",
+ "./data/testFile2",
+ ]);
+ let provider = new MockCloudfileAccount();
+ provider.init("aKey");
+
+ let cwc = open_compose_new_mail(mc);
+ add_cloud_attachments(cwc, provider, false);
+
+ wait_for_notification_to_show(cwc.window, kBoxId, "bigAttachmentUploading");
+ gMockCloudfileManager.rejectUploads();
+ wait_for_notification_to_stop(cwc.window, kBoxId, "bigAttachmentUploading");
+
+ close_compose_window(cwc);
+ gMockPromptService.unregister();
+});
+
+/**
+ * Test that we do not show the Filelink offer notification if we convert
+ * a Filelink back into a normal attachment. Also test, that the privacy
+ * notification is correctly shown and hidden.
+ */
+add_task(async function test_no_offer_on_conversion() {
+ const kFiles = ["./data/testFile1", "./data/testFile2"];
+ // Set the notification threshold to 0 to ensure that we get it.
+ Services.prefs.setIntPref(kOfferThreshold, 0);
+
+ // Insert some Filelinks...
+ gMockFilePicker.returnFiles = collectFiles(kFiles);
+ let provider = new MockCloudfileAccount();
+ provider.init("someKey");
+
+ // Override uploadFile to succeed instantaneously so that we don't have
+ // to worry about waiting for the onStopRequest method being called
+ // asynchronously.
+ provider.uploadFile = function (window, aFile) {
+ return Promise.resolve({
+ id: 1,
+ url: "https://some.cloud.net/1",
+ path: aFile.path,
+ size: aFile.fileSize,
+ });
+ };
+
+ let cw = open_compose_new_mail();
+ add_cloud_attachments(cw, provider, false);
+
+ assert_cloudfile_notification_displayed(cw, false);
+ assert_privacy_warning_notification_displayed(cw, true);
+
+ // Now convert the file back into a normal attachment
+ select_attachments(cw, 0);
+ await cw.window.convertSelectedToRegularAttachment();
+ assert_cloudfile_notification_displayed(cw, false);
+ assert_privacy_warning_notification_displayed(cw, true);
+
+ // Convert also the other file, the privacy notification should no longer
+ // be shown as well.
+ select_attachments(cw, 1);
+ await cw.window.convertSelectedToRegularAttachment();
+ assert_cloudfile_notification_displayed(cw, false);
+ assert_privacy_warning_notification_displayed(cw, false);
+
+ close_compose_window(cw);
+
+ // Now put the old threshold back.
+ Services.prefs.setIntPref(kOfferThreshold, maxSize);
+});
+
+/**
+ * Test that when we kick off an upload via the offer notification, then
+ * the upload notification is shown.
+ */
+add_task(async function test_offer_then_upload_notifications() {
+ const kFiles = ["./data/testFile1", "./data/testFile2"];
+ // Set the notification threshold to 0 to ensure that we get it.
+ Services.prefs.setIntPref(kOfferThreshold, 0);
+
+ // We're going to add attachments to the attachmentbucket, and we'll
+ // use the add_attachments helper function to do it. First, retrieve
+ // some file URIs...
+ let fileURIs = collectFiles(kFiles).map(
+ file => Services.io.newFileURI(file).spec
+ );
+
+ // Create our mock provider
+ let provider = new MockCloudfileAccount();
+ provider.init("someKey");
+
+ // Override uploadFile to succeed instantaneously so that we don't have
+ // to worry about waiting for the onStopRequest method being called
+ // asynchronously.
+ provider.uploadFile = function (window, aFile) {
+ return Promise.resolve({
+ id: 1,
+ url: "https://some.cloud.net/1",
+ path: aFile.path,
+ size: aFile.fileSize,
+ });
+ };
+
+ let cw = open_compose_new_mail();
+
+ // Attach the files, saying that each is 500 bytes large - which should
+ // certainly trigger the offer.
+ add_attachments(cw, fileURIs, [500, 500]);
+ // Assert that the offer is displayed.
+ assert_cloudfile_notification_displayed(cw, true);
+ // Select both attachments in the attachmentbucket, and choose to convert
+ // them.
+ select_attachments(cw, 0, 1);
+ // Convert them.
+ await cw.window.convertSelectedToCloudAttachment(provider);
+
+ // The offer should now be gone...
+ assert_cloudfile_notification_displayed(cw, false);
+ // And the upload notification should be displayed.
+ assert_upload_notification_displayed(cw, true);
+
+ close_compose_window(cw);
+
+ // Now put the old threshold back.
+ Services.prefs.setIntPref(kOfferThreshold, maxSize);
+});
+
+/**
+ * Test that when we first upload some files, we get the privacy warning
+ * message. We should only get this the first time.
+ */
+add_task(function test_privacy_warning_notification() {
+ gMockPromptService.register();
+ gMockPromptService.returnValue = false;
+ gMockFilePicker.returnFiles = collectFiles([
+ "./data/testFile1",
+ "./data/testFile2",
+ ]);
+ let provider = new MockCloudfileAccount();
+ provider.init("aKey");
+
+ let cwc = open_compose_new_mail(mc);
+ add_cloud_attachments(cwc, provider, false);
+
+ wait_for_notification_to_show(cwc.window, kBoxId, "bigAttachmentUploading");
+ gMockCloudfileManager.resolveUploads();
+ wait_for_notification_to_stop(cwc.window, kBoxId, "bigAttachmentUploading");
+
+ // Assert that the warning is displayed.
+ assert_privacy_warning_notification_displayed(cwc, true);
+
+ // Close the privacy warning notification...
+ close_privacy_warning_notification(cwc);
+
+ // And now upload some more files. We shouldn't get the warning again.
+ gMockFilePicker.returnFiles = collectFiles([
+ "./data/testFile3",
+ "./data/testFile4",
+ ]);
+ add_cloud_attachments(cwc, provider, false);
+ gMockCloudfileManager.resolveUploads();
+ assert_privacy_warning_notification_displayed(cwc, false);
+
+ close_compose_window(cwc);
+ gMockPromptService.unregister();
+});
+
+/**
+ * Test that when all cloud attachments are removed, the privacy warning will
+ * be removed as well.
+ */
+add_task(function test_privacy_warning_notification() {
+ gMockPromptService.register();
+ gMockPromptService.returnValue = false;
+ gMockFilePicker.returnFiles = collectFiles([
+ "./data/testFile1",
+ "./data/testFile2",
+ ]);
+ let provider = new MockCloudfileAccount();
+ provider.init("aKey");
+
+ let cwc = open_compose_new_mail(mc);
+ add_cloud_attachments(cwc, provider, false);
+
+ wait_for_notification_to_show(cwc.window, kBoxId, "bigAttachmentUploading");
+ gMockCloudfileManager.resolveUploads();
+ wait_for_notification_to_stop(cwc.window, kBoxId, "bigAttachmentUploading");
+
+ // Assert that the warning is displayed.
+ assert_privacy_warning_notification_displayed(cwc, true);
+
+ // Assert that the warning is still displayed, if one attachment is removed.
+ delete_attachment(cwc, 1);
+ assert_privacy_warning_notification_displayed(cwc, true);
+
+ // Assert that the warning is not displayed, after both attachments are removed.
+ delete_attachment(cwc, 0);
+ assert_privacy_warning_notification_displayed(cwc, false);
+
+ close_compose_window(cwc);
+ gMockPromptService.unregister();
+});
+
+/**
+ * Test that the privacy warning notification does not persist when closing
+ * and re-opening a compose window.
+ */
+add_task(function test_privacy_warning_notification_no_persist() {
+ gMockPromptService.register();
+ gMockPromptService.returnValue = false;
+ gMockFilePicker.returnFiles = collectFiles([
+ "./data/testFile1",
+ "./data/testFile2",
+ ]);
+ let provider = new MockCloudfileAccount();
+ provider.init("mocktestKey");
+
+ let cwc = open_compose_new_mail(mc);
+ add_cloud_attachments(cwc, provider, false);
+
+ wait_for_notification_to_show(cwc.window, kBoxId, "bigAttachmentUploading");
+ gMockCloudfileManager.resolveUploads();
+ wait_for_notification_to_stop(cwc.window, kBoxId, "bigAttachmentUploading");
+
+ // Assert that the warning is displayed.
+ assert_privacy_warning_notification_displayed(cwc, true);
+
+ // Close the compose window
+ close_compose_window(cwc);
+
+ // Open a new compose window
+ cwc = open_compose_new_mail(mc);
+
+ // We shouldn't be displaying the privacy warning.
+ assert_privacy_warning_notification_displayed(cwc, false);
+
+ close_compose_window(cwc);
+ gMockPromptService.unregister();
+});
+
+/**
+ * Test that if we close the privacy warning in a composer, it will still
+ * spawn in a new one.
+ */
+add_task(function test_privacy_warning_notification_open_after_close() {
+ gMockPromptService.register();
+ gMockPromptService.returnValue = false;
+ gMockFilePicker.returnFiles = collectFiles([
+ "./data/testFile1",
+ "./data/testFile2",
+ ]);
+ let provider = new MockCloudfileAccount();
+ provider.init("aKey");
+
+ let cwc = open_compose_new_mail(mc);
+ add_cloud_attachments(cwc, provider, false);
+
+ wait_for_notification_to_show(cwc.window, kBoxId, "bigAttachmentUploading");
+ gMockCloudfileManager.resolveUploads();
+ wait_for_notification_to_stop(cwc.window, kBoxId, "bigAttachmentUploading");
+
+ // Assert that the warning is displayed.
+ assert_privacy_warning_notification_displayed(cwc, true);
+
+ // Close the privacy warning notification...
+ close_privacy_warning_notification(cwc);
+
+ close_compose_window(cwc);
+
+ // Open a new compose window
+ cwc = open_compose_new_mail(mc);
+
+ gMockFilePicker.returnFiles = collectFiles([
+ "./data/testFile3",
+ "./data/testFile4",
+ ]);
+ add_cloud_attachments(cwc, provider, false);
+
+ wait_for_notification_to_show(cwc.window, kBoxId, "bigAttachmentUploading");
+ gMockCloudfileManager.resolveUploads();
+ wait_for_notification_to_stop(cwc.window, kBoxId, "bigAttachmentUploading");
+
+ // Assert that the privacy warning notification is displayed again.
+ assert_privacy_warning_notification_displayed(cwc, true);
+
+ close_compose_window(cwc);
+ gMockPromptService.unregister();
+
+ Assert.report(
+ false,
+ undefined,
+ undefined,
+ "Test ran to completion successfully"
+ );
+});
diff --git a/comm/mail/test/browser/cloudfile/data/testFile1 b/comm/mail/test/browser/cloudfile/data/testFile1
new file mode 100644
index 0000000000..e07edf8e40
--- /dev/null
+++ b/comm/mail/test/browser/cloudfile/data/testFile1
@@ -0,0 +1 @@
+Thundercats single-origin coffee culpa, irony minim vero sunt laborum synth aesthetic. Wayfarers photo booth dolore 8-bit, DIY four loko skateboard forage portland id consectetur. Aesthetic aliquip raw denim aute tofu consequat. Before they sold out etsy cliche marfa, magna seitan fixie brooklyn voluptate laborum messenger bag chillwave narwhal truffaut. Cray cupidatat PBR delectus aliqua synth cillum gentrify. Aesthetic do vegan etsy locavore. Veniam assumenda ea, cupidatat aute vero qui.
diff --git a/comm/mail/test/browser/cloudfile/data/testFile2 b/comm/mail/test/browser/cloudfile/data/testFile2
new file mode 100644
index 0000000000..0509cc907e
--- /dev/null
+++ b/comm/mail/test/browser/cloudfile/data/testFile2
@@ -0,0 +1,3 @@
+Nesciunt odio minim, cupidatat photo booth non post-ironic street art banh mi salvia duis aesthetic squid single-origin coffee. Ex DIY trust fund butcher, esse mustache consequat authentic bushwick twee gentrify hella PBR kogi sustainable. PBR nihil VHS veniam, occaecat dreamcatcher odio iphone irony vero seitan mollit fanny pack adipisicing. Swag jean shorts labore, aesthetic dolore letterpress gluten-free lomo ex wes anderson. Et street art cred Austin velit, raw denim do blog godard leggings. Accusamus adipisicing excepteur occaecat cray sriracha. Leggings brunch artisan occaecat, 3 wolf moon forage mlkshk ad farm-to-table.
+
+
diff --git a/comm/mail/test/browser/cloudfile/data/testFile3 b/comm/mail/test/browser/cloudfile/data/testFile3
new file mode 100644
index 0000000000..627d5147b5
--- /dev/null
+++ b/comm/mail/test/browser/cloudfile/data/testFile3
@@ -0,0 +1,3 @@
+Commodo et laborum fingerstache semiotics etsy. Organic locavore next level, master cleanse raw denim consectetur wes anderson ethical tempor photo booth quis. Leggings pop-up sed trust fund. Chillwave godard velit high life, typewriter umami trust fund. Laboris aliquip assumenda you probably haven't heard of them exercitation portland. Ea do selvage, stumptown dolore etsy commodo tattooed kogi assumenda. Aute tempor carles consequat cray locavore.
+
+Craft beer consectetur anim ex fap consequat, helvetica hella nihil retro before they sold out letterpress cillum mlkshk. Deserunt tempor scenester put a bird on it kale chips mlkshk occaecat, et umami artisan letterpress raw denim sapiente. Echo park pork belly marfa sunt. Iphone aesthetic fanny pack mollit. Irony pork belly bespoke, shoreditch locavore fixie iphone officia mollit mlkshk consequat hoodie mixtape. Nisi consectetur locavore, godard whatever occaecat id blog ethical wolf hoodie PBR. Trust fund sunt mustache, enim eiusmod aesthetic helvetica leggings pinterest laboris polaroid brooklyn.
diff --git a/comm/mail/test/browser/cloudfile/data/testFile4 b/comm/mail/test/browser/cloudfile/data/testFile4
new file mode 100644
index 0000000000..0076011da0
--- /dev/null
+++ b/comm/mail/test/browser/cloudfile/data/testFile4
@@ -0,0 +1 @@
+Ullamco farm-to-table banh mi, echo park dolor lomo chillwave seitan cred ad mustache 3 wolf moon excepteur. Odd future placeat sint, cliche consequat portland banksy vinyl 3 wolf moon high life you probably haven't heard of them nisi jean shorts. Eu freegan skateboard, kogi etsy beard aliquip blog sapiente pour-over sunt. Cosby sweater jean shorts wayfarers keytar trust fund, four loko pitchfork pinterest forage semiotics lo-fi cray beard swag butcher. Odd future cred swag, pork belly keytar velit terry richardson locavore id brunch excepteur commodo occupy mollit pickled. Street art voluptate pickled fap, ad craft beer cupidatat messenger bag placeat helvetica. Enim raw denim occupy pork belly irure et.
diff --git a/comm/mail/test/browser/cloudfile/head.js b/comm/mail/test/browser/cloudfile/head.js
new file mode 100644
index 0000000000..436af7ea0e
--- /dev/null
+++ b/comm/mail/test/browser/cloudfile/head.js
@@ -0,0 +1,7 @@
+/* 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/. */
+
+function collectFiles(files) {
+ return files.map(filename => new FileUtils.File(getTestFilePath(filename)));
+}
diff --git a/comm/mail/test/browser/cloudfile/html/settings-with-link.xhtml b/comm/mail/test/browser/cloudfile/html/settings-with-link.xhtml
new file mode 100644
index 0000000000..838c247a8f
--- /dev/null
+++ b/comm/mail/test/browser/cloudfile/html/settings-with-link.xhtml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <body>
+ <a id="a" href="https://www.example.com/">Click me!</a>
+ </body>
+</html>