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
--- 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><input type=file></tt>'s
* file list.
*
* @param {HTMLInputElement} el
* An <tt><input type=file></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 {