Skip to content

Commit

Permalink
Xpath implementation moved into bootstrap java project. XPath now les…
Browse files Browse the repository at this point in the history
…s buggy and adheres to the full xpath spec. Finding by XPath now operates on the uncompressed xml hierarchy. Fixes #3307
  • Loading branch information
Jonahss committed Aug 20, 2014
1 parent fa30d90 commit 44e75bb
Show file tree
Hide file tree
Showing 11 changed files with 379 additions and 67 deletions.
48 changes: 4 additions & 44 deletions lib/devices/android/android-controller.js
Expand Up @@ -58,15 +58,9 @@ androidController.findUIElementOrElements = function (strategy, selector, many,
};

var doFind = function (findCb) {
if (strategy === "xpath") {
this.findUIElementsByXPath(selector, many, function (err, res) {
this.handleFindCb(err, res, many, findCb);
}.bind(this));
} else {
this.proxy(["find", params], function (err, res) {
this.handleFindCb(err, res, many, findCb);
}.bind(this));
}
this.proxy(["find", params], function (err, res) {
this.handleFindCb(err, res, many, findCb);
}.bind(this));
}.bind(this);
this.implicitWaitForCondition(doFind, cb);
};
Expand Down Expand Up @@ -107,47 +101,13 @@ var _instanceAndClassFromDomNode = function (node) {
};

androidController.findUIElementsByXPath = function (selector, many, cb) {
this.getPageSource(function (err, res) {
if (err || res.status !== status.codes.Success.code) return cb(err, res);
var dom, nodes;
var xmlSource = res.value;
try {
dom = new XMLDom.DOMParser().parseFromString(xmlSource);
nodes = xpath.select(selector, dom);
} catch (e) {
logger.error(e);
return cb(e);
}
var instanceClassPairs = _.map(nodes, _instanceAndClassFromDomNode);
instanceClassPairs = _.compact(instanceClassPairs);

if (!many) instanceClassPairs = instanceClassPairs.slice(0, 1);

if (!many && instanceClassPairs.length < 1) {
// if we don't have any matching nodes, and we wanted at least one, fail
return cb(null, {
status: status.codes.NoSuchElement.code,
value: null
});
} else if (instanceClassPairs.length < 1) {
// and if we don't have any matching nodes, return the empty array
return cb(null, {
status: status.codes.Success.code,
value: []
});
}

var selectorString = instanceClassPairs.map(function (pair) {
return pair.class + ":" + pair.instance;
}).join(",");

var findParams = {
strategy: "index paths",
selector: selectorString,
selector: selector,
multiple: many
};
this.proxy(["find", findParams], cb);
}.bind(this));
};

