UNPKG

appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

1,043 lines 87.5 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.XCUITestDriver = void 0; const appium_idb_1 = __importDefault(require("appium-idb")); const appium_ios_simulator_1 = require("appium-ios-simulator"); const appium_webdriveragent_1 = require("appium-webdriveragent"); const driver_1 = require("appium/driver"); const support_1 = require("appium/support"); const async_lock_1 = __importDefault(require("async-lock")); const asyncbox_1 = require("asyncbox"); const bluebird_1 = __importDefault(require("bluebird")); const lodash_1 = __importDefault(require("lodash")); const lru_cache_1 = require("lru-cache"); const node_events_1 = __importDefault(require("node:events")); const node_path_1 = __importDefault(require("node:path")); const node_url_1 = __importDefault(require("node:url")); const app_utils_1 = require("./app-utils"); const activeAppInfoCommands = __importStar(require("./commands/active-app-info")); const alertCommands = __importStar(require("./commands/alert")); const appManagementCommands = __importStar(require("./commands/app-management")); const appearanceCommands = __importStar(require("./commands/appearance")); const appStringsCommands = __importStar(require("./commands/app-strings")); const auditCommands = __importStar(require("./commands/audit")); const batteryCommands = __importStar(require("./commands/battery")); const biometricCommands = __importStar(require("./commands/biometric")); const certificateCommands = __importStar(require("./commands/certificate")); const clipboardCommands = __importStar(require("./commands/clipboard")); const conditionCommands = __importStar(require("./commands/condition")); const contentSizeCommands = __importStar(require("./commands/content-size")); const contextCommands = __importStar(require("./commands/context")); const deviceInfoCommands = __importStar(require("./commands/device-info")); const elementCommands = __importStar(require("./commands/element")); const executeCommands = __importStar(require("./commands/execute")); const fileMovementCommands = __importStar(require("./commands/file-movement")); const findCommands = __importStar(require("./commands/find")); const generalCommands = __importStar(require("./commands/general")); const geolocationCommands = __importStar(require("./commands/geolocation")); const gestureCommands = __importStar(require("./commands/gesture")); const iohidCommands = __importStar(require("./commands/iohid")); const keychainsCommands = __importStar(require("./commands/keychains")); const keyboardCommands = __importStar(require("./commands/keyboard")); const localizationCommands = __importStar(require("./commands/localization")); const locationCommands = __importStar(require("./commands/location")); const lockCommands = __importStar(require("./commands/lock")); const logCommands = __importStar(require("./commands/log")); const memoryCommands = __importStar(require("./commands/memory")); const navigationCommands = __importStar(require("./commands/navigation")); const notificationsCommands = __importStar(require("./commands/notifications")); const pasteboardCommands = __importStar(require("./commands/pasteboard")); const pcapCommands = __importStar(require("./commands/pcap")); const performanceCommands = __importStar(require("./commands/performance")); const permissionsCommands = __importStar(require("./commands/permissions")); const proxyHelperCommands = __importStar(require("./commands/proxy-helper")); const recordAudioCommands = __importStar(require("./commands/record-audio")); const recordScreenCommands = __importStar(require("./commands/recordscreen")); const screenshotCommands = __importStar(require("./commands/screenshots")); const sourceCommands = __importStar(require("./commands/source")); const simctlCommands = __importStar(require("./commands/simctl")); const timeoutCommands = __importStar(require("./commands/timeouts")); const webCommands = __importStar(require("./commands/web")); const xctestCommands = __importStar(require("./commands/xctest")); const xctestRecordScreenCommands = __importStar(require("./commands/xctest-record-screen")); const increaseContrastCommands = __importStar(require("./commands/increase-contrast")); const desired_caps_1 = require("./desired-caps"); const device_connections_factory_1 = require("./device/device-connections-factory"); const execute_method_map_1 = require("./execute-method-map"); const method_map_1 = require("./method-map"); const py_ios_device_client_1 = require("./device/clients/py-ios-device-client"); const real_device_management_1 = require("./device/real-device-management"); const simulator_management_1 = require("./device/simulator-management"); const utils_1 = require("./utils"); const app_infos_cache_1 = require("./app-infos-cache"); const context_1 = require("./commands/context"); const SHUTDOWN_OTHER_FEAT_NAME = 'shutdown_other_sims'; const CUSTOMIZE_RESULT_BUNDLE_PATH = 'customize_result_bundle_path'; const defaultServerCaps = { webStorageEnabled: false, locationContextEnabled: false, browserName: '', platform: 'MAC', javascriptEnabled: true, databaseEnabled: false, takesScreenshot: true, networkConnectionEnabled: false, }; const WDA_SIM_STARTUP_RETRIES = 2; const WDA_REAL_DEV_STARTUP_RETRIES = 1; const WDA_REAL_DEV_TUTORIAL_URL = 'https://appium.github.io/appium-xcuitest-driver/latest/preparation/real-device-config/'; const WDA_STARTUP_RETRY_INTERVAL = 10000; const DEFAULT_SETTINGS = { nativeWebTap: false, nativeWebTapStrict: false, useJSONSource: false, webScreenshotMode: 'native', shouldUseCompactResponses: true, elementResponseAttributes: 'type,label', // Read https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Utilities/FBConfiguration.m for following settings' values mjpegServerScreenshotQuality: 25, mjpegServerFramerate: 10, screenshotQuality: 1, mjpegScalingFactor: 100, // set `reduceMotion` to `null` so that it will be verified but still set either true/false reduceMotion: null, pageSourceExcludedAttributes: '' }; // This lock assures, that each driver session does not // affect shared resources of the other parallel sessions const SHARED_RESOURCES_GUARD = new async_lock_1.default(); const WEB_ELEMENTS_CACHE_SIZE = 500; const SUPPORTED_ORIENATIONS = ['LANDSCAPE', 'PORTRAIT']; const DEFAULT_MJPEG_SERVER_PORT = 9100; /* eslint-disable no-useless-escape */ const NO_PROXY_NATIVE_LIST = [ ['DELETE', /window/], ['GET', /^\/session\/[^\/]+$/], ['GET', /alert_text/], ['GET', /alert\/[^\/]+/], ['GET', /appium/], ['GET', /attribute/], ['GET', /context/], ['GET', /location/], ['GET', /log/], ['GET', /screenshot/], ['GET', /size/], ['GET', /source/], ['GET', /timeouts$/], ['GET', /url/], ['GET', /window/], ['POST', /accept_alert/], ['POST', /actions$/], ['DELETE', /actions$/], ['POST', /alert_text/], ['POST', /alert\/[^\/]+/], ['POST', /appium/], ['POST', /appium\/device\/is_locked/], ['POST', /appium\/device\/lock/], ['POST', /appium\/device\/unlock/], ['POST', /back/], ['POST', /clear/], ['POST', /context/], ['POST', /dismiss_alert/], ['POST', /element\/active/], // MJSONWP get active element should proxy ['POST', /element$/], ['POST', /elements$/], ['POST', /execute/], ['POST', /keys/], ['POST', /log/], ['POST', /receive_async_response/], // always, in case context switches while waiting ['POST', /session\/[^\/]+\/location/], // geo location, but not element location ['POST', /shake/], ['POST', /timeouts/], ['POST', /url/], ['POST', /value/], ['POST', /window/], ['DELETE', /cookie/], ['GET', /cookie/], ['POST', /cookie/], ]; const NO_PROXY_WEB_LIST = [ ['GET', /attribute/], ['GET', /element/], ['GET', /text/], ['GET', /title/], ['POST', /clear/], ['POST', /click/], ['POST', /element/], ['POST', /forward/], ['POST', /frame/], ['POST', /keys/], ['POST', /refresh/], ...NO_PROXY_NATIVE_LIST, ]; /* eslint-enable no-useless-escape */ const MEMOIZED_FUNCTIONS = ['getStatusBarHeight', 'getDevicePixelRatio', 'getScreenInfo']; // Capabilities that do not have xcodebuild process const CAP_NAMES_NO_XCODEBUILD_REQUIRED = ['webDriverAgentUrl', 'usePreinstalledWDA']; class XCUITestDriver extends driver_1.BaseDriver { static newMethodMap = method_map_1.newMethodMap; static executeMethodMap = execute_method_map_1.executeMethodMap; curWindowHandle; selectingNewPage; contexts; curContext; curWebFrames; webviewCalibrationResult; asyncPromise; asyncWaitMs; _syslogWebsocketListener; _perfRecorders; webElementsCache; _conditionInducerService; // needs types _remoteXPCConditionInducerConnection; // RemoteXPC DVT connection for iOS>=18 condition inducer _isSafariIphone; _isSafariNotched; _waitingAtoms; lifecycleData; _audioRecorder; xcodeVersion; _trafficCapture; _recentScreenRecorder; _device; _iosSdkVersion; _wda; _remote; logs; _bidiServerLogListener; // Additional properties that were missing appInfosCache; doesSupportBidi; jwpProxyActive; proxyReqRes; safari; cachedWdaStatus; _currentUrl; pageLoadMs; landscapeWebCoordsOffset; mjpegStream; constructor(opts, shouldValidateCaps = true) { super(opts, shouldValidateCaps); this.locatorStrategies = [ 'xpath', 'id', 'name', 'class name', '-ios predicate string', '-ios class chain', 'accessibility id', 'css selector', ]; this.webLocatorStrategies = [ 'link text', 'css selector', 'tag name', 'link text', 'partial link text', ]; this.curWebFrames = []; this._perfRecorders = []; this.desiredCapConstraints = desired_caps_1.desiredCapConstraints; this.webElementsCache = new lru_cache_1.LRUCache({ max: WEB_ELEMENTS_CACHE_SIZE, }); this.webviewCalibrationResult = null; this._waitingAtoms = { count: 0, alertNotifier: new node_events_1.default(), alertMonitor: bluebird_1.default.resolve(), }; this.resetIos(); this.settings = new driver_1.DeviceSettings(DEFAULT_SETTINGS, this.onSettingsUpdate.bind(this)); this.logs = {}; this._trafficCapture = null; // memoize functions here, so that they are done on a per-instance basis for (const fn of MEMOIZED_FUNCTIONS) { this[fn] = lodash_1.default.memoize(this[fn]); } this.lifecycleData = {}; this._audioRecorder = null; this.appInfosCache = new app_infos_cache_1.AppInfosCache(this.log); this._remote = null; this.doesSupportBidi = true; this._wda = null; } // Override methods from BaseDriver async createSession(w3cCaps1, w3cCaps2, w3cCaps3, driverData) { try { const [sessionId, initialCaps] = await super.createSession(w3cCaps1, w3cCaps2, w3cCaps3, driverData); let caps = initialCaps; // merge cli args to opts, and if we did merge any, revalidate opts to ensure the final set // is also consistent if (this.mergeCliArgsToOpts()) { this.validateDesiredCaps({ ...caps, ...this.cliArgs }); } await this.start(); // merge server capabilities + desired capabilities caps = { ...defaultServerCaps, ...caps }; // update the udid with what is actually used caps.udid = this.opts.udid; // ensure we track nativeWebTap capability as a setting as well if (lodash_1.default.has(this.opts, 'nativeWebTap')) { await this.updateSettings({ nativeWebTap: this.opts.nativeWebTap }); } // ensure we track nativeWebTapStrict capability as a setting as well if (lodash_1.default.has(this.opts, 'nativeWebTapStrict')) { await this.updateSettings({ nativeWebTapStrict: this.opts.nativeWebTapStrict }); } // ensure we track useJSONSource capability as a setting as well if (lodash_1.default.has(this.opts, 'useJSONSource')) { await this.updateSettings({ useJSONSource: this.opts.useJSONSource }); } const wdaSettings = { elementResponseAttributes: DEFAULT_SETTINGS.elementResponseAttributes, shouldUseCompactResponses: DEFAULT_SETTINGS.shouldUseCompactResponses, }; if ('elementResponseAttributes' in this.opts && lodash_1.default.isString(this.opts.elementResponseAttributes)) { wdaSettings.elementResponseAttributes = this.opts.elementResponseAttributes; } if ('shouldUseCompactResponses' in this.opts && lodash_1.default.isBoolean(this.opts.shouldUseCompactResponses)) { wdaSettings.shouldUseCompactResponses = this.opts.shouldUseCompactResponses; } if ('mjpegServerScreenshotQuality' in this.opts && lodash_1.default.isNumber(this.opts.mjpegServerScreenshotQuality)) { wdaSettings.mjpegServerScreenshotQuality = this.opts.mjpegServerScreenshotQuality; } if ('mjpegServerFramerate' in this.opts && lodash_1.default.isNumber(this.opts.mjpegServerFramerate)) { wdaSettings.mjpegServerFramerate = this.opts.mjpegServerFramerate; } if (lodash_1.default.has(this.opts, 'screenshotQuality')) { this.log.info(`Setting the quality of phone screenshot: '${this.opts.screenshotQuality}'`); wdaSettings.screenshotQuality = this.opts.screenshotQuality; } // ensure WDA gets our defaults instead of whatever its own might be await this.updateSettings(wdaSettings); await this.handleMjpegOptions(); return [ sessionId, caps, ]; } catch (e) { this.log.error(JSON.stringify(e)); await this.deleteSession(); throw e; } } async deleteSession(sessionId) { await utils_1.removeAllSessionWebSocketHandlers.bind(this)(); for (const recorder of lodash_1.default.compact([ this._recentScreenRecorder, this._audioRecorder, this._trafficCapture, ])) { await recorder.interrupt(true); await recorder.cleanup(); } if (!lodash_1.default.isEmpty(this._perfRecorders)) { await bluebird_1.default.all(this._perfRecorders.map((x) => x.stop(true))); this._perfRecorders = []; } if (this._conditionInducerService || this._remoteXPCConditionInducerConnection) { try { await this.disableConditionInducer(); } catch (err) { this.log.warn(`Cannot disable condition inducer: ${err.message}`); } } await this.stop(); if (this._wda && this.isXcodebuildNeeded()) { if (this.opts.clearSystemFiles) { let synchronizationKey = XCUITestDriver.name; const derivedDataPath = await this.wda.retrieveDerivedDataPath(); if (derivedDataPath) { synchronizationKey = node_path_1.default.normalize(derivedDataPath); } await SHARED_RESOURCES_GUARD.acquire(synchronizationKey, async () => { await (0, utils_1.clearSystemFiles)(this.wda); }); } else { this.log.debug('Not clearing log files. Use `clearSystemFiles` capability to turn on.'); } } if (this._remote) { this.log.debug('Found a remote debugger session. Removing...'); await this.stopRemote(); } if (this.opts.resetOnSessionStartOnly === false) { await this.runReset(true); } const simulatorDevice = this.isSimulator() ? this.device : null; if (simulatorDevice && this.lifecycleData.createSim) { this.log.debug(`Deleting simulator created for this run (udid: '${simulatorDevice.udid}')`); await simulator_management_1.shutdownSimulator.bind(this)(); await simulatorDevice.delete(); } const shouldResetLocationService = this.isRealDevice() && !!this.opts.resetLocationService; if (shouldResetLocationService) { try { await this.mobileResetLocationService(); } catch { /* Ignore this error since mobileResetLocationService already logged the error */ } } await this.logs.syslog?.stopCapture(); lodash_1.default.values(this.logs).forEach((x) => x?.removeAllListeners?.()); if (this._bidiServerLogListener) { this.log.unwrap().off('log', this._bidiServerLogListener); } this.logs = {}; if (this.mjpegStream) { this.log.info('Closing MJPEG stream'); this.mjpegStream.stop(); } this.resetIos(); await super.deleteSession(sessionId); } async executeCommand(cmd, ...args) { this.log.debug(`Executing command '${cmd}'`); if (cmd === 'receiveAsyncResponse') { return await this.receiveAsyncResponse(args[0], args[1]); } // TODO: once this fix gets into base driver remove from here if (cmd === 'getStatus') { return await this.getStatus(); } return await super.executeCommand(cmd, ...args); } proxyActive() { return Boolean(this.jwpProxyActive); } getProxyAvoidList() { if (this.isWebview()) { return NO_PROXY_WEB_LIST; } return NO_PROXY_NATIVE_LIST; } canProxy() { return true; } validateLocatorStrategy(strategy) { super.validateLocatorStrategy(strategy, this.isWebContext()); } validateDesiredCaps(caps) { if (!super.validateDesiredCaps(caps)) { return false; } // make sure that the capabilities have one of `app` or `bundleId` if (lodash_1.default.toLower(caps.browserName) !== 'safari' && !caps.app && !caps.bundleId) { this.log.info('The desired capabilities include neither an app nor a bundleId. ' + 'WebDriverAgent will be started without the default app'); } if (!support_1.util.coerceVersion(String(caps.platformVersion), false)) { this.log.warn(`'platformVersion' capability ('${caps.platformVersion}') is not a valid version number. ` + `Consider fixing it or be ready to experience an inconsistent driver behavior.`); } const verifyProcessArgument = (processArguments) => { const { args, env } = processArguments; if (!lodash_1.default.isNil(args) && !lodash_1.default.isArray(args)) { throw this.log.errorWithException('processArguments.args must be an array of strings'); } if (!lodash_1.default.isNil(env) && !lodash_1.default.isPlainObject(env)) { throw this.log.errorWithException('processArguments.env must be an object <key,value> pair {a:b, c:d}'); } }; // `processArguments` should be JSON string or an object with arguments and/ environment details if (caps.processArguments) { if (lodash_1.default.isString(caps.processArguments)) { try { // try to parse the string as JSON caps.processArguments = JSON.parse(caps.processArguments); verifyProcessArgument(caps.processArguments); } catch (err) { throw this.log.errorWithException(`processArguments must be a JSON format or an object with format {args : [], env : {a:b, c:d}}. ` + `Both environment and argument can be null. Error: ${err}`); } } else if (lodash_1.default.isPlainObject(caps.processArguments)) { verifyProcessArgument(caps.processArguments); } else { throw this.log.errorWithException(`'processArguments must be an object, or a string JSON object with format {args : [], env : {a:b, c:d}}. ` + `Both environment and argument can be null.`); } } // there is no point in having `keychainPath` without `keychainPassword` if ((caps.keychainPath && !caps.keychainPassword) || (!caps.keychainPath && caps.keychainPassword)) { throw this.log.errorWithException(`If 'keychainPath' is set, 'keychainPassword' must also be set (and vice versa).`); } // `resetOnSessionStartOnly` should be set to true by default this.opts.resetOnSessionStartOnly = !support_1.util.hasValue(this.opts.resetOnSessionStartOnly) || this.opts.resetOnSessionStartOnly; this.opts.useNewWDA = support_1.util.hasValue(this.opts.useNewWDA) ? this.opts.useNewWDA : false; if (caps.commandTimeouts) { caps.commandTimeouts = (0, utils_1.normalizeCommandTimeouts)(caps.commandTimeouts); } if (lodash_1.default.isString(caps.webDriverAgentUrl)) { const { protocol, host } = node_url_1.default.parse(caps.webDriverAgentUrl); if (lodash_1.default.isEmpty(protocol) || lodash_1.default.isEmpty(host)) { throw this.log.errorWithException(`'webDriverAgentUrl' capability is expected to contain a valid WebDriverAgent server URL. ` + `'${caps.webDriverAgentUrl}' is given instead`); } } if (caps.browserName) { if (caps.bundleId) { throw this.log.errorWithException(`'browserName' cannot be set together with 'bundleId' capability`); } // warn if the capabilities have both `app` and `browser, although this // is common with selenium grid if (caps.app) { this.log.warn(`The capabilities should generally not include both an 'app' and a 'browserName'`); } } if (caps.permissions) { try { for (const [bundleId, perms] of lodash_1.default.toPairs(JSON.parse(caps.permissions))) { if (!lodash_1.default.isString(bundleId)) { throw new Error(`'${JSON.stringify(bundleId)}' must be a string`); } if (!lodash_1.default.isPlainObject(perms)) { throw new Error(`'${JSON.stringify(perms)}' must be a JSON object`); } } } catch (e) { throw this.log.errorWithException(`'${caps.permissions}' is expected to be a valid object with format ` + `{"<bundleId1>": {"<serviceName1>": "<serviceStatus1>", ...}, ...}. Original error: ${e.message}`); } } if (caps.platformVersion && !support_1.util.coerceVersion(caps.platformVersion, false)) { throw this.log.errorWithException(`'platformVersion' must be a valid version number. ` + `'${caps.platformVersion}' is given instead.`); } // additionalWebviewBundleIds is an array, JSON array, or string if (caps.additionalWebviewBundleIds) { caps.additionalWebviewBundleIds = this.helpers.parseCapsArray(caps.additionalWebviewBundleIds); } // finally, return true since the superclass check passed, as did this return true; } // Getter methods get wda() { if (!this._wda) { throw new Error('WebDriverAgent is not initialized'); } return this._wda; } get remote() { if (!this._remote) { throw new Error('Remote debugger is not initialized'); } return this._remote; } get driverData() { // TODO fill out resource info here return {}; } get device() { return this._device; } // Utility methods isSafari() { return !!this.safari; } isRealDevice() { return 'devicectl' in (this.device ?? {}); } isSimulator() { return 'simctl' in (this.device ?? {}); } isXcodebuildNeeded() { return !(CAP_NAMES_NO_XCODEBUILD_REQUIRED.some((x) => Boolean(this.opts[x]))); } // Core driver methods async onSettingsUpdate(key, value) { // skip sending the update request to the WDA nor saving it in opts // to not spend unnecessary time. if (['pageSourceExcludedAttributes'].includes(key)) { return; } if (key !== 'nativeWebTap' && key !== 'nativeWebTapStrict') { return await this.proxyCommand('/appium/settings', 'POST', { settings: { [key]: value }, }); } this.opts[key] = !!value; } async getStatus() { const status = { ready: true, message: 'The driver is ready to accept new connections', build: await (0, utils_1.getDriverInfo)(), }; if (this.cachedWdaStatus) { status.wda = this.cachedWdaStatus; } return status; } mergeCliArgsToOpts() { let didMerge = false; // this.cliArgs should never include anything we do not expect. for (const [key, value] of Object.entries(this.cliArgs ?? {})) { if (lodash_1.default.has(this.opts, key)) { this.log.info(`CLI arg '${key}' with value '${value}' overwrites value '${this.opts[key]}' sent in via caps)`); didMerge = true; } this.opts[key] = value; } return didMerge; } async handleMjpegOptions() { await this.allocateMjpegServerPort(); // turn on mjpeg stream reading if requested if (this.opts.mjpegScreenshotUrl) { this.log.info(`Starting MJPEG stream reading URL: '${this.opts.mjpegScreenshotUrl}'`); this.mjpegStream = new support_1.mjpeg.MJpegStream(this.opts.mjpegScreenshotUrl); await this.mjpegStream.start(); } } async allocateMjpegServerPort() { const mjpegServerPort = this.opts.mjpegServerPort || DEFAULT_MJPEG_SERVER_PORT; this.log.debug(`Forwarding MJPEG server port ${mjpegServerPort} to local port ${mjpegServerPort}`); try { await device_connections_factory_1.DEVICE_CONNECTIONS_FACTORY.requestConnection(this.opts.udid, mjpegServerPort, { devicePort: mjpegServerPort, usePortForwarding: this.isRealDevice(), }); } catch (error) { if (lodash_1.default.isUndefined(this.opts.mjpegServerPort)) { this.log.warn(`Cannot forward the device port ${DEFAULT_MJPEG_SERVER_PORT} to the local port ${DEFAULT_MJPEG_SERVER_PORT}. ` + `Certain features, like MJPEG-based screen recording, will be unavailable during this session. ` + `Try to customize the value of 'mjpegServerPort' capability as a possible solution`); } else { this.log.debug(error.stack); throw new Error(`Cannot ensure MJPEG broadcast functionality by forwarding the local port ${mjpegServerPort} ` + `requested by the 'mjpegServerPort' capability to the device port ${mjpegServerPort}. ` + `Original error: ${error}`); } } } getDefaultUrl() { // Setting this to some external URL slows down the session init return `${this.getWdaLocalhostRoot()}/health`; } async start() { this.opts.noReset = !!this.opts.noReset; this.opts.fullReset = !!this.opts.fullReset; await (0, utils_1.printUser)(); this._iosSdkVersion = null; // For WDA and xcodebuild const { device, udid, realDevice } = await this.determineDevice(); this.log.info(`Determining device to run tests on: udid: '${udid}', real device: ${realDevice}`); this._device = device; this.opts.udid = udid; if (this.opts.simulatorDevicesSetPath) { if (realDevice) { this.log.info(`The 'simulatorDevicesSetPath' capability is only supported for Simulator devices`); } else { this.log.info(`Setting simulator devices set path to '${this.opts.simulatorDevicesSetPath}'`); this.device.devicesSetPath = this.opts.simulatorDevicesSetPath; } } // at this point if there is no platformVersion, get it from the device if (!this.opts.platformVersion) { this.opts.platformVersion = await this.device.getPlatformVersion(); this.log.info(`No platformVersion specified. Using device version: '${this.opts.platformVersion}'`); } const normalizedVersion = (0, utils_1.normalizePlatformVersion)(this.opts.platformVersion); if (this.opts.platformVersion !== normalizedVersion) { this.log.info(`Normalized platformVersion capability value '${this.opts.platformVersion}' to '${normalizedVersion}'`); this.opts.platformVersion = normalizedVersion; } this.caps.platformVersion = this.opts.platformVersion; if (lodash_1.default.isEmpty(this.xcodeVersion) && (this.isXcodebuildNeeded() || this.isSimulator())) { // no `webDriverAgentUrl`, or on a simulator, so we need an Xcode version this.xcodeVersion = await (0, utils_1.getAndCheckXcodeVersion)(); } this.logEvent('xcodeDetailsRetrieved'); if (lodash_1.default.toLower(this.opts.browserName) === 'safari') { this.log.info('Safari test requested'); this.safari = true; this.opts.app = undefined; this.opts.processArguments = this.opts.processArguments || {}; real_device_management_1.applySafariStartupArgs.bind(this)(); this.opts.bundleId = app_utils_1.SAFARI_BUNDLE_ID; this._currentUrl = this.opts.safariInitialUrl || this.getDefaultUrl(); } else if (this.opts.app || this.opts.bundleId) { await this.configureApp(); } this.logEvent('appConfigured'); // fail very early if the app doesn't actually exist // or if bundle id doesn't point to an installed app if (this.opts.app) { await (0, utils_1.checkAppPresent)(this.opts.app); if (!this.opts.bundleId) { this.opts.bundleId = await this.appInfosCache.extractBundleId(this.opts.app); } } await this.runReset(); this._wda = new appium_webdriveragent_1.WebDriverAgent({ ...this.opts, device: this.device, realDevice: this.isRealDevice(), iosSdkVersion: this._iosSdkVersion ?? undefined, reqBasePath: this.basePath, }, this.log); // Derived data path retrieval is an expensive operation // We could start that now in background and get the cached result // whenever it is needed (async () => { try { await this.wda.retrieveDerivedDataPath(); } catch (e) { this.log.debug(e); } })(); const memoizedLogInfo = lodash_1.default.memoize(() => { this.log.info("'skipLogCapture' is set. Skipping starting logs such as crash, system, safari console and safari network."); }); const startLogCapture = async () => { if (this.opts.skipLogCapture) { memoizedLogInfo(); return false; } const result = await this.startLogCapture(); if (result) { this.logEvent('logCaptureStarted'); } return result; }; const isLogCaptureStarted = await startLogCapture(); this.log.info(`Setting up ${this.isRealDevice() ? 'real device' : 'simulator'}`); if (this.isSimulator()) { await this.initSimulator(); if (!isLogCaptureStarted) { // Retry log capture if Simulator was not running before await startLogCapture(); } } else if (this.opts.customSSLCert) { await new py_ios_device_client_1.Pyidevice({ udid, log: this.log, }).installProfile({ payload: this.opts.customSSLCert }); this.logEvent('customCertInstalled'); } await this.installAUT(); // if we only have bundle identifier and no app, fail if it is not already installed if (!this.opts.app && this.opts.bundleId && !this.isSafari() && !(await this.device.isAppInstalled(this.opts.bundleId))) { throw this.log.errorWithException(`App with bundle identifier '${this.opts.bundleId}' unknown`); } if (this.isSimulator()) { if (this.opts.permissions) { this.log.debug('Setting the requested permissions before WDA is started'); for (const [bundleId, permissionsMapping] of lodash_1.default.toPairs(JSON.parse(this.opts.permissions))) { await this.device.setPermissions(bundleId, permissionsMapping); } } // TODO: Deprecate and remove this block together with calendarAccessAuthorized capability if (lodash_1.default.isBoolean(this.opts.calendarAccessAuthorized)) { this.log.warn(`The 'calendarAccessAuthorized' capability is deprecated and will be removed soon. ` + `Consider using 'permissions' one instead with 'calendar' key`); const methodName = `${this.opts.calendarAccessAuthorized ? 'enable' : 'disable'}CalendarAccess`; await this.device[methodName](this.opts.bundleId); } } await this.startWda(); if (lodash_1.default.isString(this.opts.orientation)) { await this.setInitialOrientation(this.opts.orientation); this.logEvent('orientationSet'); } if (this.isSafari() || this.opts.autoWebview) { await this.activateRecentWebview(); } else { // We want to always setup the initial context value upon session startup await context_1.notifyBiDiContextChange.bind(this)(); } if (this.isSafari()) { if ((0, utils_1.shouldSetInitialSafariUrl)(this.opts)) { this.log.info(`About to set the initial Safari URL to '${this.getCurrentUrl()}'`); if (lodash_1.default.isNil(this.opts.safariInitialUrl) && lodash_1.default.isNil(this.opts.initialDeeplinkUrl)) { this.log.info(`Use the 'safariInitialUrl' capability to customize it`); } ; await this.setUrl(this.getCurrentUrl() || this.getDefaultUrl()); } else { const currentUrl = await this.getUrl(); this.log.info(`Current URL: ${currentUrl}`); this.setCurrentUrl(currentUrl); } } } async runReset(enforceSimulatorShutdown = false) { this.logEvent('resetStarted'); if (this.isRealDevice()) { await real_device_management_1.runRealDeviceReset.bind(this)(); } else { await simulator_management_1.runSimulatorReset.bind(this)(enforceSimulatorShutdown); } this.logEvent('resetComplete'); } async stop() { this.jwpProxyActive = false; this.proxyReqRes = null; if (this._wda?.fullyStarted) { if (this.wda.jwproxy) { try { await this.proxyCommand(`/session/${this.sessionId}`, 'DELETE'); } catch (err) { // an error here should not short-circuit the rest of clean up this.log.debug(`Unable to DELETE session on WDA: '${err.message}'. Continuing shutdown.`); } } // The former could cache the xcodebuild, so should not quit the process. // If the session skipped the xcodebuild (this.wda.canSkipXcodebuild), the this.wda instance // should quit properly. if ((!this.wda.webDriverAgentUrl && this.opts.useNewWDA) || this.wda.canSkipXcodebuild) { await this.wda.quit(); } } device_connections_factory_1.DEVICE_CONNECTIONS_FACTORY.releaseConnection(this.opts.udid); } async initSimulator() { const device = this.device; if (this.opts.shutdownOtherSimulators) { this.assertFeatureEnabled(SHUTDOWN_OTHER_FEAT_NAME); await simulator_management_1.shutdownOtherSimulators.bind(this)(); } await this.startSim(); if (this.opts.customSSLCert) { // Simulator must be booted in order to call this helper await device.addCertificate(this.opts.customSSLCert); this.logEvent('customCertInstalled'); } if (await simulator_management_1.setSafariPrefs.bind(this)()) { this.log.debug('Safari preferences have been updated'); } if (await simulator_management_1.setLocalizationPrefs.bind(this)()) { this.log.debug('Localization preferences have been updated'); } const promises = ['reduceMotion', 'reduceTransparency', 'autoFillPasswords'] .filter((optName) => lodash_1.default.isBoolean(this.opts[optName])) .map((optName) => { this.log.info(`Setting ${optName} to ${this.opts[optName]}`); return device[`set${lodash_1.default.upperFirst(optName)}`](this.opts[optName]); }); await bluebird_1.default.all(promises); if (this.opts.launchWithIDB) { try { const idb = new appium_idb_1.default({ udid: this.opts.udid }); await idb.connect(); // @ts-ignore This is ok. We are going to ditch idb soon anyway device.idb = idb; } catch (e) { this.log.debug(e.stack); this.log.warn(`idb will not be used for Simulator interaction. Original error: ${e.message}`); } } this.logEvent('simStarted'); } async startWda() { // Don't cleanup the processes if webDriverAgentUrl is set if (!support_1.util.hasValue(this.wda.webDriverAgentUrl)) { await this.wda.cleanupObsoleteProcesses(); } const usePortForwarding = this.isRealDevice() && !this.wda.webDriverAgentUrl && (0, utils_1.isLocalHost)(this.wda.wdaBaseUrl); await device_connections_factory_1.DEVICE_CONNECTIONS_FACTORY.requestConnection(this.opts.udid, this.wda.url.port, { devicePort: usePortForwarding ? this.wda.wdaRemotePort : null, usePortForwarding, }); // Let multiple WDA binaries with different derived data folders be built in parallel // Concurrent WDA builds from the same source will cause xcodebuild synchronization errors let synchronizationKey = XCUITestDriver.name; if (this.opts.useXctestrunFile || !(await this.wda.isSourceFresh())) { // First-time compilation is an expensive operation, which is done faster if executed // sequentially. Xcodebuild spreads the load caused by the clang compiler to all available CPU cores const derivedDataPath = await this.wda.retrieveDerivedDataPath(); if (derivedDataPath) { synchronizationKey = node_path_1.default.normalize(derivedDataPath); } } this.log.debug(`Starting WebDriverAgent initialization with the synchronization key '${synchronizationKey}'`); if (SHARED_RESOURCES_GUARD.isBusy() && !this.opts.derivedDataPath && !this.opts.bootstrapPath) { this.log.debug(`Consider setting a unique 'derivedDataPath' capability value for each parallel driver instance ` + `to avoid conflicts and speed up the building process`); } if (this.opts.usePreinstalledWDA && this.opts.prebuiltWDAPath && !(await support_1.fs.exists(this.opts.prebuiltWDAPath))) { throw new Error(`The prebuilt WebDriverAgent app at '${this.opts.prebuiltWDAPath}' provided as 'prebuiltWDAPath' ` + `capability value does not exist or is not accessible`); } return await SHARED_RESOURCES_GUARD.acquire(synchronizationKey, async () => { if (this.opts.useNewWDA) { this.log.debug(`Capability 'useNewWDA' set to true, so uninstalling WDA before proceeding`); await this.wda.quitAndUninstall(); this.logEvent('wdaUninstalled'); } else if (!support_1.util.hasValue(this.wda.webDriverAgentUrl) && this.isXcodebuildNeeded()) { await this.wda.setupCaching(); } // local helper for the two places we need to uninstall wda and re-start it const quitAndUninstall = async (msg) => { this.log.debug(msg); if (!this.isXcodebuildNeeded()) { this.log.debug(`Not quitting/uninstalling WebDriverAgent since at least one of ${CAP_NAMES_NO_XCODEBUILD_REQUIRED} capabilities is provided`); throw new Error(msg); } this.log.warn('Quitting and uninstalling WebDriverAgent'); await this.wda.quitAndUninstall(); throw new Error(msg); }; // Used in the following WDA build if (this.opts.resultBundlePath) { this.assertFeatureEnabled(CUSTOMIZE_RESULT_BUNDLE_PATH); } let startupRetries = this.opts.wdaStartupRetries || (this.isRealDevice() ? WDA_REAL_DEV_STARTUP_RETRIES : WDA_SIM_STARTUP_RETRIES); const startupRetryInterval = this.opts.wdaStartupRetryInterval || WDA_STARTUP_RETRY_INTERVAL; // These values help only xcodebuild. if (this.isXcodebuildNeeded()) { this.log.debug(`Trying to start WebDriverAgent ${startupRetries} times with ${startupRetryInterval}ms interval`); if (!support_1.util.hasValue(this.opts.wdaStartupRetries) && !support_1.util.hasValue(this.opts.wdaStartupRetryInterval)) { this.log.debug(`These values can be customized by changing wdaStartupRetries/wdaStartupRetryInterval capabilities`); } } else { // The startup retry will be one time if the session does not need WDA build this.log.debug(`Trying to start WebDriverAgent once since at least one of ${CAP_NAMES_NO_XCODEBUILD_REQUIRED} capabilities is provided`); startupRetries = 1; } let shortCircuitError = null; let retryCount = 0; await (0, asyncbox_1.retryInterval)(startupRetries, startupRetryInterval, async () => { this.logEvent('wdaStartAttempted'); if (retryCount > 0) { this.log.info(`Retrying WDA startup (${retryCount + 1} of ${startupRetries})`); } try { if (this.opts.usePreinstalledWDA) { await this.preparePreinstalledWda(); } if (!this.sessionId) { throw new Error('Session ID is required but was not set'); } this.cachedWdaStatus = await this.wda.launch(this.sessionId); } catch (err) { this.logEvent('wdaStartFailed'); this.log.debug(err.stack); retryCount++; let errorMsg = `Unable to launch WebDriverAgent. Original error: ${err.message}`; if (this.isRealDevice()) { errorMsg += `. Make sure you follow the tutorial at ${WDA_REAL_DEV_TUTORIAL_URL}`; } if (this.opts.usePreinstalledWDA) { try { // In case the bundle id process start got failed because of // auth popup in the device. Then, the bundle id process itself started. It is safe to stop it here. await this.mobileKillApp(this.wda.bundleIdForXctest); } catch { } ; // Mostly it failed to start the WDA process as no the bundle id // e.g. '<bundle id of WDA> not found on device <udid>' errorMsg = `Unable to launch WebDriverAgent. Original error: ${err.message}. ` + `Make sure the application ${this.wda.bundleIdForXctest} exists and it is launchable.`; if (this.isRealDevice()) { errorMsg += ` ${WDA_REAL_DEV_TUTORIAL_URL} may help to complete the preparation.`; } ; throw new Error(errorMsg); } else { await quitAndUninstall(errorMsg); } } this.proxyReqRes = this.wda.proxyReqRes.bind(this.wda); this.jwpProxyActive = true; try { this.logEvent('wdaSessionAttempted'); this.log.debug('Sending createSession command to WDA'); this.cachedWdaStatus = this.cachedWdaStatus || (await this.proxyCommand('/status', 'GET')); await this.startWdaSession(this.opts.bundleId, this.opts.processArguments); this.logEvent('wdaSessionStarted'); } catch (err) { this.logEvent('wdaSessionFailed'); this.log.debug(err.stack); if (err instanceof driver_1.errors.TimeoutError) { // Session startup timed out. There is no point to retry shortCircuitError = err; return; } let errorMsg = `Unable to start WebDriverAgent session. Original error: ${err.message}`; if (this.isRealDevice() && lodash_1.default.includes(err.message, 'xcodebuild')) { errorMsg += ` Make sure you follow the tutorial at ${WDA_REAL_DEV_TUTORIAL_URL}.`; } throw new Error(errorMsg); } if (this.opts.clearSystemFiles && this.isXcodebuildNeeded()) { await (0, utils_1.markSystemFilesForCleanup)(this.wda); } // We don't restrict the version, but show what version of WDA is running on the device for debugging purposes. if (this.cachedWdaStatus?.build) { this.log.info(`WebDriverAgent version: '${this.cachedWdaStatus.build.version}'`); } else { this.log.warn(`WebDriverAgent does not provide any version information. ` + `This might indicate either a custom or an outdated build.`); } // we expect certain socket errors until this point, but now // mark things as fully working this.wda.fullyS