Bug 1495895 - make adding compose attachments more robust in MozMill tests. r=jorgk
authoraceman <acelists@atlas.sk>
Tue, 02 Oct 2018 14:30:00 +0200
changeset 33289 686249883a13de5ea4f23965cf2895c7fa006563
parent 33288 214f2141656709436ccec5deb8df5a1a329f12c9
child 33290 b9846bdfa088b43e94d7a801d87622248c79e78d
push id387
push userclokep@gmail.com
push dateMon, 10 Dec 2018 21:30:47 +0000
reviewersjorgk
bugs1495895
Bug 1495895 - make adding compose attachments more robust in MozMill tests. r=jorgk
mail/test/mozmill/attachment/test-attachment-events.js
mail/test/mozmill/cloudfile/test-cloudfile-attachment-item.js
mail/test/mozmill/cloudfile/test-cloudfile-attachment-urls.js
mail/test/mozmill/cloudfile/test-cloudfile-notifications.js
mail/test/mozmill/shared-modules/test-cloudfile-backend-helpers.js
mail/test/mozmill/shared-modules/test-compose-helpers.js
--- a/mail/test/mozmill/attachment/test-attachment-events.js
+++ b/mail/test/mozmill/attachment/test-attachment-events.js
@@ -51,39 +51,39 @@ function test_attachments_added_on_singl
     lastEvent = event;
   }
 
   // Open up the compose window
   let cw = open_compose_new_mail(mc);
   cw.e("attachmentBucket").addEventListener(kAttachmentsAdded, listener);
 
   // Attach a single file
-  add_attachment(cw, "http://www.example.com/1", 0);
+  add_attachment(cw, "http://www.example.com/1", 0, false);
 
   // Make sure we only saw the event once
   assert_equals(1, eventCount);
 
   // Make sure that we were passed the right subject
   let subjects = lastEvent.detail;
   assert_true(subjects instanceof Ci.nsIMutableArray);
   assert_equals("http://www.example.com/1",
                 subjects.queryElementAt(0, Ci.nsIMsgAttachment).url);
 
   // Make sure that we can get that event again if we
   // attach more files.
-  add_attachment(cw, "http://www.example.com/2", 0);
+  add_attachment(cw, "http://www.example.com/2", 0, false);
   assert_equals(2, eventCount);
   subjects = lastEvent.detail;
   assert_true(subjects instanceof Ci.nsIMutableArray);
   assert_equals("http://www.example.com/2",
                 subjects.queryElementAt(0, Ci.nsIMsgAttachment).url);
 
   // And check that we don't receive the event if we try to attach a file
   // that's already attached.
