diff options
Diffstat (limited to '')
10 files changed, 2729 insertions, 0 deletions
diff --git a/comm/mail/test/browser/message-header/browser.ini b/comm/mail/test/browser/message-header/browser.ini new file mode 100644 index 0000000000..786f724775 --- /dev/null +++ b/comm/mail/test/browser/message-header/browser.ini @@ -0,0 +1,18 @@ +[DEFAULT] +head = head.js +prefs = + mail.provider.suppress_dialog_on_startup=true + mail.spotlight.firstRunDone=true + mail.winsearch.firstRunDone=true + mailnews.start_page.override_url=about:blank + mailnews.start_page.url=about:blank + datareporting.policy.dataSubmissionPolicyBypassNotification=true +subsuite = thunderbird +support-files = data/** + +[browser_messageHeader.js] +[browser_messageHeaderCustomize.js] +[browser_phishingBar.js] +[browser_replyIdentity.js] +[browser_replyToListFromAddressSelection.js] +[browser_returnReceipt.js] diff --git a/comm/mail/test/browser/message-header/browser_messageHeader.js b/comm/mail/test/browser/message-header/browser_messageHeader.js new file mode 100644 index 0000000000..f198a36f3f --- /dev/null +++ b/comm/mail/test/browser/message-header/browser_messageHeader.js @@ -0,0 +1,1237 @@ +/* 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/. */ + +/** + * Test functionality in the message header, + */ + +"use strict"; + +var { + create_address_book, + create_mailing_list, + ensure_no_card_exists, + get_cards_in_all_address_books_for_email, + get_mailing_list_from_address_book, +} = ChromeUtils.import( + "resource://testing-common/mozmill/AddressBookHelpers.jsm" +); +var { wait_for_content_tab_load } = ChromeUtils.import( + "resource://testing-common/mozmill/ContentTabHelpers.jsm" +); +var { + add_message_to_folder, + assert_selected_and_displayed, + be_in_folder, + close_popup, + create_folder, + create_message, + gDefaultWindowHeight, + get_smart_folder_named, + get_about_3pane, + get_about_message, + inboxFolder, + mc, + msgGen, + restore_default_window_size, + select_click_row, + select_none, + wait_for_message_display_completion, + wait_for_popup_to_open, +} = ChromeUtils.import( + "resource://testing-common/mozmill/FolderDisplayHelpers.jsm" +); +var { element_visible_recursive } = ChromeUtils.import( + "resource://testing-common/mozmill/DOMHelpers.jsm" +); +var { resize_to } = ChromeUtils.import( + "resource://testing-common/mozmill/WindowHelpers.jsm" +); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +let about3Pane = get_about_3pane(); +let aboutMessage = get_about_message(); + +const LINES_PREF = "mailnews.headers.show_n_lines_before_more"; + +// Used to get the accessible object for a DOM node. +var gAccService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService +); + +var folder; +var folderMore; +var gInterestingMessage; + +add_setup(async function () { + folder = await create_folder("MessageWindowA"); + folderMore = await create_folder("MesageHeaderMoreButton"); + + // Create a message that has the interesting headers that commonly shows up in + // the message header pane for testing. + gInterestingMessage = create_message({ + cc: msgGen.makeNamesAndAddresses(20), + subject: + "This is a really, really, really, really, really, really, really, really, long subject.", + clobberHeaders: { + Newsgroups: "alt.test", + "Reply-To": "J. Doe <j.doe@momo.invalid>", + "Content-Base": "https://example.com/", + Bcc: "Richard Roe <richard.roe@momo.invalid>", + }, + }); + + await add_message_to_folder([folder], gInterestingMessage); + + // Create a message that has multiple to and cc addresses. + let msgMore1 = create_message({ + to: msgGen.makeNamesAndAddresses(40), + cc: msgGen.makeNamesAndAddresses(40), + }); + await add_message_to_folder([folderMore], msgMore1); + + // Create a message that has multiple to and cc addresses. + let msgMore2 = create_message({ + to: msgGen.makeNamesAndAddresses(20), + cc: msgGen.makeNamesAndAddresses(20), + }); + await add_message_to_folder([folderMore], msgMore2); + + // Create a regular message with one recipient. + let msg = create_message(); + await add_message_to_folder([folder], msg); + + // Some of these tests critically depends on the window width, collapse + // everything that might be in the way. + mc.window.document.getElementById( + "tabmail" + ).currentTabInfo.folderPaneVisible = false; + + // Disable animations on the panel, so that we don't have to deal with + // async openings. The panel is lazy-loaded, so it needs to be referenced + // this way rather than finding it in the DOM. + aboutMessage.editContactInlineUI.panel.setAttribute("animate", false); + await ensure_table_view(); + + registerCleanupFunction(async () => { + await ensure_cards_view(); + // Delete created folder. + folder.deleteSelf(null); + folderMore.deleteSelf(null); + + // Restore animation to the contact panel. + aboutMessage.document + .getElementById("editContactPanel") + .removeAttribute("animate"); + }); +}); + +/** + * Helper function that takes an array of header recipients elements and + * returns the last one in the list that is not hidden. Returns null if no + * such element exists. + * + * @param {HTMLOListElement} recipientsList - The list element containing all + * recipient addresses. + */ +function get_last_visible_address(recipientsList) { + let last = recipientsList.childNodes[recipientsList.childNodes.length - 1]; + // Avoid returning the "more" button. + if (last.classList.contains("show-more-recipients")) { + return recipientsList.childNodes[recipientsList.childNodes.length - 2]; + } + return last; +} + +add_task(async function test_add_tag_with_really_long_label() { + await be_in_folder(folder); + await ensure_table_view(); + + // Select the first message, which will display it. + let curMessage = select_click_row(0); + + assert_selected_and_displayed(mc, curMessage); + + let topLabel = aboutMessage.document.getElementById("expandedfromLabel"); + let bottomLabel = aboutMessage.document.getElementById( + "expandedsubjectLabel" + ); + if (topLabel.clientWidth != bottomLabel.clientWidth) { + throw new Error( + `Header columns have different widths! ${topLabel.clientWidth} != ${bottomLabel.clientWidth}` + ); + } + let defaultWidth = topLabel.clientWidth; + + // Make the tags label really long. + let tagsLabel = aboutMessage.document.getElementById("expandedtagsLabel"); + let oldTagsValue = tagsLabel.value; + tagsLabel.value = "taaaaaaaaaaaaaaaaaags"; + if (topLabel.clientWidth != bottomLabel.clientWidth) { + tagsLabel.value = oldTagsValue; + throw new Error( + `Header columns have different widths! ${topLabel.clientWidth} != ${bottomLabel.clientWidth}` + ); + } + + if (topLabel.clientWidth != defaultWidth) { + tagsLabel.value = oldTagsValue; + throw new Error( + `Header columns changed width! ${topLabel.clientWidth} != ${defaultWidth}` + ); + } + + let fromRow = aboutMessage.document.getElementById("expandedfromRow"); + // Add the first tag, and make sure that the label are the same length. + fromRow.focus(); + EventUtils.synthesizeKey("1", {}, aboutMessage); + if (topLabel.clientWidth != bottomLabel.clientWidth) { + tagsLabel.value = oldTagsValue; + throw new Error( + `Header columns have different widths! ${topLabel.clientWidth} != ${bottomLabel.clientWidth}` + ); + } + + if (topLabel.clientWidth == defaultWidth) { + tagsLabel.value = oldTagsValue; + throw new Error( + `Header columns didn't change width! ${topLabel.clientWidth} == ${defaultWidth}` + ); + } + + // Remove the tag and put it back so that the a11y label gets regenerated + // with the normal value rather than "taaaaaaaags". + tagsLabel.value = oldTagsValue; + fromRow.focus(); + EventUtils.synthesizeKey("1", {}, aboutMessage); + fromRow.focus(); + EventUtils.synthesizeKey("1", {}, aboutMessage); +}).skip(); + +/** + * Data and methods for a space. + * + * @typedef {object} HeaderInfo + * @property {string} name - Used for pretty-printing in exceptions. + * @property {Function} element - A callback returning the DOM + * element with the data. + * @property {Function} expectedName - A callback returning the expected value + * of the accessible name of the DOM element. + */ +/** + * List all header rows that we want to test. + * + * @type {HeaderInfo[]} + */ +const headersToTest = [ + { + name: "Subject", + element() { + return aboutMessage.document.getElementById("expandedsubjectBox"); + }, + expectedName(element) { + return `${ + aboutMessage.document.getElementById("expandedsubjectLabel").value + }: ${element.value.textContent}`; + }, + }, + { + name: "Content-Base", + element() { + return aboutMessage.document.getElementById("expandedcontent-baseBox"); + }, + expectedName(element) { + return `${ + aboutMessage.document.getElementById("expandedcontent-baseLabel").value + }: ${element.value.textContent}`; + }, + }, +]; + +/** + * Use the information from HeaderInfo to verify that screen readers will do the + * right thing with the given message header. + * + * @param {HeaderInfo} header - The HeaderInfo data type object. + */ +async function verify_header_a11y(header) { + let element = header.element(); + Assert.notEqual( + element, + null, + `element not found for header '${header.name}'` + ); + + let headerAccessible; + await TestUtils.waitForCondition( + () => (headerAccessible = gAccService.getAccessibleFor(element)) != null, + `didn't find accessible element for header '${header.name}'` + ); + + let expectedName = header.expectedName(element); + Assert.equal( + headerAccessible.name, + expectedName, + `headerAccessible.name for ${header.name} ` + + `was '${headerAccessible.name}'; expected '${expectedName}'` + ); +} + +/** + * Test the accessibility attributes of the various message headers. + * + * INFO: The gInterestingMessage has no tags until after + * test_add_tag_with_really_long_label, so ensure it runs after that one. + */ +add_task(async function test_a11y_attrs() { + await be_in_folder(folder); + // Convert the SyntheticMessage gInterestingMessage into an actual nsIMsgDBHdr + // XPCOM message. + let hdr = folder.msgDatabase.getMsgHdrForMessageID( + gInterestingMessage.messageId + ); + // Select and open the interesting message. + let curMessage = select_click_row( + about3Pane.gDBView.findIndexOfMsgHdr(hdr, false) + ); + // Make sure it loads. + assert_selected_and_displayed(mc, curMessage); + // Test all the headers with this message. + headersToTest.forEach(verify_header_a11y); +}); + +/** + * Test the keyboard accessibility of the toolbarbuttons on the message header. + */ +add_task(async function enter_msg_hdr_toolbar() { + await be_in_folder(folder); + // Convert the SyntheticMessage gInterestingMessage into an actual nsIMsgDBHdr + // XPCOM message. + let hdr = folder.msgDatabase.getMsgHdrForMessageID( + gInterestingMessage.messageId + ); + // Select and open the interesting message. + let curMessage = select_click_row( + about3Pane.gDBView.findIndexOfMsgHdr(hdr, false) + ); + // Make sure it loads. + assert_selected_and_displayed(mc, curMessage); + + const BUTTONS_SELECTOR = `toolbarbutton:not([hidden="true"],[is="toolbarbutton-menu-button"]), toolbaritem[id="hdrSmartReplyButton"]>toolbarbutton:not([hidden="true"])>dropmarker, button:not([hidden])`; + let headerToolbar = aboutMessage.document.getElementById( + "header-view-toolbar" + ); + let headerButtons = headerToolbar.querySelectorAll(BUTTONS_SELECTOR); + + // Create an array of all the menu popups on message header. + let msgHdrMenupopups = headerToolbar.querySelectorAll(".no-icon-menupopup"); + let menupopupToOpen, msgHdrActiveElement; + + // Press tab while on the message selected. + EventUtils.synthesizeKey("KEY_Tab", {}, about3Pane); + Assert.equal( + headerButtons[0].id, + aboutMessage.document.activeElement.id, + "focused on first msgHdr toolbar button" + ); + + // Simulate the Arrow Right keypress to make sure the correct button gets the + // focus. + for (let i = 1; i < headerButtons.length; i++) { + let previousElement = document.activeElement; + EventUtils.synthesizeKey("KEY_ArrowRight", {}, about3Pane); + Assert.equal( + aboutMessage.document.activeElement.id, + headerButtons[i].id, + "The next button is focused" + ); + Assert.ok( + aboutMessage.document.activeElement.tabIndex == 0 && + previousElement.tabIndex == -1, + "The roving tab index was updated" + ); + msgHdrActiveElement = aboutMessage.document.activeElement; + + // Simulate Enter and Space keypress events to ensure the menus in the + // message header buttons area are keyboard accessible. + if ( + msgHdrActiveElement.hasAttribute("type") && + msgHdrActiveElement.getAttribute("type") == "menu" + ) { + let parentID = msgHdrActiveElement.parentElement.id; + for (let menupopup of msgHdrMenupopups) { + if ( + menupopup.id.replace("Dropdown", "") == + parentID.replace("Button", "") || + menupopup.id.replace("Popup", "") == + msgHdrActiveElement.id.replace("Button", "") + ) { + menupopupToOpen = menupopup; + } + } + + let menupopupOpenEnterPromise = BrowserTestUtils.waitForEvent( + menupopupToOpen, + "popupshown" + ); + EventUtils.synthesizeKey("KEY_Enter", {}, about3Pane); + await menupopupOpenEnterPromise; + + let menupopupClosePromise = BrowserTestUtils.waitForEvent( + menupopupToOpen, + "popuphidden" + ); + EventUtils.synthesizeKey("KEY_Escape", {}, about3Pane); + await menupopupClosePromise; + + Assert.equal( + msgHdrActiveElement.id, + headerButtons[i].id, + "The correct button is focused" + ); + + let menupopupOpenSpacePromise = BrowserTestUtils.waitForEvent( + menupopupToOpen, + "popupshown" + ); + // Simulate Space keypress. + EventUtils.synthesizeKey(" ", {}, about3Pane); + await menupopupOpenSpacePromise; + + EventUtils.synthesizeKey("KEY_Escape", {}, about3Pane); + await menupopupClosePromise; + + Assert.equal( + msgHdrActiveElement.id, + headerButtons[i].id, + "The correct button is focused after opening and closing the menupopup" + ); + } + } + + // Simulate the Arrow Left keypress to make sure the correct button gets the + // focus. + for (let i = headerButtons.length - 2; i > -1; i--) { + let previousElement = document.activeElement; + EventUtils.synthesizeKey("KEY_ArrowLeft", {}, about3Pane); + Assert.equal( + aboutMessage.document.activeElement.id, + headerButtons[i].id, + "The previous button is focused" + ); + Assert.ok( + aboutMessage.document.activeElement.tabIndex == 0 && + previousElement.tabIndex == -1, + "The roving tab index was updated" + ); + } + EventUtils.synthesizeKey("KEY_Tab", {}, about3Pane); + Assert.equal( + aboutMessage.document.activeElement.id, + "fromRecipient0", + "The sender is now focused" + ); +}).__skipMe = AppConstants.platform == "macosx"; + +// Full keyboard navigation on OSX only works if Full Keyboard Access setting is +// set to All Control in System Keyboard Preferences. This also works with the +// setting, Keyboard > Keyboard navigation, in addition to +// Accessibility > Keyboard > Full Keyboard Access. + +add_task(function test_more_button_with_many_recipients() { + // Start on the interesting message. + let curMessage = select_click_row(0); + + // Make sure it loads. + wait_for_message_display_completion(mc); + assert_selected_and_displayed(mc, curMessage); + + // Click on the "more" button. + EventUtils.synthesizeMouseAtCenter( + aboutMessage.document.getElementById("expandedccBox").moreButton, + {}, + aboutMessage + ); + + let msgHeader = aboutMessage.document.getElementById("messageHeader"); + // Check that the message header can scroll to fit all recipients. + Assert.ok( + msgHeader.classList.contains("scrollable"), + "The message header is scrollable" + ); + + // Switch to the boring message, to force the more button to collapse. + curMessage = select_click_row(1); + + // Make sure it loads. + wait_for_message_display_completion(mc); + assert_selected_and_displayed(mc, curMessage); + + // Check that the message header is not scrollable anymore + Assert.notEqual( + msgHeader.classList.contains("scrollable"), + "The message header is not scrollable" + ); +}); + +/** + * Test that clicking the add to address book button updates the UI properly. + * + * @param {HTMLOListElement} recipientsList + */ +function subtest_more_widget_ab_button_click(recipientsList) { + let recipient = get_last_visible_address(recipientsList); + ensure_no_card_exists(recipient.emailAddress); + + // Scroll to the bottom first so the address is in view. + let view = aboutMessage.document.getElementById("messageHeader"); + view.scrollTop = view.scrollHeight - view.clientHeight; + + EventUtils.synthesizeMouseAtCenter(recipient.abIndicator, {}, aboutMessage); + + Assert.ok( + recipient.abIndicator.classList.contains("in-address-book"), + "The recipient was added to the Address Book" + ); +} + +/** + * Test that we can open up the inline contact editor when we + * click on the address book button. + */ +add_task(async function test_clicking_ab_button_opens_inline_contact_editor() { + // Make sure we're in the right folder. + await be_in_folder(folder); + // Add a new message. + let msg = create_message(); + await add_message_to_folder([folder], msg); + // Open the latest message. + select_click_row(-1); + wait_for_message_display_completion(mc); + + // Ensure that the inline contact editing panel is not open + let contactPanel = aboutMessage.document.getElementById("editContactPanel"); + Assert.notEqual(contactPanel.state, "open"); + + let recipientsList = + aboutMessage.document.getElementById("expandedtoBox").recipientsList; + subtest_more_widget_ab_button_click(recipientsList); + + // Ok, if we're here, then the star has been clicked, and + // the contact has been added to our AB. + let recipient = get_last_visible_address(recipientsList); + + let panelOpened = TestUtils.waitForCondition( + () => contactPanel.state == "open", + "The contactPanel was opened" + ); + // Click on the star, and ensure that the inline contact editing panel opens. + EventUtils.synthesizeMouseAtCenter(recipient.abIndicator, {}, aboutMessage); + await panelOpened; + + EventUtils.synthesizeMouseAtCenter( + aboutMessage.document.getElementById("editContactPanelEditDetailsButton"), + {}, + aboutMessage + ); + wait_for_content_tab_load(undefined, "about:addressbook"); + // TODO check the card. + mc.window.document.getElementById("tabmail").closeTab(); +}); + +/** + * Test that clicking references context menu works properly. + */ +add_task(async function test_msg_id_context_menu() { + Services.prefs.setBoolPref("mailnews.headers.showReferences", true); + + // Add a new message. + let msg = create_message({ + clobberHeaders: { + References: + "<4880C986@example.com> <4880CAB2@example.com> <4880CC76@example.com>", + }, + }); + await add_message_to_folder([folder], msg); + await be_in_folder(folder); + + // Open the latest message. + select_click_row(-1); + + // Right click to show the context menu. + EventUtils.synthesizeMouseAtCenter( + aboutMessage.document.querySelector( + "#expandedreferencesBox .header-message-id" + ), + { type: "contextmenu" }, + aboutMessage + ); + await wait_for_popup_to_open( + aboutMessage.document.getElementById("messageIdContext") + ); + + // Ensure Open Message For ID is shown and that Open Browser With Message-ID + // isn't shown. + Assert.ok( + !aboutMessage.document.getElementById( + "messageIdContext-openMessageForMsgId" + ).hidden, + "The menu item is hidden" + ); + Assert.ok( + aboutMessage.document.getElementById( + "messageIdContext-openBrowserWithMsgId" + ).hidden, + "The menu item is visible" + ); + + await close_popup( + mc, + aboutMessage.document.getElementById("messageIdContext") + ); + + // Reset the preferences. + Services.prefs.setBoolPref("mailnews.headers.showReferences", false); +}); + +/** + * Test that if a contact belongs to a mailing list within their address book, + * then the inline contact editor will not allow the user to change what address + * book the contact belongs to. The editor should also show a message to explain + * why the contact cannot be moved. + */ +add_task( + async function test_address_book_switch_disabled_on_contact_in_mailing_list() { + const MAILING_LIST_DIRNAME = "Some Mailing List"; + const ADDRESS_BOOK_NAME = "Some Address Book"; + // Add a new message. + let msg = create_message(); + await add_message_to_folder([folder], msg); + + // Make sure we're in the right folder. + await be_in_folder(folder); + + // Open the latest message. + select_click_row(-1); + + // Ensure that the inline contact editing panel is not open + let contactPanel = aboutMessage.document.getElementById("editContactPanel"); + Assert.notEqual(contactPanel.state, "open"); + + let recipientsList = + aboutMessage.document.getElementById("expandedtoBox").recipientsList; + subtest_more_widget_ab_button_click(recipientsList); + + // Ok, if we're here, then the star has been clicked, and + // the contact has been added to our AB. + let recipient = get_last_visible_address(recipientsList); + + let panelOpened = TestUtils.waitForCondition( + () => contactPanel.state == "open", + "The contactPanel was opened" + ); + // Click on the address book button, and ensure that the inline contact + // editing panel opens. + EventUtils.synthesizeMouseAtCenter(recipient.abIndicator, {}, aboutMessage); + await panelOpened; + + let abDrop = aboutMessage.document.getElementById( + "editContactAddressBookList" + ); + // Ensure that the address book dropdown is not disabled + Assert.ok(!abDrop.disabled); + + let warningMsg = aboutMessage.document.getElementById( + "contactMoveDisabledText" + ); + // We should not be displaying any warning + Assert.ok(warningMsg.hidden); + + // Now close the popup. + contactPanel.hidePopup(); + + // For the contact that was added, create a mailing list in the address book + // it resides in, and then add that contact to the mailing list. + let cards = get_cards_in_all_address_books_for_email( + recipient.emailAddress + ); + + // There should be only one copy of this email address in the address books. + Assert.equal(cards.length, 1); + + // Remove the card from any of the address books.s + ensure_no_card_exists(recipient.emailAddress); + + // Add the card to a new address book, and insert it into a mailing list + // under that address book. + let ab = create_address_book(ADDRESS_BOOK_NAME); + ab.dropCard(cards[0], false); + let ml = create_mailing_list(MAILING_LIST_DIRNAME); + ab.addMailList(ml); + + // Now we have to retrieve the mailing list from the address book, in order + // for us to add and delete cards from it. + ml = get_mailing_list_from_address_book(ab, MAILING_LIST_DIRNAME); + ml.addCard(cards[0]); + + // Click on the address book button, and ensure that the inline contact + // editing panel opens. + EventUtils.synthesizeMouseAtCenter(recipient.abIndicator, {}, aboutMessage); + await panelOpened; + + // The dropdown should be disabled now + Assert.ok(abDrop.disabled); + // We should be displaying a warning + Assert.ok(!warningMsg.hidden); + + contactPanel.hidePopup(); + + // And if we remove the contact from the mailing list, the warning should be + // gone and the address book switching menu re-enabled. + ml.deleteCards([cards[0]]); + + // Click on the address book button, and ensure that the inline contact + // editing panel opens. + EventUtils.synthesizeMouseAtCenter(recipient.abIndicator, {}, aboutMessage); + await panelOpened; + + // Ensure that the address book dropdown is not disabled + Assert.ok(!abDrop.disabled); + // We should not be displaying any warning + Assert.ok(warningMsg.hidden); + + contactPanel.hidePopup(); + } +); + +/** + * Test that clicking the adding an address node adds it to the address book. + */ +add_task(async function test_add_contact_from_context_menu() { + let popup = aboutMessage.document.getElementById("emailAddressPopup"); + let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + // Click the contact to show the emailAddressPopup popup menu. + let recipient = aboutMessage.document.querySelector( + "#expandedfromBox .header-recipient" + ); + EventUtils.synthesizeMouseAtCenter(recipient, {}, aboutMessage); + await popupShown; + + const addToAddressBookItem = aboutMessage.document.getElementById( + "addToAddressBookItem" + ); + Assert.ok(!addToAddressBookItem.hidden, "addToAddressBookItem is not hidden"); + + const editContactItem = + aboutMessage.document.getElementById("editContactItem"); + Assert.ok(editContactItem.hidden, "editContactItem is hidden"); + + let recipientAdded = TestUtils.waitForCondition( + () => recipient.abIndicator.classList.contains("in-address-book"), + "The recipient was added to the address book" + ); + + // Click the Add to Address Book context menu entry. + // NOTE: Use activateItem because macOS uses native context menus. + popup.activateItem(addToAddressBookItem); + // (for reasons unknown, the pop-up does not close itself) + await close_popup(mc, popup); + await recipientAdded; + + // NOTE: We need to redefine these selectors otherwise the popup will not + // properly close for some reason. + let popup2 = aboutMessage.document.getElementById("emailAddressPopup"); + let popupShown2 = BrowserTestUtils.waitForEvent(popup, "popupshown"); + + // Now click the contact again, the context menu should now show the Edit + // Contact menu instead. + EventUtils.synthesizeMouseAtCenter(recipient, {}, aboutMessage); + await popupShown2; + // (for reasons unknown, the pop-up does not close itself) + await close_popup(mc, popup2); + + Assert.ok(addToAddressBookItem.hidden, "addToAddressBookItem is hidden"); + Assert.ok(!editContactItem.hidden, "editContactItem is not hidden"); +}); + +add_task(async function test_that_msg_without_date_clears_previous_headers() { + await be_in_folder(folder); + + // Create a message with a descriptive subject. + let msg = create_message({ subject: "this is without date" }); + + // Ensure that this message doesn't have a Date header. + delete msg.headers.Date; + + // Sdd the message to the end of the folder. + await add_message_to_folder([folder], msg); + + // Not the first anymore. The timestamp is that of "NOW". + // Select and open the LAST message. + let curMessage = select_click_row(-1); + + // Make sure it loads. + wait_for_message_display_completion(mc); + assert_selected_and_displayed(mc, curMessage); + + // Since we didn't give create_message an argument that would create a + // Newsgroups header, the newsgroups <row> element should be collapsed. + // However, since the previously displayed message _did_ have such a header, + // certain bugs in the display of this header could cause the collapse + // never to have happened. + Assert.ok( + aboutMessage.document.getElementById("expandednewsgroupsRow").hidden, + "The Newsgroups header row is hidden." + ); +}); + +/** + * Get the number of lines in one of the multi-recipient-row fields. + * + * @param {HTMLOListElement} node - The recipients container of a header row. + * @returns {int} - The number of rows. + */ +function help_get_num_lines(node) { + let style = getComputedStyle(node.firstElementChild); + return Math.round( + parseFloat(getComputedStyle(node).height) / + parseFloat(style.height + style.paddingTop + style.paddingBottom) + ); +} + +/** + * Test that the "more" button displays when it should. + * + * @param {HTMLOListElement} node - The recipients container of a header row. + * @param {boolean} [showAll=false] - If we're currently showing all the + * recipients. + */ +async function subtest_more_widget_display(node, showAll = false) { + // Test that the `To` element doesn't have more than max lines. + let numLines = help_get_num_lines(node); + // Get the max line pref. + let maxLines = Services.prefs.getIntPref(LINES_PREF); + + if (showAll) { + await BrowserTestUtils.waitForCondition( + () => numLines > maxLines, + `Currently visible lines are more than the number of max lines. ${numLines} > ${maxLines}` + ); + await BrowserTestUtils.waitForCondition( + () => + !aboutMessage.document + .getElementById("expandedtoBox") + .querySelector(".show-more-recipients"), + "The `more` button doesn't exist." + ); + } else { + await BrowserTestUtils.waitForCondition( + () => numLines <= maxLines, + `Currently visible lines are fewer than the number of max lines. ${numLines} <= ${maxLines}` + ); + // Test that we've got a "more" button and that it's visible. + await BrowserTestUtils.waitForCondition( + () => + !aboutMessage.document.getElementById("expandedtoBox").moreButton + .hidden, + "The `more` button is visible." + ); + } +} + +/** + * Test that activating the "more" button displays all the addresses. + * + * @param {HTMLOListElement} node - The recipients container of a header row. + */ +function subtest_more_widget_activate(node) { + let oldNumLines = help_get_num_lines(node); + + let moreButton = node.querySelector(".show-more-recipients"); + Assert.ok(moreButton, "The more button should exist"); + moreButton.focus(); + // Activate the "more" button. + EventUtils.synthesizeKey("KEY_Enter", {}, aboutMessage); + + // Make sure that the "more" button was removed when showing all addresses. + Assert.ok( + !node.querySelector(".show-more-recipients"), + "The more button should not exist anymore." + ); + + // Test that we actually have more lines than we did before! + let newNumLines = help_get_num_lines(node); + Assert.greater( + newNumLines, + oldNumLines, + "Number of address lines present increases after more click" + ); +} + +/** + * Test the behavior of the "more" button. + */ +add_task(async function test_view_more_button() { + // Generate message with 35 recipients to guarantee overflow. + await be_in_folder(folder); + let msg = create_message({ + toCount: 35, + subject: "Many To addresses to test_more_widget", + }); + + // Add the message to the end of the folder. + await add_message_to_folder([folder], msg); + + // Select and open the injected message. + // It is at the second last message in the display list. + let curMessage = select_click_row(-2); + // FIXME: Switch between a couple of messages to allow the UI to properly + // refresh and fetch the proper recipients row width in order to avoid an + // unexpected recipients wrapping. This happens because the width calculation + // happens before the message header layout is fully generated. + let prevMessage = select_click_row(-3); + wait_for_message_display_completion(mc); + assert_selected_and_displayed(mc, prevMessage); + + curMessage = select_click_row(-2); + + // Make sure it loads. + wait_for_message_display_completion(mc); + assert_selected_and_displayed(mc, curMessage); + + // Get the sender address. + let node = + aboutMessage.document.getElementById("expandedtoBox").recipientsList; + await subtest_more_widget_display(node); + subtest_more_widget_activate(node); +}); + +/** + * Test the focus behavior when activating the more button. + */ +add_task(async function test_view_more_button_focus() { + // Generate message with 35 recipients to guarantee overflow. + await be_in_folder(folder); + let msg = create_message({ + toCount: 35, + subject: "Test more button focus", + }); + + // Add the message to the end of the folder. + await add_message_to_folder([folder], msg); + + for (let { focusMore, useKeyboard } of [ + { focusMore: true, useKeyboard: false }, + { focusMore: true, useKeyboard: true }, + { focusMore: false, useKeyboard: false }, + ]) { + // Reload the message. + let prevMessage = select_click_row(-1); + wait_for_message_display_completion(mc); + assert_selected_and_displayed(mc, prevMessage); + + let curMessage = select_click_row(-2); + wait_for_message_display_completion(mc); + assert_selected_and_displayed(mc, curMessage); + + let items = [ + ...aboutMessage.document.querySelectorAll( + "#expandedtoBox .recipients-list li" + ), + ]; + Assert.greater(items.length, 2, "Should have enough items for the test"); + let moreButton = aboutMessage.document.querySelector( + "#expandedtoBox .show-more-recipients" + ); + Assert.ok(moreButton, "The more button should exist"); + Assert.ok( + items[items.length - 1].contains(moreButton), + "More button should be the final button in the list" + ); + let index; + if (focusMore) { + index = items.length - 1; + moreButton.focus(); + Assert.ok( + moreButton.matches(":focus"), + "The more button can receive focus" + ); + } else { + index = 1; + items[1].focus(); + Assert.ok( + items[1].matches(":focus"), + "The second item can receive focus" + ); + } + if (useKeyboard) { + EventUtils.synthesizeKey("KEY_Enter", {}, aboutMessage); + } else { + EventUtils.synthesizeMouseAtCenter(moreButton, {}, aboutMessage); + } + + Assert.ok( + !aboutMessage.document.querySelector( + "#expandedtoBox .show-more-recipients" + ), + "The more button should be removed" + ); + items = [ + ...aboutMessage.document.querySelectorAll( + "#expandedtoBox .recipients-list li" + ), + ]; + Assert.ok( + items[index].matches(":focus"), + `The focus should be on item ${index}` + ); + } +}); + +/** + * Test that all addresses are shown in show all header mode. + */ +add_task(async function test_show_all_header_mode() { + async function toggle_header_mode(show) { + let popup = document.getElementById("otherActionsPopup"); + let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + EventUtils.synthesizeMouseAtCenter( + document.getElementById("otherActionsButton"), + {}, + mc.window + ); + await popupShown; + + let panel = document.getElementById("messageHeaderCustomizationPanel"); + let customizeBtn = document.getElementById( + "messageHeaderMoreMenuCustomize" + ); + let panelShown = BrowserTestUtils.waitForEvent(panel, "popupshown"); + EventUtils.synthesizeMouseAtCenter(customizeBtn, {}, mc.window); + await panelShown; + + let viewAllHeaders = document.getElementById("headerViewAllHeaders"); + + let modeChanged = await TestUtils.waitForCondition( + () => + document + .getElementById("messageHeader") + .getAttribute("show_header_mode") == show + ? "all" + : "normal", + "Message header updated correctly" + ); + EventUtils.synthesizeMouseAtCenter(viewAllHeaders, {}, mc.window); + await modeChanged; + + Assert.ok( + viewAllHeaders.checked == show, + "The view all headers checkbox was updated to the correct state" + ); + + await BrowserTestUtils.waitForCondition( + () => + aboutMessage.document.getElementById("expandedsubjectBox").value + .textContent, + "The message was loaded" + ); + + let panelHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + await panelHidden; + } + + // Generate message with 35 recipients. + await be_in_folder(folder); + let msg = create_message({ + toCount: 35, + subject: "many To addresses for test_show_all_header_mode", + }); + + // Add the message to the end of the folder. + await add_message_to_folder([folder], msg); + + // Select and open the added message. + // It is at the second last position in the display list. + let curMessage = select_click_row(-2); + + // Make sure it loads. + wait_for_message_display_completion(mc); + assert_selected_and_displayed(mc, curMessage); + + await toggle_header_mode(true); + wait_for_message_display_completion(mc); + assert_selected_and_displayed(mc, curMessage); + let node = + aboutMessage.document.getElementById("expandedtoBox").recipientsList; + await subtest_more_widget_display(node, true); + + await toggle_header_mode(false); + wait_for_message_display_completion(mc); + assert_selected_and_displayed(mc, curMessage); + await subtest_more_widget_display(node); + subtest_more_widget_activate(node); + await subtest_more_widget_display(node, true); +}).skip(); + +async function help_test_starred_messages() { + await be_in_folder(folder); + + // Select the last message, which will display it. + let curMessage = select_click_row(-1); + wait_for_message_display_completion(mc); + assert_selected_and_displayed(mc, curMessage); + + let starButton = aboutMessage.document.getElementById("starMessageButton"); + // The message shouldn't be starred. + Assert.ok( + !starButton.classList.contains("flagged"), + "The message is not starred" + ); + + // Press s to mark the message as starred. + EventUtils.synthesizeKey("s", {}, aboutMessage); + // The message should be starred. + Assert.ok(starButton.classList.contains("flagged"), "The message is starred"); + + // Click on the star button. + EventUtils.synthesizeMouseAtCenter(starButton, {}, aboutMessage); + // The message shouldn't be starred. + Assert.ok( + !starButton.classList.contains("flagged"), + "The message is not starred" + ); + + // Click again on the star button. + EventUtils.synthesizeMouseAtCenter(starButton, {}, aboutMessage); + // The message should be starred. + Assert.ok(starButton.classList.contains("flagged"), "The message is starred"); + + // Select the first message. + curMessage = select_click_row(0); + wait_for_message_display_completion(mc); + assert_selected_and_displayed(mc, curMessage); + + // The newly selected message shouldn't be starred. + Assert.ok( + !starButton.classList.contains("flagged"), + "The message is not starred" + ); + + // Select again the last message. + curMessage = select_click_row(-1); + wait_for_message_display_completion(mc); + assert_selected_and_displayed(mc, curMessage); + + // The message should still be starred. + Assert.ok(starButton.classList.contains("flagged"), "The message is starred"); + + let hdr = folder.msgDatabase.getMsgHdrForMessageID(curMessage.messageId); + // Update the starred state not through a click on the star button, to make + // sure the method works. + hdr.markFlagged(false); + // The message should be starred. + Assert.ok( + !starButton.classList.contains("flagged"), + "The message is not starred" + ); +} + +/** + * Test the marking of a message as starred, be sure the header is properly + * updated and changing selected message doesn't affect the state of others. + */ +add_task(async function test_starred_message() { + await help_test_starred_messages(); +}); + +add_task(async function test_starred_message_unified_mode() { + mc.window.document.getElementById( + "tabmail" + ).currentTabInfo.folderPaneVisible = true; + select_none(); + // Show the "Unified" folders view. + mc.folderTreeView.activeModes = "smart"; + // Hide the all folders view. The activeModes setter takes care of removing + // the mode is is already visible. + mc.folderTreeView.activeModes = "all"; + + await help_test_starred_messages(); + + mc.window.document.getElementById( + "tabmail" + ).currentTabInfo.folderPaneVisible = false; + select_none(); + // Show the "All" folders view. + mc.folderTreeView.activeModes = "all"; + // Hide the "Unified" folders view. The activeModes setter takes care of + // removing the mode is is already visible. + mc.folderTreeView.activeModes = "smart"; +}).skip(); +/** + * Test the DBListener to be sure is initialized and cleared when needed, and it + * doesn't change when not needed. + */ +add_task(async function test_folder_db_listener() { + await be_in_folder(folderMore); + // Select the last message, which will display it. + let curMessage = select_click_row(-1); + wait_for_message_display_completion(mc); + assert_selected_and_displayed(mc, curMessage); + + Assert.ok( + aboutMessage.gFolderDBListener.isRegistered, + "The folder DB listener was initialized" + ); + Assert.equal( + folderMore, + aboutMessage.gFolderDBListener.selectedFolder, + "The current folder was stored correctly" + ); + + // Keep a reference before it gets cleared. + let gFolderDBRef = aboutMessage.gFolderDBListener; + + // Collapse the message pane. + aboutMessage.HideMessageHeaderPane(); + + Assert.ok(!gFolderDBRef.isRegistered, "The folder DB listener was cleared"); + Assert.equal( + folderMore, + gFolderDBRef.selectedFolder, + "The current folder wasn't cleared and is still the same" + ); + + // Change folder + await be_in_folder(folder); + + // Select the last message, which will display it. + curMessage = select_click_row(-1); + wait_for_message_display_completion(mc); + assert_selected_and_displayed(mc, curMessage); + + Assert.ok( + aboutMessage.gFolderDBListener?.isRegistered, + "The folder DB listener was initialized" + ); + Assert.equal( + folder, + aboutMessage.gFolderDBListener.selectedFolder, + "The current folder was stored correctly" + ); +}); + +/** + * Remove the reference to the accessibility service so that it stops observing + * vsync notifications at the end of the test. + */ +add_task(function cleanup() { + gAccService = null; + // The actual reference to the XPCOM object will be dropped at the next GC, + // so force one to happen immediately. + Cu.forceGC(); +}); diff --git a/comm/mail/test/browser/message-header/browser_messageHeaderCustomize.js b/comm/mail/test/browser/message-header/browser_messageHeaderCustomize.js new file mode 100644 index 0000000000..67ca412587 --- /dev/null +++ b/comm/mail/test/browser/message-header/browser_messageHeaderCustomize.js @@ -0,0 +1,388 @@ +/* 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/. */ + +/** + * Test the message header customization features. + */ +var { + add_message_to_folder, + assert_selected_and_displayed, + be_in_folder, + close_popup, + create_folder, + create_message, + gDefaultWindowHeight, + get_smart_folder_named, + get_about_3pane, + get_about_message, + inboxFolder, + mc, + msgGen, + restore_default_window_size, + select_click_row, + select_none, + wait_for_message_display_completion, + wait_for_popup_to_open, +} = ChromeUtils.import( + "resource://testing-common/mozmill/FolderDisplayHelpers.jsm" +); + +let about3Pane = get_about_3pane(); +let aboutMessage = get_about_message(); + +var { MailTelemetryForTests } = ChromeUtils.import( + "resource:///modules/MailGlue.jsm" +); +var { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +var gFolder; + +add_setup(async function () { + Services.xulStore.removeDocument( + "chrome://messenger/content/messenger.xhtml" + ); + Services.telemetry.clearScalars(); + + let account = createAccount(); + gFolder = await createSubfolder(account.incomingServer.rootFolder, "test0"); + createMessages(gFolder, 1); + + registerCleanupFunction(() => { + gFolder.deleteSelf(null); + MailServices.accounts.removeAccount(account, true); + Services.xulStore.removeDocument( + "chrome://messenger/content/messenger.xhtml" + ); + }); +}); + +add_task(async function test_customize_toolbar_buttons() { + be_in_folder(gFolder); + select_click_row(0); + + let moreBtn = aboutMessage.document.getElementById("otherActionsButton"); + // Make sure we loaded the expected message. + await assertVisibility(moreBtn, true, "The more button is visible"); + + // Confirm we're starting from a clean state. + let header = aboutMessage.document.getElementById("messageHeader"); + Assert.ok( + header.classList.contains("message-header-show-recipient-avatar"), + "The From recipient is showing the avatar" + ); + let avatar = aboutMessage.document.querySelector(".recipient-avatar"); + await assertVisibility(avatar, true, "The recipient avatar is shown"); + + Assert.ok( + header.classList.contains("message-header-show-sender-full-address"), + "The From recipient is showing the full address on two lines" + ); + let multiLine = aboutMessage.document.querySelector(".recipient-multi-line"); + await assertVisibility( + multiLine, + true, + "The recipient multi line is visible" + ); + let singleLine = aboutMessage.document.querySelector( + ".recipient-single-line" + ); + await assertVisibility( + singleLine, + false, + "he recipient single line is hidden" + ); + + Assert.ok( + header.classList.contains("message-header-hide-label-column"), + "The labels column is hidden" + ); + + let firstLabel = aboutMessage.document.querySelector(".message-header-label"); + Assert.equal( + firstLabel.style.minWidth, + "0px", + "The first label has no min-width value" + ); + await assertVisibility(firstLabel, false, "The labels column is hidden"); + + Assert.ok( + header.classList.contains("message-header-large-subject"), + "The message header has a large subject" + ); + Assert.ok( + !header.classList.contains("message-header-buttons-only-icons"), + "The message header buttons aren't showing only icons" + ); + Assert.ok( + !header.classList.contains("message-header-buttons-only-text"), + "The message header buttons aren't showing only text" + ); + + MailTelemetryForTests.reportUIConfiguration(); + let scalarName = "tb.ui.configuration.message_header"; + let scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertScalarUnset(scalars, scalarName); + + let popup = aboutMessage.document.getElementById("otherActionsPopup"); + let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + EventUtils.synthesizeMouseAtCenter(moreBtn, {}, aboutMessage); + await popupShown; + + let panel = aboutMessage.document.getElementById( + "messageHeaderCustomizationPanel" + ); + let customizeBtn = aboutMessage.document.getElementById( + "messageHeaderMoreMenuCustomize" + ); + let panelShown = BrowserTestUtils.waitForEvent(panel, "popupshown"); + EventUtils.synthesizeMouseAtCenter(customizeBtn, {}, aboutMessage); + await panelShown; + + let buttonStyle = aboutMessage.document.getElementById("headerButtonStyle"); + // Assert the options are in a default state. + Assert.equal( + buttonStyle.value, + "default", + "The buttons style is in the default state" + ); + let subjectLarge = aboutMessage.document.getElementById("headerSubjectLarge"); + Assert.ok(subjectLarge.checked, "The subject field is in the default state"); + + let showAvatar = aboutMessage.document.getElementById("headerShowAvatar"); + Assert.ok( + showAvatar.checked, + "The show avatar field is in the default state" + ); + + let showFullAddress = aboutMessage.document.getElementById( + "headerShowFullAddress" + ); + Assert.ok( + showFullAddress.checked, + "The show full address field is in the default state" + ); + + let hideLabels = aboutMessage.document.getElementById("headerHideLabels"); + Assert.ok( + hideLabels.checked, + "The hide labels field is in the default state" + ); + + let openMenuPopup = async function () { + aboutMessage.document.getElementById("headerButtonStyle").focus(); + + let menuPopupShown = BrowserTestUtils.waitForEvent( + aboutMessage.document.querySelector("#headerButtonStyle menupopup"), + "popupshown" + ); + // Use the keyboard to open and cycle through the menulist items because the + // mouse events are unreliable in tests. + EventUtils.synthesizeMouseAtCenter( + aboutMessage.document.getElementById("headerButtonStyle"), + {}, + aboutMessage + ); + await menuPopupShown; + }; + + // Cycle through the buttons style and confirm the style is properly applied. + // Use the keyboard to open and cycle through the menulist items because the + // mouse events are unreliable in tests. + await openMenuPopup(); + EventUtils.sendKey("down", aboutMessage); + EventUtils.sendKey("return", aboutMessage); + + await BrowserTestUtils.waitForCondition( + () => header.classList.contains("message-header-buttons-only-text"), + "The buttons are showing only text" + ); + Assert.ok( + header.classList.contains("message-header-large-subject"), + "The subject line wasn't changed" + ); + Assert.ok( + header.classList.contains("message-header-show-recipient-avatar"), + "The avatar visibility wasn't changed" + ); + Assert.ok( + header.classList.contains("message-header-show-sender-full-address"), + "The full address visibility wasn't changed" + ); + Assert.ok( + header.classList.contains("message-header-hide-label-column"), + "The labels column visibility wasn't changed" + ); + + await openMenuPopup(); + EventUtils.sendKey("down", aboutMessage); + EventUtils.sendKey("return", aboutMessage); + await BrowserTestUtils.waitForCondition( + () => header.classList.contains("message-header-buttons-only-icons"), + "The buttons are showing only icons" + ); + Assert.ok( + header.classList.contains("message-header-large-subject"), + "The subject line wasn't changed" + ); + + await openMenuPopup(); + EventUtils.sendKey("up", aboutMessage); + EventUtils.sendKey("up", aboutMessage); + EventUtils.sendKey("return", aboutMessage); + await BrowserTestUtils.waitForCondition( + () => + !header.classList.contains("message-header-buttons-only-icons") && + !header.classList.contains("message-header-buttons-only-text") && + header.classList.contains("message-header-large-subject") && + header.classList.contains("message-header-show-recipient-avatar") && + header.classList.contains("message-header-show-sender-full-address") && + header.classList.contains("message-header-hide-label-column"), + "The message header is clear of any custom style" + ); + + EventUtils.synthesizeMouseAtCenter(subjectLarge, {}, aboutMessage); + await BrowserTestUtils.waitForCondition( + () => !header.classList.contains("message-header-large-subject"), + "The subject line was changed" + ); + + EventUtils.synthesizeMouseAtCenter(showAvatar, {}, aboutMessage); + await BrowserTestUtils.waitForCondition( + () => !header.classList.contains("message-header-show-recipient-avatar"), + "The avatar style was changed" + ); + await assertVisibility(avatar, false, "The recipient avatar is hidden"); + + EventUtils.synthesizeMouseAtCenter(showFullAddress, {}, aboutMessage); + await BrowserTestUtils.waitForCondition( + () => !header.classList.contains("message-header-show-sender-full-address"), + "The full address style was changed" + ); + await assertVisibility( + multiLine, + false, + "The recipient multi line is hidden" + ); + await assertVisibility( + singleLine, + true, + "The recipient single line is visible" + ); + + EventUtils.synthesizeMouseAtCenter(hideLabels, {}, aboutMessage); + await BrowserTestUtils.waitForCondition( + () => !header.classList.contains("message-header-hide-label-column"), + "The labels column style was changed" + ); + await assertVisibility(firstLabel, true, "The first label is visible"); + await BrowserTestUtils.waitForCondition( + () => firstLabel.style.minWidth != "0px", + "The first label has a min-width value" + ); + + await openMenuPopup(); + EventUtils.sendKey("down", aboutMessage); + EventUtils.sendKey("down", aboutMessage); + EventUtils.sendKey("return", aboutMessage); + await BrowserTestUtils.waitForCondition( + () => header.classList.contains("message-header-buttons-only-icons"), + "The buttons are showing only icons" + ); + await BrowserTestUtils.waitForCondition( + () => !header.classList.contains("message-header-large-subject"), + "The subject line edit was maintained" + ); + + let panelHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, aboutMessage); + await panelHidden; + + await BrowserTestUtils.waitForCondition( + () => + Services.xulStore.hasValue( + "chrome://messenger/content/messenger.xhtml", + "messageHeader", + "layout" + ), + "The customization data was saved" + ); + + MailTelemetryForTests.reportUIConfiguration(); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar(scalars, scalarName, "subjectLarge", 0); + TelemetryTestUtils.assertKeyedScalar(scalars, scalarName, "buttonStyle", 1); + TelemetryTestUtils.assertKeyedScalar(scalars, scalarName, "hideLabels", 0); + TelemetryTestUtils.assertKeyedScalar(scalars, scalarName, "showAvatar", 0); + TelemetryTestUtils.assertKeyedScalar( + scalars, + scalarName, + "showFullAddress", + 0 + ); + + popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + EventUtils.synthesizeMouseAtCenter(moreBtn, {}, aboutMessage); + await popupShown; + + panelShown = BrowserTestUtils.waitForEvent(panel, "popupshown"); + EventUtils.synthesizeMouseAtCenter(customizeBtn, {}, aboutMessage); + await panelShown; + + await openMenuPopup(); + EventUtils.sendKey("up", aboutMessage); + EventUtils.sendKey("up", aboutMessage); + EventUtils.sendKey("return", aboutMessage); + + await BrowserTestUtils.waitForCondition( + () => + !header.classList.contains("message-header-buttons-only-icons") && + !header.classList.contains("message-header-buttons-only-text"), + "The buttons style was reverted to the default" + ); + + EventUtils.synthesizeMouseAtCenter(subjectLarge, {}, aboutMessage); + EventUtils.synthesizeMouseAtCenter(showAvatar, {}, aboutMessage); + EventUtils.synthesizeMouseAtCenter(showFullAddress, {}, aboutMessage); + EventUtils.synthesizeMouseAtCenter(hideLabels, {}, aboutMessage); + + panelHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, aboutMessage); + await panelHidden; + + await BrowserTestUtils.waitForCondition( + () => header.classList.contains("message-header-large-subject"), + "The subject line is large again" + ); + await assertVisibility(avatar, true, "The recipient avatar is visible"); + await assertVisibility( + multiLine, + true, + "The recipient multi line is visible" + ); + await assertVisibility( + singleLine, + false, + "he recipient single line is hidden" + ); + await BrowserTestUtils.waitForCondition( + () => firstLabel.style.minWidth == "0px", + "The first label has no min-width value" + ); + await assertVisibility(firstLabel, false, "The labels column is hidden"); + + MailTelemetryForTests.reportUIConfiguration(); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar(scalars, scalarName, "subjectLarge", 1); + TelemetryTestUtils.assertKeyedScalar(scalars, scalarName, "buttonStyle", 0); + TelemetryTestUtils.assertKeyedScalar(scalars, scalarName, "hideLabels", 1); + TelemetryTestUtils.assertKeyedScalar(scalars, scalarName, "showAvatar", 1); + TelemetryTestUtils.assertKeyedScalar( + scalars, + scalarName, + "showFullAddress", + 1 + ); +}); diff --git a/comm/mail/test/browser/message-header/browser_phishingBar.js b/comm/mail/test/browser/message-header/browser_phishingBar.js new file mode 100644 index 0000000000..f4b429725e --- /dev/null +++ b/comm/mail/test/browser/message-header/browser_phishingBar.js @@ -0,0 +1,307 @@ +/* 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/. */ + +/** + * Test that phishing notifications behave properly. + */ + +"use strict"; + +var { gMockExtProtSvcReg } = ChromeUtils.import( + "resource://testing-common/mozmill/ContentTabHelpers.jsm" +); +var { + add_message_to_folder, + be_in_folder, + create_folder, + create_message, + get_about_message, + inboxFolder, + mc, + open_message_from_file, + select_click_row, + wait_for_message_display_completion, +} = ChromeUtils.import( + "resource://testing-common/mozmill/FolderDisplayHelpers.jsm" +); +var { + assert_notification_displayed, + get_notification_button, + wait_for_notification_to_show, + wait_for_notification_to_stop, +} = ChromeUtils.import( + "resource://testing-common/mozmill/NotificationBoxHelpers.jsm" +); +var { + async_plan_for_new_window, + close_window, + plan_for_modal_dialog, + plan_for_new_window, + wait_for_new_window, + click_menus_in_sequence, +} = ChromeUtils.import("resource://testing-common/mozmill/WindowHelpers.jsm"); +var folder; + +var kBoxId = "mail-notification-top"; +var kNotificationValue = "maybeScam"; + +add_setup(async function () { + gMockExtProtSvcReg.register(); + + folder = await create_folder("PhishingBarA"); + await add_message_to_folder( + [folder], + create_message({ + body: { + body: '<form action="http://localhost/download-me"><input></form>.', + contentType: "text/html", + }, + }) + ); + await add_message_to_folder([folder], create_message()); + await add_message_to_folder( + [folder], + create_message({ + body: { + body: "check out http://130.128.4.1. and http://130.128.4.2/.", + contentType: "text/plain", + }, + }) + ); + await add_message_to_folder( + [folder], + create_message({ + body: { + body: '<a href="http://subdomain.google.com/">http://www.google.com</a>.', + contentType: "text/html", + }, + }) + ); + await add_message_to_folder( + [folder], + create_message({ + body: { + body: '<a href="http://subdomain.google.com/">http://google.com</a>.', + contentType: "text/html", + }, + }) + ); + await add_message_to_folder( + [folder], + create_message({ + body: { + body: '<a href="http://evilhost">http://localhost</a>.', + contentType: "text/html", + }, + }) + ); + await add_message_to_folder( + [folder], + create_message({ + body: { + body: '<form action="http://localhost/download-me"><input></form>.', + contentType: "text/html", + }, + }) + ); +}); + +registerCleanupFunction(() => { + gMockExtProtSvcReg.unregister(); +}); + +/** + * Make sure the notification shows, and goes away once the Ignore menuitem + * is clicked. + */ +async function assert_ignore_works(aController) { + let aboutMessage = get_about_message(aController.window); + wait_for_notification_to_show(aboutMessage, kBoxId, kNotificationValue); + let prefButton = get_notification_button( + aboutMessage, + kBoxId, + kNotificationValue, + { popup: "phishingOptions" } + ); + EventUtils.synthesizeMouseAtCenter(prefButton, {}, prefButton.ownerGlobal); + await click_menus_in_sequence( + aboutMessage.document.getElementById("phishingOptions"), + [{ id: "phishingOptionIgnore" }] + ); + wait_for_notification_to_stop(aboutMessage, kBoxId, kNotificationValue); +} + +/** + * Helper function to click the first link in a message if one is available. + */ +function click_link_if_available() { + let msgBody = + get_about_message().getMessagePaneBrowser().contentDocument.body; + if (msgBody.getElementsByTagName("a").length > 0) { + msgBody.getElementsByTagName("a")[0].click(); + } +} + +/** + * Test that when viewing a message, choosing ignore hides the the phishing + * notification. + */ +add_task(async function test_ignore_phishing_warning_from_message() { + let aboutMessage = get_about_message(); + + await be_in_folder(folder); + select_click_row(0); + await assert_ignore_works(mc); + + select_click_row(1); + // msg 1 is normal -> no phishing warning + assert_notification_displayed( + aboutMessage, + kBoxId, + kNotificationValue, + false + ); + select_click_row(0); + // msg 0 is a potential phishing attempt, but we ignored it so that should + // be remembered + assert_notification_displayed( + aboutMessage, + kBoxId, + kNotificationValue, + false + ); +}); + +/** + * Test that when viewing en eml file, choosing ignore hides the phishing + * notification. + */ +add_task(async function test_ignore_phishing_warning_from_eml() { + let file = new FileUtils.File(getTestFilePath("data/evil.eml")); + + let msgc = await open_message_from_file(file); + await assert_ignore_works(msgc); + close_window(msgc); +}).skip(); + +/** + * Test that when viewing an attached eml file, the phishing notification works. + */ +add_task(async function test_ignore_phishing_warning_from_eml_attachment() { + let file = new FileUtils.File(getTestFilePath("data/evil-attached.eml")); + + let msgc = await open_message_from_file(file); + let aboutMessage = get_about_message(msgc.window); + + // Make sure the root message shows the phishing bar. + wait_for_notification_to_show(aboutMessage, kBoxId, kNotificationValue); + + // Open the attached message. + let newWindowPromise = async_plan_for_new_window("mail:messageWindow"); + aboutMessage.document + .getElementById("attachmentList") + .getItemAtIndex(0) + .attachment.open(); + let msgc2 = await newWindowPromise; + wait_for_message_display_completion(msgc2, true); + + // Now make sure the attached message shows the phishing bar. + wait_for_notification_to_show( + get_about_message(msgc2.window), + kBoxId, + kNotificationValue + ); + + close_window(msgc2); + close_window(msgc); +}).skip(); + +/** + * Test that when viewing a message with an auto-linked ip address, we don't + * get a warning when clicking the link. + * We'll have http://130.128.4.1 vs. http://130.128.4.1/ + */ +add_task(async function test_no_phishing_warning_for_ip_sameish_text() { + await be_in_folder(folder); + select_click_row(2); // Mail with Public IP address. + click_link_if_available(); + assert_notification_displayed( + get_about_message(), + kBoxId, + kNotificationValue, + false + ); // not shown +}); + +/** + * Test that when viewing a message with a link whose base domain matches but + * has a different subdomain (e.g. http://subdomain.google.com/ vs + * http://google.com/), we don't get a warning if the link is pressed. + */ +add_task(async function test_no_phishing_warning_for_subdomain() { + let aboutMessage = get_about_message(); + await be_in_folder(folder); + select_click_row(3); + click_link_if_available(); + assert_notification_displayed( + aboutMessage, + kBoxId, + kNotificationValue, + false + ); // not shown + + select_click_row(4); + click_link_if_available(); + assert_notification_displayed( + aboutMessage, + kBoxId, + kNotificationValue, + false + ); // not shown +}); + +/** + * Test that when clicking a link where the text and/or href + * has no TLD, we still warn as appropriate. + */ +add_task(async function test_phishing_warning_for_local_domain() { + await be_in_folder(folder); + select_click_row(5); + + let dialogAppeared = false; + + plan_for_modal_dialog("commonDialogWindow", function (ctrler) { + dialogAppeared = true; + }); + + click_link_if_available(); + + Assert.ok(dialogAppeared); +}); + +/** + * Test that we warn about emails which contain <form>s with action attributes. + */ +add_task(async function test_phishing_warning_for_action_form() { + await be_in_folder(folder); + select_click_row(6); + assert_notification_displayed( + get_about_message(), + kBoxId, + kNotificationValue, + true + ); // shown + + Assert.report( + false, + undefined, + undefined, + "Test ran to completion successfully" + ); +}); + +registerCleanupFunction(async function teardown() { + await be_in_folder(inboxFolder); + folder.deleteSelf(null); +}); diff --git a/comm/mail/test/browser/message-header/browser_replyIdentity.js b/comm/mail/test/browser/message-header/browser_replyIdentity.js new file mode 100644 index 0000000000..fa1c1a7dae --- /dev/null +++ b/comm/mail/test/browser/message-header/browser_replyIdentity.js @@ -0,0 +1,231 @@ +/* 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 that actions such as replying choses the most suitable identity. + */ + +"use strict"; + +var { close_compose_window, open_compose_with_reply } = ChromeUtils.import( + "resource://testing-common/mozmill/ComposeHelpers.jsm" +); +var { + add_message_to_folder, + assert_selected_and_displayed, + be_in_folder, + create_message, + mc, + select_click_row, +} = ChromeUtils.import( + "resource://testing-common/mozmill/FolderDisplayHelpers.jsm" +); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var testFolder = null; + +var identity1Email = "carl@example.com"; +var identity2Email = "lenny@springfield.invalid"; + +add_setup(async function () { + addIdentitiesAndFolder(); + // Msg #0 + await add_message_to_folder( + [testFolder], + create_message({ + from: "Homer <homer@example.com>", + to: "workers@springfield.invalid", + subject: "no matching identity, like bcc/list", + body: { + body: "Alcohol is a way of life, alcohol is my way of life, and I aim to keep it.", + }, + clobberHeaders: {}, + }) + ); + // Msg #1 + await add_message_to_folder( + [testFolder], + create_message({ + from: "Homer <homer@example.com>", + to: "powerplant-workers@springfield.invalid", + subject: "only delivered-to header matching identity", + body: { + body: "Just because I don't care doesn't mean I don't understand.", + }, + clobberHeaders: { + "Delivered-To": "<" + identity2Email + ">", + }, + }) + ); + // Msg #2 + await add_message_to_folder( + [testFolder], + create_message({ + from: "Homer <homer@example.com>", + to: "powerplant-workers@springfield.invalid, Apu <apu@test.invalid>", + cc: "other." + identity2Email, + subject: "subpart of cc address matching identity", + body: { body: "Blame the guy who doesn't speak Engish." }, + clobberHeaders: {}, + }) + ); + // Msg #3 + await add_message_to_folder( + [testFolder], + create_message({ + from: "Homer <homer@example.com>", + to: "Lenny <" + identity2Email + ">", + subject: "normal to:address match, with full name", + body: { + body: "Remember as far as anyone knows, we're a nice normal family.", + }, + }) + ); + // Msg #4 + await add_message_to_folder( + [testFolder], + create_message({ + from: "Homer <homer@example.com>", + to: "powerplant-workers@springfield.invalid", + subject: "delivered-to header matching only subpart of identity email", + body: { body: "Mmmm...Forbidden donut" }, + clobberHeaders: { + "Delivered-To": "<other." + identity2Email + ">", + }, + }) + ); + // Msg #5 + await add_message_to_folder( + [testFolder], + create_message({ + from: identity2Email + " <" + identity2Email + ">", + to: "Marge <marge@example.com>", + subject: "from second self", + body: { + body: "All my life I've had one dream, to achieve my many goals.", + }, + }) + ); +}); + +function addIdentitiesAndFolder() { + let server = MailServices.accounts.createIncomingServer( + "nobody", + "Reply Identity Testing", + "pop3" + ); + testFolder = server.rootFolder + .QueryInterface(Ci.nsIMsgLocalMailFolder) + .createLocalSubfolder("Replies"); + + let identity = MailServices.accounts.createIdentity(); + identity.email = identity1Email; + + let identity2 = MailServices.accounts.createIdentity(); + identity2.email = identity2Email; + + let account = MailServices.accounts.createAccount(); + account.incomingServer = server; + account.addIdentity(identity); + account.addIdentity(identity2); +} + +function checkReply(replyWin, expectedFromEmail) { + let identityList = replyWin.window.document.getElementById("msgIdentity"); + if (!identityList.selectedItem.label.includes(expectedFromEmail)) { + throw new Error( + "The From address is not correctly selected! Expected: " + + expectedFromEmail + + "; Actual: " + + identityList.selectedItem.label + ); + } +} + +add_task(async function test_reply_no_matching_identity() { + await be_in_folder(testFolder); + + let msg = select_click_row(0); + assert_selected_and_displayed(mc, msg); + + let replyWin = open_compose_with_reply(); + // Should have selected the default identity. + checkReply(replyWin, identity1Email); + close_compose_window(replyWin); +}); + +add_task(async function test_reply_matching_only_deliveredto() { + await be_in_folder(testFolder); + + let msg = select_click_row(1); + assert_selected_and_displayed(mc, msg); + + let replyWin = open_compose_with_reply(); + // Should have selected the second id, which is listed in Delivered-To:. + checkReply(replyWin, identity2Email); + close_compose_window(replyWin); +}).skip(); + +add_task(async function test_reply_matching_subaddress() { + await be_in_folder(testFolder); + + let msg = select_click_row(2); + assert_selected_and_displayed(mc, msg); + + let replyWin = open_compose_with_reply(); + // Should have selected the first id, the email doesn't fully match. + // other.lenny != "our" lenny + checkReply(replyWin, identity1Email); + close_compose_window(replyWin); +}); + +add_task(async function test_reply_to_matching_second_id() { + await be_in_folder(testFolder); + + let msg = select_click_row(3); + assert_selected_and_displayed(mc, msg); + + let replyWin = open_compose_with_reply(); + // Should have selected the second id, which was in To;. + checkReply(replyWin, identity2Email); + close_compose_window(replyWin); +}); + +add_task(async function test_deliveredto_to_matching_only_parlty() { + await be_in_folder(testFolder); + + let msg = select_click_row(4); + assert_selected_and_displayed(mc, msg); + + let replyWin = open_compose_with_reply(); + // Should have selected the (default) first id. + checkReply(replyWin, identity1Email); + close_compose_window(replyWin); +}); + +/** + * A reply from self is treated as a follow-up. And this self + * was the second identity, so the reply should also be from the second identity. + */ +add_task(async function test_reply_to_self_second_id() { + await be_in_folder(testFolder); + + let msg = select_click_row(5); + assert_selected_and_displayed(mc, msg); + + let replyWin = open_compose_with_reply(); + // Should have selected the second id, which was in From. + checkReply(replyWin, identity2Email); + close_compose_window(replyWin, false /* no prompt*/); + + Assert.report( + false, + undefined, + undefined, + "Test ran to completion successfully" + ); +}); diff --git a/comm/mail/test/browser/message-header/browser_replyToListFromAddressSelection.js b/comm/mail/test/browser/message-header/browser_replyToListFromAddressSelection.js new file mode 100644 index 0000000000..b72d28de45 --- /dev/null +++ b/comm/mail/test/browser/message-header/browser_replyToListFromAddressSelection.js @@ -0,0 +1,121 @@ +/* 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/. */ + +/* + * Test for the most suitable identity in From address for reply-to-list + */ + +"use strict"; + +var { close_compose_window, open_compose_with_reply_to_list } = + ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm"); +var { assert_selected_and_displayed, be_in_folder, mc, select_click_row } = + ChromeUtils.import( + "resource://testing-common/mozmill/FolderDisplayHelpers.jsm" + ); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var testFolder = null; +var msgHdr = null; +var replyToListWindow = null; + +var identityString1 = "tinderbox_correct_identity@foo.invalid"; + +add_setup(function () { + addIdentitiesAndFolder(); + addMessageToFolder(testFolder); +}); + +function addMessageToFolder(aFolder) { + var msgId = Services.uuid.generateUUID() + "@mozillamessaging.invalid"; + + var source = + "From - Sat Nov 1 12:39:54 2008\n" + + "X-Mozilla-Status: 0001\n" + + "X-Mozilla-Status2: 00000000\n" + + "Delivered-To: <tinderbox_identity333@foo.invalid>\n" + + "Delivered-To: <" + + identityString1 + + ">\n" + + "Delivered-To: <tinderbox_identity555@foo.invalid>\n" + + "Message-ID: <" + + msgId + + ">\n" + + "Date: Wed, 11 Jun 2008 20:32:02 -0400\n" + + "From: Tester <tests@mozillamessaging.invalid>\n" + + "User-Agent: Thunderbird 3.0a2pre (Macintosh/2008052122)\n" + + "MIME-Version: 1.0\n" + + "List-ID: <list.mozillamessaging.invalid>\n" + + "List-Post: <list.mozillamessaging.invalid>, \n" + + " <mailto: list@mozillamessaging.invalid>\n" + + "To: recipient@mozillamessaging.invalid\n" + + "Subject: a subject\n" + + "Content-Type: text/html; charset=ISO-8859-1\n" + + "Content-Transfer-Encoding: 7bit\n" + + "\ntext body\n"; + + aFolder.QueryInterface(Ci.nsIMsgLocalMailFolder); + aFolder.gettingNewMessages = true; + aFolder.addMessage(source); + aFolder.gettingNewMessages = false; + + return aFolder.msgDatabase.getMsgHdrForMessageID(msgId); +} + +function addIdentitiesAndFolder() { + let identity2 = MailServices.accounts.createIdentity(); + // identity.fullName = "Tinderbox_Identity1"; + identity2.email = "tinderbox_identity1@foo.invalid"; + + let identity = MailServices.accounts.createIdentity(); + // identity.fullName = "Tinderbox_Identity1"; + identity.email = identityString1; + + let server = MailServices.accounts.createIncomingServer( + "nobody", + "Test Local Folders", + "pop3" + ); + let localRoot = server.rootFolder.QueryInterface(Ci.nsIMsgLocalMailFolder); + testFolder = localRoot.createLocalSubfolder("Test Folder"); + + let account = MailServices.accounts.createAccount(); + account.incomingServer = server; + account.addIdentity(identity); + account.addIdentity(identity2); +} + +add_task(async function test_Reply_To_List_From_Address() { + await be_in_folder(testFolder); + + let curMessage = select_click_row(0); + assert_selected_and_displayed(mc, curMessage); + + replyToListWindow = open_compose_with_reply_to_list(); + + var identityList = + replyToListWindow.window.document.getElementById("msgIdentity"); + + // see if it's the correct identity selected + if (!identityList.selectedItem.label.includes(identityString1)) { + throw new Error( + "The From address is not correctly selected! Expected: " + + identityString1 + + "; Actual: " + + identityList.selectedItem.label + ); + } + + close_compose_window(replyToListWindow); + + Assert.report( + false, + undefined, + undefined, + "Test ran to completion successfully" + ); +}); diff --git a/comm/mail/test/browser/message-header/browser_returnReceipt.js b/comm/mail/test/browser/message-header/browser_returnReceipt.js new file mode 100644 index 0000000000..f48fa04313 --- /dev/null +++ b/comm/mail/test/browser/message-header/browser_returnReceipt.js @@ -0,0 +1,208 @@ +/* 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/. */ + +/* + * Test return receipt (MDN) stuff. + */ + +"use strict"; + +var { + add_message_to_folder, + assert_selected_and_displayed, + be_in_folder, + create_folder, + create_message, + get_about_message, + mc, + select_click_row, +} = ChromeUtils.import( + "resource://testing-common/mozmill/FolderDisplayHelpers.jsm" +); +var { assert_notification_displayed } = ChromeUtils.import( + "resource://testing-common/mozmill/NotificationBoxHelpers.jsm" +); + +var folder; + +var kBoxId = "mail-notification-top"; +var kNotificationValue = "mdnRequested"; + +add_setup(async function () { + folder = await create_folder("ReturnReceiptTest"); + + // Create a message that requests a return receipt. + let msg0 = create_message({ + from: ["Ake", "ake@example.com"], + clobberHeaders: { "Disposition-Notification-To": "ake@example.com" }, + }); + await add_message_to_folder([folder], msg0); + + // ... and one that doesn't request a return receipt. + let msg1 = create_message(); + await add_message_to_folder([folder], msg1); + + // Create a message that requests a return receipt to a different address. + let msg2 = create_message({ + from: ["Mimi", "me@example.org"], + clobberHeaders: { "Disposition-Notification-To": "other@example.com" }, + }); + await add_message_to_folder([folder], msg2); + + // Create a message that requests a return receipt to different addresses. + let msg3 = create_message({ + from: ["Bobby", "bob@example.org"], + clobberHeaders: { + "Disposition-Notification-To": "ex1@example.com, ex2@example.com", + }, + }); + await add_message_to_folder([folder], msg3); + + // Create a message that requests a return receipt using non-standard header. + let msg4 = create_message({ + from: ["Ake", "ake@example.com"], + clobberHeaders: { "Return-Receipt-To": "ake@example.com" }, + }); + await add_message_to_folder([folder], msg4); + + // Create a message that requests a return receipt to a different address + // using non-standard header. + let msg5 = create_message({ + from: ["Mimi", "me@example.org"], + clobberHeaders: { "Return-Receipt-To": "other@example.com" }, + }); + await add_message_to_folder([folder], msg5); + + // Create a message that requests a return receipt to different addresses + // using non-standard header. + let msg6 = create_message({ + from: ["Bobby", "bob@example.org"], + clobberHeaders: { "Return-Receipt-To": "ex1@example.com, ex2@example.com" }, + }); + await add_message_to_folder([folder], msg6); + + await be_in_folder(folder); +}); + +/** Utility to select a message. */ +function gotoMsg(row) { + let curMessage = select_click_row(row); + assert_selected_and_displayed(mc, curMessage); +} + +/** + * Utility to make sure the MDN bar is shown / not shown. + */ +function assert_mdn_shown(shouldShow) { + assert_notification_displayed( + get_about_message(), + kBoxId, + kNotificationValue, + shouldShow + ); +} + +/** + * Utility function to make sure the notification contains a certain text. + */ +function assert_mdn_text_contains(text, shouldContain) { + let nb = get_about_message().document.getElementById(kBoxId); + let box = nb.querySelector(".notificationbox-stack")._notificationBox; + let notificationText = box.currentNotification.messageText.textContent; + if (shouldContain && !notificationText.includes(text)) { + throw new Error( + "mdnBar should contain text=" + + text + + "; notificationText=" + + notificationText + ); + } + if (!shouldContain && notificationText.includes(text)) { + throw new Error( + "mdnBar shouldn't contain text=" + + text + + "; notificationText=" + + notificationText + ); + } +} + +/** + * Test that return receipts are not shown when Disposition-Notification-To + * and Return-Receipt-To isn't set. + */ +add_task(function test_no_mdn_for_normal_msgs() { + gotoMsg(0); // TODO this shouldn't be needed but the selection goes to 0 on focus. + gotoMsg(1); // This message doesn't request a return receipt. + assert_mdn_shown(false); +}); + +/** + * Test that return receipts are shown when Disposition-Notification-To is set. + */ +add_task(function test_basic_mdn_shown() { + gotoMsg(0); // This message requests a return receipt. + assert_mdn_shown(true); + assert_mdn_text_contains("ake@example.com", false); // only name should show +}); + +/** + * Test that return receipts are shown when Return-Receipt-To is set. + */ +add_task(function test_basic_mdn_shown_nonrfc() { + gotoMsg(4); // This message requests a return receipt. + assert_mdn_shown(true); + assert_mdn_text_contains("ake@example.com", false); // only name should show +}); + +/** + * Test that return receipts warns when the mdn address is different. + * The RFC compliant version. + */ +add_task(function test_mdn_when_from_and_disposition_to_differs() { + gotoMsg(2); // Should display a notification with warning. + assert_mdn_shown(true); + assert_mdn_text_contains("other@example.com", true); // address should show +}); + +/** + * Test that return receipts warns when the mdn address is different. + * The RFC non-compliant version. + */ +add_task(function test_mdn_when_from_and_disposition_to_differs_nonrfc() { + gotoMsg(5); // Should display a notification with warning. + assert_mdn_shown(true); + assert_mdn_text_contains("other@example.com", true); // address should show +}); + +/** + * Test that return receipts warns when the mdn address consists of multiple + * addresses. + */ +add_task(function test_mdn_when_disposition_to_multi() { + gotoMsg(3); + // Should display a notification with warning listing all the addresses. + assert_mdn_shown(true); + assert_mdn_text_contains("ex1@example.com", true); + assert_mdn_text_contains("ex2@example.com", true); +}); + +/** + * Test that return receipts warns when the mdn address consists of multiple + * addresses. Non-RFC compliant version. + */ +add_task(function test_mdn_when_disposition_to_multi_nonrfc() { + gotoMsg(6); + // Should display a notification with warning listing all the addresses. + assert_mdn_shown(true); + assert_mdn_text_contains("ex1@example.com", true); + assert_mdn_text_contains("ex2@example.com", true); + + Assert.report( + false, + undefined, + undefined, + "Test ran to completion successfully" + ); +}); diff --git a/comm/mail/test/browser/message-header/data/evil-attached.eml b/comm/mail/test/browser/message-header/data/evil-attached.eml new file mode 100644 index 0000000000..24e3874ff4 --- /dev/null +++ b/comm/mail/test/browser/message-header/data/evil-attached.eml @@ -0,0 +1,22 @@ +Date: Mon, 10 Jan 2011 12:00:00 -0500
+From: phisher@evil.com
+To: user@example.com
+Subject: Please look at this attachment!
+Content-Type: multipart/mixed; boundary="BOUNDARY"
+
+This is a multi-part message in MIME format.
+--BOUNDARY
+Content-Type: text/html
+
+See below.
+--BOUNDARY
+Content-Type: message/rfc822
+
+Date: Mon, 10 Jan 2011 12:00:00 -0500
+From: phisher@evil.com
+To: user@example.com
+Subject: Please click on this link!
+Content-Type: text/html
+
+<form action="http://localhost/download-me"><input></form>
+--BOUNDARY--
diff --git a/comm/mail/test/browser/message-header/data/evil.eml b/comm/mail/test/browser/message-header/data/evil.eml new file mode 100644 index 0000000000..938ade296b --- /dev/null +++ b/comm/mail/test/browser/message-header/data/evil.eml @@ -0,0 +1,7 @@ +Date: Mon, 10 Jan 2011 12:00:00 -0500
+From: phisher@evil.com
+To: user@example.com
+Subject: Please click on this link!
+Content-Type: text/html
+
+<form action="http://localhost/download-me"><input></form>
diff --git a/comm/mail/test/browser/message-header/head.js b/comm/mail/test/browser/message-header/head.js new file mode 100644 index 0000000000..2a2c6e1b6a --- /dev/null +++ b/comm/mail/test/browser/message-header/head.js @@ -0,0 +1,190 @@ +/* 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/. */ + +var { MailConsts } = ChromeUtils.import("resource:///modules/MailConsts.jsm"); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); +var { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); + +registerCleanupFunction(() => { + let tabmail = document.getElementById("tabmail"); + is(tabmail.tabInfo.length, 1); + + while (tabmail.tabInfo.length > 1) { + tabmail.closeTab(tabmail.tabInfo[1]); + } + + // Some tests that open new windows don't return focus to the main window + // in a way that satisfies mochitest, and the test times out. + Services.focus.focusedWindow = window; + // Focus an element in the main window, then blur it again to avoid it + // hijacking keypresses. + let mainWindowElement = document.getElementById("button-appmenu"); + mainWindowElement.focus(); + mainWindowElement.blur(); + + Services.prefs.clearUserPref("mail.pane_config.dynamic"); + Services.xulStore.removeValue( + "chrome://messenger/content/messenger.xhtml", + "threadPane", + "view" + ); +}); + +function createAccount(type = "none") { + let account; + + if (type == "local") { + MailServices.accounts.createLocalMailAccount(); + account = MailServices.accounts.FindAccountForServer( + MailServices.accounts.localFoldersServer + ); + } else { + account = MailServices.accounts.createAccount(); + account.incomingServer = MailServices.accounts.createIncomingServer( + `${account.key}user`, + "localhost", + type + ); + } + + info(`Created account ${account.toString()}`); + return account; +} + +async function createSubfolder(parent, name) { + parent.createSubfolder(name, null); + return parent.getChildNamed(name); +} + +function createMessages(folder, makeMessagesArg) { + if (typeof makeMessagesArg == "number") { + makeMessagesArg = { count: makeMessagesArg }; + } + if (!createMessages.messageGenerator) { + createMessages.messageGenerator = new MessageGenerator(); + } + + let messages = createMessages.messageGenerator.makeMessages(makeMessagesArg); + let messageStrings = messages.map(message => message.toMboxString()); + folder.QueryInterface(Ci.nsIMsgLocalMailFolder); + folder.addMessageBatch(messageStrings); +} + +async function openMessageInTab(msgHdr) { + if (!msgHdr.QueryInterface(Ci.nsIMsgDBHdr)) { + throw new Error("No message passed to openMessageInTab"); + } + + // Ensure the behaviour pref is set to open a new tab. It is the default, + // but you never know. + let oldPrefValue = Services.prefs.getIntPref("mail.openMessageBehavior"); + Services.prefs.setIntPref( + "mail.openMessageBehavior", + MailConsts.OpenMessageBehavior.NEW_TAB + ); + MailUtils.displayMessages([msgHdr]); + Services.prefs.setIntPref("mail.openMessageBehavior", oldPrefValue); + + let win = Services.wm.getMostRecentWindow("mail:3pane"); + let tab = win.document.getElementById("tabmail").currentTabInfo; + let browser = tab.browser; + + await promiseMessageLoaded(browser, msgHdr); + return tab; +} + +async function openMessageInWindow(msgHdr) { + if (!msgHdr.QueryInterface(Ci.nsIMsgDBHdr)) { + throw new Error("No message passed to openMessageInWindow"); + } + + let messageWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded( + undefined, + async win => + win.document.documentURI == + "chrome://messenger/content/messageWindow.xhtml" + ); + MailUtils.openMessageInNewWindow(msgHdr); + + let messageWindow = await messageWindowPromise; + let browser = messageWindow.document.getElementById("messagepane"); + + await promiseMessageLoaded(browser, msgHdr); + return messageWindow; +} + +async function promiseMessageLoaded(browser, msgHdr) { + let messageURI = msgHdr.folder.getUriForMsg(msgHdr); + messageURI = MailServices.messageServiceFromURI(messageURI).getUrlForUri( + messageURI, + null + ); + + if ( + browser.webProgress?.isLoadingDocument || + !browser.currentURI?.equals(messageURI) + ) { + await BrowserTestUtils.browserLoaded( + browser, + null, + uri => uri == messageURI.spec + ); + } +} + +async function assertVisibility(element, isVisible, msg) { + await TestUtils.waitForCondition( + () => BrowserTestUtils.is_visible(element) == isVisible, + `The ${element.id} should be ${isVisible ? "visible" : "hidden"}: ${msg}` + ); +} + +/** + * Helper method to switch to a cards view with vertical layout. + */ +async function ensure_cards_view() { + const { threadTree, threadPane } = + document.getElementById("tabmail").currentAbout3Pane; + + Services.prefs.setIntPref("mail.pane_config.dynamic", 2); + Services.xulStore.setValue( + "chrome://messenger/content/messenger.xhtml", + "threadPane", + "view", + "cards" + ); + threadPane.updateThreadView("cards"); + + await BrowserTestUtils.waitForCondition( + () => threadTree.getAttribute("rows") == "thread-card", + "The tree view switched to a cards layout" + ); +} + +/** + * Helper method to switch to a table view with classic layout. + */ +async function ensure_table_view() { + const { threadTree, threadPane } = + document.getElementById("tabmail").currentAbout3Pane; + + Services.prefs.setIntPref("mail.pane_config.dynamic", 0); + Services.xulStore.setValue( + "chrome://messenger/content/messenger.xhtml", + "threadPane", + "view", + "table" + ); + threadPane.updateThreadView("table"); + + await BrowserTestUtils.waitForCondition( + () => threadTree.getAttribute("rows") == "thread-row", + "The tree view switched to a table layout" + ); +} |