Bug 1333014 - Align element interaction errors with spec; r?whimboo draft
authorAndreas Tolfsen <ato@mozilla.com>
Fri, 03 Feb 2017 19:30:13 +0000
changeset 503047 8e3854a81b28608e27187896c0132d388ad94f48
parent 502997 e03e0c60462c775c7558a1dc9d5cf2076c3cd1f9
child 503048 10c5ca8a49e796597bf2aed71b41b20288663678
push id50471
push userbmo:ato@mozilla.com
push dateWed, 22 Mar 2017 19:18:41 +0000
reviewerswhimboo
bugs1333014
milestone55.0a1
Bug 1333014 - Align element interaction errors with spec; r?whimboo This renames the ElementNotVisibleError to ElementNotInteractableError, and adds a new ElementClickInterceptedError. MozReview-Commit-ID: 6cjVghUCvyv
testing/marionette/driver.js
testing/marionette/error.js
testing/marionette/event.js
testing/marionette/interaction.js
testing/marionette/listener.js
testing/marionette/test_error.js
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -2573,17 +2573,18 @@ GeckoDriver.prototype.getTextFromDialog 
  * an element not visible error is returned.
  */
 GeckoDriver.prototype.sendKeysToDialog = function (cmd, resp) {
   this._checkIfAlertIsPresent();
 
   // see toolkit/components/prompts/content/commonDialog.js
   let {loginContainer, loginTextbox} = this.dialog.ui;
   if (loginContainer.hidden) {
-    throw new ElementNotVisibleError("This prompt does not accept text input");
+    throw new ElementNotInteractableError(
+        "This prompt does not accept text input");
   }
 
   let win = this.dialog.window ? this.dialog.window : this.getCurrentWindow();
   event.sendKeysToElement(
       cmd.parameters.value,
       loginTextbox,
       {ignoreVisibility: true},
       win);
--- a/testing/marionette/error.js
+++ b/testing/marionette/error.js
@@ -2,18 +2,19 @@
  * 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 {interfaces: Ci, utils: Cu} = Components;
 
 const ERRORS = new Set([
+  "ElementClickInterceptedError",
   "ElementNotAccessibleError",
-  "ElementNotVisibleError",
+  "ElementNotInteractableError",
   "InsecureCertificateError",
   "InvalidArgumentError",
   "InvalidElementStateError",
   "InvalidSelectorError",
   "InvalidSessionIDError",
   "JavaScriptError",
   "MoveTargetOutOfBoundsError",
   "NoAlertOpenError",
@@ -199,20 +200,48 @@ class WebDriverError extends Error {
 
 class ElementNotAccessibleError extends WebDriverError {
   constructor (message) {
     super(message);
     this.status = "element not accessible";
   }
 }
 
-class ElementNotVisibleError extends WebDriverError {
+/**
+ * An element click could not be completed because the element receiving
+ * the events is obscuring the element that was requested clicked.
+ *
+ * @param {Element=} obscuredEl
+ *     Element obscuring the element receiving the click.  Providing this
+ *     is not required, but will produce a nicer error message.
+ * @param {Map.<string, number>} coords
+ *     Original click location.  Providing this is not required, but
+ *     will produce a nicer error message.
+ */
+class ElementClickInterceptedError extends WebDriverError {
+  constructor (obscuredEl = undefined, coords = undefined) {
+    let msg = "";
+    if (obscuredEl && coords) {
+      const doc = obscuredEl.ownerDocument;
+      const overlayingEl = doc.elementFromPoint(coords.x, coords.y);
+      msg = error.pprint`Element ${obscuredEl} is not clickable ` +
+          `at point (${coords.x},${coords.y}) ` +
+          error.pprint`because another element ${overlayingEl} ` +
+          `obscures it`;
+    }
+
+    super(msg);
+    this.status = "element click intercepted";
+  }
+}
+
+class ElementNotInteractableError extends WebDriverError {
   constructor (message) {
     super(message);
-    this.status = "element not visible";
+    this.status = "element not interactable";
   }
 }
 
 class InsecureCertificateError extends WebDriverError {
   constructor (message) {
     super(message);
     this.status = "insecure certificate";
   }
@@ -390,17 +419,18 @@ class UnsupportedOperationError extends 
   constructor (message) {
     super(message);
     this.status = "unsupported operation";
   }
 }
 
 const STATUSES = new Map([
   ["element not accessible", ElementNotAccessibleError],
-  ["element not visible", ElementNotVisibleError],
+  ["element not interactable", ElementNotInteractableError],
+  ["element click intercepted", ElementClickInterceptedError],
   ["insecure certificate", InsecureCertificateError],
   ["invalid argument", InvalidArgumentError],
   ["invalid element state", InvalidElementStateError],
   ["invalid selector", InvalidSelectorError],
   ["invalid session id", InvalidSessionIDError],
   ["javascript error", JavaScriptError],
   ["move target out of bounds", MoveTargetOutOfBoundsError],
   ["no alert open", NoAlertOpenError],
--- a/testing/marionette/event.js
+++ b/testing/marionette/event.js
@@ -1302,17 +1302,17 @@ event.sendKeysToElement = function (
 
     let value = keySequence.join("");
     for (let i = 0; i < value.length; i++) {
       let c = value.charAt(i);
       event.sendSingleKey(c, modifiers, window);
     }
 
   } else {
-    throw new ElementNotVisibleError("Element is not visible");
+    throw new ElementNotInteractableError("Element is not visible");
   }
 };
 
 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
@@ -80,35 +80,35 @@ this.interaction = {};
 
 /**
  * Interact with an element by clicking it.
  *
  * The element is scrolled into view before visibility- or interactability
  * checks are performed.
  *
  * Selenium-style visibility checks will be performed if |specCompat|
- * is false (default).  Otherwise pointer-interactability checks will be
- * performed.  If either of these fail an {@code ElementNotVisibleError}
- * is returned.
+ * is false (default).  Otherwise pointer-interactability
+ * checks will be performed.  If either of these fail an
+ * {@code ElementNotInteractableError} is returned.
  *
  * If |strict| is enabled (defaults to disabled), further accessibility
  * checks will be performed, and these may result in an {@code
  * ElementNotAccessibleError} being returned.
  *
  * When |el| is not enabled, an {@code InvalidElementStateError}
  * is returned.
  *
  * @param {DOMElement|XULElement} el
  *     Element to click.
  * @param {boolean=} strict
  *     Enforce strict accessibility tests.
  * @param {boolean=} specCompat
  *     Use WebDriver specification compatible interactability definition.
  *
- * @throws {ElementNotVisibleError}
+ * @throws {ElementNotInteractable}
  *     If either Selenium-style visibility check or
  *     pointer-interactability check fails.
  * @throws {ElementNotAccessibleError}
  *     If |strict| is true and element is not accessible.
  * @throws {InvalidElementStateError}
  *     If |el| is not enabled.
  */
 interaction.clickElement = function*(el, strict = false, specCompat = false) {
@@ -125,17 +125,17 @@ interaction.clickElement = function*(el,
     if (!element.isPointerInteractable(visibilityCheckEl)) {
       element.scrollIntoView(el);
     }
     interactable = element.isPointerInteractable(visibilityCheckEl);
   } else {
     interactable = element.isVisible(visibilityCheckEl);
   }
   if (!interactable) {
-    throw new ElementNotVisibleError();
+    throw new ElementNotInteractableError();
   }
 
   if (!atom.isElementEnabled(el)) {
     throw new InvalidElementStateError("Element is not enabled");
   }
 
   yield a11y.getAccessible(el, true).then(acc => {
     a11y.assertVisible(acc, el, interactable);
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -638,17 +638,17 @@ function emitTouchEvent(type, touch) {
 /**
  * Function that perform a single tap
  */
 function singleTap(id, corx, cory) {
   let el = seenEls.get(id, curContainer);
   // after this block, the element will be scrolled into view
   let visible = element.isVisible(el, corx, cory);
   if (!visible) {
-    throw new ElementNotVisibleError("Element is not currently visible and may not be manipulated");
+    throw new ElementNotInteractableError("Element is not currently visible and may not be manipulated");
   }
 
   let a11y = accessibility.get(capabilities.get("moz:accessibilityChecks"));
   return a11y.getAccessible(el, true).then(acc => {
     a11y.assertVisible(acc, el, visible);
     a11y.assertActionable(acc, el);
     if (!curContainer.frame.document.createTouch) {
       legacyactions.mouseEventsOnly = true;
--- a/testing/marionette/test_error.js
+++ b/testing/marionette/test_error.js
@@ -162,31 +162,59 @@ add_test(function test_WebDriverError() 
   equal("WebDriverError", err.name);
   equal("foo", err.message);
   equal("webdriver error", err.status);
   ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
+add_test(function test_ElementClickInterceptedError() {
+  let otherEl = {
+    nodeType: 1,
+    localName: "a",
+    classList: [],
+  };
+  let obscuredEl = {
+    nodeType: 1,
+    localName: "b",
+    classList: [],
+    ownerDocument: {
+      elementFromPoint: function (x, y) {
+        return otherEl;
+      },
+    },
+  };
+
+  let err = new ElementClickInterceptedError(obscuredEl, {x: 1, y: 2});
+  equal("ElementClickInterceptedError", err.name);
+  equal("Element <b> is not clickable at point (1,2) " +
+      "because another element <a> obscures it",
+      err.message);
+  equal("element click intercepted", err.status);
+  ok(err instanceof WebDriverError);
+
+  run_next_test();
+});
+
 add_test(function test_ElementNotAccessibleError() {
   let err = new ElementNotAccessibleError("foo");
   equal("ElementNotAccessibleError", err.name);
   equal("foo", err.message);
   equal("element not accessible", err.status);
   ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
-add_test(function test_ElementNotVisibleError() {
-  let err = new ElementNotVisibleError("foo");
-  equal("ElementNotVisibleError", err.name);
+add_test(function test_ElementNotInteractableError() {
+  let err = new ElementNotInteractableError("foo");
+  equal("ElementNotInteractableError", err.name);
   equal("foo", err.message);
-  equal("element not visible", err.status);
+  equal("element not interactable", err.status);
   ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
 add_test(function test_InvalidArgumentError() {
   let err = new InvalidArgumentError("foo");
   equal("InvalidArgumentError", err.name);