Bug 1275273 - Make WebDriver:IsElementSelected conform to spec. r?maja_zf draft
authorAndreas Tolfsen <ato@sny.no>
Tue, 10 Oct 2017 14:51:27 +0100
changeset 678521 038efe35fa44bdb44335a2ddae2bd80c7e8837e5
parent 678520 465a7d4794268d9c29f0058b99b47b4f84be7cad
child 735347 8762ebded17288f732e70eccdc8a0e4ff813ebcd
push id83944
push userbmo:ato@sny.no
push dateWed, 11 Oct 2017 14:20:41 +0000
reviewersmaja_zf
bugs1275273
milestone58.0a1
Bug 1275273 - Make WebDriver:IsElementSelected conform to spec. r?maja_zf Splits interaction.isElementSelected into two parts: one checking whether the element DOM properties are selected/checked, and the other checking accessibility. This so this so that the selectedness can be unit tested, as we do not have the capability of standing up accessibility in the xpcshell tests. The second part of this change moves us away from atom.isElementSelected in favour of a specification conforming implementation in Marionette. This is a word-by-word implementation of the Is Element Selected command from WebDriver. MozReview-Commit-ID: 93WDKbPcEIB
testing/marionette/element.js
testing/marionette/interaction.js
testing/marionette/test_element.js
--- a/testing/marionette/element.js
+++ b/testing/marionette/element.js
@@ -17,16 +17,35 @@ const {
 } = Cu.import("chrome://marionette/content/error.js", {});
 const {PollPromise} = Cu.import("chrome://marionette/content/sync.js", {});
 
 this.EXPORTED_SYMBOLS = ["element"];
 
 const XMLNS = "http://www.w3.org/1999/xhtml";
 const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
