UNPKG

kuben-appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

317 lines (284 loc) 11.5 kB
import { errors } from 'appium-base-driver'; import { util } from 'appium-support'; import { iosCommands } from 'kuben-appium-ios-driver'; import _ from 'lodash'; import log from '../logger'; let helpers = {}, extensions = {}, commands = {}; commands.moveTo = iosCommands.gesture.moveTo; commands.mobileShake = async function () { if (!this.isSimulator()) { throw new errors.UnknownError('Shake is not supported on real devices'); } await this.opts.device.shake(); }; commands.click = async function (el) { if (!this.isWebContext()) { // there are multiple commands that map here, so manually proxy return await this.nativeClick(el); } el = util.unwrapElement(el); if ((await this.settings.getSettings()).nativeWebTap) { // atoms-based clicks don't always work in safari 7 log.debug('Using native web tap'); await this.nativeWebTap(el); } else { let atomsElement = this.useAtomsElement(el); return await this.executeAtom('click', [atomsElement]); } }; function gesturesChainToString (gestures, keysToInclude = ['options']) { return gestures.map((item) => { let otherKeys = _.difference(_.keys(item), ['action']); otherKeys = _.isArray(keysToInclude) ? _.intersection(otherKeys, keysToInclude) : otherKeys; if (otherKeys.length) { return `${item.action}` + `(${_.map(otherKeys, (x) => x + '=' + (_.isPlainObject(item[x]) ? JSON.stringify(item[x]) : item[x])).join(', ')})`; } return item.action; }).join('-'); } commands.performActions = async function (actions) { log.debug(`Received the following W3C actions: ${JSON.stringify(actions, null, ' ')}`); // This is mandatory, since WDA only supports TOUCH pointer type // and Selenium API uses MOUSE as the default one const preprocessedActions = actions .map((action) => Object.assign({}, action, action.type === 'pointer' ? { parameters: { pointerType: 'touch' } } : {})) .map((action) => { const modifiedAction = _.clone(action) || {}; // Selenium API unexpectedly inserts zero pauses, which are not supported by WDA modifiedAction.actions = (action.actions || []) .filter((innerAction) => !(innerAction.type === 'pause' && innerAction.duration === 0)); return modifiedAction; }); log.debug(`Preprocessed actions: ${JSON.stringify(preprocessedActions, null, ' ')}`); return await this.proxyCommand('/actions', 'POST', {actions: preprocessedActions}); }; commands.performTouch = async function (gestures) { log.debug(`Received the following touch action: ${gesturesChainToString(gestures)}`); try { return await this.proxyCommand('/wda/touch/perform', 'POST', {actions: gestures}); } catch (e) { if (!this.isWebContext()) { throw e; } log.errorAndThrow('The Touch API is aimed for usage in NATIVE context. ' + 'Consider using "execute" API with custom events trigger script ' + `to emulate touch events being in WEBVIEW context. Original error: ${e.message}`); } }; commands.performMultiAction = async function (actions) { log.debug(`Received the following multi touch action:`); for (let i in actions) { log.debug(` ${parseInt(i, 10) + 1}: ${_.map(actions[i], 'action').join('-')}`); } try { return await this.proxyCommand('/wda/touch/multi/perform', 'POST', {actions}); } catch (e) { if (!this.isWebContext()) { throw e; } log.errorAndThrow('The MultiTouch API is aimed for usage in NATIVE context. ' + 'Consider using "execute" API with custom events trigger script ' + `to emulate multitouch events being in WEBVIEW context. Original error: ${e.message}`); } }; commands.nativeClick = async function (el) { el = util.unwrapElement(el); let endpoint = `/element/${el}/click`; return await this.proxyCommand(endpoint, 'POST', {}); }; /* * See https://github.com/facebook/WebDriverAgent/blob/master/WebDriverAgentLib/Commands/FBElementCommands.m * to get the info about available WDA gestures API * * See https://developer.apple.com/reference/xctest/xcuielement and * https://developer.apple.com/reference/xctest/xcuicoordinate to get the detailed description of * all XCTest gestures */ helpers.mobileScroll = async function (opts = {}, swipe = false) { if (!opts.element) { opts.element = await this.findNativeElementOrElements(`class name`, `XCUIElementTypeApplication`, false); } // WDA supports four scrolling strategies: predication based on name, direction, // predicateString, and toVisible, in that order. Swiping requires direction. let params = {}; if (opts.name && !swipe) { params.name = opts.name; } else if (opts.direction) { if (!['up', 'down', 'left', 'right'].includes(opts.direction.toLowerCase())) { let msg = 'Direction must be up, down, left or right'; log.errorAndThrow(msg); } params.direction = opts.direction; } else if (opts.predicateString && !swipe) { params.predicateString = opts.predicateString; } else if (opts.toVisible && !swipe) { params.toVisible = opts.toVisible; } else { let msg = swipe ? 'Mobile swipe requires direction' : 'Mobile scroll supports the following strategies: name, direction, predicateString, and toVisible. Specify one of these'; log.errorAndThrow(msg); } // we can also optionally pass a distance which appears to be a ratio of // screen height, so 1.0 means a full screen's worth of scrolling if (!swipe && opts.distance) { params.distance = opts.distance; } let element = opts.element.ELEMENT || opts.element; let endpoint = `/wda/element/${element}/${swipe ? 'swipe' : 'scroll'}`; return await this.proxyCommand(endpoint, 'POST', params); }; helpers.mobileSwipe = async function (opts = {}) { return await this.mobileScroll(opts, true); }; function parseFloatParameter (paramName, paramValue, methodName) { if (_.isUndefined(paramValue)) { log.errorAndThrow(`"${paramName}" parameter is mandatory for "${methodName}" call`); } const result = parseFloat(paramValue); if (isNaN(result)) { log.errorAndThrow(`"${paramName}" parameter should be a valid number. "${paramValue}" is given instead`); } return result; } helpers.mobilePinch = async function (opts = {}) { if (!opts.element) { opts.element = await this.findNativeElementOrElements(`class name`, `XCUIElementTypeApplication`, false); } const params = { scale: parseFloatParameter('scale', opts.scale, 'pinch'), velocity: parseFloatParameter('velocity', opts.velocity, 'pinch') }; const el = opts.element.ELEMENT || opts.element; return await this.proxyCommand(`/wda/element/${el}/pinch`, 'POST', params); }; helpers.mobileDoubleTap = async function (opts = {}) { if (opts.element) { // Double tap element const el = opts.element.ELEMENT || opts.element; return await this.proxyCommand(`/wda/element/${el}/doubleTap`, 'POST'); } // Double tap coordinates const params = { x: parseFloatParameter('x', opts.x, 'doubleTap'), y: parseFloatParameter('y', opts.y, 'doubleTap') }; return await this.proxyCommand('/wda/doubleTap', 'POST', params); }; helpers.mobileTwoFingerTap = async function (opts = {}) { if (!opts.element) { opts.element = await this.findNativeElementOrElements(`class name`, `XCUIElementTypeApplication`, false); } const el = opts.element.ELEMENT || opts.element; return await this.proxyCommand(`/wda/element/${el}/twoFingerTap`, 'POST'); }; helpers.mobileTouchAndHold = async function (opts = {}) { let params = { duration: parseFloatParameter('duration', opts.duration, 'touchAndHold') }; if (opts.element) { // Long tap element const el = opts.element.ELEMENT || opts.element; return await this.proxyCommand(`/wda/element/${el}/touchAndHold`, 'POST', params); } // Long tap coordinates params.x = parseFloatParameter('x', opts.x, 'touchAndHold'); params.y = parseFloatParameter('y', opts.y, 'touchAndHold'); return await this.proxyCommand('/wda/touchAndHold', 'POST', params); }; helpers.mobileTap = async function (opts = {}) { const params = { x: parseFloatParameter('x', opts.x, 'tap'), y: parseFloatParameter('y', opts.y, 'tap') }; const el = opts.element ? (opts.element.ELEMENT || opts.element) : '0'; return await this.proxyCommand(`/wda/tap/${el}`, 'POST', params); }; helpers.mobileDragFromToForDuration = async function (opts = {}) { const params = { duration: parseFloatParameter('duration', opts.duration, 'dragFromToForDuration'), fromX: parseFloatParameter('fromX', opts.fromX, 'dragFromToForDuration'), fromY: parseFloatParameter('fromY', opts.fromY, 'dragFromToForDuration'), toX: parseFloatParameter('toX', opts.toX, 'dragFromToForDuration'), toY: parseFloatParameter('toY', opts.toY, 'dragFromToForDuration') }; if (opts.element) { // Drag element const el = opts.element.ELEMENT || opts.element; return await this.proxyCommand(`/wda/element/${el}/dragfromtoforduration`, 'POST', params); } // Drag coordinates return await this.proxyCommand('/wda/dragfromtoforduration', 'POST', params); }; helpers.mobileSelectPickerWheelValue = async function (opts = {}) { if (!opts.element) { log.errorAndThrow('Element id is expected to be set for selectPickerWheelValue method'); } if (!_.isString(opts.order) || !['next', 'previous'].includes(opts.order.toLowerCase())) { log.errorAndThrow(`The mandatory 'order' parameter is expected to be equal either to 'next' or 'previous'. ` + `'${opts.order}' is given instead`); } const el = opts.element.ELEMENT || opts.element; const params = {order: opts.order}; if (opts.offset) { params.offset = parseFloatParameter('offset', opts.offset, 'selectPickerWheelValue'); } return await this.proxyCommand(`/wda/pickerwheel/${el}/select`, 'POST', params); }; helpers.getCoordinates = async function (gesture) { let el = gesture.options.element; // defaults let coordinates = {x: 0, y: 0, areOffsets: false}; let optionX = null; if (gesture.options.x) { optionX = parseFloatParameter('x', gesture.options.x, 'getCoordinates'); } let optionY = null; if (gesture.options.y) { optionY = parseFloatParameter('y', gesture.options.y, 'getCoordinates'); } // figure out the element coordinates. if (el) { let rect = await this.getElementRect(el); let pos = {x: rect.x, y: rect.y}; let size = {w: rect.width, h: rect.height}; // defaults let offsetX = 0; let offsetY = 0; // get the real offsets if (optionX || optionY) { offsetX = (optionX || 0); offsetY = (optionY || 0); } else { offsetX = (size.w / 2); offsetY = (size.h / 2); } // apply the offsets coordinates.x = pos.x + offsetX; coordinates.y = pos.y + offsetY; } else { // moveTo coordinates are passed in as offsets coordinates.areOffsets = (gesture.action === 'moveTo'); coordinates.x = (optionX || 0); coordinates.y = (optionY || 0); } return coordinates; }; helpers.applyMoveToOffset = function (firstCoordinates, secondCoordinates) { if (secondCoordinates.areOffsets) { return { x: firstCoordinates.x + secondCoordinates.x, y: firstCoordinates.y + secondCoordinates.y, }; } else { return secondCoordinates; } }; Object.assign(extensions, helpers, commands); export { extensions, helpers, commands, gesturesChainToString }; export default extensions;