One of the main innovations in Illuminator is the way that elements can be accessed: selectors. Assume you have a root element mainWindow
:
var mainWindow = UIATarget.localTarget().frontMostApp().mainWindow();
Normally in UIAutomation, to access a screen element you must access it by direct reference:
var myButton = mainWindow.tableViews()["My table"].cells()["My cell"]buttons()["My button"];
A common problem in UIAutomation is that referring to an element in this way can leave you with a UIAElementNil
if your desired element has not yet been placed on the screen. To allow for this, Illuminator uses a selector to indirectly refer to a given screen element; selectors can be passed as data, and evaluated repeatedly until the element to which they refer becomes available.
There are 4 types of selectors in Illuminator, all working relative to a parent element. 2 are "Hard" selectors, and 2 are "soft" selectors. Hard selectors make direct reference to one and only one element, while soft selectors describe general descriptions that may refer to several distinct elements at once.
A lookup function takes the parent element as an argument and returns a child element. In the "My button"
example, it would look like the following:
var mySelector = function (myMainWindow) {
return myMainWindow.tableViews()["My table"].cells()["My cell"]buttons()["My button"];
};
var myButton = mainWindow.getChildElement(mySelector);
Lookup functions are the fastest type of selector, and will return one and only one element (UIAElementNil
or otherwise).
Note
Lookup functions are designed to work relative to a specific element (in this case,
mainWindow
).
A string selector provides comparable speed to a lookup function, but without the extra syntax required to write a function. The string is eval
'd, with the parent element in the scope as element
.
var mySelector = 'element.tableViews()["My table"].cells()["My cell"]buttons()["My button"]';
var myButton = mainWindow.getChildElement(mySelector);
Whether the use of eval
is good programming practice is beyond the scope of this document.
A criteria selector is a javascript object containing values that will be matched against each element in the tree beneath the parent element.
Note: due to extremely poor performance in UIAutomation's javascript performance in iOS 8.x (5ms to evaluate some object methods instead of ~0ms), the future reliability of soft selectors is threatened (by Apple). As a compromise, iOS 8 searches are done using only the
.elements()
array instead of element-specific lookups.
var mySelector = {name: "My button", UIAType: "UIAButton"};
var myButton = mainWindow.getChildElement(mySelector);
The following criteria fields are accepted:
UIAType
, matching the class name of the UIAElementnameRegex
, a regular expression that will be applied to thename()
method- One of the following property names matching the actual value of that property:
rect
hasKeyboardFocus
isEnabled
isValid
isVisible
label
name
value
Criteria selectors can be much slower than lookup functions or string selectors (0.2-0.7 seconds is typical, depending on how many elements are in the tree), but have the advantage of being resilient to any changes in the app's element hierarchy.
Unlike lookup functions or strings, selectors can return multiple elements, or none. Because of the relatively costly access time, Illuminator logs to the console the direct references to any elements found by evaluating a selector.
To help constrain the number of elements returned by a critera selector (in cases where there may be several matches), criteria can be chained together in array form. This can be an expensive operation, as all elements returned by the first selector in the array will be used as parent elements for the second selector in the array, and so on.
var mySelector = [{name: "My table", UIAType: "UIATableView"},
{name: "My cell", UIAType: "UIATableCell"},
{name: "My button", UIAType: "UIAButton"}];
var myButton = mainWindow.getChildElement(mySelector);
For convenience, it's possible to extend the set of fields that a criteria selector can respond to. This is done by overriding the UIAElement
's prototype preProcessSelector
function with a user-supplied function. The function should take a criteria object (or array of criteria objects), and map any custom fields into the known fields described above.
This trivial preprocessor example intercepts a custom field -- a currency amount
specified as a float -- and converts it to a condition on the name
field.
function preProcessSelectorWithCurrency(originalSelector) {
if (isHardSelector(originalSelector)) return originalSelector;
// simplify case by making everything an array
var selector;
if (originalSelector instanceof Array) {
selector = originalSelector;
} else {
selector = [originalSelector];
}
var outputSelector = [];
// iterate over the selectors in the chain
for (var i = 0; i < selector.length; ++i) {
// iterate over the fields in one criteria selector and build the modified criteria
var criteria = {};
for (var k in selector[i]) {
// keep all fields the same, but if we encounter "amount" then convert it
var v = selector[i][k];
if (key == "amount") {
criteria["name"] = "$" + v.toFixed(2);
} else {
criteria[k] = v;
}
}
outputSelector.push(criteria);
}
return outputSelector;
}
// immediately place this function inside prototype
UIAElement.prototype["preProcessSelector"] = preProcessSelectorWithCurrency;