appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
347 lines • 14.8 kB
JavaScript
;
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