Bug 1284232 - Marshal all HTML and JS collections; r?automatedtester
This patch adds marshaling of HTMLFormControlsCollection,
HTMLAllCollection, and HTMLOptionsCollection element collections to
Marionette.
It will allow us you to return from HTMLSelectElement.options,
document.forms[0].elements, and document.all. This is in
addition to the already supported document.querySelector
(NodeList), document.getElementsBy* (HTMLCollection), and
Array.from(ELEMENT...) collections.
MozReview-Commit-ID: 71a65lZRn4S
--- a/testing/marionette/element.js
+++ b/testing/marionette/element.js
@@ -282,19 +282,20 @@ function find_(container, strategy, sele
let res;
try {
res = searchFn(strategy, selector, rootNode, startNode);
} catch (e) {
throw new InvalidSelectorError(
`Given ${strategy} expression "${selector}" is invalid: ${e}`);
}
- if (element.isElementCollection(res)) {
- return res;
- } else if (res) {
+ if (res) {
+ if (opts.all) {
+ return res;
+ }
return [res];
}
return [];
}
/**
* Find a single element by XPath expression.
*
@@ -572,24 +573,32 @@ function implicitlyWaitFor(func, timeout
let elementSearch = function() {
let res;
try {
res = func();
} catch (e) {
reject(e);
}
- // empty arrays evaluate to true in JS,
- // so we must first ascertan if the result is a collection
- //
- // we also return immediately if timeout is 0,
- // allowing |func| to be evaluated at least once
- let col = element.isElementCollection(res);
- if (((col && res.length > 0 ) || (!col && !!res)) ||
- (startTime == endTime || new Date().getTime() >= endTime)) {
+ if (
+ // collections that might contain web elements
+ // should be checked until they are not empty
+ (element.isCollection(res) && res.length > 0)
+
+ // !![] (ensuring boolean type on empty array) always returns true
+ // and we can only use it on non-collections
+ || (!element.isCollection(res) && !!res)
+
+ // return immediately if timeout is 0,
+ // allowing |func| to be evaluted at least once
+ || startTime == endTime
+
+ // return if timeout has elapsed
+ || new Date().getTime() >= endTime
+ ) {
resolve(res);
}
};
// the repeating slack timer waits |interval|
// before invoking |elementSearch|
elementSearch();
@@ -600,29 +609,32 @@ function implicitlyWaitFor(func, timeout
timer.cancel();
return res;
}, err => {
timer.cancel();
throw err;
});
}
-element.isElementCollection = function(seq) {
- if (seq === null) {
- return false;
- }
+/** Determines if |obj| is an HTML or JS collection. */
+element.isCollection = function(seq) {
+ switch (Object.prototype.toString.call(seq)) {
+ case "[object Arguments]":
+ case "[object Array]":
+ case "[object FileList]":
+ case "[object HTMLAllCollection]":
+ case "[object HTMLCollection]":
+ case "[object HTMLFormControlsCollection]":
+ case "[object HTMLOptionsCollection]":
+ case "[object NodeList]":
+ return true;
- const arrayLike = {
- "[object Array]": 0,
- "[object HTMLCollection]": 1,
- "[object NodeList]": 2,
- };
-
- let typ = Object.prototype.toString.call(seq);
- return typ in arrayLike;
+ default:
+ return false;
+ }
};
element.makeWebElement = function(uuid) {
return {
[element.Key]: uuid,
[element.LegacyKey]: uuid,
};
};
@@ -701,53 +713,50 @@ element.fromJson = function(
* @param {element.Store} seenEls
* Element store to use for lookup of web element references.
*
* @return {?}
* Same object as provided by |obj| with the elements replaced by
* web elements.
*/
element.toJson = function(obj, seenEls) {
- switch (typeof obj) {
- case "undefined":
- return null;
+ let t = Object.prototype.toString.call(obj);
- case "boolean":
- case "number":
- case "string":
- return obj;
+ // null
+ if (t == "[object Undefined]" || t == "[object Null]") {
+ return null;
+ }
- case "object":
- if (obj === null) {
- return obj;
- }
+ // literals
+ else if (t == "[object Boolean]" || t == "[object Number]" || t == "[object String]") {
+ return obj;
+ }
- // NodeList, HTMLCollection
- else if (element.isElementCollection(obj)) {
- return [...obj].map(el => element.toJson(el, seenEls));
- }
+ // Array, NodeList, HTMLCollection, et al.
+ else if (element.isCollection(obj)) {
+ return [...obj].map(el => element.toJson(el, seenEls));
+ }
- // DOM element
- else if (obj.nodeType == 1) {
- let uuid = seenEls.add(obj);
- return {[element.Key]: uuid, [element.LegacyKey]: uuid};
- }
+ // HTMLElement
+ else if ("nodeType" in obj && obj.nodeType == 1) {
+ let uuid = seenEls.add(obj);
+ return {[element.Key]: uuid, [element.LegacyKey]: uuid};
+ }
- // arbitrary objects
- else {
- let rv = {};
- for (let prop in obj) {
- try {
- rv[prop] = element.toJson(obj[prop], seenEls);
- } catch (e if (e.result == Cr.NS_ERROR_NOT_IMPLEMENTED)) {
- logger.debug(`Skipping ${prop}: ${e.message}`);
- }
- }
- return rv;
+ // arbitrary objects + files
+ else {
+ let rv = {};
+ for (let prop in obj) {
+ try {
+ rv[prop] = element.toJson(obj[prop], seenEls);
+ } catch (e if (e.result == Cr.NS_ERROR_NOT_IMPLEMENTED)) {
+ logger.debug(`Skipping ${prop}: ${e.message}`);
}
+ }
+ return rv;
}
};
/**
* Check if the element is detached from the current frame as well as
* the optional shadow root (when inside a Shadow DOM context).
*
* @param {nsIDOMElement} el
--- a/testing/marionette/harness/marionette/tests/unit/test_execute_script.py
+++ b/testing/marionette/harness/marionette/tests/unit/test_execute_script.py
@@ -1,16 +1,17 @@
# 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
import os
from marionette_driver import By, errors
+from marionette_driver.marionette import HTMLElement
from marionette import MarionetteTestCase
def inline(doc):
return "data:text/html;charset=utf-8,%s" % urllib.quote(doc)
elements = inline("<p>foo</p> <p>bar</p>")
@@ -283,8 +284,57 @@ class TestExecuteChrome(TestExecuteConte
def test_return_web_element(self):
pass
def test_return_web_element_array(self):
pass
def test_return_web_element_nodelist(self):
pass
+
+
+class TestElementCollections(MarionetteTestCase):
+ def assertSequenceIsInstance(self, seq, typ):
+ for item in seq:
+ self.assertIsInstance(item, typ)
+
+ def test_array(self):
+ self.marionette.navigate(inline("<p>foo <p>bar"))
+ els = self.marionette.execute_script("return Array.from(document.querySelectorAll('p'))")
+ self.assertIsInstance(els, list)
+ self.assertEqual(2, len(els))
+ self.assertSequenceIsInstance(els, HTMLElement)
+
+ def test_html_all_collection(self):
+ self.marionette.navigate(inline("<p>foo <p>bar"))
+ els = self.marionette.execute_script("return document.all")
+ self.assertIsInstance(els, list)
+ # <html>, <head>, <body>, <p>, <p>
+ self.assertEqual(5, len(els))
+ self.assertSequenceIsInstance(els, HTMLElement)
+
+ def test_html_collection(self):
+ self.marionette.navigate(inline("<p>foo <p>bar"))
+ els = self.marionette.execute_script("return document.getElementsByTagName('p')")
+ self.assertIsInstance(els, list)
+ self.assertEqual(2, len(els))
+ self.assertSequenceIsInstance(els, HTMLElement)
+
+ def test_html_form_controls_collection(self):
+ self.marionette.navigate(inline("<form><input><input></form>"))
+ els = self.marionette.execute_script("return document.forms[0].elements")
+ self.assertIsInstance(els, list)
+ self.assertEqual(2, len(els))
+ self.assertSequenceIsInstance(els, HTMLElement)
+
+ def test_html_options_collection(self):
+ self.marionette.navigate(inline("<select><option><option></select>"))
+ els = self.marionette.execute_script("return document.querySelector('select').options")
+ self.assertIsInstance(els, list)
+ self.assertEqual(2, len(els))
+ self.assertSequenceIsInstance(els, HTMLElement)
+
+ def test_node_list(self):
+ self.marionette.navigate(inline("<p>foo <p>bar"))
+ els = self.marionette.execute_script("return document.querySelectorAll('p')")
+ self.assertIsInstance(els, list)
+ self.assertEqual(2, len(els))
+ self.assertSequenceIsInstance(els, HTMLElement)
--- a/testing/marionette/harness/marionette/tests/unit/test_file_upload.py
+++ b/testing/marionette/harness/marionette/tests/unit/test_file_upload.py
@@ -17,57 +17,56 @@ multiple = "data:text/html,%s" % urllib.
upload = lambda url: "data:text/html,%s" % urllib.quote("""
<form action='%s' method=post enctype='multipart/form-data'>
<input type=file>
<input type=submit>
</form>""" % url)
class TestFileUpload(MarionetteTestCase):
-
def test_sets_one_file(self):
self.marionette.navigate(single)
input = self.input
exp = None
with tempfile() as f:
input.send_keys(f.name)
exp = [f.name]
- files = self.get_files(input)
+ files = self.get_file_names(input)
self.assertEqual(len(files), 1)
- self.assertFilesEqual(files, exp)
+ self.assertFileNamesEqual(files, exp)
def test_sets_multiple_files(self):
self.marionette.navigate(multiple)
input = self.input
exp = None
with contextlib.nested(tempfile(), tempfile()) as (a, b):
input.send_keys(a.name)
input.send_keys(b.name)
exp = [a.name, b.name]
- files = self.get_files(input)
+ files = self.get_file_names(input)
self.assertEqual(len(files), 2)
- self.assertFilesEqual(files, exp)
+ self.assertFileNamesEqual(files, exp)
def test_sets_multiple_indentical_files(self):
self.marionette.navigate(multiple)
input = self.input
exp = []
with tempfile() as f:
input.send_keys(f.name)
input.send_keys(f.name)
exp = f.name
- files = self.get_files(input)
+ files = self.get_file_names(input)
self.assertEqual(len(files), 2)
- self.assertFilesEqual(files, exp)
+ self.assertFileNamesEqual(files, exp)
def test_clear_file(self):
self.marionette.navigate(single)
input = self.input
with tempfile() as f:
input.send_keys(f.name)
@@ -117,21 +116,21 @@ class TestFileUpload(MarionetteTestCase)
def submit(self):
return self.find_inputs()[1]
@property
def body(self):
return Wait(self.marionette).until(
expected.element_present(By.TAG_NAME, "body"))
+ def get_file_names(self, el):
+ fl = self.get_files(el)
+ return [f["name"] for f in fl]
+
def get_files(self, el):
- # This is horribly complex because (1) Marionette doesn't serialise arrays properly,
- # and (2) accessing File.name in the content JS throws a permissions
- # error.
- fl = self.marionette.execute_script(
+ return self.marionette.execute_script(
"return arguments[0].files", script_args=[el])
- return [f["name"] for f in [v for k, v in fl.iteritems() if k.isdigit()]]
- def assertFilesEqual(self, act, exp):
+ def assertFileNamesEqual(self, act, exp):
# File array returned from browser doesn't contain full path names,
# this cuts off the path of the expected files.
filenames = [f.rsplit("/", 0)[-1] for f in act]
self.assertListEqual(filenames, act)