diff --git a/docs/finding-elements.md b/docs/finding-elements.md
index 051692bf1..4b94244bd 100644
--- a/docs/finding-elements.md
+++ b/docs/finding-elements.md
@@ -5,8 +5,13 @@ Appium supports a subset of the WebDriver locator strategies:
* find by "tag name" (i.e., ui component type)
* find by "name" (i.e., the text, label, or developer-generated ID a.k.a 'accessibilityIdentifier' of an element)
+ NOTE: the "name" locator strategy will be deprecated on mobile devices, and will not be a part of Appium v1.0
* find by "xpath" (i.e., an abstract representation of a path to an element, with certain constraints)
+Appium additionally supports some of the [Mobile JSON Wire Protocol](https://code.google.com/p/selenium/source/browse/spec-draft.md?repo=mobile) locator strategies
+
+* `-ios_uiautomation`: a string corresponding to a recursive element search using the UIAutomation library (iOS-only)
+
###Tag name mapping
You can use the direct UIAutomation component type name for the tag name, or use the simplified mapping (used in some examples below) found here:
@@ -117,6 +122,14 @@ Python:
driver.find_elements_by_tag_name('tableCell')[5].click()
```
+### Using the -ios_uiautomation locator strategy
+
+WD.js:
+
+```js
+driver.element('-ios_uiautomation', '.elements()[1].cells()[2]').getAttribute('name');
+```
+
# FindAndAct
If you want, you can find and act on an element in a single command (iOS-only).
diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/Find.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/Find.java
index b0ca1cca9..3946723df 100644
--- a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/Find.java
+++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/Find.java
@@ -33,10 +33,10 @@ import com.android.uiautomator.core.UiSelector;
/**
* This handler is used to find elements in the Android UI.
- *
+ *
* Based on which {@link Strategy}, {@link UiSelector}, and optionally the
* contextId, the element Id or Ids are returned to the user.
- *
+ *
*/
public class Find extends CommandHandler {
// These variables are expected to persist across executions.
@@ -61,11 +61,11 @@ public class Find extends CommandHandler {
/*
* @param command The {@link AndroidCommand} used for this handler.
- *
+ *
* @return {@link AndroidCommandResult}
- *
+ *
* @throws JSONException
- *
+ *
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@@ -75,8 +75,13 @@ public class Find extends CommandHandler {
final Hashtable params = command.params();
// only makes sense on a device
- final Strategy strategy = Strategy.fromString((String) params
- .get("strategy"));
+ final Strategy strategy;
+ try {
+ strategy = Strategy.fromString((String) params
+ .get("strategy"));
+ } catch (final InvalidStrategyException e) {
+ return new AndroidCommandResult(WDStatus.UNKNOWN_COMMAND, e.getMessage());
+ }
final String contextId = (String) params.get("context");
if (strategy == Strategy.DYNAMIC) {
@@ -280,12 +285,12 @@ public class Find extends CommandHandler {
/**
* Get the element from the {@link AndroidElementsHash} and return the element
* id using JSON.
- *
+ *
* @param sel
* A UiSelector that targets the element to fetch.
* @param contextId
* The Id of the element used for the context.
- *
+ *
* @return JSONObject
* @throws JSONException
* @throws ElementNotFoundException
@@ -301,12 +306,12 @@ public class Find extends CommandHandler {
/**
* Get an array of elements from the {@link AndroidElementsHash} and return
* the element's ids using JSON.
- *
+ *
* @param sel
* A UiSelector that targets the element to fetch.
* @param contextId
* The Id of the element used for the context.
- *
+ *
* @return JSONObject
* @throws JSONException
* @throws UiObjectNotFoundException
@@ -326,7 +331,7 @@ public class Find extends CommandHandler {
/**
* Create and return a UiSelector based on the strategy, text, and how many
* you want returned.
- *
+ *
* @param strategy
* The {@link Strategy} used to search for the element.
* @param text
@@ -406,7 +411,7 @@ public class Find extends CommandHandler {
/**
* Create and return a UiSelector based on Xpath attributes.
- *
+ *
* @param path
* The Xpath path.
* @param attr
@@ -415,7 +420,7 @@ public class Find extends CommandHandler {
* Any constraint.
* @param substr
* Any substr.
- *
+ *
* @return UiSelector
* @throws AndroidCommandException
*/
diff --git a/lib/devices/common.js b/lib/devices/common.js
index 6c904a0d4..e7048f289 100644
--- a/lib/devices/common.js
+++ b/lib/devices/common.js
@@ -214,6 +214,11 @@ exports.checkValidLocStrat = function (strat, includeWeb, cb) {
'partial link text'
]);
}
+ if (!includeWeb) {
+ validStrats = validStrats.concat([
+ '-ios_uiautomation'
+ ]);
+ }
if (!_.contains(validStrats, strat)) {
logger.info("Invalid locator strategy: " + strat);
diff --git a/lib/devices/ios/ios-controller.js b/lib/devices/ios/ios-controller.js
index 12c6fe1c1..fdc099a7b 100644
--- a/lib/devices/ios/ios-controller.js
+++ b/lib/devices/ios/ios-controller.js
@@ -48,32 +48,41 @@ iOSController.findUIElementOrElements = function (strategy, selector, ctx, many,
var ext = many ? 's' : '';
var command = "";
- if (strategy === "name") {
- command = ["au.getElement", ext, "ByName('", selector, "'", ctx, ")"].join('');
- } else if (strategy === "xpath") {
- command = ["au.getElement", ext, "ByXpath('", selector, "'", ctx, ")"].join('');
- } else if (strategy === "id") {
- selector = selector.replace(/'/g, "\\\\'"); // must escape single quotes
- command = ["var exact = au.mainApp().getFirstWithPredicateWeighted(\"name == '", selector,
- "' || label == '", selector, "' || value == '", selector, "'\");"].join('');
- command += ["exact && exact.status == 0 ? exact : au.mainApp().getFirstWith",
- "PredicateWeighted(\"name contains[c] '", selector, "' || label contains[c] '",
- selector, "' || value contains[c] '", selector, "'\");"].join('');
- } else {
- command = ["au.getElement", ext, "ByType('", selector, "'", ctx, ")"].join('');
+ switch (strategy) {
+ case "name":
+ command = ["au.getElement", ext, "ByName('", selector, "'", ctx, ")"].join('');
+ break;
+ case "xpath":
+ command = ["au.getElement", ext, "ByXpath('", selector, "'", ctx, ")"].join('');
+ break;
+ case "id":
+ selector = selector.replace(/'/g, "\\\\'"); // must escape single quotes
+ command = ["var exact = au.mainApp().getFirstWithPredicateWeighted(\"name == '", selector,
+ "' || label == '", selector, "' || value == '", selector, "'\");"].join('');
+ command += ["exact && exact.status == 0 ? exact : au.mainApp().getFirstWith",
+ "PredicateWeighted(\"name contains[c] '", selector, "' || label contains[c] '",
+ selector, "' || value contains[c] '", selector, "'\");"].join('');
+ break;
+ case "-ios_uiautomation":
+ command = ["au.getElement", ext, "ByUIAutomation('", selector, "'", ctx, ")"].join('');
+ break;
+ default:
+ command = ["au.getElement", ext, "ByType('", selector, "'", ctx, ")"].join('');
}
this.proxy(command, function (err, res) {
this.handleFindCb(err, res, many, findCb);
}.bind(this));
}.bind(this);
+
+
if (_.contains(this.supportedStrategies, strategy)) {
this.waitForCondition(this.implicitWaitMs, doFind, cb);
} else {
cb(null, {
status: status.codes.UnknownError.code
, value: "Sorry, we don't support the '" + strategy + "' locator " +
- "strategy yet"
+ "strategy for ios yet"
});
}
};
diff --git a/lib/devices/ios/ios.js b/lib/devices/ios/ios.js
index f4e02c68a..a4f6fb651 100644
--- a/lib/devices/ios/ios.js
+++ b/lib/devices/ios/ios.js
@@ -89,7 +89,7 @@ IOS.prototype.init = function () {
this.curWebCoords = null;
this.onPageChangeCb = null;
this.dontDeleteSimApps = false;
- this.supportedStrategies = ["name", "tag name", "xpath", "id"];
+ this.supportedStrategies = ["name", "tag name", "xpath", "id", "-ios_uiautomation"];
this.localizableStrings = {};
this.keepAppToRetainPrefs = false;
};
diff --git a/test/functional/android/apidemos/find-element-specs.js b/test/functional/android/apidemos/find-element-specs.js
index cb8badcad..dfa7a1dad 100644
--- a/test/functional/android/apidemos/find-element-specs.js
+++ b/test/functional/android/apidemos/find-element-specs.js
@@ -154,14 +154,6 @@ describe("apidemo - find elements -", function () {
.should.become("App")
.nodeify(done);
});
- it('should get an error when strategy doesnt exist', function (done) {
- driver
- .elementByCss('button').catch(function (err) {
- err.cause.value.message.should.equal("Invalid locator strategy: css selector");
- throw err;
- }).should.be.rejectedWith(/status: 9/)
- .nodeify(done);
- });
});
describe('unallowed tag names', function () {
@@ -172,4 +164,14 @@ describe("apidemo - find elements -", function () {
.nodeify(done);
});
});
+ describe('invalid locator strategy', function () {
+ it('should not accept -ios_uiautomation locator strategy', function (done) {
+ driver
+ .elements('-ios_uiautomation', '.elements()').catch(function (err) {
+ throw JSON.stringify(err.cause.value);
+ })
+ .should.be.rejectedWith(/The requested resource could not be found/)
+ .nodeify(done);
+ });
+ });
});
diff --git a/test/functional/ios/uicatalog/find-element-specs.js b/test/functional/ios/uicatalog/find-element-specs.js
index dc9df2da0..7de5895ff 100644
--- a/test/functional/ios/uicatalog/find-element-specs.js
+++ b/test/functional/ios/uicatalog/find-element-specs.js
@@ -24,6 +24,12 @@ describe('uicatalog - find element -', function () {
.getAttribute('name').should.become("Buttons, Various uses of UIButton")
.nodeify(done);
});
+ it('should find a single element using elementByName', function (done) {
+ driver
+ .elementByName('UICatalog').then(function (el) {
+ el.should.exist;
+ }).nodeify(done);
+ });
it('should find an element within descendants', function (done) {
driver
.elementByTagName('tableView').then(function (el) {
@@ -93,6 +99,14 @@ describe('uicatalog - find element -', function () {
return driver.elementByTagName('tableCell').click();
};
+ if (process.env.FAST_TESTS) {
+ afterEach(function (done) {
+ driver
+ .back()
+ .nodeify(done);
+ });
+ }
+
it('should return the last button', function (done) {
driver
.resolve(setupXpath(driver))
@@ -166,4 +180,84 @@ describe('uicatalog - find element -', function () {
.nodeify(done);
});
});
+
+ describe('FindElement(s)ByUIAutomation', function () {
+
+ before(function (done) {
+ driver.element('-ios_uiautomation', '.navigationBars()[0]')
+ .getAttribute('name').then(function (name) {
+ if (name !== 'UICatalog') {
+ driver.back().then(done);
+ } else {
+ done();
+ }
+ });
+ });
+
+ it('should process most basic UIAutomation query', function (done) {
+ driver.elements('-ios_uiautomation', '.elements()').then(function (els) {
+ els.length.should.equal(2);
+ _(els).each(function (el) {
+ el.should.exist;
+ });
+ }).nodeify(done);
+ });
+ it('should process UIAutomation queries if user leaves out the first period', function (done) {
+ driver.elements('-ios_uiautomation', 'elements()').then(function (els) {
+ els.length.should.equal(2);
+ _(els).each(function (el) {
+ el.should.exist;
+ });
+ }).nodeify(done);
+ });
+ it('should get a single element', function (done) {
+ driver.element('-ios_uiautomation', '.elements()[0]').getAttribute('name')
+ .should.become('UICatalog')
+ .nodeify(done);
+ });
+ it('should get a single element', function (done) {
+ driver.element('-ios_uiautomation', '.elements()[1]').getAttribute('name')
+ .should.become('Empty list')
+ .nodeify(done);
+ });
+ it('should get single element as array', function (done) {
+ driver.elements('-ios_uiautomation', '.tableViews()[0]').then(function (els) {
+ els.length.should.equal(1);
+ }).nodeify(done);
+ });
+ it('should find elements by index multiple times', function (done) {
+ driver.element('-ios_uiautomation', '.elements()[1].cells()[2]').getAttribute('name')
+ .should.become('TextFields, Uses of UITextField')
+ .nodeify(done);
+ });
+ it('should find elements by name', function (done) {
+ driver.element('-ios_uiautomation', '.elements()["UICatalog"]').getAttribute('name')
+ .should.become('UICatalog')
+ .nodeify(done);
+ });
+ it('should find elements by name and index', function (done) {
+ driver.element('-ios_uiautomation', '.elements()["Empty list"].cells()[3]').getAttribute('name')
+ .should.become('SearchBar, Use of UISearchBar')
+ .nodeify(done);
+ });
+ describe('start from a given context instead of root target', function (done) {
+ it('should process a simple query', function (done) {
+ driver.element('-ios_uiautomation', '.elements()[1]').then(function (el) {
+ el.elements('-ios_uiautomation', '.elements()').then(function (els) {
+ els.length.should.equal(12);
+ _(els).each(function (el) {
+ el.should.exist;
+ });
+ }).nodeify(done);
+ });
+ });
+ it('should find elements by name', function (done) {
+ driver.element('-ios_uiautomation', '.elements()[1]').then(function (el) {
+ el.element('-ios_uiautomation', '.elements()["Buttons, Various uses of UIButton"]').then(function (el) {
+ el.should.exist;
+ }).nodeify(done);
+ });
+ });
+ });
+ });
});