Merge pull request #3428 from Jonahss/xpathInside

Moved xpath into boostrap java
This commit is contained in:
Jonah
2014-08-21 11:31:25 -07:00
22 changed files with 424 additions and 452 deletions

View File

@@ -18,9 +18,8 @@ Language | Source
[php]: https://github.com/appium/php-client
[nuget]: http://www.nuget.org/packages/Appium.WebDriver/
Note that some methods such as `endTestCoverage()` and `complexFind()` are
Note that some methods such as `endTestCoverage()` are
not generally useful. Proper coverage support will be added once [this issue](https://github.com/appium/appium/issues/2448)
is resolved. `complexFind()` will be removed once [this issue](https://github.com/appium/appium/issues/2264)
is resolved. If you want to use them anyway, consult the documentation for the bindings on GitHub.
### Lock

View File

@@ -7,14 +7,11 @@ var errors = require('../../server/errors.js')
, helpers = require('../../helpers.js')
, status = require('../../server/status.js')
, NotYetImplementedError = errors.NotYetImplementedError
, exec = require('child_process').exec
, fs = require('fs')
, temp = require('temp')
, async = require('async')
, mkdirp = require('mkdirp')
, path = require('path')
, xpath = require("xpath")
, XMLDom = require("xmldom")
, AdmZip = require("adm-zip")
, helpers = require('../../helpers.js')
, Args = require("vargs").Constructor;
@@ -58,15 +55,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);
};
@@ -93,63 +84,6 @@ androidController.findElementsFromElement = function (element, strategy, selecto
this.findUIElementOrElements(strategy, selector, true, element, cb);
};
var _instanceAndClassFromDomNode = function (node) {
var androidClass = node.getAttribute("class");
var instance = node.getAttribute("instance");
// if we can't get the class and instance, return nothing, since we cannot select the node
// this should only happen for the wrapping <hierarchy> tag.
if (!androidClass || !instance) {
return null;
}
return {class: androidClass, instance: instance};
};
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,
multiple: many
};
this.proxy(["find", findParams], cb);
}.bind(this));
};
androidController.setValueImmediate = function (elementId, value, cb) {
cb(new NotYetImplementedError(), null);
};
@@ -318,15 +252,6 @@ androidController.getCssProperty = function (elementId, propertyName, cb) {
cb(new NotYetImplementedError(), null);
};
var _updateSourceXMLNodeNames = function (source) {
var newSource;
var origDom = new XMLDom.DOMParser().parseFromString(source);
var newDom = new XMLDom.DOMImplementation().createDocument(null);
_annotateXmlNodes(newDom, newDom, origDom);
newSource = new XMLDom.XMLSerializer().serializeToString(newDom);
return newSource;
};
var _getNodeClass = function (node) {
var nodeClass = null;
_.each(node.attributes, function (attr) {
@@ -371,53 +296,7 @@ var _annotateXmlNodes = function (newDom, newParent, oldNode, instances) {
};
androidController.getPageSource = function (cb) {
var xmlFile = temp.path({suffix: '.xml'});
var onDeviceXmlPath = this.dataDir + '/local/tmp/dump.xml';
async.series(
[
function (cb) {
this.proxy(["dumpWindowHierarchy"], cb);
}.bind(this),
function (cb) {
var cmd = this.adb.adbCmd + ' pull ' + onDeviceXmlPath + ' "' + xmlFile + '"';
logger.debug('transferPageSourceXML command: ' + cmd);
exec(cmd, { maxBuffer: 524288 }, function (err, stdout, stderr) {
if (err) {
logger.error(stderr);
return cb(err);
}
cb(null);
});
}.bind(this)
],
// Top level cb
function (err) {
if (err) return cb(err);
var xml = '';
if (fs.existsSync(xmlFile)) {
xml = fs.readFileSync(xmlFile, 'utf8');
fs.unlinkSync(xmlFile);
}
// xml file may not exist or it could be empty.
if (xml === '') {
var error = "dumpWindowHierarchy failed";
logger.error(error);
return cb(error);
}
try {
xml = _updateSourceXMLNodeNames(xml);
} catch (e) {
logger.error(e);
return cb(e);
}
cb(null, {
status: status.codes.Success.code
, value: xml
});
});
this.proxy(["source", {}], cb);
};
androidController.getAlertText = function (cb) {

View File

@@ -38,7 +38,6 @@ class AndroidCommandExecutor {
map.put("getSize", new GetSize());
map.put("wake", new Wake());
map.put("pressBack", new PressBack());
map.put("dumpWindowHierarchy", new DumpWindowHierarchy());
map.put("pressKeyCode", new PressKeyCode());
map.put("longPressKeyCode", new LongPressKeyCode());
map.put("takeScreenshot", new TakeScreenshot());
@@ -46,6 +45,7 @@ class AndroidCommandExecutor {
map.put("getDataDir", new GetDataDir());
map.put("performMultiPointerGesture", new MultiPointerGesture());
map.put("openNotification", new OpenNotification());
map.put("source", new Source());
}
/**

View File

@@ -165,7 +165,8 @@ class SocketServer {
} else if (cmd.commandType() == AndroidCommandType.ACTION) {
try {
res = executor.execute(cmd);
} catch (final Exception e) {
} catch (final Exception e) { // Here we catch all possible exceptions and return a JSON Wire Protocol UnknownError
// This prevents exceptions from halting the bootstrap app
res = new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage());
}
} else {

View File

@@ -0,0 +1,8 @@
package io.appium.android.bootstrap.exceptions;
/**
* If an invalid element selector is encountered
*/
public class InvalidSelectorException extends Throwable {
public InvalidSelectorException(String message) { super(message); }
}

View File

@@ -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); }
}

View File

@@ -1,65 +0,0 @@
package io.appium.android.bootstrap.handler;
import android.os.Environment;
import com.android.uiautomator.core.UiDevice;
import io.appium.android.bootstrap.AndroidCommand;
import io.appium.android.bootstrap.AndroidCommandResult;
import io.appium.android.bootstrap.CommandHandler;
import io.appium.android.bootstrap.utils.NotImportantViews;
import java.io.File;
/**
* This handler is used to dumpWindowHierarchy.
* https://android.googlesource.com/
* platform/frameworks/testing/+/master/uiautomator
* /library/core-src/com/android/uiautomator/core/UiDevice.java
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
public class DumpWindowHierarchy extends CommandHandler {
// Note that
// "new File(new File(Environment.getDataDirectory(), "local/tmp"), fileName)"
// is directly from the UiDevice.java source code.
private static final File dumpFolder = new File(Environment.getDataDirectory(), "local/tmp");
private static final String dumpFileName = "dump.xml";
private static final File dumpFile = new File(dumpFolder, dumpFileName);
private static void deleteDumpFile() {
if (dumpFile.exists()) {
dumpFile.delete();
}
}
public static boolean dump() {
dumpFolder.mkdirs();
deleteDumpFile();
try {
// dumpWindowHierarchy often has a NullPointerException
UiDevice.getInstance().dumpWindowHierarchy(dumpFileName);
} catch (Exception e) {
e.printStackTrace();
// If there's an error then the dumpfile may exist and be empty.
deleteDumpFile();
}
return dumpFile.exists();
}
/*
* @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)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command) {
NotImportantViews.discard(true);
return getSuccessResult(dump());
}
}

View File

@@ -1,25 +1,26 @@
package io.appium.android.bootstrap.handler;
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.ElementNotFoundException;
import io.appium.android.bootstrap.exceptions.InvalidSelectorException;
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.ClassInstancePair;
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.XMLHierarchy;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import javax.xml.parsers.ParserConfigurationException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.regex.Pattern;
import static io.appium.android.bootstrap.utils.API.API_18;
@@ -33,7 +34,6 @@ import static io.appium.android.bootstrap.utils.API.API_18;
public class Find extends CommandHandler {
// These variables are expected to persist across executions.
AndroidElementsHash elements = AndroidElementsHash.getInstance();
Dynamic dynamic = new Dynamic();
static JSONObject apkStrings = null;
UiAutomatorParser uiAutomatorParser = new UiAutomatorParser();
/**
@@ -74,134 +74,12 @@ public class Find extends CommandHandler {
}
final String contextId = (String) params.get("context");
if (strategy == Strategy.DYNAMIC) {
Logger.debug("Finding dynamic.");
final JSONArray selectors = (JSONArray) params.get("selector");
final String option = selectors.get(0).toString().toLowerCase();
final boolean all = option.contentEquals("all");
Logger.debug("Returning all? " + all);
UiScrollable scrollable = null;
final boolean scroll = option.contentEquals("scroll");
boolean canScroll = true;
if (scroll) {
UiSelector scrollableListView = new UiSelector().className(
android.widget.ListView.class).scrollable(true);
if (!new UiObject(scrollableListView).exists()) {
// Select anything that's scrollable if there's no list view.
scrollableListView = new UiSelector().scrollable(true);
}
// Nothing scrollable exists.
if (!new UiObject(scrollableListView).exists()) {
// we're not going to scroll
canScroll = false;
}
scrollable = new UiScrollable(scrollableListView).setAsVerticalList();
}
Logger.debug("Scrolling? " + scroll);
// Return the first element of the first selector that matches.
Logger.debug(selectors.toString());
try {
int finalizer = 0;
JSONArray pair;
List<AndroidElement> elementResults = new ArrayList<AndroidElement>();
final JSONArray jsonResults = new JSONArray();
// Start at 1 to skip over all.
for (int selIndex = all || scroll ? 1 : 0; selIndex < selectors
.length(); selIndex++) {
Logger.debug("Parsing selector " + selIndex);
pair = (JSONArray) selectors.get(selIndex);
Logger.debug("Pair is: " + pair);
UiSelector sel;
// 100+ int represents a method called on the element
// after the element has been found.
// [[4,"android.widget.EditText"],[100]] => 100
final int int0 = pair.getJSONArray(pair.length() - 1).getInt(0);
Logger.debug("int0: " + int0);
sel = dynamic.get(pair);
Logger.debug("Selector: " + sel.toString());
if (int0 >= 100) {
finalizer = int0;
Logger.debug("Finalizer " + Integer.toString(int0));
}
try {
// fetch will throw on not found.
if (finalizer != 0) {
if (all) {
Logger.debug("Finding all with finalizer");
List<AndroidElement> eles = elements.getElements(
sel, contextId);
Logger.debug("Elements found: " + eles);
for (final String found : Dynamic.finalize(eles, finalizer)) {
jsonResults.put(found);
}
continue;
} else {
final AndroidElement ele = elements.getElement(sel, contextId);
final String result = Dynamic.finalize(ele, finalizer);
return getSuccessResult(result);
}
}
if (all) {
for (AndroidElement e : elements.getElements(sel, contextId)) {
elementResults.add(e);
}
continue;
} else if (scroll && canScroll) {
Logger.debug("Scrolling into view...");
final boolean result = scrollable.scrollIntoView(sel);
if (!result) {
continue; // try scrolling next selector
}
// return the element we've scrolled to
return getSuccessResult(fetchElement(sel, contextId));
} else {
return getSuccessResult(fetchElement(sel, contextId));
}
} catch (final ElementNotFoundException enf) {
Logger.debug("Not found.");
}
} // end for loop
if (all) {
// matching on multiple selectors may return duplicate elements
elementResults = ElementHelpers.dedupe(elementResults);
for (final AndroidElement el : elementResults) {
jsonResults.put(new JSONObject().put("ELEMENT", el.getId()));
}
return getSuccessResult(jsonResults);
}
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
"No such element.");
} catch (final Exception e) {
final String errorMessage = e.getMessage();
if (errorMessage != null
&& errorMessage
.contains("UiAutomationService not connected. Did you call #register()?")) {
// Crash on not connected so Appium restarts the bootstrap jar.
throw new RuntimeException(e);
}
return getErrorResult(errorMessage);
}
}
final String text = (String) params.get("selector");
final boolean multiple = (Boolean) params.get("multiple");
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);
} else {
NotImportantViews.discard(false);
}
try {
Object result = null;
List<UiSelector> selectors = getSelectors(strategy, text, multiple);
@@ -211,7 +89,7 @@ public class Find extends CommandHandler {
try {
Logger.debug("Using: " + sel.toString());
result = fetchElement(sel, contextId);
} catch (final ElementNotFoundException e) {
} catch (final ElementNotFoundException ignored) {
}
if (result != null) {
break;
@@ -226,7 +104,7 @@ public class Find extends CommandHandler {
Logger.debug("Using: " + sel.toString());
List<AndroidElement> elementsFromSelector = fetchElements(sel, contextId);
foundElements.addAll(elementsFromSelector);
} catch (final UiObjectNotFoundException e) {
} catch (final UiObjectNotFoundException ignored) {
}
}
if (strategy == Strategy.ANDROID_UIAUTOMATOR) {
@@ -248,6 +126,10 @@ public class Find extends CommandHandler {
return new AndroidCommandResult(WDStatus.UNKNOWN_COMMAND, e.getMessage());
} catch (final ElementNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT, e.getMessage());
} catch (ParserConfigurationException e) {
return getErrorResult("Error parsing xml hierarchy dump: " + e.getMessage());
} catch (InvalidSelectorException e) {
return new AndroidCommandResult(WDStatus.INVALID_SELECTOR, e.getMessage());
}
}
@@ -270,28 +152,6 @@ public class Find extends CommandHandler {
return res.put("ELEMENT", el.getId());
}
/**
* Get a single element by its index and its parent indexes. Used to resolve
* an xpath query
*
* @param indexPath
* @return
* @throws ElementNotFoundException
* @throws JSONException
*/
private JSONObject fetchElementByClassAndInstance(final String indexPath)
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];
UiSelector sel = new UiSelector().className(androidClass).instance(Integer.parseInt(instance));
return fetchElement(sel, "");
}
/**
* Get an array of AndroidElement objects from the {@link AndroidElementsHash}
*
@@ -323,37 +183,6 @@ public class Find extends CommandHandler {
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>(
Arrays.asList(selector.split(",")));
final JSONArray resArray = new JSONArray();
JSONObject resEl = new JSONObject();
for (final String indexPath : indexPaths) {
try {
resEl = fetchElementByClassAndInstance(indexPath);
resArray.put(resEl);
} catch (final JSONException e) {
return new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage());
} catch (final ElementNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
e.getMessage());
}
}
if (multiple) {
return getSuccessResult(resArray);
} else {
return getSuccessResult(resEl);
}
}
/**
* Create and return a UiSelector based on the strategy, text, and how many
* you want returned.
@@ -370,11 +199,16 @@ public class Find extends CommandHandler {
*/
private List<UiSelector> getSelectors(final Strategy strategy,
final String text, final boolean many) throws InvalidStrategyException,
ElementNotFoundException, UiSelectorSyntaxException {
ElementNotFoundException, UiSelectorSyntaxException, ParserConfigurationException, InvalidSelectorException {
final List<UiSelector> selectors = new ArrayList<UiSelector>();
UiSelector sel = new UiSelector();
switch (strategy) {
case XPATH:
for (UiSelector selector : getXPathSelectors(text, many)) {
selectors.add(selector);
}
break;
case CLASS_NAME:
sel = sel.className(text);
if (!many) {
@@ -434,7 +268,7 @@ public class Find extends CommandHandler {
selectors.add(sel);
break;
case ANDROID_UIAUTOMATOR:
List<UiSelector> parsedSelectors = new ArrayList<UiSelector>();
List<UiSelector> parsedSelectors;
try {
parsedSelectors = uiAutomatorParser.parse(text);
} catch (final UiSelectorSyntaxException e) {
@@ -475,4 +309,24 @@ public class Find extends CommandHandler {
return sel;
}
}
/** returns List of UiSelectors for an xpath expression **/
private List<UiSelector> getXPathSelectors(final String expression, final boolean multiple) throws ElementNotFoundException, ParserConfigurationException, InvalidSelectorException {
List<UiSelector> selectors = new ArrayList<UiSelector>();
ArrayList<ClassInstancePair> pairs = XMLHierarchy.getClassInstancePairs(expression);
if (!multiple) {
if (pairs.size() == 0) {
throw new NoSuchElementException();
}
selectors.add(pairs.get(0).getSelector());
} else {
for (ClassInstancePair pair : pairs) {
selectors.add(pair.getSelector());
}
}
return selectors;
}
}

View File

@@ -0,0 +1,47 @@
package io.appium.android.bootstrap.handler;
import io.appium.android.bootstrap.AndroidCommand;
import io.appium.android.bootstrap.AndroidCommandResult;
import io.appium.android.bootstrap.CommandHandler;
import io.appium.android.bootstrap.utils.XMLHierarchy;
import org.json.JSONException;
import org.w3c.dom.Document;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.StringWriter;
/**
* Get page source. Return as string of XML doc
*/
public class Source extends CommandHandler {
@Override
public AndroidCommandResult execute(AndroidCommand command) throws JSONException {
Document doc = (Document) XMLHierarchy.getFormattedXMLDoc();
TransformerFactory tf = TransformerFactory.newInstance();
StringWriter writer = new StringWriter();
Transformer transformer;
String xmlString;
try {
transformer = tf.newTransformer();
transformer.transform(new DOMSource(doc), new StreamResult(writer));
xmlString = writer.getBuffer().toString();
} catch (TransformerConfigurationException e) {
e.printStackTrace();
throw new RuntimeException("Something went terribly wrong while converting xml document to string");
} catch (TransformerException e) {
return getErrorResult("Could not parse xml hierarchy to string: " + e.getMessage());
}
return getSuccessResult(xmlString);
}
}

View File

@@ -12,8 +12,7 @@ public enum Strategy {
NAME("name"),
LINK_TEXT("link text"),
PARTIAL_LINK_TEXT("partial link text"),
INDEX_PATHS("index paths"),
DYNAMIC("dynamic"),
XPATH("xpath"),
ACCESSIBILITY_ID("accessibility id"),
ANDROID_UIAUTOMATOR("-android uiautomator");

View File

@@ -0,0 +1,32 @@
package io.appium.android.bootstrap.utils;
import com.android.uiautomator.core.UiSelector;
/**
* Simple class for holding a String 2-tuple. An android class, and instance number, used for finding elements by xpath.
*/
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;
}
public UiSelector getSelector() {
String androidClass = getAndroidClass();
String instance = getInstance();
return new UiSelector().className(androidClass).instance(Integer.parseInt(instance));
}
}

View File

@@ -0,0 +1,184 @@
package io.appium.android.bootstrap.utils;
import android.os.Environment;
import com.android.uiautomator.core.UiDevice;
import io.appium.android.bootstrap.exceptions.ElementNotFoundException;
import io.appium.android.bootstrap.exceptions.InvalidSelectorException;
import io.appium.android.bootstrap.exceptions.PairCreationException;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.HashMap;
/**
* Created by jonahss on 8/12/14.
*/
public abstract class XMLHierarchy {
public static ArrayList<ClassInstancePair> getClassInstancePairs(String xpathExpression) throws ElementNotFoundException, InvalidSelectorException, ParserConfigurationException {
XPath xpath = XPathFactory.newInstance().newXPath();
XPathExpression exp = null;
try {
exp = xpath.compile(xpathExpression);
} catch (XPathExpressionException e) {
throw new InvalidSelectorException(e.getMessage());
}
Node formattedXmlRoot;
formattedXmlRoot = getFormattedXMLDoc();
return getClassInstancePairs(exp, formattedXmlRoot);
}
public static ArrayList<ClassInstancePair> getClassInstancePairs(XPathExpression xpathExpression, Node root) throws ElementNotFoundException {
NodeList nodes;
try {
nodes = (NodeList) xpathExpression.evaluate(root, XPathConstants.NODESET);
} catch (XPathExpressionException e) {
e.printStackTrace();
throw new ElementNotFoundException("XMLWindowHierarchy could not be parsed: " + e.getMessage());
}
ArrayList<ClassInstancePair> pairs = new ArrayList<ClassInstancePair>();
for (int i = 0; i < nodes.getLength(); i++) {
if (nodes.item(i).getNodeType() == Node.ELEMENT_NODE) {
try {
pairs.add(getPairFromNode(nodes.item(i)));
} catch (PairCreationException e) { }
}
}
return pairs;
}
public static InputSource getRawXMLHierarchy() {
// Note that
// "new File(new File(Environment.getDataDirectory(), "local/tmp"), fileName)"
// is directly from the UiDevice.java source code.
final File dumpFolder = new File(Environment.getDataDirectory(), "local/tmp");
final String dumpFileName = "dump.xml";
final File dumpFile = new File(dumpFolder, dumpFileName);
dumpFolder.mkdirs();
dumpFile.delete();
//compression off by default TODO add this as a config option
NotImportantViews.discard(false);
try {
// dumpWindowHierarchy often has a NullPointerException
UiDevice.getInstance().dumpWindowHierarchy(dumpFileName);
} catch (Exception e) {
e.printStackTrace();
// If there's an error then the dumpfile may exist and be empty.
dumpFile.delete();
}
try {
return new InputSource(new FileReader(dumpFile));
} catch (FileNotFoundException e) {
e.printStackTrace();
throw new RuntimeException("Failed to Dump Window Hierarchy");
}
}
public static Node getFormattedXMLDoc() {
return formatXMLInput(getRawXMLHierarchy());
}
public static Node formatXMLInput(InputSource input) {
XPath xpath = XPathFactory.newInstance().newXPath();
Node root = null;
try {
root = (Node) xpath.evaluate("/", input, XPathConstants.NODE);
} catch (XPathExpressionException e) {
throw new RuntimeException("Could not read xml hierarchy: " + e.getMessage());
}
HashMap<String, Integer> instances = new HashMap<String, Integer>();
// rename all the nodes with their "class" attribute
// add an instance attribute
annotateNodes(root, instances);
return root;
}
private static ClassInstancePair getPairFromNode(Node node) throws PairCreationException {
NamedNodeMap attrElements = node.getAttributes();
String androidClass;
String instance;
try {
androidClass = attrElements.getNamedItem("class").getNodeValue();
instance = attrElements.getNamedItem("instance").getNodeValue();
} catch (Exception e) {
throw new PairCreationException("Could not create ClassInstancePair object: " + e.getMessage());
}
return new ClassInstancePair(androidClass, instance);
}
private static void annotateNodes(Node node, HashMap<String, Integer>instances) {
NodeList children = node.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
if (children.item(i).getNodeType() == Node.ELEMENT_NODE) {
visitNode(children.item(i), instances);
annotateNodes(children.item(i), instances);
}
}
}
// set the node's tag name to the same as it's android class.
// also number all instances of each class with an "instance" number. It increments for each class separately.
// this allows use to use class and instance to identify a node.
// we also take this chance to clean class names that might have dollar signs in them (and other odd characters)
private static void visitNode(Node node, HashMap<String, Integer> instances) {
Document doc = node.getOwnerDocument();
NamedNodeMap attributes = node.getAttributes();
String androidClass;
try {
androidClass = attributes.getNamedItem("class").getNodeValue();
} catch (Exception e) {
return;
}
androidClass = cleanTagName(androidClass);
if (!instances.containsKey(androidClass)) {
instances.put(androidClass, 0);
}
Integer instance = instances.get(androidClass);
Node attrNode = doc.createAttribute("instance");
attrNode.setNodeValue(instance.toString());
attributes.setNamedItem(attrNode);
doc.renameNode(node, node.getNamespaceURI(), androidClass);
instances.put(androidClass, instance+1);
}
private static String cleanTagName(String name) {
name = name.replaceAll("[$@#&]", ".");
return name.replaceAll("\\s", "");
}
}

File diff suppressed because one or more lines are too long

View File

@@ -216,7 +216,6 @@ exports.checkValidLocStrat = function (strat, includeWeb, cb) {
'xpath',
'id',
'name',
'dynamic',
'class name'
];
var nativeStrats = [
@@ -232,7 +231,7 @@ exports.checkValidLocStrat = function (strat, includeWeb, cb) {
];
var nativeDeprecations = {};
var webDeprecations = {};
var deprecations = {dynamic: '-android uiautomator or -ios uiautomation', name: 'accessibility id'};
var deprecations = {name: 'accessibility id'};
if (includeWeb) {
validStrats = validStrats.concat(webStrats);

View File

@@ -435,26 +435,6 @@ exports.mobileDrag = function (req, res) {
req.device.drag(startX, startY, endX, endY, duration, touchCount, element, destEl, getResponseHandler(req, res));
};
exports.find = function (req, res) {
var strategy = "dynamic"
, selector = req.body;
// some clients send the selector as the `selector` property of an object,
// rather than the whole body.
// selector ought to be an array
if (selector && !Array.isArray(selector)) {
selector = req.body.selector;
}
var all = selector && selector[0] && typeof selector[0] === "string" && selector[0].toLowerCase() === "all";
if (all) {
req.device.findElements(strategy, selector, getResponseHandler(req, res));
} else {
req.device.findElement(strategy, selector, getResponseHandler(req, res));
}
};
exports.mobileSwipe = function (req, res) {
req.body = _.defaults(req.body, {
touchCount: 1

View File

@@ -127,7 +127,6 @@ module.exports = function (appium) {
rest.post('/wd/hub/session/:sessionId?/appium/app/reset', controller.reset);
rest.post('/wd/hub/session/:sessionId?/appium/app/background', controller.background);
rest.post('/wd/hub/session/:sessionId?/appium/app/end_test_coverage', controller.endCoverage);
rest.post('/wd/hub/session/:sessionId?/appium/app/complex_find', controller.find);
rest.post('/wd/hub/session/:sessionId?/appium/app/strings', controller.getStrings);
rest.post('/wd/hub/session/:sessionId?/appium/element/:elementId?/value', controller.setValueImmediate);

View File

@@ -93,7 +93,7 @@
"ws": "~0.4.31",
"xml2js": "~0.4.4",
"xmldom": "~0.1.19",
"xpath": "~0.0.6"
"xpath": "0.0.7"
},
"scripts": {
"test": "grunt travis"

View File

@@ -58,16 +58,4 @@ describe("apidemos - attributes", function () {
});
// TODO: tests for checkable, checked, clickable, focusable, focused,
// longClickable, scrollable, selected
// TODO: fix that, the second scroll doesn't scroll far enough.
it('should be able to get selected value of a tab @skip-android-all', function (done) {
driver
.complexFind(["scroll", [[3, "views"]], [[7, "views"]]]).click()
.complexFind(["scroll", [[3, "tabs"]], [[7, "tabs"]]]).click()
.complexFind(["scroll", [[3, "content by id"]], [[7, "content by id"]]]).click()
.elementsByClassName("android.widget.TextView").then(function (els) {
els[0].getAttribute('selected').should.become('false'); // the 1st text is not selected
els[1].getAttribute('selected').should.become('true'); // tab 1 is selected
}).nodeify(done);
});
});

View File

@@ -38,21 +38,21 @@ describe("apidemo - find - by xpath", function () {
});
it('should find the last element', function (done) {
driver
.elementByXPath("//" + t + "[last()]").text()
.elementByXPath("(//" + t + ")[last()]").text()
.then(function (text) {
["OS", "Text", "Views"].should.include(text);
}).nodeify(done);
});
it('should find element by xpath index and child', function (done) {
driver
.elementByXPath("//" + f + "[1]/" + l + "[1]/" + t + "[3]").text()
.elementByXPath("//" + f + "[2]/" + l + "[1]/" + t + "[3]").text()
.should.become("App")
.nodeify(done);
});
it('should find element by index and embedded desc', function (done) {
driver
.elementByXPath("//" + f + "//" + t + "[4]").text()
.should.become("App")
.should.become("Content")
.nodeify(done);
});
it('should find all elements', function (done) {

View File

@@ -1,27 +0,0 @@
"use strict";
var setup = require("../../../common/setup-base")
, desired = require("../desired");
describe("apidemo - find - complex", function () {
var driver;
setup(this, desired).then(function (d) { driver = d; });
it('should scroll to an element by text or content desc', function (done) {
driver
.complexFind(["scroll", [[3, "views"]], [[7, "views"]]]).text()
.should.become("Views")
.nodeify(done);
});
it('should find a single element by content-description', function (done) {
driver.complexFind([[[7, "Animation"]]]).text()
.should.become("Animation")
.nodeify(done);
});
it('should find a single element by text', function (done) {
driver.complexFind([[[3, "Animation"]]]).text()
.should.become("Animation")
.nodeify(done);
});
});

View File

@@ -18,7 +18,7 @@ describe("apidemos - source", function () {
it('should return the page source', function (done) {
driver
.elementByNameOrNull('Accessibility') // waiting for page to load
.elementByAccessibilityId('Animation') // waiting for page to load
.source()
.then(function (source) {
assertSource(source);
@@ -26,11 +26,11 @@ describe("apidemos - source", function () {
});
it('should return the page source without crashing other commands', function (done) {
driver
.complexFind([[[3, "Animation"]]])
.elementByAccessibilityId('Animation')
.source().then(function (source) {
assertSource(source);
})
.complexFind([[[3, "Animation"]]])
.elementByAccessibilityId('Animation')
.nodeify(done);
});
});

View File

@@ -4,8 +4,6 @@ var common = require('../../lib/devices/common.js')
, checkValidLocStrat = common.checkValidLocStrat
, _ = require('underscore');
describe('devices/common.js', function () {
var nullCb = function () {};
@@ -31,7 +29,6 @@ describe('devices/common.js', function () {
'xpath',
'id',
'name',
'dynamic',
'class name'
];