mirror of
https://github.com/appium/appium.git
synced 2026-02-09 03:09:02 -06:00
The old compressXml did not presist across uiautomator sessions. By requesting 'DumpWindowHierarchyCompressed' it ensures the result is always compressed, even if the server had been restarted. For some reason if compressXml is disabled then the xpath index on Android will return the wrong result.
1098 lines
31 KiB
JavaScript
1098 lines
31 KiB
JavaScript
"use strict";
|
|
|
|
var errors = require('../../server/errors.js')
|
|
, _ = require('underscore')
|
|
, logger = require('../../server/logger.js').get('appium')
|
|
, deviceCommon = require('../common.js')
|
|
, helpers = require('../../helpers.js')
|
|
, jwpSuccess = deviceCommon.jwpSuccess
|
|
, 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")
|
|
, helpers = require('../../helpers.js')
|
|
, warnDeprecated = helpers.logDeprecationWarning;
|
|
|
|
var androidController = {};
|
|
var NATIVE_WIN = "NATIVE_APP";
|
|
var WEBVIEW_WIN = "WEBVIEW";
|
|
var WEBVIEW_BASE = WEBVIEW_WIN + "_";
|
|
|
|
androidController.keyevent = function (keycode, metastate, cb) {
|
|
this.proxy(["pressKeyCode", {keycode: keycode, metastate: metastate}], cb);
|
|
};
|
|
|
|
androidController.defaultContext = function () {
|
|
return NATIVE_WIN;
|
|
};
|
|
|
|
androidController.findElement = function (strategy, selector, cb) {
|
|
this.findUIElementOrElements(strategy, selector, false, "", cb);
|
|
};
|
|
|
|
androidController.findElements = function (strategy, selector, cb) {
|
|
this.findUIElementOrElements(strategy, selector, true, "", cb);
|
|
};
|
|
|
|
androidController.findUIElementOrElements = function (strategy, selector, many, context, cb) {
|
|
if (!deviceCommon.checkValidLocStrat(strategy, false, cb)) {
|
|
return;
|
|
}
|
|
if (strategy === "xpath" && context) {
|
|
return cb(new Error("Cannot use xpath locator strategy from an element. " +
|
|
"It can only be used from the root element"));
|
|
}
|
|
var params = {
|
|
strategy: strategy
|
|
, selector: selector
|
|
, context: context
|
|
, multiple: many
|
|
};
|
|
if (strategy === "name") {
|
|
helpers.logDeprecationWarning("Locator strategy", '"name"', '"accessibility id"');
|
|
}
|
|
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));
|
|
}
|
|
}.bind(this);
|
|
this.waitForCondition(this.implicitWaitMs, doFind, cb);
|
|
};
|
|
|
|
androidController.handleFindCb = function (err, res, many, findCb) {
|
|
if (err) {
|
|
findCb(false, err, res);
|
|
} else {
|
|
if (!many && res.status === 0 && res.value !== null) {
|
|
findCb(true, err, res);
|
|
} else if (many && typeof res.value !== 'undefined' && res.value.length > 0) {
|
|
findCb(true, err, res);
|
|
} else {
|
|
findCb(false, err, res);
|
|
}
|
|
}
|
|
};
|
|
|
|
androidController.findElementFromElement = function (element, strategy, selector, cb) {
|
|
this.findUIElementOrElements(strategy, selector, false, element, cb);
|
|
};
|
|
|
|
androidController.findElementsFromElement = function (element, strategy, selector, cb) {
|
|
this.findUIElementOrElements(strategy, selector, true, element, cb);
|
|
};
|
|
|
|
var _pathFromDomNode = function (node) {
|
|
var path = "";
|
|
_.each(node.attributes, function (attrObj) {
|
|
if (attrObj.name === "index") {
|
|
path = _pathFromDomNode(node.parentNode) + "/" + attrObj.value;
|
|
}
|
|
});
|
|
return path;
|
|
};
|
|
|
|
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);
|
|
}
|
|
if (!many) nodes = nodes.slice(0, 1);
|
|
var indexPaths = _.map(nodes, _pathFromDomNode);
|
|
if (!many && indexPaths.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 (indexPaths.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 findParams = {
|
|
strategy: "index paths",
|
|
selector: indexPaths.join(","),
|
|
multiple: many
|
|
};
|
|
this.proxy(["find", findParams], cb);
|
|
}.bind(this));
|
|
};
|
|
|
|
androidController.setValueImmediate = function (elementId, value, cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.setValue = function (elementId, value, cb) {
|
|
this.proxy(["element:setText", {elementId: elementId, text: value}], cb);
|
|
};
|
|
|
|
androidController.click = function (elementId, cb) {
|
|
this.proxy(["element:click", {elementId: elementId}], cb);
|
|
};
|
|
|
|
androidController.touchLongClick = function (elementId, x, y, duration, cb) {
|
|
var opts = {};
|
|
if (elementId) opts.elementId = elementId;
|
|
if (x) opts.x = x;
|
|
if (y) opts.y = y;
|
|
if (duration) opts.duration = duration;
|
|
this.proxy(["element:touchLongClick", opts], cb);
|
|
};
|
|
|
|
androidController.touchDown = function (elementId, x, y, cb) {
|
|
var opts = {};
|
|
if (elementId) opts.elementId = elementId;
|
|
if (x) opts.x = x;
|
|
if (y) opts.y = y;
|
|
this.proxy(["element:touchDown", opts], cb);
|
|
};
|
|
|
|
androidController.touchUp = function (elementId, x, y, cb) {
|
|
var opts = {};
|
|
if (elementId) opts.elementId = elementId;
|
|
if (x) opts.x = x;
|
|
if (y) opts.y = y;
|
|
this.proxy(["element:touchUp", opts], cb);
|
|
};
|
|
|
|
androidController.touchMove = function (elementId, x, y, cb) {
|
|
var opts = {};
|
|
if (elementId) opts.elementId = elementId;
|
|
if (x) opts.x = x;
|
|
if (y) opts.y = y;
|
|
this.proxy(["element:touchMove", opts], cb);
|
|
};
|
|
|
|
androidController.getStrings = function (cb) {
|
|
this.proxy(["getStrings"], cb);
|
|
};
|
|
|
|
androidController.complexTap = function (tapCount, touchCount, duration, x, y, elementId, cb) {
|
|
this.proxy(["click", {x: x, y: y}], cb);
|
|
};
|
|
|
|
androidController.clear = function (elementId, cb) {
|
|
this.proxy(["element:clear", {elementId: elementId}], cb);
|
|
};
|
|
|
|
androidController.submit = function (elementId, cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.getName = function (elementId, cb) {
|
|
var p = {elementId: elementId, attribute: "className"};
|
|
this.proxy(["element:getAttribute", p], cb);
|
|
};
|
|
|
|
androidController.getText = function (elementId, cb) {
|
|
this.proxy(["element:getText", {elementId: elementId}], cb);
|
|
};
|
|
|
|
androidController.getAttribute = function (elementId, attributeName, cb) {
|
|
var p = {elementId: elementId, attribute: attributeName};
|
|
this.proxy(["element:getAttribute", p], cb);
|
|
};
|
|
|
|
androidController.getLocation = function (elementId, cb) {
|
|
this.proxy(["element:getLocation", {elementId: elementId}], cb);
|
|
};
|
|
|
|
androidController.getSize = function (elementId, cb) {
|
|
this.proxy(["element:getSize", {elementId: elementId}], cb);
|
|
};
|
|
|
|
androidController.getWindowSize = function (windowHandle, cb) {
|
|
this.proxy(["getDeviceSize"], cb);
|
|
};
|
|
|
|
androidController.back = function (cb) {
|
|
this.proxy(["pressBack"], cb);
|
|
};
|
|
|
|
androidController.forward = function (cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.refresh = function (cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.getPageIndex = function (elementId, cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.keys = function (elementId, keys, cb) {
|
|
this.proxy(["element:setText", {elementId: elementId, text: keys}], cb);
|
|
};
|
|
|
|
androidController.frame = function (frame, cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.leaveWebView = function (cb) {
|
|
warnDeprecated('function', 'leaveWebView', 'context(null)');
|
|
this.setWindow(this.defaultContext(), cb);
|
|
};
|
|
|
|
androidController.implicitWait = function (ms, cb) {
|
|
this.implicitWaitMs = parseInt(ms, 10);
|
|
logger.info("Set Android implicit wait to " + ms + "ms");
|
|
cb(null, {
|
|
status: status.codes.Success.code
|
|
, value: null
|
|
});
|
|
};
|
|
|
|
androidController.asyncScriptTimeout = function (ms, cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.executeAsync = function (script, args, responseUrl, cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.elementDisplayed = function (elementId, cb) {
|
|
var p = {elementId: elementId, attribute: "displayed"};
|
|
this.proxy(["element:getAttribute", p], function (err, res) {
|
|
if (err) return cb(err);
|
|
var displayed = res.value === 'true';
|
|
cb(null, {
|
|
status: status.codes.Success.code
|
|
, value: displayed
|
|
});
|
|
});
|
|
};
|
|
|
|
androidController.elementEnabled = function (elementId, cb) {
|
|
var p = {elementId: elementId, attribute: "enabled"};
|
|
this.proxy(["element:getAttribute", p], function (err, res) {
|
|
if (err) return cb(err);
|
|
var enabled = res.value === 'true';
|
|
cb(null, {
|
|
status: status.codes.Success.code
|
|
, value: enabled
|
|
});
|
|
});
|
|
};
|
|
|
|
androidController.elementSelected = function (elementId, cb) {
|
|
var p = {elementId: elementId, attribute: "selected"};
|
|
this.proxy(["element:getAttribute", p], function (err, res) {
|
|
if (err) return cb(err);
|
|
var selected = res.value === 'true';
|
|
cb(null, {
|
|
status: status.codes.Success.code
|
|
, value: selected
|
|
});
|
|
});
|
|
};
|
|
|
|
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);
|
|
_buildClassNodeFromPlainNode(newDom, newDom, origDom);
|
|
newSource = new XMLDom.XMLSerializer().serializeToString(newDom);
|
|
return newSource;
|
|
};
|
|
|
|
var _getNodeClass = function (node) {
|
|
var nodeClass = null;
|
|
_.each(node.attributes, function (attr) {
|
|
if (attr.name === "class") {
|
|
nodeClass = attr.value;
|
|
}
|
|
});
|
|
return nodeClass;
|
|
};
|
|
|
|
var _copyNodeAttributes = function (oldNode, newNode) {
|
|
_.each(oldNode.attributes, function (attr) {
|
|
newNode.setAttribute(attr.name, attr.value);
|
|
});
|
|
};
|
|
|
|
var _buildClassNodeFromPlainNode = function (newDom, newParent, oldNode) {
|
|
var newNode;
|
|
var nodeClass = _getNodeClass(oldNode);
|
|
if (nodeClass) {
|
|
newNode = newDom.createElement(nodeClass);
|
|
_copyNodeAttributes(oldNode, newNode);
|
|
} else {
|
|
newNode = oldNode.cloneNode(false);
|
|
}
|
|
newParent.appendChild(newNode);
|
|
if (oldNode.hasChildNodes()) {
|
|
_.each(oldNode.childNodes, function (childNode) {
|
|
_buildClassNodeFromPlainNode(newDom, newNode, childNode);
|
|
});
|
|
}
|
|
};
|
|
|
|
androidController.getPageSource = function (cb) {
|
|
var xmlFile = temp.path({suffix: '.xml'});
|
|
var onDeviceXmlPath = this.dataDir + '/local/tmp/dump.xml';
|
|
async.series(
|
|
[
|
|
function (cb) {
|
|
var dumpCommand = this.args.compressXml ? "dumpWindowHierarchyCompressed" : "dumpWindowHierarchy";
|
|
this.proxy([dumpCommand], 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.warn(stderr);
|
|
return cb(err);
|
|
}
|
|
cb(null);
|
|
});
|
|
}.bind(this)
|
|
|
|
],
|
|
// Top level cb
|
|
function (err) {
|
|
if (err) return cb(err);
|
|
var xml = fs.readFileSync(xmlFile, 'utf8');
|
|
fs.unlinkSync(xmlFile);
|
|
try {
|
|
xml = _updateSourceXMLNodeNames(xml);
|
|
} catch (e) {
|
|
logger.error(e);
|
|
return cb(e);
|
|
}
|
|
cb(null, {
|
|
status: status.codes.Success.code
|
|
, value: xml
|
|
});
|
|
});
|
|
};
|
|
|
|
androidController.getAlertText = function (cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.setAlertText = function (text, cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.postAcceptAlert = function (cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.postDismissAlert = function (cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.lock = function (secs, cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.getOrientation = function (cb) {
|
|
this.proxy(["orientation", {}], cb);
|
|
};
|
|
|
|
androidController.setOrientation = function (orientation, cb) {
|
|
this.proxy(["orientation", {orientation: orientation}], cb);
|
|
};
|
|
|
|
androidController.endCoverage = function (intentToBroadcast, ecOnDevicePath, cb) {
|
|
var localfile = temp.path({prefix: 'appium', suffix: '.ec'});
|
|
if (fs.existsSync(localfile)) fs.unlinkSync(localfile);
|
|
var b64data = "";
|
|
|
|
async.series([
|
|
function (cb) {
|
|
// ensure the ec we're pulling is newly created as a result of the intent.
|
|
this.adb.rimraf(ecOnDevicePath, function () { cb(); });
|
|
}.bind(this),
|
|
function (cb) {
|
|
this.adb.broadcastProcessEnd(intentToBroadcast, this.appProcess, cb);
|
|
}.bind(this),
|
|
function (cb) {
|
|
this.adb.pull(ecOnDevicePath, localfile, cb);
|
|
}.bind(this),
|
|
function (cb) {
|
|
fs.readFile(localfile, function (err, data) {
|
|
if (err) return cb(err);
|
|
b64data = new Buffer(data).toString('base64');
|
|
cb();
|
|
});
|
|
}.bind(this),
|
|
],
|
|
function (err) {
|
|
if (fs.existsSync(localfile)) fs.unlinkSync(localfile);
|
|
if (err) return cb(err);
|
|
cb(null, {
|
|
status: status.codes.Success.code
|
|
, value: b64data
|
|
});
|
|
});
|
|
};
|
|
|
|
androidController.pullFile = function (remotePath, cb) {
|
|
var localFile = temp.path({prefix: 'appium', suffix: '.tmp'});
|
|
var b64data = "";
|
|
|
|
async.series([
|
|
function (cb) {
|
|
this.adb.pull(remotePath, localFile, cb);
|
|
}.bind(this),
|
|
function (cb) {
|
|
fs.readFile(localFile, function (err, data) {
|
|
if (err) return cb(err);
|
|
b64data = new Buffer(data).toString('base64');
|
|
cb();
|
|
});
|
|
}.bind(this),
|
|
],
|
|
function (err) {
|
|
if (fs.existsSync(localFile)) fs.unlinkSync(localFile);
|
|
if (err) return cb(err);
|
|
cb(null, {
|
|
status: status.codes.Success.code
|
|
, value: b64data
|
|
});
|
|
});
|
|
};
|
|
|
|
androidController.pushFile = function (base64Data, remotePath, cb) {
|
|
var localFile = temp.path({prefix: 'appium', suffix: '.tmp'});
|
|
mkdirp.sync(path.dirname(localFile));
|
|
|
|
async.series([
|
|
function (cb) {
|
|
var content = new Buffer(base64Data, 'base64');
|
|
var fd = fs.openSync(localFile, 'w');
|
|
fs.writeSync(fd, content, 0, content.length, 0);
|
|
fs.closeSync(fd);
|
|
|
|
// adb push creates folders and overwrites existing files.
|
|
this.adb.push(localFile, remotePath, cb);
|
|
}.bind(this),
|
|
],
|
|
function (err) {
|
|
if (fs.existsSync(localFile)) fs.unlinkSync(localFile);
|
|
if (err) return cb(err);
|
|
cb(null, {
|
|
status: status.codes.Success.code
|
|
});
|
|
});
|
|
};
|
|
|
|
androidController.getScreenshot = function (cb) {
|
|
var localfile = temp.path({prefix: 'appium', suffix: '.png'});
|
|
var b64data = "";
|
|
|
|
async.series([
|
|
function (cb) {
|
|
var png = "/data/local/tmp/screenshot.png";
|
|
var cmd = ['"/system/bin/rm', png + ';', '/system/bin/screencap -p',
|
|
png, '"'].join(' ');
|
|
this.adb.shell(cmd, cb);
|
|
}.bind(this),
|
|
function (cb) {
|
|
if (fs.existsSync(localfile)) fs.unlinkSync(localfile);
|
|
this.adb.pull('/data/local/tmp/screenshot.png', localfile, cb);
|
|
}.bind(this),
|
|
function (cb) {
|
|
fs.readFile(localfile, function (err, data) {
|
|
if (err) return cb(err);
|
|
b64data = new Buffer(data).toString('base64');
|
|
cb();
|
|
});
|
|
},
|
|
function (cb) {
|
|
fs.unlink(localfile, function (err) {
|
|
if (err) return cb(err);
|
|
cb();
|
|
});
|
|
}
|
|
],
|
|
// Top level cb
|
|
function (err) {
|
|
if (err) return cb(err);
|
|
cb(null, {
|
|
status: status.codes.Success.code
|
|
, value: b64data
|
|
});
|
|
});
|
|
};
|
|
|
|
androidController.fakeFlick = function (xSpeed, ySpeed, swipe, cb) {
|
|
this.proxy(["flick", {xSpeed: xSpeed, ySpeed: ySpeed}], cb);
|
|
};
|
|
|
|
androidController.fakeFlickElement = function (elementId, xoffset, yoffset, speed, cb) {
|
|
this.proxy(["element:flick", {xoffset: xoffset, yoffset: yoffset, speed: speed, elementId: elementId}], cb);
|
|
};
|
|
|
|
androidController.swipe = function (startX, startY, endX, endY, duration, touchCount, elId, cb) {
|
|
if (startX === 'null') {
|
|
startX = 0.5;
|
|
}
|
|
if (startY === 'null') {
|
|
startY = 0.5;
|
|
}
|
|
var swipeOpts = {
|
|
startX: startX
|
|
, startY: startY
|
|
, endX: endX
|
|
, endY: endY
|
|
, steps: Math.round(duration * this.swipeStepsPerSec)
|
|
};
|
|
if (elId !== null) {
|
|
swipeOpts.elementId = elId;
|
|
this.proxy(["element:swipe", swipeOpts], cb);
|
|
} else {
|
|
this.proxy(["swipe", swipeOpts], cb);
|
|
}
|
|
};
|
|
|
|
androidController.rotate = function (x, y, radius, rotation, duration, touchCount, elId, cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.pinchClose = function (startX, startY, endX, endY, duration, percent, steps, elId, cb) {
|
|
var pinchOpts = {
|
|
direction: 'in'
|
|
, elementId: elId
|
|
, percent: percent
|
|
, steps: steps
|
|
};
|
|
this.proxy(["element:pinch", pinchOpts], cb);
|
|
};
|
|
|
|
androidController.pinchOpen = function (startX, startY, endX, endY, duration, percent, steps, elId, cb) {
|
|
var pinchOpts = {
|
|
direction: 'out'
|
|
, elementId: elId
|
|
, percent: percent
|
|
, steps: steps
|
|
};
|
|
this.proxy(["element:pinch", pinchOpts], cb);
|
|
};
|
|
|
|
androidController.flick = function (startX, startY, endX, endY, touchCount, elId, cb) {
|
|
if (startX === 'null') {
|
|
startX = 0.5;
|
|
}
|
|
if (startY === 'null') {
|
|
startY = 0.5;
|
|
}
|
|
var swipeOpts = {
|
|
startX: startX
|
|
, startY: startY
|
|
, endX: endX
|
|
, endY: endY
|
|
, steps: Math.round(0.2 * this.swipeStepsPerSec)
|
|
};
|
|
if (elId !== null) {
|
|
swipeOpts.elementId = elId;
|
|
this.proxy(["element:swipe", swipeOpts], cb);
|
|
} else {
|
|
this.proxy(["swipe", swipeOpts], cb);
|
|
}
|
|
};
|
|
|
|
androidController.drag = function (startX, startY, endX, endY, duration, touchCount, elementId, destElId, cb) {
|
|
var dragOpts = {
|
|
elementId: elementId
|
|
, destElId: destElId
|
|
, startX: startX
|
|
, startY: startY
|
|
, endX: endX
|
|
, endY: endY
|
|
, steps: Math.round(duration * this.dragStepsPerSec)
|
|
};
|
|
|
|
if (elementId !== null) {
|
|
this.proxy(["element:drag", dragOpts], cb);
|
|
} else {
|
|
this.proxy(["drag", dragOpts], cb);
|
|
}
|
|
};
|
|
|
|
androidController.scrollTo = function (elementId, text, direction, cb) {
|
|
// instead of the elementId as the element to be scrolled too,
|
|
// it's the scrollable view to swipe until the uiobject that has the
|
|
// text is found.
|
|
var opts = {
|
|
text: text
|
|
, direction: direction
|
|
, elementId: elementId
|
|
};
|
|
this.proxy(["element:scrollTo", opts], cb);
|
|
};
|
|
|
|
androidController.scroll = function (direction, cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.shake = function (cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.setLocation = function (latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy, course, speed, cb) {
|
|
var cmd = "geo fix " + longitude + " " + latitude;
|
|
this.adb.sendTelnetCommand(cmd, function (err, res) {
|
|
if (err) {
|
|
return cb(null, {
|
|
status: status.codes.UnknownError.code
|
|
, value: "Could not set geolocation via telnet to device"
|
|
});
|
|
}
|
|
cb(null, {
|
|
status: status.codes.Success.code
|
|
, value: res
|
|
});
|
|
});
|
|
};
|
|
|
|
androidController.hideKeyboard = function (keyName, cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.url = function (url, cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.active = function (cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.getCurrentContext = function (cb) {
|
|
var response = {
|
|
status: status.codes.Success.code
|
|
, value: this.curContext || null
|
|
};
|
|
cb(null, response);
|
|
};
|
|
|
|
androidController.getContexts = function (cb) {
|
|
this.listWebviews(function (err, webviews) {
|
|
if (err) return cb(err);
|
|
this.contexts = [NATIVE_WIN];
|
|
_.each(webviews, function (view, idx) {
|
|
this.contexts.push(WEBVIEW_BASE + (idx + 1).toString());
|
|
}.bind(this));
|
|
logger.info("Available contexts: " + this.contexts);
|
|
cb(null, {
|
|
status: status.codes.Success.code
|
|
, value: this.contexts
|
|
});
|
|
}.bind(this));
|
|
};
|
|
|
|
androidController.setContext = function (name, cb) {
|
|
if (name === null) {
|
|
name = this.defaultContext();
|
|
} else if (name === WEBVIEW_WIN) {
|
|
name = WEBVIEW_BASE + "1";
|
|
}
|
|
this.getContexts(function () {
|
|
if (!_.contains(this.contexts, name)) {
|
|
return cb(null, {
|
|
status: status.codes.NoSuchContext.code
|
|
, value: "Context '" + name + "' does not exist"
|
|
});
|
|
}
|
|
if (name === this.curContext) {
|
|
return jwpSuccess(cb);
|
|
}
|
|
var next = function (err) {
|
|
if (err) return cb(err);
|
|
this.curContext = name;
|
|
jwpSuccess(cb);
|
|
}.bind(this);
|
|
|
|
// current ChromeDriver doesn't handle more than a single web view
|
|
if (name.indexOf(WEBVIEW_WIN) !== -1) {
|
|
this.startChromedriverProxy(next);
|
|
} else {
|
|
this.stopChromedriverProxy(next);
|
|
}
|
|
}.bind(this));
|
|
};
|
|
|
|
androidController.getWindowHandle = function (cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.getWindowHandles = function (cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.setWindow = function (name, cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.closeWindow = function (cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.clearWebView = function (cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.execute = function (script, args, cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.convertElementForAtoms = deviceCommon.convertElementForAtoms;
|
|
|
|
androidController.title = function (cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.moveTo = function (element, xoffset, yoffset, cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.clickCurrent = function (button, cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.getCookies = function (cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.setCookie = function (cookie, cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.deleteCookie = function (cookie, cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.deleteCookies = function (cb) {
|
|
cb(new NotYetImplementedError(), null);
|
|
};
|
|
|
|
androidController.resetAndStartApp = function (cb) {
|
|
async.series([
|
|
this.resetApp.bind(this),
|
|
this.waitForActivityToStop.bind(this),
|
|
this.startApp.bind(this)
|
|
], cb);
|
|
};
|
|
|
|
// controller.js#isAppInstalled expects weird response, hence this hack
|
|
androidController.isAppInstalled = function (appPackage, cb) {
|
|
this.adb.isAppInstalled(appPackage, function (err, installed) {
|
|
if (installed) {
|
|
return cb(null, [true]);
|
|
}
|
|
cb(err, []);
|
|
});
|
|
};
|
|
|
|
androidController.removeApp = function (appPackage, cb) {
|
|
var removeCommand = null;
|
|
if (this.args.udid) {
|
|
removeCommand = 'adb -s ' + this.args.udid + ' uninstall ' + appPackage;
|
|
} else {
|
|
removeCommand = 'adb uninstall ' + appPackage;
|
|
}
|
|
deviceCommon.removeApp(removeCommand, this.args.udid, appPackage, cb);
|
|
};
|
|
|
|
androidController.installApp = function (appPackage, cb) {
|
|
var installationCommand = null;
|
|
if (this.args.udid) {
|
|
installationCommand = 'adb -s ' + this.args.udid + ' install ' + appPackage;
|
|
} else {
|
|
installationCommand = 'adb install ' + appPackage;
|
|
}
|
|
deviceCommon.installApp(installationCommand, this.args.udid, appPackage, cb);
|
|
};
|
|
|
|
androidController.unpackApp = function (req, cb) {
|
|
deviceCommon.unpackApp(req, '.apk', cb);
|
|
};
|
|
|
|
androidController.tap = function (elementId, x, y, count, cb) {
|
|
if (typeof x === "undefined" || x === null) x = 0;
|
|
if (typeof y === "undefined" || y === null) y = 0;
|
|
if (typeof count === "undefined" || count === null) count = 1;
|
|
|
|
var i = 0;
|
|
var opts = {};
|
|
var loop = function (err, res) {
|
|
if (err) return cb(err);
|
|
if (i++ >= count) return cb(err, res);
|
|
|
|
this.proxy(opts, loop);
|
|
}.bind(this);
|
|
|
|
if (elementId) {
|
|
// we are either tapping on the default location of the element
|
|
// or an offset from the top left corner
|
|
if (x !== 0 || y !== 0) {
|
|
this.proxy(["element:getLocation", {elementId: elementId}], function (err, res) {
|
|
if (err) return cb(err);
|
|
var value = res.value;
|
|
x += parseInt(value.x);
|
|
y += parseInt(value.y);
|
|
|
|
opts = ["click", {x: x, y: y}];
|
|
loop();
|
|
}.bind(this));
|
|
} else {
|
|
opts = ["element:click", {elementId: elementId}];
|
|
loop();
|
|
}
|
|
} else {
|
|
// we have absolute coordinates
|
|
opts = ["click", {x: x, y: y}];
|
|
loop();
|
|
}
|
|
};
|
|
|
|
androidController.doTouchAction = function (action, opts, cb) {
|
|
switch (action) {
|
|
case 'tap':
|
|
return this.tap(opts.element, opts.x, opts.y, opts.count, cb);
|
|
case 'press':
|
|
return this.touchDown(opts.element, opts.x, opts.y, cb);
|
|
case 'release':
|
|
return this.touchUp(opts.element, opts.x, opts.y, cb);
|
|
case 'moveTo':
|
|
return this.touchMove(opts.element, opts.x, opts.y, cb);
|
|
case 'wait':
|
|
return setTimeout(function () {
|
|
cb(null, {"value": true, "status": status.codes.Success.code});
|
|
}, opts.ms);
|
|
case 'longPress':
|
|
if (typeof opts.duration === 'undefined' || !opts.duration) {
|
|
opts.duration = 1000;
|
|
}
|
|
return this.touchLongClick(opts.element, opts.x, opts.y, opts.duration, cb);
|
|
case 'cancel':
|
|
// TODO: clarify behavior of 'cancel' action and fix this
|
|
logger.warn("Cancel action currently has no effect");
|
|
break;
|
|
default:
|
|
return cb("unknown action '" + action + "'");
|
|
}
|
|
};
|
|
|
|
// drag is *not* press-move-release, so we need to translate
|
|
// drag works fine for scroll, as well
|
|
var doTouchDrag = function (gestures, cb) {
|
|
var longPress = gestures[0];
|
|
var elementId = longPress.options.element;
|
|
this.getLocation(elementId, function (err, res) {
|
|
if (err) return cb(err);
|
|
|
|
var startX = res.value.x;
|
|
var startY = res.value.y;
|
|
|
|
var moveTo = gestures[1];
|
|
var destElId = moveTo.options.element;
|
|
var endX = moveTo.options.x;
|
|
var endY = moveTo.options.y;
|
|
|
|
if (typeof destElId !== 'undefined' && destElId) {
|
|
this.getLocation(destElId, function (err, res) {
|
|
if (err) return cb(err);
|
|
|
|
endX = res.value.x + (endX || 0);
|
|
endY = res.value.y + (endY || 0);
|
|
|
|
return this.drag(startX, startY, endX, endY, 1, 1, elementId, destElId, cb);
|
|
}.bind(this));
|
|
} else {
|
|
return this.drag(startX, startY, endX, endY, 1, 1, elementId, destElId, cb);
|
|
}
|
|
}.bind(this));
|
|
|
|
|
|
return;
|
|
};
|
|
|
|
androidController.performTouch = function (gestures, cb) {
|
|
// some things are special
|
|
var actions = _.pluck(gestures, "action");
|
|
if (actions[0] === 'longPress' && actions[1] === 'moveTo' && actions[2] === 'release') {
|
|
return doTouchDrag.apply(this, [gestures, cb]);
|
|
} else if ((actions[actions.length - 2] === 'tap' ||
|
|
actions[actions.length - 2] === 'longPress') && actions[actions.length - 1] === 'release') {
|
|
// the `longPress` and `tap` methods release on their own
|
|
gestures.pop();
|
|
}
|
|
var cycleThroughGestures = function (err, res) {
|
|
if (err) return cb(err);
|
|
|
|
var gesture = gestures.shift();
|
|
if (typeof gesture === "undefined") return cb(null, res);
|
|
|
|
this.doTouchAction(gesture.action, gesture.options, cycleThroughGestures);
|
|
}.bind(this);
|
|
cycleThroughGestures();
|
|
};
|
|
|
|
androidController.parseTouch = function (gestures, cb) {
|
|
if (_.last(gestures).action === 'release') {
|
|
gestures.pop();
|
|
}
|
|
|
|
var needsPoint = function (action) {
|
|
return _.contains(['press', 'moveTo', 'tap', 'longPress'], action);
|
|
};
|
|
|
|
var touchStateObjects = [];
|
|
async.eachSeries(gestures, function (gesture, done) {
|
|
var tapPoint = false;
|
|
if (needsPoint(gesture.action)) { // press, longPress, moveTo and tap all need a position
|
|
var elementId = gesture.options.element;
|
|
if (elementId) {
|
|
this.getLocation(elementId, function (err, res) {
|
|
if (err) return done(err); // short circuit and quit
|
|
|
|
var pos = { x: res.value.x, y: res.value.y };
|
|
this.getSize(elementId, function (err, res) {
|
|
if (err) return done(err);
|
|
var size = {w: res.value.width, h: res.value.height};
|
|
|
|
if (gesture.options.x || gesture.options.y) {
|
|
tapPoint = {
|
|
offset: false,
|
|
x: pos.x + (gesture.options.x || 0),
|
|
y: pos.y + (gesture.options.y || 0)
|
|
};
|
|
} else {
|
|
tapPoint = {
|
|
offset: false,
|
|
x: pos.x + (size.w / 2),
|
|
y: pos.y + (size.h / 2)
|
|
};
|
|
}
|
|
|
|
var touchStateObject = {
|
|
timeOffset: 0.005,
|
|
touch: tapPoint
|
|
};
|
|
touchStateObjects.push(touchStateObject);
|
|
done();
|
|
});
|
|
}.bind(this));
|
|
} else {
|
|
// expects absolute coordinates, so we need to save these as offsets
|
|
// and then translate when everything is done
|
|
tapPoint = {
|
|
offset: true,
|
|
x: (gesture.options.x || 0),
|
|
y: (gesture.options.y || 0)
|
|
};
|
|
|
|
touchStateObject = {
|
|
timeOffset: 0.005,
|
|
touch: tapPoint
|
|
};
|
|
touchStateObjects.push(touchStateObject);
|
|
done();
|
|
}
|
|
} else {
|
|
// in this case we need the previous entry's tap point
|
|
tapPoint = false; // temporary marker
|
|
var offset = 0.005;
|
|
if (gesture.action === 'wait') {
|
|
if (typeof gesture.options.ms !== 'undefined' || gesture.options.ms !== null) {
|
|
offset = (parseInt(gesture.options.ms) / 1000);
|
|
}
|
|
}
|
|
var touchStateObject = {
|
|
timeOffset: offset,
|
|
touch: tapPoint
|
|
};
|
|
touchStateObjects.push(touchStateObject);
|
|
done();
|
|
}
|
|
}.bind(this), function (err) {
|
|
if (err) return cb(err);
|
|
|
|
// we need to change the time (which is now an offset)
|
|
// and the position (which may be an offset)
|
|
var prevPos = null,
|
|
time = 0;
|
|
_.each(touchStateObjects, function (state) {
|
|
if (state.touch === false) {
|
|
// if we have no position (this happens with `wait`) we need the previous one
|
|
state.touch = prevPos;
|
|
} else if (state.touch.offset && prevPos) {
|
|
// the current position is an offset
|
|
state.touch.x += prevPos.x;
|
|
state.touch.y += prevPos.y;
|
|
}
|
|
delete state.touch.offset;
|
|
prevPos = state.touch;
|
|
|
|
var timeOffset = state.timeOffset;
|
|
time += timeOffset;
|
|
state.time = helpers.truncateDecimals(time, 3);
|
|
|
|
delete state.timeOffset;
|
|
});
|
|
|
|
cb(null, touchStateObjects);
|
|
});
|
|
};
|
|
|
|
androidController.performMultiAction = function (elementId, actions, cb) {
|
|
var states = [];
|
|
async.eachSeries(actions, function (action, done) {
|
|
this.parseTouch(action, function (err, val) {
|
|
if (err) return done(err);
|
|
|
|
states.push(val);
|
|
done();
|
|
}.bind(this));
|
|
}.bind(this), function (err) {
|
|
if (err) return cb(err);
|
|
|
|
var opts;
|
|
if (elementId) {
|
|
opts = {
|
|
elementId: elementId,
|
|
actions: states
|
|
};
|
|
return this.proxy(["element:performMultiPointerGesture", opts], cb);
|
|
} else {
|
|
opts = {
|
|
actions: states
|
|
};
|
|
return this.proxy(["performMultiPointerGesture", opts], cb);
|
|
}
|
|
}.bind(this));
|
|
};
|
|
|
|
module.exports = androidController;
|