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
This commit is contained in:
Andreas Tolfsen 2017-10-10 14:51:27 +01:00
parent 4671781f91
commit 545d5ec9f4
3 changed files with 134 additions and 52 deletions

View File

@ -22,6 +22,25 @@ 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);
@ -668,6 +687,46 @@ element.isStale = function(el, window = undefined) {
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

View File

@ -57,25 +57,6 @@ const DISABLED_ATTRIBUTE_SUPPORTED_XUL = new Set([
"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.
@ -478,34 +459,25 @@ interaction.isElementEnabled = function(el, strict = false) {
};
/**
* 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 => {

View File

@ -6,8 +6,28 @@ 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,
@ -17,28 +37,59 @@ let el = {
}
};
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();
});