Bug 1255955 - Add support for interacting with <select> elements; r?automatedtester
This patch introduces support for clicking on <select> and <select
multiple> elements to Marionette. As <select> elements, especially
<select multiple>, are operating system level concepts usually implemented
with native widget sets, this patch takes the approach of dispatching
generated events.
MozReview-Commit-ID: 9kwOva43AOL
new file mode 100644
--- /dev/null
+++ b/testing/marionette/harness/marionette/tests/unit/test_select.py
@@ -0,0 +1,163 @@
+# 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/.
+
+import urllib
+
+from marionette import MarionetteTestCase
+from marionette_driver.by import By
+
+
+def inline(doc):
+ return "data:text/html;charset=utf-8,%s" % urllib.quote(doc)
+
+
+class SelectTestCase(MarionetteTestCase):
+ def assertSelected(self, option_element):
+ self.assertTrue(option_element.is_selected(), "<option> element not selected")
+ self.assertTrue(self.marionette.execute_script(
+ "return arguments[0].selected", script_args=[option_element], sandbox=None),
+ "<option> selected attribute not updated")
+
+ def assertNotSelected(self, option_element):
+ self.assertFalse(option_element.is_selected(), "<option> is selected")
+ self.assertFalse(self.marionette.execute_script(
+ "return arguments[0].selected", script_args=[option_element], sandbox=None),
+ "<option> selected attribute not updated")
+
+
+class TestSelect(SelectTestCase):
+ def test_single(self):
+ self.marionette.navigate(inline("""
+ <select>
+ <option>first
+ <option>second
+ </select>"""))
+ select = self.marionette.find_element(By.TAG_NAME, "select")
+ options = self.marionette.find_elements(By.TAG_NAME, "option")
+
+ self.assertSelected(options[0])
+ options[1].click()
+ self.assertSelected(options[1])
+
+ def test_deselect(self):
+ self.marionette.navigate(inline("""
+ <select>
+ <option>first
+ <option>second
+ <option>third
+ </select>"""))
+ select = self.marionette.find_element(By.TAG_NAME, "select")
+ options = self.marionette.find_elements(By.TAG_NAME, "option")
+
+ options[0].click()
+ self.assertSelected(options[0])
+ options[1].click()
+ self.assertSelected(options[1])
+ options[2].click()
+ self.assertSelected(options[2])
+ options[0].click()
+ self.assertSelected(options[0])
+
+ def test_out_of_view(self):
+ self.marionette.navigate(inline("""
+ <select>
+ <option>1
+ <option>2
+ <option>3
+ <option>4
+ <option>5
+ <option>6
+ <option>7
+ <option>8
+ <option>9
+ <option>10
+ <option>11
+ <option>12
+ <option>13
+ <option>14
+ <option>15
+ <option>16
+ <option>17
+ <option>18
+ <option>19
+ <option>20
+ </select>"""))
+ select = self.marionette.find_element(By.TAG_NAME, "select")
+ options = self.marionette.find_elements(By.TAG_NAME, "option")
+
+ options[14].click()
+ self.assertSelected(options[14])
+
+
+class TestSelectMultiple(SelectTestCase):
+ def test_single(self):
+ self.marionette.navigate(inline("<select multiple> <option>first </select>"))
+ option = self.marionette.find_element(By.TAG_NAME, "option")
+ option.click()
+ self.assertSelected(option)
+
+ def test_multiple(self):
+ self.marionette.navigate(inline("""
+ <select multiple>
+ <option>first
+ <option>second
+ <option>third
+ </select>"""))
+ select = self.marionette.find_element(By.TAG_NAME, "select")
+ options = select.find_elements(By.TAG_NAME, "option")
+
+ options[1].click()
+ self.assertSelected(options[1])
+
+ options[2].click()
+ self.assertSelected(options[2])
+ self.assertSelected(options[1])
+
+ def test_deselect_selected(self):
+ self.marionette.navigate(inline("<select multiple> <option>first </select>"))
+ option = self.marionette.find_element(By.TAG_NAME, "option")
+ option.click()
+ self.assertSelected(option)
+ option.click()
+ self.assertNotSelected(option)
+
+ def test_deselect_preselected(self):
+ self.marionette.navigate(inline("""
+ <select multiple>
+ <option selected>first
+ </select>"""))
+ option = self.marionette.find_element(By.TAG_NAME, "option")
+ self.assertSelected(option)
+ option.click()
+ self.assertNotSelected(option)
+
+ def test_out_of_view(self):
+ self.marionette.navigate(inline("""
+ <select multiple>
+ <option>1
+ <option>2
+ <option>3
+ <option>4
+ <option>5
+ <option>6
+ <option>7
+ <option>8
+ <option>9
+ <option>10
+ <option>11
+ <option>12
+ <option>13
+ <option>14
+ <option>15
+ <option>16
+ <option>17
+ <option>18
+ <option>19
+ <option>20
+ </select>"""))
+ select = self.marionette.find_element(By.TAG_NAME, "select")
+ options = self.marionette.find_elements(By.TAG_NAME, "option")
+
+ options[-1].click()
+ self.assertSelected(options[-1])
--- a/testing/marionette/harness/marionette/tests/unit/unit-tests.ini
+++ b/testing/marionette/harness/marionette/tests/unit/unit-tests.ini
@@ -125,9 +125,10 @@ skip-if = buildapp == 'b2g' || os == "wi
[test_using_permissions.py]
[test_using_prefs.py]
[test_shadow_dom.py]
[test_chrome.py]
skip-if = buildapp == 'b2g'
-[test_addons.py]
\ No newline at end of file
+[test_addons.py]
+[test_select.py]
--- a/testing/marionette/interaction.js
+++ b/testing/marionette/interaction.js
@@ -86,50 +86,140 @@ this.interaction = {};
* @param {boolean=} specCompat
* Use WebDriver specification compatible interactability definition.
*/
interaction.clickElement = function(el, strict = false, specCompat = false) {
let a11y = accessibility.get(strict);
return a11y.getAccessible(el, true).then(acc => {
let win = getWindow(el);
+ let selectEl;
+ let visibilityCheckEl = el;
+ if (el.localName == "option") {
+ selectEl = interaction.getSelectForOptionElement(el);
+ visibilityCheckEl = selectEl;
+ }
+
let visible = false;
if (specCompat) {
- visible = element.isInteractable(el);
+ visible = element.isInteractable(visibilityCheckEl);
if (!visible) {
el.scrollIntoView(false);
}
- visible = element.isInteractable(el);
+ visible = element.isInteractable(visibilityCheckEl);
} else {
- visible = element.isVisible(el);
+ visible = element.isVisible(visibilityCheckEl);
}
if (!visible) {
throw new ElementNotVisibleError("Element is not visible");
}
- a11y.assertVisible(acc, el, visible);
- if (!atom.isElementEnabled(el)) {
+ a11y.assertVisible(acc, visibilityCheckEl, visible);
+ if (!atom.isElementEnabled(visibilityCheckEl)) {
throw new InvalidElementStateError("Element is not enabled");
}
- a11y.assertEnabled(acc, el, true);
- a11y.assertActionable(acc, el);
+ a11y.assertEnabled(acc, visibilityCheckEl, true);
+ a11y.assertActionable(acc, visibilityCheckEl);
+ // chrome elements
if (element.isXULElement(el)) {
- el.click();
+ if (el.localName == "option") {
+ interaction.selectOption(el);
+ } else {
+ el.click();
+ }
+
+ // content elements
} else {
- let rects = el.getClientRects();
- let coords = {
- x: rects[0].left + rects[0].width / 2.0,
- y: rects[0].top + rects[0].height / 2.0,
- };
- event.synthesizeMouseAtPoint(coords.x, coords.y, {} /* opts */, win);
+ if (el.localName == "option") {
+ interaction.selectOption(el);
+ } else {
+ let centre = interaction.calculateCentreCoords(el);
+ let opts = {};
+ event.synthesizeMouseAtPoint(centre.x, centre.y, opts, win);
+ }
}
});
};
+interaction.calculateCentreCoords = function(el) {
+ let rects = el.getClientRects();
+ return {
+ x: rects[0].left + rects[0].width / 2.0,
+ y: rects[0].top + rects[0].height / 2.0,
+ };
+};
+
+/**
+ * Select <option> element in a <select> list.
+ *
+ * Because the dropdown list of select elements are implemented using
+ * native widget technology, our trusted synthesised events are not able
+ * to reach them. Dropdowns are instead handled mimicking DOM events,
+ * which for obvious reasons is not ideal, but at the current point in
+ * time considered to be good enough.
+ *
+ * @param {HTMLOptionElement} option
+ * Option element to select.
+ *
+ * @throws TypeError
+ * If |el| is a XUL element or not an <option> element.
+ * @throws Error
+ * If unable to find |el|'s parent <select> element.
+ */
+interaction.selectOption = function(el) {
+ if (element.isXULElement(el)) {
+ throw new Error("XUL dropdowns not supported");
+ }
+ if (el.localName != "option") {
+ throw new TypeError("Invalid elements");
+ }
+
+ let win = getWindow(el);
+ let parent = interaction.getSelectForOptionElement(el);
+
+ event.mouseover(parent);
+ event.mousemove(parent);
+ event.mousedown(parent);
+ event.focus(parent);
+ event.input(parent);
+
+ // toggle selectedness the way holding down control works
+ el.selected = !el.selected;
+
+ event.change(parent);
+ event.mouseup(parent);
+ event.click(parent);
+};
+
+/**
+ * Locate the <select> element that encapsulate an <option> element.
+ *
+ * @param {HTMLOptionElement} optionEl
+ * Option element.
+ *
+ * @return {HTMLSelectElement}
+ * Select element wrapping |optionEl|.
+ *
+ * @throws {Error}
+ * If unable to find the <select> element.
+ */
+interaction.getSelectForOptionElement = function(optionEl) {
+ let parent = optionEl;
+ while (parent.parentNode && parent.localName != "select") {
+ parent = parent.parentNode;
+ }
+
+ if (parent.localName != "select") {
+ throw new Error("Unable to find parent of <option> element");
+ }
+
+ return parent;
+};
+
/**
* 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