import type { TouchAction, TouchActions } from '../types.js' /** * Constants around commands */ const TOUCH_ACTIONS = ['press', 'longPress', 'tap', 'moveTo', 'wait', 'release'] const POS_ACTIONS = TOUCH_ACTIONS.slice(0, 4) const ACCEPTED_OPTIONS = ['x', 'y', 'element'] export const SCRIPT_PREFIX = '/* __wdio script__ */' export const SCRIPT_SUFFIX = '/* __wdio script end__ */' /** * These scripts are loaded by Esbuild at build time and injected into the bundle */ declare const WDIO_RESQ_SCRIPT: string export const resqScript = WDIO_RESQ_SCRIPT interface FormattedTouchAction extends Omit { element?: string } interface FormattedActions { action: string options?: FormattedTouchAction } export const formatArgs = function ( scope: WebdriverIO.Browser | WebdriverIO.Element, actions: TouchActions[] ): FormattedActions[] { return actions.map((action: TouchAction) => { if (Array.isArray(action)) { return formatArgs(scope, action) as unknown as FormattedActions } if (typeof action === 'string') { action = { action } } const formattedAction: FormattedActions = { action: action.action, options: {} as FormattedTouchAction } /** * don't propagate for actions that don't require element options */ const actionElement = action.element && typeof (action.element as unknown as WebdriverIO.Element).elementId === 'string' ? (action.element as unknown as WebdriverIO.Element).elementId : (scope as WebdriverIO.Element).elementId if (POS_ACTIONS.includes(action.action) && formattedAction.options && actionElement) { formattedAction.options.element = actionElement } if (formattedAction.options && typeof action.x === 'number' && isFinite(action.x)) { formattedAction.options.x = action.x } if (formattedAction.options && typeof action.y === 'number' && isFinite(action.y)) { formattedAction.options.y = action.y } if (formattedAction.options && action.ms) { formattedAction.options.ms = action.ms } /** * remove options property if empty */ if (formattedAction.options && Object.keys(formattedAction.options).length === 0) { delete formattedAction.options } return formattedAction }) } /** * Make sure action has proper options before sending command to Appium. * * @param {Object} params touchAction parameters * @return null */ export const validateParameters = (params: FormattedActions) => { const options = Object.keys(params.options || {}) if (params.action === 'release' && options.length !== 0) { throw new Error( 'action "release" doesn\'t accept any options ' + `("${options.join('", "')}" found)` ) } if ( params.action === 'wait' && (options.includes('x') || options.includes('y')) ) { throw new Error('action "wait" doesn\'t accept x or y options') } if (POS_ACTIONS.includes(params.action)) { for (const option in params.options) { if (!ACCEPTED_OPTIONS.includes(option)) { throw new Error(`action "${params.action}" doesn't accept "${option}" as option`) } } if (options.length === 0) { throw new Error( `Touch actions like "${params.action}" need at least some kind of ` + 'position information like "element", "x" or "y" options, you\'ve none given.' ) } } } export const touchAction = function ( this: WebdriverIO.Browser | WebdriverIO.Element, actions: TouchActions ) { if (!this.multiTouchPerform || !this.touchPerform) { throw new Error('touchAction can be used with Appium only.') } if (!Array.isArray(actions)) { actions = [actions as TouchAction] } const formattedAction = formatArgs(this, actions) const protocolCommand = Array.isArray(actions[0]) // cast old JSONWP ? this.multiTouchPerform.bind(this) as unknown as (actions: object[]) => Promise : this.touchPerform.bind(this) formattedAction.forEach((params) => validateParameters(params)) return protocolCommand(formattedAction) }