mirror of
https://github.com/appium/appium.git
synced 2026-04-29 23:09:31 -05:00
Merge pull request #2224 from imurchie/isaac-touch
First pass at iOS touch for Appium 1.0
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user