Bug 1414322 - Refactor sendKeysToElement methods. draft
authorHenrik Skupin <mail@hskupin.info>
Thu, 09 Nov 2017 20:39:51 +0100
changeset 705892 e0c60eb25a743c729554e046cb578677dd9e7ddd
parent 705891 73604ef2c7d64a2ba74f6455449636f95d1137de
child 705893 54ff545501bdcb8d3820596b0a0c5614722bde7b
push id91640
push userbmo:hskupin@gmail.com
push dateThu, 30 Nov 2017 22:25:18 +0000
bugs1414322
milestone59.0a1
Bug 1414322 - Refactor sendKeysToElement methods. Each call to sendKeysToElement should go through the interaction module, and never by directly calling event.sendKeysToElement. This will make sure that keyboard interactability checks will always be performed, even for chrome scope like alerts or modal dialogs. MozReview-Commit-ID: GoDKjMsNZsq
testing/marionette/driver.js
testing/marionette/element.js
testing/marionette/event.js
testing/marionette/interaction.js
testing/marionette/listener.js
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -24,31 +24,29 @@ Cu.import("chrome://marionette/content/c
 Cu.import("chrome://marionette/content/cert.js");
 Cu.import("chrome://marionette/content/cookie.js");
 const {
   ChromeWebElement,
   element,
   WebElement,
 } = Cu.import("chrome://marionette/content/element.js", {});
 const {
-  ElementNotInteractableError,
   InsecureCertificateError,
   InvalidArgumentError,
   InvalidCookieDomainError,
   InvalidSelectorError,
   NoAlertOpenError,
   NoSuchFrameError,
   NoSuchWindowError,
   SessionNotCreatedError,
   UnknownError,
   UnsupportedOperationError,
   WebDriverError,
 } = Cu.import("chrome://marionette/content/error.js", {});
 Cu.import("chrome://marionette/content/evaluate.js");
-Cu.import("chrome://marionette/content/event.js");
 Cu.import("chrome://marionette/content/interaction.js");
 Cu.import("chrome://marionette/content/l10n.js");
 Cu.import("chrome://marionette/content/legacyaction.js");
 Cu.import("chrome://marionette/content/modal.js");
 Cu.import("chrome://marionette/content/proxy.js");
 Cu.import("chrome://marionette/content/reftest.js");
 Cu.import("chrome://marionette/content/session.js");
 const {
@@ -2617,18 +2615,17 @@ GeckoDriver.prototype.sendKeysToElement 
 
   let id = assert.string(cmd.parameters.id);
   let text = assert.string(cmd.parameters.text);
   let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.Chrome:
       let el = this.curBrowser.seenEls.get(webEl);
-      await interaction.sendKeysToElement(
-          el, text, true, this.a11yChecks);
+      await interaction.sendKeysToElement(el, text, this.a11yChecks);
       break;
 
     case Context.Content:
       await this.listener.sendKeysToElement(webEl, text);
       break;
   }
 };
 
@@ -3274,32 +3271,24 @@ GeckoDriver.prototype.getTextFromDialog 
  * @throws {ElementNotInteractableError}
  *     If the current user prompt is an alert or confirm.
  * @throws {NoSuchAlertError}
  *     If there is no current user prompt.
  * @throws {UnsupportedOperationError}
  *     If the current user prompt is something other than an alert,
  *     confirm, or a prompt.
  */