+/** XUL elements that support checked property. */
+const XUL_CHECKED_ELS = new Set([
+  "button",
+  "checkbox",
+  "listitem",
+  "toolbarbutton",
+]);
+
+/** XUL elements that support selected property. */
+const XUL_SELECTED_ELS = new Set([
+  "listitem",
+  "menu",
+  "menuitem",
+  "menuseparator",
+  "radio",
+  "richlistitem",
+  "tab",
+]);
+
 const uuidGen = Cc["@mozilla.org/uuid-generator;1"]
     .getService(Ci.nsIUUIDGenerator);
 
 /**
  * This module provides shared functionality for dealing with DOM-
  * and web elements in Marionette.
  *
  * A web element is an abstraction used to identify an element when it
@@ -664,16 +683,56 @@ element.isStale = function(el, window = 
       el.ownerDocument !== window.document) {
     return true;
   }
 
   return !el.isConnected;
 };
 
 /**
+ * Determine if <var>el</var> is selected or not.
+ *
+ * This operation only makes sense on
+ * <tt>&lt;input type=checkbox&gt;</tt>,
+ * <tt>&lt;input type=radio&gt;</tt>,
+ * and <tt>&gt;option&gt;</tt> elements.
+ *
+ * @param {(DOMElement|XULElement)} el
+ *     Element to test if selected.
+ *
+ * @return {boolean}
+ *     True if element is selected, false otherwise.
+ */
+element.isSelected = function(el) {
+  if (!el) {
+    return false;
+  }
+
+  if (element.isXULElement(el)) {
+    if (XUL_CHECKED_ELS.has(el.tagName)) {
+      return el.checked;
+    } else if (XUL_SELECTED_ELS.has(el.tagName)) {
+      return el.selected;
+    }
+
+  // TODO(ato): Use element.isDOMElement when bug 1400256 lands
+  } else if (typeof el == "object" &&
+      "nodeType" in el &&
+      el.nodeType == el.ELEMENT_NODE) {
+    if (el.localName == "input" && ["checkbox", "radio"].includes(el.type)) {
+      return el.checked;
+    } else if (el.localName == "option") {
+      return el.selected;
+    }
+  }
+
+  return false;
+};
+
+/**
  * This function generates a pair of coordinates relative to the viewport
  * given a target element and coordinates relative to that element's
  * top-left corner.
  *
  * @param {Node} node
  *     Target node.
  * @param {number=} xOffset
  *     Horizontal offset relative to target's top-left corner.
--- a/testing/marionette/interaction.js
+++ b/testing/marionette/interaction.js
@@ -52,35 +52,16 @@ const DISABLED_ATTRIBUTE_SUPPORTED_XUL =
   "TAB",
   "TABS",
   "TEXTBOX",
   "TIMEPICKER",
   "TOOLBARBUTTON",
   "TREE",
 ]);
 
-/** XUL elements that support checked property. */
-const CHECKED_PROPERTY_SUPPORTED_XUL = new Set([
-  "BUTTON",
-  "CHECKBOX",
-  "LISTITEM",
-  "TOOLBARBUTTON",
-]);
-
-/** XUL elements that support selected property. */
-const SELECTED_PROPERTY_SUPPORTED_XUL = new Set([
-  "LISTITEM",
-  "MENU",
-  "MENUITEM",
-  "MENUSEPARATOR",
-  "RADIO",
-  "RICHLISTITEM",
-  "TAB",
-]);
-
 /**
  * Common form controls that user can change the value property
  * interactively.
  */
 const COMMON_FORM_CONTROLS = new Set([
   "input",
   "textarea",
   "select",
@@ -473,44 +454,35 @@ interaction.isElementEnabled = function(
   let a11y = accessibility.get(strict);
   return a11y.getAccessible(el).then(acc => {
     a11y.assertEnabled(acc, el, enabled);
     return enabled;
   });
 };
 
 /**
- * Determines if the referenced element is selected or not.
+ * Determines if the referenced element is selected or not, with
+ * an additional accessibility check if <var>strict</var> is true.
  *
- * This operation only makes sense on input elements of the Checkbox-
- * and Radio Button states, or option elements.
+ * This operation only makes sense on input elements of the checkbox-
+ * and radio button states, and option elements.
  *
- * @param {DOMElement|XULElement} el
+ * @param {(DOMElement|XULElement)} el
  *     Element to test if is selected.
  * @param {boolean=} [strict=false] strict
  *     Enforce strict accessibility tests.
  *
  * @return {boolean}
  *     True if element is selected, false otherwise.
+ *
+ * @throws {ElementNotAccessibleError}
+ *     If <var>el</var> is not accessible when <var>strict</var> is true.
  */
 interaction.isElementSelected = function(el, strict = false) {
-  let selected = true;
-  let win = getWindow(el);
-
-  if (element.isXULElement(el)) {
-    let tagName = el.tagName.toUpperCase();
-    if (CHECKED_PROPERTY_SUPPORTED_XUL.has(tagName)) {
-      selected = el.checked;
-    }
-    if (SELECTED_PROPERTY_SUPPORTED_XUL.has(tagName)) {
-      selected = el.selected;
-    }
-  } else {
-    selected = atom.isElementSelected(el, win);
-  }
+  let selected = element.isSelected(el);
 
   let a11y = accessibility.get(strict);
   return a11y.getAccessible(el).then(acc => {
     a11y.assertSelected(acc, el, selected);
     return selected;
   });
 };
 
--- a/testing/marionette/test_element.js
+++ b/testing/marionette/test_element.js
@@ -1,49 +1,100 @@
 /* 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/. */
 
 const {utils: Cu} = Components;
 
 Cu.import("chrome://marionette/content/element.js");
 
-let el = {
-  getBoundingClientRect: function() {
+class DOMElement {
+  constructor(tagName, attrs = {}) {
+    this.tagName = tagName;
+    this.localName = tagName;
+
+    for (let attr in attrs) {
+      this[attr] = attrs[attr];
+    }
+
+    if (this.localName == "option") {
+      this.selected = false;
+    }
+
+    if (this.localName == "input" && ["checkbox", "radio"].includes(this.type)) {
+      this.checked = false;
+    }
+  }
+
+  get nodeType() { return 1; }
+  get ELEMENT_NODE() { return 1; }
+
+  getBoundingClientRect() {
     return {
       top: 0,
       left: 0,
       width: 100,
       height: 100,
     };
   }
 };
 
+const domEl = new DOMElement("p");
+
+add_test(function test_isSelected() {
+  let checkbox = new DOMElement("input", {type: "checkbox"});
+  ok(!element.isSelected(checkbox));
+  checkbox.checked = true;
+  ok(element.isSelected(checkbox));
+
+  // selected is not a property of <input type=checkbox>
+  checkbox.selected = true;
+  checkbox.checked = false;
+  ok(!element.isSelected(checkbox));
+
+  let option = new DOMElement("option");
+  ok(!element.isSelected(option));
+  option.selected = true;
+  ok(element.isSelected(option));
+
+  // checked is not a property of <option>
+  option.checked = true;
+  option.selected = false;
+  ok(!element.isSelected(option));
+
+  // anything else should not be selected
+  for (let typ of [domEl, undefined, null, "foo", true, [], {}]) {
+    ok(!element.isSelected(typ));
+  }
+
+  run_next_test();
+});
+
 add_test(function test_coordinates() {
-  let p = element.coordinates(el);
+  let p = element.coordinates(domEl);
   ok(p.hasOwnProperty("x"));
   ok(p.hasOwnProperty("y"));
   equal("number", typeof p.x);
   equal("number", typeof p.y);
 
-  deepEqual({x: 50, y: 50}, element.coordinates(el));
-  deepEqual({x: 10, y: 10}, element.coordinates(el, 10, 10));
-  deepEqual({x: -5, y: -5}, element.coordinates(el, -5, -5));
+  deepEqual({x: 50, y: 50}, element.coordinates(domEl));
+  deepEqual({x: 10, y: 10}, element.coordinates(domEl, 10, 10));
+  deepEqual({x: -5, y: -5}, element.coordinates(domEl, -5, -5));
 
   Assert.throws(() => element.coordinates(null));
 
-  Assert.throws(() => element.coordinates(el, "string", undefined));
-  Assert.throws(() => element.coordinates(el, undefined, "string"));
-  Assert.throws(() => element.coordinates(el, "string", "string"));
-  Assert.throws(() => element.coordinates(el, {}, undefined));
-  Assert.throws(() => element.coordinates(el, undefined, {}));
-  Assert.throws(() => element.coordinates(el, {}, {}));
-  Assert.throws(() => element.coordinates(el, [], undefined));
-  Assert.throws(() => element.coordinates(el, undefined, []));
-  Assert.throws(() => element.coordinates(el, [], []));
+  Assert.throws(() => element.coordinates(domEl, "string", undefined));
+  Assert.throws(() => element.coordinates(domEl, undefined, "string"));
+  Assert.throws(() => element.coordinates(domEl, "string", "string"));
+  Assert.throws(() => element.coordinates(domEl, {}, undefined));
+  Assert.throws(() => element.coordinates(domEl, undefined, {}));
+  Assert.throws(() => element.coordinates(domEl, {}, {}));
+  Assert.throws(() => element.coordinates(domEl, [], undefined));
+  Assert.throws(() => element.coordinates(domEl, undefined, []));
+  Assert.throws(() => element.coordinates(domEl, [], []));
 
   run_next_test();
 });
 
 add_test(function test_isWebElementReference() {
   strictEqual(element.isWebElementReference({[element.Key]: "some_id"}), true);
   strictEqual(element.isWebElementReference({[element.LegacyKey]: "some_id"}), true);
   strictEqual(element.isWebElementReference(