From 6b0e9708ab2360c19c1e488301f49b78a15b5663 Mon Sep 17 00:00:00 2001 From: jonahss Date: Fri, 14 Mar 2014 17:33:14 -0700 Subject: [PATCH] add -ios_uiautomation locator strategy --- docs/finding-elements.md | 13 +++ .../android/bootstrap/handler/Find.java | 33 ++++--- lib/devices/common.js | 5 + lib/devices/ios/ios-controller.js | 37 +++++--- lib/devices/ios/ios.js | 2 +- .../android/apidemos/find-element-specs.js | 18 ++-- .../ios/uicatalog/find-element-specs.js | 94 +++++++++++++++++++ 7 files changed, 165 insertions(+), 37 deletions(-) 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); + }); + }); + }); + }); });