-GeckoDriver.prototype.sendKeysToDialog = function(cmd) {
-  let win = assert.window(this.getCurrentWindow());
+GeckoDriver.prototype.sendKeysToDialog = async function(cmd) {
+  assert.window(this.getCurrentWindow());
   this._checkIfAlertIsPresent();
 
   // see toolkit/components/prompts/content/commonDialog.js
-  let {loginContainer, loginTextbox} = this.dialog.ui;
-  if (loginContainer.hidden) {
-    throw new ElementNotInteractableError(
-        "This prompt does not accept text input");
-  }
-
-  event.sendKeysToElement(
-      cmd.parameters.text,
-      loginTextbox,
-      {ignoreVisibility: true},
-      this.dialog.window ? this.dialog.window : win);
+  let {loginTextbox} = this.dialog.ui;
+  await interaction.sendKeysToElement(
+      loginTextbox, cmd.parameters.text, this.a11yChecks);
 };
 
 GeckoDriver.prototype._checkIfAlertIsPresent = function() {
   if (!this.dialog || !this.dialog.ui) {
     throw new NoAlertOpenError("No modal dialog is currently open");
   }
 };
 
--- a/testing/marionette/element.js
+++ b/testing/marionette/element.js
@@ -927,18 +927,17 @@ element.isInView = function(el) {
  *     the target's bounding box.
  *
  * @return {boolean}
  *     True if visible, false otherwise.
  */
 element.isVisible = function(el, x = undefined, y = undefined) {
   let win = el.ownerGlobal;
 
-  // Bug 1094246: webdriver's isShown doesn't work with content xul
-  if (!element.isXULElement(el) && !atom.isElementDisplayed(el, win)) {
+  if (!atom.isElementDisplayed(el, win)) {
     return false;
   }
 
   if (el.tagName.toLowerCase() == "body") {
     return true;
   }
 
   if (!element.inViewport(el, x, y)) {
--- a/testing/marionette/event.js
+++ b/testing/marionette/event.js
@@ -6,18 +6,16 @@
 this.event = {};
 
 "use strict";
 /* global content, is */
 
 const {interfaces: Ci, utils: Cu, classes: Cc} = Components;
 
 Cu.import("chrome://marionette/content/element.js");
-const {ElementNotInteractableError} =
-    Cu.import("chrome://marionette/content/error.js", {});
 
 const dblclickTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 
 //  Max interval between two clicks that should result in a dblclick (in ms)
 const DBLCLICK_INTERVAL = 640;
 
 this.EXPORTED_SYMBOLS = ["event"];
 
@@ -1344,58 +1342,30 @@ event.sendSingleKey = function(keyToSend
     modifiers[modName] = !modifiers[modName];
   } else if (modifiers.shiftKey && keyName != "Shift") {
     keyName = keyName.toUpperCase();
   }
   event.synthesizeKey(keyName, modifiers, window);
 };
 
 /**
- * Focus element and, if a textual input field and no previous selection
- * state exists, move the caret to the end of the input field.
- *
- * @param {Element} element
- *     Element to focus.
- */
-function focusElement(element) {
-  let t = element.type;
-  if (t && (t == "text" || t == "textarea")) {
-    if (element.selectionEnd == 0) {
-      let len = element.value.length;
-      element.setSelectionRange(len, len);
-    }
-  }
-  element.focus();
-}
-
-/**
  * @param {string} keyString
  * @param {Element} element
- * @param {Object.<string, boolean>=} opts
  * @param {Window=} window
  */
-event.sendKeysToElement = function(
-    keyString, el, opts = {}, window = undefined) {
-
-  if (opts.ignoreVisibility || element.isVisible(el)) {
-    focusElement(el);
+event.sendKeysToElement = function(keyString, el, window = undefined) {
+  // make Object.<modifier, false> map
+  let modifiers = Object.create(event.Modifiers);
+  for (let modifier in event.Modifiers) {
+    modifiers[modifier] = false;
+  }
 
-    // make Object.<modifier, false> map
-    let modifiers = Object.create(event.Modifiers);
-    for (let modifier in event.Modifiers) {
-      modifiers[modifier] = false;
-    }
-
-    for (let i = 0; i < keyString.length; i++) {
-      let c = keyString.charAt(i);
-      event.sendSingleKey(c, modifiers, window);
-    }
-
-  } else {
-    throw new ElementNotInteractableError("Element is not visible");
+  for (let i = 0; i < keyString.length; i++) {
+    let c = keyString.charAt(i);
+    event.sendSingleKey(c, modifiers, window);
   }
 };
 
 event.sendEvent = function(eventType, el, modifiers = {}, opts = {}) {
   opts.canBubble = opts.canBubble || true;
 
   let doc = el.ownerDocument || el.document;
   let ev = doc.createEvent("Event");
--- a/testing/marionette/interaction.js
+++ b/testing/marionette/interaction.js
@@ -1,16 +1,18 @@
 /* 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/. */
 
 "use strict";
 
 const {utils: Cu} = Components;
 
+Cu.import("resource://gre/modules/Preferences.jsm");
+
 Cu.import("chrome://marionette/content/accessibility.js");
 Cu.import("chrome://marionette/content/atom.js");
 Cu.import("chrome://marionette/content/element.js");
 const {
   ElementClickInterceptedError,
   ElementNotInteractableError,
   InvalidArgumentError,
   InvalidElementStateError,
@@ -323,16 +325,34 @@ interaction.flushEventLoop = async funct
     el.removeEventListener("click", clickEv);
   };
 
   return new TimedPromise(spinEventLoop, {timeout: 500, throws: null})
       .then(removeListeners);
 };
 
 /**
+ * Focus element and, if a textual input field and no previous selection
+ * state exists, move the caret to the end of the input field.
+ *
+ * @param {Element} element
+ *     Element to focus.
+ */
+interaction.focusElement = function(el) {
+  let t = el.type;
+  if (t && (t == "text" || t == "textarea")) {
+    if (el.selectionEnd == 0) {
+      let len = el.value.length;
+      el.setSelectionRange(len, len);
+    }
+  }
+  el.focus();
+};
+
+/**
  * Appends <var>path</var> to an <tt>&lt;input type=file&gt;</tt>'s
  * file list.
  *
  * @param {HTMLInputElement} el
  *     An <tt>&lt;input type=file&gt;</tt> element.
  * @param {string} path
  *     Full path to file.
  *
@@ -393,28 +413,45 @@ interaction.setFormControlValue = functi
 
 /**
  * Send keys to element.
  *
  * @param {DOMElement|XULElement} el
  *     Element to send key events to.
  * @param {Array.<string>} value
  *     Sequence of keystrokes to send to the element.
- * @param {boolean} ignoreVisibility
- *     Flag to enable or disable element visibility tests.
  * @param {boolean=} [strict=false] strict
  *     Enforce strict accessibility tests.
  */
 interaction.sendKeysToElement = async function(
-    el, value, ignoreVisibility, strict = false) {
-  let win = getWindow(el);
-  let a11y = accessibility.get(strict);
-  let acc = await a11y.getAccessible(el, true);
-  a11y.assertActionable(acc, el);
-  event.sendKeysToElement(value, el, {ignoreVisibility: false}, win);
+    el, value, strict = false) {
+  const a11y = accessibility.get(strict);
+  const win = getWindow(el);
+
+  if (el.type == "file") {
+    await interaction.uploadFile(el, value);
+  } else if ((el.type == "date" || el.type == "time") &&
+      Preferences.get("dom.forms.datetime")) {
+    interaction.setFormControlValue(el, value);
+  } else {
+    let visibilityCheckEl  = el;
+    if (el.localName == "option") {
+      visibilityCheckEl = element.getContainer(el);
+    }
+
+    if (!element.isVisible(visibilityCheckEl)) {
+      throw new ElementNotInteractableError("Element is not visible");
+    }
+
+    let acc = await a11y.getAccessible(el, true);
+    a11y.assertActionable(acc, el);
+
+    interaction.focusElement(el);
+    event.sendKeysToElement(value, el, win);
+  }
 };
 
 /**
  * Determine the element displayedness of an element.
  *
  * @param {DOMElement|XULElement} el
  *     Element to determine displayedness of.
  * @param {boolean=} [strict=false] strict
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -9,17 +9,16 @@
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 const winUtil = content.QueryInterface(Ci.nsIInterfaceRequestor)
     .getInterface(Ci.nsIDOMWindowUtils);
 
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/Log.jsm");
-Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 Cu.import("chrome://marionette/content/accessibility.js");
 Cu.import("chrome://marionette/content/action.js");
 Cu.import("chrome://marionette/content/atom.js");
 Cu.import("chrome://marionette/content/capture.js");
 const {
   element,
@@ -1420,25 +1419,20 @@ function isElementEnabled(el) {
  * and Radio Button states, or option elements.
  */
 function isElementSelected(el) {
   return interaction.isElementSelected(
       el, capabilities.get("moz:accessibilityChecks"));
 }
 
 async function sendKeysToElement(el, val) {
-  if (el.type == "file") {
-    await interaction.uploadFile(el, val);
-  } else if ((el.type == "date" || el.type == "time") &&
-      Preferences.get("dom.forms.datetime")) {
-    interaction.setFormControlValue(el, val);
-  } else {
-    await interaction.sendKeysToElement(
-        el, val, false, capabilities.get("moz:accessibilityChecks"));
-  }
+  await interaction.sendKeysToElement(
+      el, val,
+      capabilities.get("moz:accessibilityChecks"),
+  );
 }
 
 /** Clear the text of an element. */
 function clearElement(el) {
   try {
     if (el.type == "file") {
       el.value = null;
     } else {