mirror of
https://github.com/appium/appium.git
synced 2026-02-15 06:20:22 -06:00
Merge pull request #3428 from Jonahss/xpathInside
Moved xpath into boostrap java
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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'
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user