androidController.setValueImmediate = function (elementId, value, cb) {
Expand Down
@@ -0,0 +1,8 @@
package io.appium.android.bootstrap.exceptions;

/**
* For trying to create a ClassInstancePair and something goes wrong.
*/
public class PairCreationException extends Throwable {
public PairCreationException(String msg) { super(msg); }
}
Expand Up @@ -6,6 +6,7 @@
import io.appium.android.bootstrap.AndroidCommandResult;
import io.appium.android.bootstrap.CommandHandler;
import io.appium.android.bootstrap.utils.NotImportantViews;
import io.appium.android.bootstrap.utils.XMLCleanser;

import java.io.File;

Expand Down Expand Up @@ -38,6 +39,7 @@ public static boolean dump() {
try {
// dumpWindowHierarchy often has a NullPointerException
UiDevice.getInstance().dumpWindowHierarchy(dumpFileName);
XMLCleanser.cleanFile(dumpFile);
} catch (Exception e) {
e.printStackTrace();
// If there's an error then the dumpfile may exist and be empty.
Expand Down
Expand Up @@ -9,15 +9,14 @@
import io.appium.android.bootstrap.exceptions.InvalidStrategyException;
import io.appium.android.bootstrap.exceptions.UiSelectorSyntaxException;
import io.appium.android.bootstrap.selector.Strategy;
import io.appium.android.bootstrap.utils.ElementHelpers;
import io.appium.android.bootstrap.utils.NotImportantViews;
import io.appium.android.bootstrap.utils.UiAutomatorParser;
import io.appium.android.bootstrap.utils.*;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.List;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -195,9 +194,8 @@ public AndroidCommandResult execute(final AndroidCommand command)
Logger.debug("Finding " + text + " using " + strategy.toString()
+ " with the contextId: " + contextId + " multiple: " + multiple);

if (strategy == Strategy.INDEX_PATHS) {
NotImportantViews.discard(true);
return findElementsByIndexPaths(text, multiple);
if (strategy == Strategy.XPATH) {
return findElementsByXPath(text, multiple);
} else {
NotImportantViews.discard(false);
}
Expand Down Expand Up @@ -274,18 +272,16 @@ private JSONObject fetchElement(final UiSelector sel, final String contextId)
* Get a single element by its index and its parent indexes. Used to resolve
* an xpath query
*
* @param indexPath
* @param pair
* @return
* @throws ElementNotFoundException
* @throws JSONException
*/
private JSONObject fetchElementByClassAndInstance(final String indexPath)
private JSONObject fetchElementByClassAndInstance(ClassInstancePair pair)
throws ElementNotFoundException, JSONException {

// path looks like "className:instanceNumber" eg: "android.widget.Button:2"
String[] classInstancePair = indexPath.split(":");
String androidClass = classInstancePair[0];
String instance = classInstancePair[1];
String androidClass = pair.getAndroidClass();
String instance = pair.getInstance();

UiSelector sel = new UiSelector().className(androidClass).instance(Integer.parseInt(instance));

Expand Down Expand Up @@ -323,13 +319,8 @@ private JSONArray elementsToJSONArray(List<AndroidElement> els) throws JSONExcep
return resArray;
}

/**
* Get a find element result by looking through the paths of indexes used to
* retrieve elements from an XPath search
*
* @param selector
* @return
*/

/*
private AndroidCommandResult findElementsByIndexPaths(final String selector,
final Boolean multiple) {
final ArrayList<String> indexPaths = new ArrayList<String>(
Expand All @@ -353,6 +344,41 @@ private AndroidCommandResult findElementsByIndexPaths(final String selector,
return getSuccessResult(resEl);
}
}
*/

private AndroidCommandResult findElementsByXPath(final String expression, final Boolean multiple) {

ArrayList<ClassInstancePair> pairs;
try {
pairs = XMLHierarchy.getClassInstancePairs(expression);
} catch (ElementNotFoundException e) {
return new AndroidCommandResult(WDStatus.ELEMENT_IS_NOT_SELECTABLE, e.getMessage());
} catch (XPathExpressionException e) {
return new AndroidCommandResult(WDStatus.INVALID_SELECTOR, e.getMessage());
} catch (ParserConfigurationException e) {
return new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage());
}

try {
if (!multiple) {
if (pairs.size() == 0) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT);
}
JSONObject resEl = fetchElementByClassAndInstance(pairs.get(0));
return getSuccessResult(resEl);
} else {
JSONArray resArray = new JSONArray();
for (ClassInstancePair pair : pairs) {
resArray.put(fetchElementByClassAndInstance(pair));
}
return getSuccessResult(resArray);
}
} catch (ElementNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT, e.getMessage());
} catch (JSONException e) {
return new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage());
}
}

/**
* Create and return a UiSelector based on the strategy, text, and how many
Expand Down
Expand Up @@ -13,6 +13,7 @@ public enum Strategy {
LINK_TEXT("link text"),
PARTIAL_LINK_TEXT("partial link text"),
INDEX_PATHS("index paths"),
XPATH("xpath"),
DYNAMIC("dynamic"),
ACCESSIBILITY_ID("accessibility id"),
ANDROID_UIAUTOMATOR("-android uiautomator");
Expand Down
@@ -0,0 +1,23 @@
package io.appium.android.bootstrap.utils;

/**
* Created by jonahss on 8/12/14.
*/
public class ClassInstancePair {

private String androidClass;
private String instance;

public ClassInstancePair(String clazz, String inst) {
androidClass = clazz;
instance = inst;
}

public String getAndroidClass() {
return androidClass;
}

public String getInstance() {
return instance;
}
}
@@ -0,0 +1,38 @@
package io.appium.android.bootstrap.utils;

import java.io.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class XMLCleanser {

//
private static Pattern pattern = Pattern.compile("(<\\S*)(\\s*[#\\$]+[#\\$\\s]*)(\\S*>)");
private static Matcher matcher = pattern.matcher("");


public static void cleanFile(File dumpFile) throws IOException {
// the xml for windowHierarchy is all on one line. Adding newlines and rewriting to a file is probably just as
// inefficient as reading the whole file into memory for when we cleanse it

BufferedReader reader = new BufferedReader(new FileReader(dumpFile));
String line, wholeFile = "";
while ((line = reader.readLine()) != null) {
line = cleanLine(line);
wholeFile = wholeFile + line;
}
reader.close();

BufferedWriter writer = new BufferedWriter(new FileWriter(dumpFile));
writer.write(wholeFile);
writer.close();

}

public static String cleanLine(String line) {

matcher.reset(line);
return matcher.replaceAll("$1.$3");
}

}

0 comments on commit 44e75bb

Please sign in to comment.