diff options
Diffstat (limited to 'comm/mail/test/browser/message-header/browser_messageHeader.js')
-rw-r--r-- | comm/mail/test/browser/message-header/browser_messageHeader.js | 1237 |
1 files changed, 1237 insertions, 0 deletions
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(); +}); |