diff options
Diffstat (limited to 'comm/mail/test/browser/composition/browser_addressWidgets.js')
-rw-r--r-- | comm/mail/test/browser/composition/browser_addressWidgets.js | 773 |
1 files changed, 773 insertions, 0 deletions
diff --git a/comm/mail/test/browser/composition/browser_addressWidgets.js b/comm/mail/test/browser/composition/browser_addressWidgets.js new file mode 100644 index 0000000000..4e5db02a72 --- /dev/null +++ b/comm/mail/test/browser/composition/browser_addressWidgets.js @@ -0,0 +1,773 @@ +/* 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 proper enabling of addressing widgets. + */ + +"use strict"; + +var { click_menus_in_sequence } = ChromeUtils.import( + "resource://testing-common/mozmill/WindowHelpers.jsm" +); + +var { close_compose_window, open_compose_new_mail } = ChromeUtils.import( + "resource://testing-common/mozmill/ComposeHelpers.jsm" +); +var { be_in_folder, FAKE_SERVER_HOSTNAME } = ChromeUtils.import( + "resource://testing-common/mozmill/FolderDisplayHelpers.jsm" +); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var cwc = null; // compose window controller +var accountPOP3 = null; +var accountNNTP = null; +var originalAccountCount; + +add_setup(function () { + // Ensure we're in the tinderbox account as that has the right identities set + // up for this test. + let server = MailServices.accounts.findServer( + "tinderbox", + FAKE_SERVER_HOSTNAME, + "pop3" + ); + accountPOP3 = MailServices.accounts.FindAccountForServer(server); + + // There may be pre-existing accounts from other tests. + originalAccountCount = MailServices.accounts.allServers.length; +}); + +/** + * Check if the address type items are in the wished state. + * + * @param {Window} win - The window to search in. + * @param {string[]} itemsEnabled - List of item values that should be visible. + */ +function check_address_types_state(win, itemsEnabled) { + for (let item of win.document.querySelectorAll( + "#extraAddressRowsMenu > menuitem" + )) { + let buttonId = item.dataset.buttonId; + let showRowEl; + if (buttonId) { + let button = win.document.getElementById(buttonId); + if (item.dataset.preferButton == "true") { + showRowEl = button; + Assert.ok(item.hidden, `${item.id} menuitem should be hidden`); + } else { + showRowEl = item; + Assert.ok(button.hidden, `${button.id} button should be hidden`); + } + } else { + showRowEl = item; + } + + let type = item.id.replace(/ShowAddressRowMenuItem$/, ""); + + let expectShown = itemsEnabled.includes(type); + let row = win.document.querySelector( + `.address-row[data-recipienttype="${type}"]` + ); + if (expectShown) { + // Either the row or the element that shows it should be visible, but not + // both. + if (row.classList.contains("hidden")) { + Assert.ok( + !showRowEl.hidden, + `${showRowEl.id} should be visible when the row is hidden` + ); + } else { + Assert.ok( + showRowEl.hidden, + `${showRowEl.id} should be hidden when the row is visible` + ); + } + } else { + // Both the row and the element that shows it should be hidden. + Assert.ok(row.classList.contains("hidden"), `${row.id} should be hidden`); + Assert.ok(showRowEl.hidden, `${showRowEl.id} should be hidden`); + } + } +} + +/** + * With only a POP3 account, no News related address types should be enabled. + */ +function check_mail_address_types(win) { + check_address_types_state(win, [ + "addr_to", + "addr_cc", + "addr_reply", + "addr_bcc", + ]); +} + +/** + * With a NNTP account, all address types should be enabled. + */ +function check_nntp_address_types(win) { + check_address_types_state(win, [ + "addr_to", + "addr_cc", + "addr_reply", + "addr_bcc", + "addr_newsgroups", + "addr_followup", + ]); +} + +/** + * With an NNTP account, the 'To' addressing row should be hidden. + */ +function check_collapsed_pop_recipient(cwc) { + Assert.ok( + cwc.window.document + .getElementById("addressRowTo") + .classList.contains("hidden") + ); +} + +function add_NNTP_account() { + // Create a NNTP server + let nntpServer = MailServices.accounts + .createIncomingServer(null, "example.nntp.invalid", "nntp") + .QueryInterface(Ci.nsINntpIncomingServer); + + let identity = MailServices.accounts.createIdentity(); + identity.email = "tinderbox2@example.invalid"; + + accountNNTP = MailServices.accounts.createAccount(); + accountNNTP.incomingServer = nntpServer; + accountNNTP.addIdentity(identity); + // Now there should be 1 more account. + Assert.equal( + MailServices.accounts.allServers.length, + originalAccountCount + 1 + ); +} + +function remove_NNTP_account() { + // Remove our NNTP account to leave the profile clean. + MailServices.accounts.removeAccount(accountNNTP); + // There should be only the original accounts left. + Assert.equal(MailServices.accounts.allServers.length, originalAccountCount); +} + +/** + * Bug 399446 & bug 922614 + * Test that the allowed address types depend on the account type + * we are sending from. + */ +add_task(async function test_address_types() { + // Be sure there is no NNTP account yet. + for (let account of MailServices.accounts.accounts) { + Assert.notEqual( + account.incomingServer.type, + "nntp", + "There is a NNTP account existing unexpectedly" + ); + } + + // Open compose window on the existing POP3 account. + await be_in_folder(accountPOP3.incomingServer.rootFolder); + cwc = open_compose_new_mail(); + check_mail_address_types(cwc.window); + close_compose_window(cwc); + + add_NNTP_account(); + + // From now on, we should always get all possible address types offered, + // regardless of which account is used of composing (bug 922614). + await be_in_folder(accountNNTP.incomingServer.rootFolder); + cwc = open_compose_new_mail(); + check_nntp_address_types(cwc.window); + check_collapsed_pop_recipient(cwc); + close_compose_window(cwc); + + // Now try the same accounts but choosing them in the From dropdown + // inside compose window. + await be_in_folder(accountPOP3.incomingServer.rootFolder); + cwc = open_compose_new_mail(); + check_nntp_address_types(cwc.window); + + let NNTPidentity = accountNNTP.defaultIdentity.key; + EventUtils.synthesizeMouseAtCenter( + cwc.window.document.getElementById("msgIdentity"), + {}, + cwc.window.document.getElementById("msgIdentity").ownerGlobal + ); + await click_menus_in_sequence( + cwc.window.document.getElementById("msgIdentityPopup"), + [{ identitykey: NNTPidentity }] + ); + check_nntp_address_types(cwc.window); + + // Switch back to the POP3 account. + let POP3identity = accountPOP3.defaultIdentity.key; + EventUtils.synthesizeMouseAtCenter( + cwc.window.document.getElementById("msgIdentity"), + {}, + cwc.window.document.getElementById("msgIdentity").ownerGlobal + ); + await click_menus_in_sequence( + cwc.window.document.getElementById("msgIdentityPopup"), + [{ identitykey: POP3identity }] + ); + check_nntp_address_types(cwc.window); + + close_compose_window(cwc); + + remove_NNTP_account(); + + // Now the NNTP account is lost, so we should be back to mail only addresses. + await be_in_folder(accountPOP3.incomingServer.rootFolder); + cwc = open_compose_new_mail(); + check_mail_address_types(cwc.window); + close_compose_window(cwc); +}); + +add_task(async function test_address_suppress_leading_comma_space() { + await be_in_folder(accountPOP3.incomingServer.rootFolder); + let controller = open_compose_new_mail(); + + let addrInput = controller.window.document.getElementById("toAddrInput"); + Assert.ok(addrInput); + Assert.equal(addrInput.value, ""); + + // Create a pill. + addrInput.value = "person@org"; + // Comma triggers the pill creation. + // Note: the address input should already have focus. + EventUtils.synthesizeKey(",", {}, controller.window); + + let addrPill = await TestUtils.waitForCondition( + () => + controller.window.document.querySelector( + "#toAddrContainer > .address-pill" + ), + "Pill creation" + ); + Assert.equal(addrInput.value, ""); + let pillInput = addrPill.querySelector("input"); + Assert.ok(pillInput); + + // Asserts that the input has the correct exceptional behaviour for 'comma' + // and 'space'. + async function assertKeyInput(input) { + // Since we will be partially testing for a lack of response to the " " and + // "," key presses, we first run the tests with the "a" key press to assure + // us that the tests would otherwise capture the normal behaviour. This will + // also shows us that the comma and space behaviour is exceptional. + for (let key of ["a", " ", ","]) { + // Clear input. + input.value = ""; + await TestUtils.waitForTick(); + + // Type the key in an empty input. + let eventPromise = BrowserTestUtils.waitForEvent(input, "keydown"); + EventUtils.synthesizeKey(key, {}, controller.window); + await eventPromise; + + if (key === " " || key === ",") { + // Key is suppressed, so the input remains empty. + Assert.equal(input.value, ""); + } else { + // Normal behaviour: key is added to the input. + Assert.equal(input.value, key); + } + + // If the input is not empty, we should still have the normal behaviour. + input.value = "z"; + input.selectionStart = 1; + input.SelectionEnd = 1; + await TestUtils.waitForTick(); + + await BrowserTestUtils.synthesizeKey( + key, + {}, + controller.window.browsingContext + ); + await new Promise(resolve => requestAnimationFrame(resolve)); + + Assert.equal(input.value, "z" + key); + + // Test typing the key to replace all the trimmed input. + // Sample text with two spaces as start and end. Also includes a 2 + // character emoji. + let someText = " some, text📜 "; + for (let selection of [ + { start: 0, end: 0 }, + { start: 1, end: 0 }, + { start: 0, end: 1 }, + { start: 2, end: 2 }, + ]) { + input.value = someText; + input.selectionStart = selection.start; + input.selectionEnd = someText.length - selection.end; + await TestUtils.waitForTick(); + + // Type the key to replace the text. + await BrowserTestUtils.synthesizeKey( + key, + {}, + controller.window.browsingContext + ); + await new Promise(resolve => requestAnimationFrame(resolve)); + + if (key === " " || key === ",") { + // Key is suppressed and input is empty. + Assert.equal(input.value, ""); + } else { + // Normal behaviour: key replaces the selected text. + Assert.equal( + input.value, + someText.slice(0, selection.start) + + key + + someText.slice(someText.length - selection.end) + ); + } + } + + // If we do not replace all the trimmed input, we should still have + // normal behaviour. + input.value = " text "; + input.selectionStart = 1; + // Select up to 'x'. + input.selectionEnd = 5; + await TestUtils.waitForTick(); + + await BrowserTestUtils.synthesizeKey( + key, + {}, + controller.window.browsingContext + ); + await new Promise(resolve => requestAnimationFrame(resolve)); + Assert.equal(input.value, " " + key + "t "); + } + } + + // Assert that the address input has the correct behaviour for key presses. + // Note: the address input should still have focus. + await assertKeyInput(addrInput); + + // Now test the behaviour when editing a pill. + // First, we need to get into editing mode by clicking the pill twice. + EventUtils.synthesizeMouseAtCenter( + addrPill, + { clickCount: 1 }, + controller.window + ); + let clickPromise = BrowserTestUtils.waitForEvent(addrPill, "click"); + // We do not want a double click, but two separate clicks. + EventUtils.synthesizeMouseAtCenter( + addrPill, + { clickCount: 1 }, + controller.window + ); + await clickPromise; + + Assert.ok(!pillInput.hidden); + + // Assert that editing a pill has the same behaviour as the address input. + await assertKeyInput(pillInput); + + close_compose_window(controller); +}); + +add_task(async function test_pill_creation_in_all_fields() { + await be_in_folder(accountPOP3.incomingServer.rootFolder); + let cwc = open_compose_new_mail(); + + let addresses = ["person@org", "foo@address.valid", "invalid", "foo@address"]; + let subjectField = cwc.window.document.getElementById("msgSubject"); + + // Helper method to create multiple pills in a field. + async function assertPillsCreationInField(input) { + Assert.ok(input); + Assert.equal(input.value, ""); + + // Write an address in the field. + input.value = addresses[0]; + // Enter triggers the pill creation. + EventUtils.synthesizeKey("VK_RETURN", {}, cwc.window); + // Assert the pill was created. + await TestUtils.waitForCondition( + () => + input + .closest(".address-container") + .querySelectorAll("mail-address-pill").length == 1, + "Pills created" + ); + // Assert the pill has the correct address. + Assert.equal( + input + .closest(".address-container") + .querySelectorAll("mail-address-pill")[0].emailAddress, + addresses[0] + ); + + // Write another address in the field. + input.value = addresses[1]; + // Tab triggers the pill creation. + EventUtils.synthesizeKey("VK_TAB", {}, cwc.window); + // Assert the pill was created. + await TestUtils.waitForCondition( + () => + input + .closest(".address-container") + .querySelectorAll("mail-address-pill").length == 2, + "Pills created" + ); + // Assert the pill has the correct address. + Assert.equal( + input + .closest(".address-container") + .querySelectorAll("mail-address-pill")[1].emailAddress, + addresses[1] + ); + + // Write an invalid email address in the To field. + input.value = addresses[2]; + // Enter triggers the pill creation. + EventUtils.synthesizeKey("VK_RETURN", {}, cwc.window); + // Assert that an invalid address pill was created. + await TestUtils.waitForCondition( + () => + input + .closest(".address-container") + .querySelectorAll("mail-address-pill.invalid-address").length == 1, + "Invalid pill created" + ); + // Assert the pill has the correct address. + Assert.equal( + input + .closest(".address-container") + .querySelector("mail-address-pill.invalid-address").emailAddress, + addresses[2] + ); + + // Write another address in the field. + input.value = addresses[3]; + // Focusing on another element triggers the pill creation. + subjectField.focus(); + // Assert the pill was created. + await TestUtils.waitForCondition( + () => + input + .closest(".address-container") + .querySelectorAll("mail-address-pill").length == 4, + "Pills created" + ); + // Assert the pill has the correct address. + Assert.equal( + input + .closest(".address-container") + .querySelectorAll("mail-address-pill")[3].emailAddress, + addresses[3] + ); + } + + // The To field is visible and focused by default when the compose window is + // first opened. + // Test pill creation for the To input field. + let toInput = cwc.window.document.getElementById("toAddrInput"); + await assertPillsCreationInField(toInput); + + // Click on the Cc recipient label. + let ccInput = cwc.window.document.getElementById("ccAddrInput"); + EventUtils.synthesizeMouseAtCenter( + cwc.window.document.getElementById("addr_ccShowAddressRowButton"), + {}, + cwc.window + ); + // The Cc field should now be visible. + Assert.ok( + !ccInput.closest(".address-row").classList.contains("hidden"), + "The Cc field is visible" + ); + // Test pill creation for the Cc input field. + await assertPillsCreationInField(ccInput); + + // Click on the Bcc recipient label. + let bccInput = cwc.window.document.getElementById("bccAddrInput"); + EventUtils.synthesizeMouseAtCenter( + cwc.window.document.getElementById("addr_bccShowAddressRowButton"), + {}, + cwc.window + ); + // The Bcc field should now be visible. + Assert.ok( + !bccInput.closest(".address-row").classList.contains("hidden"), + "The Bcc field is visible" + ); + // Test pill creation for the Bcc input field. + await assertPillsCreationInField(bccInput); + + // Focus on the Bcc field and hold press the Backspace key. + bccInput.focus(); + EventUtils.synthesizeKey("KEY_Backspace", { repeat: 5 }, cwc.window); + + // All pills should be deleted, but the focus should remain on the Bcc field. + Assert.equal( + bccInput.closest(".address-container").querySelectorAll("mail-address-pill") + .length, + 0, + "All pills in the Bcc field have been removed." + ); + Assert.ok( + !bccInput.closest(".address-row").classList.contains("hidden"), + "The Bcc field is still visible" + ); + + // Press and hold Backspace again. + EventUtils.synthesizeKey("KEY_Backspace", { repeat: 2 }, cwc.window); + + // Confirm the Bcc field is closed and the focus moved to the Cc field. + Assert.ok( + bccInput.closest(".address-row").classList.contains("hidden"), + "The Bcc field was closed" + ); + Assert.equal(cwc.window.document.activeElement, ccInput); + + // Now we're on the Cc field. Press and hold Backspace to delete all pills. + EventUtils.synthesizeKey("KEY_Backspace", { repeat: 5 }, cwc.window); + + // All pills should be deleted, but the focus should remain on the Cc field. + Assert.equal( + ccInput.closest(".address-container").querySelectorAll("mail-address-pill") + .length, + 0, + "All pills in the Cc field have been removed." + ); + Assert.ok( + !ccInput.closest(".address-row").classList.contains("hidden"), + "The Cc field is still visible" + ); + + // Press and hold Backspace again. + EventUtils.synthesizeKey("KEY_Backspace", { repeat: 2 }, cwc.window); + + // Confirm the Cc field is closed and the focus moved to the To field. + Assert.ok( + ccInput.closest(".address-row").classList.contains("hidden"), + "The Cc field was closed" + ); + Assert.equal(cwc.window.document.activeElement, toInput); + + // Now we're on the To field. Press and hold Backspace to delete all pills. + EventUtils.synthesizeKey("KEY_Backspace", { repeat: 5 }, cwc.window); + + // All pills should be deleted, but the focus should remain on the To field. + Assert.equal( + toInput.closest(".address-container").querySelectorAll("mail-address-pill") + .length, + 0, + "All pills in the To field have been removed." + ); + Assert.ok( + !toInput.closest(".address-row").classList.contains("hidden"), + "The To field is still visible" + ); + + // Press and hold Backspace again. + EventUtils.synthesizeKey("KEY_Backspace", { repeat: 2 }, cwc.window); + + // Long backspace keypress on the To field shouldn't do anything if the field + // is empty. Confirm the To field is still visible and the focus stays on the + // To field. + Assert.ok( + !toInput.closest(".address-row").classList.contains("hidden"), + "The To field is still visible" + ); + Assert.equal(cwc.window.document.activeElement, toInput); + + close_compose_window(cwc); +}); + +add_task(async function test_addressing_fields_shortcuts() { + await be_in_folder(accountPOP3.incomingServer.rootFolder); + let cwc = open_compose_new_mail(); + + let addrToInput = cwc.window.document.getElementById("toAddrInput"); + // The To input field should be empty. + Assert.equal(addrToInput.value, ""); + // The To input field should be the currently focused element. + Assert.equal(cwc.window.document.activeElement, addrToInput); + + const modifiers = + AppConstants.platform == "macosx" + ? { accelKey: true, shiftKey: true } + : { ctrlKey: true, shiftKey: true }; + + let addrCcInput = cwc.window.document.getElementById("ccAddrInput"); + let ccRowShownPromise = BrowserTestUtils.waitForCondition( + () => !addrCcInput.closest(".address-row").classList.contains("hidden"), + "The Cc addressing row is not visible." + ); + // Press the Ctrl/Cmd+Shift+C. + EventUtils.synthesizeKey("C", modifiers, cwc.window); + // The Cc addressing row should be visible. + await ccRowShownPromise; + // The Cc input field should be currently focused. + Assert.equal(cwc.window.document.activeElement, addrCcInput); + + let addrBccInput = cwc.window.document.getElementById("bccAddrInput"); + let bccRowShownPromise = BrowserTestUtils.waitForCondition( + () => !addrBccInput.closest(".address-row").classList.contains("hidden"), + "The Bcc addressing row is not visible." + ); + // Press the Ctrl/Cmd+Shift+B. + EventUtils.synthesizeKey("B", modifiers, cwc.window); + await bccRowShownPromise; + // The Bcc input field should be currently focused. + Assert.equal(cwc.window.document.activeElement, addrBccInput); + + // Press the Ctrl/Cmd+Shift+T. + EventUtils.synthesizeKey("T", modifiers, cwc.window); + // The To input field should be the currently focused element. + Assert.equal(cwc.window.document.activeElement, addrToInput); + + // Press the Ctrl/Cmd+Shift+C. + EventUtils.synthesizeKey("C", modifiers, cwc.window); + // The Cc input field should be currently focused. + Assert.equal(cwc.window.document.activeElement, addrCcInput); + + // Press the Ctrl/Cmd+Shift+B. + EventUtils.synthesizeKey("B", modifiers, cwc.window); + // The Bcc input field should be currently focused. + Assert.equal(cwc.window.document.activeElement, addrBccInput); + + close_compose_window(cwc); +}); + +add_task(async function test_pill_deletion_and_focus() { + await be_in_folder(accountPOP3.incomingServer.rootFolder); + let cwc = open_compose_new_mail(); + + // When the compose window is opened, the focus should be on the To field. + let toInput = cwc.window.document.getElementById("toAddrInput"); + Assert.equal(cwc.window.document.activeElement, toInput); + + const modifiers = + AppConstants.platform == "macosx" ? { accelKey: true } : { ctrlKey: true }; + const addresses = "person@org, foo@address.valid, invalid, foo@address"; + + // Test the To field. + test_deletion_and_focus_on_input(cwc, toInput, addresses, modifiers); + + // Reveal and test the Cc field. + EventUtils.synthesizeMouseAtCenter( + cwc.window.document.getElementById("addr_ccShowAddressRowButton"), + {}, + cwc.window + ); + test_deletion_and_focus_on_input( + cwc, + cwc.window.document.getElementById("ccAddrInput"), + addresses, + modifiers + ); + + // Reveal and test the Bcc field. + EventUtils.synthesizeMouseAtCenter( + cwc.window.document.getElementById("addr_bccShowAddressRowButton"), + {}, + cwc.window + ); + test_deletion_and_focus_on_input( + cwc, + cwc.window.document.getElementById("bccAddrInput"), + addresses, + modifiers + ); + + close_compose_window(cwc); +}); + +function test_deletion_and_focus_on_input(cwc, input, addresses, modifiers) { + // Focus on the input before adding anything to be sure keyboard shortcut are + // triggered from the right element. + input.focus(); + + // Fill the input field with a long of string of comma separated addresses. + input.value = addresses; + + // Enter triggers the pill creation. + EventUtils.synthesizeKey("VK_RETURN", {}, cwc.window); + + let container = input.closest(".address-container"); + // We should now have 4 pills. + Assert.equal( + container.querySelectorAll("mail-address-pill").length, + 4, + "All pills in the field have been created." + ); + + // One pill should be flagged as invalid. + Assert.equal( + container.querySelectorAll("mail-address-pill.invalid-address").length, + 1, + "One created pill is invalid." + ); + + // After pills creation, the same field should be still focused. + Assert.equal(cwc.window.document.activeElement, input); + + // Keypress left arrow should focus and select the last created pill. + EventUtils.synthesizeKey("KEY_ArrowLeft", {}, cwc.window); + Assert.equal( + container.querySelectorAll("mail-address-pill[selected]").length, + 1, + "One pill is currently selected." + ); + + // Pressing delete should delete the selected pill and move the focus back to + // the input. + EventUtils.synthesizeKey("KEY_Delete", {}, cwc.window); + Assert.equal( + container.querySelectorAll("mail-address-pill").length, + 3, + "One pill correctly deleted." + ); + Assert.equal(cwc.window.document.activeElement, input); + + // Keypress left arrow to select the last available pill. + EventUtils.synthesizeKey("KEY_ArrowLeft", {}, cwc.window); + Assert.equal( + container.querySelectorAll("mail-address-pill[selected]").length, + 1, + "One pill is currently selected." + ); + + // BackSpace should delete the pill and focus on the previous adjacent pill. + EventUtils.synthesizeKey("KEY_Backspace", {}, cwc.window); + Assert.equal( + container.querySelectorAll("mail-address-pill").length, + 2, + "One pill correctly deleted." + ); + let selectedPill = container.querySelector("mail-address-pill[selected]"); + Assert.equal(cwc.window.document.activeElement, selectedPill); + + // Pressing CTRL+A should select all pills. + EventUtils.synthesizeKey("a", modifiers, cwc.window); + Assert.equal( + container.querySelectorAll("mail-address-pill[selected]").length, + 2, + "All remaining 2 pills are currently selected." + ); + + // BackSpace should delete all pills and focus on empty inptu field. + EventUtils.synthesizeKey("KEY_Backspace", {}, cwc.window); + Assert.equal( + container.querySelectorAll("mail-address-pill").length, + 0, + "All pills have been deleted." + ); + Assert.equal(cwc.window.document.activeElement, input); +} |