/* 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/. */ /** * Checks various attachments display correctly */ "use strict"; var { close_compose_window, open_compose_with_forward } = ChromeUtils.import( "resource://testing-common/mozmill/ComposeHelpers.jsm" ); var { add_message_to_folder, assert_attachment_list_focused, assert_message_pane_focused, assert_selected_and_displayed, be_in_folder, close_popup, create_folder, create_message, get_about_message, mc, msgGen, plan_to_wait_for_folder_events, select_click_row, select_none, wait_for_folder_events, wait_for_message_display_completion, wait_for_popup_to_open, } = ChromeUtils.import( "resource://testing-common/mozmill/FolderDisplayHelpers.jsm" ); var { SyntheticPartLeaf, SyntheticPartMultiMixed } = ChromeUtils.import( "resource://testing-common/mailnews/MessageGenerator.jsm" ); var { async_plan_for_new_window, close_window, plan_for_modal_dialog, wait_for_modal_dialog, } = ChromeUtils.import("resource://testing-common/mozmill/WindowHelpers.jsm"); var folder; var messages; var textAttachment = "One of these days... people like me will rise up and overthrow you, and " + "the end of tyranny by the homeostatic machine will have arrived. The day " + "of human values and compassion and simple warmth will return, and when " + "that happens someone like myself who has gone through an ordeal and who " + "genuinely needs hot coffee to pick him up and keep him functioning when " + "he has to function will get the hot coffee whether he happens to have a " + "poscred readily available or not."; var binaryAttachment = textAttachment; add_setup(async function () { folder = await create_folder("AttachmentA"); var attachedMessage = msgGen.makeMessage({ body: { body: "I'm an attached email!" }, attachments: [ { body: textAttachment, filename: "inner attachment.txt", format: "" }, ], }); // create some messages that have various types of attachments messages = [ // no attachment {}, // text attachment { attachments: [{ body: textAttachment, filename: "ubik.txt", format: "" }], }, // binary attachment; filename has 9 "1"s, which should be just within the // limit for showing the original name { attachments: [ { body: binaryAttachment, contentType: "application/octet-stream", filename: "ubik-111111111.xxyyzz", format: "", }, ], }, // multiple attachments { attachments: [ { body: textAttachment, filename: "ubik.txt", format: "" }, { body: binaryAttachment, contentType: "application/octet-stream", filename: "ubik.xxyyzz", format: "", }, ], }, // attachment with a long name; the attachment bar should crop this { attachments: [ { body: textAttachment, filename: "this-is-a-file-with-an-extremely-long-name-" + "that-seems-to-go-on-forever-seriously-you-" + "would-not-believe-how-long-this-name-is-it-" + "surely-exceeds-the-maximum-filename-length-" + "for-most-filesystems.txt", format: "", }, ], }, // a message with a text attachment and an email attachment, which in turn // has its own text attachment { bodyPart: new SyntheticPartMultiMixed([ new SyntheticPartLeaf("I'm a message!"), new SyntheticPartLeaf(textAttachment, { filename: "outer attachment.txt", contentType: "text/plain", format: "", }), attachedMessage, ]), }, // evilly-named attachment; spaces should be collapsed and trimmed on the // ends { attachments: [ { body: textAttachment, contentType: "application/octet-stream", filename: " ubik .txt .evil ", sanitizedFilename: "ubik .txt .evil", format: "", }, ], }, // another evilly-named attachment; filename has 10 "_"s, which should be // just enough to trigger the sanitizer { attachments: [ { body: textAttachment, contentType: "application/octet-stream", filename: "ubik.txt__________.evil", sanitizedFilename: "ubik.txt_…_.evil", format: "", }, ], }, // No texdir change in the filename please. { attachments: [ { body: textAttachment, filename: "ABC\u202EE.txt.zip", sanitizedFilename: "ABC.E.txt.zip", }, ], }, ]; // Add another evilly-named attachment for Windows tests, to ensure that // trailing periods are stripped. if ("@mozilla.org/windows-registry-key;1" in Cc) { messages.push({ attachments: [ { body: textAttachment, contentType: "application/octet-stream", filename: "ubik.evil. . . . . . . . . ....", sanitizedFilename: "ubik.evil", format: "", }, ], }); } for (let i = 0; i < messages.length; i++) { await add_message_to_folder([folder], create_message(messages[i])); } }); /** * Set the pref to ensure that the attachments pane starts out (un)expanded * * @param expand true if the attachment pane should start out expanded, * false otherwise */ function ensure_starts_expanded(expand) { Services.prefs.setBoolPref( "mailnews.attachments.display.start_expanded", expand ); } add_task(async function test_attachment_view_collapsed() { await be_in_folder(folder); select_click_row(0); assert_selected_and_displayed(0); if ( !get_about_message().document.getElementById("attachmentView").collapsed ) { throw new Error("Attachment pane expanded when it shouldn't be!"); } }); add_task(async function test_attachment_view_expanded() { await be_in_folder(folder); for (let i = 1; i < messages.length; i++) { select_click_row(i); assert_selected_and_displayed(i); if ( get_about_message().document.getElementById("attachmentView").collapsed ) { throw new Error( "Attachment pane collapsed (on message #" + i + " when it shouldn't be!" ); } } }); add_task(async function test_attachment_name_sanitization() { await be_in_folder(folder); let aboutMessage = get_about_message(); let attachmentList = aboutMessage.document.getElementById("attachmentList"); for (let i = 0; i < messages.length; i++) { if ("attachments" in messages[i]) { select_click_row(i); assert_selected_and_displayed(i); let attachments = messages[i].attachments; if (messages[i].attachments.length == 1) { Assert.equal( aboutMessage.document.getElementById("attachmentName").value, attachments[0].sanitizedFilename || attachments[0].filename ); } for (let j = 0; j < attachments.length; j++) { Assert.equal( attachmentList.getItemAtIndex(j).getAttribute("name"), attachments[j].sanitizedFilename || attachments[j].filename ); } } } }); add_task(async function test_long_attachment_name() { await be_in_folder(folder); select_click_row(4); assert_selected_and_displayed(4); let aboutMessage = get_about_message(); let messagepaneBox = aboutMessage.document.getElementById("messagepanebox"); let attachmentBar = aboutMessage.document.getElementById("attachmentBar"); Assert.ok( messagepaneBox.getBoundingClientRect().width >= attachmentBar.getBoundingClientRect().width, "Attachment bar has expanded off the edge of the window!" ); }); /** * Make sure that, when opening attached messages, we only show the attachments * "beneath" the attached message (as opposed to all attachments for the root * message). */ add_task(async function test_attached_message_attachments() { await be_in_folder(folder); select_click_row(5); assert_selected_and_displayed(5); // Make sure we have the expected number of attachments in the root message: // an outer text attachment, an attached email, and an inner text attachment. let aboutMessage = get_about_message(); Assert.equal( aboutMessage.document.getElementById("attachmentList").itemCount, 3 ); // Open the attached email. let newWindowPromise = async_plan_for_new_window("mail:messageWindow"); aboutMessage.document .getElementById("attachmentList") .getItemAtIndex(1) .attachment.open(); let msgc = await newWindowPromise; wait_for_message_display_completion(msgc, true); // Make sure we have the expected number of attachments in the attached // message: just an inner text attachment. Assert.equal( msgc.window.document.getElementById("attachmentList").itemCount, 1 ); close_window(msgc); }).skip(); add_task(async function test_attachment_name_click() { await be_in_folder(folder); select_click_row(1); assert_selected_and_displayed(1); let aboutMessage = get_about_message(); let attachmentList = aboutMessage.document.getElementById("attachmentList"); Assert.ok( attachmentList.collapsed, "Attachment list should start out collapsed!" ); // Ensure the open dialog appears when clicking on the attachment name and // that the attachment list doesn't expand. plan_for_modal_dialog("unknownContentTypeWindow", function () {}); EventUtils.synthesizeMouseAtCenter( aboutMessage.document.getElementById("attachmentName"), { clickCount: 1 }, aboutMessage ); wait_for_modal_dialog("unknownContentTypeWindow"); Assert.ok( attachmentList.collapsed, "Attachment list should not expand when clicking on attachmentName!" ); }); /** * Test that right-clicking on a particular element opens the expected context * menu. * * @param elementId the id of the element to right click on * @param contextMenuId the id of the context menu that should appear */ async function subtest_attachment_right_click(elementId, contextMenuId) { let aboutMessage = get_about_message(); let element = aboutMessage.document.getElementById(elementId); let contextMenu = aboutMessage.document.getElementById(contextMenuId); let shownPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown"); EventUtils.synthesizeMouseAtCenter( element, { type: "contextmenu" }, aboutMessage ); await shownPromise; let hiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden"); contextMenu.hidePopup(); await hiddenPromise; await new Promise(resolve => requestAnimationFrame(resolve)); } add_task(async function test_attachment_right_click_single() { await be_in_folder(folder); select_click_row(1); assert_selected_and_displayed(1); await subtest_attachment_right_click( "attachmentIcon", "attachmentItemContext" ); await subtest_attachment_right_click( "attachmentCount", "attachmentItemContext" ); await subtest_attachment_right_click( "attachmentName", "attachmentItemContext" ); await subtest_attachment_right_click( "attachmentSize", "attachmentItemContext" ); await subtest_attachment_right_click( "attachmentToggle", "attachment-toolbar-context-menu" ); await subtest_attachment_right_click( "attachmentSaveAllSingle", "attachment-toolbar-context-menu" ); await subtest_attachment_right_click( "attachmentBar", "attachment-toolbar-context-menu" ); }); add_task(async function test_attachment_right_click_multiple() { await be_in_folder(folder); select_click_row(3); assert_selected_and_displayed(3); await subtest_attachment_right_click( "attachmentIcon", "attachmentListContext" ); await subtest_attachment_right_click( "attachmentCount", "attachmentListContext" ); await subtest_attachment_right_click( "attachmentSize", "attachmentListContext" ); await subtest_attachment_right_click( "attachmentToggle", "attachment-toolbar-context-menu" ); await subtest_attachment_right_click( "attachmentSaveAllMultiple", "attachment-toolbar-context-menu" ); await subtest_attachment_right_click( "attachmentBar", "attachment-toolbar-context-menu" ); }); /** * Test that clicking on various elements in the attachment bar toggles the * attachment list. * * @param elementId the id of the element to click */ function subtest_attachment_list_toggle(elementId) { let aboutMessage = get_about_message(); let attachmentList = aboutMessage.document.getElementById("attachmentList"); let element = aboutMessage.document.getElementById(elementId); EventUtils.synthesizeMouseAtCenter(element, { clickCount: 1 }, aboutMessage); Assert.ok( !attachmentList.collapsed, `Attachment list should be expanded after clicking ${elementId}!` ); assert_attachment_list_focused(); EventUtils.synthesizeMouseAtCenter(element, { clickCount: 1 }, aboutMessage); Assert.ok( attachmentList.collapsed, `Attachment list should be collapsed after clicking ${elementId} again!` ); assert_message_pane_focused(); } add_task(async function test_attachment_list_expansion() { await be_in_folder(folder); select_click_row(1); assert_selected_and_displayed(1); let aboutMessage = get_about_message(); Assert.ok( aboutMessage.document.getElementById("attachmentList").collapsed, "Attachment list should start out collapsed!" ); subtest_attachment_list_toggle("attachmentToggle"); subtest_attachment_list_toggle("attachmentIcon"); subtest_attachment_list_toggle("attachmentCount"); subtest_attachment_list_toggle("attachmentSize"); subtest_attachment_list_toggle("attachmentBar"); // Ensure that clicking the "Save All" button doesn't expand the attachment // list. let dm = aboutMessage.document.querySelector( "#attachmentSaveAllSingle .toolbarbutton-menubutton-dropmarker" ); EventUtils.synthesizeMouseAtCenter(dm, { clickCount: 1 }, aboutMessage); Assert.ok( aboutMessage.document.getElementById("attachmentList").collapsed, "Attachment list should be collapsed after clicking save button!" ); }).skip(); add_task(async function test_attachment_list_starts_expanded() { ensure_starts_expanded(true); await be_in_folder(folder); select_click_row(2); assert_selected_and_displayed(2); Assert.ok( !get_about_message().document.getElementById("attachmentList").collapsed, "Attachment list should start out expanded!" ); }); add_task(async function test_selected_attachments_are_cleared() { ensure_starts_expanded(false); await be_in_folder(folder); // First, select the message with two attachments. select_click_row(3); // Expand the attachment list. let aboutMessage = get_about_message(); EventUtils.synthesizeMouseAtCenter( aboutMessage.document.getElementById("attachmentToggle"), { clickCount: 1 }, aboutMessage ); // Select both the attachments. let attachmentList = aboutMessage.document.getElementById("attachmentList"); Assert.equal( attachmentList.selectedItems.length, 1, "On first load the first item should be selected" ); // We can just click on the first element, but the second one needs a // ctrl-click (or cmd-click for those Mac-heads among us). EventUtils.synthesizeMouseAtCenter( attachmentList.children[0], { clickCount: 1 }, aboutMessage ); EventUtils.synthesizeMouse( attachmentList.children[1], 5, 5, { accelKey: true }, aboutMessage ); Assert.equal( attachmentList.selectedItems.length, 2, "We had the wrong number of selected items after selecting some!" ); // Switch to the message with one attachment, and make sure there are no // selected attachments. select_click_row(2); // Expand the attachment list again. EventUtils.synthesizeMouseAtCenter( aboutMessage.document.getElementById("attachmentToggle"), { clickCount: 1 }, aboutMessage ); Assert.equal( attachmentList.selectedItems.length, 1, "After loading a new message the first item should be selected" ); }); add_task(async function test_select_all_attachments_key() { await be_in_folder(folder); // First, select the message with two attachments. select_none(); select_click_row(3); // Expand the attachment list. let aboutMessage = get_about_message(); EventUtils.synthesizeMouseAtCenter( aboutMessage.document.getElementById("attachmentToggle"), { clickCount: 1 }, aboutMessage ); let attachmentList = aboutMessage.document.getElementById("attachmentList"); attachmentList.focus(); EventUtils.synthesizeKey("a", { accelKey: true }, aboutMessage); Assert.equal( attachmentList.selectedItems.length, 2, "Should have selected all attachments!" ); }); add_task(async function test_delete_attachment_key() { await be_in_folder(folder); // First, select the message with two attachments. select_none(); select_click_row(3); // Expand the attachment list. assert_selected_and_displayed(3); let aboutMessage = get_about_message(); if (aboutMessage.document.getElementById("attachmentList").collapsed) { EventUtils.synthesizeMouseAtCenter( aboutMessage.document.getElementById("attachmentToggle"), { clickCount: 1 }, aboutMessage ); } let firstAttachment = aboutMessage.document.getElementById("attachmentList").firstElementChild; EventUtils.synthesizeMouseAtCenter( firstAttachment, { clickCount: 1 }, aboutMessage ); // Try deleting with the delete key let dialogPromise = BrowserTestUtils.promiseAlertDialog("cancel"); firstAttachment.focus(); EventUtils.synthesizeKey("VK_DELETE", {}, aboutMessage); await dialogPromise; // Try deleting with the shift-delete key combo. dialogPromise = BrowserTestUtils.promiseAlertDialog("cancel"); firstAttachment.focus(); EventUtils.synthesizeKey("VK_DELETE", { shiftKey: true }, aboutMessage); await dialogPromise; }).skip(); add_task(async function test_attachments_compose_menu() { await be_in_folder(folder); // First, select the message with two attachments. select_none(); select_click_row(3); let cwc = open_compose_with_forward(); let attachment = cwc.window.document.getElementById("attachmentBucket"); // On Linux and OSX, focus events don't seem to be sent to child elements properly if // the parent window is not focused. This causes some random oranges for us. // We use the force_focus function to "cheat" a bit, and trigger the function // that focusing normally would fire. We do normal focusing for Windows. function force_focus(aId) { let element = cwc.window.document.getElementById(aId); element.focus(); if (["linux", "macosx"].includes(AppConstants.platform)) { // First, call the window's default controller's function. cwc.window.defaultController.isCommandEnabled("cmd_delete"); // Walk up the DOM tree and call isCommandEnabled on the first controller // that supports "cmd_delete". while (element != cwc.window.document) { // NOTE: html elements (like body) don't have controllers. let numControllers = element.controllers?.getControllerCount() || 0; for (let i = 0; numControllers; i++) { let currController = element.controllers.getControllerAt(i); if (currController.supportsCommand("cmd_delete")) { currController.isCommandEnabled("cmd_delete"); return; } } element = element.parentNode; } } } // Click on a portion of the attachmentBucket to focus on it. The last // attachment should be selected since we don't handle any action on an empty // bucket, and we always ensure that the last attached file is visible. force_focus("attachmentBucket"); Assert.equal( "Remove Attachment", cwc.window.document.getElementById("cmd_delete").getAttribute("label"), "attachmentBucket with last attachment is focused!" ); // We opened a message with 2 attachments, so index 1 should be focused. Assert.equal(attachment.selectedIndex, 1, "Last attachment is focused!"); // Select 1 attachment, and // focus the subject to see the label change and to execute isCommandEnabled attachment.selectedIndex = 0; force_focus("msgSubject"); Assert.equal( "Delete", cwc.window.document.getElementById("cmd_delete").getAttribute("label"), "attachmentBucket is not focused!" ); // Focus back to the attachmentBucket force_focus("attachmentBucket"); Assert.equal( "Remove Attachment", cwc.window.document.getElementById("cmd_delete").getAttribute("label"), "Only 1 attachment is selected!" ); // Select multiple attachments, and focus the identity for the same purpose attachment.selectAll(); force_focus("msgIdentity"); Assert.equal( "Delete", cwc.window.document.getElementById("cmd_delete").getAttribute("label"), "attachmentBucket is not focused!" ); // Focus back to the attachmentBucket force_focus("attachmentBucket"); Assert.equal( "Remove Attachments", cwc.window.document.getElementById("cmd_delete").getAttribute("label"), "Multiple attachments are selected!" ); close_compose_window(cwc); }); add_task(async function test_delete_from_toolbar() { await be_in_folder(folder); // First, select the message with two attachments. select_none(); select_click_row(3); // Expand the attachment list. assert_selected_and_displayed(3); let aboutMessage = get_about_message(); if (aboutMessage.document.getElementById("attachmentList").collapsed) { EventUtils.synthesizeMouseAtCenter( aboutMessage.document.getElementById("attachmentToggle"), { clickCount: 1 }, aboutMessage ); } let firstAttachment = aboutMessage.document.getElementById("attachmentList").firstElementChild; EventUtils.synthesizeMouseAtCenter( firstAttachment, { clickCount: 1 }, aboutMessage ); // Make sure clicking the "Delete" toolbar button with an attachment focused // deletes the *message*. plan_to_wait_for_folder_events("DeleteOrMoveMsgCompleted"); EventUtils.synthesizeMouseAtCenter( aboutMessage.document.getElementById("hdrTrashButton"), { clickCount: 1 }, aboutMessage ); wait_for_folder_events(); }).skip(); registerCleanupFunction(() => { // Remove created folders. folder.deleteSelf(null); });