UNPKG

appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

347 lines 14.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.RealDevice = void 0; exports.getConnectedDevices = getConnectedDevices; const support_1 = require("appium/support"); const path_1 = __importDefault(require("path")); const appium_ios_device_1 = require("appium-ios-device"); const bluebird_1 = __importStar(require("bluebird")); const logger_1 = __importDefault(require("./logger")); const lodash_1 = __importDefault(require("lodash")); const app_utils_1 = require("./app-utils"); const ios_fs_helpers_1 = require("./ios-fs-helpers"); const devicectl_1 = require("./real-device-clients/devicectl"); const APPLICATION_INSTALLED_NOTIFICATION = 'com.apple.mobile.application_installed'; const APPLICATION_NOTIFICATION_TIMEOUT_MS = 30 * 1000; const INSTALLATION_STAGING_DIR = 'PublicStaging'; /** * @returns {Promise<string[]>} */ async function getConnectedDevices() { return await appium_ios_device_1.utilities.getConnectedDevices(); } /** * @typedef {Object} InstallOptions * @param {number} [timeoutMs=240000] Application installation timeout in milliseconds */ /** * @typedef {Object} InstallOrUpgradeOptions * @property {number} timeout Install/upgrade timeout in milliseconds * @property {boolean} isUpgrade Whether it is an app upgrade or a new install */ class RealDevice { /** * @param {string} udid * @param {import('@appium/types').AppiumLogger} [logger] */ constructor(udid, logger) { this.udid = udid; this._log = logger ?? logger_1.default; this.devicectl = new devicectl_1.Devicectl(this.udid, this._log); } /** * @returns {import('@appium/types').AppiumLogger} */ get log() { return this._log; } /** * @param {string} bundleId */ async remove(bundleId) { const service = await appium_ios_device_1.services.startInstallationProxyService(this.udid); try { await service.uninstallApplication(bundleId); } finally { service.close(); } } /** * @param {string} bundleId */ async removeApp(bundleId) { await this.remove(bundleId); } /** * * @param {string} appPath * @param {string} bundleId * @param {InstallOptions} [opts={}] */ async install(appPath, bundleId, opts = {}) { const { timeoutMs = ios_fs_helpers_1.IO_TIMEOUT_MS, } = opts; const timer = new support_1.timing.Timer().start(); const afcService = await appium_ios_device_1.services.startAfcService(this.udid); try { let bundlePathOnPhone; if ((await support_1.fs.stat(appPath)).isFile()) { // https://github.com/doronz88/pymobiledevice3/blob/6ff5001f5776e03b610363254e82d7fbcad4ef5f/pymobiledevice3/services/installation_proxy.py#L75 bundlePathOnPhone = `/${path_1.default.basename(appPath)}`; await (0, ios_fs_helpers_1.pushFile)(afcService, appPath, bundlePathOnPhone, { timeoutMs, }); } else { bundlePathOnPhone = `${INSTALLATION_STAGING_DIR}/${bundleId}`; await (0, ios_fs_helpers_1.pushFolder)(afcService, appPath, bundlePathOnPhone, { enableParallelPush: true, timeoutMs, }); } await this.installOrUpgradeApplication(bundlePathOnPhone, { timeout: Math.max(timeoutMs - timer.getDuration().asMilliSeconds, 60000), isUpgrade: await this.isAppInstalled(bundleId), }); } catch (err) { this.log.debug(err.stack); let errMessage = `Cannot install the ${bundleId} application`; if (err instanceof bluebird_1.TimeoutError) { errMessage += `. Consider increasing the value of 'appPushTimeout' capability (the current value equals to ${timeoutMs}ms)`; } errMessage += `. Original error: ${err.message}`; throw new Error(errMessage); } finally { afcService.close(); } this.log.info(`The installation of '${bundleId}' succeeded after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); } /** * @param {string} bundlePathOnPhone * @param {InstallOrUpgradeOptions} opts */ async installOrUpgradeApplication(bundlePathOnPhone, { isUpgrade, timeout }) { const notificationService = await appium_ios_device_1.services.startNotificationProxyService(this.udid); const installationService = await appium_ios_device_1.services.startInstallationProxyService(this.udid); const appInstalledNotification = new bluebird_1.default((resolve) => { notificationService.observeNotification(APPLICATION_INSTALLED_NOTIFICATION, { notification: resolve, }); }); const clientOptions = { PackageType: 'Developer' }; try { if (isUpgrade) { this.log.debug(`An upgrade of the existing application is going to be performed. ` + `Will timeout in ${timeout.toFixed(0)} ms`); await installationService.upgradeApplication(bundlePathOnPhone, clientOptions, timeout); } else { this.log.debug(`A new application installation is going to be performed. ` + `Will timeout in ${timeout.toFixed(0)} ms`); await installationService.installApplication(bundlePathOnPhone, clientOptions, timeout); } try { await appInstalledNotification.timeout(APPLICATION_NOTIFICATION_TIMEOUT_MS, `Could not get the application installed notification within ` + `${APPLICATION_NOTIFICATION_TIMEOUT_MS}ms but we will continue`); } catch (e) { this.log.warn(e.message); } } finally { installationService.close(); notificationService.close(); } } /** * Alias for {@linkcode install} * @param {string} appPath * @param {string} bundleId * @param {InstallOptions} [opts={}] */ async installApp(appPath, bundleId, opts = {}) { return await this.install(appPath, bundleId, opts); } /** * Return an application object if test app has 'bundleid'. * The target bundleid can be User and System apps. * * @param {string} bundleId The bundleId to ensure it is installed * @return {Promise<boolean>} Returns True if the app is installed * on the device under test. */ async isAppInstalled(bundleId) { return Boolean(await this.fetchAppInfo(bundleId)); } /** * Fetches various attributes, like bundle id, version, entitlements etc. of * an installed application. * * @param {string} bundleId the bundle identifier of an app to check * @param {string|string[]|undefined} returnAttributes If provided then * only fetches the requested attributes of the app into the resulting object. * Some apps may have too many attributes, so it makes sense to limit these * by default if you don't need all of them. * @returns {Promise<Object|undefined>} Either app info as an object or undefined * if the app is not found. */ async fetchAppInfo(bundleId, returnAttributes = ['CFBundleIdentifier', 'CFBundleVersion']) { const service = await appium_ios_device_1.services.startInstallationProxyService(this.udid); try { return (await service.lookupApplications({ bundleIds: bundleId, // https://github.com/appium/appium/issues/18753 returnAttributes, }))[bundleId]; } finally { service.close(); } } /** * @param {string} bundleId * @param {string} platformVersion * @returns {Promise<boolean>} */ async terminateApp(bundleId, platformVersion) { let instrumentService; let installProxyService; try { installProxyService = await appium_ios_device_1.services.startInstallationProxyService(this.udid); const apps = await installProxyService.listApplications({ returnAttributes: ['CFBundleIdentifier', 'CFBundleExecutable'] }); if (!apps[bundleId]) { this.log.info(`The bundle id '${bundleId}' did not exist`); return false; } const executableName = apps[bundleId].CFBundleExecutable; this.log.debug(`The executable name for the bundle id '${bundleId}' was '${executableName}'`); // 'devicectl' has overhead (generally?) than the instrument service via appium-ios-device, // so hre uses the 'devicectl' only for iOS 17+. if (support_1.util.compareVersions(platformVersion, '>=', '17.0')) { this.log.debug(`Calling devicectl to kill the process`); const pids = (await this.devicectl.listProcesses()) .filter(({ executable }) => executable.endsWith(`/${executableName}`)) .map(({ processIdentifier }) => processIdentifier); if (lodash_1.default.isEmpty(pids)) { this.log.info(`The process of the bundle id '${bundleId}' was not running`); return false; } await this.devicectl.sendSignalToProcess(pids[0], 2); } else { instrumentService = await appium_ios_device_1.services.startInstrumentService(this.udid); // The result of "runningProcesses" includes `bundle_id` key in iOS 16+ (possibly a specific 16.x+) // then here may not be necessary to find a process with `CFBundleExecutable` // after dropping older iOS version support. const processes = await instrumentService.callChannel(appium_ios_device_1.INSTRUMENT_CHANNEL.DEVICE_INFO, 'runningProcesses'); const process = processes.selector.find((process) => process.name === executableName); if (!process) { this.log.info(`The process of the bundle id '${bundleId}' was not running`); return false; } await instrumentService.callChannel(appium_ios_device_1.INSTRUMENT_CHANNEL.PROCESS_CONTROL, 'killPid:', `${process.pid}`); } } catch (err) { this.log.warn(`Failed to kill '${bundleId}'. Original error: ${err.stderr || err.message}`); return false; } finally { if (installProxyService) { installProxyService.close(); } if (instrumentService) { instrumentService.close(); } } return true; } /** * @param {string} bundleName The name of CFBundleName in Info.plist * * @returns {Promise<string[]>} A list of User level apps' bundle ids which has * 'CFBundleName' attribute as 'bundleName'. */ async getUserInstalledBundleIdsByBundleName(bundleName) { const service = await appium_ios_device_1.services.startInstallationProxyService(this.udid); try { const applications = await service.listApplications({ applicationType: 'User', returnAttributes: ['CFBundleIdentifier', 'CFBundleName'] }); return lodash_1.default.reduce(applications, (acc, { CFBundleName }, key) => { if (CFBundleName === bundleName) { acc.push(key); } return acc; }, /** @type {string[]} */ ([])); } finally { service.close(); } } /** * @returns {Promise<string>} */ async getPlatformVersion() { return await appium_ios_device_1.utilities.getOSVersion(this.udid); } /** * @param {import('./driver').XCUITestDriverOpts} opts * @returns {Promise<void>} */ async reset({ bundleId, fullReset }) { if (!bundleId || !fullReset || bundleId === app_utils_1.SAFARI_BUNDLE_ID) { // Safari cannot be removed as system app. // Safari process handling will be managed by WDA // with noReset, forceAppLaunch or shouldTerminateApp capabilities. return; } this.log.debug(`Reset: fullReset requested. Will try to uninstall the app '${bundleId}'.`); if (!(await this.isAppInstalled(bundleId))) { this.log.debug('Reset: app not installed. No need to uninstall'); return; } try { await this.remove(bundleId); } catch (err) { this.log.error(`Reset: could not remove '${bundleId}' from device: ${err.message}`); throw err; } this.log.debug(`Reset: removed '${bundleId}'`); } } exports.RealDevice = RealDevice; exports.default = RealDevice; //# sourceMappingURL=real-device.js.map