mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1250102 - Rewrite element location to be promise-compatible; r=automatedtester
Element location is rewritten with this patch in order to make it compatible for use with promises. This makes consuming the API nicer in the wider context of Marionette, since it no longer takes callbacks and no longer has to be wrapped in external promises to be compatible with the new dispatching technique. MozReview-Commit-ID: DjZOXPqkZ5j
This commit is contained in:
parent
cb7c4c2028
commit
c188659970
@ -312,89 +312,131 @@ ElementManager.prototype = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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.
|
||||
* 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.
|
||||
*
|
||||
* @return nsIDOMElement or list of nsIDOMElements
|
||||
* Returns the element(s) by calling the on_success function.
|
||||
* 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 {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.
|
||||
*
|
||||
* @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));
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
|
||||
// serialise elements for return
|
||||
let rv = [];
|
||||
for (let el of foundEls) {
|
||||
let ref = this.addToKnownElements(el);
|
||||
let we = element.makeWebElement(ref);
|
||||
rv.push(we);
|
||||
}
|
||||
|
||||
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 [];
|
||||
},
|
||||
|
||||
/**
|
||||
@ -595,10 +637,107 @@ ElementManager.prototype = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
Loading…
Reference in New Issue
Block a user