UNPKG

appium-xcuitest-driver-conan

Version:

Appium driver for iOS using XCUITest for backend

896 lines (774 loc) 30.4 kB
import { BaseDriver } from 'appium-base-driver'; import { util } from 'appium-support'; import _ from 'lodash'; import { launch } from 'node-simctl'; import WebDriverAgent from './webdriveragent'; import log from './logger'; import { simBooted, createSim, getExistingSim, runSimulatorReset, installToSimulator } from './simulator-management'; import { killAllSimulators, simExists, getSimulator, installSSLCert, uninstallSSLCert, BOOT_COMPLETED_EVENT } from 'appium-ios-simulator'; import { retryInterval } from 'asyncbox'; import { settings as iosSettings, defaultServerCaps, appUtils, IWDP } from 'appium-ios-driver'; import desiredCapConstraints from './desired-caps'; import commands from './commands/index'; import { detectUdid, getAndCheckXcodeVersion, getAndCheckIosSdkVersion, adjustWDAAttachmentsPermissions, checkAppPresent, getDriverInfo, clearSystemFiles } from './utils'; import { getConnectedDevices, runRealDeviceReset, installToRealDevice, getRealDeviceObj } from './real-device-management'; import B from 'bluebird'; import { version } from '../../package.json'; // eslint-disable-line import/no-unresolved const SAFARI_BUNDLE_ID = 'com.apple.mobilesafari'; const WDA_STARTUP_RETRIES = 2; const DEFAULT_TIMEOUT_KEY = 'default'; const WDA_STARTUP_RETRY_INTERVAL = 10000; const NO_PROXY_NATIVE_LIST = [ ['GET', /^\/session\/[^\/]+$/], ['GET', /context/], ['POST', /context/], ['GET', /window/], ['POST', /window/], ['DELETE', /window/], ['POST', /execute/], ['POST', /element$/], ['POST', /elements$/], ['POST', /timeouts/], ['GET', /alert_text/], ['POST', /alert_text/], ['POST', /accept_alert/], ['POST', /dismiss_alert/], ['GET', /source/], ['GET', /screenshot/], ['POST', /appium/], ['GET', /appium/], ['POST', /touch/], ['GET', /log/], ['POST', /log/], ['POST', /moveto/], ['POST', /receive_async_response/], // always, in case context switches while waiting ['GET', /location/], ['GET', /size/], ['POST', /value/], ['POST', /keys/], ['POST', /back/], ['POST', /session\/[^\/]+\/location/], // geo location, but not element location ['POST', /appium\/device\/lock/], ['POST', /shake/], ['POST', /clear/], ]; const NO_PROXY_WEB_LIST = [ ['GET', /title/], ['GET', /url/], ['POST', /url/], ['POST', /element/], ['POST', /forward/], ['GET', /attribute/], ['GET', /text/], ['POST', /clear/], ['GET', /element/], ['POST', /click/], ['POST', /refresh/], ['GET', /cookie/], ['POST', /cookie/], ['DELETE', /cookie/], ['POST', /frame/], ['POST', /keys/], ].concat(NO_PROXY_NATIVE_LIST); function normalizeCommandTimeouts (value) { // The value is normalized already if (typeof value !== 'string') { return value; } let result = {}; // Use as default timeout for all commands if a single integer value is provided if (!isNaN(value)) { result[DEFAULT_TIMEOUT_KEY] = _.toInteger(value); return result; } // JSON object has been provided. Let's parse it try { result = JSON.parse(value); if (!_.isPlainObject(result)) { throw new Error(); } } catch (err) { log.errorAndThrow(`"commandTimeouts" capability should be a valid JSON object. "${value}" was given instead`); } for (let [cmd, timeout] of _.toPairs(result)) { if (!_.isInteger(timeout) || timeout <= 0) { log.errorAndThrow(`The timeout for "${cmd}" should be a valid natural number of milliseconds. "${timeout}" was given instead`); } } return result; } class XCUITestDriver extends BaseDriver { constructor (opts = {}, shouldValidateCaps = true) { super(opts, shouldValidateCaps); log.debug(`XCUITestDriver version: ${version}`); this.desiredCapConstraints = desiredCapConstraints; this.locatorStrategies = [ 'xpath', 'id', 'name', 'class name', '-ios predicate string', '-ios class chain', 'accessibility id' ]; this.webLocatorStrategies = [ 'link text', 'css selector', 'tag name', 'link text', 'partial link text' ]; this.resetIos(); } resetIos () { this.opts = this.opts || {}; this.wda = null; this.opts.device = null; this.jwpProxyActive = false; this.proxyReqRes = null; this.jwpProxyAvoid = []; this.safari = false; this.cachedWdaStatus = null; // some things that commands imported from appium-ios-driver need this.curWebFrames = []; this.webElementIds = []; this._currentUrl = null; this.curContext = null; this.xcodeVersion = null; this.iosSdkVersion = null; this.contexts = []; this.implicitWaitMs = 0; this.asynclibWaitMs = 0; this.pageLoadMs = 6000; } get driverData () { // TODO fill out resource info here return {}; } async getStatus () { if (typeof this.driverInfo === 'undefined') { this.driverInfo = await getDriverInfo(); } let status = {build: {version: this.driverInfo.version}}; if (this.cachedWdaStatus) { status.wda = this.cachedWdaStatus; } return status; } async createSession (caps) { this.lifecycleData = {}; // this is used for keeping track of the state we start so when we delete the session we can put things back try { // TODO add validation on caps // TODO handle otherSessionData for multiple sessions let [sessionId] = await super.createSession(caps); this.opts.sessionId = sessionId; await this.start(); // merge server capabilities + desired capabilities caps = Object.assign({}, defaultServerCaps, caps); // update the udid with what is actually used caps.udid = this.opts.udid; return [sessionId, caps]; } catch (e) { log.error(e); await this.deleteSession(); throw e; } } async start () { this.opts.noReset = !!this.opts.noReset; this.opts.fullReset = !!this.opts.fullReset; if (!this.xcodeVersion) { this.xcodeVersion = await getAndCheckXcodeVersion(); let tools = !this.xcodeVersion.toolsVersion ? '' : `(tools v${this.xcodeVersion.toolsVersion})`; log.debug(`Xcode version set to '${this.xcodeVersion.versionString}' ${tools}`); } this.iosSdkVersion = await getAndCheckIosSdkVersion(); log.debug(`iOS SDK Version set to '${this.iosSdkVersion}'`); if (this.opts.platformVersion && parseFloat(this.opts.platformVersion) < 9.0) { throw Error(`Platform version must be 9.0 or above. '${this.opts.platformVersion}' is not supported.`); } this.logEvent('xcodeDetailsRetrieved'); let {device, udid, realDevice} = await this.determineDevice(); log.info(`Determining device to run tests on: udid: '${udid}', real device: ${realDevice}`); this.opts.device = device; this.opts.udid = udid; this.opts.realDevice = realDevice; if (this.isSimulator() && this.opts.customSSLCert) { await installSSLCert(this.opts.customSSLCert, this.opts.udid); this.logEvent('customCertInstalled'); } // at this point if there is no platformVersion, get it from the device if (!this.opts.platformVersion) { if (this.opts.device && _.isFunction(this.opts.device.getPlatformVersion)) { this.opts.platformVersion = await this.opts.device.getPlatformVersion(); log.info(`No platformVersion specified. Using device version: '${this.opts.platformVersion}'`); } else { // TODO: this is when it is a real device. when we have a real object wire it in } } // make sure that the xcode we are using can handle the platform // if (parseFloat(this.opts.platformVersion) > parseFloat(this.iosSdkVersion)) { // let msg = `Xcode ${this.xcodeVersion.versionString} has a maximum SDK version of ${this.iosSdkVersion}. ` + // `It does not support iOS version ${this.opts.platformVersion}`; // log.errorAndThrow(msg); // } // if ((this.opts.browserName || '').toLowerCase() === 'safari') { log.info('Safari test requested'); this.safari = true; this.opts.app = undefined; this.opts.processArguments = this.opts.processArguments || {}; this.opts.bundleId = SAFARI_BUNDLE_ID; this._currentUrl = this.opts.safariInitialUrl || ( this.isRealDevice() ? 'http://appium.io' : `http://${this.opts.address}:${this.opts.port}/welcome` ); this.opts.processArguments.args = ['-u', this._currentUrl]; } else { 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 checkAppPresent(this.opts.app); } else if (this.opts.bundleId && !this.safari) { if (!await this.opts.device.isAppInstalled(this.opts.bundleId)) { log.errorAndThrow(`App with bundle identifier '${this.opts.bundleId}' unknown`); } } if (!this.opts.bundleId) { this.opts.bundleId = await this.extractBundleId(this.opts.app); } if (!this.opts.realDevice) { if (typeof this.opts.scaleFactor !== 'undefined') { log.info(`Setting non-default Simulator scale factor to '${this.opts.scaleFactor}'`); device.setScaleFactor(this.opts.scaleFactor); } if (typeof this.opts.connectHardwareKeyboard !== 'undefined') { log.info(`Setting 'connectHardwareKeyboard' Simulator option to '${this.opts.connectHardwareKeyboard ? 'on' : 'off'}'`); device.setConnectHardwareKeyboard(this.opts.connectHardwareKeyboard); } } await this.runReset(); // handle logging await this.startLogCapture(); this.logEvent('logCaptureStarted'); log.info(`Setting up ${this.isRealDevice() ? 'real device' : 'simulator'}`); if (this.isRealDevice()) { if (this.opts.app) { await this.installApp(); this.logEvent('appInstalled'); } } else { this.localeConfig = await iosSettings.setLocale(this.opts.device, this.opts, {}, this.isSafari()); await iosSettings.setPreferences(this.opts.device, this.opts, this.isSafari()); let installAppPromise = null; if (this.opts.app) { if (await simBooted(this.opts.device)) { installAppPromise = this.installApp(); } else { installAppPromise = new B(async (resolve, reject) => { this.opts.device.on(BOOT_COMPLETED_EVENT, async () => { try { await this.installApp(); resolve(); } catch (err) { reject(err); } }); }); } } await this.startSim(); this.logEvent('simStarted'); await installAppPromise; this.logEvent('appInstalled'); } await this.startWda(this.opts.sessionId, realDevice); await this.setInitialOrientation(this.opts.orientation); this.logEvent('orientationSet'); if (this.isRealDevice() && this.opts.startIWDP) { try { await this.startIWDP(); log.debug(`Started ios_webkit_debug proxy server at: ${this.iwdpServer.endpoint}`); } catch (err) { log.errorAndThrow(`Could not start ios_webkit_debug_proxy server: ${err.message}`); } } if (this.isSafari() || this.opts.autoWebview) { log.debug('Waiting for initial webview'); await this.navToInitialWebview(); this.logEvent('initialWebviewNavigated'); } if (!this.isRealDevice()) { if (this.opts.calendarAccessAuthorized) { await this.opts.device.enableCalendarAccess(this.opts.bundleId); } else if (this.opts.calendarAccessAuthorized === false) { await this.opts.device.disableCalendarAccess(this.opts.bundleId); } } } async startWda (sessionId, realDevice) { let startupRetries = this.opts.wdaStartupRetries || WDA_STARTUP_RETRIES; let startupRetryInterval = this.opts.wdaStartupRetryInterval || WDA_STARTUP_RETRY_INTERVAL; await retryInterval(startupRetries, startupRetryInterval, async () => { this.logEvent('wdaStartAttempted'); this.wda = new WebDriverAgent(this.xcodeVersion, this.opts); if (this.opts.useNewWDA) { log.debug(`Capability 'useNewWDA' set, so uninstalling WDA before proceeding`); await this.wda.uninstall(); this.logEvent('wdaUninstalled'); } // local helper for the two places we need to uninstall wda and re-start it let quitAndUninstall = async (msg) => { log.debug(msg); log.debug('Quitting and uninstalling WebDriverAgent, then retrying'); await this.wda.quit(); await this.wda.uninstall(); throw new Error(msg); }; let wdaStatus = null; try { wdaStatus = await this.wda.launch(sessionId, realDevice); } catch (err) { this.logEvent('wdaStartFailed'); await quitAndUninstall(`Unable to launch WebDriverAgent because of xcodebuild failure: ${err.message}`); } this.proxyReqRes = this.wda.proxyReqRes.bind(this.wda); this.jwpProxyActive = true; try { await retryInterval(15, 1000, async () => { this.logEvent('wdaSessionAttempted'); log.debug('Sending createSession command to WDA'); try { if (wdaStatus) { this.cachedWdaStatus = wdaStatus; } else { // This will put the '/status' output into the cache await this.proxyCommand('/status', 'GET'); } await this.startWdaSession(this.opts.bundleId, this.opts.processArguments); } catch (err) { log.debug('Failed to create WDA session. Retrying...'); throw err; } }); this.logEvent('wdaSessionStarted'); } catch (err) { return await quitAndUninstall(`Unable to start WebDriverAgent session: ${err.message}`); } this.opts.preventWDAAttachments = !util.hasValue(this.opts.preventWDAAttachments) || this.opts.preventWDAAttachments; await adjustWDAAttachmentsPermissions(this.opts.preventWDAAttachments ? '555' : '755'); this.logEvent('wdaPermsAdjusted'); // we expect certain socket errors until this point, but now // mark things as fully working this.wda.fullyStarted = true; this.logEvent('wdaStarted'); }); } // create an alias so we can actually unit test createSession by stubbing // this async extractBundleId (app) { return await appUtils.extractBundleId(app); } async runReset (opts = null) { this.logEvent('resetStarted'); if (this.isRealDevice()) { await runRealDeviceReset(this.opts.device, opts || this.opts); } else { await runSimulatorReset(this.opts.device, opts || this.opts); } this.logEvent('resetComplete'); } async deleteSession () { await this.stop(); // reset the permissions on the derived data folder, if necessary if (this.opts.preventWDAAttachments) { await adjustWDAAttachmentsPermissions('755'); } if (this.opts.clearSystemFiles) { await clearSystemFiles(this.wda, !!this.opts.showXcodeLog); } else { log.debug('Not clearing log files. Use `clearSystemFiles` capability to turn on.'); } if (this.isWebContext()) { log.debug('In a web session. Removing remote debugger'); await this.stopRemote(); } if (this.opts.resetOnSessionStartOnly === false) { await this.runReset(); } if (this.isSimulator() && this.opts.udid && this.opts.customSSLCert) { await uninstallSSLCert(this.opts.customSSLCert, this.opts.udid); } if (this.isSimulator() && !this.opts.noReset && !!this.opts.device) { if (this.lifecycleData.createSim) { log.debug('Deleting simulator created for this run'); await this.opts.device.shutdown(); await this.opts.device.delete(); } } if (!_.isEmpty(this.logs)) { this.logs.syslog.stopCapture(); this.logs = {}; } if (this.iwdpServer) { this.stopIWDP(); } this.resetIos(); await super.deleteSession(); } async stop () { this.jwpProxyActive = false; this.proxyReqRes = null; if (this.wda && 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 log.debug(`Unable to DELETE session on WDA: '${err.message}'. Continuing shutdown.`); } } await this.wda.quit(); } } async executeCommand (cmd, ...args) { log.debug(`Executing command '${cmd}'`); if (cmd === 'receiveAsyncResponse') { return await this.receiveAsyncResponse(...args); } // TODO: once this fix gets into base driver remove from here if (cmd === 'getStatus') { return await this.getStatus(); } return await super.executeCommand(cmd, ...args); } async configureApp () { function appIsPackageOrBundle (app) { return (/^([a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+)+$/).test(app); } // the app name is a bundleId assign it to the bundleId property if (!this.opts.bundleId && appIsPackageOrBundle(this.opts.app)) { this.opts.bundleId = this.opts.app; this.opts.app = ''; } // we have a bundle ID, but no app, or app is also a bundle if ((this.opts.bundleId && appIsPackageOrBundle(this.opts.bundleId)) && (this.opts.app === '' || appIsPackageOrBundle(this.opts.app))) { log.debug('App is an iOS bundle, will attempt to run as pre-existing'); return; } // check for supported build-in apps if (this.opts.app && this.opts.app.toLowerCase() === 'settings') { this.opts.bundleId = 'com.apple.Preferences'; this.opts.app = null; return; } else if (this.opts.app && this.opts.app.toLowerCase() === 'calendar') { this.opts.bundleId = 'com.apple.mobilecal'; this.opts.app = null; return; } try { // download if necessary this.opts.app = await this.helpers.configureApp(this.opts.app, '.app', this.opts.mountRoot, this.opts.windowsShareUserName, this.opts.windowsSharePassword); } catch (err) { log.error(err); throw new Error( `Bad app: ${this.opts.app}. App paths need to be absolute, or relative to the appium ` + 'server install dir, or a URL to compressed file, or a special app name.'); } } async determineDevice () { // in the one case where we create a sim, we will set this state this.lifecycleData.createSim = false; // if we get generic names, translate them this.opts.deviceName = (function translateDeviceName (dn = '') { let deviceName = dn; if (dn.toLowerCase() === 'iphone simulator') { deviceName = 'iPhone 6'; } else if (dn.toLowerCase() === 'ipad simulator') { deviceName = 'iPad Retina'; } if (deviceName !== dn) { log.debug(`Changing deviceName from '${dn}' to '${deviceName}'`); } return deviceName; })(this.opts.deviceName); // check for a particular simulator if (this.opts.udid && (await simExists(this.opts.udid))) { let device = await getSimulator(this.opts.udid); return {device, realDevice: false, udid: this.opts.udid}; } // check for a particular real device if (this.opts.udid) { if (this.opts.udid.toLowerCase() === 'auto') { this.opts.udid = await detectUdid(); } else { // make sure it is a connected device. If not, the udid passed in is invalid let devices = await getConnectedDevices(); log.debug(`Available devices: ${devices.join(', ')}`); if (devices.indexOf(this.opts.udid) === -1) { throw new Error(`Unknown device or simulator UDID: '${this.opts.udid}'`); } } let device = await getRealDeviceObj(this.opts.udid); return {device, realDevice: true, udid: this.opts.udid}; } // figure out the correct simulator to use, given the desired capabilities let device = await getExistingSim(this.opts.deviceName, this.opts.platformVersion); // check for an existing simulator if (device) { return {device, realDevice: false, udid: device.udid}; } // no device of this type exists, so create one log.info('Simulator udid not provided, using desired caps to create a new simulator'); if (!this.opts.platformVersion) { log.info(`No platformVersion specified. Using latest version Xcode supports: '${this.iosSdkVersion}' ` + `This may cause problems if a simulator does not exist for this platform version.`); this.opts.platformVersion = this.iosSdkVersion; } device = await this.createSim(); return {device, realDevice: false, udid: device.udid}; } async startSim () { // TODO for now just kill all sims unless specified udid is booted. // if booted, use it. if not booted, start it up // if no udid, well lets see if we can start one up based on desired caps // if we support multiple sims we need to change this if (await simBooted(this.opts.device)) { log.info(`Simulator with udid '${this.opts.udid}' already booted`); return; } log.info(`Simulator with udid '${this.opts.udid}' not booted. Booting up now`); await killAllSimulators(); await this.opts.device.run(undefined, this.opts.allowTouchIdEnroll); } async createSim () { this.lifecycleData.createSim = true; // create sim for caps let sim = await createSim(this.opts, this.sessionId); log.info(`Created simulator with udid '${sim.udid}'.`); return sim; } async launchApp () { const APP_LAUNCH_TIMEOUT = 20 * 1000; this.logEvent('appLaunchAttempted'); await launch(this.opts.device.udid, this.opts.bundleId); let checkStatus = async () => { let response = await this.proxyCommand('/status', 'GET'); let currentApp = response.currentApp.bundleID; if (currentApp !== this.opts.bundleId) { throw new Error(`${this.opts.bundleId} not in foreground. ${currentApp} is in foreground`); } }; log.info(`Waiting for '${this.opts.bundleId}' to be in foreground`); let retries = parseInt(APP_LAUNCH_TIMEOUT / 200, 10); await retryInterval(retries, 200, checkStatus); log.info(`${this.opts.bundleId} is in foreground`); this.logEvent('appLaunched'); } async startWdaSession (bundleId, processArguments) { let args = processArguments ? processArguments.args : []; let env = processArguments ? processArguments.env : {}; let shouldWaitForQuiescence = util.hasValue(this.opts.waitForQuiescence) ? this.opts.waitForQuiescence : true; let maxTypingFrequency = util.hasValue(this.opts.maxTypingFrequency) ? this.opts.maxTypingFrequency : 60; let shouldUseSingletonTestManager = util.hasValue(this.opts.shouldUseSingletonTestManager) ? this.opts.shouldUseSingletonTestManager : true; let shouldUseTestManagerForVisibilityDetection = false; if (util.hasValue(this.opts.simpleIsVisibleCheck)) { shouldUseTestManagerForVisibilityDetection = this.opts.simpleIsVisibleCheck; } if ((this.opts.platformVersion || '').indexOf('9') === 0) { shouldUseTestManagerForVisibilityDetection = true; } let desired = { desiredCapabilities: { bundleId, arguments: args, environment: env, shouldWaitForQuiescence, shouldUseTestManagerForVisibilityDetection, maxTypingFrequency, shouldUseSingletonTestManager, } }; await this.proxyCommand('/session', 'POST', desired); } // Override Proxy methods from BaseDriver proxyActive () { return this.jwpProxyActive; } getProxyAvoidList () { if (this.isWebview()) { return NO_PROXY_WEB_LIST; } return NO_PROXY_NATIVE_LIST; } canProxy () { return true; } isSafari () { return !!this.safari; } isRealDevice () { return this.opts.realDevice; } isSimulator () { return !this.opts.realDevice; } isWebview () { return this.isSafari() || this.isWebContext(); } validateLocatorStrategy (strategy) { super.validateLocatorStrategy(strategy, this.isWebContext()); } validateDesiredCaps (caps) { // check with the base class, and return if it fails let res = super.validateDesiredCaps(caps); if (!res) { return res; } // make sure that the capabilities have one of `app` or `bundleId` if ((caps.browserName || '').toLowerCase() !== 'safari' && !caps.app && !caps.bundleId) { let msg = 'The desired capabilities must include either an app or a bundleId for iOS'; log.errorAndThrow(msg); } let verifyProcessArgument = (processArguments) => { if (!_.isNil(processArguments.args) && !_.isArray(processArguments.args)) { log.errorAndThrow('processArguments.args must be an array of string'); } if (!_.isNil(processArguments.env) && !_.isObject(caps.processArguments.env)) { log.errorAndThrow('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 (_.isString(caps.processArguments)) { try { // try to parse the string as JSON caps.processArguments = JSON.parse(caps.processArguments); verifyProcessArgument(caps.processArguments); } catch (err) { log.errorAndThrow(`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 (_.isObject(caps.processArguments)) { verifyProcessArgument(caps.processArguments); } else { log.errorAndThrow('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)) { log.errorAndThrow(`If 'keychainPath' is set, 'keychainPassword' must also be set (and vice versa).`); } if (caps.autoAcceptAlerts || caps.autoDismissAlerts) { log.warn(`The capabilities 'autoAcceptAlerts' and 'autoDismissAlerts' ` + `do not work for XCUITest-based tests. Please adjust your ` + `alert handling accordingly.`); } // `resetOnSessionStartOnly` should be set to true by default this.opts.resetOnSessionStartOnly = !util.hasValue(this.opts.resetOnSessionStartOnly) || this.opts.resetOnSessionStartOnly; // finally, return true since the superclass check passed, as did this return true; } async installApp () { if (this.isSafari()) { return; } // if user has passed in desiredCaps.autoLaunch = false // meaning they will manage app install / launching if (this.opts.autoLaunch === false) { return; } if (this.isRealDevice()) { await installToRealDevice (this.opts.device, this.opts.app, this.opts.bundleId, this.opts.noReset); } else { await installToSimulator(this.opts.device, this.opts.app, this.opts.bundleId, this.opts.noReset); } if (util.hasValue(this.opts.iosInstallPause)) { // https://github.com/appium/appium/issues/6889 let pause = parseInt(this.opts.iosInstallPause, 10); log.debug(`iosInstallPause set. Pausing ${pause} ms before continuing`); await B.delay(pause); } } async setInitialOrientation (orientation) { if (typeof orientation !== 'string') { orientation = 'PORTRAIT'; } orientation = orientation.toUpperCase(); if (!_.includes(['LANDSCAPE', 'PORTRAIT'], orientation)) { log.debug(`Unable to set initial orientation to '${orientation}'`); return; } log.debug(`Setting initial orientation to '${orientation}'`); try { await this.proxyCommand('/orientation', 'POST', {orientation}); this.opts.curOrientation = orientation; } catch (err) { log.warn(`Setting initial orientation failed with: ${err}`); } } _getCommandTimeout (cmdName) { this.opts.commandTimeouts = normalizeCommandTimeouts(this.opts.commandTimeouts); if (this.opts.commandTimeouts) { if (cmdName && _.has(this.opts.commandTimeouts, cmdName)) { return this.opts.commandTimeouts[cmdName]; } return this.opts.commandTimeouts[DEFAULT_TIMEOUT_KEY]; } } /** * Get session capabilities merged with what WDA reports * This is a library command but needs to call 'super' so can't be on * a helper object */ async getSession () { // call super to get event timings, etc... let driverSession = await super.getSession(); let wdaCaps = await this.proxyCommand('/', 'GET'); log.info("Merging WDA caps over Appium caps for session detail response"); return Object.assign({udid: this.opts.udid}, driverSession, wdaCaps.capabilities); } async startIWDP () { this.logEvent('iwdpStarting'); this.iwdpServer = new IWDP(this.opts.webkitDebugProxyPort, this.opts.udid); await this.iwdpServer.start(); this.logEvent('iwdpStarted'); } async stopIWDP () { if (this.iwdpServer) { await this.iwdpServer.stop(); delete this.iwdpServer; } } async reset () { if (this.opts.noReset) { // This is to make sure reset happens even if noReset is set to true let opts = _.cloneDeep(this.opts); opts.noReset = false; opts.fullReset = false; const shutdownHandler = this.resetOnUnexpectedShutdown; this.resetOnUnexpectedShutdown = () => {}; try { await this.runReset(opts); } finally { this.resetOnUnexpectedShutdown = shutdownHandler; } } await super.reset(); } } for (let [cmd, fn] of _.toPairs(commands)) { XCUITestDriver.prototype[cmd] = fn; } export default XCUITestDriver; export { XCUITestDriver };