diff --git a/lib/commands/app-management.js b/lib/commands/app-management.js index 54d1403a..8bb7bc53 100644 --- a/lib/commands/app-management.js +++ b/lib/commands/app-management.js @@ -1,7 +1,6 @@ import {util} from '@appium/support'; import {waitForCondition, longSleep} from 'asyncbox'; import _ from 'lodash'; -import {requireArgs} from '../utils'; import {EOL} from 'node:os'; import B from 'bluebird'; @@ -33,11 +32,12 @@ export async function isAppInstalled(appId, opts = {}) { /** * @this {AndroidDriver} - * @param {import('./types').IsAppInstalledOpts} opts + * @param {string} appId Application package identifier + * @param {string | number} [user] The user ID for which the package is installed. + * The `current` user id is used by default. * @returns {Promise} */ -export async function mobileIsAppInstalled(opts) { - const {appId, user} = requireArgs('appId', opts); +export async function mobileIsAppInstalled(appId, user) { const _opts = {}; if (util.hasValue(user)) { _opts.user = `${user}`; @@ -47,7 +47,7 @@ export async function mobileIsAppInstalled(opts) { /** * @this {AndroidDriver} - * @param {string} appId + * @param {string} appId Application package identifier * @returns {Promise} */ export async function queryAppState(appId) { @@ -69,33 +69,13 @@ export async function queryAppState(appId) { /** * @this {AndroidDriver} - * @param {import('./types').QueryAppStateOpts} opts - * @returns {Promise} - */ -export async function mobileQueryAppState(opts) { - const {appId} = requireArgs('appId', opts); - return await this.queryAppState(appId); -} - -/** - * @this {AndroidDriver} - * @param {string} appId + * @param {string} appId Application package identifier * @returns {Promise} */ export async function activateApp(appId) { return await this.adb.activateApp(appId); } -/** - * @this {AndroidDriver} - * @param {import('./types').ActivateAppOpts} opts - * @returns {Promise} - */ -export async function mobileActivateApp(opts) { - const {appId} = requireArgs('appId', opts); - return await this.adb.activateApp(appId); -} - /** * @this {AndroidDriver} * @param {string} appId @@ -108,18 +88,27 @@ export async function removeApp(appId, opts = {}) { /** * @this {import('../driver').AndroidDriver} - * @param {import('./types').RemoveAppOpts} opts + * @param {string} appId Application package identifier + * @param {number} [timeout] The count of milliseconds to wait until the + * app is uninstalled. + * @param {boolean} [keepData] Set to true in order to keep the + * application data and cache folders after uninstall. + * @param {boolean} [skipInstallCheck] Whether to check if the app is installed prior to + * uninstalling it. By default this is checked. * @returns {Promise} */ -export async function mobileRemoveApp(opts) { - const {appId} = requireArgs('appId', opts); - return await this.removeApp(appId, opts); +export async function mobileRemoveApp(appId, timeout, keepData, skipInstallCheck) { + return await this.removeApp(appId, { + timeout, + keepData, + skipInstallCheck, + }); } /** * @this {AndroidDriver} * @param {string} appId - * @param {Omit} [options={}] + * @param {import('./types').TerminateAppOpts} [options={}] * @returns {Promise} */ export async function terminateApp(appId, options = {}) { @@ -180,12 +169,15 @@ export async function terminateApp(appId, options = {}) { /** * @this {AndroidDriver} - * @param {import('./types').TerminateAppOpts} opts + * @param {string} appId Application package identifier + * @param {number|string} [timeout] The count of milliseconds to wait until the app is terminated. + * 500ms by default. * @returns {Promise} */ -export async function mobileTerminateApp(opts) { - const {appId} = requireArgs('appId', opts); - return await this.terminateApp(appId, opts); +export async function mobileTerminateApp(appId, timeout) { + return await this.terminateApp(appId, { + timeout, + }); } /** @@ -201,14 +193,49 @@ export async function installApp(appPath, opts) { /** * @this {AndroidDriver} - * @param {import('./types').InstallAppOpts} opts + * @param {string} appPath + * @param {boolean} [checkVersion] + * @param {number} [timeout] The count of milliseconds to wait until the app is installed. + * 20000ms by default. + * @param {boolean} [allowTestPackages] Set to true in order to allow test packages installation. + * `false` by default. + * @param {boolean} [useSdcard] Set to true to install the app on sdcard instead of the device memory. + * `false` by default. + * @param {boolean} [grantPermissions] Set to true in order to grant all the + * permissions requested in the application's manifest automatically after the installation is completed + * under Android 6+. `false` by default. + * @param {boolean} [replace] Set it to false if you don't want the application to be upgraded/reinstalled + * if it is already present on the device. `true` by default. + * @param {boolean} [noIncremental] Forcefully disables incremental installs if set to `true`. + * Read https://developer.android.com/preview/features#incremental for more details. + * `false` by default. * @returns {Promise} */ -export async function mobileInstallApp(opts) { - const {appPath, checkVersion} = requireArgs('appPath', opts); +export async function mobileInstallApp( + appPath, + checkVersion, + timeout, + allowTestPackages, + useSdcard, + grantPermissions, + replace, + noIncremental, +) { + const opts = { + timeout, + allowTestPackages, + useSdcard, + grantPermissions, + replace, + noIncremental, + }; if (checkVersion) { const localPath = await this.helpers.configureApp(appPath, APP_EXTENSIONS); - await this.adb.installOrUpgrade(localPath, null, Object.assign({}, opts, {enforceCurrentBuild: false})); + await this.adb.installOrUpgrade(localPath, null, Object.assign({}, { + appPath, + checkVersion, + ...opts, + }, {enforceCurrentBuild: false})); return; } @@ -217,11 +244,10 @@ export async function mobileInstallApp(opts) { /** * @this {AndroidDriver} - * @param {import('./types').ClearAppOpts} opts + * @param {string} appId Application package identifier * @returns {Promise} */ -export async function mobileClearApp(opts) { - const {appId} = requireArgs('appId', opts); +export async function mobileClearApp(appId) { await this.adb.clear(appId); } diff --git a/lib/commands/appearance.js b/lib/commands/appearance.js index 2abeeaff..a009d86a 100644 --- a/lib/commands/appearance.js +++ b/lib/commands/appearance.js @@ -1,5 +1,3 @@ -import {requireArgs} from '../utils'; - const RESPONSE_PATTERN = /:\s+(\w+)/; /** @@ -7,11 +5,15 @@ const RESPONSE_PATTERN = /:\s+(\w+)/; * * @since Android 10 * @this {import('../driver').AndroidDriver} - * @property {import('./types').SetUiModeOpts} + * @param {string} mode The UI mode to set the value for. + * Supported values are: 'night' and 'car' + * @param {string} value The actual mode value to set. + * Supported value for different UI modes are: + * - night: yes|no|auto|custom_schedule|custom_bedtime + * - car: yes|no * @returns {Promise} */ -export async function mobileSetUiMode(opts) { - const {mode, value} = requireArgs(['mode', 'value'], opts); +export async function mobileSetUiMode(mode, value) { await this.adb.shell(['cmd', 'uimode', mode, value]); } @@ -20,12 +22,12 @@ export async function mobileSetUiMode(opts) { * * @since Android 10 * @this {import('../driver').AndroidDriver} - * @property {import('./types').GetUiModeOpts} + * @param {string} mode The UI mode to set the value for. + * Supported values are: 'night' and 'car' * @returns {Promise} The actual state for the queried UI mode, * for example 'yes' or 'no' */ -export async function mobileGetUiMode(opts) { - const {mode} = requireArgs(['mode'], opts); +export async function mobileGetUiMode(mode) { const response = await this.adb.shell(['cmd', 'uimode', mode]); // response looks like 'Night mode: no' const match = RESPONSE_PATTERN.exec(response); diff --git a/lib/commands/bluetooth.js b/lib/commands/bluetooth.js index 4ddf2a4d..dae8f21e 100644 --- a/lib/commands/bluetooth.js +++ b/lib/commands/bluetooth.js @@ -11,13 +11,12 @@ const SUPPORTED_ACTIONS = /** @type {const} */ ({ * Performs the requested action on the default bluetooth adapter * * @this {AndroidDriver} - * @param {import('./types').BluetoothOptions} opts + * @param {'enable' | 'disable' | 'unpairAll'} action * @returns {Promise} * @throws {Error} if the device under test has no default bluetooth adapter * or there was a failure while performing the action. */ -export async function mobileBluetooth(opts) { - const {action} = opts; +export async function mobileBluetooth(action) { switch (action) { case SUPPORTED_ACTIONS.ENABLE: await this.settingsApp.setBluetoothState(true); diff --git a/lib/commands/context/exports.js b/lib/commands/context/exports.js index 2fd78aa5..e2e1afcd 100644 --- a/lib/commands/context/exports.js +++ b/lib/commands/context/exports.js @@ -73,16 +73,16 @@ export async function setContext(name) { /** * @this {AndroidDriver} - * @param {any} [opts={}] + * @param {number} [waitForWebviewMs] * @returns {Promise} */ -export async function mobileGetContexts(opts = {}) { +export async function mobileGetContexts(waitForWebviewMs) { const _opts = { androidDeviceSocket: this.opts.androidDeviceSocket, ensureWebviewsHavePages: true, webviewDevtoolsPort: this.opts.webviewDevtoolsPort, enableWebviewDetailsCollection: true, - waitForWebviewMs: opts.waitForWebviewMs || 0, + waitForWebviewMs: waitForWebviewMs || 0, }; return await getWebViewsMapping.bind(this)(_opts); } diff --git a/lib/commands/device/emulator-actions.js b/lib/commands/device/emulator-actions.js index 4a0d5795..468b162c 100644 --- a/lib/commands/device/emulator-actions.js +++ b/lib/commands/device/emulator-actions.js @@ -1,6 +1,6 @@ import {util} from '@appium/support'; -import {requireArgs} from '../../utils'; import {requireEmulator} from './utils'; +import { errors } from 'appium/driver'; /** * @deprecated Use mobile: extension @@ -15,11 +15,14 @@ export async function fingerprint(fingerprintId) { /** * @this {import('../../driver').AndroidDriver} - * @param {import('../types').FingerprintOpts} opts + * @param {string | number} fingerprintId The value is the `finger_id` for the finger that was "scanned". It is a + * unique integer that you assign for each virtual fingerprint. When the app + * is running you can run this same command each time the emulator prompts you + * for a fingerprint, you can run the adb command and pass it the `finger_id` + * to simulate the fingerprint scan. * @returns {Promise} */ -export async function mobileFingerprint(opts) { - const {fingerprintId} = requireArgs('fingerprintId', opts); +export async function mobileFingerprint(fingerprintId) { await this.fingerprint(fingerprintId); } @@ -37,11 +40,11 @@ export async function sendSMS(phoneNumber, message) { /** * @this {import('../../driver').AndroidDriver} - * @param {import('../types').SendSMSOpts} opts + * @param {string} phoneNumber The phone number to send SMS to + * @param {string} message The message payload * @returns {Promise} */ -export async function mobileSendSms(opts) { - const {phoneNumber, message} = requireArgs(['phoneNumber', 'message'], opts); +export async function mobileSendSms(phoneNumber, message) { await this.sendSMS(phoneNumber, message); } @@ -59,11 +62,11 @@ export async function gsmCall(phoneNumber, action) { /** * @this {import('../../driver').AndroidDriver} - * @param {import('../types').GsmCallOpts} opts + * @param {string} phoneNumber The phone number to call to + * @param {import('../types').GsmAction} action Action to take * @returns {Promise} */ -export async function mobileGsmCall(opts) { - const {phoneNumber, action} = requireArgs(['phoneNumber', 'action'], opts); +export async function mobileGsmCall(phoneNumber, action) { await this.gsmCall(phoneNumber, action); } @@ -80,11 +83,10 @@ export async function gsmSignal(signalStrengh) { /** * @this {import('../../driver').AndroidDriver} - * @param {import('../types').GsmSignalStrengthOpts} opts + * @param {import('../types').GsmSignalStrength} strength The signal strength value * @returns {Promise} */ -export async function mobileGsmSignal(opts) { - const {strength} = requireArgs('strength', opts); +export async function mobileGsmSignal(strength) { await this.gsmSignal(strength); } @@ -101,11 +103,10 @@ export async function gsmVoice(state) { /** * @this {import('../../driver').AndroidDriver} - * @param {import('../types').GsmVoiceOpts} opts + * @param {import('../types').GsmVoiceState} state * @returns {Promise} */ -export async function mobileGsmVoice(opts) { - const {state} = requireArgs('state', opts); +export async function mobileGsmVoice(state) { await this.gsmVoice(state); } @@ -122,11 +123,10 @@ export async function powerAC(state) { /** * @this {import('../../driver').AndroidDriver} - * @param {import('../types').PowerACOpts} opts + * @param {import('../types').PowerACState} state * @returns {Promise} */ -export async function mobilePowerAc(opts) { - const {state} = requireArgs('state', opts); +export async function mobilePowerAc(state) { await this.powerAC(state); } @@ -143,11 +143,10 @@ export async function powerCapacity(batteryPercent) { /** * @this {import('../../driver').AndroidDriver} - * @param {import('../types').PowerCapacityOpts} opts + * @param {number} percent Percentage value in range `[0, 100]` * @return {Promise} */ -export async function mobilePowerCapacity(opts) { - const {percent} = requireArgs('percent', opts); +export async function mobilePowerCapacity(percent) { await this.powerCapacity(percent); } @@ -164,27 +163,26 @@ export async function networkSpeed(networkSpeed) { /** * @this {import('../../driver').AndroidDriver} - * @param {import('../types').NetworkSpeedOpts} opts + * @param {import('../types').NetworkSpeed} speed * @returns {Promise} */ -export async function mobileNetworkSpeed(opts) { - const {speed} = requireArgs('speed', opts); +export async function mobileNetworkSpeed(speed) { await this.networkSpeed(speed); } /** * @this {import('../../driver').AndroidDriver} - * @param {import('../types').SensorSetOpts} opts + * @param {string} sensorType Sensor type as declared in `adb.SENSORS` + * @param {string} value Value to set to the sensor * @returns {Promise} */ -export async function sensorSet(opts) { +export async function sensorSet(sensorType, value) { requireEmulator.bind(this)('sensorSet is only available for emulators'); - const {sensorType, value} = opts; if (!util.hasValue(sensorType)) { - throw this.log.errorWithException(`'sensorType' argument is required`); + throw new errors.InvalidArgumentError(`'sensorType' argument is required`); } if (!util.hasValue(value)) { - throw this.log.errorWithException(`'value' argument is required`); + throw new errors.InvalidArgumentError(`'value' argument is required`); } await this.adb.sensorSet(sensorType, /** @type {any} */ (value)); } diff --git a/lib/commands/device/emulator-console.js b/lib/commands/device/emulator-console.js index 33bc14ce..558963b9 100644 --- a/lib/commands/device/emulator-console.js +++ b/lib/commands/device/emulator-console.js @@ -4,14 +4,19 @@ const EMU_CONSOLE_FEATURE = 'emulator_console'; /** * @this {import('../../driver').AndroidDriver} - * @param {import('../types').ExecOptions} opts * @returns {Promise} + * @param {string | string[]} command The actual command to execute. + * @see {@link https://developer.android.com/studio/run/emulator-console} + * @param {number} [execTimeout] A timeout used to wait for a server reply to the given command in + * milliseconds. 60000ms by default + * @param {number} [connTimeout] Console connection timeout in milliseconds. + * 5000ms by default. + * @param {number} [initTimeout] Telnet console initialization timeout in milliseconds (the time between the + * connection happens and the command prompt is available) */ -export async function mobileExecEmuConsoleCommand(opts) { +export async function mobileExecEmuConsoleCommand(command, execTimeout, connTimeout, initTimeout) { this.assertFeatureEnabled(EMU_CONSOLE_FEATURE); - const {command, execTimeout, connTimeout, initTimeout} = opts; - if (!command) { throw new errors.InvalidArgumentError(`The 'command' argument is mandatory`); } diff --git a/lib/commands/deviceidle.js b/lib/commands/deviceidle.js index e0cffabb..2237318e 100644 --- a/lib/commands/deviceidle.js +++ b/lib/commands/deviceidle.js @@ -8,12 +8,11 @@ const SUPPORTED_ACTIONS = ['whitelistAdd', 'whitelistRemove']; * Read https://www.protechtraining.com/blog/post/diving-into-android-m-doze-875 * for more details. * - * @param {import('./types').DeviceidleOpts} opts + * @param {'whitelistAdd' | 'whitelistRemove'} action The action name to execute + * @param {string} [packages] Either a single package or multiple packages to add or remove from the idle whitelist * @returns {Promise} */ -export async function mobileDeviceidle(opts) { - const {action, packages} = opts; - +export async function mobileDeviceidle(action, packages) { if (!(_.isString(packages) || _.isArray(packages))) { throw new errors.InvalidArgumentError(`packages argument must be a string or an array`); } diff --git a/lib/commands/execute.js b/lib/commands/execute.js index fec89fc7..28666fbc 100644 --- a/lib/commands/execute.js +++ b/lib/commands/execute.js @@ -1,125 +1,20 @@ import _ from 'lodash'; import {errors, PROTOCOLS} from 'appium/driver'; +import { util } from '@appium/support'; -/** - * @this {import('../driver').AndroidDriver} - * @returns {import('@appium/types').StringRecord} - */ -export function mobileCommandsMapping() { - return { - shell: 'mobileShell', - - execEmuConsoleCommand: 'mobileExecEmuConsoleCommand', - - startLogsBroadcast: 'mobileStartLogsBroadcast', - stopLogsBroadcast: 'mobileStopLogsBroadcast', - - changePermissions: 'mobileChangePermissions', - getPermissions: 'mobileGetPermissions', - - performEditorAction: 'mobilePerformEditorAction', - - getDeviceTime: 'mobileGetDeviceTime', - - startScreenStreaming: 'mobileStartScreenStreaming', - stopScreenStreaming: 'mobileStopScreenStreaming', - - getNotifications: 'mobileGetNotifications', - - listSms: 'mobileListSms', - - pushFile: 'mobilePushFile', - pullFile: 'mobilePullFile', - pullFolder: 'mobilePullFolder', - deleteFile: 'mobileDeleteFile', - - isAppInstalled: 'mobileIsAppInstalled', - queryAppState: 'mobileQueryAppState', - activateApp: 'mobileActivateApp', - removeApp: 'mobileRemoveApp', - terminateApp: 'mobileTerminateApp', - installApp: 'mobileInstallApp', - clearApp: 'mobileClearApp', - - startService: 'mobileStartService', - stopService: 'mobileStopService', - startActivity: 'mobileStartActivity', - broadcast: 'mobileBroadcast', - - getContexts: 'mobileGetContexts', - - lock: 'mobileLock', - unlock: 'mobileUnlock', - isLocked: 'isLocked', - - refreshGpsCache: 'mobileRefreshGpsCache', - - startMediaProjectionRecording: 'mobileStartMediaProjectionRecording', - isMediaProjectionRecordingRunning: 'mobileIsMediaProjectionRecordingRunning', - stopMediaProjectionRecording: 'mobileStopMediaProjectionRecording', - - getConnectivity: 'mobileGetConnectivity', - setConnectivity: 'mobileSetConnectivity', - - hideKeyboard: 'hideKeyboard', - isKeyboardShown: 'isKeyboardShown', - - deviceidle: 'mobileDeviceidle', - - bluetooth: 'mobileBluetooth', - - nfc: 'mobileNfc', - - setUiMode: 'mobileSetUiMode', - getUiMode: 'mobileGetUiMode', - - injectEmulatorCameraImage: 'mobileInjectEmulatorCameraImage', - - sendTrimMemory: 'mobileSendTrimMemory', - - getPerformanceData: 'mobileGetPerformanceData', - getPerformanceDataTypes: 'getPerformanceDataTypes', - - toggleGps: 'toggleLocationServices', - isGpsEnabled: 'isLocationServicesEnabled', - - getDisplayDensity: 'getDisplayDensity', - getSystemBars: 'getSystemBars', - statusBar: 'mobilePerformStatusBarCommand', - - fingerprint: 'mobileFingerprint', - sendSms: 'mobileSendSms', - gsmCall: 'mobileGsmCall', - gsmSignal: 'mobileGsmSignal', - gsmVoice: 'mobileGsmVoice', - powerAc: 'mobilePowerAc', - powerCapacity: 'mobilePowerCapacity', - networkSpeed: 'mobileNetworkSpeed', - sensorSet: 'sensorSet', - - getCurrentActivity: 'getCurrentActivity', - getCurrentPackage: 'getCurrentPackage', - - setGeolocation: 'mobileSetGeolocation', - getGeolocation: 'mobileGetGeolocation', - resetGeolocation: 'mobileResetGeolocation', - }; -} +const EXECUTE_SCRIPT_PREFIX = 'mobile:'; /** - * @this {import('../driver').AndroidDriver} + * @this {AndroidDriver} * @param {string} script - * @param {import('@appium/types').StringRecord[]|import('@appium/types').StringRecord} [args] + * @param {ExecuteMethodArgs} [args] * @returns {Promise} */ export async function execute(script, args) { - if (script.match(/^mobile:/)) { - this.log.info(`Executing native command '${script}'`); - script = script.replace(/^mobile:/, '').trim(); - return await this.executeMobile( - script, - Array.isArray(args) ? (args[0]) : args, - ); + if (_.startsWith(script, EXECUTE_SCRIPT_PREFIX)) { + const formattedScript = script.trim().replace(/^mobile:\s*/, `${EXECUTE_SCRIPT_PREFIX} `); + const executeMethodArgs = preprocessExecuteMethodArgs(args); + return await this.executeMethod(formattedScript, [executeMethodArgs]); } if (!this.isWebContext()) { throw new errors.NotImplementedError(); @@ -137,19 +32,42 @@ export async function execute(script, args) { }); } +// #region Internal Helpers + /** - * @this {import('../driver').AndroidDriver} - * @param {string} mobileCommand - * @param {import('@appium/types').StringRecord} [opts={}] - * @returns {Promise} + * Massages the arguments going into an execute method. + * + * @param {ExecuteMethodArgs} [args] + * @returns {StringRecord} */ -export async function executeMobile(mobileCommand, opts = {}) { - const mobileCommandsMapping = this.mobileCommandsMapping(); - if (!(mobileCommand in mobileCommandsMapping)) { - throw new errors.UnknownCommandError( - `Unknown mobile command "${mobileCommand}". ` + - `Only ${_.keys(mobileCommandsMapping)} commands are supported.`, +function preprocessExecuteMethodArgs(args) { + const executeMethodArgs = /** @type {StringRecord} */ ((_.isArray(args) ? _.first(args) : 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; + } + + /** + * Automatically unwraps the `elementId` prop _if and only if_ the execute method expects it. + */ + if ('elementId' in executeMethodArgs) { + executeMethodArgs.elementId = util.unwrapElement( + /** @type {import('@appium/types').Element|string} */ (executeMethodArgs.elementId), ); } - return await this[mobileCommandsMapping[mobileCommand]](opts); + + return executeMethodArgs; } + +// #endregion + +/** + * @typedef {import('../driver').AndroidDriver} AndroidDriver + * @typedef {import('@appium/types').StringRecord} StringRecord + * @typedef {readonly any[] | readonly [StringRecord] | Readonly} ExecuteMethodArgs + */ \ No newline at end of file diff --git a/lib/commands/file-actions.js b/lib/commands/file-actions.js index aaa6ba7c..42b61004 100644 --- a/lib/commands/file-actions.js +++ b/lib/commands/file-actions.js @@ -2,7 +2,6 @@ import _ from 'lodash'; import {fs, util, zip, tempDir} from '@appium/support'; import path from 'path'; import {errors} from 'appium/driver'; -import {requireArgs} from '../utils'; const CONTAINER_PATH_MARKER = '@'; // https://regex101.com/r/PLdB0G/2 @@ -11,7 +10,10 @@ const ANDROID_MEDIA_RESCAN_INTENT = 'android.intent.action.MEDIA_SCANNER_SCAN_FI /** * @this {import('../driver').AndroidDriver} - * @param {string} remotePath + * @param {string} remotePath The full path to the remote file or a specially formatted path, which + * points to an item inside an app bundle, for example `@my.app.id/my/path`. + * It is mandatory for the app bundle to have debugging enabled in order to + * use the latter `remotePath` format. * @returns {Promise} */ export async function pullFile(remotePath) { @@ -59,18 +61,11 @@ export async function pullFile(remotePath) { /** * @this {import('../driver').AndroidDriver} - * @param {import('./types').PullFileOpts} opts - * @returns {Promise} - */ -export async function mobilePullFile(opts) { - const {remotePath} = requireArgs('remotePath', opts); - return await this.pullFile(remotePath); -} - -/** - * @this {import('../driver').AndroidDriver} - * @param {string} remotePath - * @param {string} base64Data + * @param {string} remotePath The full path to the remote file or a specially formatted path, which + * points to an item inside an app bundle, for example `@my.app.id/my/path`. + * It is mandatory for the app bundle to have debugging enabled in order to + * use the latter `remotePath` format. + * @param {string} base64Data Base64-encoded content of the file to be pushed. * @returns {Promise} */ export async function pushFile(remotePath, base64Data) { @@ -138,17 +133,7 @@ export async function pushFile(remotePath, base64Data) { /** * @this {import('../driver').AndroidDriver} - * @param {import('./types').PushFileOpts} opts - * @returns {Promise} - */ -export async function mobilePushFile(opts) { - const {remotePath, payload} = requireArgs(['remotePath', 'payload'], opts); - return await this.pushFile(remotePath, payload); -} - -/** - * @this {import('../driver').AndroidDriver} - * @param {string} remotePath + * @param {string} remotePath The full path to the remote folder * @returns {Promise} */ export async function pullFolder(remotePath) { @@ -167,21 +152,11 @@ export async function pullFolder(remotePath) { /** * @this {import('../driver').AndroidDriver} - * @param {import('./types').PullFolderOpts} opts - * @returns {Promise} - */ -export async function mobilePullFolder(opts) { - const {remotePath} = requireArgs('remotePath', opts); - return await this.pullFolder(remotePath); -} - -/** - * @this {import('../driver').AndroidDriver} - * @param {import('./types').DeleteFileOpts} opts + * @param {string} remotePath The full path to the remote file or a file inside an application bundle + * (for example `@my.app.id/path/in/bundle`) * @returns {Promise} */ -export async function mobileDeleteFile(opts) { - const {remotePath} = requireArgs('remotePath', opts); +export async function mobileDeleteFile(remotePath) { if (remotePath.endsWith('/')) { throw new errors.InvalidArgumentError( `It is expected that remote path points to a folder and not to a file. ` + diff --git a/lib/commands/geolocation.js b/lib/commands/geolocation.js index aab0c71a..60e06321 100644 --- a/lib/commands/geolocation.js +++ b/lib/commands/geolocation.js @@ -37,10 +37,16 @@ export async function setGeoLocation(location) { /** * @this {import('../driver').AndroidDriver} - * @param {import('@appium/types').Location} opts + * @param {number} latitude + * @param {number} longitude + * @param {number} [altitude] */ -export async function mobileSetGeolocation(opts) { - await this.settingsApp.setGeoLocation(opts, this.isEmulator()); +export async function mobileSetGeolocation(latitude, longitude, altitude) { + await this.settingsApp.setGeoLocation({ + latitude, + longitude, + altitude, + }, this.isEmulator()); } /** @@ -51,11 +57,13 @@ export async function mobileSetGeolocation(opts) { * must be at version 30 (Android R) or higher. * * @this {import('../driver').AndroidDriver} - * @param {import('./types').GpsCacheRefreshOpts} [opts={}] + * @param {number} [timeoutMs] The maximum number of milliseconds + * to block until GPS cache is refreshed. Providing zero or a negative + * value to it skips waiting completely. + * 20000ms by default. * @returns {Promise} */ -export async function mobileRefreshGpsCache(opts = {}) { - const {timeoutMs} = opts; +export async function mobileRefreshGpsCache(timeoutMs) { await this.settingsApp.refreshGeoLocationCache(timeoutMs); } diff --git a/lib/commands/image-injection.js b/lib/commands/image-injection.js index 9caca854..d8580255 100644 --- a/lib/commands/image-injection.js +++ b/lib/commands/image-injection.js @@ -75,15 +75,14 @@ export async function prepareEmulatorForImageInjection(sdkRoot) { * `injectedImageProperties` capability. * * @this {AndroidDriver} - * @param {import('./types').ImageInjectionOpts} opts + * @param {string} payload Base64-encoded payload of a .png image to be injected * @returns {Promise} */ -export async function mobileInjectEmulatorCameraImage(opts) { +export async function mobileInjectEmulatorCameraImage(payload) { if (!this.isEmulator()) { throw new Error('The image injection feature is only available on emulators'); } - const {payload} = opts; if (!_.isString(payload) || _.size(payload) <= PNG_MAGIC_LENGTH) { throw new errors.InvalidArgumentError( `You must provide a valid base64-encoded .PNG data as the 'payload' argument` diff --git a/lib/commands/intent.js b/lib/commands/intent.js index 163d93f6..73d506b3 100644 --- a/lib/commands/intent.js +++ b/lib/commands/intent.js @@ -75,11 +75,52 @@ export async function startActivity( /** * @this {import('../driver').AndroidDriver} - * @param {import('./types').StartActivityOpts} [opts={}] + * @param {boolean} [wait] Set it to `true` if you want to block the method call + * until the activity manager's process returns the control to the system. + * false by default. + * @param {boolean} [stop] Set it to `true` to force stop the target + * app before starting the activity + * false by default. + * @param {string | number} [windowingMode] The windowing mode to launch the activity into. + * Check + * https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/WindowConfiguration.java + * for more details on possible windowing modes (constants starting with + * `WINDOWING_MODE_`). + * @param {string | number} [activityType] The activity type to launch the activity as. + * Check https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/WindowConfiguration.java + * for more details on possible activity types (constants starting with `ACTIVITY_TYPE_`). + * @param {number | string} [display] The display identifier to launch the activity into. + * @param {string} [user] + * @param {string} [intent] + * @param {string} [action] + * @param {string} [pkg] + * @param {string} [uri] + * @param {string} [mimeType] + * @param {string} [identifier] + * @param {string} [component] + * @param {string | string[]} [categories] + * @param {string[][]} [extras] + * @param {string} [flags] * @returns {Promise} */ -export async function mobileStartActivity(opts = {}) { - const {user, wait, stop, windowingMode, activityType, display} = opts; +export async function mobileStartActivity( + wait, + stop, + windowingMode, + activityType, + display, + user, + intent, + action, + pkg, + uri, + mimeType, + identifier, + component, + categories, + extras, + flags, +) { const cmd = [ 'am', (await this.adb.getApiLevel()) < API_LEVEL_ANDROID_8 ? 'start' : 'start-activity', @@ -102,17 +143,55 @@ export async function mobileStartActivity(opts = {}) { if (!_.isNil(display)) { cmd.push('--display', String(display)); } - cmd.push(...parseIntentSpec(opts)); + cmd.push(...parseIntentSpec({ + intent, + action, + package: pkg, + uri, + mimeType, + identifier, + component, + categories, + extras, + flags, + })); return await this.adb.shell(cmd); } /** * @this {import('../driver').AndroidDriver} - * @param {import('./types').BroadcastOpts} [opts={}] + * @param {string | number} [user] The user ID for which the broadcast is sent. + * The `current` alias assumes the current user ID. + * `all` by default. + * @param {string} [receiverPermission] Require receiver to hold the given permission. + * @param {boolean} [allowBackgroundActivityStarts] Whether the receiver may start activities even if in the background. + * @param {string} [intent] + * @param {string} [action] + * @param {string} [pkg] + * @param {string} [uri] + * @param {string} [mimeType] + * @param {string} [identifier] + * @param {string} [component] + * @param {string | string[]} [categories] + * @param {string[][]} [extras] + * @param {string} [flags] * @returns {Promise} */ -export async function mobileBroadcast(opts = {}) { - const {user, receiverPermission, allowBackgroundActivityStarts} = opts; +export async function mobileBroadcast( + receiverPermission, + allowBackgroundActivityStarts, + user, + intent, + action, + pkg, + uri, + mimeType, + identifier, + component, + categories, + extras, + flags, +) { const cmd = ['am', 'broadcast']; if (!_.isNil(user)) { cmd.push('--user', String(user)); @@ -123,17 +202,53 @@ export async function mobileBroadcast(opts = {}) { if (allowBackgroundActivityStarts) { cmd.push('--allow-background-activity-starts'); } - cmd.push(...parseIntentSpec(opts)); + cmd.push(...parseIntentSpec({ + intent, + action, + package: pkg, + uri, + mimeType, + identifier, + component, + categories, + extras, + flags, + })); return await this.adb.shell(cmd); } /** * @this {import('../driver').AndroidDriver} - * @param {import('./types').StartServiceOpts} [opts={}] + * @param {boolean} [foreground] Set it to `true` if your service must be started as foreground service. + * This option is ignored if the API level of the device under test is below + * 26 (Android 8). + * @param {string} [user] + * @param {string} [intent] + * @param {string} [action] + * @param {string} [pkg] + * @param {string} [uri] + * @param {string} [mimeType] + * @param {string} [identifier] + * @param {string} [component] + * @param {string | string[]} [categories] + * @param {string[][]} [extras] + * @param {string} [flags] * @returns {Promise} */ -export async function mobileStartService(opts = {}) { - const {user, foreground} = opts; +export async function mobileStartService( + foreground, + user, + intent, + action, + pkg, + uri, + mimeType, + identifier, + component, + categories, + extras, + flags, +) { const cmd = ['am']; if ((await this.adb.getApiLevel()) < API_LEVEL_ANDROID_8) { cmd.push('startservice'); @@ -143,17 +258,49 @@ export async function mobileStartService(opts = {}) { if (!_.isNil(user)) { cmd.push('--user', String(user)); } - cmd.push(...parseIntentSpec(opts)); + cmd.push(...parseIntentSpec({ + intent, + action, + package: pkg, + uri, + mimeType, + identifier, + component, + categories, + extras, + flags, + })); return await this.adb.shell(cmd); } /** * @this {import('../driver').AndroidDriver} - * @param {import('./types').StopServiceOpts} [opts={}] + * @param {string} [user] + * @param {string} [intent] + * @param {string} [action] + * @param {string} [pkg] + * @param {string} [uri] + * @param {string} [mimeType] + * @param {string} [identifier] + * @param {string} [component] + * @param {string | string[]} [categories] + * @param {string[][]} [extras] + * @param {string} [flags] * @returns {Promise} */ -export async function mobileStopService(opts = {}) { - const {user} = opts; +export async function mobileStopService( + user, + intent, + action, + pkg, + uri, + mimeType, + identifier, + component, + categories, + extras, + flags, +) { const cmd = [ 'am', (await this.adb.getApiLevel()) < API_LEVEL_ANDROID_8 ? 'stopservice' : 'stop-service', @@ -161,7 +308,18 @@ export async function mobileStopService(opts = {}) { if (!_.isNil(user)) { cmd.push('--user', String(user)); } - cmd.push(...parseIntentSpec(opts)); + cmd.push(...parseIntentSpec({ + intent, + action, + package: pkg, + uri, + mimeType, + identifier, + component, + categories, + extras, + flags, + })); try { return await this.adb.shell(cmd); } catch (e) { diff --git a/lib/commands/keyboard.js b/lib/commands/keyboard.js index fe291b54..bf580212 100644 --- a/lib/commands/keyboard.js +++ b/lib/commands/keyboard.js @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import _ from 'lodash'; import {errors} from 'appium/driver'; -import {requireArgs} from '../utils'; import {UNICODE_IME, EMPTY_IME} from 'io.appium.settings'; /** @@ -80,11 +79,10 @@ export async function longPressKeyCode(keycode, metastate) { /** * @this {import('../driver').AndroidDriver} - * @param {import('./types').PerformEditorActionOpts} opts + * @param {string | number} action * @returns {Promise} */ -export async function mobilePerformEditorAction(opts) { - const {action} = requireArgs('action', opts); +export async function mobilePerformEditorAction(action) { await this.settingsApp.performEditorAction(action); } diff --git a/lib/commands/lock/exports.js b/lib/commands/lock/exports.js index 6c683b4c..b4ae62cf 100644 --- a/lib/commands/lock/exports.js +++ b/lib/commands/lock/exports.js @@ -17,16 +17,6 @@ import { } from './helpers'; import _ from 'lodash'; -/** - * @this {AndroidDriver} - * @param {import('../types').LockOpts} opts - * @returns {Promise} - */ -export async function mobileLock(opts = {}) { - const {seconds} = opts; - return await this.lock(seconds); -} - /** * @this {AndroidDriver} * @param {number} [seconds] @@ -64,11 +54,20 @@ export async function unlock() { /** * @this {AndroidDriver} - * @param {import('../types').UnlockOptions} [opts={}] + * @param {string} [key] The unlock key. The value of this key depends on the actual unlock type and + * could be a pin/password/pattern value or a biometric finger id. + * If not provided then the corresponding value from session capabilities is + * used. + * @param {import('../types').UnlockType} [type] The unlock type. + * If not provided then the corresponding value from session capabilities is used. + * @param {string} [strategy] Setting it to 'uiautomator' will enforce the driver to avoid using special + * ADB shortcuts in order to speed up the unlock procedure. + * 'uiautomator' by default. + * @param {number} [timeoutMs] The maximum time in milliseconds to wait until the screen gets unlocked + * 2000ms byde fault. * @returns {Promise} */ -export async function mobileUnlock(opts = {}) { - const {key, type, strategy, timeoutMs} = opts; +export async function mobileUnlock(key, type, strategy, timeoutMs) { if (!key && !type) { await this.unlock(); } else { diff --git a/lib/commands/media-projection.js b/lib/commands/media-projection.js index 030b6bb0..0c310280 100644 --- a/lib/commands/media-projection.js +++ b/lib/commands/media-projection.js @@ -10,13 +10,25 @@ const DEFAULT_FILENAME_FORMAT = 'YYYY-MM-DDTHH-mm-ss'; /** * @this {import('../driver').AndroidDriver} - * @param {import('./types').StartMediaProjectionRecordingOpts} [options={}] + * @param {string} [resolution] Maximum supported resolution on-device (Detected automatically by the app + * itself), which usually equals to Full HD 1920x1080 on most phones however + * you can change it to following supported resolutions as well: "1920x1080", + * "1280x720", "720x480", "320x240", "176x144". + * @param {'high' | 'normal' | 'low'} [priority] Recording thread priority. + * If you face performance drops during testing with recording enabled, you + * can reduce recording priority + * 'high' by default + * @param {number} [maxDurationSec] Maximum allowed duration is 15 minutes; you can increase it if your test + * takes longer than that. 900s by default. + * @param {string} [filename] You can type recording video file name as you want, but recording currently + * supports only "mp4" format so your filename must end with ".mp4". An + * invalid file name will fail to start the recording. If not provided then + * the current timestamp will be used as file name. * @returns {Promise} */ -export async function mobileStartMediaProjectionRecording(options = {}) { +export async function mobileStartMediaProjectionRecording(resolution, priority, maxDurationSec, filename) { await verifyMediaProjectionRecordingIsSupported(this.adb); - const {resolution, priority, maxDurationSec, filename} = options; const recorder = this.settingsApp.makeMediaProjectionRecorder(); const fname = adjustMediaExtension(filename || moment().format(DEFAULT_FILENAME_FORMAT)); const didStart = await recorder.start({ @@ -48,10 +60,34 @@ export async function mobileIsMediaProjectionRecordingRunning() { /** * @this {import('../driver').AndroidDriver} - * @param {import('./types').StopMediaProjectionRecordingOpts} [options={}] + * @param {string} [remotePath] The path to the remote location, where the resulting video should be + * uploaded. The following protocols are supported: http/https, ftp. Null or + * empty string value (the default setting) means the content of resulting + * file should be encoded as Base64 and passed as the endpoont response value. + * An exception will be thrown if the generated media file is too big to fit + * into the available process memory. + * @param {string} [user] The name of the user for the remote authentication. + * @param {string} [pass] The password for the remote authentication. + * @param {import('@appium/types').HTTPMethod} [method] The http multipart upload method name. + * 'PUT' by default. + * @param {import('@appium/types').StringRecord} [headers] Additional headers mapping for multipart http(s) uploads + * @param {string} [fileFieldName] The name of the form field, where the file content BLOB should be stored + * for http(s) uploads. 'file' by default. + * @param {import('./types').FormFields} [formFields] Additional form fields for multipart http(s) uploads + * @param {number} [uploadTimeout] The actual media upload request timeout in milliseconds. + * Defaults to `@appium/support.net.DEFAULT_TIMEOUT_MS` * @returns {Promise} */ -export async function mobileStopMediaProjectionRecording(options = {}) { +export async function mobileStopMediaProjectionRecording( + remotePath, + user, + pass, + method, + headers, + fileFieldName, + formFields, + uploadTimeout, +) { await verifyMediaProjectionRecordingIsSupported(this.adb); const recorder = this.settingsApp.makeMediaProjectionRecorder(); @@ -65,7 +101,6 @@ export async function mobileStopMediaProjectionRecording(options = {}) { throw new Error(`No recent media projection recording have been found. Did you start any?`); } - const {remotePath} = options; if (_.isEmpty(remotePath)) { const {size} = await fs.stat(recentRecordingPath); this.log.debug( @@ -73,7 +108,15 @@ export async function mobileStopMediaProjectionRecording(options = {}) { ); } try { - return await uploadRecordedMedia(recentRecordingPath, remotePath, options); + return await uploadRecordedMedia(recentRecordingPath, remotePath, { + user, + pass, + method, + headers, + fileFieldName, + formFields, + uploadTimeout, + }); } finally { await fs.rimraf(path.dirname(recentRecordingPath)); } @@ -85,7 +128,7 @@ export async function mobileStopMediaProjectionRecording(options = {}) { * * @param {string} localFile * @param {string} [remotePath] - * @param {import('./types').StopMediaProjectionRecordingOpts} uploadOptions + * @param {UploadOptions} uploadOptions * @returns */ async function uploadRecordedMedia(localFile, remotePath, uploadOptions = {}) { @@ -102,9 +145,6 @@ async function uploadRecordedMedia(localFile, remotePath, uploadOptions = {}) { formFields, uploadTimeout: timeout, } = uploadOptions; - /** - * @type {Omit & {auth?: {user: string, pass: string}, timeout?: number}} - */ const options = { method: method || 'PUT', headers, @@ -144,6 +184,17 @@ async function verifyMediaProjectionRecordingIsSupported(adb) { // #endregion +/** + * @typedef {Object} UploadOptions + * @property {string} [user] + * @property {string} [pass] + * @property {import('@appium/types').HTTPMethod} [method] + * @property {import('@appium/types').StringRecord} [headers] + * @property {string} [fileFieldName] + * @property {import('./types').FormFields} [formFields] + * @property {number} [uploadTimeout] + */ + /** * @typedef {import('appium-adb').ADB} ADB */ diff --git a/lib/commands/memory.js b/lib/commands/memory.js index 3cad34b8..3cc9b185 100644 --- a/lib/commands/memory.js +++ b/lib/commands/memory.js @@ -6,12 +6,12 @@ import {errors} from 'appium/driver'; * for more details. * * @this {import('../driver').AndroidDriver} - * @param {import('./types').SendTrimMemoryOpts} opts + * @param {string} pkg The package name to send the `trimMemory` event to + * @param {'COMPLETE' | 'MODERATE' | 'BACKGROUND' | 'UI_HIDDEN' | 'RUNNING_CRITICAL' | 'RUNNING_LOW' | 'RUNNING_MODERATE'} level The + * actual memory trim level to be sent * @returns {Promise} */ -export async function mobileSendTrimMemory(opts) { - const {pkg, level} = opts; - +export async function mobileSendTrimMemory(pkg, level) { if (!pkg) { throw new errors.InvalidArgumentError(`The 'pkg' argument must be provided`); } diff --git a/lib/commands/network.js b/lib/commands/network.js index bb6c766b..c2a2852b 100644 --- a/lib/commands/network.js +++ b/lib/commands/network.js @@ -9,7 +9,7 @@ const DATA_MASK = 0b100; const WIFI_KEY_NAME = 'wifi'; const DATA_KEY_NAME = 'data'; const AIRPLANE_MODE_KEY_NAME = 'airplaneMode'; -const SUPPORTED_SERVICE_NAMES = /** @type {const} */ ([ +const SUPPORTED_SERVICE_NAMES = /** @type {import('./types').ServiceType[]} */ ([ WIFI_KEY_NAME, DATA_KEY_NAME, AIRPLANE_MODE_KEY_NAME, @@ -46,24 +46,28 @@ export async function isWifiOn() { /** * @since Android 12 (only real devices, emulators work in all APIs) * @this {import('../driver').AndroidDriver} - * @param {import('./types').SetConnectivityOpts} [opts={}] + * @param {boolean} [wifi] Either to enable or disable Wi-Fi. + * An unset value means to not change the state for the given service. + * @param {boolean} [data] Either to enable or disable mobile data connection. + * An unset value means to not change the state for the given service. + * @param {boolean} [airplaneMode] Either to enable to disable the Airplane Mode + * An unset value means to not change the state for the given service. * @returns {Promise} */ -export async function mobileSetConnectivity(opts = {}) { - const {wifi, data, airplaneMode} = opts; +export async function mobileSetConnectivity(wifi, data, airplaneMode) { if (_.every([wifi, data, airplaneMode], _.isUndefined)) { throw new errors.InvalidArgumentError( `Either one of ${JSON.stringify(SUPPORTED_SERVICE_NAMES)} options must be provided`, ); } - const currentState = await this.mobileGetConnectivity({ - services: /** @type {import('./types').ServiceType[]} */ ([ + const currentState = await this.mobileGetConnectivity( + /** @type {import('./types').ServiceType[]} */ ([ ...(_.isUndefined(wifi) ? [] : [WIFI_KEY_NAME]), ...(_.isUndefined(data) ? [] : [DATA_KEY_NAME]), ...(_.isUndefined(airplaneMode) ? [] : [AIRPLANE_MODE_KEY_NAME]), ]), - }); + ); /** @type {(Promise|(() => Promise))[]} */ const setters = []; if (!_.isUndefined(wifi) && currentState.wifi !== Boolean(wifi)) { @@ -87,11 +91,11 @@ export async function mobileSetConnectivity(opts = {}) { /** * @this {import('../driver').AndroidDriver} - * @param {import('./types').GetConnectivityOpts} [opts={}] + * @param {import('./types').ServiceType[] | import('./types').ServiceType} [services] one or more + * services to get the connectivity for. * @returns {Promise} */ -export async function mobileGetConnectivity(opts = {}) { - let {services = SUPPORTED_SERVICE_NAMES} = opts; +export async function mobileGetConnectivity(services = SUPPORTED_SERVICE_NAMES) { const svcs = _.castArray(services); const unsupportedServices = _.difference(services, SUPPORTED_SERVICE_NAMES); if (!_.isEmpty(unsupportedServices)) { diff --git a/lib/commands/nfc.js b/lib/commands/nfc.js index 6c900d1a..55ddc666 100644 --- a/lib/commands/nfc.js +++ b/lib/commands/nfc.js @@ -10,13 +10,12 @@ const SUPPORTED_ACTIONS = /** @type {const} */ ({ * Performs the requested action on the default NFC adapter * * @this {AndroidDriver} - * @param {import('./types').NfcOptions} opts + * @param { 'enable' | 'disable'} action * @returns {Promise} * @throws {Error} if the device under test has no default NFC adapter * or there was a failure while performing the action. */ -export async function mobileNfc(opts) { - const {action} = opts; +export async function mobileNfc(action) { switch (action) { case SUPPORTED_ACTIONS.ENABLE: await this.adb.setNfcOn(true); diff --git a/lib/commands/performance.js b/lib/commands/performance.js index f438eae4..49ef7a9d 100644 --- a/lib/commands/performance.js +++ b/lib/commands/performance.js @@ -1,7 +1,6 @@ // @ts-check import {retryInterval} from 'asyncbox'; import _ from 'lodash'; -import {requireArgs} from '../utils'; export const NETWORK_KEYS = [ [ @@ -121,11 +120,11 @@ export async function getPerformanceData(packageName, dataType, retries = 2) { * - cpuinfo: [[user, kernel], [0.9, 1.3]] * * @this {AndroidDriver} - * @param {import('./types').PerformanceDataOpts} opts + * @param {string} packageName The name of the package identifier to fetch the data for + * @param {import('./types').PerformanceDataType} dataType One of supported subsystem to fetch the data for. * @returns {Promise} */ -export async function mobileGetPerformanceData(opts) { - const {packageName, dataType} = requireArgs(['packageName', 'dataType'], opts); +export async function mobileGetPerformanceData(packageName, dataType) { return await this.getPerformanceData(packageName, dataType); } diff --git a/lib/commands/permissions.js b/lib/commands/permissions.js index 4094f865..b689fd51 100644 --- a/lib/commands/permissions.js +++ b/lib/commands/permissions.js @@ -26,18 +26,35 @@ const PERMISSIONS_TYPE = Object.freeze({ /** * @this {import('../driver').AndroidDriver} - * @param {import('./types').ChangePermissionsOpts} opts - * @returns {Promise} + * @param {string | string[]} permissions If `target` is set to 'pm': + * The full name of the permission to be changed + * or a list of permissions. Check https://developer.android.com/reference/android/Manifest.permission + * to get the full list of standard Android permssion names. Mandatory argument. + * If 'all' magic string is passed then the chosen action is going to be applied to all + * permisisons requested/granted by 'appPackage'. + * If `target` is set to 'appops': + * The full name of the appops permission to be changed + * or a list of permissions. Check AppOpsManager.java sources to get the full list of + * available appops permission names. Mandatory argument. + * Examples: 'ACTIVITY_RECOGNITION', 'SMS_FINANCIAL_TRANSACTIONS', 'READ_SMS', 'ACCESS_NOTIFICATIONS'. + * The 'all' magic string is unsupported. + * @param {string} [appPackage] The application package to set change permissions on. Defaults to the + * package name under test + * @param {string} [action] One of `PM_ACTION` values if `target` is set to 'pm', otherwise + * one of `APPOPS_ACTION` values + * @param {'pm' | 'appops'} [target='pm'] Either 'pm' or 'appops'. The 'appops' one requires + * 'adb_shell' server security option to be enabled. */ -export async function mobileChangePermissions(opts) { - const { - permissions, - appPackage = this.opts.appPackage, - action = _.toLower(opts.target) === PERMISSION_TARGET.APPOPS - ? APPOPS_ACTION.ALLOW - : PM_ACTION.GRANT, - target = PERMISSION_TARGET.PM, - } = opts; +export async function mobileChangePermissions( + permissions, + appPackage, + action, + target = PERMISSION_TARGET.PM +) { + appPackage ??= this.opts.appPackage; + action ??= _.toLower(target) === PERMISSION_TARGET.APPOPS + ? APPOPS_ACTION.ALLOW + : PM_ACTION.GRANT; if (_.isNil(permissions)) { throw new errors.InvalidArgumentError(`'permissions' argument is required`); } @@ -64,11 +81,13 @@ export async function mobileChangePermissions(opts) { /** * @this {import('../driver').AndroidDriver} - * @param {import('./types').GetPermissionsOpts} [opts={}] + * @param {string} [type='requested'] One of possible permission types to get. + * @param {string} [appPackage] The application package to set change permissions on. + * Defaults to the package name under test * @returns {Promise} */ -export async function mobileGetPermissions(opts = {}) { - const {type = PERMISSIONS_TYPE.REQUESTED, appPackage = this.opts.appPackage} = opts; +export async function mobileGetPermissions(type = PERMISSIONS_TYPE.REQUESTED, appPackage) { + appPackage ??= this.opts.appPackage; /** * @type {(pkg: string) => Promise} */ diff --git a/lib/commands/shell.js b/lib/commands/shell.ts similarity index 73% rename from lib/commands/shell.js rename to lib/commands/shell.ts index 2c7ad58e..9579bd1b 100644 --- a/lib/commands/shell.js +++ b/lib/commands/shell.ts @@ -4,14 +4,13 @@ import _ from 'lodash'; import {exec} from 'teen_process'; import {ADB_SHELL_FEATURE} from '../utils'; -/** - * @this {import('../driver').AndroidDriver} - * @param {import('./types').ShellOpts} [opts={}] - * @returns {Promise}; - */ -export async function mobileShell(opts) { +export async function mobileShell( + command: string, + args: string[] = [], + timeout: number = 20000, + includeStderr?: T, +): Promise { this.assertFeatureEnabled(ADB_SHELL_FEATURE); - const {command, args = /** @type {string[]} */ ([]), timeout = 20000, includeStderr} = opts ?? {}; if (!_.isString(command)) { throw new errors.InvalidArgumentError(`The 'command' argument is mandatory`); @@ -22,11 +21,13 @@ export async function mobileShell(opts) { try { const {stdout, stderr} = await exec(this.adb.executable.path, adbArgs, {timeout}); if (includeStderr) { + // @ts-ignore We know what we are doing here return { stdout, stderr, }; } + // @ts-ignore We know what we are doing here return stdout; } catch (e) { const err = /** @type {import('teen_process').ExecError} */ (e); @@ -37,7 +38,3 @@ export async function mobileShell(opts) { ); } } - -/** - * @typedef {import('appium-adb').ADB} ADB - */ diff --git a/lib/commands/streamscreen.js b/lib/commands/streamscreen.js index b9539938..2f595ef2 100644 --- a/lib/commands/streamscreen.js +++ b/lib/commands/streamscreen.js @@ -35,24 +35,48 @@ const ADB_SCREEN_STREAMING_FEATURE = 'adb_screen_streaming'; /** * @this {import('../driver').AndroidDriver} - * @param {import('./types').StartScreenStreamingOpts} [options={}] + * @param {number} [width] The scaled width of the device's screen. + * If unset then the script will assign it to the actual screen width measured + * in pixels. + * @param {number} [height] The scaled height of the device's screen. + * If unset then the script will assign it to the actual screen height + * measured in pixels. + * @param {number} [bitRate=4000000] The video bit rate for the video, in bits per second. + * The default value is 4 Mb/s. You can increase the bit rate to improve video + * quality, but doing so results in larger movie files. + * @param {string} [host='127.0.0.1'] The IP address/host name to start the MJPEG server on. + * You can set it to `0.0.0.0` to trigger the broadcast on all available + * network interfaces. + * @param {number} [port=8093] The port number to start the MJPEG server on. + * @param {number} [tcpPort=8094] The port number to start the internal TCP MJPEG broadcast on. + * @param {string} [pathname] The HTTP request path the MJPEG server should be available on. + * If unset, then any pathname on the given `host`/`port` combination will + * work. Note that the value should always start with a single slash: `/` + * @param {number} [quality=70] The quality value for the streamed JPEG images. + * This number should be in range `[1,100]`, where `100` is the best quality. + * @param {boolean} [considerRotation=false] If set to `true` then GStreamer pipeline will increase the dimensions of + * the resulting images to properly fit images in both landscape and portrait + * orientations. + * Set it to `true` if the device rotation is not going to be the same during + * the broadcasting session. + * @param {boolean} [logPipelineDetails=false] Whether to log GStreamer pipeline events into the standard log output. + * Might be useful for debugging purposes. * @returns {Promise} - */ -export async function mobileStartScreenStreaming(options = {}) { +*/ +export async function mobileStartScreenStreaming( + width, + height, + bitRate, + host = DEFAULT_HOST, + port = DEFAULT_PORT, + pathname, + tcpPort = DEFAULT_PORT + 1, + quality = DEFAULT_QUALITY, + considerRotation = false, + logPipelineDetails = false, +) { this.assertFeatureEnabled(ADB_SCREEN_STREAMING_FEATURE); - const { - width, - height, - bitRate, - host = DEFAULT_HOST, - port = DEFAULT_PORT, - pathname, - tcpPort = DEFAULT_PORT + 1, - quality = DEFAULT_QUALITY, - considerRotation = false, - logPipelineDetails = false, - } = options; if (_.isUndefined(this._screenStreamingProps)) { await verifyStreamingRequirements(this.adb); } diff --git a/lib/commands/system-bars.js b/lib/commands/system-bars.js index ec6808d4..6c203b24 100644 --- a/lib/commands/system-bars.js +++ b/lib/commands/system-bars.js @@ -1,8 +1,5 @@ -// @ts-check - import {errors} from 'appium/driver'; import _ from 'lodash'; -import {requireArgs} from '../utils'; const WINDOW_TITLE_PATTERN = /^\s+Window\s#\d+\sWindow\{[0-9a-f]+\s\w+\s([\w-]+)\}:$/; const FRAME_PATTERN = /\bm?[Ff]rame=\[([0-9.-]+),([0-9.-]+)\]\[([0-9.-]+),([0-9.-]+)\]/; @@ -38,12 +35,14 @@ export async function getSystemBars() { /** * @this {import('../driver').AndroidDriver} - * @param {import('./types').StatusBarCommandOpts} opts + * @param {import('./types').StatusBarCommand} command Each list + * item must separated with a new line (`\n`) character. + * @param {string} [component] The name of the tile component. + * It is only required for `(add|remove|click)Tile` commands. + * Example value: `com.package.name/.service.QuickSettingsTileComponent` * @returns {Promise} */ -export async function mobilePerformStatusBarCommand(opts) { - const {command} = requireArgs('command', opts); - +export async function mobilePerformStatusBarCommand(command, component) { /** * * @param {string} cmd @@ -57,8 +56,7 @@ export async function mobilePerformStatusBarCommand(opts) { cmd, ...(argsCallable ? _.castArray(argsCallable()) : []), ]); - const tileCommandArgsCallable = () => - /** @type {string} */ (requireArgs('component', opts).component); + const tileCommandArgsCallable = () => /** @type {string} */ (component); const statusBarCommands = _.fromPairs( /** @type {const} */ ([ ['expandNotifications', ['expand-notifications']], diff --git a/lib/commands/time.js b/lib/commands/time.js index a4db94c1..e51ca8ae 100644 --- a/lib/commands/time.js +++ b/lib/commands/time.js @@ -24,11 +24,11 @@ export async function getDeviceTime(format = MOMENT_FORMAT_ISO8601) { /** * @this {AndroidDriver} - * @param {import('./types').DeviceTimeOpts} [opts={}] + * @param {string} [format='YYYY-MM-DDTHH:mm:ssZ'] * @returns {Promise} */ -export async function mobileGetDeviceTime(opts = {}) { - return await this.getDeviceTime(opts.format); +export async function mobileGetDeviceTime(format) { + return await this.getDeviceTime(format); } /** diff --git a/lib/commands/types.ts b/lib/commands/types.ts index 75685c8b..81587c86 100644 --- a/lib/commands/types.ts +++ b/lib/commands/types.ts @@ -1,109 +1,21 @@ import type {HTTPMethod, StringRecord} from '@appium/types'; -import type {InstallOptions, UninstallOptions} from 'appium-adb'; import type {AndroidDriverCaps} from '../driver'; -export interface SwipeOpts { - startX: number; - startY: number; - endX: number; - endY: number; - steps: number; - elementId?: string | number; -} - -export interface DragOpts { - elementId?: string | number; - destElId?: string | number; - startX: number; - startY: number; - endX: number; - endY: number; - steps: number; -} - /** * @privateRemarks probably better defined in `appium-adb` */ export type GsmAction = 'call' | 'accept' | 'cancel' | 'hold'; -export interface GsmCallOpts { - /** - * The phone number to call to - */ - phoneNumber: string; - /** - * Action to take - */ - action: GsmAction; -} - /** * One of possible signal strength values, where 4 is the best signal. * @privateRemarks maybe should be an enum? */ export type GsmSignalStrength = 0 | 1 | 2 | 3 | 4; -export interface GsmSignalStrengthOpts { - /** - * The signal strength value - */ - strength: GsmSignalStrength; -} - -export interface SendSMSOpts { - /** - * The phone number to send SMS to - */ - phoneNumber: string; - /** - * The message payload - */ - message: string; -} - -export interface FingerprintOpts { - /** - * The value is the `finger_id` for the finger that was "scanned". It is a - * unique integer that you assign for each virtual fingerprint. When the app - * is running you can run this same command each time the emulator prompts you - * for a fingerprint, you can run the adb command and pass it the `finger_id` - * to simulate the fingerprint scan. - */ - fingerprintId: string | number; -} - export type GsmVoiceState = 'on' | 'off'; export type PowerACState = 'on' | 'off'; -export interface GsmVoiceOpts { - state: GsmVoiceState; -} - -export interface PowerACOpts { - state: PowerACState; -} - -export interface PowerCapacityOpts { - /** - * Percentage value in range `[0, 100]` - */ - percent: number; -} - -export interface SensorSetOpts { - /** - * Sensor type as declared in `adb.SENSORS` - * @privateRemarks what is `adb.SENSORS`? - * - */ - sensorType: string; - /** - * Value to set to the sensor - */ - value: string; -} - export type NetworkSpeed = | 'gsm' | 'scsd' @@ -115,37 +27,6 @@ export type NetworkSpeed = | 'evdo' | 'full'; -export interface NetworkSpeedOpts { - speed: NetworkSpeed; -} - -export interface IsAppInstalledOpts { - /** - * Application package identifier - */ - appId: string; - - /** - * The user ID for which the package is installed. - * The `current` user id is used by default. - */ - user?: string | number; -} - -export interface ClearAppOpts { - /** - * Application package identifier - */ - appId: string; -} - -export interface QueryAppStateOpts { - /** - * Application package identifier - */ - appId: string; -} - /** * Returned by `queryAppState` * - `0` - is the app is not installed @@ -161,26 +42,6 @@ export interface TerminateAppOpts { * @defaultValue 500 */ timeout?: number | string; - /** - * Application package identifier - */ - appId: string; -} - -export interface ActivateAppOpts { - /** - * Application package identifier - */ - appId: string; -} - -export interface RemoveAppOpts extends UninstallOptions { - appId: string; -} - -export interface InstallAppOpts extends InstallOptions { - appPath: string; - checkVersion: boolean; } export interface WebviewsMapping { @@ -267,70 +128,6 @@ export interface DoSetElementValueOpts { replace: boolean; } -export interface ExecOptions { - /** - * The actual command to execute. - * - * @see {@link https://developer.android.com/studio/run/emulator-console} - */ - command: string | string[]; - /** - * A timeout used to wait for a server reply to the given command in - * milliseconds - * @defaultValue 60000 - */ - execTimeout?: number; - /** - * Console connection timeout in milliseconds - * @defaultValue 5000 - */ - connTimeout?: number; - /** - * Telnet console initialization timeout in milliseconds (the time between the - * connection happens and the command prompt is available) - */ - initTimeout?: number; -} - -export interface PullFileOpts { - /** - * The full path to the remote file or a specially formatted path, which - * points to an item inside an app bundle, for example `@my.app.id/my/path`. - * It is mandatory for the app bundle to have debugging enabled in order to - * use the latter `remotePath` format. - */ - remotePath: string; -} - -export interface PushFileOpts { - /** - * The full path to the remote file or a specially formatted path, which - * points to an item inside an app bundle, for example `@my.app.id/my/path`. - * It is mandatory for the app bundle to have debugging enabled in order to - * use the latter `remotePath` format. - */ - remotePath: string; - /** - * Base64-encoded content of the file to be pushed. - */ - payload: string; -} - -export interface PullFolderOpts { - /** - * The full path to the remote folder - */ - remotePath: string; -} - -export interface DeleteFileOpts { - /** - * The full path to the remote file or a file inside an application bundle - * (for example `@my.app.id/path/in/bundle`) - */ - remotePath: string; -} - export interface FindElementOpts { strategy: string; selector: string; @@ -344,17 +141,6 @@ export interface SendKeysOpts { replace?: boolean; } -export interface DeviceTimeOpts { - /** - * @defaultValue 'YYYY-MM-DDTHH:mm:ssZ' - */ - format?: string; -} - -export interface PerformEditorActionOpts { - action: string | number; -} - export interface ListSmsOpts { /** * Maximum count of recent SMS messages @@ -367,35 +153,6 @@ export type UnlockType = 'pin' | 'pinWithKeyEvent' | 'password' | 'pattern'; export type UnlockStrategy = 'locksettings' | 'uiautomator'; -export interface UnlockOptions { - /** - * The unlock key. The value of this key depends on the actual unlock type and - * could be a pin/password/pattern value or a biometric finger id. - * - * If not provided then the corresponding value from session capabilities is - * used. - */ - key?: string; - /** - * The unlock type. - * - * If not provided then the corresponding value from session capabilities is - * used. - */ - type?: UnlockType; - /** - * Setting it to 'uiautomator' will enforce the driver to avoid using special - * ADB shortcuts in order to speed up the unlock procedure. - * @defaultValue 'uiautomator' - */ - strategy?: UnlockStrategy; - /** - * The maximum time in milliseconds to wait until the screen gets unlocked - * @defaultValue 2000 - */ - timeoutMs?: number; -} - export interface IntentOpts { /** * The user ID for which the service is started. @@ -482,148 +239,6 @@ export interface IntentOpts { flags?: string; } -export interface StartActivityOpts extends IntentOpts { - /** - * Set it to `true` if you want to block the method call - * until the activity manager's process returns the control to the system. - * @defaultValue false - */ - wait?: boolean; - /** - * Set it to `true` to force stop the target - * app before starting the activity - * @defaultValue false - */ - stop?: boolean; - /** - * The windowing mode to launch the activity into. - * - * Check - * https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/WindowConfiguration.java - * for more details on possible windowing modes (constants starting with - * `WINDOWING_MODE_`). - */ - windowingMode?: number | string; - /** - * The activity type to launch the activity as. - * - * Check https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/WindowConfiguration.java - * for more details on possible activity types (constants starting with `ACTIVITY_TYPE_`). - */ - activityType?: number | string; - /** - * The display identifier to launch the activity into. - */ - display?: number | string; -} - -export interface BroadcastOpts extends IntentOpts { - /** - * The user ID for which the broadcast is sent. - * - * The `current` alias assumes the current user ID. - * @defaultValue `all` - */ - user?: string | number; - /** - * Require receiver to hold the given permission. - */ - receiverPermission?: string; - /** - * Whether the receiver may start activities even if in the background. - */ - allowBackgroundActivityStarts?: boolean; -} - -export interface StartServiceOpts extends IntentOpts { - /** - * Set it to `true` if your service must be started as foreground service. - * - * This option is ignored if the API level of the device under test is below - * 26 (Android 8). - */ - foreground?: boolean; -} - -export type StopServiceOpts = IntentOpts; - -export interface StartMediaProjectionRecordingOpts { - /** - * Maximum supported resolution on-device (Detected automatically by the app - * itself), which usually equals to Full HD 1920x1080 on most phones however - * you can change it to following supported resolutions as well: "1920x1080", - * "1280x720", "720x480", "320x240", "176x144". - */ - resolution?: string; - /** - * Maximum allowed duration is 15 minutes; you can increase it if your test - * takes longer than that. - * @defaultValue 900 - */ - maxDurationSec?: number; - /** - * Recording thread priority. - * - * If you face performance drops during testing with recording enabled, you - * can reduce recording priority - * - * @defaultValue 'high' - */ - priority?: 'high' | 'normal' | 'low'; - /** - * You can type recording video file name as you want, but recording currently - * supports only "mp4" format so your filename must end with ".mp4". An - * invalid file name will fail to start the recording. If not provided then - * the current timestamp will be used as file name. - */ - filename?: string; -} - -export interface StopMediaProjectionRecordingOpts { - /** - * The path to the remote location, where the resulting video should be - * uploaded. The following protocols are supported: http/https, ftp. Null or - * empty string value (the default setting) means the content of resulting - * file should be encoded as Base64 and passed as the endpoont response value. - * An exception will be thrown if the generated media file is too big to fit - * into the available process memory. - */ - remotePath?: string; - /** - * The name of the user for the remote authentication. - */ - user?: string; - /** - * The password for the remote authentication. - */ - pass?: string; - /** - * The http multipart upload method name. - * @defaultValue 'PUT' - */ - method?: HTTPMethod; - /** - * Additional headers mapping for multipart http(s) uploads - */ - headers?: StringRecord; - /** - * The name of the form field, where the file content BLOB should be stored - * for http(s) uploads - * @defaultValue 'file' - */ - fileFieldName?: string; - /** - * Additional form fields for multipart http(s) uploads - */ - formFields?: FormFields; - /** - * The actual media upload request timeout in milliseconds. - * - * Defaults to `@appium/support.net.DEFAULT_TIMEOUT_MS` - */ - uploadTimeout?: number; -} - export type FormFields = StringRecord | [key: string, value: any][]; export interface GetConnectivityResult { @@ -643,101 +258,8 @@ export interface GetConnectivityResult { export type ServiceType = 'wifi' | 'data' | 'airplaneMode'; -export interface GetConnectivityOpts { - /** - * one or more services to get the connectivity for. - */ - services?: ServiceType[] | ServiceType; -} - -export interface SetConnectivityOpts { - /** - * Either to enable or disable Wi-Fi. - * An unset value means to not change the state for the given service. - */ - wifi?: boolean; - - /** - * Either to enable or disable mobile data connection. - * An unset value means to not change the state for the given service. - */ - data?: boolean; - - /** - * Either to enable to disable the Airplane Mode - * An unset value means to not change the state for the given service. - */ - airplaneMode?: boolean; -} - -export interface GpsCacheRefreshOpts { - /** - * The maximum number of milliseconds - * to block until GPS cache is refreshed. Providing zero or a negative - * value to it skips waiting completely. - * @defaultValue 20000 - */ - timeoutMs?: number; -} - -export interface PerformanceDataOpts { - /** - * The name of the package identifier to fetch the data for - */ - packageName: string; - /** - * One of supported subsystem to fetch the data for. - */ - dataType: PerformanceDataType; -} - export type PerformanceDataType = 'batteryinfo' | 'cpuinfo' | 'memoryinfo' | 'networkinfo'; -export interface GetPermissionsOpts { - /** - * One of possible permission types to get. - * @defaultValue 'requested' - */ - type?: string; - /** - * The application package to set change permissions on. Defaults to the - * package name under test - */ - appPackage?: string; -} - -export interface ChangePermissionsOpts { - /** - * If `target` is set to 'pm': - * The full name of the permission to be changed - * or a list of permissions. Check https://developer.android.com/reference/android/Manifest.permission - * to get the full list of standard Android permssion names. Mandatory argument. - * If 'all' magic string is passed then the chosen action is going to be applied to all - * permisisons requested/granted by 'appPackage'. - * If `target` is set to 'appops': - * The full name of the appops permission to be changed - * or a list of permissions. Check AppOpsManager.java sources to get the full list of - * available appops permission names. Mandatory argument. - * Examples: 'ACTIVITY_RECOGNITION', 'SMS_FINANCIAL_TRANSACTIONS', 'READ_SMS', 'ACCESS_NOTIFICATIONS'. - * The 'all' magic string is unsupported. - */ - permissions: string | string[]; - /** - * The application package to set change permissions on. Defaults to the - * package name under test - */ - appPackage?: string; - /** - * One of `PM_ACTION` values if `target` is set to 'pm', otherwise one of `APPOPS_ACTION` values - */ - action?: string; - /** - * Either 'pm' or 'appops'. The 'appops' one requires 'adb_shell' server security option to be enabled. - * @defaultValue 'pm' - */ - target?: 'pm' | 'appops'; -} - export interface StartScreenRecordingOpts { /** * The path to the remote location, where the captured video should be @@ -867,95 +389,6 @@ export interface StopScreenRecordingOpts { formFields?: FormFields; } -/** - * @privateRemarks inferred from usage - */ -export interface ShellOpts { - command: string; - args?: string[]; - timeout?: number; - includeStderr?: boolean; -} - -export interface StartScreenStreamingOpts { - /** - * The scaled width of the device's screen. - * - * If unset then the script will assign it to the actual screen width measured - * in pixels. - */ - width?: number; - /** - * The scaled height of the device's screen. - * - * If unset then the script will assign it to the actual screen height - * measured in pixels. - */ - height?: number; - /** - * The video bit rate for the video, in bits per second. - * - * The default value is 4 Mb/s. You can increase the bit rate to improve video - * quality, but doing so results in larger movie files. - * @defaultValue 4000000 - */ - bitRate?: number; - /** - * The IP address/host name to start the MJPEG server on. - * - * You can set it to `0.0.0.0` to trigger the broadcast on all available - * network interfaces. - * - * @defaultValue '127.0.0.1' - */ - host?: string; - /** - * The HTTP request path the MJPEG server should be available on. - * - * If unset, then any pathname on the given `host`/`port` combination will - * work. Note that the value should always start with a single slash: `/` - */ - pathname?: string; - /** - * The port number to start the internal TCP MJPEG broadcast on. - * - * This type of broadcast always starts on the loopback interface - * (`127.0.0.1`). - * - * @defaultValue 8094 - */ - tcpPort?: number; - /** - * The port number to start the MJPEG server on. - * - * @defaultValue 8093 - */ - port?: number; - /** - * The quality value for the streamed JPEG images. - * - * This number should be in range `[1,100]`, where `100` is the best quality. - * - * @defaultValue 70 - */ - quality?: number; - /** - * If set to `true` then GStreamer pipeline will increase the dimensions of - * the resulting images to properly fit images in both landscape and portrait - * orientations. - * - * Set it to `true` if the device rotation is not going to be the same during - * the broadcasting session. - */ - considerRotation?: boolean; - /** - * Whether to log GStreamer pipeline events into the standard log output. - * - * Might be useful for debugging purposes. - */ - logPipelineDetails?: boolean; -} - export interface DeviceInfo { width: number; height: number; @@ -1017,77 +450,6 @@ export type StatusBarCommand = | 'clickTile' | 'getStatusIcons'; -export interface StatusBarCommandOpts { - /** - * Each list item must separated with a new line (`\n`) character. - */ - command: StatusBarCommand; - /** - * The name of the tile component. - * - * It is only required for `(add|remove|click)Tile` commands. - * Example value: `com.package.name/.service.QuickSettingsTileComponent` - */ - component?: string; -} - -export interface LockOpts { - /** - * The number to keep the locked. - * 0 or empty value will keep the device locked. - */ - seconds?: number; -} - -export interface DeviceidleOpts { - /** The action name to execute */ - action: 'whitelistAdd' | 'whitelistRemove'; - /** Either a single package or multiple packages to add or remove from the idle whitelist */ - packages?: string | string[]; -} - -export interface SendTrimMemoryOpts { - /** The package name to send the `trimMemory` event to */ - pkg: string; - /** The actual memory trim level to be sent */ - level: - | 'COMPLETE' - | 'MODERATE' - | 'BACKGROUND' - | 'UI_HIDDEN' - | 'RUNNING_CRITICAL' - | 'RUNNING_LOW' - | 'RUNNING_MODERATE'; -} - -export interface ImageInjectionOpts { - /** Base64-encoded payload of a .png image to be injected */ - payload: string; -} - -export interface SetUiModeOpts { - /** - * The UI mode to set the value for. - * Supported values are: 'night' and 'car' - */ - mode: string; - /** - * The actual mode value to set. - * Supported value for different UI modes are: - * - night: yes|no|auto|custom_schedule|custom_bedtime - * - car: yes|no - */ - value: string; -} - -export interface GetUiModeOpts { - /** - * The UI mode to set the value for. - * Supported values are: 'night' and 'car' - */ - mode: string; -} - export interface SmsListResultItem { id: string; address: string; @@ -1226,14 +588,6 @@ export type ADBLaunchInfo = Pick< 'appPackage' | 'appWaitActivity' | 'appActivity' | 'appWaitPackage' >; -export interface BluetoothOptions { - action: 'enable' | 'disable' | 'unpairAll'; -} - -export interface NfcOptions { - action: 'enable' | 'disable'; -} - export interface InjectedImageSize { /** X scale value in range (0..) */ scaleX?: number; diff --git a/lib/driver.ts b/lib/driver.ts index 8de10869..86afe0c4 100644 --- a/lib/driver.ts +++ b/lib/driver.ts @@ -76,9 +76,7 @@ import { mobileClearApp, mobileInstallApp, installApp, - mobileActivateApp, mobileIsAppInstalled, - mobileQueryAppState, mobileRemoveApp, mobileTerminateApp, terminateApp, @@ -109,16 +107,11 @@ import { } from './commands/element'; import { execute, - executeMobile, - mobileCommandsMapping, } from './commands/execute'; import { pullFile, - mobilePullFile, pullFolder, - mobilePullFolder, pushFile, - mobilePushFile, mobileDeleteFile, } from './commands/file-actions'; import {findElOrEls, doFindElementOrEls} from './commands/find'; @@ -158,7 +151,7 @@ import { longPressKeyCode, mobilePerformEditorAction, } from './commands/keyboard'; -import {lock, unlock, mobileLock, mobileUnlock, isLocked} from './commands/lock/exports'; +import {lock, unlock, mobileUnlock, isLocked} from './commands/lock/exports'; import { supportedLogTypes, mobileStartLogsBroadcast, @@ -213,6 +206,7 @@ import {mobileShell} from './commands/shell'; import {mobileStartScreenStreaming, mobileStopScreenStreaming} from './commands/streamscreen'; import {getSystemBars, mobilePerformStatusBarCommand} from './commands/system-bars'; import {getDeviceTime, mobileGetDeviceTime} from './commands/time'; +import { executeMethodMap } from './execute-method-map'; export type AndroidDriverCaps = DriverCaps; export type W3CAndroidDriverCaps = W3CDriverCaps; @@ -226,6 +220,8 @@ class AndroidDriver implements ExternalDriver { static newMethodMap = newMethodMap; + static executeMethodMap = executeMethodMap; + jwpProxyAvoid: RouteMatcher[]; adb: ADB; @@ -409,9 +405,7 @@ class AndroidDriver mobileClearApp = mobileClearApp; mobileInstallApp = mobileInstallApp; installApp = installApp; - mobileActivateApp = mobileActivateApp; mobileIsAppInstalled = mobileIsAppInstalled; - mobileQueryAppState = mobileQueryAppState; mobileRemoveApp = mobileRemoveApp; mobileTerminateApp = mobileTerminateApp; terminateApp = terminateApp; @@ -444,15 +438,10 @@ class AndroidDriver getSize = getSize; execute = execute; - executeMobile = executeMobile; - mobileCommandsMapping = mobileCommandsMapping; pullFile = pullFile; - mobilePullFile = mobilePullFile; pullFolder = pullFolder; - mobilePullFolder = mobilePullFolder; pushFile = pushFile; - mobilePushFile = mobilePushFile; mobileDeleteFile = mobileDeleteFile; findElOrEls = findElOrEls; @@ -501,7 +490,6 @@ class AndroidDriver lock = lock; unlock = unlock; - mobileLock = mobileLock; mobileUnlock = mobileUnlock; isLocked = isLocked; diff --git a/lib/execute-method-map.ts b/lib/execute-method-map.ts new file mode 100644 index 00000000..5a20be21 --- /dev/null +++ b/lib/execute-method-map.ts @@ -0,0 +1,464 @@ +import {ExecuteMethodMap} from '@appium/types'; + +const INTENT_PARAMS = [ + 'user', + 'intent', + 'action', + 'package', + 'uri', + 'mimeType', + 'identifier', + 'component', + 'categories', + 'extras', + 'flags', +] as const; + +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: startLogsBroadcast': { + command: 'mobileStartLogsBroadcast', + }, + 'mobile: stopLogsBroadcast': { + command: 'mobileStopLogsBroadcast', + }, + + '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: getDeviceTime': { + command: 'mobileGetDeviceTime', + params: { + optional: ['format'], + }, + }, + + 'mobile: startScreenStreaming': { + command: 'mobileStartScreenStreaming', + params: { + optional: [ + 'width', + 'height', + 'bitRate', + 'host', + 'port', + 'pathname', + 'tcpPort', + 'quality', + 'considerRotation', + 'logPipelineDetails', + ], + }, + }, + 'mobile: stopScreenStreaming': { + command: 'mobileStopScreenStreaming', + }, + + 'mobile: getNotifications': { + command: 'mobileGetNotifications', + }, + + 'mobile: listSms': { + command: 'mobileListSms', + }, + + 'mobile: pushFile': { + command: 'pushFile', + params: { + required: ['remotePath', 'payload'], + }, + }, + 'mobile: pullFolder': { + command: 'pullFolder', + params: { + required: ['remotePath'], + }, + }, + 'mobile: pullFile': { + command: 'pullFile', + params: { + required: ['remotePath'], + }, + }, + 'mobile: deleteFile': { + command: 'mobileDeleteFile', + params: { + required: ['remotePath'], + }, + }, + + 'mobile: isAppInstalled': { + command: 'mobileIsAppInstalled', + params: { + required: ['appId'], + optional: ['user'], + }, + }, + 'mobile: queryAppState': { + command: 'queryAppState', + params: { + required: ['appId'], + }, + }, + 'mobile: activateApp': { + command: 'activateApp', + params: { + required: ['appId'], + }, + }, + 'mobile: removeApp': { + command: 'mobileRemoveApp', + params: { + required: ['appId'], + optional: ['timeout', 'keepData', 'skipInstallCheck'], + }, + }, + 'mobile: terminateApp': { + command: 'mobileTerminateApp', + params: { + required: ['appId'], + optional: ['timeout'], + }, + }, + 'mobile: installApp': { + command: 'mobileInstallApp', + params: { + required: ['appPath'], + optional: [ + 'checkVersion', + 'timeout', + 'allowTestPackages', + 'useSdcard', + 'grantPermissions', + 'replace', + 'noIncremental', + ], + }, + }, + 'mobile: clearApp': { + command: 'mobileClearApp', + params: { + required: ['appId'], + }, + }, + + 'mobile: startService': { + command: 'mobileStartService', + params: { + optional: [ + 'foreground', + ...INTENT_PARAMS, + ], + }, + }, + 'mobile: stopService': { + command: 'mobileStopService', + params: { + optional: [ + ...INTENT_PARAMS, + ], + }, + }, + 'mobile: startActivity': { + command: 'mobileStartActivity', + params: { + optional: [ + 'wait', + 'stop', + 'windowingMode', + 'activityType', + 'display', + ...INTENT_PARAMS, + ], + }, + }, + 'mobile: broadcast': { + command: 'mobileBroadcast', + params: { + optional: [ + 'receiverPermission', + 'allowBackgroundActivityStarts', + ...INTENT_PARAMS, + ], + }, + }, + + 'mobile: getContexts': { + command: 'mobileGetContexts', + params: { + optional: [ + 'waitForWebviewMs', + ], + }, + }, + + 'mobile: lock': { + command: 'lock', + 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', + 'priority', + 'maxDurationSec', + '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: hideKeyboard': { + command: 'hideKeyboard', + }, + 'mobile: isKeyboardShown': { + command: 'isKeyboardShown', + }, + + 'mobile: deviceidle': { + command: 'mobileDeviceidle', + params: { + required: ['action'], + optional: ['packages'], + } + }, + + 'mobile: bluetooth': { + command: 'mobileBluetooth', + params: { + required: ['action'], + } + }, + 'mobile: nfc': { + command: 'mobileNfc', + params: { + required: ['action'], + } + }, + + 'mobile: setUiMode': { + command: 'mobileSetUiMode', + params: { + required: ['mode', 'value'], + } + }, + 'mobile: getUiMode': { + command: 'mobileGetUiMode', + params: { + required: ['mode'], + } + }, + + 'mobile: injectEmulatorCameraImage': { + command: 'mobileInjectEmulatorCameraImage', + params: { + required: ['payload'], + } + }, + + 'mobile: sendTrimMemory': { + command: 'mobileSendTrimMemory', + params: { + required: ['pkg', 'level'], + } + }, + + 'mobile: getPerformanceData': { + command: 'mobileGetPerformanceData', + params: { + required: ['packageName', 'dataType'], + } + }, + 'mobile: getPerformanceDataTypes': { + command: 'getPerformanceDataTypes', + }, + + 'mobile: toggleGps': { + command: 'toggleLocationServices', + }, + 'mobile: isGpsEnabled': { + command: 'isLocationServicesEnabled', + }, + + 'mobile: getDisplayDensity': { + command: 'getDisplayDensity', + }, + 'mobile: getSystemBars': { + command: 'getSystemBars', + }, + 'mobile: statusBar': { + command: 'mobilePerformStatusBarCommand', + params: { + required: ['command'], + optional: ['component'], + } + }, + + '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: getCurrentActivity': { + command: 'getCurrentActivity', + }, + 'mobile: getCurrentPackage': { + command: 'getCurrentPackage', + }, + + 'mobile: setGeolocation': { + command: 'mobileSetGeolocation', + params: { + required: ['latitude', 'longitude'], + optional: ['altitude'], + } + }, + 'mobile: getGeolocation': { + command: 'mobileGetGeolocation', + }, + 'mobile: resetGeolocation': { + command: 'mobileResetGeolocation', + }, +} as const satisfies ExecuteMethodMap; diff --git a/test/unit/commands/app-management-specs.js b/test/unit/commands/app-management-specs.js index 78534b35..7dbb6027 100644 --- a/test/unit/commands/app-management-specs.js +++ b/test/unit/commands/app-management-specs.js @@ -3,7 +3,6 @@ import {AndroidDriver} from '../../../lib/driver'; import {fs} from '@appium/support'; import B from 'bluebird'; import {ADB} from 'appium-adb'; -import { errors } from 'appium/driver'; /** @type {AndroidDriver} */ let driver; @@ -60,24 +59,21 @@ describe('App Management', function () { }); }); describe('mobileIsAppInstalled', function () { - it('should raise required error if appId was not provided', async function () { - await driver.mobileIsAppInstalled({}).should.be.rejectedWith(errors.InvalidArgumentError); - }); it('should return true if app is installed', async function () { sandbox.stub(driver.adb, 'isAppInstalled').withArgs('pkg').returns(true); - (await driver.mobileIsAppInstalled({appId: 'pkg'})).should.be.true; + (await driver.mobileIsAppInstalled('pkg')).should.be.true; }); it('should return true if app is installed with undefined user', async function () { sandbox.stub(driver.adb, 'isAppInstalled').withArgs('pkg').returns(true); - (await driver.mobileIsAppInstalled({appId: 'pkg'})).should.be.true; + (await driver.mobileIsAppInstalled('pkg')).should.be.true; }); it('should return true if app is installed with user string', async function () { sandbox.stub(driver.adb, 'isAppInstalled').withArgs('pkg', {user: '1'}).returns(true); - (await driver.mobileIsAppInstalled({appId: 'pkg', user: '1'})).should.be.true; + (await driver.mobileIsAppInstalled('pkg', '1')).should.be.true; }); it('should return true if app is installed with user number', async function () { sandbox.stub(driver.adb, 'isAppInstalled').withArgs('pkg', {user: '1'}).returns(true); - (await driver.mobileIsAppInstalled({appId: 'pkg', user: 1})).should.be.true; + (await driver.mobileIsAppInstalled('pkg', 1)).should.be.true; }); }); describe('removeApp', function () { diff --git a/test/unit/commands/emulator-actions-specs.js b/test/unit/commands/emulator-actions-specs.js index de3a6aaf..f97a655a 100644 --- a/test/unit/commands/emulator-actions-specs.js +++ b/test/unit/commands/emulator-actions-specs.js @@ -24,16 +24,16 @@ describe('Emulator Actions', function () { }); describe('sensorSet', function () { it('should call sensorSet', async function () { - sandbox.stub(driver, 'sensorSet'); - await driver.executeMobile('sensorSet', {sensorType: 'light', value: 0}); - driver.sensorSet.calledWithExactly({sensorType: 'light', value: 0}).should.be.true; + const sensorSetStub = sandbox.stub(driver, 'sensorSet'); + await driver.execute('mobile:sensorSet', [{sensorType: 'light', value: 0}]); + sensorSetStub.calledWith('light', 0).should.be.true; }); it('should be reject if arguments are missing', function () { driver - .executeMobile('sensorSet', {sensor: 'light', value: 0}) + .execute('mobile: sensorSet', [{sensor: 'light', value: 0}]) .should.eventually.be.rejectedWith(`'sensorType' argument is required`); driver - .executeMobile('sensorSet', {sensorType: 'light', val: 0}) + .execute('mobile: sensorSet', [{sensorType: 'light', val: 0}]) .should.eventually.be.rejectedWith(`'value' argument is required`); }); });