diff --git a/docs/en/uiautomator_uiselector.md b/docs/en/uiautomator_uiselector.md new file mode 100644 index 000000000..e41f3d1bf --- /dev/null +++ b/docs/en/uiautomator_uiselector.md @@ -0,0 +1,37 @@ +# uiautomator UiSelector + +Appium enables searching using [UiSelectors](http://developer.android.com/tools/help/uiautomator/UiSelector.html). +[UiScrollable](http://developer.android.com/tools/help/uiautomator/UiScrollable.html) +is also supported. + +Note that the index selector is unreliable so prefer instance instead. The +following examples are written against the api demos apk using Ruby. + + +Find the first textview. + +```ruby +first_textview = find_element(:uiautomator, 'new UiSelector().className("android.widget.TextView").instance(0)'); +``` + +Find the first element by text. + +```ruby +first_text = find_element(:uiautomator, 'new UiSelector().text("Animation")') +first_text.text # "Animation" +``` + +Find the first scrollable element, then find a TextView with the text "Tabs". +The "Tabs" element will be scrolled into view. + +```ruby +element = find_element(:uiautomator, 'new UiScrollable(new UiSelector().scrollable(true).instance(0)).getChildByText(new UiSelector().className("android.widget.TextView"), "Tabs")') +``` + +As a special case, scrollIntoView returns the element that is scrolled into view. +scrollIntoView allows scrolling to any UiSelector. + +```ruby +element = find_element(:uiautomator, 'new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text("WebView").instance(0));') +element.text # "WebView" +``` \ No newline at end of file diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/utils/UiAutomatorParser.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/utils/UiAutomatorParser.java index 71476279c..5461f4770 100644 --- a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/utils/UiAutomatorParser.java +++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/utils/UiAutomatorParser.java @@ -1,6 +1,7 @@ package io.appium.android.bootstrap.utils; import com.android.uiautomator.core.UiSelector; +import io.appium.android.bootstrap.Logger; import io.appium.android.bootstrap.exceptions.UiSelectorSyntaxException; import java.util.ArrayList; @@ -77,8 +78,10 @@ public class UiAutomatorParser { statement = text.substring(0, index); if (UiScrollableParser.isUiScrollable(statement)) { + Logger.debug("Parsing scrollable: " + statement); selectors.add(scrollableParser.parse(statement)); } else { + Logger.debug("Parsing selector: " + statement); selectors.add(selectorParser.parse(statement)); } diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/utils/UiScrollableParser.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/utils/UiScrollableParser.java index b030f5ee1..2f14565c7 100644 --- a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/utils/UiScrollableParser.java +++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/utils/UiScrollableParser.java @@ -3,6 +3,7 @@ package io.appium.android.bootstrap.utils; import com.android.uiautomator.core.UiObject; import com.android.uiautomator.core.UiScrollable; import com.android.uiautomator.core.UiSelector; +import io.appium.android.bootstrap.Logger; import io.appium.android.bootstrap.exceptions.UiSelectorSyntaxException; import java.lang.reflect.InvocationTargetException; @@ -183,6 +184,12 @@ public class UiScrollableParser { } private void applyArgsToMethod(Method method, ArrayList arguments) throws UiSelectorSyntaxException { + StringBuilder sb = new StringBuilder(); + for (String arg : arguments) { + sb.append(arg + ", "); + } + Logger.debug("UiScrollable invoking method: " + method + " args: " + sb.toString()); + if (method.getGenericReturnType() == UiScrollable.class && returnedUiObject) { throw new UiSelectorSyntaxException("Cannot call UiScrollable method \"" + method.getName() + "\" on a UiObject instance"); } @@ -208,17 +215,31 @@ public class UiScrollableParser { convertedArgs.add(coerceArgToType(parameterTypes[i], arguments.get(i))); } - if (method.getGenericReturnType() == UiScrollable.class) { + String methodName = method.getName(); + Logger.debug("Method name: " + methodName); + boolean scrollIntoView = methodName.contentEquals("scrollIntoView"); + + if (method.getGenericReturnType() == UiScrollable.class || scrollIntoView) { if (convertedArgs.size() > 1) { throw new UiSelectorSyntaxException("No UiScrollable method that returns type UiScrollable takes more than 1 argument"); } try { - scrollable = (UiScrollable)method.invoke(scrollable, convertedArgs.get(0)); + if (scrollIntoView) { + Logger.debug("Setting uiObject for scrollIntoView"); + UiSelector arg = (UiSelector) convertedArgs.get(0); + returnedUiObject = true; + uiObject = new UiObject(arg); + Logger.debug("Invoking method: " + method + " with: " + uiObject); + method.invoke(scrollable, uiObject); + Logger.debug("Invoke complete."); + } else { + scrollable = (UiScrollable)method.invoke(scrollable, convertedArgs.get(0)); + } } catch (IllegalAccessException e) { e.printStackTrace(); throw new UiSelectorSyntaxException("problem using reflection to call this method"); } catch (InvocationTargetException e) { - e.printStackTrace(); + Logger.error(e.getCause().toString()); // we're only interested in the cause. InvocationTarget wraps the underlying problem. throw new UiSelectorSyntaxException("problem using reflection to call this method"); } } @@ -259,6 +280,7 @@ public class UiScrollableParser { } private Object coerceArgToType(Type type, String argument) throws UiSelectorSyntaxException { + Logger.debug("UiScrollable coerce type: " + type + " arg: " + argument); if (type == boolean.class) { if (argument.equals("true")) { return true; @@ -288,7 +310,7 @@ public class UiScrollableParser { } } - if (type == UiSelector.class) { + if (type == UiSelector.class || type == UiObject.class) { UiSelectorParser parser = new UiSelectorParser(); return parser.parse(argument); } diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/utils/UiSelectorParser.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/utils/UiSelectorParser.java index 63765eee8..afe451d28 100644 --- a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/utils/UiSelectorParser.java +++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/utils/UiSelectorParser.java @@ -1,6 +1,7 @@ package io.appium.android.bootstrap.utils; import com.android.uiautomator.core.UiSelector; +import io.appium.android.bootstrap.Logger; import io.appium.android.bootstrap.exceptions.UiSelectorSyntaxException; import java.lang.reflect.InvocationTargetException; @@ -151,6 +152,7 @@ public class UiSelectorParser { } private Object coerceArgToType(Type type, String argument) throws UiSelectorSyntaxException { + Logger.debug("UiSelector coerce type: " + type + " arg: " + argument); if (type == boolean.class) { if (argument.equals("true")) { return true; diff --git a/test/functional/android/apidemos/find/by-uiautomator-specs.js b/test/functional/android/apidemos/find/by-uiautomator-specs.js index 0eda15946..bbdc7cd7e 100644 --- a/test/functional/android/apidemos/find/by-uiautomator-specs.js +++ b/test/functional/android/apidemos/find/by-uiautomator-specs.js @@ -153,6 +153,13 @@ describe("apidemo - find elements - by uiautomator", function () { text.should.equal("Views"); }).nodeify(done); }); + it('should allow UiScrollable scrollIntoView', function (done) { + driver.elementByAndroidUIAutomator('new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text("Views").instance(0));') + .text() + .then(function (text) { + text.should.equal("Views"); + }).nodeify(done); + }); it('should error reasonably if a UiScrollable does not return a UiObject', function (done) { driver.elementByAndroidUIAutomator('new UiScrollable(new UiSelector().scrollable(true).instance(0)).setMaxSearchSwipes(10)') .should.be.rejectedWith(/status: 9/)