-  add_attachment(cw, "http://www.example.com/2");
+  add_attachment(cw, "http://www.example.com/2", null, false);
   assert_equals(2, eventCount);
 
   cw.e("attachmentBucket").removeEventListener(kAttachmentsAdded, listener);
   close_compose_window(cw);
 }
 
 /**
  * Test that the attachments-added event is fired when we add a series
@@ -102,17 +102,17 @@ function test_attachments_added_on_multi
   // make sure that we observed the right event subjects later on.
   let attachmentUrls = ["http://www.example.com/1",
                         "http://www.example.com/2"];
 
   // Open the compose window and add the attachments
   let cw = open_compose_new_mail(mc);
   cw.e("attachmentBucket").addEventListener(kAttachmentsAdded, listener);
 
-  add_attachments(cw, attachmentUrls);
+  add_attachments(cw, attachmentUrls, null, false);
 
   // Make sure we only saw a single attachments-added for this group
   // of files.
   assert_equals(1, eventCount);
 
   // Now make sure we got passed the right subjects for the event
   let subjects = lastEvent.detail;
   assert_true(subjects instanceof Ci.nsIMutableArray);
@@ -129,31 +129,31 @@ function test_attachments_added_on_multi
                     "http://www.example.com/2",
                     "http://www.example.com/3"];
 
   // Open the compose window and attach the files, and ensure that we saw
   // the attachments-added event
   cw = open_compose_new_mail(mc);
   cw.e("attachmentBucket").addEventListener(kAttachmentsAdded, listener);
 
-  add_attachments(cw, attachmentUrls);
+  add_attachments(cw, attachmentUrls, null, false);
   assert_equals(2, eventCount);
 
   // Make sure that we got the right subjects back
   subjects = lastEvent.detail;
   assert_true(subjects instanceof Ci.nsIMutableArray);
   assert_equals(3, subjects.length);
 
   for (let attachment of fixIterator(subjects, Ci.nsIMsgAttachment)) {
     assert_true(attachmentUrls.includes(attachment.url));
   }
 
   // Make sure we don't fire the event again if we try to attach the same
   // files.
-  add_attachments(cw, attachmentUrls);
+  add_attachments(cw, attachmentUrls, null, false);
   assert_equals(2, eventCount);
 
   cw.e("attachmentBucket").removeEventListener(kAttachmentsAdded, listener);
   close_compose_window(cw);
 }
 
 /**
  * Test that the attachments-removed event is fired when removing a
--- a/mail/test/mozmill/cloudfile/test-cloudfile-attachment-item.js
+++ b/mail/test/mozmill/cloudfile/test-cloudfile-attachment-item.js
@@ -72,16 +72,18 @@ function test_upload_cancel_repeat() {
 
     // Select the attachment, and choose to convert it to a Filelink
     let attachmentitem = select_attachments(cw, 0)[0];
     cw.window.convertSelectedToCloudAttachment(provider);
     cw.waitFor(() => started);
 
     assert_can_cancel_upload(cw, provider, listener, file);
   }
+
+  close_compose_window(cw);
 }
 
 /**
  * Test that we can cancel a whole series of files being uploaded at once.
  */
 function test_upload_multiple_and_cancel() {
   const kFiles = ["./data/testFile1",
                   "./data/testFile2",
@@ -96,20 +98,23 @@ function test_upload_multiple_and_cancel
   let cw = open_compose_new_mail();
 
   let listener;
   provider.uploadFile = function(aFile, aListener) {
     listener = aListener;
     listener.onStartRequest(null, null);
   };
 
-  cw.window.attachToCloud(provider);
+  add_cloud_attachments(cw, provider, false);
 
-  for (let i = files.length - 1; i >= 0; --i)
+  for (let i = files.length - 1; i >= 0; --i) {
     assert_can_cancel_upload(cw, provider, listener, files[i]);
+  }
+
+  close_compose_window(cw);
 }
 
 /**
  * Helper function that takes an upload in progress, and cancels it,
  * ensuring that the nsIMsgCloduFileProvider.uploadCanceled status message
  * is returned to the passed in listener.
  *
  * @param aController the compose window controller to use.
--- a/mail/test/mozmill/cloudfile/test-cloudfile-attachment-urls.js
+++ b/mail/test/mozmill/cloudfile/test-cloudfile-attachment-urls.js
@@ -128,17 +128,17 @@ function prepare_some_attachments_and_re
   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);
-  cw.window.attachToCloud(provider);
+  add_cloud_attachments(cw, provider);
   return cw;
 }
 
 /**
  * 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.
@@ -163,17 +163,17 @@ function prepare_some_attachments_and_fo
   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);
-  cw.window.attachToCloud(provider);
+  add_cloud_attachments(cw, provider);
   return cw;
 }
 
 /**
  * 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.
  *
@@ -237,17 +237,17 @@ function test_inserts_linebreak_on_empty
  * 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, __file__);
   let provider = new MockCloudfileAccount();
   provider.init("someKey");
   let cw = open_compose_new_mail();
-  cw.window.attachToCloud(provider);
+  add_cloud_attachments(cw, provider);
 
   let [root, list, urls] = wait_for_attachment_urls(cw, kFiles.length);
 
   let br = root.previousSibling;
   assert_equals(br.localName, "br",
                 "The attachment URL containment node should be preceded by " +
                 "a linebreak");
 
@@ -266,17 +266,17 @@ function subtest_inserts_linebreak_on_em
  * accidentally insert the attachment URL containment within the signature
  * node.
  */
 function test_inserts_linebreak_on_empty_compose_with_signature() {
   gMockFilePicker.returnFiles = collectFiles(kFiles, __file__);
   let provider = new MockCloudfileAccount();
   provider.init("someKey");
   let cw = open_compose_new_mail();
-  cw.window.attachToCloud(provider);
+  add_cloud_attachments(cw, provider);
   // 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, list, urls] = wait_for_attachment_urls(cw, kFiles.length);
 
   let br = assert_previous_nodes("br", root, 1);
 
   let mailBody = get_compose_body(cw);
@@ -294,17 +294,17 @@ function test_inserts_linebreak_on_empty
               "The pre should have the moz-signature class");
 
   close_window(cw);
 
   Services.prefs.setBoolPref(kHtmlPrefKey, false);
 
   // Now let's try with plaintext mail.
   cw = open_compose_new_mail();
-  cw.window.attachToCloud(provider);
+  add_cloud_attachments(cw, provider);
   [root, list, urls] = wait_for_attachment_urls(cw, kFiles.length);
 
   br = assert_previous_nodes("br", root, 1);
 
   mailBody = get_compose_body(cw);
   assert_equals(mailBody.firstChild, br,
                 "The linebreak should be the first child of the compose body");
 
@@ -368,17 +368,17 @@ function test_adding_filelinks_to_writte
  */
 function subtest_adding_filelinks_to_written_message() {
   gMockFilePicker.returnFiles = collectFiles(kFiles, __file__);
   let provider = new MockCloudfileAccount();
   provider.init("someKey");
   let cw = open_compose_new_mail();
 
   type_in_composer(cw, kLines);
-  cw.window.attachToCloud(provider);
+  add_cloud_attachments(cw, provider);
 
   let [root, list, urls] = wait_for_attachment_urls(cw, kFiles.length);
 
   let br = root.previousSibling;
   assert_equals(br.localName, "br",
                 "The attachment URL containment node should be preceded by " +
                 "a linebreak");
   br = br.previousSibling;
@@ -680,17 +680,17 @@ function test_converting_filelink_update
 function subtest_converting_filelink_updates_urls() {
   gMockFilePicker.returnFiles = collectFiles(kFiles, __file__);
   let providerA = new MockCloudfileAccount();
   let providerB = new MockCloudfileAccount();
   providerA.init("providerA");
   providerB.init("providerB");
 
   let cw = open_compose_new_mail();
-  cw.window.attachToCloud(providerA);
+  add_cloud_attachments(cw, providerA);
 
   let [root, list, urls] = wait_for_attachment_urls(cw, kFiles.length);
 
   // Convert each Filelink to providerB, ensuring that the URLs are replaced.
   for (let i = 0; i < kFiles.length; ++i) {
     let url = urls[i];
     select_attachments(cw, i);
     cw.window.convertSelectedToCloudAttachment(providerB);
@@ -721,17 +721,17 @@ function test_converting_filelink_to_nor
  * the body of the email.
  */
 function subtest_converting_filelink_to_normal_removes_url() {
   gMockFilePicker.returnFiles = collectFiles(kFiles, __file__);
   let provider = new MockCloudfileAccount();
   provider.init("someKey");
 
   let cw = open_compose_new_mail();
-  cw.window.attachToCloud(provider);
+  add_cloud_attachments(cw, provider);
 
   let [root, list, urls] = wait_for_attachment_urls(cw, kFiles.length);
 
   for (let i = 0; i < kFiles.length; ++i) {
     select_attachments(cw, i);
     cw.window.convertSelectedToRegularAttachment();
 
     let urls = list.querySelectorAll(".cloudAttachmentItem");
@@ -762,25 +762,25 @@ function test_filelinks_work_after_manua
  * inserted.
  */
 function subtest_filelinks_work_after_manual_removal() {
   // Insert some Filelinks...
   gMockFilePicker.returnFiles = collectFiles(kFiles, __file__);
   let provider = new MockCloudfileAccount();
   provider.init("someKey");
   let cw = open_compose_new_mail();
-  cw.window.attachToCloud(provider);
+  add_cloud_attachments(cw, provider);
 
   let [root, list, urls] = wait_for_attachment_urls(cw, kFiles.length);
 
   // Now remove the root node from the document body
   root.remove();
 
   gMockFilePicker.returnFiles = collectFiles(["./data/testFile3"], __file__);
-  cw.window.attachToCloud(provider);
+  add_cloud_attachments(cw, provider);
   [root, list, urls] = wait_for_attachment_urls(cw, 1);
 
   close_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
@@ -806,17 +806,17 @@ function subtest_insertion_restores_care
   // 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.
-  cw.window.attachToCloud(provider);
+  add_cloud_attachments(cw, provider);
   let [root, list, urls] = wait_for_attachment_urls(cw, kFiles.length);
 
   // Type some text.
   const kTypedIn = "Test";
   type_in_composer(cw, [kTypedIn]);
 
   // That text should be inserted just above the root attachment URL node.
   let textNode = assert_previous_text(root.previousSibling, [kTypedIn]);
--- a/mail/test/mozmill/cloudfile/test-cloudfile-notifications.js
+++ b/mail/test/mozmill/cloudfile/test-cloudfile-notifications.js
@@ -13,18 +13,16 @@ var MODULE_NAME = 'test-cloudfile-notifi
 var RELATIVE_ROOT = '../shared-modules';
 var MODULE_REQUIRES = ['folder-display-helpers',
                        'compose-helpers',
                        'cloudfile-helpers',
                        'attachment-helpers',
                        'prompt-helpers',
                        'notificationbox-helpers'];
 
-//var controller = {};
-//ChromeUtils.import("chrome://mozmill/content/modules/controller.js", controller);
 ChromeUtils.import('resource://gre/modules/Services.jsm');
 
 var maxSize, oldInsertNotificationPref;
 
 var kOfferThreshold = "mail.compose.big_attachments.threshold_kb";
 var kInsertNotificationPref = "mail.compose.big_attachments.insert_notification";
 
 var kBoxId = "attachmentNotificationBox";
@@ -175,24 +173,25 @@ function test_no_notification_if_disable
  * notification bar displayed (unless preffed off).
  */
 function test_link_insertion_notification_single() {
   gMockFilePicker.returnFiles = collectFiles(['./data/testFile1'], __file__);
   let provider = new MockCloudfileAccount();
   provider.init("aKey");
 
   let cwc = open_compose_new_mail(mc);
-  cwc.window.attachToCloud(provider);
+  add_cloud_attachments(cwc, provider);
 
   assert_upload_notification_displayed(cwc, true);
   close_upload_notification(cwc);
 
   Services.prefs.setBoolPref(kInsertNotificationPref, false);
   gMockFilePicker.returnFiles = collectFiles(['./data/testFile2'], __file__);
-  cwc.window.attachToCloud(provider);
+  add_cloud_attachments(cwc, provider);
+
   assert_upload_notification_displayed(cwc, false);
   Services.prefs.setBoolPref(kInsertNotificationPref, true);
 
   close_compose_window(cwc);
 }
 
 /**
  * Tests that if we upload multiple files, we get the link insertion
@@ -200,25 +199,26 @@ function test_link_insertion_notificatio
  */
 function test_link_insertion_notification_multiple() {
   gMockFilePicker.returnFiles = collectFiles(['./data/testFile1',
                                               './data/testFile2'], __file__);
   let provider = new MockCloudfileAccount();
   provider.init("aKey");
 
   let cwc = open_compose_new_mail(mc);
-  cwc.window.attachToCloud(provider);
+  add_cloud_attachments(cwc, provider);
 
   assert_upload_notification_displayed(cwc, true);
   close_upload_notification(cwc);
 
   Services.prefs.setBoolPref(kInsertNotificationPref, false);
   gMockFilePicker.returnFiles = collectFiles(['./data/testFile3',
                                               './data/testFile4'], __file__);
-  cwc.window.attachToCloud(provider);
+  add_cloud_attachments(cwc, provider);
+
   assert_upload_notification_displayed(cwc, false);
   Services.prefs.setBoolPref(kInsertNotificationPref, true);
 
   close_compose_window(cwc);
 }
 
 /**
  * Tests that the link insertion notification bar goes away even
@@ -233,19 +233,20 @@ function test_link_insertion_goes_away_o
   provider.init("aKey");
 
   provider.uploadFile = function(aFile, aListener) {
     aListener.onStartRequest(null, null);
     cwc.window.setTimeout(function() {
       aListener.onStopRequest(null, null,
                               Ci.nsIMsgCloudFileProvider.uploadErr);
     }, 500);
-  }
+  };
+
   let cwc = open_compose_new_mail(mc);
-  cwc.window.attachToCloud(provider);
+  add_cloud_attachments(cwc, provider, false);
 
   assert_upload_notification_displayed(cwc, true);
   wait_for_notification_to_stop(cwc, kBoxId, "bigAttachmentUploading");
 
   close_compose_window(cwc);
   gMockPromptService.unregister();
 }
 
@@ -267,17 +268,18 @@ function test_no_offer_on_conversion() {
   // to worry about waiting for the onStopRequest method being called
   // asynchronously.
   provider.uploadFile = function(aFile, aListener) {
     aListener.onStartRequest(null, null);
     aListener.onStopRequest(null, null, Cr.NS_OK);
   };
 
   let cw = open_compose_new_mail();
-  cw.window.attachToCloud(provider);
+  add_cloud_attachments(cw, provider, false);
+
   assert_cloudfile_notification_displayed(cw, false);
   // Now convert the file back into a normal attachment
   select_attachments(cw, 0);
   cw.window.convertSelectedToRegularAttachment();
 
   assert_cloudfile_notification_displayed(cw, false);
 
   close_compose_window(cw);
@@ -352,31 +354,31 @@ function test_privacy_warning_notificati
   provider.uploadFile = function(aFile, aListener) {
     aListener.onStartRequest(null, null);
     cwc.window.setTimeout(function() {
       aListener.onStopRequest(null, null,
                               Cr.NS_OK);
     }, 500);
   }
   let cwc = open_compose_new_mail(mc);
-  cwc.window.attachToCloud(provider);
+  add_cloud_attachments(cwc, provider);
 
   assert_upload_notification_displayed(cwc, true);
   wait_for_notification_to_stop(cwc, 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'], __file__);
-  cwc.window.attachToCloud(provider);
+  add_cloud_attachments(cwc, provider, false);
   assert_privacy_warning_notification_displayed(cwc, false);
 
   close_compose_window(cwc);
   gMockPromptService.unregister();
 }
 
 /**
  * Test that the privacy warning notification does not persist when closing
@@ -393,17 +395,17 @@ function test_privacy_warning_notificati
   provider.uploadFile = function(aFile, aListener) {
     aListener.onStartRequest(null, null);
     cwc.window.setTimeout(function() {
       aListener.onStopRequest(null, null,
                               Cr.NS_OK);
     }, 500);
   }
   let cwc = open_compose_new_mail(mc);
-  cwc.window.attachToCloud(provider);
+  add_cloud_attachments(cwc, provider, false);
 
   assert_upload_notification_displayed(cwc, true);
   wait_for_notification_to_stop(cwc, kBoxId, "bigAttachmentUploading");
 
   // Assert that the warning is displayed.
   assert_privacy_warning_notification_displayed(cwc, true);
 
   // Close the compose window
@@ -434,17 +436,17 @@ function test_privacy_warning_notificati
   provider.uploadFile = function(aFile, aListener) {
     aListener.onStartRequest(null, null);
     cwc.window.setTimeout(function() {
       aListener.onStopRequest(null, null,
                               Cr.NS_OK);
     }, 500);
   }
   let cwc = open_compose_new_mail(mc);
-  cwc.window.attachToCloud(provider);
+  add_cloud_attachments(cwc, provider, false);
 
   assert_upload_notification_displayed(cwc, true);
   wait_for_notification_to_stop(cwc, kBoxId, "bigAttachmentUploading");
 
   // Assert that the warning is displayed.
   assert_privacy_warning_notification_displayed(cwc, true);
 
   // Close the privacy warning notification...
@@ -452,17 +454,17 @@ function test_privacy_warning_notificati
 
   close_compose_window(cwc);
 
   // Open a new compose window
   cwc = open_compose_new_mail(mc);
 
   gMockFilePicker.returnFiles = collectFiles(['./data/testFile3',
                                               './data/testFile4'], __file__);
-  cwc.window.attachToCloud(provider);
+  add_cloud_attachments(cwc, provider);
 
   assert_upload_notification_displayed(cwc, true);
   wait_for_notification_to_stop(cwc, kBoxId, "bigAttachmentUploading");
 
   // Assert that the privacy warning notification is displayed again.
   assert_privacy_warning_notification_displayed(cwc, true);
 
   close_compose_window(cwc);
--- a/mail/test/mozmill/shared-modules/test-cloudfile-backend-helpers.js
+++ b/mail/test/mozmill/shared-modules/test-cloudfile-backend-helpers.js
@@ -110,18 +110,19 @@ function assert_can_cancel_uploads(aCont
     fileListenerMap.push(mapping);
   }
 
   // Wait for the first file to start uploading...
   wh.wait_for_observable_event("cloudfile:uploadStarted");
 
   // Go backwards through the file list, ensuring that we can cancel the
   // last file, all the way to the first.
-  for (let i = aFiles.length - 1; i >= 0; --i)
+  for (let i = aFiles.length - 1; i >= 0; --i) {
     aProvider.cancelFileUpload(aFiles[i]);
+  }
 
   aController.waitFor(function() {
     return fileListenerMap.length == aFiles.length &&
            fileListenerMap.every(function(aMapping) {
-             return aMapping.cancelled
+             return aMapping.cancelled;
            })
   }, "Timed out waiting for cancellation to occur");
 }
--- a/mail/test/mozmill/shared-modules/test-compose-helpers.js
+++ b/mail/test/mozmill/shared-modules/test-compose-helpers.js
@@ -46,16 +46,17 @@ function installInto(module) {
   module.close_compose_window = close_compose_window;
   module.wait_for_compose_window = wait_for_compose_window;
   module.setup_msg_contents = setup_msg_contents;
   module.clear_recipient = clear_recipient;
   module.toggle_recipient_type = toggle_recipient_type;
   module.create_msg_attachment = create_msg_attachment;
   module.add_attachments = add_attachments;
   module.add_attachment = add_attachments;
+  module.add_cloud_attachments = add_cloud_attachments;
   module.delete_attachment = delete_attachment;
   module.get_compose_body = get_compose_body;
   module.type_in_composer = type_in_composer;
   module.assert_previous_text = assert_previous_text;
   module.get_msg_source = get_msg_source;
 }
 
 /**
@@ -349,35 +350,86 @@ function create_msg_attachment(aUrl, aSi
   attachment.url = aUrl;
   if(aSize)
     attachment.size = aSize;
 
   return attachment;
 }
 
 /**
- * Add an attachment to the compose window
- * @param aComposeWindow the composition window in question
- * @param aUrl the URL for this attachment (either a file URL or a web URL)
- * @param aSize (optional) the file size of this attachment, in bytes
+ * Add an attachment to the compose window.
+ *
+ * @param aController  the controller of the composition window in question
+ * @param aUrl         the URL for this attachment (either a file URL or a web URL)
+ * @param aSize (optional)  the file size of this attachment, in bytes
+ * @param aWaitAdded (optional)  True to wait for the attachments to be fully added, false otherwise.
  */
-function add_attachments(aComposeWindow, aUrls, aSizes) {
+function add_attachments(aController, aUrls, aSizes, aWaitAdded = true) {
   if (!Array.isArray(aUrls))
     aUrls = [aUrls];
 
   if (!Array.isArray(aSizes))
     aSizes = [aSizes];
 
   let attachments = [];
 
   for (let [i, url] of aUrls.entries()) {
     attachments.push(create_msg_attachment(url, aSizes[i]));
   }
 
-  aComposeWindow.window.AddAttachments(attachments);
+  let attachmentsDone = false;
+  function collectAddedAttachments(event) {
+    folderDisplayHelper.assert_equals(event.detail.length, attachments.length);
+    attachmentsDone = true;
+  }
+
+  let bucket = aController.e("attachmentBucket");
+  if (aWaitAdded)
+    bucket.addEventListener("attachments-added", collectAddedAttachments, { once: true });
+  aController.window.AddAttachments(attachments);
+  if (aWaitAdded)
+    aController.waitFor(() => attachmentsDone, "Attachments adding didn't finish");
+  aController.sleep(0);
+}
+
+/**
+ * Add a cloud (filelink) attachment to the compose window.
+ *
+ * @param aController    The controller of the composition window in question.
+ * @param aProvider      The provider account to upload to, with files to be uploaded.
+ * @param aWaitUploaded (optional)  True to wait for the attachments to be uploaded, false otherwise.
+ */
+function add_cloud_attachments(aController, aProvider, aWaitUploaded = true) {
+  let bucket = aController.e("attachmentBucket");
+
+  let attachmentsSubmitted = false;
+  function uploadAttachments(event) {
+    attachmentsSubmitted = true;
+    if (aWaitUploaded) {
+      // event.detail contains an array of nsIMsgAttachment objects that were uploaded.
+      attachmentCount = event.detail.length;
+      for (let attachment of event.detail) {
+        let item = bucket.findItemForAttachment(attachment);
+        item.addEventListener("attachment-uploaded", collectUploadedAttachments, { once: true });
+      }
+    }
+  }
+
+  let attachmentCount = 0;
+  function collectUploadedAttachments(event) {
+    attachmentCount--;
+  }
+
+  bucket.addEventListener("attachments-uploading", uploadAttachments, { once: true });
+  aController.window.attachToCloud(aProvider);
+  aController.waitFor(() => attachmentsSubmitted, "Couldn't attach attachments for upload");
+  if (aWaitUploaded) {
+    aController.waitFor(() => attachmentCount == 0, "Attachments uploading didn't finish");
+  }
+  aController.sleep(0);
 }
 
 /**
  * Delete an attachment from the compose window
  * @param aComposeWindow the composition window in question
  * @param aIndex the index of the attachment in the attachment pane
  */
 function delete_attachment(aComposeWindow, aIndex) {