summaryrefslogtreecommitdiffstats
path: root/comm/mail/test/browser/composition/browser_focus.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/test/browser/composition/browser_focus.js')
-rw-r--r--comm/mail/test/browser/composition/browser_focus.js523
1 files changed, 523 insertions, 0 deletions
diff --git a/comm/mail/test/browser/composition/browser_focus.js b/comm/mail/test/browser/composition/browser_focus.js
new file mode 100644
index 0000000000..852f2e99cc
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_focus.js
@@ -0,0 +1,523 @@
+/* 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 cycling through the focus of the 3pane's panes works correctly.
+ */
+
+"use strict";
+
+var { add_attachments, close_compose_window, open_compose_new_mail } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var { mc } = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+requestLongerTimeout(3);
+
+/**
+ * Test the cycling of focus in the composition window through (Shift+)F6.
+ *
+ * @param {MozMillController} controller - Controller for the compose window.
+ * @param {object} options - Options to set for the test.
+ * @param {boolean} options.useTab - Whether to use Ctrl+Tab instead of F6.
+ * @param {boolean} options.attachment - Whether to add an attachment.
+ * @param {boolean} options.notifications - Whether to show notifications.
+ * @param {boolean} options.languageButton - Whether to show the language
+ * menu button.
+ * @param {boolean} options.contacts - Whether to show the contacts side pane.
+ * @param {string} otherHeader - The name of the custom header to show.
+ */
+async function checkFocusCycling(controller, options) {
+ let win = controller.window;
+ let doc = win.document;
+ let contactDoc;
+ let contactsInput;
+ let identityElement = doc.getElementById("msgIdentity");
+ let bccButton = doc.getElementById("addr_bccShowAddressRowButton");
+ let toInput = doc.getElementById("toAddrInput");
+ let bccInput = doc.getElementById("bccAddrInput");
+ let subjectInput = doc.getElementById("msgSubject");
+ let editorElement = doc.getElementById("messageEditor");
+ let attachmentElement = doc.getElementById("attachmentBucket");
+ let extraMenuButton = doc.getElementById("extraAddressRowsMenuButton");
+ let languageButton = doc.getElementById("languageStatusButton");
+ let firstNotification;
+ let secondNotification;
+
+ if (Services.ww.activeWindow != win) {
+ // Wait for the window to be in focus before beginning.
+ await BrowserTestUtils.waitForEvent(win, "activate");
+ }
+
+ let key = options.useTab ? "VK_TAB" : "VK_F6";
+ let goForward = () =>
+ EventUtils.synthesizeKey(key, { ctrlKey: options.useTab }, win);
+ let goBackward = () =>
+ EventUtils.synthesizeKey(
+ key,
+ { ctrlKey: options.useTab, shiftKey: true },
+ win
+ );
+
+ if (options.attachment) {
+ add_attachments(controller, "https://www.mozilla.org/");
+ }
+
+ if (options.contacts) {
+ // Open the contacts sidebar.
+ EventUtils.synthesizeKey("VK_F9", {}, win);
+ contactsInput = await TestUtils.waitForCondition(() => {
+ contactDoc = doc.getElementById("contactsBrowser").contentDocument;
+ return contactDoc.getElementById("peopleSearchInput");
+ }, "Waiting for the contacts pane to load");
+ }
+
+ if (options.languageButton) {
+ // languageButton only shows if we have more than one dictionary, but we
+ // will show it anyway.
+ languageButton.hidden = false;
+ }
+
+ // Show the bcc row by clicking the button.
+ EventUtils.synthesizeMouseAtCenter(bccButton, {}, win);
+
+ // Show the custom row.
+ let otherRow = doc.querySelector(
+ `.address-row[data-recipienttype="${options.otherHeader}"]`
+ );
+ // Show the input.
+ let menu = doc.getElementById("extraAddressRowsMenu");
+ let promise = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(extraMenuButton, {}, win);
+ await promise;
+ promise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ menu.activateItem(doc.getElementById(otherRow.dataset.showSelfMenuitem));
+ await promise;
+ let otherHeaderInput = otherRow.querySelector(".address-row-input");
+
+ // Move the initial focus back to the To input.
+ toInput.focus();
+
+ if (options.notifications) {
+ // Exceed the recipient threshold.
+ Assert.equal(
+ win.gComposeNotification.allNotifications.length,
+ 0,
+ "Should be no initial notifications"
+ );
+ let notificationPromise = TestUtils.waitForCondition(
+ () => win.gComposeNotification.allNotifications[0],
+ "First notification shown"
+ );
+ EventUtils.sendString("a@b.org,c@d.org", win);
+ EventUtils.synthesizeKey("KEY_Enter", {}, win);
+ firstNotification = await notificationPromise;
+ }
+
+ // We start on the addressing widget and go from there.
+
+ // From To to Subject.
+ goForward();
+ Assert.ok(bccInput.matches(":focus"), "forward to bcc row");
+ goForward();
+ Assert.ok(otherHeaderInput.matches(":focus"), "forward to other row");
+ goForward();
+ Assert.ok(subjectInput.matches(":focus"), "forward to subject");
+
+ if (options.notifications && !options.attachment) {
+ // Include an attachment key word in the subject.
+ let notificationPromise = TestUtils.waitForCondition(() => {
+ let notifications = win.gComposeNotification.allNotifications;
+ if (notifications.length != 2) {
+ return null;
+ }
+ return notifications[1];
+ }, "Second notification shown");
+ EventUtils.sendString("My attached file", win);
+ secondNotification = await notificationPromise;
+ Assert.notEqual(
+ firstNotification,
+ secondNotification,
+ "New notification shown second"
+ );
+ }
+
+ // From Subject to Message Body.
+ goForward();
+ // The editor's body will not match ":focus", even when it has focus, instead,
+ // we use the parent window's activeElement.
+ Assert.equal(editorElement, doc.activeElement, "forward to message body");
+
+ // From Message Body to Attachment bucket if visible.
+ goForward();
+ if (options.attachment) {
+ Assert.ok(attachmentElement.matches(":focus"), "forward to attachments");
+ goForward();
+ }
+
+ if (options.notifications) {
+ Assert.equal(
+ firstNotification,
+ doc.activeElement,
+ "forward to notification"
+ );
+ goForward();
+ }
+
+ // From Message Body (or Attachment bucket) to Language button.
+ if (options.languageButton) {
+ Assert.ok(languageButton.matches(":focus"), "forward to status bar");
+ goForward();
+ }
+
+ // From Language button to contacts pane.
+ if (options.contacts) {
+ Assert.ok(
+ contactsInput.matches(":focus-within"),
+ "forward to contacts pane"
+ );
+ goForward();
+ }
+
+ // From contacts pane to identity.
+ Assert.ok(identityElement.matches(":focus"), "forward to 'from' row");
+
+ // Back to the To input.
+ goForward();
+ Assert.ok(toInput.matches(":focus"), "forward to 'to' row");
+
+ // Reverse the direction.
+
+ goBackward();
+ Assert.ok(identityElement.matches(":focus"), "backward to 'from' row");
+
+ goBackward();
+ if (options.contacts) {
+ Assert.ok(
+ contactsInput.matches(":focus-within"),
+ "backward to contacts pane"
+ );
+ goBackward();
+ }
+
+ if (options.languageButton) {
+ Assert.ok(languageButton.matches(":focus"), "backward to status bar");
+ goBackward();
+ }
+
+ if (options.notifications) {
+ Assert.equal(
+ firstNotification,
+ doc.activeElement,
+ "backward to notification"
+ );
+ goBackward();
+ }
+
+ if (options.attachment) {
+ Assert.ok(attachmentElement.matches(":focus"), "backward to attachments");
+ goBackward();
+ }
+
+ Assert.equal(editorElement, doc.activeElement, "backward to message body");
+ goBackward();
+ Assert.ok(subjectInput.matches(":focus"), "backward to subject");
+ goBackward();
+ Assert.ok(otherHeaderInput.matches(":focus"), "backward to other row");
+ goBackward();
+ Assert.ok(bccInput.matches(":focus"), "backward to bcc row");
+ goBackward();
+
+ Assert.ok(toInput.matches(":focus"), "backward to 'to' row");
+
+ // Now test some other elements that aren't the main focus point of their
+ // areas. I.e. focusable elements that are within an area, but are not
+ // focused when the area is *entered* through F6 or Ctrl+Tab. When these
+ // elements have focus, we still want F6 or Ctrl+Tab to move the focus to the
+ // neighbouring area.
+
+ // Focus the close button.
+ let bccCloseButton = doc.querySelector("#addressRowBcc .remove-field-button");
+ bccCloseButton.focus();
+ goForward();
+ Assert.ok(
+ otherHeaderInput.matches(":focus"),
+ "from close bcc button to other row"
+ );
+ goBackward();
+ // The input is focused on return.
+ Assert.ok(bccInput.matches(":focus"), "back to bcc row");
+ // Same the other way.
+ bccCloseButton.focus();
+ goBackward();
+ Assert.ok(toInput.matches(":focus"), "from close bcc button to 'to' row");
+
+ if (options.contacts) {
+ let addressBookList = contactDoc.getElementById("addressbookList");
+ addressBookList.focus();
+ goForward();
+ Assert.ok(
+ identityElement.matches(":focus"),
+ "from addressbook selector to 'from' row"
+ );
+ goBackward();
+ // The input is focused on return.
+ Assert.ok(contactsInput.matches(":focus-within"), "back to contacts input");
+ // Same the other way.
+ addressBookList.focus();
+ goBackward();
+ if (options.languageButton) {
+ Assert.ok(
+ languageButton.matches(":focus"),
+ "from addressbook selector to status bar"
+ );
+ } else if (options.notifications) {
+ Assert.equal(
+ firstNotification,
+ doc.activeElement,
+ "from addressbook selector to notification"
+ );
+ } else if (options.attachment) {
+ Assert.ok(
+ attachmentElement.matches(":focus"),
+ "from addressbook selector to attachments"
+ );
+ } else {
+ Assert.equal(
+ editorElement,
+ doc.activeElement,
+ "from addressbook selector to message body"
+ );
+ }
+ }
+
+ // Cc button and extra address rows menu button are in the same area as the
+ // message identity.
+ let ccButton = doc.getElementById("addr_ccShowAddressRowButton");
+ ccButton.focus();
+ goBackward();
+ if (options.contacts) {
+ Assert.ok(
+ contactsInput.matches(":focus-within"),
+ "from Cc button to contacts"
+ );
+ } else if (options.languageButton) {
+ Assert.ok(languageButton.matches(":focus"), "from Cc button to status bar");
+ } else if (options.notifications) {
+ Assert.equal(
+ firstNotification,
+ doc.activeElement,
+ "from Cc button to notification"
+ );
+ } else if (options.attachment) {
+ Assert.ok(
+ attachmentElement.matches(":focus"),
+ "from Cc button to attachments"
+ );
+ } else {
+ Assert.equal(
+ editorElement,
+ doc.activeElement,
+ "from Cc button to message body"
+ );
+ }
+ goForward();
+ // Return to the input.
+ Assert.ok(identityElement.matches(":focus"), "back to 'from' row");
+
+ // Try in the other direction with the extra menu button.
+ extraMenuButton.focus();
+ goForward();
+ Assert.ok(toInput.matches(":focus"), "from extra menu button to 'to' row");
+ goBackward();
+ // Return to the input.
+ Assert.ok(identityElement.matches(":focus"), "back to 'from' row again");
+
+ if (options.attachment) {
+ let attachmentArea = doc.getElementById("attachmentArea");
+ let attachmentSummary = attachmentArea.querySelector("summary");
+ Assert.ok(attachmentArea.open, "Attachment area should be open");
+ for (let open of [true, false]) {
+ if (open) {
+ Assert.ok(attachmentArea.open, "Attachment area should be open");
+ } else {
+ // Close the attachment bucket. In this case, the focus will move to the
+ // summary element (where the bucket can be shown again).
+ EventUtils.synthesizeMouseAtCenter(attachmentSummary, {}, win);
+ Assert.ok(!attachmentArea.open, "Attachment area should be closed");
+ }
+
+ // Focus the attachmentSummary.
+ attachmentSummary.focus();
+ goBackward();
+ Assert.equal(
+ editorElement,
+ doc.activeElement,
+ `backward from attachment summary (open: ${open}) to message body`
+ );
+ goForward();
+ if (open) {
+ // Focus returns to the bucket when it is open.
+ Assert.ok(
+ attachmentElement.matches(":focus"),
+ "forward to attachment bucket"
+ );
+ } else {
+ // Otherwise, it returns to the summary.
+ Assert.ok(
+ attachmentSummary.matches(":focus"),
+ "forward to attachment summary"
+ );
+ }
+ // Try reverse.
+ attachmentSummary.focus();
+ goForward();
+ if (options.notifications) {
+ Assert.equal(
+ firstNotification,
+ doc.activeElement,
+ `forward from attachment summary (open: ${open}) to notification`
+ );
+ } else if (options.languageButton) {
+ Assert.ok(
+ languageButton.matches(":focus"),
+ `forward from attachment summary (open: ${open}) to status bar`
+ );
+ } else if (options.contacts) {
+ Assert.ok(
+ contactsInput.matches(":focus-within"),
+ `forward from attachment summary (open: ${open}) to contacts pane`
+ );
+ } else {
+ Assert.ok(
+ identityElement.matches(":focus"),
+ `forward from attachment summary (open: ${open}) to 'from' row`
+ );
+ }
+ goBackward();
+ if (open) {
+ Assert.ok(
+ attachmentElement.matches(":focus"),
+ "return to attachment bucket"
+ );
+ } else {
+ Assert.ok(
+ attachmentSummary.matches(":focus"),
+ "return to attachment summary"
+ );
+ // Open again.
+ EventUtils.synthesizeMouseAtCenter(attachmentSummary, {}, win);
+ Assert.ok(attachmentArea.open, "Attachment area should be open again");
+ }
+ }
+ }
+
+ if (options.notifications) {
+ // Focus inside the notification.
+ let closeButton = (secondNotification || firstNotification).closeButton;
+ closeButton.focus();
+
+ goBackward();
+
+ if (options.attachment) {
+ Assert.ok(
+ attachmentElement.matches(":focus"),
+ "backward from notification button to attachments"
+ );
+ } else {
+ Assert.equal(
+ editorElement,
+ doc.activeElement,
+ "backward from notification button to message body"
+ );
+ }
+ goForward();
+ // Go to the first notification.
+ Assert.equal(
+ firstNotification,
+ doc.activeElement,
+ "forward to the first notification"
+ );
+
+ // Try reverse.
+ closeButton.focus();
+ goForward();
+ if (options.languageButton) {
+ Assert.ok(
+ languageButton.matches(":focus"),
+ "forward from notification button to status bar"
+ );
+ } else if (options.contacts) {
+ Assert.ok(
+ contactsInput.matches(":focus-within"),
+ "forward from notification button to contacts pane"
+ );
+ } else {
+ Assert.ok(
+ identityElement.matches(":focus"),
+ "forward from notification button to 'from' row"
+ );
+ }
+ goBackward();
+ Assert.equal(
+ firstNotification,
+ doc.activeElement,
+ "return to the first notification"
+ );
+ }
+
+ // Contacts pane is persistent, so we close it again.
+ if (options.contacts) {
+ // Close the contacts sidebar.
+ EventUtils.synthesizeKey("VK_F9", {}, win);
+ }
+}
+
+add_task(async function test_jump_focus() {
+ // Make sure the accessibility tabfocus is set to 7 to enable normal Tab
+ // focus on non-input field elements. This is necessary only for macOS as
+ // the default value is 2 instead of the default 7 used on Windows and Linux.
+ Services.prefs.setIntPref("accessibility.tabfocus", 7);
+ let prevHeader = Services.prefs.getCharPref("mail.compose.other.header");
+ let prevThreshold = Services.prefs.getIntPref(
+ "mail.compose.warn_public_recipients.threshold"
+ );
+ // Set two custom headers, but only one is shown.
+ Services.prefs.setCharPref(
+ "mail.compose.other.header",
+ "X-Header2,X-Header1"
+ );
+ Services.prefs.setIntPref("mail.compose.warn_public_recipients.threshold", 2);
+ for (let useTab of [false, true]) {
+ for (let attachment of [false, true]) {
+ for (let notifications of [false, true]) {
+ for (let languageButton of [false, true]) {
+ for (let contacts of [false, true]) {
+ let options = {
+ useTab,
+ attachment,
+ notifications,
+ languageButton,
+ contacts,
+ otherHeader: "X-Header1",
+ };
+ info(`Test run: ${JSON.stringify(options)}`);
+ let controller = open_compose_new_mail();
+ await checkFocusCycling(controller, options);
+ close_compose_window(controller);
+ }
+ }
+ }
+ }
+ }
+
+ // Reset the preferences.
+ Services.prefs.clearUserPref("accessibility.tabfocus");
+ Services.prefs.setCharPref("mail.compose.other.header", prevHeader);
+ Services.prefs.setIntPref(
+ "mail.compose.warn_public_recipients.threshold",
+ prevThreshold
+ );
+});