From 66f5bba0f420797d2b0bfd5a4499e4cd931006d3 Mon Sep 17 00:00:00 2001 From: abhinvv1 Date: Tue, 11 Mar 2025 02:31:24 +0530 Subject: [PATCH 1/6] feat: implement wda restart incase of socket hangups --- lib/commands/find.js | 17 +++- lib/driver.js | 228 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 205 insertions(+), 40 deletions(-) diff --git a/lib/commands/find.js b/lib/commands/find.js index 4dc084b30..5bb194c2d 100644 --- a/lib/commands/find.js +++ b/lib/commands/find.js @@ -89,8 +89,8 @@ export default { if (rewroteSelector) { this.log.info( `Rewrote incoming selector from '${initSelector}' to ` + - `'${selector}' to match XCUI type. You should consider ` + - `updating your tests to use the new selectors directly`, + `'${selector}' to match XCUI type. You should consider ` + + `updating your tests to use the new selectors directly`, ); } @@ -122,7 +122,10 @@ export default { els = /** @type {Element[]|undefined} */ ( await this.proxyCommand(endpoint, method, body) ); - } catch { + } catch (err) { + if (err.message && err.message.match(/ECONNREFUSED/)) { + throw err; + } els = []; } // we succeed if we get some elements @@ -132,7 +135,11 @@ export default { if (err.message && err.message.match(/Condition unmet/)) { // condition was not met setting res to empty array els = []; - } else { + } + else if (err.message && err.message.match(/ECONNREFUSED/)) { + throw err; + } + else { throw err; } } @@ -193,7 +200,7 @@ function rewriteMagicScrollable(mult, log = null) { } log?.info( 'Rewrote request for scrollable descendants to class chain ' + - `format with selector '${selector}'`, + `format with selector '${selector}'`, ); return [strategy, selector]; } diff --git a/lib/driver.js b/lib/driver.js index 568bc9741..7b6c01cc1 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -23,7 +23,7 @@ import {desiredCapConstraints} from './desired-caps'; import {DEVICE_CONNECTIONS_FACTORY} from './device-connections-factory'; import {executeMethodMap} from './execute-method-map'; import {newMethodMap} from './method-map'; -import { Pyidevice } from './real-device-clients/py-ios-device-client'; +import {Pyidevice} from './real-device-clients/py-ios-device-client'; import { installToRealDevice, runRealDeviceReset, @@ -61,8 +61,8 @@ import { shouldSetInitialSafariUrl, translateDeviceName, } from './utils'; -import { AppInfosCache } from './app-infos-cache'; -import { notifyBiDiContextChange } from './commands/context'; +import {AppInfosCache} from './app-infos-cache'; +import {notifyBiDiContextChange} from './commands/context'; const SHUTDOWN_OTHER_FEAT_NAME = 'shutdown_other_sims'; const CUSTOMIZE_RESULT_BUNDLE_PATH = 'customize_result_bundle_path'; @@ -325,6 +325,7 @@ export class XCUITestDriver extends BaseDriver { this.appInfosCache = new AppInfosCache(this.log); this.remote = null; this.doesSupportBidi = true; + this._isRecovering = false; } async onSettingsUpdate(key, value) { @@ -515,15 +516,15 @@ export class XCUITestDriver extends BaseDriver { if (_.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`, + `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}`, + `requested by the 'mjpegServerPort' capability to the device port ${mjpegServerPort}. ` + + `Original error: ${error}`, ); } } @@ -613,7 +614,7 @@ export class XCUITestDriver extends BaseDriver { await this.runReset(); this.wda = new WebDriverAgent( - /** @type {import('appium-xcode').XcodeVersion} */ (this.xcodeVersion), + /** @type {import('appium-xcode').XcodeVersion} */(this.xcodeVersion), { ...this.opts, device: this.device, @@ -689,11 +690,10 @@ export class XCUITestDriver extends BaseDriver { if (_.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`, + `Consider using 'permissions' one instead with 'calendar' key`, ); - const methodName = `${ - this.opts.calendarAccessAuthorized ? 'enable' : 'disable' - }CalendarAccess`; + const methodName = `${this.opts.calendarAccessAuthorized ? 'enable' : 'disable' + }CalendarAccess`; await this.device[methodName](this.opts.bundleId); } } @@ -811,7 +811,7 @@ export class XCUITestDriver extends BaseDriver { 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`, + `to avoid conflicts and speed up the building process`, ); } @@ -888,7 +888,7 @@ export class XCUITestDriver extends BaseDriver { await this.preparePreinstalledWda(); } - this.cachedWdaStatus = await this.wda.launch(/** @type {string} */ (this.sessionId)); + this.cachedWdaStatus = await this.wda.launch(/** @type {string} */(this.sessionId)); } catch (err) { this.logEvent('wdaStartFailed'); this.log.debug(err.stack); @@ -902,7 +902,7 @@ export class XCUITestDriver extends BaseDriver { // 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 {}; + } catch { }; // Mostly it failed to start the WDA process as no the bundle id // e.g. ' not found on device ' @@ -1074,6 +1074,104 @@ export class XCUITestDriver extends BaseDriver { DEVICE_CONNECTIONS_FACTORY.releaseConnection(this.opts.udid); } + isWdaConnectionError(err) { + if (!err) { + return false; + } + + const errorMessage = err.message || ''; + const connectionErrors = [ + 'ECONNREFUSED', + 'ECONNRESET', + 'socket hang up', + 'timed out', + 'Connection refused', + 'connect ETIMEDOUT', + 'Original error: Error: socket hang up', + ]; + + return connectionErrors.some((errStr) => errorMessage.includes(errStr)); + } + + async reconnectWda() { + const timeout = this.opts.wdaConnectionTimeout || 15000; + const interval = this.opts.wdaStartupRetryInterval || 1000; + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + try { + const status = await this.proxyCommand('/status', 'GET'); + this.log.info(`WDA is now reachable with status: ${JSON.stringify(status)}`); + return status; + } catch (err) { + this.log.debug(`WDA still not reachable: ${err.message}. Retrying in ${interval}ms...`); + await this.delay(interval); + } + } + throw new Error(`WDA did not become reachable within ${timeout} ms.`); + } + + /** + * @param {number} ms - Delay time in milliseconds + * @returns {Promise} + */ + async delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + async reinitializeSession() { + this.log.info('Reinitializing WDA session mapping...'); + const originalSessionId = this.sessionId; + try { + await this.proxyCommand('/status', 'GET'); + this.log.info('WDA process is still running'); + } catch (err) { + this.log.warn(`WDA process check failed: ${err.message}`); + this.log.info('Restarting WDA process...'); + + if (this.wda && this.wda.fullyStarted) { + try { + await this.wda.quit(); + } catch (quitErr) { + this.log.warn(`Error quitting stale WDA: ${quitErr.message}`); + } + } + + try { + await this.startWda(); + await this.delay(10000); + } catch (startErr) { + this.log.error(`Failed to start WDA: ${startErr.message}`); + throw startErr; + } + } + + try { + await this.startWdaSession(this.opts.bundleId, this.opts.processArguments); + this.proxyReqRes = this.wda.proxyReqRes.bind(this.wda); + this.jwpProxyActive = true; + this.sessionId = originalSessionId; + + this.log.info(`WDA session reinitialized successfully, WDA session ID: ${this.sessionId}`); + if (this._currentContext && this._currentContext !== 'NATIVE_APP') { + try { + this.log.info(`Attempting to restore webview context: ${this._currentContext}`); + await this.activateRecentWebview(); + } catch (contextErr) { + this.log.warn(`Failed to restore webview context: ${contextErr.message}`); + this._currentContext = 'NATIVE_APP'; + } + } + + this.log.info(`WDA session reinitialized successfully, new WDA session ID: ${this.sessionId}`); + return this.sessionId; + } catch (err) { + this.log.error(`Failed to initialize WDA session: ${err.message}`); + throw err; + } finally { + this._isRecovering = false; + } + } /** * * @param {string} cmd @@ -1081,16 +1179,76 @@ export class XCUITestDriver extends BaseDriver { * @returns {Promise} */ async executeCommand(cmd, ...args) { - this.log.debug(`Executing command '${cmd}'`); + this.log.debug(`Executing command_11111 '${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); + + if (cmd === 'createSession') { + return await super.executeCommand(cmd, ...args); + } + + const maxAttempts = this.opts.wdaStartupRetries || 5; + let attempts = 0; + + while (true) { + try { + return await super.executeCommand(cmd, ...args); + } catch (err) { + this.log.debug(`Error executing command '${cmd}': ${err.message}`); + attempts++; + + const isWdaCrash = this.isWdaConnectionError(err) || + (err.message && ( + err.message.includes('invalid session id') || + err.message.includes('Cannot find any matching app') || + err.message.includes('Session does not exist') + )); + + if (!isWdaCrash || attempts > maxAttempts) { + throw err; + } + this._isRecovering = true; + this.log.warn(`WDA appears to have crashed or become unresponsive. Attempt ${attempts}/${maxAttempts} to recover...`); + + try { + this.log.info('Attempting to reconnect to WDA...'); + await this.reconnectWda(); + + try { + await this.proxyCommand(`/session/${this.sessionId}/source`, 'GET'); + this.log.info('WDA connection restored, session still valid'); + } catch (sessionErr) { + if (sessionErr.message && + ( + sessionErr.message.includes('invalid session id') || + sessionErr.message.includes('Cannot find any matching app') || + sessionErr.message.includes('Session does not exist') + )) { + this.log.info('WDA session is invalid, reinitializing session...'); + await this.reinitializeSession(); + } + } + // if (this.opts.bundleId) { + // await this.proxyCommand('/wda/apps/activate', 'POST', { + // bundleId: this.opts.bundleId, + // shouldTerminateApp: false + // }); + // } + } catch (recoveryErr) { + this.log.warn(`Reconnection failed: ${recoveryErr.message}`); + this.log.info('Attempting full WDA reinitialization...'); + await this.reinitializeSession(); + } + + this.log.info('Recovery successful, retrying original command'); + } + } } async configureApp() { @@ -1145,7 +1303,7 @@ export class XCUITestDriver extends BaseDriver { if (!this.opts.platformVersion && this._iosSdkVersion) { this.log.info( `No platformVersion specified. Using the latest version Xcode supports: '${this._iosSdkVersion}'. ` + - `This may cause problems if a simulator does not exist for this platform version.`, + `This may cause problems if a simulator does not exist for this platform version.`, ); this.opts.platformVersion = normalizePlatformVersion(this._iosSdkVersion); } @@ -1208,7 +1366,7 @@ export class XCUITestDriver extends BaseDriver { this.log.info( `No real device udid has been provided in capabilities. ` + - `Will select a matching simulator to run the test.`, + `Will select a matching simulator to run the test.`, ); await setupVersionCaps(); if (this.opts.enforceFreshSimulatorCreation) { @@ -1282,14 +1440,14 @@ export class XCUITestDriver extends BaseDriver { if (!_.isArray(args)) { throw new Error( `processArguments.args capability is expected to be an array. ` + - `${JSON.stringify(args)} is given instead`, + `${JSON.stringify(args)} is given instead`, ); } const env = processArguments ? _.cloneDeep(processArguments.env) || {} : {}; if (!_.isPlainObject(env)) { throw new Error( `processArguments.env capability is expected to be a dictionary. ` + - `${JSON.stringify(env)} is given instead`, + `${JSON.stringify(env)} is given instead`, ); } @@ -1331,8 +1489,8 @@ export class XCUITestDriver extends BaseDriver { // @ts-expect-error - do not assign arbitrary properties to `this.opts` elementResponseFields: this.opts.elementResponseFields, disableAutomaticScreenshots: this.opts.disableAutomaticScreenshots, - shouldTerminateApp: this.opts.shouldTerminateApp ?? true, - forceAppLaunch: this.opts.forceAppLaunch ?? true, + shouldTerminateApp: this._isRecovering ? false : (this.opts.shouldTerminateApp ?? true), + forceAppLaunch: this._isRecovering ? false : (this.opts.forceAppLaunch ?? true), appLaunchStateTimeoutSec: this.opts.appLaunchStateTimeoutSec, useNativeCachingStrategy: this.opts.useNativeCachingStrategy ?? true, forceSimulatorSoftwareKeyboardPresence: @@ -1416,14 +1574,14 @@ export class XCUITestDriver extends BaseDriver { if (_.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', + 'WebDriverAgent will be started without the default app', ); } if (!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.`, + `Consider fixing it or be ready to experience an inconsistent driver behavior.`, ); } @@ -1449,7 +1607,7 @@ export class XCUITestDriver extends BaseDriver { } 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}`, + `Both environment and argument can be null. Error: ${err}`, ); } } else if (_.isPlainObject(caps.processArguments)) { @@ -1457,7 +1615,7 @@ export class XCUITestDriver extends BaseDriver { } 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.`, + `Both environment and argument can be null.`, ); } } @@ -1486,7 +1644,7 @@ export class XCUITestDriver extends BaseDriver { if (_.isEmpty(protocol) || _.isEmpty(host)) { throw this.log.errorWithException( `'webDriverAgentUrl' capability is expected to contain a valid WebDriverAgent server URL. ` + - `'${caps.webDriverAgentUrl}' is given instead`, + `'${caps.webDriverAgentUrl}' is given instead`, ); } } @@ -1519,7 +1677,7 @@ export class XCUITestDriver extends BaseDriver { } catch (e) { throw this.log.errorWithException( `'${caps.permissions}' is expected to be a valid object with format ` + - `{"": {"": "", ...}, ...}. Original error: ${e.message}`, + `{"": {"": "", ...}, ...}. Original error: ${e.message}`, ); } } @@ -1527,7 +1685,7 @@ export class XCUITestDriver extends BaseDriver { if (caps.platformVersion && !util.coerceVersion(caps.platformVersion, false)) { throw this.log.errorWithException( `'platformVersion' must be a valid version number. ` + - `'${caps.platformVersion}' is given instead.`, + `'${caps.platformVersion}' is given instead.`, ); } @@ -1605,12 +1763,12 @@ export class XCUITestDriver extends BaseDriver { if (shouldUpgrade) { this.log.info( `The installed version of ${bundleId} is lower than the candidate one ` + - `(${candidateBundleVersion} > ${appBundleVersion}). The app will be upgraded.`, + `(${candidateBundleVersion} > ${appBundleVersion}). The app will be upgraded.`, ); } else { this.log.info( `The candidate version of ${bundleId} is lower than the installed one ` + - `(${candidateBundleVersion} <= ${appBundleVersion}). The app won't be reinstalled.`, + `(${candidateBundleVersion} <= ${appBundleVersion}). The app won't be reinstalled.`, ); } return { @@ -1710,7 +1868,7 @@ export class XCUITestDriver extends BaseDriver { if (!SUPPORTED_ORIENATIONS.includes(dstOrientation)) { this.log.debug( `The initial orientation value '${orientation}' is unknown. ` + - `Only ${JSON.stringify(SUPPORTED_ORIENATIONS)} are supported.`, + `Only ${JSON.stringify(SUPPORTED_ORIENATIONS)} are supported.`, ); return; } @@ -1742,7 +1900,7 @@ export class XCUITestDriver extends BaseDriver { async reset() { throw new Error( `The reset API has been deprecated and is not supported anymore. ` + - `Consider using corresponding 'mobile:' extensions to manage the state of the app under test.`, + `Consider using corresponding 'mobile:' extensions to manage the state of the app under test.`, ); } From acb00af5c6bd99461517a4da90d6d1b915772026 Mon Sep 17 00:00:00 2001 From: abhinvv1 Date: Tue, 11 Mar 2025 03:13:05 +0530 Subject: [PATCH 2/6] fix: typo and remove comment --- lib/driver.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/driver.js b/lib/driver.js index 7b6c01cc1..0b688a6c2 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -1179,7 +1179,7 @@ export class XCUITestDriver extends BaseDriver { * @returns {Promise} */ async executeCommand(cmd, ...args) { - this.log.debug(`Executing command_11111 '${cmd}'`); + this.log.debug(`Executing command '${cmd}'`); if (cmd === 'receiveAsyncResponse') { return await this.receiveAsyncResponse(...args); @@ -1234,12 +1234,6 @@ export class XCUITestDriver extends BaseDriver { await this.reinitializeSession(); } } - // if (this.opts.bundleId) { - // await this.proxyCommand('/wda/apps/activate', 'POST', { - // bundleId: this.opts.bundleId, - // shouldTerminateApp: false - // }); - // } } catch (recoveryErr) { this.log.warn(`Reconnection failed: ${recoveryErr.message}`); this.log.info('Attempting full WDA reinitialization...'); From 868276970f2c9871c9feb9514064e8622473763d Mon Sep 17 00:00:00 2001 From: abhinvv1 Date: Tue, 11 Mar 2025 22:57:26 +0530 Subject: [PATCH 3/6] code refactoring --- lib/commands/find.js | 4 +- lib/desired-caps.js | 6 +- lib/driver.js | 196 ++++++++++++++++++++++++++++++------------- 3 files changed, 143 insertions(+), 63 deletions(-) diff --git a/lib/commands/find.js b/lib/commands/find.js index 5bb194c2d..43ef6c589 100644 --- a/lib/commands/find.js +++ b/lib/commands/find.js @@ -123,7 +123,7 @@ export default { await this.proxyCommand(endpoint, method, body) ); } catch (err) { - if (err.message && err.message.match(/ECONNREFUSED/)) { + if (err.message && err.message.match(/socket hang up/)) { throw err; } els = []; @@ -136,7 +136,7 @@ export default { // condition was not met setting res to empty array els = []; } - else if (err.message && err.message.match(/ECONNREFUSED/)) { + else if (err.message && err.message.match(/socket hang up/)) { throw err; } else { diff --git a/lib/desired-caps.js b/lib/desired-caps.js index 5c5666156..807bb3b52 100644 --- a/lib/desired-caps.js +++ b/lib/desired-caps.js @@ -383,7 +383,11 @@ const desiredCapConstraints = /** @type {const} */ ({ pageLoadStrategy: { isString: true, inclusionCaseInsensitive: ['none', 'eager', 'normal'] - } + }, + enableWdaRestart: { + presence: false, + isBoolean: true, + }, }); export {desiredCapConstraints, PLATFORM_NAME_IOS, PLATFORM_NAME_TVOS}; diff --git a/lib/driver.js b/lib/driver.js index 0b688a6c2..505e7d572 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -326,6 +326,9 @@ export class XCUITestDriver extends BaseDriver { this.remote = null; this.doesSupportBidi = true; this._isRecovering = false; + if (_.isNil(this.opts.enableWdaRestart)) { + this.opts.enableWdaRestart = false; + } } async onSettingsUpdate(key, value) { @@ -1074,30 +1077,34 @@ export class XCUITestDriver extends BaseDriver { DEVICE_CONNECTIONS_FACTORY.releaseConnection(this.opts.udid); } + /** + * Checks if an error is related to WDA connection issues + * @param {Error|null|undefined} err - The error to check + * @returns {boolean} - True if the error is a WDA connection error + */ isWdaConnectionError(err) { if (!err) { return false; } const errorMessage = err.message || ''; - const connectionErrors = [ - 'ECONNREFUSED', - 'ECONNRESET', - 'socket hang up', - 'timed out', - 'Connection refused', - 'connect ETIMEDOUT', - 'Original error: Error: socket hang up', - ]; + const CONNECTION_ERRORS = ['ECONNREFUSED', 'socket hang up']; - return connectionErrors.some((errStr) => errorMessage.includes(errStr)); + return CONNECTION_ERRORS.some((errStr) => errorMessage.includes(errStr)); } + /** + * Reconnects to WDA within a specified timeout + * @returns {Promise} - The WDA status + * @throws {Error} - If WDA does not become reachable within the timeout + */ async reconnectWda() { const timeout = this.opts.wdaConnectionTimeout || 15000; const interval = this.opts.wdaStartupRetryInterval || 1000; const startTime = Date.now(); + this.log.info(`Attempting to reconnect to WDA with ${timeout}ms timeout`); + while (Date.now() - startTime < timeout) { try { const status = await this.proxyCommand('/status', 'GET'); @@ -1108,10 +1115,12 @@ export class XCUITestDriver extends BaseDriver { await this.delay(interval); } } - throw new Error(`WDA did not become reachable within ${timeout} ms.`); + + throw new Error(`WDA did not become reachable within ${timeout}ms`); } /** + * Creates a promise that resolves after the specified time * @param {number} ms - Delay time in milliseconds * @returns {Promise} */ @@ -1119,17 +1128,25 @@ export class XCUITestDriver extends BaseDriver { return new Promise((resolve) => setTimeout(resolve, ms)); } + /** + * Reinitializes the WDA session + * @returns {Promise} - The session ID + * @throws {Error} - If session reinitialization fails + */ async reinitializeSession() { this.log.info('Reinitializing WDA session mapping...'); + const originalSessionId = this.sessionId; - try { - await this.proxyCommand('/status', 'GET'); + const isRunning = await this.wda.isRunning(); + + // Handle WDA process state + if (isRunning) { this.log.info('WDA process is still running'); - } catch (err) { - this.log.warn(`WDA process check failed: ${err.message}`); - this.log.info('Restarting WDA process...'); + } else { + this.log.info('WDA process is not running. Restarting WDA process...'); - if (this.wda && this.wda.fullyStarted) { + // Quit any stale WDA instance + if (this.wda?.fullyStarted) { try { await this.wda.quit(); } catch (quitErr) { @@ -1137,9 +1154,9 @@ export class XCUITestDriver extends BaseDriver { } } + // Start a new WDA instance try { await this.startWda(); - await this.delay(10000); } catch (startErr) { this.log.error(`Failed to start WDA: ${startErr.message}`); throw startErr; @@ -1147,23 +1164,24 @@ export class XCUITestDriver extends BaseDriver { } try { + // Initialize a new session await this.startWdaSession(this.opts.bundleId, this.opts.processArguments); + + // Restore proxy and session settings this.proxyReqRes = this.wda.proxyReqRes.bind(this.wda); this.jwpProxyActive = true; this.sessionId = originalSessionId; - this.log.info(`WDA session reinitialized successfully, WDA session ID: ${this.sessionId}`); - if (this._currentContext && this._currentContext !== 'NATIVE_APP') { - try { - this.log.info(`Attempting to restore webview context: ${this._currentContext}`); - await this.activateRecentWebview(); - } catch (contextErr) { - this.log.warn(`Failed to restore webview context: ${contextErr.message}`); - this._currentContext = 'NATIVE_APP'; - } + this.log.info(`WDA session reinitialized with ID: ${this.sessionId}`); + + // Attempt to restore previous context if not NATIVE_APP + await this.restoreContext(); + + if (!this.sessionId) { + throw new Error('Session ID is unexpectedly null after reinitialization'); } - this.log.info(`WDA session reinitialized successfully, new WDA session ID: ${this.sessionId}`); + this.log.info(`WDA session reinitialization completed successfully`); return this.sessionId; } catch (err) { this.log.error(`Failed to initialize WDA session: ${err.message}`); @@ -1172,11 +1190,30 @@ export class XCUITestDriver extends BaseDriver { this._isRecovering = false; } } + /** - * - * @param {string} cmd - * @param {...any} args - * @returns {Promise} + * Attempts to restore the previous webview context + * @private + * @returns {Promise} + */ + async restoreContext() { + if (this._currentContext && this._currentContext !== 'NATIVE_APP') { + try { + this.log.info(`Attempting to restore webview context: ${this._currentContext}`); + await this.activateRecentWebview(); + } catch (contextErr) { + this.log.warn(`Failed to restore webview context: ${contextErr.message}`); + this._currentContext = 'NATIVE_APP'; + } + } + } + + /** + * Executes a command with automatic WDA recovery on failure + * @param {string} cmd - Command to execute + * @param {...any} args - Command arguments + * @returns {Promise} - Command result + * @throws {Error} - If command execution fails after recovery attempts */ async executeCommand(cmd, ...args) { this.log.debug(`Executing command '${cmd}'`); @@ -1193,6 +1230,12 @@ export class XCUITestDriver extends BaseDriver { return await super.executeCommand(cmd, ...args); } + const wdaRestartEnabled = Boolean(this.opts.enableWdaRestart) || true; // temp commit for testing + + if (!wdaRestartEnabled) { + return await super.executeCommand(cmd, ...args); + } + const maxAttempts = this.opts.wdaStartupRetries || 5; let attempts = 0; @@ -1203,45 +1246,78 @@ export class XCUITestDriver extends BaseDriver { this.log.debug(`Error executing command '${cmd}': ${err.message}`); attempts++; - const isWdaCrash = this.isWdaConnectionError(err) || - (err.message && ( - err.message.includes('invalid session id') || - err.message.includes('Cannot find any matching app') || - err.message.includes('Session does not exist') - )); + const isWdaCrash = this.isWdaCrashOrSessionInvalid(err); if (!isWdaCrash || attempts > maxAttempts) { throw err; } + this._isRecovering = true; - this.log.warn(`WDA appears to have crashed or become unresponsive. Attempt ${attempts}/${maxAttempts} to recover...`); + this.log.warn(`WDA issue detected. Recovery attempt ${attempts}/${maxAttempts}...`); try { - this.log.info('Attempting to reconnect to WDA...'); - await this.reconnectWda(); - - try { - await this.proxyCommand(`/session/${this.sessionId}/source`, 'GET'); - this.log.info('WDA connection restored, session still valid'); - } catch (sessionErr) { - if (sessionErr.message && - ( - sessionErr.message.includes('invalid session id') || - sessionErr.message.includes('Cannot find any matching app') || - sessionErr.message.includes('Session does not exist') - )) { - this.log.info('WDA session is invalid, reinitializing session...'); - await this.reinitializeSession(); - } - } + await this.handleWdaRecovery(); + this.log.info('Recovery successful, retrying original command'); } catch (recoveryErr) { - this.log.warn(`Reconnection failed: ${recoveryErr.message}`); - this.log.info('Attempting full WDA reinitialization...'); - await this.reinitializeSession(); + this.log.error(`Recovery attempt failed: ${recoveryErr.message}`); + if (attempts >= maxAttempts) { + throw new Error( + `Failed to recover WDA after ${maxAttempts} attempts: ${recoveryErr.message}`, + ); + } } + } + } + } - this.log.info('Recovery successful, retrying original command'); + /** + * Checks if an error indicates a WDA crash or invalid session + * @private + * @param {Error} err - The error to check + * @returns {boolean} - True if the error indicates WDA crash or invalid session + */ + isWdaCrashOrSessionInvalid(err) { + if (this.isWdaConnectionError(err)) { + return true; + } + + return Boolean( + err.message && + (err.message.includes('invalid session id') || + err.message.includes('Session does not exist')), + ); + } + + /** + * Handles WDA recovery by attempting reconnection or reinitialization + * @private + * @returns {Promise} + */ + async handleWdaRecovery() { + try { + this.log.info('Attempting to reconnect to WDA...'); + await this.reconnectWda(); + + try { + // Verify session is still valid + // This is important because in case the WDA was not killed and it was only an intermittent connection issue, + // and the session is still valid then this would prevent us restarting it unnecesarilly + await this.proxyCommand(`/session/${this.sessionId}/url`, 'GET'); + this.log.info('WDA connection restored, session still valid'); + } catch (sessionErr) { + if (this.isWdaCrashOrSessionInvalid(sessionErr)) { + this.log.info('WDA session is invalid, reinitializing session...'); + await this.reinitializeSession(); + } else { + throw sessionErr; + } } + } catch (reconnectErr) { + this.log.warn(`Reconnection failed: ${reconnectErr.message}`); + this.log.info('Attempting full WDA reinitialization...'); + await this.reinitializeSession(); + } finally { + this._isRecovering = false; } } From add2077b9a632e5dba1c1151086dd6435ceeb855 Mon Sep 17 00:00:00 2001 From: abhinvv1 Date: Wed, 12 Mar 2025 01:51:29 +0530 Subject: [PATCH 4/6] chore: add unit tests and fix retrying logic --- lib/driver.js | 60 +++--- test/unit/driver-specs.js | 374 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 399 insertions(+), 35 deletions(-) diff --git a/lib/driver.js b/lib/driver.js index 505e7d572..c4c88442e 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -1107,16 +1107,21 @@ export class XCUITestDriver extends BaseDriver { while (Date.now() - startTime < timeout) { try { - const status = await this.proxyCommand('/status', 'GET'); - this.log.info(`WDA is now reachable with status: ${JSON.stringify(status)}`); - return status; + const isRunning = await this.wda.isRunning(); + if (isRunning) { + this.log.info(`WDA is now reachable with status: ${isRunning}}`); + return isRunning; + } else { + this.log.debug(`WDA still not reachable, status: ${isRunning}. Retrying in ${interval}ms...`); + await this.delay(interval); + } } catch (err) { this.log.debug(`WDA still not reachable: ${err.message}. Retrying in ${interval}ms...`); await this.delay(interval); } } - throw new Error(`WDA did not become reachable within ${timeout}ms`); + throw new Error(`Failed to reconnect to WDA within ${timeout}ms`); } /** @@ -1135,56 +1140,41 @@ export class XCUITestDriver extends BaseDriver { */ async reinitializeSession() { this.log.info('Reinitializing WDA session mapping...'); - const originalSessionId = this.sessionId; - const isRunning = await this.wda.isRunning(); - // Handle WDA process state - if (isRunning) { - this.log.info('WDA process is still running'); - } else { - this.log.info('WDA process is not running. Restarting WDA process...'); + try { + const isRunning = await this.wda.isRunning(); - // Quit any stale WDA instance - if (this.wda?.fullyStarted) { - try { - await this.wda.quit(); - } catch (quitErr) { - this.log.warn(`Error quitting stale WDA: ${quitErr.message}`); + if (!isRunning) { + this.log.info('WDA process is not running. Restarting WDA process...'); + if (this.wda?.fullyStarted) { + try { + await this.wda.quit(); + } catch (quitErr) { + this.log.warn(`Error quitting stale WDA: ${quitErr.message}`); + } } - } - - // Start a new WDA instance - try { await this.startWda(); - } catch (startErr) { - this.log.error(`Failed to start WDA: ${startErr.message}`); - throw startErr; + } else { + this.log.info('WDA process is still running'); } - } - try { - // Initialize a new session await this.startWdaSession(this.opts.bundleId, this.opts.processArguments); - - // Restore proxy and session settings this.proxyReqRes = this.wda.proxyReqRes.bind(this.wda); this.jwpProxyActive = true; this.sessionId = originalSessionId; this.log.info(`WDA session reinitialized with ID: ${this.sessionId}`); - - // Attempt to restore previous context if not NATIVE_APP await this.restoreContext(); if (!this.sessionId) { throw new Error('Session ID is unexpectedly null after reinitialization'); } - this.log.info(`WDA session reinitialization completed successfully`); + this.log.info('WDA session reinitialization completed successfully'); return this.sessionId; } catch (err) { - this.log.error(`Failed to initialize WDA session: ${err.message}`); + this.log.error(`Failed to reinitialize WDA session: ${err.message}`); throw err; } finally { this._isRecovering = false; @@ -1221,7 +1211,7 @@ export class XCUITestDriver extends BaseDriver { 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(); } @@ -1236,7 +1226,7 @@ export class XCUITestDriver extends BaseDriver { return await super.executeCommand(cmd, ...args); } - const maxAttempts = this.opts.wdaStartupRetries || 5; + const maxAttempts = this.opts.wdaStartupRetries || 2; let attempts = 0; while (true) { diff --git a/test/unit/driver-specs.js b/test/unit/driver-specs.js index f13b17a22..39c213e00 100644 --- a/test/unit/driver-specs.js +++ b/test/unit/driver-specs.js @@ -460,4 +460,378 @@ describe('XCUITestDriver', function () { }); } }); + describe('executeCommand with WDA restart flow', function () { + let driver; + let superExecuteCommandStub; + let isWdaConnectionErrorStub; + let reconnectWdaStub; + let reinitializeSessionStub; + let proxyCommandStub; + let logStub; + + beforeEach(function () { + driver = new XCUITestDriver(); + driver.sessionId = 'test-session-id'; + + superExecuteCommandStub = sandbox.stub(); + sandbox.stub(XCUITestDriver.prototype, 'executeCommand').callsFake(function (cmd, ...args) { + return driver.executeCommand.wrappedMethod.call(this, cmd, ...args); + }); + + isWdaConnectionErrorStub = sandbox.stub(driver, 'isWdaConnectionError'); + reconnectWdaStub = sandbox.stub(driver, 'reconnectWda'); + reinitializeSessionStub = sandbox.stub(driver, 'reinitializeSession'); + proxyCommandStub = sandbox.stub(driver, 'proxyCommand'); + + logStub = { + debug: sandbox.stub(), + info: sandbox.stub(), + warn: sandbox.stub(), + error: sandbox.stub(), + }; + + sandbox.stub(driver, 'log').get(() => logStub); + Object.defineProperty(Object.getPrototypeOf(XCUITestDriver.prototype), 'executeCommand', { + value: superExecuteCommandStub, + writable: true, + }); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('should bypass recovery flow when enableWdaRestart is not set', async function () { + driver.opts = {enableWdaRestart: false}; + superExecuteCommandStub.resolves('success'); + + const result = await driver.executeCommand('test-command', 'arg1'); + + expect(result).to.equal('success'); + expect(superExecuteCommandStub.calledOnce).to.be.true; + expect(superExecuteCommandStub.calledWith('test-command', 'arg1')).to.be.true; + expect(reconnectWdaStub.called).to.be.false; + expect(reinitializeSessionStub.called).to.be.false; + }); + + it('should bypass recovery flow for special commands even with enableWdaRestart true', async function () { + driver.opts = {enableWdaRestart: true}; + driver.getStatus = sandbox.stub().resolves({status: 'ok'}); + await driver.executeCommand('getStatus'); + expect(driver.getStatus.calledOnce).to.be.true; + expect(superExecuteCommandStub.called).to.be.false; + driver.getStatus.reset(); + superExecuteCommandStub.resolves({sessionId: 'new-session'}); + await driver.executeCommand('createSession'); + expect(superExecuteCommandStub.calledWith('createSession')).to.be.true; + }); + + it('should attempt recovery when WDA connection error occurs and enableWdaRestart is true', async function () { + driver.opts = { + enableWdaRestart: true, + wdaStartupRetries: 2, + }; + + const connectionError = new Error('socket hang up'); + superExecuteCommandStub.onFirstCall().rejects(connectionError); + superExecuteCommandStub.onSecondCall().resolves('success after recovery'); + + isWdaConnectionErrorStub.withArgs(connectionError).returns(true); + reconnectWdaStub.resolves({status: 0}); + proxyCommandStub.resolves(); + + const result = await driver.executeCommand('findElement', 'accessibility id', 'submit'); + + expect(result).to.equal('success after recovery'); + expect(superExecuteCommandStub.callCount).to.equal(2); + expect(reconnectWdaStub.calledOnce).to.be.true; + expect(logStub.warn.calledWith(sandbox.match(/WDA issue detected/))).to.be.true; + expect(logStub.info.calledWith('Recovery successful, retrying original command')).to.be.true; + }); + + it('should reinitialize session when proxyCommand fails with invalid session error', async function () { + driver.opts = { + enableWdaRestart: true, + wdaStartupRetries: 2, + }; + + const connectionError = new Error('socket hang up'); + const sessionError = new Error('invalid session id'); + + superExecuteCommandStub.onFirstCall().rejects(connectionError); + superExecuteCommandStub.onSecondCall().resolves('success after recovery'); + + isWdaConnectionErrorStub.withArgs(connectionError).returns(true); + reconnectWdaStub.resolves({status: 0}); + proxyCommandStub.rejects(sessionError); + isWdaConnectionErrorStub.withArgs(sessionError).returns(false); + reinitializeSessionStub.resolves('new-session-id'); + + const result = await driver.executeCommand('findElement', 'accessibility id', 'submit'); + + expect(result).to.equal('success after recovery'); + expect(superExecuteCommandStub.callCount).to.equal(2); + expect(reconnectWdaStub.calledOnce).to.be.true; + expect(reinitializeSessionStub.calledOnce).to.be.true; + expect(logStub.info.calledWith(sandbox.match(/WDA session is invalid/))).to.be.true; + }); + + it('should skip directly to reinitializeSession when reconnectWda fails', async function () { + driver.opts = { + enableWdaRestart: true, + wdaStartupRetries: 2, + }; + + const connectionError = new Error('socket hang up'); + const reconnectError = new Error('reconnect failed'); + + superExecuteCommandStub.onFirstCall().rejects(connectionError); + superExecuteCommandStub.onSecondCall().resolves('success after recovery'); + + isWdaConnectionErrorStub.withArgs(connectionError).returns(true); + reconnectWdaStub.rejects(reconnectError); + reinitializeSessionStub.resolves('new-session-id'); + + const result = await driver.executeCommand('findElement', 'accessibility id', 'submit'); + + expect(result).to.equal('success after recovery'); + expect(superExecuteCommandStub.callCount).to.equal(2); + expect(reconnectWdaStub.calledOnce).to.be.true; + expect(reinitializeSessionStub.calledOnce).to.be.true; + expect(logStub.warn.calledWith(sandbox.match(/Reconnection failed/))).to.be.true; + expect(logStub.info.calledWith('Attempting full WDA reinitialization...')).to.be.true; + }); + + it('should give up after max attempts and throw the original error', async function () { + driver.opts = { + enableWdaRestart: true, + wdaStartupRetries: 2, + }; + + const connectionError = new Error('socket hang up'); + + superExecuteCommandStub.rejects(connectionError); + isWdaConnectionErrorStub.withArgs(connectionError).returns(true); + reconnectWdaStub.resolves({status: 0}); + proxyCommandStub.resolves(); + + try { + await driver.executeCommand('findElement', 'accessibility id', 'submit'); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err).to.equal(connectionError); + expect(superExecuteCommandStub.callCount).to.equal(3); // Initial + 2 retries + expect(reconnectWdaStub.callCount).to.equal(2); + expect(logStub.warn.calledWith(sandbox.match(/WDA issue detected. Recovery attempt 2\/2/))) + .to.be.true; + } + }); + + it('should not attempt recovery for non-connection errors when enableWdaRestart is true', async function () { + driver.opts = { + enableWdaRestart: true, + wdaStartupRetries: 2, + }; + + const nonConnectionError = new Error('element not found'); + superExecuteCommandStub.rejects(nonConnectionError); + isWdaConnectionErrorStub.withArgs(nonConnectionError).returns(false); + + try { + await driver.executeCommand('findElement', 'accessibility id', 'submit'); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err).to.equal(nonConnectionError); + expect(superExecuteCommandStub.calledOnce).to.be.true; + expect(reconnectWdaStub.called).to.be.false; + expect(reinitializeSessionStub.called).to.be.false; + } + }); + + it('should detect session invalid errors and treat them as recoverable', async function () { + driver.opts = { + enableWdaRestart: true, + wdaStartupRetries: 2, + }; + + const sessionError = new Error('Session does not exist'); + superExecuteCommandStub.onFirstCall().rejects(sessionError); + superExecuteCommandStub.onSecondCall().resolves('success after recovery'); + + isWdaConnectionErrorStub.withArgs(sessionError).returns(false); + + reconnectWdaStub.resolves({status: 0}); + proxyCommandStub.rejects(new Error('invalid session id')); + reinitializeSessionStub.resolves('new-session-id'); + + const result = await driver.executeCommand('findElement', 'accessibility id', 'submit'); + + expect(result).to.equal('success after recovery'); + expect(superExecuteCommandStub.callCount).to.equal(2); + expect(reinitializeSessionStub.calledOnce).to.be.true; + }); + }); + + describe('isWdaConnectionError', function () { + let driver; + + beforeEach(function () { + driver = new XCUITestDriver(); + }); + + it('should return false for null or undefined error', function () { + expect(driver.isWdaConnectionError(null)).to.be.false; + expect(driver.isWdaConnectionError(undefined)).to.be.false; + }); + + it('should return true for ECONNREFUSED error', function () { + const error = new Error('Failed to connect: ECONNREFUSED'); + expect(driver.isWdaConnectionError(error)).to.be.true; + }); + + it('should return true for socket hang up error', function () { + const error = new Error('socket hang up'); + expect(driver.isWdaConnectionError(error)).to.be.true; + }); + + it('should return false for non-connection errors', function () { + const error = new Error('element not found'); + expect(driver.isWdaConnectionError(error)).to.be.false; + }); + }); + + describe('XCUITestDriver - reconnectWda', function () { + let driver; + let sandbox; + let clock; + let logStub; + + beforeEach(function () { + sandbox = createSandbox(); + clock = sandbox.useFakeTimers(); + + driver = new XCUITestDriver(); + + driver.wda = { + isRunning: sandbox.stub(), + }; + + // Stub logging + logStub = { + debug: sandbox.stub(), + info: sandbox.stub(), + warn: sandbox.stub(), + error: sandbox.stub(), + }; + sandbox.stub(driver, 'log').get(() => logStub); + sandbox.stub(driver, 'delay').callsFake((ms) => { + clock.tick(ms); + return Promise.resolve(); + }); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('should successfully reconnect when WDA responds immediately', async function () { + driver.wda.isRunning.resolves(true); + + const result = await driver.reconnectWda(); + + expect(result).to.be.true; + expect(driver.wda.isRunning.calledOnce).to.be.true; + expect(logStub.info.calledWith(sandbox.match('WDA is now reachable'))).to.be.true; + }); + + it('should retry until successful within timeout period', async function () { + driver.wda.isRunning + .onFirstCall() + .resolves(false) + .onSecondCall() + .resolves(false) + .onThirdCall() + .resolves(true); + + const result = await driver.reconnectWda(); + + expect(result).to.be.true; + expect(driver.wda.isRunning.callCount).to.equal(3); + expect(driver.delay.callCount).to.be.at.least(2); + expect(logStub.info.calledWith(sandbox.match('WDA is now reachable'))).to.be.true; + }); + + it('should throw error if reconnection fails within timeout period', async function () { + driver.wda.isRunning.resolves(false); + const timeoutMs = 1000; + + await expect(driver.reconnectWda(timeoutMs)).to.be.rejectedWith( + /Failed to reconnect to WDA within.*ms/, + ); + expect(driver.wda.isRunning.called).to.be.true; + }); + }); + + describe('XCUITestDriver - reinitializeSession', function () { + let driver; + let sandbox; + let wdaStub; + let logStub; + let startWdaStub; + + beforeEach(function () { + sandbox = createSandbox(); + + driver = new XCUITestDriver(); + driver.wda = { + isRunning: sandbox.stub(), + }; + + logStub = { + debug: sandbox.stub(), + info: sandbox.stub(), + warn: sandbox.stub(), + error: sandbox.stub(), + }; + sandbox.stub(driver, 'log').get(() => logStub); + driver._isRecovering = true; + driver.opts = {bundleId: 'com.example'}; + + wdaStub = { + isRunning: sandbox.stub(), + quit: sandbox.stub().resolves(), + fullyStarted: true, + proxyReqRes: sandbox.stub(), + }; + driver.wda = wdaStub; + + startWdaStub = sandbox.stub(driver, 'startWda'); + sandbox.stub(driver, 'startWdaSession').resolves(); + sandbox.stub(driver, 'restoreContext').resolves(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('should throw error if starting WDA fails and reset _isRecovering', async function () { + wdaStub.isRunning.resolves(false); + startWdaStub.rejects(new Error('Start WDA failed')); + + await expect(driver.reinitializeSession()).to.be.rejectedWith('Start WDA failed'); + expect(driver._isRecovering).to.be.false; + expect(logStub.error.calledWith(sandbox.match('Failed to reinitialize WDA session'))).to.be + .true; + expect(startWdaStub.calledOnce).to.be.true; + }); + + it('should reset _isRecovering even if isRunning throws an error', async function () { + wdaStub.isRunning.rejects(new Error('WDA check failed')); + + await expect(driver.reinitializeSession()).to.be.rejectedWith('WDA check failed'); + expect(driver._isRecovering).to.be.false; + expect(logStub.error.calledWith(sandbox.match('Failed to reinitialize WDA session'))).to.be + .true; + }); + }); }); From 5da6a48cda436db3488b1a302d988ef20ccd1565 Mon Sep 17 00:00:00 2001 From: abhinvv1 Date: Wed, 12 Mar 2025 02:46:39 +0530 Subject: [PATCH 5/6] fix:typo --- lib/commands/find.js | 3 +-- lib/driver.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/commands/find.js b/lib/commands/find.js index 43ef6c589..ad59c0052 100644 --- a/lib/commands/find.js +++ b/lib/commands/find.js @@ -138,8 +138,7 @@ export default { } else if (err.message && err.message.match(/socket hang up/)) { throw err; - } - else { + } else { throw err; } } diff --git a/lib/driver.js b/lib/driver.js index c4c88442e..bbcfc2211 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -1220,7 +1220,7 @@ export class XCUITestDriver extends BaseDriver { return await super.executeCommand(cmd, ...args); } - const wdaRestartEnabled = Boolean(this.opts.enableWdaRestart) || true; // temp commit for testing + const wdaRestartEnabled = Boolean(this.opts.enableWdaRestart); if (!wdaRestartEnabled) { return await super.executeCommand(cmd, ...args); From 2bf35eae62716199384b8cc5f46a3e7500622ea6 Mon Sep 17 00:00:00 2001 From: abhinvv1 Date: Wed, 12 Mar 2025 10:57:09 +0530 Subject: [PATCH 6/6] temporary commit to trigger FTs --- lib/driver.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/driver.js b/lib/driver.js index bbcfc2211..868bfacac 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -23,7 +23,7 @@ import {desiredCapConstraints} from './desired-caps'; import {DEVICE_CONNECTIONS_FACTORY} from './device-connections-factory'; import {executeMethodMap} from './execute-method-map'; import {newMethodMap} from './method-map'; -import {Pyidevice} from './real-device-clients/py-ios-device-client'; +import { Pyidevice } from './real-device-clients/py-ios-device-client'; import { installToRealDevice, runRealDeviceReset, @@ -61,8 +61,8 @@ import { shouldSetInitialSafariUrl, translateDeviceName, } from './utils'; -import {AppInfosCache} from './app-infos-cache'; -import {notifyBiDiContextChange} from './commands/context'; +import { AppInfosCache } from './app-infos-cache'; +import { notifyBiDiContextChange } from './commands/context'; const SHUTDOWN_OTHER_FEAT_NAME = 'shutdown_other_sims'; const CUSTOMIZE_RESULT_BUNDLE_PATH = 'customize_result_bundle_path'; @@ -617,7 +617,7 @@ export class XCUITestDriver extends BaseDriver { await this.runReset(); this.wda = new WebDriverAgent( - /** @type {import('appium-xcode').XcodeVersion} */(this.xcodeVersion), + /** @type {import('appium-xcode').XcodeVersion} */ (this.xcodeVersion), { ...this.opts, device: this.device, @@ -695,8 +695,9 @@ export class XCUITestDriver extends BaseDriver { `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`; + const methodName = `${ + this.opts.calendarAccessAuthorized ? 'enable' : 'disable' + }CalendarAccess`; await this.device[methodName](this.opts.bundleId); } } @@ -1086,7 +1087,6 @@ export class XCUITestDriver extends BaseDriver { if (!err) { return false; } - const errorMessage = err.message || ''; const CONNECTION_ERRORS = ['ECONNREFUSED', 'socket hang up'];