--- a/testing/marionette/element.js
+++ b/testing/marionette/element.js
@@ -307,99 +307,141 @@ ElementManager.prototype = {
namedArgs[prop] = arg['__marionetteArgs'][prop];
}
}
});
return namedArgs;
},
/**
- * Find an element or elements starting at the document root or
- * given node, using the given search strategy. Search
- * will continue until the search timelimit has been reached.
+ * Find a single element or a collection of elements starting at the
+ * document root or a given node.
+ *
+ * If |timeout| is above 0, an implicit search technique is used.
+ * This will wait for the duration of |timeout| for the element
+ * to appear in the DOM.
+ *
+ * See the |element.Strategies| enum for a full list of supported
+ * search strategies that can be passed to |strategy|.
+ *
+ * Available flags for |opts|:
+ *
+ * |all|
+ * If true, a multi-element search selector is used and a sequence
+ * of elements will be returned. Otherwise a single element.
+ *
+ * |timeout|
+ * Duration to wait before timing out the search. If |all| is
+ * false, a NoSuchElementError is thrown if unable to find
+ * the element within the timeout duration.
+ *
+ * |startNode|
+ * Element to use as the root of the search.
*
- * @param nsIDOMWindow, ShadowRoot container
- * The window and an optional shadow root that contains the element
- * @param object values
- * The 'using' member of values will tell us which search
- * method to use. The 'value' member tells us the value we
- * are looking for.
- * If this object has an 'element' member, this will be used
- * as the start node instead of the document root
- * If this object has a 'time' member, this number will be
- * used to see if we have hit the search timelimit.
- * @param boolean all
- * If true, all found elements will be returned.
- * If false, only the first element will be returned.
- * @param function on_success
- * Callback used when operating is successful.
- * @param function on_error
- * Callback to invoke when an error occurs.
+ * @param {Object.<string, Window>} container
+ * Window object and an optional shadow root that contains the
+ * root shadow DOM element.
+ * @param {string} strategy
+ * Search strategy whereby to locate the element(s).
+ * @param {string} selector
+ * Selector search pattern. The selector must be compatible with
+ * the chosen search |strategy|.
+ * @param {Object.<string, ?>} opts
+ * Options.
+ *
+ * @return {Promise: (WebElement|Array<WebElement>)}
+ * Single element or a sequence of elements.
*
- * @return nsIDOMElement or list of nsIDOMElements
- * Returns the element(s) by calling the on_success function.
+ * @throws InvalidSelectorError
+ * If |strategy| is unknown.
+ * @throws InvalidSelectorError
+ * If |selector| is malformed.
+ * @throws NoSuchElementError
+ * If a single element is requested, this error will throw if the
+ * element is not found.
*/
- find: function EM_find(container, values, searchTimeout, all, on_success, on_error, command_id) {
- let startTime = values.time ? values.time : new Date().getTime();
- let rootNode = container.shadowRoot || container.frame.document;
- let startNode = (values.element != undefined) ?
- this.getKnownElement(values.element, container) : rootNode;
- if (this.elementStrategies.indexOf(values.using) < 0) {
- throw new InvalidSelectorError(`No such strategy: ${values.using}`);
+ find: function(container, strategy, selector, opts = {}) {
+ opts.all = !!opts.all;
+ opts.timeout = opts.timeout || 0;
+
+ let searchFn;
+ if (opts.all) {
+ searchFn = this.findElements.bind(this);
+ } else {
+ searchFn = this.findElement.bind(this);
}
- let found;
- try {
- found = all ? this.findElements(values.using, values.value, rootNode, startNode) :
- this.findElement(values.using, values.value, rootNode, startNode);
- } catch (e) {
- throw new InvalidSelectorError(`Given ${values.using} expression "${values.value}" is invalid`);
- }
- let type = Object.prototype.toString.call(found);
- let isArrayLike = ((type == '[object Array]') || (type == '[object HTMLCollection]') || (type == '[object NodeList]'));
- if (found == null || (isArrayLike && found.length <= 0)) {
- if (!searchTimeout || new Date().getTime() - startTime > searchTimeout) {
- if (all) {
- on_success([], command_id); // findElements should return empty list
- } else {
- // Format message depending on strategy if necessary
- let message = `Unable to locate element: ${values.value}`
- if (values.using == ANON) {
- message = "Unable to locate anonymous children";
- } else if (values.using == ANON_ATTRIBUTE) {
- message = `Unable to locate anonymous element: ${JSON.stringify(values.value)}`;
+ return new Promise((resolve, reject) => {
+ let findElements = implicitlyWaitFor(
+ () => this.find_(container, strategy, selector, searchFn, opts),
+ opts.timeout);
+
+ findElements.then(foundEls => {
+ // when looking for a single element and none is found,
+ // an error must be thrown
+ if (foundEls.length == 0 && !opts.all) {
+ let msg;
+ switch (strategy) {
+ case ANON:
+ msg = "Unable to locate anonymous children";
+ break;
+
+ case ANON_ATTRIBUTE:
+ msg = "Unable to locate anonymous element: " + JSON.stringify(selector);
+ break;
+
+ default:
+ msg = "Unable to locate element: " + selector;
}
- on_error(new NoSuchElementError(message), command_id);
+
+ reject(new NoSuchElementError(msg));
+ }
+
+ // serialise elements for return
+ let rv = [];
+ for (let el of foundEls) {
+ let ref = this.addToKnownElements(el);
+ let we = element.makeWebElement(ref);
+ rv.push(we);
}
- } else {
- values.time = startTime;
- this.timer.initWithCallback(this.find.bind(this, container, values,
- searchTimeout, all,
- on_success, on_error,
- command_id),
- 100,
- Ci.nsITimer.TYPE_ONE_SHOT);
- }
+
+ if (opts.all) {
+ resolve(rv);
+ }
+ resolve(rv[0]);
+ }, reject);
+ });
+ },
+
+ find_: function(container, strategy, selector, searchFn, opts) {
+ let rootNode = container.shadowRoot || container.frame.document;
+ let startNode;
+ if (opts.startNode) {
+ startNode = this.getKnownElement(opts.startNode, container);
} else {
- if (isArrayLike) {
- let ids = []
- for (let i = 0 ; i < found.length ; i++) {
- let foundElement = this.addToKnownElements(found[i]);
- let returnElement = {
- [this.elementKey] : foundElement,
- [this.w3cElementKey] : foundElement,
- };
- ids.push(returnElement);
- }
- on_success(ids, command_id);
- } else {
- let id = this.addToKnownElements(found);
- on_success({[this.elementKey]: id, [this.w3cElementKey]:id}, command_id);
- }
+ startNode = rootNode;
+ }
+
+ if (strategy in element.Strategies) {
+ throw new InvalidSelectorError("No such strategy: " + strategy);
}
+
+ let res;
+ try {
+ res = searchFn(strategy, selector, rootNode, startNode);
+ } catch (e) {
+ throw new InvalidSelectorError(`Given ${strategy} expression "${selector}" is invalid`);
+ }
+
+ if (element.isElementCollection(res)) {
+ return res;
+ } else if (res !== null) {
+ return [res];
+ }
+ return [];
},
/**
* Find a value by XPATH
*
* @param nsIDOMElement root
* Document root
* @param string value
@@ -590,20 +632,117 @@ ElementManager.prototype = {
break;
default:
throw new InvalidSelectorError(`No such strategy: ${using}`);
}
return elements;
},
};
+/**
+ * Runs function off the main thread until its return value is truthy
+ * or the provided timeout is reached. The function is guaranteed to be
+ * run at least once, irregardless of the timeout.
+ *
+ * A truthy return value constitutes a truthful boolean, positive number,
+ * object, or non-empty array.
+ *
+ * The |func| is evaluated every |interval| for as long as its runtime
+ * duration does not exceed |interval|. If the runtime evaluation duration
+ * of |func| is greater than |interval|, evaluations of |func| are queued.
+ *
+ * @param {function(): ?} func
+ * Function to run off the main thread.
+ * @param {number} timeout
+ * Desired timeout. If 0 or less than the runtime evaluation time
+ * of |func|, |func| is guaranteed to run at least once.
+ * @param {number=} interval
+ * Duration between each poll of |func| in milliseconds. Defaults to
+ * 100 milliseconds.
+ *
+ * @return {Promise}
+ * Yields the return value from |func|. The promise is rejected if
+ * |func| throws.
+ */
+function implicitlyWaitFor(func, timeout, interval = 100) {
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
+ return new Promise((resolve, reject) => {
+ let startTime = new Date().getTime();
+ let endTime = startTime + timeout;
+
+ let observer = 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)) {
+ resolve(res);
+ }
+ };
+
+ timer.init(observer, interval, Ci.nsITimer.TYPE_REPEATING_SLACK);
+
+ // cancel timer and return result for yielding
+ }).then(res => {
+ timer.cancel();
+ return res;
+ });
+};
this.element = {};
+
element.LegacyKey = "ELEMENT";
element.Key = "element-6066-11e4-a52e-4f735466cecf";
+
+element.Strategies = {
+ CLASS_NAME: 0,
+ SELECTOR: 1,
+ ID: 2,
+ NAME: 3,
+ LINK_TEXT: 4,
+ PARTIAL_LINK_TEXT: 5,
+ TAG: 6,
+ XPATH: 7,
+ ANON: 8,
+ ANON_ATTRIBUTE: 9,
+};
+
+element.isElementCollection = function(seq) {
+ if (seq === null) {
+ return false;
+ }
+
+ const arrayLike = {
+ "[object Array]": 0,
+ "[object HTMLCollection]": 1,
+ "[object NodeList]": 2,
+ };
+
+ let typ = Object.prototype.toString.call(seq);
+ return typ in arrayLike;
+};
+
+element.makeWebElement = function(uuid) {
+ return {
+ [element.Key]: uuid,
+ [element.LegacyKey]: uuid,
+ };
+};
+
element.generateUUID = function() {
let uuid = uuidGen.generateUUID().toString();
return uuid.substring(1, uuid.length - 1);
};
/**
* This function generates a pair of coordinates relative to the viewport
* given a target element and coordinates relative to that element's