diff --git a/lib/commands/execute.js b/lib/commands/execute.js index b1ebb6312..033982dc2 100644 --- a/lib/commands/execute.js +++ b/lib/commands/execute.js @@ -1,120 +1,89 @@ import _ from 'lodash'; -import {errors, PROTOCOLS} from 'appium/driver'; -import {AndroidUiautomator2Driver} from '../driver'; - -const MOBILE_SCRIPT_NAME_PREFIX = 'mobile:'; +import {AndroidDriver} from 'appium-android-driver'; /** - * @override - * @privateRemarks Because the "mobile" commands (execute methods) in this - * driver universally accept an options object, this method will _not_ call - * into `BaseDriver.executeMethod`. * @this {AndroidUiautomator2Driver} - * @param {string} script - * @param {any[]} [args] - * @returns {Promise} + * @returns {import('@appium/types').StringRecord} */ -export async function execute(script, args) { - const mobileScriptName = toExecuteMethodName(script); - const isWebContext = this.isWebContext(); - if (mobileScriptName && isWebContext || !isWebContext) { - if (mobileScriptName) { - const executeMethodArgs = preprocessExecuteMethodArgs(args); - this.log.info(`Executing method '${mobileScriptName}'`); - return await this.executeMobile(mobileScriptName, executeMethodArgs); - } - // Just pass the script name through and let it fail with a proper error message - return await this.executeMobile(`${script}`, {}); - } - const endpoint = - /** @type {import('appium-chromedriver').Chromedriver} */ (this.chromedriver).jwproxy - .downstreamProtocol === PROTOCOLS.MJSONWP - ? '/execute' - : '/execute/sync'; - return await /** @type {import('appium-chromedriver').Chromedriver} */ ( - this.chromedriver - ).jwproxy.command(endpoint, 'POST', { - script, - args, - }); +export function mobileCommandsMapping() { + const commonMapping = new AndroidDriver().mobileCommandsMapping.call(this); + return { + ...commonMapping, + dragGesture: 'mobileDragGesture', + flingGesture: 'mobileFlingGesture', + doubleClickGesture: 'mobileDoubleClickGesture', + clickGesture: 'mobileClickGesture', + longClickGesture: 'mobileLongClickGesture', + pinchCloseGesture: 'mobilePinchCloseGesture', + pinchOpenGesture: 'mobilePinchOpenGesture', + swipeGesture: 'mobileSwipeGesture', + scrollGesture: 'mobileScrollGesture', + scrollBackTo: 'mobileScrollBackTo', + scroll: 'mobileScroll', + viewportScreenshot: 'mobileViewportScreenshot', + viewportRect: 'mobileViewPortRect', + + deepLink: 'mobileDeepLink', + + acceptAlert: 'mobileAcceptAlert', + dismissAlert: 'mobileDismissAlert', + + batteryInfo: 'mobileGetBatteryInfo', + + deviceInfo: 'mobileGetDeviceInfo', + + openNotifications: 'openNotifications', + + type: 'mobileType', + replaceElementValue: 'mobileReplaceElementValue', + + getAppStrings: 'mobileGetAppStrings', + + installMultipleApks: 'mobileInstallMultipleApks', + + pressKey: 'mobilePressKey', + + screenshots: 'mobileScreenshots', + + scheduleAction: 'mobileScheduleAction', + getActionHistory: 'mobileGetActionHistory', + unscheduleAction: 'mobileUnscheduleAction', + }; } /** * @override * @this {AndroidUiautomator2Driver} - * @param {string} script Must be of the form `mobile: `, which - * differs from its parent class implementation. + * @param {string} mobileCommand * @param {import('@appium/types').StringRecord} [opts={}] * @returns {Promise} */ -export async function executeMobile(script, opts = {}) { - if (!(script in AndroidUiautomator2Driver.executeMethodMap)) { - const commandNames = _.map( - _.keys(AndroidUiautomator2Driver.executeMethodMap), - (value) => value.slice(8) - ); - throw new errors.UnknownCommandError( - `Unknown mobile command "${script}". ` + - `Only ${commandNames.join(', ')} commands are supported.` - ); - } - const methodName = - AndroidUiautomator2Driver.executeMethodMap[ - /** @type {keyof import('../execute-method-map').Uiautomator2ExecuteMethodMap} */ (script) - ].command; - - return await /** @type {(opts?: any) => Promise} */ (this[methodName])(opts); +export async function executeMobile(mobileCommand, opts = {}) { + return await new AndroidDriver().executeMobile.call(this, mobileCommand, preprocessOptions(opts)); } // #region Internal Helpers /** - * Messages the arguments going into an execute method. - * @remarks A similar method is implemented in `appium-xcuitest-driver`, but it - * appears the methods in here handle unwrapping of `Element` objects, so we do - * not do that here. - * @param {readonly any[] | readonly [StringRecord] | Readonly} [args] + * Renames the deprecated `element` key to `elementId`. Historically, + * all of the pre-Execute-Method-Map execute methods accepted an `element` _or_ and `elementId` param. + * This assigns the `element` value to `elementId` if `elementId` is not already present. + * + * @param {import('@appium/types').StringRecord} [opts={}] * @internal - * @returns {StringRecord} + * @returns {import('@appium/types').StringRecord|undefined} */ -function preprocessExecuteMethodArgs(args) { - if (_.isArray(args)) { - args = _.first(args); +function preprocessOptions(opts = {}) { + if (_.isPlainObject(opts) && !('elementId' in opts) && 'element' in opts) { + opts.elementId = opts.element; + delete opts.element; + this.log.debug(`Replaced the obsolete 'element' key with 'elementId'`); } - const executeMethodArgs = /** @type {StringRecord} */ (args ?? {}); - /** - * Renames the deprecated `element` key to `elementId`. Historically, - * all of the pre-Execute-Method-Map execute methods accepted an `element` _or_ and `elementId` param. - * This assigns the `element` value to `elementId` if `elementId` is not already present. - */ - if (!('elementId' in executeMethodArgs) && 'element' in executeMethodArgs) { - executeMethodArgs.elementId = executeMethodArgs.element; - delete executeMethodArgs.element; - } - - return executeMethodArgs; -} - -/** - * Type guard to check if a script is an execute method. - * @param {any} script - * @internal - * @returns {string?} - */ -function toExecuteMethodName(script) { - return _.startsWith(script, MOBILE_SCRIPT_NAME_PREFIX) - ? script.replace(new RegExp(`${MOBILE_SCRIPT_NAME_PREFIX}\\s*`), `${MOBILE_SCRIPT_NAME_PREFIX} `) - : null; + return opts; } // #endregion /** - * @typedef {import('../uiautomator2').UiAutomator2Server} UiAutomator2Server - * @typedef {import('appium-adb').ADB} ADB - */ - -/** - * @template [T=any] - * @typedef {import('@appium/types').StringRecord} StringRecord + * @typedef {import('../driver').AndroidUiautomator2Driver} AndroidUiautomator2Driver */ diff --git a/lib/commands/gestures.js b/lib/commands/gestures.js index 2b7a83923..39eda3a36 100644 --- a/lib/commands/gestures.js +++ b/lib/commands/gestures.js @@ -238,8 +238,7 @@ export async function mobileScrollBackTo(opts) { */ export async function mobileScroll(opts) { const { - element, - elementId, // `element` is deprecated, use `elementId` instead + elementId, strategy, selector, maxSwipes, @@ -253,7 +252,7 @@ export async function mobileScroll(opts) { '/gestures/scroll_to', 'POST', { - origin: toOrigin(elementId || element), + origin: toOrigin(elementId), params: {strategy, selector, maxSwipes}, } ); diff --git a/lib/driver.ts b/lib/driver.ts index 25df33227..b3d74d916 100644 --- a/lib/driver.ts +++ b/lib/driver.ts @@ -22,7 +22,6 @@ import path from 'node:path'; import {checkPortStatus, findAPortNotInUse} from 'portscanner'; import type {ExecError} from 'teen_process'; import UIAUTOMATOR2_CONSTRAINTS, {type Uiautomator2Constraints} from './constraints'; -import {executeMethodMap} from './execute-method-map'; import {APKS_EXTENSION, APK_EXTENSION} from './extensions'; import {newMethodMap} from './method-map'; import { signApp } from './helpers'; @@ -81,8 +80,8 @@ import { mobileReplaceElementValue, } from './commands/element'; import { - execute, executeMobile, + mobileCommandsMapping, } from './commands/execute'; import { doFindElementOrEls, @@ -266,8 +265,6 @@ class AndroidUiautomator2Driver { static newMethodMap = newMethodMap; - static executeMethodMap = executeMethodMap; - uiautomator2: UiAutomator2Server; systemPort: number | undefined; @@ -1030,8 +1027,8 @@ class AndroidUiautomator2Driver clear = clear; mobileReplaceElementValue = mobileReplaceElementValue; - execute = execute; executeMobile = executeMobile; + mobileCommandsMapping = mobileCommandsMapping; doFindElementOrEls = doFindElementOrEls; diff --git a/lib/execute-method-map.ts b/lib/execute-method-map.ts deleted file mode 100644 index 956093a1a..000000000 --- a/lib/execute-method-map.ts +++ /dev/null @@ -1,624 +0,0 @@ -/** - * @privateRemarks This was created by hand from the type definitions in `lib/commands` here and in `appium-android-driver`. - * @module - */ - -export const executeMethodMap = { - 'mobile: shell': { - command: 'mobileShell', - params: { - required: ['command'], - optional: ['args', 'timeout', 'includeStderr'], - }, - }, - 'mobile: execEmuConsoleCommand': { - command: 'mobileExecEmuConsoleCommand', - params: { - required: ['command'], - optional: ['execTimeout', 'connTimeout', 'initTimeout'], - }, - }, - 'mobile: dragGesture': { - command: 'mobileDragGesture', - params: { - optional: ['elementId', 'startX', 'startY', 'endX', 'endY', 'speed'], - }, - }, - 'mobile: flingGesture': { - command: 'mobileFlingGesture', - params: { - required: ['direction'], - optional: ['elementId', 'left', 'top', 'width', 'height', 'speed'], - }, - }, - 'mobile: doubleClickGesture': { - command: 'mobileDoubleClickGesture', - params: { - optional: ['elementId', 'x', 'y'], - }, - }, - 'mobile: clickGesture': { - command: 'mobileClickGesture', - params: { - optional: ['elementId', 'x', 'y'], - }, - }, - 'mobile: longClickGesture': { - command: 'mobileLongClickGesture', - params: { - optional: ['elementId', 'x', 'y', 'duration'], - }, - }, - 'mobile: pinchCloseGesture': { - command: 'mobilePinchCloseGesture', - params: { - required: ['percent'], - optional: ['elementId', 'left', 'top', 'width', 'height', 'speed'], - }, - }, - 'mobile: pinchOpenGesture': { - command: 'mobilePinchOpenGesture', - params: { - required: ['percent'], - optional: ['elementId', 'left', 'top', 'width', 'height', 'speed'], - }, - }, - 'mobile: swipeGesture': { - command: 'mobileSwipeGesture', - params: { - required: ['direction', 'percent'], - optional: ['elementId', 'left', 'top', 'width', 'height', 'speed'], - }, - }, - 'mobile: scrollGesture': { - command: 'mobileScrollGesture', - params: { - required: ['direction', 'percent'], - optional: ['elementId', 'left', 'top', 'width', 'height', 'speed'], - }, - }, - 'mobile: scrollBackTo': { - command: 'mobileScrollBackTo', - params: { - required: ['elementId', 'elementToId'], - }, - }, - 'mobile: scroll': { - command: 'mobileScroll', - params: { - required: ['strategy', 'selector'], - optional: ['elementId', 'maxSwipes', 'element'], - }, - }, - 'mobile: viewportScreenshot': { - command: 'mobileViewportScreenshot', - }, - 'mobile: viewportRect': { - command: 'mobileViewPortRect', - }, - - 'mobile: deepLink': { - command: 'mobileDeepLink', - params: { - required: ['url', 'package'], - optional: ['waitForLaunch'], - }, - }, - - 'mobile: startLogsBroadcast': { - command: 'mobileStartLogsBroadcast', - }, - 'mobile: stopLogsBroadcast': { - command: 'mobileStopLogsBroadcast', - }, - - 'mobile: deviceidle': { - command: 'mobileDeviceidle', - params: { - required: ['action'], - optional: ['packages'], - }, - }, - - 'mobile: acceptAlert': { - command: 'mobileAcceptAlert', - params: { - optional: ['buttonLabel'], - }, - }, - 'mobile: dismissAlert': { - command: 'mobileDismissAlert', - params: { - optional: ['buttonLabel'], - }, - }, - - 'mobile: batteryInfo': { - command: 'mobileGetBatteryInfo', - }, - - 'mobile: deviceInfo': { - command: 'mobileGetDeviceInfo', - }, - - 'mobile: getDeviceTime': { - command: 'mobileGetDeviceTime', - params: { - optional: ['format'], - }, - }, - - 'mobile: changePermissions': { - command: 'mobileChangePermissions', - params: { - required: ['permissions'], - optional: ['appPackage', 'action', 'target'], - }, - }, - 'mobile: getPermissions': { - command: 'mobileGetPermissions', - params: { - optional: ['type', 'appPackage'], - }, - }, - - 'mobile: performEditorAction': { - command: 'mobilePerformEditorAction', - params: { - required: ['action'], - }, - }, - - 'mobile: startScreenStreaming': { - command: 'mobileStartScreenStreaming', - params: { - optional: [ - 'width', - 'height', - 'bitrate', - 'host', - 'pathname', - 'tcpPort', - 'port', - 'quality', - 'considerRotation', - 'logPipelineDetails', - ], - }, - }, - 'mobile: stopScreenStreaming': { - command: 'mobileStopScreenStreaming', - }, - - 'mobile: getNotifications': { - command: 'mobileGetNotifications', - }, - 'mobile: openNotifications': { - command: 'openNotifications', - }, - - 'mobile: listSms': { - command: 'mobileListSms', - params: { - optional: ['max'], - }, - }, - - 'mobile: type': { - command: 'mobileType', - params: { - required: ['text'], - }, - }, - 'mobile: replaceElementValue': { - command: 'mobileReplaceElementValue', - params: { - required: ['elementId', 'text'], - }, - }, - - 'mobile: pushFile': { - command: 'mobilePushFile', - params: { - required: ['payload', 'remotePath'], - }, - }, - 'mobile: pullFile': { - command: 'mobilePullFile', - params: { - required: ['remotePath'], - }, - }, - 'mobile: pullFolder': { - command: 'mobilePullFolder', - params: { - required: ['remotePath'], - }, - }, - 'mobile: deleteFile': { - command: 'mobileDeleteFile', - params: { - required: ['remotePath'], - }, - }, - - 'mobile: isAppInstalled': { - command: 'mobileIsAppInstalled', - params: { - required: ['appId'], - optional: ['user'], - }, - }, - 'mobile: queryAppState': { - command: 'mobileQueryAppState', - params: { - required: ['appId'], - }, - }, - 'mobile: activateApp': { - command: 'mobileActivateApp', - params: { - required: ['appId'], - }, - }, - 'mobile: removeApp': { - command: 'mobileRemoveApp', - params: { - required: ['appId'], - optional: ['timeout', 'keepData'], - }, - }, - 'mobile: terminateApp': { - command: 'mobileTerminateApp', - params: { - required: ['appId'], - optional: ['timeout'], - }, - }, - 'mobile: installApp': { - command: 'mobileInstallApp', - params: { - required: ['appPath'], - optional: ['timeout', 'allowTestPackages', 'useSdcard', 'grantPermissions', 'replace', 'checkVersion'], - }, - }, - 'mobile: clearApp': { - command: 'mobileClearApp', - params: { - required: ['appId'], - }, - }, - 'mobile: backgroundApp': { - command: 'mobileBackgroundApp', - params: { - optional: ['seconds'], - }, - }, - 'mobile: getCurrentActivity': { - command: 'getCurrentActivity', - }, - 'mobile: getCurrentPackage': { - command: 'getCurrentPackage', - }, - - 'mobile: startActivity': { - command: 'mobileStartActivity', - params: { - optional: ['wait', 'stop', 'windowingMode', 'activityType', 'display'], - }, - }, - 'mobile: startService': { - command: 'mobileStartService', - params: { - optional: [ - 'user', - 'intent', - 'action', - 'package', - 'uri', - 'mimeType', - 'identifier', - 'component', - 'categories', - 'extras', - 'flags', - 'wait', - 'stop', - 'windowingMode', - 'activityType', - 'display', - ], - }, - }, - 'mobile: stopService': { - command: 'mobileStopService', - params: { - optional: [ - 'user', - 'intent', - 'action', - 'package', - 'uri', - 'mimeType', - 'identifier', - 'component', - 'categories', - 'extras', - 'flags', - ], - }, - }, - 'mobile: broadcast': { - command: 'mobileBroadcast', - params: { - optional: [ - 'user', - 'intent', - 'action', - 'package', - 'uri', - 'mimeType', - 'identifier', - 'component', - 'categories', - 'extras', - 'flags', - 'receiverPermission', - 'allowBackgroundActivityStarts', - ], - }, - }, - - 'mobile: getContexts': { - command: 'mobileGetContexts', - params: { - optional: ['waitForWebviewMs'], - }, - }, - - 'mobile: getAppStrings': { - command: 'mobileGetAppStrings', - params: { - optional: ['language'], - }, - }, - - 'mobile: installMultipleApks': { - command: 'mobileInstallMultipleApks', - params: { - required: ['apks'], - optional: ['options'], - }, - }, - - 'mobile: lock': { - command: 'mobileLock', - params: { - optional: ['seconds'], - }, - }, - 'mobile: unlock': { - command: 'mobileUnlock', - params: { - optional: ['key', 'type', 'strategy', 'timeoutMs'], - }, - }, - 'mobile: isLocked': { - command: 'isLocked', - }, - - 'mobile: refreshGpsCache': { - command: 'mobileRefreshGpsCache', - params: { - optional: ['timeoutMs'], - }, - }, - - 'mobile: startMediaProjectionRecording': { - command: 'mobileStartMediaProjectionRecording', - params: { - optional: ['resolution', 'maxDurationSec', 'priority', 'filename'], - }, - }, - 'mobile: isMediaProjectionRecordingRunning': { - command: 'mobileIsMediaProjectionRecordingRunning', - }, - 'mobile: stopMediaProjectionRecording': { - command: 'mobileStopMediaProjectionRecording', - params: { - optional: [ - 'remotePath', - 'user', - 'pass', - 'method', - 'headers', - 'fileFieldName', - 'formFields', - 'uploadTimeout', - ], - }, - }, - - 'mobile: getConnectivity': { - command: 'mobileGetConnectivity', - params: { - optional: ['services'], - }, - }, - 'mobile: setConnectivity': { - command: 'mobileSetConnectivity', - params: { - optional: ['wifi', 'data', 'airplaneMode'], - }, - }, - 'mobile: toggleGps': { - command: 'toggleLocationServices', - }, - 'mobile: isGpsEnabled': { - command: 'isLocationServicesEnabled', - }, - - 'mobile: hideKeyboard': { - command: 'hideKeyboard', - }, - 'mobile: isKeyboardShown': { - command: 'isKeyboardShown', - }, - - 'mobile: pressKey': { - command: 'mobilePressKey', - params: { - required: ['keycode'], - optional: ['metastate', 'flags', 'isLongPress'], - }, - }, - - 'mobile: getDisplayDensity': { - command: 'getDisplayDensity', - }, - 'mobile: getSystemBars': { - command: 'getSystemBars', - }, - - 'mobile: fingerprint': { - command: 'mobileFingerprint', - params: { - required: ['fingerprintId'], - }, - }, - 'mobile: sendSms': { - command: 'mobileSendSms', - params: { - required: ['phoneNumber', 'message'], - }, - }, - 'mobile: gsmCall': { - command: 'mobileGsmCall', - params: { - required: ['phoneNumber', 'action'], - }, - }, - 'mobile: gsmSignal': { - command: 'mobileGsmSignal', - params: { - required: ['strength'], - }, - }, - 'mobile: gsmVoice': { - command: 'mobileGsmVoice', - params: { - required: ['state'], - }, - }, - 'mobile: powerAc': { - command: 'mobilePowerAc', - params: { - required: ['state'], - }, - }, - 'mobile: powerCapacity': { - command: 'mobilePowerCapacity', - params: { - required: ['percent'], - }, - }, - 'mobile: networkSpeed': { - command: 'mobileNetworkSpeed', - params: { - required: ['speed'], - }, - }, - 'mobile: sensorSet': { - command: 'sensorSet', - params: { - required: ['sensorType', 'value'], - }, - }, - - 'mobile: getPerformanceData': { - command: 'mobileGetPerformanceData', - params: { - required: ['packageName', 'dataType'], - }, - }, - 'mobile: getPerformanceDataTypes': { - command: 'getPerformanceDataTypes', - }, - - 'mobile: statusBar': { - command: 'mobilePerformStatusBarCommand', - params: { - required: ['command'], - optional: ['component'], - }, - }, - - 'mobile: screenshots': { - command: 'mobileScreenshots', - params: { - optional: ['displayId'], - }, - }, - - 'mobile: scheduleAction': { - command: 'mobileScheduleAction', - params: { - optional: ['opts'], - }, - }, - 'mobile: getActionHistory': { - command: 'mobileGetActionHistory', - params: { - optional: ['opts'], - }, - }, - 'mobile: unscheduleAction': { - command: 'mobileUnscheduleAction', - params: { - optional: ['opts'], - }, - }, - - 'mobile: getUiMode': { - command: 'mobileGetUiMode', - params: { - optional: ['opts'], - }, - }, - 'mobile: setUiMode': { - command: 'mobileSetUiMode', - params: { - optional: ['opts'], - }, - }, - - 'mobile: sendTrimMemory': { - command: 'mobileSendTrimMemory', - params: { - optional: ['opts'], - } - }, - - 'mobile: injectEmulatorCameraImage': { - command: 'mobileInjectEmulatorCameraImage', - params: { - optional: ['opts'], - } - }, - - 'mobile: bluetooth': { - command: 'mobileBluetooth', - params: { - optional: ['opts'], - } - }, - - 'mobile: nfc': { - command: 'mobileNfc', - params: { - optional: ['opts'], - } - }, -} as const; - -export type Uiautomator2ExecuteMethodMap = typeof executeMethodMap; diff --git a/package.json b/package.json index f36a518c7..095138e15 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ }, "dependencies": { "appium-adb": "^12.2.0", - "appium-android-driver": "^9.5.0", + "appium-android-driver": "^9.6.0", "appium-chromedriver": "^5.6.28", "appium-uiautomator2-server": "^7.0.1", "asyncbox": "^3.0.0", diff --git a/test/unit/commands/general-specs.js b/test/unit/commands/general-specs.js index ebfe04ee8..b044a8dfa 100644 --- a/test/unit/commands/general-specs.js +++ b/test/unit/commands/general-specs.js @@ -11,7 +11,6 @@ const {expect} = chai; chai.use(chaiAsPromised).use(sinonChai); describe('General', function () { - /** @type {AndroidUiautomator2Driver} */ let driver; /** @type {import('sinon').SinonSandbox} */ let sandbox; @@ -39,8 +38,8 @@ describe('General', function () { }); it('should raise error on non-existent mobile command', async function () { - await expect(driver.executeMobile('mobile: fruta', {})).to.be.rejectedWith( - /Unknown mobile command "mobile: fruta"/ + await expect(driver.execute('mobile: fruta', {})).to.be.rejectedWith( + /Unknown mobile command "fruta"/ ); }); @@ -50,7 +49,7 @@ describe('General', function () { // implementation, which is stubbed out. it('should call sensorSet', async function () { sandbox.stub(driver, 'sensorSet'); - await driver.executeMobile('mobile: sensorSet', { + await driver.execute('mobile: sensorSet', { sensorType: 'acceleration', value: '0:9.77631:0.812349', }); @@ -74,18 +73,18 @@ describe('General', function () { }); it('should call mobileInstallMultipleApks', async function () { - await driver.executeMobile('mobile: installMultipleApks', {apks: ['/path/to/test/apk.apk']}); + await driver.execute('mobile: installMultipleApks', {apks: ['/path/to/test/apk.apk']}); expect(adb.installMultipleApks).to.have.been.calledOnceWith(['/path/to/test/apk.apk']); }); it('should reject if no apks were given', async function () { await expect( - driver.executeMobile('mobile: installMultipleApks', {apks: []}) + driver.execute('mobile: installMultipleApks', {apks: []}) ).to.be.rejectedWith('No apks are given to install'); }); it('should reject if no apks were given', async function () { - await expect(driver.executeMobile('mobile: installMultipleApks')).to.be.rejectedWith( + await expect(driver.execute('mobile: installMultipleApks')).to.be.rejectedWith( 'No apks are given to install' ); });