summaryrefslogtreecommitdiffstats
path: root/comm/mail/test/browser/composition/browser_attachmentDragDrop.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/test/browser/composition/browser_attachmentDragDrop.js')
-rw-r--r--comm/mail/test/browser/composition/browser_attachmentDragDrop.js689
1 files changed, 689 insertions, 0 deletions
diff --git a/comm/mail/test/browser/composition/browser_attachmentDragDrop.js b/comm/mail/test/browser/composition/browser_attachmentDragDrop.js
new file mode 100644
index 0000000000..bc826574c7
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_attachmentDragDrop.js
@@ -0,0 +1,689 @@
+/* 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 the Drag and Drop functionalities of the attachment bucket in the
+ * message compose window.
+ */
+
+"use strict";
+
+var { CloudFileTestProvider } = ChromeUtils.import(
+ "resource://testing-common/mozmill/CloudfileHelpers.jsm"
+);
+var { gMockFilePicker, gMockFilePickReg } = ChromeUtils.import(
+ "resource://testing-common/mozmill/AttachmentHelpers.jsm"
+);
+
+var { open_compose_new_mail, close_compose_window, add_attachments } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ add_message_to_folder,
+ be_in_folder,
+ create_folder,
+ create_message,
+ FAKE_SERVER_HOSTNAME,
+ get_about_message,
+ inboxFolder,
+ mc,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+const dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
+ Ci.nsIDragService
+);
+
+var gCloudFileProvider;
+var gCloudFileAccount;
+const kFiles = [
+ "./data/attachment.txt",
+ "./data/base64-bug1586890.eml",
+ "./data/base64-encoded-msg.eml",
+ "./data/base64-with-whitespace.eml",
+ "./data/body-greek.eml",
+ "./data/body-utf16.eml",
+];
+
+add_setup(async function () {
+ // Prepare the mock file picker.
+ gMockFilePickReg.register();
+ gMockFilePicker.returnFiles = collectFiles(kFiles);
+
+ // Register an extension based cloudFile provider.
+ gCloudFileProvider = new CloudFileTestProvider("testProvider");
+ await gCloudFileProvider.register(this);
+ gCloudFileAccount = await gCloudFileProvider.createAccount("testAccount");
+});
+
+registerCleanupFunction(async function () {
+ gMockFilePickReg.unregister();
+ // Remove the cloudFile account and unregister the provider.
+ await gCloudFileProvider.removeAccount(gCloudFileAccount);
+ await gCloudFileProvider.unregister();
+});
+
+function getDragOverTarget(win) {
+ return win.document.getElementById("messageArea");
+}
+
+function getDropTarget(win) {
+ return win.document.getElementById("dropAttachmentOverlay");
+}
+
+function initDragSession({ dragData, dropEffect }) {
+ let dropAction;
+ switch (dropEffect) {
+ case null:
+ case undefined:
+ case "move":
+ dropAction = Ci.nsIDragService.DRAGDROP_ACTION_MOVE;
+ break;
+ case "copy":
+ dropAction = Ci.nsIDragService.DRAGDROP_ACTION_COPY;
+ break;
+ case "link":
+ dropAction = Ci.nsIDragService.DRAGDROP_ACTION_LINK;
+ break;
+ default:
+ throw new Error(`${dropEffect} is an invalid drop effect value`);
+ }
+
+ const dataTransfer = new DataTransfer();
+ dataTransfer.dropEffect = dropEffect;
+
+ for (let i = 0; i < dragData.length; i++) {
+ const item = dragData[i];
+ for (let j = 0; j < item.length; j++) {
+ dataTransfer.mozSetDataAt(item[j].type, item[j].data, i);
+ }
+ }
+
+ dragService.startDragSessionForTests(dropAction);
+ const session = dragService.getCurrentSession();
+ session.dataTransfer = dataTransfer;
+
+ return session;
+}
+
+/**
+ * Helper method to simulate a drag and drop action above the window.
+ */
+async function simulateDragAndDrop(win, dragData, type) {
+ let dropTarget = getDropTarget(win);
+ let dragOverTarget = getDragOverTarget(win);
+ let dropEffect = "move";
+
+ let session = initDragSession({ dragData, dropEffect });
+
+ info("Simulate drag over and wait for the drop target to be visible");
+
+ EventUtils.synthesizeDragOver(
+ dragOverTarget,
+ dragOverTarget,
+ dragData,
+ dropEffect,
+ win
+ );
+
+ // This make sure that the fake dataTransfer has still
+ // the expected drop effect after the synthesizeDragOver call.
+ session.dataTransfer.dropEffect = "move";
+
+ await BrowserTestUtils.waitForCondition(
+ () => dropTarget.classList.contains("show"),
+ "Wait for the drop target element to be visible"
+ );
+
+ // If the dragged file is an image, the attach inline container should be
+ // visible.
+ if (type == "image" || type == "inline" || type == "link") {
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ !win.document.getElementById("addInline").classList.contains("hidden"),
+ "Wait for the addInline element to be visible"
+ );
+ } else {
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ win.document.getElementById("addInline").classList.contains("hidden"),
+ "Wait for the addInline element to be hidden"
+ );
+ }
+
+ if (type == "inline") {
+ // Change the drop target to the #addInline container.
+ dropTarget = win.document.getElementById("addInline");
+ }
+
+ info("Simulate drop dragData on drop target");
+
+ EventUtils.synthesizeDropAfterDragOver(
+ null,
+ session.dataTransfer,
+ dropTarget,
+ win,
+ { _domDispatchOnly: true }
+ );
+
+ if (type == "inline") {
+ let editor = win.GetCurrentEditor();
+
+ await BrowserTestUtils.waitForCondition(() => {
+ editor.selectAll();
+ return editor.getSelectedElement("img");
+ }, "Confirm the image was added to the message body");
+
+ Assert.equal(
+ win.document.getElementById("attachmentBucket").itemCount,
+ 0,
+ "Confirm the file hasn't been attached"
+ );
+ } else {
+ // The dropped files should have been attached.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ win.document.getElementById("attachmentBucket").itemCount ==
+ dragData.length,
+ "Wait for the file to be attached"
+ );
+ }
+
+ dragService.endDragSession(true);
+}
+
+/**
+ * Test how the attachment overlay reacts to an image file being dragged above
+ * the message compose window.
+ */
+add_task(async function test_image_file_drag() {
+ let file = new FileUtils.File(getTestFilePath("data/tb-logo.png"));
+ let cwc = open_compose_new_mail();
+
+ await simulateDragAndDrop(
+ cwc.window,
+ [[{ type: "application/x-moz-file", data: file }]],
+ "image"
+ );
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test how the attachment overlay reacts to an image file being dragged above
+ * the message compose window and dropped above the inline container.
+ */
+add_task(async function test_image_file_drag() {
+ let file = new FileUtils.File(getTestFilePath("data/tb-logo.png"));
+ let cwc = open_compose_new_mail();
+
+ await simulateDragAndDrop(
+ cwc.window,
+ [[{ type: "application/x-moz-file", data: file }]],
+ "inline"
+ );
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test how the attachment overlay reacts to a text file being dragged above
+ * the message compose window.
+ */
+add_task(async function test_text_file_drag() {
+ let file = new FileUtils.File(getTestFilePath("data/attachment.txt"));
+ let cwc = open_compose_new_mail();
+
+ await simulateDragAndDrop(
+ cwc.window,
+ [[{ type: "application/x-moz-file", data: file }]],
+ "text"
+ );
+
+ close_compose_window(cwc);
+});
+
+add_task(async function test_message_drag() {
+ let folder = await create_folder("dragondrop");
+ let subject = "Dragons don't drop from the sky";
+ let body = "Dragons can fly after all.";
+ await be_in_folder(folder);
+ await add_message_to_folder(
+ [folder],
+ create_message({ subject, body: { body } })
+ );
+ select_click_row(0);
+
+ let msgStr = get_about_message().gMessageURI;
+ let msgUrl = MailServices.messageServiceFromURI(msgStr).getUrlForUri(msgStr);
+
+ let cwc = open_compose_new_mail();
+ let attachmentBucket = cwc.window.document.getElementById("attachmentBucket");
+
+ await simulateDragAndDrop(
+ cwc.window,
+ [
+ [
+ { type: "text/x-moz-message", data: msgStr },
+ { type: "text/x-moz-url", data: msgUrl.spec },
+ {
+ type: "application/x-moz-file-promise-url",
+ data: msgUrl.spec + "?fileName=" + encodeURIComponent("message.eml"),
+ },
+ {
+ type: "application/x-moz-file-promise",
+ data: new window.messageFlavorDataProvider(),
+ },
+ ],
+ ],
+ "message"
+ );
+
+ let attachment = attachmentBucket.childNodes[0].attachment;
+ Assert.equal(
+ attachment.name,
+ "Dragons don't drop from the sky.eml",
+ "attachment should have expected file name"
+ );
+ Assert.equal(
+ attachment.contentType,
+ "message/rfc822",
+ "attachment should say it's a message"
+ );
+ Assert.notEqual(attachment, 0, "attachment should not be 0 bytes");
+
+ // Clear the added attachment.
+ await cwc.window.RemoveAttachments([attachmentBucket.childNodes[0]]);
+
+ // Try the same with mail.forward_add_extension false.
+ Services.prefs.setBoolPref("mail.forward_add_extension", false);
+
+ await simulateDragAndDrop(
+ cwc.window,
+ [
+ [
+ { type: "text/x-moz-message", data: msgStr },
+ { type: "text/x-moz-url", data: msgUrl.spec },
+ {
+ type: "application/x-moz-file-promise-url",
+ data: msgUrl.spec + "?fileName=" + encodeURIComponent("message.eml"),
+ },
+ {
+ type: "application/x-moz-file-promise",
+ data: new window.messageFlavorDataProvider(),
+ },
+ ],
+ ],
+ "message"
+ );
+
+ let attachment2 = attachmentBucket.childNodes[0].attachment;
+ Assert.equal(
+ attachment2.name,
+ "Dragons don't drop from the sky",
+ "attachment2 should have expected file name"
+ );
+ Assert.equal(
+ attachment2.contentType,
+ "message/rfc822",
+ "attachment2 should say it's a message"
+ );
+ Assert.notEqual(attachment2, 0, "attachment2 should not be 0 bytes");
+
+ Services.prefs.clearUserPref("mail.forward_add_extension");
+
+ close_compose_window(cwc);
+ await be_in_folder(inboxFolder);
+ folder.deleteSelf(null);
+});
+
+add_task(async function test_link_drag() {
+ let cwc = open_compose_new_mail();
+ await simulateDragAndDrop(
+ cwc.window,
+ [
+ [
+ {
+ type: "text/uri-list",
+ data: "https://example.com",
+ },
+ {
+ type: "text/x-moz-url",
+ data: "https://example.com\nExample website",
+ },
+ { type: "application/x-moz-file", data: "" },
+ ],
+ ],
+ "link"
+ );
+
+ let attachment =
+ cwc.window.document.getElementById("attachmentBucket").childNodes[0]
+ .attachment;
+ Assert.equal(
+ attachment.name,
+ "Example website",
+ "Attached link has expected name"
+ );
+ Assert.equal(
+ attachment.url,
+ "https://example.com",
+ "Attached link has correct URL"
+ );
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Get the attachment item for the given url.
+ *
+ * @param {Element} bucket - The element to search in for the attachment item.
+ * @param {string} url - The url of the attachment to find.
+ *
+ * @returns {Element?} - The item with the given attachment url, or null if none
+ * was found.
+ */
+function getAttachmentItem(bucket, url) {
+ for (let child of bucket.childNodes) {
+ if (child.attachment.url == url) {
+ return child;
+ }
+ }
+ return null;
+}
+
+/**
+ * Assert that the given bucket has the given selected items.
+ *
+ * @param {Element} bucket - The bucket to check.
+ * @param {Element[]} selectedItems - The expected selected items in the bucket.
+ */
+function assertSelection(bucket, selectedItems) {
+ for (let child of bucket.childNodes) {
+ if (selectedItems.includes(child)) {
+ Assert.ok(
+ child.selected,
+ `${child.attachment.url} item should be selected`
+ );
+ } else {
+ Assert.ok(
+ !child.selected,
+ `${child.attachment.url} item should not be selected`
+ );
+ }
+ }
+}
+
+/**
+ * Select the given attachment items in the bucket.
+ *
+ * @param {Element} bucket - The attachment bucket to select from.
+ * @param {Element[]} itemSet - The set of attachment items to select. This must
+ * contain at least one item.
+ */
+function selectAttachments(bucket, itemSet) {
+ let win = bucket.ownerGlobal;
+ let first = true;
+ for (let item of itemSet) {
+ item.scrollIntoView();
+ EventUtils.synthesizeMouseAtCenter(item, { ctrlKey: !first }, win);
+ first = false;
+ }
+ assertSelection(bucket, itemSet);
+}
+
+/**
+ * Perform a single drag operation between attachment buckets.
+ *
+ * @param {Element} dragSrc - The attachment item to start dragging from.
+ * @param {Element} destBucket - The attachment bucket to drag to.
+ * @param {string[]} expectUrls - The expected list of all the attachment urls
+ * in the destBucket after the drop. Note this should include both the current
+ * attachments as well as the expected gained attachments.
+ */
+async function moveAttachments(dragSrc, destBucket, expectUrls) {
+ let srcWindow = dragSrc.ownerGlobal;
+ let destWindow = destBucket.ownerGlobal;
+ let dragOverTarget = getDragOverTarget(destWindow);
+ let dropTarget = getDropTarget(destWindow);
+
+ let [dragOverResult, dataTransfer] = EventUtils.synthesizeDragOver(
+ dragSrc,
+ dragOverTarget,
+ null,
+ null,
+ srcWindow,
+ destWindow
+ );
+
+ EventUtils.synthesizeDropAfterDragOver(
+ dragOverResult,
+ dataTransfer,
+ dropTarget,
+ destWindow
+ );
+
+ await TestUtils.waitForCondition(
+ () => destBucket.itemCount == expectUrls.length,
+ `Destination bucket has ${expectUrls.length} attachments`
+ );
+ let items = Array.from(destBucket.childNodes);
+ for (let i = 0; i < items.length; i++) {
+ Assert.ok(
+ items[i].attachment.url.startsWith("file://") &&
+ items[i].attachment.url.includes(expectUrls[i].split(".")[0]),
+ `Attachment url ${items[i].attachment.url} should be the correct file:// url`
+ );
+ }
+}
+
+/**
+ * Perform a series of drag and drop of attachments from the given source bucket
+ * to the given destination bucket.
+ *
+ * The dragged attachment will be saved as a local temporary file. This test
+ * extracts the filename from the url and checks if the url of the attachment
+ * in the destBucket is a file:// url and has the correct file name.
+ * The original url is a mailbox:// url:
+ * mailbox:///something?number=1&part=1.4&filename=file2.txt
+ *
+ * @param {Element} srcBucket - The bucket to drag from. It must contain 6
+ * attachment items and be open.
+ * @param {Element} destBucket - The bucket to drag to. It must be empty.
+ */
+async function drag_between_buckets(srcBucket, destBucket) {
+ Assert.equal(srcBucket.itemCount, 6, "Src bucket starts with 6 attachments");
+ Assert.equal(
+ destBucket.itemCount,
+ 0,
+ "Dest bucket starts with no attachments"
+ );
+
+ let attachmentSet = Array.from(srcBucket.childNodes, item => {
+ return { url: item.attachment.url, srcItem: item };
+ });
+
+ let dragSession = Cc["@mozilla.org/widget/dragservice;1"].getService(
+ Ci.nsIDragService
+ );
+ dragSession.startDragSessionForTests(Ci.nsIDragService.DRAGDROP_ACTION_MOVE);
+
+ // NOTE: Attachment #4 is never dragged from the source to the destination
+ // bucket as part of this test.
+
+ let destUrls = [];
+
+ // Select attachment #2, and drag it.
+ selectAttachments(srcBucket, [attachmentSet[2].srcItem]);
+ destUrls.push(attachmentSet[2].url.split("=").pop());
+ await moveAttachments(attachmentSet[2].srcItem, destBucket, destUrls);
+
+ // Start with attachment #3 selected, but drag attachment #1.
+ // The drag operation should at first change the selection to attachment #1,
+ // such that it becomes the transferred file.
+ selectAttachments(srcBucket, [attachmentSet[3].srcItem]);
+ destUrls.push(attachmentSet[1].url.split("=").pop());
+ await moveAttachments(attachmentSet[1].srcItem, destBucket, destUrls);
+ // Confirm that attachment #1 was selected.
+ assertSelection(srcBucket, [attachmentSet[1].srcItem]);
+
+ // Select two attachments. And then start a drag on one of them.
+ // We expect both the selected attachments to move.
+ selectAttachments(srcBucket, [
+ attachmentSet[0].srcItem,
+ attachmentSet[3].srcItem,
+ ]);
+ destUrls.push(
+ attachmentSet[0].url.split("=").pop(),
+ attachmentSet[3].url.split("=").pop()
+ );
+ await moveAttachments(attachmentSet[3].srcItem, destBucket, destUrls);
+
+ // Select three attachments, two of which are already added.
+ // Expect the new one to be added.
+ selectAttachments(srcBucket, [
+ attachmentSet[1].srcItem,
+ attachmentSet[5].srcItem,
+ attachmentSet[2].srcItem,
+ ]);
+ destUrls.push(attachmentSet[5].url.split("=").pop());
+ await moveAttachments(attachmentSet[1].srcItem, destBucket, destUrls);
+ dragService.endDragSession(true);
+}
+
+/**
+ * Test dragging regular attachments from one composition window to another.
+ */
+add_task(async function test_drag_and_drop_between_composition_windows() {
+ let ctrlSrc = open_compose_new_mail();
+ let ctrlDest = open_compose_new_mail();
+
+ // Add attachments (via mocked file picker).
+ await ctrlSrc.window.AttachFile();
+
+ let srcAttachmentArea =
+ ctrlSrc.window.document.getElementById("attachmentArea");
+
+ // Wait for attachment area to be visible and open in response.
+ await TestUtils.waitForCondition(
+ () =>
+ BrowserTestUtils.is_visible(srcAttachmentArea) && srcAttachmentArea.open,
+ "Attachment area is visible and open"
+ );
+
+ let srcBucket = ctrlSrc.window.document.getElementById("attachmentBucket");
+ let dstBucket = ctrlDest.window.document.getElementById("attachmentBucket");
+ await drag_between_buckets(srcBucket, dstBucket);
+
+ // Make sure a dragged attachment can be converted to a cloudFile attachment.
+ try {
+ await ctrlSrc.window.UpdateAttachment(dstBucket.childNodes[0], {
+ cloudFileAccount: gCloudFileAccount,
+ });
+ Assert.ok(
+ dstBucket.childNodes[0].attachment.sendViaCloud,
+ "Regular attachment should have been converted to a cloudFile attachment."
+ );
+ } catch (ex) {
+ Assert.ok(
+ false,
+ `Converting a drag'n'dropped regular attachment to a cloudFile attachment should succeed: ${ex.message}`
+ );
+ }
+
+ close_compose_window(ctrlSrc);
+ close_compose_window(ctrlDest);
+});
+
+/**
+ * Test dragging cloudFile attachments from one composition window to another.
+ */
+add_task(async function test_cloud_drag_and_drop_between_composition_windows() {
+ let ctrlSrc = open_compose_new_mail();
+ let ctrlDest = open_compose_new_mail();
+
+ // Add cloudFile attachments (via mocked file picker).
+ await ctrlSrc.window.attachToCloudNew(gCloudFileAccount);
+
+ let srcAttachmentArea =
+ ctrlSrc.window.document.getElementById("attachmentArea");
+
+ // Wait for attachment area to be visible and open in response.
+ await TestUtils.waitForCondition(
+ () =>
+ BrowserTestUtils.is_visible(srcAttachmentArea) && srcAttachmentArea.open,
+ "Attachment area is visible and open"
+ );
+
+ let srcBucket = ctrlSrc.window.document.getElementById("attachmentBucket");
+ let dstBucket = ctrlDest.window.document.getElementById("attachmentBucket");
+ await drag_between_buckets(srcBucket, dstBucket);
+
+ // Make sure a dragged cloudFile attachment can be converted to a regular
+ // attachment.
+ try {
+ await ctrlSrc.window.UpdateAttachment(dstBucket.childNodes[0], {
+ cloudFileAccount: null,
+ });
+ Assert.ok(
+ !dstBucket.childNodes[0].attachment.sendViaCloud,
+ "CloudFile Attachment should have been converted to a regular attachment."
+ );
+ } catch (ex) {
+ Assert.ok(
+ false,
+ `Converting a drag'n'dropped cloudFile attachment to a regular attachment should succeed: ${ex.message}`
+ );
+ }
+
+ close_compose_window(ctrlSrc);
+ close_compose_window(ctrlDest);
+});
+
+/**
+ * Test dragging attachments from a message into a composition window.
+ */
+add_task(async function test_drag_and_drop_between_composition_windows() {
+ let ctrlDest = open_compose_new_mail();
+
+ let folder = await create_folder("AttachmentsForComposition");
+ await add_message_to_folder(
+ [folder],
+ create_message({
+ attachments: [0, 1, 2, 3, 4, 5].map(num => {
+ return {
+ body: "Some Text",
+ filename: `file${num}.txt`,
+ format: "text/plain",
+ };
+ }),
+ })
+ );
+ await be_in_folder(folder);
+ select_click_row(0);
+ let aboutMessage = get_about_message();
+ let srcAttachmentArea =
+ aboutMessage.document.getElementById("attachmentView");
+ Assert.ok(!srcAttachmentArea.collapsed, "Attachment area is visible");
+
+ let srcBucket = aboutMessage.document.getElementById("attachmentList");
+ EventUtils.synthesizeMouseAtCenter(
+ aboutMessage.document.getElementById("attachmentBar"),
+ {},
+ aboutMessage
+ );
+ Assert.ok(!srcBucket.collapsed, "Attachment list is visible");
+
+ await drag_between_buckets(
+ srcBucket,
+ ctrlDest.window.document.getElementById("attachmentBucket")
+ );
+
+ close_compose_window(ctrlDest);
+});
+
+function collectFiles(files) {
+ return files.map(filename => new FileUtils.File(getTestFilePath(filename)));
+}