summaryrefslogtreecommitdiffstats
path: root/comm/mail/test/browser/cloudfile/browser_attachmentUrls.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/test/browser/cloudfile/browser_attachmentUrls.js')
-rw-r--r--comm/mail/test/browser/cloudfile/browser_attachmentUrls.js1554
1 files changed, 1554 insertions, 0 deletions
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);
+}