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
--- 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><input type=checkbox></tt>,
+ * <tt><input type=radio></tt>,
+ * and <tt>>option></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(