UNPKG

appium-android-driver

Version:

Android UiAutomator and Chrome support for Appium

480 lines 19.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.APP_STATE = void 0; exports.isAppInstalled = isAppInstalled; exports.mobileIsAppInstalled = mobileIsAppInstalled; exports.queryAppState = queryAppState; exports.activateApp = activateApp; exports.removeApp = removeApp; exports.mobileRemoveApp = mobileRemoveApp; exports.terminateApp = terminateApp; exports.mobileTerminateApp = mobileTerminateApp; exports.installApp = installApp; exports.mobileInstallApp = mobileInstallApp; exports.mobileClearApp = mobileClearApp; exports.getCurrentActivity = getCurrentActivity; exports.getCurrentPackage = getCurrentPackage; exports.background = background; exports.mobileBackgroundApp = mobileBackgroundApp; exports.resetAUT = resetAUT; exports.installAUT = installAUT; exports.installOtherApks = installOtherApks; exports.uninstallOtherPackages = uninstallOtherPackages; exports.getThirdPartyPackages = getThirdPartyPackages; const support_1 = require("@appium/support"); const asyncbox_1 = require("asyncbox"); const lodash_1 = __importDefault(require("lodash")); const node_os_1 = require("node:os"); const bluebird_1 = __importDefault(require("bluebird")); const APP_EXTENSIONS = ['.apk', '.apks']; const PACKAGE_INSTALL_TIMEOUT_MS = 90000; // These constants are in sync with // https://developer.apple.com/documentation/xctest/xcuiapplicationstate/xcuiapplicationstaterunningbackground?language=objc exports.APP_STATE = ({ NOT_INSTALLED: 0, NOT_RUNNING: 1, RUNNING_IN_BACKGROUND: 3, RUNNING_IN_FOREGROUND: 4, }); /** * @typedef {Object} IsAppInstalledOptions * @property {string} [user] - The user id */ /** * @this {AndroidDriver} * @param {string} appId * @param {IsAppInstalledOptions} [opts={}] * @returns {Promise<boolean>} */ async function isAppInstalled(appId, opts = {}) { return await this.adb.isAppInstalled(appId, opts); } /** * @this {AndroidDriver} * @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<boolean>} */ async function mobileIsAppInstalled(appId, user) { const _opts = {}; if (support_1.util.hasValue(user)) { _opts.user = `${user}`; } return await this.isAppInstalled(appId, _opts); } /** * @this {AndroidDriver} * @param {string} appId Application package identifier * @returns {Promise<import('./types').AppState>} */ async function queryAppState(appId) { this.log.info(`Querying the state of '${appId}'`); if (!(await this.adb.isAppInstalled(appId))) { return exports.APP_STATE.NOT_INSTALLED; } if (!(await this.adb.processExists(appId))) { return exports.APP_STATE.NOT_RUNNING; } const appIdRe = new RegExp(`\\b${lodash_1.default.escapeRegExp(appId)}/`); for (const line of (await this.adb.dumpWindows()).split('\n')) { if (appIdRe.test(line) && ['mCurrentFocus', 'mFocusedApp'].some((x) => line.includes(x))) { return exports.APP_STATE.RUNNING_IN_FOREGROUND; } } return exports.APP_STATE.RUNNING_IN_BACKGROUND; } /** * @this {AndroidDriver} * @param {string} appId Application package identifier * @returns {Promise<void>} */ async function activateApp(appId) { return await this.adb.activateApp(appId); } /** * @this {AndroidDriver} * @param {string} appId * @param {Omit<import('appium-adb').UninstallOptions, 'appId'>} opts * @returns {Promise<boolean>} */ async function removeApp(appId, opts = {}) { return await this.adb.uninstallApk(appId, opts); } /** * @this {import('../driver').AndroidDriver} * @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<boolean>} */ async function mobileRemoveApp(appId, timeout, keepData, skipInstallCheck) { return await this.removeApp(appId, { timeout, keepData, skipInstallCheck, }); } /** * @this {AndroidDriver} * @param {string} appId * @param {import('./types').TerminateAppOpts} [options={}] * @returns {Promise<boolean>} */ async function terminateApp(appId, options = {}) { this.log.info(`Terminating '${appId}'`); const pids = await this.adb.getPIDsByName(appId); if (lodash_1.default.isEmpty(pids)) { this.log.info(`The app '${appId}' is not running`); return false; } await this.adb.forceStop(appId); const timeout = support_1.util.hasValue(options.timeout) && !Number.isNaN(options.timeout) ? parseInt(String(options.timeout), 10) : 500; if (timeout <= 0) { this.log.info(`'${appId}' has been terminated. Skipping checking of the application process state ` + `since the timeout was set to ${timeout}ms`); return true; } /** @type {number[]} */ let currentPids = []; try { await (0, asyncbox_1.waitForCondition)(async () => { if (await this.queryAppState(appId) <= exports.APP_STATE.NOT_RUNNING) { return true; } currentPids = await this.adb.getPIDsByName(appId); if (lodash_1.default.isEmpty(currentPids) || lodash_1.default.isEmpty(lodash_1.default.intersection(pids, currentPids))) { this.log.info(`The application '${appId}' was reported running, ` + `although all process ids belonging to it have been changed: ` + `(${JSON.stringify(pids)} -> ${JSON.stringify(currentPids)}). ` + `Assuming the termination was successful.`); return true; } return false; }, { waitMs: timeout, intervalMs: 100, }); } catch { if (!lodash_1.default.isEmpty(currentPids) && !lodash_1.default.isEmpty(lodash_1.default.difference(pids, currentPids))) { this.log.warn(`Some of processes belonging to the '${appId}' applcation are still running ` + `after ${timeout}ms (${JSON.stringify(pids)} -> ${JSON.stringify(currentPids)})`); } throw this.log.errorWithException(`'${appId}' is still running after ${timeout}ms timeout`); } this.log.info(`'${appId}' has been successfully terminated`); return true; } /** * @this {AndroidDriver} * @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<boolean>} */ async function mobileTerminateApp(appId, timeout) { return await this.terminateApp(appId, { timeout, }); } /** * @this {AndroidDriver} * @param {string} appPath * @param {Omit<import('appium-adb').InstallOptions, 'appId'>} opts * @returns {Promise<void>} */ async function installApp(appPath, opts) { const localPath = await this.helpers.configureApp(appPath, APP_EXTENSIONS); await this.adb.install(localPath, opts); } /** * @this {AndroidDriver} * @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<void>} */ 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({}, { appPath, checkVersion, ...opts, }, { enforceCurrentBuild: false })); return; } return await this.installApp(appPath, opts); } /** * @this {AndroidDriver} * @param {string} appId Application package identifier * @returns {Promise<void>} */ async function mobileClearApp(appId) { await this.adb.clear(appId); } /** * @this {AndroidDriver} * @returns {Promise<string>} */ async function getCurrentActivity() { return /** @type {string} */ ((await this.adb.getFocusedPackageAndActivity()).appActivity); } /** * @this {AndroidDriver} * @returns {Promise<string>} */ async function getCurrentPackage() { return /** @type {string} */ ((await this.adb.getFocusedPackageAndActivity()).appPackage); } /** * @this {AndroidDriver} * @param {number} seconds * @returns {Promise<string|true>} */ async function background(seconds) { if (seconds < 0) { // if user passes in a negative seconds value, interpret that as the instruction // to not bring the app back at all await this.adb.goToHome(); return true; } let { appPackage, appActivity } = await this.adb.getFocusedPackageAndActivity(); await this.adb.goToHome(); // people can wait for a long time, so to be safe let's use the longSleep function and log // progress periodically. const sleepMs = seconds * 1000; const thresholdMs = 30 * 1000; // use the spin-wait for anything over this threshold // for our spin interval, use 1% of the total wait time, but nothing bigger than 30s const intervalMs = lodash_1.default.min([30 * 1000, parseInt(String(sleepMs / 100), 10)]); /** * * @param {{elapsedMs: number, progress: number}} param0 */ const progressCb = ({ elapsedMs, progress }) => { const waitSecs = (elapsedMs / 1000).toFixed(0); const progressPct = (progress * 100).toFixed(2); this.log.debug(`Waited ${waitSecs}s so far (${progressPct}%)`); }; await (0, asyncbox_1.longSleep)(sleepMs, { thresholdMs, intervalMs, progressCb }); /** @type {import('appium-adb').StartAppOptions} */ let args; if (this._cachedActivityArgs && this._cachedActivityArgs[`${appPackage}/${appActivity}`]) { // the activity was started with `startActivity`, so use those args to restart args = this._cachedActivityArgs[`${appPackage}/${appActivity}`]; } else { try { this.log.debug(`Activating app '${appPackage}' in order to restore it`); await this.adb.activateApp(/** @type {string} */ (appPackage)); return true; } catch { } args = (appPackage === this.opts.appPackage && appActivity === this.opts.appActivity) || (appPackage === this.opts.appWaitPackage && (this.opts.appWaitActivity || '').split(',').includes(String(appActivity))) ? { // the activity is the original session activity, so use the original args pkg: /** @type {string} */ (this.opts.appPackage), activity: this.opts.appActivity ?? undefined, action: this.opts.intentAction, category: this.opts.intentCategory, flags: this.opts.intentFlags, waitPkg: this.opts.appWaitPackage ?? undefined, waitActivity: this.opts.appWaitActivity ?? undefined, waitForLaunch: this.opts.appWaitForLaunch, waitDuration: this.opts.appWaitDuration, optionalIntentArguments: this.opts.optionalIntentArguments, stopApp: false, user: this.opts.userProfile, } : { // the activity was started some other way, so use defaults pkg: /** @type {string} */ (appPackage), activity: appActivity ?? undefined, waitPkg: appPackage ?? undefined, waitActivity: appActivity ?? undefined, stopApp: false, }; } args = /** @type {import('appium-adb').StartAppOptions} */ (lodash_1.default.pickBy(args, (value) => !lodash_1.default.isUndefined(value))); this.log.debug(`Bringing application back to foreground with arguments: ${JSON.stringify(args)}`); return await this.adb.startApp(args); } /** * Puts the app to background and waits the given number of seconds then restores the app * if necessary. The call is blocking. * * @this {AndroidDriver} * @param {number} [seconds=-1] The amount of seconds to wait between putting the app to background and restoring it. * Any negative value means to not restore the app after putting it to background. * @returns {Promise<void>} */ async function mobileBackgroundApp(seconds = -1) { await this.background(seconds); } /** * @this {AndroidDriver} * @param {import('../driver').AndroidDriverOpts?} [opts=null] * @returns {Promise<void>} */ async function resetAUT(opts = null) { const { app, appPackage, fastReset, fullReset, androidInstallTimeout = PACKAGE_INSTALL_TIMEOUT_MS, autoGrantPermissions, allowTestPackages, } = opts ?? this.opts; if (!appPackage) { throw new Error("'appPackage' option is required"); } const isInstalled = await this.adb.isAppInstalled(appPackage); if (isInstalled) { try { await this.adb.forceStop(appPackage); } catch { } // fullReset has priority over fastReset if (!fullReset && fastReset) { const output = await this.adb.clear(appPackage); if (lodash_1.default.isString(output) && output.toLowerCase().includes('failed')) { throw new Error(`Cannot clear the application data of '${appPackage}'. Original error: ${output}`); } // executing `shell pm clear` resets previously assigned application permissions as well if (autoGrantPermissions) { try { await this.adb.grantAllPermissions(appPackage); } catch (error) { this.log.error(`Unable to grant permissions requested. Original error: ${error.message}`); } } this.log.debug(`Performed fast reset on the installed '${appPackage}' application (stop and clear)`); return; } } if (!app) { throw new Error(`Either provide 'app' option to install '${appPackage}' or ` + `consider setting 'noReset' to 'true' if '${appPackage}' is supposed to be preinstalled.`); } this.log.debug(`Running full reset on '${appPackage}' (reinstall)`); if (isInstalled) { await this.adb.uninstallApk(appPackage); } await this.adb.install(app, { grantPermissions: autoGrantPermissions, timeout: androidInstallTimeout, allowTestPackages, }); } /** * @this {AndroidDriver} * @param {import('../driver').AndroidDriverOpts?} [opts=null] * @returns {Promise<void>} */ async function installAUT(opts = null) { const { app, appPackage, fastReset, fullReset, androidInstallTimeout = PACKAGE_INSTALL_TIMEOUT_MS, autoGrantPermissions, allowTestPackages, enforceAppInstall, } = opts ?? this.opts; if (!app || !appPackage) { throw new Error("'app' and 'appPackage' options are required"); } if (fullReset) { await this.resetAUT(opts); return; } const { appState, wasUninstalled } = await this.adb.installOrUpgrade(app, appPackage, { grantPermissions: autoGrantPermissions, timeout: androidInstallTimeout, allowTestPackages, enforceCurrentBuild: enforceAppInstall, }); // There is no need to reset the newly installed app const isInstalledOverExistingApp = !wasUninstalled && appState !== this.adb.APP_INSTALL_STATE.NOT_INSTALLED; if (fastReset && isInstalledOverExistingApp) { this.log.info(`Performing fast reset on '${appPackage}'`); await this.resetAUT(opts); } } /** * @this {AndroidDriver} * @param {string[]} otherApps * @param {import('../driver').AndroidDriverOpts?} [opts=null] * @returns {Promise<void>} */ async function installOtherApks(otherApps, opts = null) { const { androidInstallTimeout = PACKAGE_INSTALL_TIMEOUT_MS, autoGrantPermissions, allowTestPackages, } = opts ?? this.opts; // Install all of the APK's asynchronously await bluebird_1.default.all(otherApps.map((otherApp) => { this.log.debug(`Installing app: ${otherApp}`); return this.adb.installOrUpgrade(otherApp, undefined, { grantPermissions: autoGrantPermissions, timeout: androidInstallTimeout, allowTestPackages, }); })); } /** * @this {AndroidDriver} * @param {string[]} appPackages * @param {string[]} [filterPackages=[]] * @returns {Promise<void>} */ async function uninstallOtherPackages(appPackages, filterPackages = []) { if (appPackages.includes('*')) { this.log.debug('Uninstall third party packages'); appPackages = await getThirdPartyPackages.bind(this)(filterPackages); } this.log.debug(`Uninstalling packages: ${appPackages}`); await bluebird_1.default.all(appPackages.map((appPackage) => this.adb.uninstallApk(appPackage))); } /** * @this {AndroidDriver} * @param {string[]} [filterPackages=[]] * @returns {Promise<string[]>} */ async function getThirdPartyPackages(filterPackages = []) { try { const packagesString = await this.adb.shell(['pm', 'list', 'packages', '-3']); const appPackagesArray = packagesString .trim() .replace(/package:/g, '') .split(node_os_1.EOL); this.log.debug(`'${appPackagesArray}' filtered with '${filterPackages}'`); return lodash_1.default.difference(appPackagesArray, filterPackages); } catch (err) { this.log.warn(`Unable to get packages with 'adb shell pm list packages -3': ${err.message}`); return []; } } /** * @typedef {import('../driver').AndroidDriver} AndroidDriver */ //# sourceMappingURL=app-management.js.map