forked from enviPath/enviPy
285 lines
9.9 KiB
JavaScript
285 lines
9.9 KiB
JavaScript
/**
|
|
*
|
|
* The Touch Action API provides the basis of all gestures that can be automated in Appium.
|
|
* It is currently only available to native apps and can not be used to interact with webapps.
|
|
* At its core is the ability to chain together _ad hoc_ individual actions, which will then be
|
|
* applied to an element in the application on the device. The basic actions that can be used are:
|
|
*
|
|
* - press (pass selector or (x,y) or both)
|
|
* - longPress (pass selector or (x,y) or both)
|
|
* - tap (pass selector or (x,y) or both)
|
|
* - moveTo (pass selector or (x,y) or both)
|
|
* - wait (pass ms (as milliseconds))
|
|
* - release (no arguments)
|
|
*
|
|
* If you use the touchAction command with a selector you don't need to pass the selector to each
|
|
* action. It will be propagated by the internally (if no x or y parameters are given).
|
|
*
|
|
* <example>
|
|
:touchAction.js
|
|
it('should do a touch gesture', function () {
|
|
var screen = $('//UITextbox');
|
|
|
|
// simple touch action on element
|
|
screen.touchAction('tap');
|
|
// same as
|
|
browser.touchAction('//UITextbox', 'tap')
|
|
|
|
// simple touch action using x y variables
|
|
browser.touchAction({
|
|
actions: 'tap', x: 300, y:200
|
|
})
|
|
|
|
// simple touch action using selector and x y variables
|
|
// tap location is 30px right and 20px down relative from the center of the element
|
|
browser.touchAction({
|
|
actions: 'tap', x: 30, y:20, selector: '//UIAApplication[1]/UIAElement[2]'
|
|
})
|
|
|
|
// multi action on an element (drag&drop)
|
|
screen.touchAction([
|
|
'press',
|
|
{ action: 'moveTo', x: 200, y: 0 },
|
|
'release'
|
|
])
|
|
// same as
|
|
browser.touchAction('//UITextbox', [
|
|
'press',
|
|
{ action: 'moveTo', x: 200, y: 0},
|
|
'release'
|
|
])
|
|
|
|
// drag&drop to element
|
|
screen.touchAction([
|
|
'press',
|
|
{ action: 'moveTo', selector: '//UIAApplication[1]/UIAElement[2]' },
|
|
'release'
|
|
]))
|
|
});
|
|
|
|
:multiTouchAction.js
|
|
it('should do a multitouch gesture', function () {
|
|
// drag&drop with two fingers 200px down
|
|
browser.touchAction([
|
|
[{action: 'press', x: 10, y: 10}, { action: 'moveTo', x: 0, y: 200 }, 'release'],
|
|
[{action: 'press', x: 100, y: 10}, { action: 'moveTo', x: 0, y: 200 }, 'release']]
|
|
])
|
|
})
|
|
* </example>
|
|
*
|
|
* @param {String} selector selector to execute the touchAction on
|
|
* @param {String} action action to execute
|
|
*
|
|
* @see https://saucelabs.com/blog/appium-sauce-labs-bootcamp-chapter-2-touch-actions
|
|
* @type mobile
|
|
* @for android, ios
|
|
* @uses mobile/performTouchAction, mobile/performMultiAction
|
|
*
|
|
*/
|
|
|
|
const TOUCH_ACTIONS = ['press', 'longPress', 'tap', 'moveTo', 'wait', 'release']
|
|
const POS_ACTIONS = TOUCH_ACTIONS.slice(0, -2)
|
|
const ACCEPTED_OPTIONS = ['x', 'y', 'selector', 'element']
|
|
|
|
export default function touchAction (selector, actions) {
|
|
if (typeof selector !== 'string' || TOUCH_ACTIONS.indexOf(selector) > -1) {
|
|
actions = selector
|
|
selector = this.lastResult
|
|
}
|
|
|
|
if (!Array.isArray(actions)) {
|
|
actions = [actions]
|
|
}
|
|
|
|
/**
|
|
* check if multiAction
|
|
*/
|
|
if (Array.isArray(actions[0])) {
|
|
actions = formatArgs(selector, actions)
|
|
return Promise.all(getSelectors.call(this, actions, true)).then((jsonElements) => {
|
|
actions = replaceSelectorsById(actions, jsonElements)
|
|
return this.performMultiAction({ actions })
|
|
})
|
|
}
|
|
|
|
actions = formatArgs(selector, actions)
|
|
return Promise.all(getSelectors.call(this, actions)).then((jsonElements) => {
|
|
actions = replaceSelectorsById(actions, jsonElements)
|
|
return this.performTouchAction({ actions })
|
|
})
|
|
}
|
|
|
|
/**
|
|
* helper to determine if action has proper option arguments
|
|
* ('press', 'longPress', 'tap', 'moveTo' need at least some kind of position information)
|
|
* @param {String} action name of action
|
|
* @param {Object} options action options
|
|
* @return {Boolean} True if don't need any options or has a position option
|
|
*/
|
|
let hasValidActionOptions = function (action, options) {
|
|
return POS_ACTIONS.indexOf(action) < 0 || (POS_ACTIONS.indexOf(action) > -1 && Object.keys(options).length > 0)
|
|
}
|
|
|
|
let formatArgs = function (selector, actions) {
|
|
return actions.map((action) => {
|
|
if (Array.isArray(action)) {
|
|
return formatArgs(selector, action)
|
|
}
|
|
|
|
const formattedAction = { action: action.action, options: {} }
|
|
|
|
/**
|
|
* propagate selector or element to options object
|
|
*/
|
|
if (
|
|
selector &&
|
|
// selector is given as string `e.g. browser.touchAction(selector, 'tap')`
|
|
typeof selector === 'string' &&
|
|
// don't propagate for actions that don't require element options
|
|
POS_ACTIONS.indexOf(typeof action === 'string' ? action : formattedAction.action) > -1 &&
|
|
// don't propagate if user has x and y set
|
|
!(isFinite(action.x) && isFinite(action.y))
|
|
) {
|
|
formattedAction.options.selector = selector
|
|
} else if (
|
|
selector &&
|
|
// selector is given by previous command
|
|
// e.g. $(selector).touchAction('tap')
|
|
selector.value &&
|
|
// don't propagate for actions that don't require element options
|
|
POS_ACTIONS.indexOf(typeof action === 'string' ? action : formattedAction.action) > -1 &&
|
|
// don't propagate if user has x and y set
|
|
!(isFinite(action.x) && isFinite(action.y))
|
|
) {
|
|
formattedAction.options.element = selector.value.ELEMENT
|
|
}
|
|
|
|
if (typeof action === 'string') {
|
|
if (!hasValidActionOptions(action, formattedAction.options)) {
|
|
throw new Error(
|
|
`Touch action "${action}" doesn't have proper options. Make sure certain actions like ` +
|
|
`${POS_ACTIONS.join(', ')} have position options like "selector", "x" or "y".`
|
|
)
|
|
}
|
|
|
|
formattedAction.action = action
|
|
|
|
/**
|
|
* remove options property if empyt
|
|
*/
|
|
if (Object.keys(formattedAction.options).length === 0) {
|
|
delete formattedAction.options
|
|
}
|
|
|
|
return formattedAction
|
|
}
|
|
|
|
if (isFinite(action.x)) formattedAction.options.x = action.x
|
|
if (isFinite(action.y)) formattedAction.options.y = action.y
|
|
if (action.ms) formattedAction.options.ms = action.ms
|
|
|
|
if (action.selector && POS_ACTIONS.indexOf(formattedAction.action) > -1) {
|
|
formattedAction.options.selector = action.selector
|
|
}
|
|
|
|
if (action.element) {
|
|
formattedAction.options.element = action.element
|
|
delete formattedAction.options.selector
|
|
}
|
|
|
|
/**
|
|
* remove options property if empyt
|
|
*/
|
|
if (Object.keys(formattedAction.options).length === 0) {
|
|
delete formattedAction.options
|
|
}
|
|
|
|
/**
|
|
* option check
|
|
* make sure action has proper options before sending command to Appium
|
|
*/
|
|
if (formattedAction.action === 'release' && formattedAction.options) {
|
|
throw new Error(
|
|
'action "release" doesn\'t accept any options ' +
|
|
`("${Object.keys(formattedAction.options).join('", "')}" found)`
|
|
)
|
|
} else if (
|
|
formattedAction.action === 'wait' &&
|
|
(Object.keys(formattedAction.options).indexOf('x') > -1 || Object.keys(formattedAction.options).indexOf('y') > -1)
|
|
) {
|
|
throw new Error('action "wait" doesn\'t accept x, y options')
|
|
} else if (POS_ACTIONS.indexOf(formattedAction.action) > -1) {
|
|
for (const option in formattedAction.options) {
|
|
if (ACCEPTED_OPTIONS.indexOf(option) === -1) {
|
|
throw new Error(`action "${formattedAction.action}" doesn't accept "${option}" as option`)
|
|
}
|
|
}
|
|
|
|
if (Object.keys(formattedAction.options || {}).length === 0) {
|
|
throw new Error(
|
|
`Touch actions like "${formattedAction.action}" need at least some kind of ` +
|
|
'position information like "selector", "x" or "y" options, you\'ve none given.'
|
|
)
|
|
}
|
|
}
|
|
|
|
return formattedAction
|
|
})
|
|
}
|
|
|
|
let getSelectors = function (actions, isMultiAction = false) {
|
|
let queriedSelectors = []
|
|
|
|
/**
|
|
* flatten actions array
|
|
*/
|
|
if (isMultiAction) {
|
|
actions = [].concat.apply([], actions)
|
|
}
|
|
|
|
return actions
|
|
/**
|
|
* map down to list of selectors
|
|
*/
|
|
.map((action) => action.options && action.options.selector)
|
|
/**
|
|
* filter actions without selector and unique selectors
|
|
*/
|
|
.filter((selector) => {
|
|
const res = Boolean(selector) && queriedSelectors.indexOf(selector) === -1
|
|
queriedSelectors.push(selector)
|
|
return res
|
|
})
|
|
/**
|
|
* call element command on selectors
|
|
*/
|
|
.map((selector) => this.element(selector))
|
|
}
|
|
|
|
/**
|
|
* replaces selector action properties with element ids after they got fetched
|
|
* @param {Object[]} actions list of actions
|
|
* @param {Object[]} elements list of fetched elements
|
|
* @return {Object[]} list of actions with proper element ids
|
|
*/
|
|
let replaceSelectorsById = function (actions, elements) {
|
|
return actions.map((action) => {
|
|
if (Array.isArray(action)) {
|
|
return replaceSelectorsById(action, elements)
|
|
}
|
|
|
|
if (!action.options || !action.options.selector) {
|
|
return action
|
|
}
|
|
|
|
elements.forEach((element) => {
|
|
if (action.options.selector === element.selector) {
|
|
action.options.element = element.value.ELEMENT
|
|
delete action.options.selector
|
|
}
|
|
})
|
|
|
|
return action
|
|
})
|
|
}
|