diff options
Diffstat (limited to 'comm/mail/test/browser/composition/browser_attachmentDragDrop.js')
-rw-r--r-- | comm/mail/test/browser/composition/browser_attachmentDragDrop.js | 689 |
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))); +} |