Skip to content

Commit

Permalink
-android uiautomator locator strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
Jonahss committed Apr 2, 2014
1 parent e9bd140 commit ff30a52
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 28 deletions.
9 changes: 9 additions & 0 deletions docs/finding-elements.md
Expand Up @@ -11,6 +11,7 @@ Appium supports a subset of the WebDriver locator strategies:
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)
* `-android uiautomator`: a string corresponding to a recursive element search using the UiAutomator Api (Android-only)
* `accessibility id`: a string corresponding to a recursive element search using the Id/Name that the native Accessibility options utilize.

###Tag name mapping
Expand Down Expand Up @@ -131,6 +132,14 @@ WD.js:
driver.element('-ios uiautomation', '.elements()[1].cells()[2]').getAttribute('name');
```

### Using the "-android uiautomator" locator strategy

WD.js:

```js
driver.element('-android uiautomator', 'new UiSelector().clickable(true)').getAttribute('name');
```

# FindAndAct<a name="findandact"></a>

If you want, you can find and act on an element in a single command (iOS-only).
Expand Down
@@ -0,0 +1,17 @@
package io.appium.android.bootstrap.exceptions;

import io.appium.android.bootstrap.utils.UiSelectorParser;

/**
* An exception involving an {@link UiSelectorParser}.
*
* @param msg
* A descriptive message describing the error.
*/
@SuppressWarnings("serial")
public class UiSelectorSyntaxException extends Exception {

public UiSelectorSyntaxException(final String msg) {
super(msg);
}
}
@@ -1,37 +1,23 @@
package io.appium.android.bootstrap.handler;

import io.appium.android.bootstrap.AndroidCommand;
import io.appium.android.bootstrap.AndroidCommandResult;
import io.appium.android.bootstrap.AndroidElement;
import io.appium.android.bootstrap.AndroidElementClassMap;
import io.appium.android.bootstrap.AndroidElementsHash;
import io.appium.android.bootstrap.CommandHandler;
import io.appium.android.bootstrap.Dynamic;
import io.appium.android.bootstrap.Logger;
import io.appium.android.bootstrap.WDStatus;
import io.appium.android.bootstrap.exceptions.AndroidCommandException;
import io.appium.android.bootstrap.exceptions.ElementNotFoundException;
import io.appium.android.bootstrap.exceptions.ElementNotInHashException;
import io.appium.android.bootstrap.exceptions.InvalidStrategyException;
import io.appium.android.bootstrap.exceptions.UnallowedTagNameException;
import android.os.Build;
import com.android.uiautomator.core.UiObject;
import com.android.uiautomator.core.UiObjectNotFoundException;
import com.android.uiautomator.core.UiScrollable;
import com.android.uiautomator.core.UiSelector;
import io.appium.android.bootstrap.*;
import io.appium.android.bootstrap.exceptions.*;
import io.appium.android.bootstrap.selector.Strategy;
import io.appium.android.bootstrap.utils.UiSelectorParser;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.os.Build;

import com.android.uiautomator.core.UiObject;
import com.android.uiautomator.core.UiObjectNotFoundException;
import com.android.uiautomator.core.UiScrollable;
import com.android.uiautomator.core.UiSelector;

/**
* This handler is used to find elements in the Android UI.
*
Expand All @@ -44,6 +30,7 @@ public class Find extends CommandHandler {
AndroidElementsHash elements = AndroidElementsHash.getInstance();
Dynamic dynamic = new Dynamic();
public static JSONObject apkStrings = null;
UiSelectorParser uiSelectorParser = new UiSelectorParser();

private Object[] cascadeChildSels(final ArrayList<UiSelector> tail,
final ArrayList<String> tailOuts) {
Expand Down Expand Up @@ -277,6 +264,8 @@ public AndroidCommandResult execute(final AndroidCommand command)
return new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage());
} catch (final AndroidCommandException e) {
return new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage());
} catch (final UiSelectorSyntaxException e) {
return new AndroidCommandResult(WDStatus.UNKNOWN_COMMAND, e.getMessage());
} catch (final UiObjectNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
e.getMessage());
Expand Down Expand Up @@ -412,7 +401,7 @@ private AndroidCommandResult findElementsByIndexPaths(final String selector,
private List<UiSelector> getSelector(final Strategy strategy,
final String text, final boolean many) throws InvalidStrategyException,
AndroidCommandException, UnallowedTagNameException,
ElementNotFoundException {
ElementNotFoundException, UiSelectorSyntaxException {
final List<UiSelector> selectors = new ArrayList<UiSelector>();
UiSelector sel = new UiSelector();

Expand Down Expand Up @@ -477,6 +466,17 @@ private List<UiSelector> getSelector(final Strategy strategy,
break;
case XPATH:
break;
case ANDROID_UIAUTOMATOR:
try {
sel = uiSelectorParser.parse(text);
} catch (UiSelectorSyntaxException e) {
throw new UiSelectorSyntaxException("Could not parse UiSelector argument: " + e.getMessage());
}
if (!many) {
sel = sel.instance(0);
}
selectors.add(sel);
break;
case LINK_TEXT:
case PARTIAL_LINK_TEXT:
case CSS_SELECTOR:
Expand Down
Expand Up @@ -10,7 +10,7 @@ public enum Strategy {
CLASS_NAME(0, "class name"), CSS_SELECTOR(1, "css selector"), ID(2, "id"), NAME(
3, "name"), LINK_TEXT(4, "link text"), PARTIAL_LINK_TEXT(5,
"partial link text"), TAG_NAME(6, "tag name"), XPATH(7, "xpath"), DYNAMIC(
8, "dynamic"), ACCESSIBILITY_ID(9, "accessibility id");
8, "dynamic"), ACCESSIBILITY_ID(9, "accessibility id"), ANDROID_UIAUTOMATOR(10, "-android uiautomator");

public static Strategy fromString(final String text) throws InvalidStrategyException {
if (text != null) {
Expand Down
@@ -0,0 +1,190 @@
package io.appium.android.bootstrap.utils;

import com.android.uiautomator.core.UiSelector;
import io.appium.android.bootstrap.exceptions.UiSelectorSyntaxException;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;

/**
* For parsing strings passed in for the "-android uiautomator" locator strategy
*/
public class UiSelectorParser {

private String text;
private UiSelector selector;
private final static Method[] methods = UiSelector.class.getDeclaredMethods();

public UiSelector parse(String textToParse) throws UiSelectorSyntaxException {
selector = new UiSelector();
text = cleanseText(textToParse);

while (text.length() > 0) {
consumePeriod();
consumeFunctionCall();
}

return selector;
}

// prepares text for the main parsing loop
private String cleanseText(String dirtyText) {
String cleanText = dirtyText.trim();

if (cleanText.startsWith("new UiSelector()")) {
cleanText = cleanText.substring(16);
}
else if (cleanText.startsWith("UiSelector()")) {
cleanText = cleanText.substring(12);
}
else if (!cleanText.startsWith(".")){
cleanText = "." + cleanText;
}

return cleanText;
}

private void consumePeriod() throws UiSelectorSyntaxException {
if (text.startsWith(".")) {
text = text.substring(1);
}
else {
throw new UiSelectorSyntaxException("Expected \".\" but saw \"" + text.charAt(0) + "\"");
}
}

/*
* consume [a-z]* then an open paren, this is our methodName
* consume .* and count open/close parens until the original open paren is close, this is our argument
*
*/
private void consumeFunctionCall() throws UiSelectorSyntaxException {
String methodName;
StringBuilder argument = new StringBuilder();

int parenIndex = text.indexOf('(');
methodName = text.substring(0, parenIndex);

int index = parenIndex+1;
int parenCount = 1;
while (parenCount > 0) {
try {
switch (text.charAt(index)) {
case ')':
parenCount--;
if (parenCount > 0) {
argument.append(text.charAt(index));
}
break;
case '(':
parenCount++;
argument.append(text.charAt(index));
break;
default:
argument.append(text.charAt(index));
}
} catch (StringIndexOutOfBoundsException e) {
throw new UiSelectorSyntaxException("unclosed paren in expression");
}
index++;
}
if (argument.length() < 1) {
throw new UiSelectorSyntaxException(methodName + " method expects an argument");
}

//add two for parentheses surrounding arg
text = text.substring(methodName.length() + argument.length() + 2);

ArrayList<Method> overloadedMethods = getSelectorMethods(methodName);
if (overloadedMethods.size() < 1) {
throw new UiSelectorSyntaxException("UiSelector has no " + methodName + " method");
}

selector = applyArgToMethods(overloadedMethods, argument.toString());
}

private ArrayList<Method> getSelectorMethods(String methodName) {
ArrayList<Method> ret = new ArrayList<Method>();
for (Method method : methods) {
if (method.getName().equals(methodName)) {
ret.add(method);
}
}
return ret;
}

private UiSelector applyArgToMethods(ArrayList<Method> methods, String argument) throws UiSelectorSyntaxException {

Object arg = null;
Method ourMethod = null;
UiSelectorSyntaxException exThrown = null;
for (Method method : methods) {
try {
Type parameterType = method.getGenericParameterTypes()[0];
arg = coerceArgToType(parameterType, argument);
ourMethod = method;
} catch (UiSelectorSyntaxException e) {
exThrown = e;
}
}

if (ourMethod == null || arg == null) {
if (exThrown != null) {
throw exThrown;
} else {
throw new UiSelectorSyntaxException("Could not apply argument " + argument + " to UiSelector method");
}
}

try {
return (UiSelector)ourMethod.invoke(selector, arg);
} catch (IllegalAccessException e) {
e.printStackTrace();
throw new UiSelectorSyntaxException("problem using reflection to call this method");
} catch (InvocationTargetException e) {
e.printStackTrace();
throw new UiSelectorSyntaxException("problem using reflection to call this method");
}

}

private Object coerceArgToType(Type type, String argument) throws UiSelectorSyntaxException {
if (type == boolean.class) {
if (argument.equals("true")) {
return true;
}
if (argument.equals("false")) {
return false;
}
throw new UiSelectorSyntaxException(argument + " is not a boolean");
}

if (type == String.class) {
if (argument.charAt(0) != '"' || argument.charAt(argument.length()-1) != '"') {
throw new UiSelectorSyntaxException(argument + " is not a string");
}
return argument.substring(1, argument.length()-1);
}

if (type == int.class) {
return Integer.parseInt(argument);
}

if (type.toString().equals("java.lang.Class<T>")) {
try {
return Class.forName(argument);
} catch (ClassNotFoundException e) {
throw new UiSelectorSyntaxException(argument + " class could not be found");
}
}

if (type == UiSelector.class) {
UiSelectorParser parser = new UiSelectorParser();
return parser.parse(argument);
}

throw new UiSelectorSyntaxException("Could not coerce " + argument + " to any sort of Type");
}
}
3 changes: 2 additions & 1 deletion lib/devices/common.js
Expand Up @@ -211,7 +211,8 @@ exports.checkValidLocStrat = function (strat, includeWeb, cb) {
var nativeStrats = [
'-ios uiautomation',
'accessibility id',
'-real xpath'
'-real xpath',
'-android uiautomator'
];
var webStrats = [
'link text',
Expand Down

0 comments on commit ff30a52

Please sign in to comment.