Merge pull request #2224 from imurchie/isaac-touch

First pass at iOS touch for Appium 1.0
This commit is contained in:
Jonathan Lipps
2014-04-01 18:09:35 -07:00
5 changed files with 352 additions and 4 deletions
@@ -997,4 +997,12 @@ androidController.unpackApp = function (req, cb) {
deviceCommon.unpackApp(req, '.apk', cb);
};
androidController.performTouch = function (gestures, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.performMultiAction = function (actions, cb) {
cb(new NotYetImplementedError(), null);
};
module.exports = androidController;
+187 -4
View File
@@ -443,7 +443,7 @@ iOSController.touchLongClick = function (elementId, cb) {
cb(new NotYetImplementedError(), null);
};
iOSController.touchDown = function (elementId, cb) {
iOSController.touchDown = function (elId, x, y, cb) {
cb(new NotYetImplementedError(), null);
};
@@ -451,7 +451,7 @@ iOSController.touchUp = function (elementId, cb) {
cb(new NotYetImplementedError(), null);
};
iOSController.touchMove = function (elementId, cb) {
iOSController.touchMove = function (elId, startX, startY, cb) {
cb(new NotYetImplementedError(), null);
};
@@ -1272,10 +1272,10 @@ iOSController.scrollTo = function (elementId, text, direction, cb) {
iOSController.scroll = function (elementId, direction, cb) {
direction = direction.charAt(0).toUpperCase() + direction.slice(1);
// By default, scroll the first scrollview.
// By default, scroll the first scrollview.
var command = "au.scrollFirstView('" + direction + "')";
if (elementId) {
// if elementId is defined, call scrollLeft, scrollRight, scrollUp, and scrollDown on the element.
// if elementId is defined, call scrollLeft, scrollRight, scrollUp, and scrollDown on the element.
command = ["au.getElement('", elementId, "').scroll", direction, "()"].join('');
}
this.proxy(command, cb);
@@ -1861,4 +1861,187 @@ iOSController.getLog = function (logType, cb) {
}
};
iOSController.performTouch = function (gestures, cb) {
this.parseTouch(gestures, function (err, touchStateObjects) {
if (err !== null) return cb(err);
this.proxy("target.touch(" + JSON.stringify(touchStateObjects) + ")", cb);
}.bind(this));
};
iOSController.parseTouch = function (gestures, cb) {
// `release` is automatic in iOS
if (_.last(gestures).action === 'release') {
gestures.pop();
}
var touchStateObjects = [];
var finishParsing = function () {
var prevPos = null;
// we need to change the time (which is now an offset)
// and the position (which may be an offset)
var time = 0;
_.each(touchStateObjects, function (state, index) {
if (state.touch[0] === false) {
// if we have no position (this happens with `wait`) we need the previous one
state.touch[0] = prevPos;
} else if (state.touch[0].offset && prevPos) {
// the current position is an offset
state.touch[0].x += prevPos.x;
state.touch[0].y += prevPos.y;
}
delete state.touch[0].offset;
prevPos = state.touch[0];
var timeOffset = state.timeOffset;
time += timeOffset;
state.time = time;
delete state.timeOffset;
});
cb(null, touchStateObjects);
}.bind(this);
var needsPoint = function (action) {
return _.contains(['press', 'moveTo', 'tap', 'longPress'], action);
};
// _.each(gestures, function (gesture, index) {
var cycleThroughGestures = function () {
var gesture = gestures.shift();
if (typeof gesture === "undefined") {
return finishParsing();
}
var tapPoint = false;
if (needsPoint(gesture.action)) { // press, longPress, moveTo and tap all need a position
var elementId = gesture.options.element;
if (elementId) {
var command = ["au.getElement('", elementId, "').rect()"].join('');
this.proxy(command, function (err, res) {
if (err) return cb(err); // short circuit and quit
var rect = res.value;
var pos = {x: rect.origin.x, y: rect.origin.y};
var size = {w: rect.size.width, h: rect.size.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.2,
touch: [
tapPoint
]
};
touchStateObjects.push(touchStateObject);
cycleThroughGestures();
}.bind(this));
} else {
// iOS 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.2,
touch: [
tapPoint
]
};
touchStateObjects.push(touchStateObject);
cycleThroughGestures();
}
} else {
// in this case we need the previous entry's tap point
tapPoint = false; // temporary marker
var offset = 0.2;
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);
cycleThroughGestures();
}
}.bind(this);
cycleThroughGestures();
};
var mergeStates = function (states) {
var getSlice = function (states, index) {
var array = [];
for (var i = 0; i < states.length; i++) {
array.push(states[i][index]);
}
return array;
};
var timeSequence = function (states) {
var seq = [];
_.each(states, function (state) {
var times = _.pluck(state, "time");
seq = _.union(seq, times);
});
return seq.sort();
};
// for now we will just assume that the times line up
var merged = [];
_.each(timeSequence(states), function (time, index) {
var slice = getSlice(states, index);
var obj = {
time: time,
touch: []
};
_.each(slice, function (action) {
obj.touch.push(action.touch[0]);
});
merged.push(obj);
});
return merged;
};
iOSController.performMultiAction = function (actions, cb) {
var states = [];
var cycleThroughActions = function () {
var action = actions.shift();
if (typeof action === "undefined") {
var mergedStates = mergeStates(states);
return this.proxy("target.touch(" + JSON.stringify(mergedStates) + ")", cb);
}
this.parseTouch(action, function (err, val) {
if (err) return cb(err); // short-circuit the loop and send the error up
states.push(val);
cycleThroughActions();
}.bind(this));
}.bind(this);
cycleThroughActions();
};
module.exports = iOSController;
+12
View File
@@ -246,6 +246,18 @@ exports.setValue = function (req, res) {
req.device.setValue(elementId, value, getResponseHandler(req, res));
};
exports.performTouch = function (req, res) {
var gestures = req.body;
req.device.performTouch(gestures, getResponseHandler(req, res));
};
exports.performMultiAction = function (req, res) {
var actions = req.body;
req.device.performMultiAction(actions, getResponseHandler(req, res));
};
exports.doClick = function (req, res) {
var elementId = req.params.elementId || req.body.element;
req.device.click(elementId, getResponseHandler(req, res));
+4
View File
@@ -79,6 +79,10 @@ module.exports = function (appium) {
rest.post('/wd/hub/session/:sessionId?/log', controller.getLog);
rest.get('/wd/hub/session/:sessionId?/log/types', controller.getLogTypes);
// touch gesture endpoints
rest.post('/wd/hub/session/:sessionId?/touch/perform', controller.performTouch);
rest.post('/wd/hub/session/:sessionId?/touch/multi/perform', controller.performMultiAction);
// allow appium to receive async response
rest.post('/wd/hub/session/:sessionId?/receive_async_response', controller.receiveAsyncResponse);
// these are for testing purposes only
+141
View File
@@ -0,0 +1,141 @@
"use strict";
var okIfAlert = require('../../../helpers/alert').okIfAlert,
setup = require("../../common/setup-base"),
desired = require('./desired'),
TouchAction = require('wd').TouchAction,
MultiAction = require('wd').MultiAction;
describe('testapp - pinch gesture -', function () {
describe('pinchOpen and pinchClose gesture', function () {
var driver;
setup(this, desired).then(function (d) { driver = d; });
it('should pinchOpen and pinchClose map after tapping Test Gesture', function (done) {
driver
.elementsByTagName('button').then(function (buttons) { return buttons[3].click(); })
.sleep(1000).then(function () { okIfAlert(driver); })
.elementByXPath('//window[1]/UIAMapView[1]')
.execute("mobile: pinchOpen", [{startX: 114.0, startY: 198.0, endX: 257.0,
endY: 256.0, duration: 5.0}])
.elementByXPath('//window[1]/UIAMapView[1]')
.execute("mobile: pinchClose", [{startX: 114.0, startY: 198.0, endX: 257.0,
endY: 256.0, duration: 5.0}])
.nodeify(done);
});
});
});
// most of these tests do not actually test anything.
// They need to be watched to make sure they are doing something right/wrong.
describe('touch actions', function () {
var driver;
setup(this, desired).then(function (d) { driver = d; });
describe('tap', function () {
it('should tap on a specified element', function (done) {
driver
.elementsByTagName('button').then(function (buttons) {
var el = buttons[3];
var action = new TouchAction(el);
return action.tap().perform();
})
.sleep(1000).then(function () { okIfAlert(driver); })
.sleep(15000)
.nodeify(done);
});
});
describe('swipe', function () {
it('should move the page', function (done) {
driver
.elementsByTagName('button').then(function (buttons) {
var el = buttons[3];
var action = new TouchAction(el);
return action.tap().perform();
})
.sleep(500).then(function () { okIfAlert(driver); })
.sleep(500)
.elementByXPath('//window[1]/UIAMapView[1]')
.then(function (el) {
var action = new TouchAction(el);
return action.press().moveTo({ x: 0, y: 100 }).release().perform();
})
.sleep(15000)
.nodeify(done);
});
});
describe('wait', function () {
it('should move the page and wait a bit', function (done) {
driver
.elementsByTagName('button').then(function (buttons) {
var el = buttons[3];
var action = new TouchAction(el);
return action.tap().perform();
})
.sleep(500).then(function () { okIfAlert(driver); })
.sleep(500)
.elementByXPath('//window[1]/UIAMapView[1]')
.then(function (el) {
var action = new TouchAction(el);
return action.press().moveTo({ x: 0, y: 100 }).wait({ ms: 5000 }).moveTo({ x: 0, y: -100 }).release().perform();
})
.sleep(15000)
.nodeify(done);
});
});
describe('pinch', function () {
it('should do some pinching', function (done) {
driver
.elementsByTagName('button').then(function (buttons) {
var el = buttons[3];
var action = new TouchAction(el);
return action.tap().perform();
})
.sleep(500).then(function () { okIfAlert(driver); })
.sleep(500)
.elementByXPath('//window[1]/UIAMapView[1]')
.then(function (el) {
var a1 = new TouchAction(el);
a1.press().moveTo({ x: -100, y: 0 }).release();
var a2 = new TouchAction(el);
a2.press().moveTo({ x: 100, y: 0 }).release();
var ma = new MultiAction(el);
ma.add(a1, a2);
ma.perform();
})
.sleep(15000)
.nodeify(done);
});
it('should do more involved pinching in and out', function (done) {
driver
.elementsByTagName('button').then(function (buttons) {
var el = buttons[3];
var action = new TouchAction(el);
return action.tap().perform();
})
.sleep(500).then(function () { okIfAlert(driver); })
.sleep(500)
.elementByXPath('//window[1]/UIAMapView[1]')
.then(function (el) {
var a1 = new TouchAction(el);
a1.press().moveTo({ x: -100, y: 0 }).wait(3000).moveTo({ x: 100, y: 0 }).release();
var a2 = new TouchAction(el);
a2.press().moveTo({ x: 100, y: 0 }).wait({ ms: 3000 }).moveTo({ x: -100, y: 0 }).release();
var ma = new MultiAction(el);
ma.add(a1, a2);
ma.perform();
})
.sleep(15000)
.nodeify(done);
});
});
});