From e1499589dcd8334aa9d47544c458fe57065e618d Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sat, 16 May 2026 17:20:48 +0200 Subject: [PATCH 1/7] feat: Drop legacy APIs --- lib/appium-ios-device.d.ts | 7 -- lib/index.ts | 2 +- lib/webdriveragent.ts | 111 ++++-------------------------- package.json | 1 - test/unit/webdriveragent-specs.ts | 104 ++++------------------------ 5 files changed, 28 insertions(+), 197 deletions(-) delete mode 100644 lib/appium-ios-device.d.ts diff --git a/lib/appium-ios-device.d.ts b/lib/appium-ios-device.d.ts deleted file mode 100644 index 5aa65bffd..000000000 --- a/lib/appium-ios-device.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare module 'appium-ios-device' { - export class Xctest { - constructor(...args: any[]); - start(): Promise; - stop(): void; - } -} diff --git a/lib/index.ts b/lib/index.ts index 4d78ec618..0bb3f0806 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,7 +1,7 @@ export {bundleWDASim} from './check-dependencies'; export {NoSessionProxy} from './no-session-proxy'; export {WebDriverAgent} from './webdriveragent'; -export {WDA_BASE_URL, WDA_RUNNER_BUNDLE_ID, PROJECT_FILE} from './constants'; +export {WDA_BASE_URL, WDA_RUNNER_APP, WDA_RUNNER_BUNDLE_ID, PROJECT_FILE} from './constants'; export {resetTestProcesses, BOOTSTRAP_PATH} from './utils'; export * from './types'; diff --git a/lib/webdriveragent.ts b/lib/webdriveragent.ts index b0d763506..33b9353a8 100644 --- a/lib/webdriveragent.ts +++ b/lib/webdriveragent.ts @@ -1,7 +1,7 @@ import {waitForCondition} from 'asyncbox'; import path from 'node:path'; import {JWProxy} from '@appium/base-driver'; -import {fs, util, plist} from '@appium/support'; +import {fs, util} from '@appium/support'; import type {AppiumLogger, StringRecord} from '@appium/types'; import {log as defaultLogger} from './logger'; import {NoSessionProxy} from './no-session-proxy'; @@ -14,21 +14,17 @@ import { import {XcodeBuild} from './xcodebuild'; import AsyncLock from 'async-lock'; import {exec} from 'teen_process'; -import {bundleWDASim} from './check-dependencies'; import { WDA_RUNNER_BUNDLE_ID, - WDA_RUNNER_APP, WDA_BASE_URL, WDA_UPGRADE_TIMESTAMP_PATH, DEFAULT_TEST_BUNDLE_SUFFIX, } from './constants'; -import {Xctest} from 'appium-ios-device'; import {strongbox} from '@appium/strongbox'; import type {WebDriverAgentArgs, AppleDevice} from './types'; const WDA_LAUNCH_TIMEOUT = 60 * 1000; const WDA_AGENT_PORT = 8100; -const WDA_CF_BUNDLE_NAME = 'WebDriverAgentRunner-Runner'; const SHARED_RESOURCES_GUARD = new AsyncLock(); const RECENT_MODULE_VERSION_ITEM_NAME = 'recentWdaModuleVersion'; const URL_PROTOCOL_SEPARATOR = '://'; @@ -64,7 +60,6 @@ export class WebDriverAgent { private readonly wdaLaunchTimeout: number; private readonly usePreinstalledWDA?: boolean; private readonly updatedWDABundleIdSuffix: string; - private xctestApiClient?: Xctest | null; private _xcodebuild?: XcodeBuild | null; private _url?: URL; @@ -112,7 +107,6 @@ export class WebDriverAgent { this.wdaLaunchTimeout = args.wdaLaunchTimeout || WDA_LAUNCH_TIMEOUT; this.usePreinstalledWDA = args.usePreinstalledWDA; - this.xctestApiClient = null; this.updatedWDABundleIdSuffix = args.updatedWDABundleIdSuffix ?? DEFAULT_TEST_BUNDLE_SUFFIX; this._xcodebuild = this.canSkipXcodebuild @@ -366,15 +360,10 @@ export class WebDriverAgent { async quit(): Promise { if (this.usePreinstalledWDA) { this.log.info('Stopping the XCTest session'); - if (this.xctestApiClient) { - this.xctestApiClient.stop(); - this.xctestApiClient = null; - } else { - try { - await this.device.simctl.terminateApp(this.bundleIdForXctest); - } catch (e: any) { - this.log.warn(e.message); - } + try { + await this.device.simctl.terminateApp(this.bundleIdForXctest); + } catch (e: any) { + this.log.warn(e.message); } } else if (!this.args.webDriverAgentUrl) { this.log.info('Shutting down sub-processes'); @@ -415,7 +404,6 @@ export class WebDriverAgent { /** * Reuse running WDA if it has the same bundle id with updatedWDABundleId. * Or reuse it if it has the default id without updatedWDABundleId. - * Uninstall it if the method faces an exception for the above situation. */ async setupCaching(): Promise { const status = await this.getStatus(0); @@ -432,9 +420,9 @@ export class WebDriverAgent { this.updatedWDABundleId !== productBundleIdentifier ) { this.log.info( - `Will uninstall running WDA since it has different bundle id. The actual value is '${productBundleIdentifier}'.`, + `Will not reuse running WDA since it has different bundle id. The actual value is '${productBundleIdentifier}'.`, ); - return await this.uninstall(); + return; } // for simulator if ( @@ -443,9 +431,9 @@ export class WebDriverAgent { WDA_RUNNER_BUNDLE_ID !== productBundleIdentifier ) { this.log.info( - `Will uninstall running WDA since its bundle id is not equal to the default value ${WDA_RUNNER_BUNDLE_ID}`, + `Will not reuse running WDA since its bundle id is not equal to the default value ${WDA_RUNNER_BUNDLE_ID}`, ); - return await this.uninstall(); + return; } const actualUpgradeTimestamp = await getWDAUpgradeTimestamp(); @@ -457,10 +445,10 @@ export class WebDriverAgent { `${actualUpgradeTimestamp}`.toLowerCase() !== `${upgradedAt}`.toLowerCase() ) { this.log.info( - 'Will uninstall running WDA since it has different version in comparison to the one ' + + 'Will not reuse running WDA since it has different version in comparison to the one ' + `which is bundled with appium-xcuitest-driver module (${actualUpgradeTimestamp} != ${upgradedAt})`, ); - return await this.uninstall(); + return; } const message = util.hasValue(productBundleIdentifier) @@ -472,38 +460,6 @@ export class WebDriverAgent { this.webDriverAgentUrl = this.url.href; } - /** - * Quit and uninstall running WDA. - */ - async quitAndUninstall(): Promise { - await this.quit(); - await this.uninstall(); - } - - private async parseBundleId(wdaBundlePath: string): Promise { - const infoPlistPath = path.join(wdaBundlePath, 'Info.plist'); - const infoPlist = (await plist.parsePlist(await fs.readFile(infoPlistPath))) as { - CFBundleIdentifier?: string; - }; - if (!infoPlist.CFBundleIdentifier) { - throw new Error(`Could not find bundle id in '${infoPlistPath}'`); - } - return infoPlist.CFBundleIdentifier; - } - - private async fetchWDABundle(): Promise { - if (!this.derivedDataPath) { - return await bundleWDASim(this.xcodebuild); - } - const wdaBundlePaths = await fs.glob(`${this.derivedDataPath}/**/*${WDA_RUNNER_APP}/`, { - absolute: true, - }); - if (wdaBundlePaths.length === 0) { - throw new Error(`Could not find the WDA bundle in '${this.derivedDataPath}'`); - } - return wdaBundlePaths[0]; - } - private setupProxies(sessionId: string): void { const proxyOpts: any = { log: this.log, @@ -549,10 +505,6 @@ export class WebDriverAgent { this.log.info(`Using WDA agent: '${this.agentPath}'`); } - private async isRunning(): Promise { - return !!(await this.getStatus()); - } - /** * Return current running WDA's status like below * { @@ -626,32 +578,6 @@ export class WebDriverAgent { return status; } - /** - * Uninstall WDAs from the test device. - * Over Xcode 11, multiple WDA can be in the device since Xcode 11 generates different WDA. - * Appium does not expect multiple WDAs are running on a device. - */ - private async uninstall(): Promise { - try { - const bundleIds = await this.device.getUserInstalledBundleIdsByBundleName(WDA_CF_BUNDLE_NAME); - if (bundleIds.length === 0) { - this.log.debug('No WDAs on the device.'); - return; - } - - this.log.debug(`Uninstalling WDAs: '${bundleIds}'`); - for (const bundleId of bundleIds) { - await this.device.removeApp(bundleId); - } - } catch (e: any) { - this.log.debug(e); - this.log.warn( - `WebDriverAgent uninstall failed. Perhaps, it is already uninstalled? ` + - `Original error: ${e.message}`, - ); - } - } - private async _cleanupProjectIfFresh(): Promise { if (this.canSkipXcodebuild) { return; @@ -725,9 +651,6 @@ export class WebDriverAgent { * https://github.com/appium/WebDriverAgent/releases * with proper sign for this case. * - * When we implement launching XCTest service via appium-ios-device, - * this implementation can be replaced with it. - * * @param opts launching WDA with devicectl command options. */ private async _launchViaDevicectl( @@ -755,17 +678,7 @@ export class WebDriverAgent { } this.log.info('Launching WebDriverAgent on the device without xcodebuild'); if (this.isRealDevice) { - // Current method to launch WDA process can be done via 'xcrun devicectl', - // but it has limitation about the WDA preinstalled package. - // https://github.com/appium/appium/issues/19206#issuecomment-2014182674 - if (this.platformVersion && util.compareVersions(this.platformVersion, '>=', '17.0')) { - await this._launchViaDevicectl({env: xctestEnv}); - } else { - this.xctestApiClient = new Xctest(this.device.udid, this.bundleIdForXctest, null, { - env: xctestEnv, - }); - await this.xctestApiClient.start(); - } + await this._launchViaDevicectl({env: xctestEnv}); } else { await this.device.simctl.exec('launch', { args: ['--terminate-running-process', this.device.udid, this.bundleIdForXctest], diff --git a/package.json b/package.json index e81f94a9a..1d6ed42c3 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,6 @@ "@appium/base-driver": "^10.3.0", "@appium/strongbox": "^1.0.0-rc.1", "@appium/support": "^7.0.0-rc.1", - "appium-ios-device": "^3.0.0", "appium-ios-simulator": "^8.0.0", "async-lock": "^1.0.0", "asyncbox": "^6.1.0", diff --git a/test/unit/webdriveragent-specs.ts b/test/unit/webdriveragent-specs.ts index cb8cf611f..2617b29c6 100644 --- a/test/unit/webdriveragent-specs.ts +++ b/test/unit/webdriveragent-specs.ts @@ -5,7 +5,7 @@ import {WebDriverAgent} from '../../lib/webdriveragent'; import * as utils from '../../lib/utils'; import path from 'node:path'; import sinon from 'sinon'; -import type {WebDriverAgentArgs, AppleDevice} from '../../lib/types'; +import type {WebDriverAgentArgs} from '../../lib/types'; chai.use(chaiAsPromised); @@ -231,48 +231,42 @@ describe('WebDriverAgent', function () { describe('setupCaching()', function () { let wda: WebDriverAgent; let wdaStub: sinon.SinonStub; - let wdaStubUninstall: sinon.SinonStub; const getTimestampStub = sinon.stub(utils, 'getWDAUpgradeTimestamp'); beforeEach(function () { wda = new WebDriverAgent(fakeConstructorArgs); wdaStub = sinon.stub(wda as any, 'getStatus'); - wdaStubUninstall = sinon.stub(wda as any, 'uninstall'); }); afterEach(function () { - for (const stub of [wdaStub, wdaStubUninstall, getTimestampStub]) { + for (const stub of [wdaStub, getTimestampStub]) { if (stub) { stub.reset(); } } }); - it('should not call uninstall since no Running WDA', async function () { + it('should not cache when no WDA is running', async function () { wdaStub.callsFake(function () { return null; }); - wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.notCalled).to.be.true; expect(wda.webDriverAgentUrl === undefined).to.be.true; }); - it('should not call uninstall since running WDA has only time', async function () { + it('should cache when running WDA has only time', async function () { wdaStub.callsFake(function () { return {build: {time: 'Jun 24 2018 17:08:21'}}; }); - wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.notCalled).to.be.true; expect(wda.webDriverAgentUrl).to.equal('http://127.0.0.1:8100/'); }); - it('should call uninstall once since bundle id is not default without updatedWDABundleId capability', async function () { + it('should not cache when bundle id is not default without updatedWDABundleId capability', async function () { wdaStub.callsFake(function () { return { build: { @@ -281,15 +275,13 @@ describe('WebDriverAgent', function () { }, }; }); - wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.calledOnce).to.be.true; expect(wda.webDriverAgentUrl === undefined).to.be.true; }); - it('should call uninstall once since bundle id is different with updatedWDABundleId capability', async function () { + it('should not cache when bundle id is different with updatedWDABundleId capability', async function () { wdaStub.callsFake(function () { return { build: { @@ -299,21 +291,17 @@ describe('WebDriverAgent', function () { }; }); - wdaStubUninstall.callsFake(() => {}); - await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.calledOnce).to.be.true; expect(wda.webDriverAgentUrl === undefined).to.be.true; }); - it('should not call uninstall since bundle id is equal to updatedWDABundleId capability', async function () { + it('should cache when bundle id is equal to updatedWDABundleId capability', async function () { wda = new WebDriverAgent({ ...fakeConstructorArgs, updatedWDABundleId: 'com.example.WebDriverAgent', }); wdaStub = sinon.stub(wda as any, 'getStatus'); - wdaStubUninstall = sinon.stub(wda as any, 'uninstall'); wdaStub.callsFake(function () { return { @@ -324,115 +312,53 @@ describe('WebDriverAgent', function () { }; }); - wdaStubUninstall.callsFake(() => {}); - await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.notCalled).to.be.true; expect(wda.webDriverAgentUrl).to.equal('http://127.0.0.1:8100/'); }); - it('should call uninstall if current revision differs from the bundled one', async function () { + it('should not cache if current revision differs from the bundled one', async function () { wdaStub.callsFake(function () { return {build: {upgradedAt: '1'}}; }); getTimestampStub.callsFake(async () => 2); - wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.calledOnce).to.be.true; + expect(wda.webDriverAgentUrl === undefined).to.be.true; }); - it('should not call uninstall if current revision is the same as the bundled one', async function () { + it('should cache if current revision is the same as the bundled one', async function () { wdaStub.callsFake(function () { return {build: {upgradedAt: '1'}}; }); getTimestampStub.callsFake(async () => 1); - wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.notCalled).to.be.true; + expect(wda.webDriverAgentUrl).to.equal('http://127.0.0.1:8100/'); }); - it('should not call uninstall if current revision cannot be retrieved from WDA status', async function () { + it('should cache if current revision cannot be retrieved from WDA status', async function () { wdaStub.callsFake(function () { return {build: {}}; }); getTimestampStub.callsFake(async () => 1); - wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.notCalled).to.be.true; + expect(wda.webDriverAgentUrl).to.equal('http://127.0.0.1:8100/'); }); - it('should not call uninstall if current revision cannot be retrieved from the file system', async function () { + it('should cache if current revision cannot be retrieved from the file system', async function () { wdaStub.callsFake(function () { return {build: {upgradedAt: '1'}}; }); getTimestampStub.callsFake(async () => null); - wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.notCalled).to.be.true; - }); - - describe('uninstall', function () { - let device: AppleDevice; - let wda: WebDriverAgent; - let deviceGetBundleIdsStub: sinon.SinonStub; - let deviceRemoveAppStub: sinon.SinonStub; - - beforeEach(function () { - device = { - getUserInstalledBundleIdsByBundleName: () => {}, - removeApp: () => {}, - } as any; - wda = new WebDriverAgent({device} as WebDriverAgentArgs); - deviceGetBundleIdsStub = sinon.stub(device, 'getUserInstalledBundleIdsByBundleName'); - deviceRemoveAppStub = sinon.stub(device, 'removeApp'); - }); - - afterEach(function () { - for (const stub of [deviceGetBundleIdsStub, deviceRemoveAppStub]) { - if (stub) { - stub.reset(); - } - } - }); - - it('should not call uninstall', async function () { - deviceGetBundleIdsStub.callsFake(() => []); - - await (wda as any).uninstall(); - expect(deviceGetBundleIdsStub.calledOnce).to.be.true; - expect(deviceRemoveAppStub.notCalled).to.be.true; - }); - - it('should call uninstall once', async function () { - const uninstalledBundIds: string[] = []; - deviceGetBundleIdsStub.callsFake(() => ['com.appium.WDA1']); - deviceRemoveAppStub.callsFake((id: string) => uninstalledBundIds.push(id)); - - await (wda as any).uninstall(); - expect(deviceGetBundleIdsStub.calledOnce).to.be.true; - expect(deviceRemoveAppStub.calledOnce).to.be.true; - expect(uninstalledBundIds).to.eql(['com.appium.WDA1']); - }); - - it('should call uninstall twice', async function () { - const uninstalledBundIds: string[] = []; - deviceGetBundleIdsStub.callsFake(() => ['com.appium.WDA1', 'com.appium.WDA2']); - deviceRemoveAppStub.callsFake((id: string) => uninstalledBundIds.push(id)); - - await (wda as any).uninstall(); - expect(deviceGetBundleIdsStub.calledOnce).to.be.true; - expect(deviceRemoveAppStub.calledTwice).to.be.true; - expect(uninstalledBundIds).to.eql(['com.appium.WDA1', 'com.appium.WDA2']); - }); + expect(wda.webDriverAgentUrl).to.equal('http://127.0.0.1:8100/'); }); }); From d2b0df982fc42db6ec0c0736fd7a13bfa6ce7036 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sat, 16 May 2026 17:26:46 +0200 Subject: [PATCH 2/7] caching --- lib/webdriveragent.ts | 20 ++++++++++++-------- test/unit/webdriveragent-specs.ts | 18 +++++++++--------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/lib/webdriveragent.ts b/lib/webdriveragent.ts index 33b9353a8..572e9a9ec 100644 --- a/lib/webdriveragent.ts +++ b/lib/webdriveragent.ts @@ -404,12 +404,14 @@ export class WebDriverAgent { /** * Reuse running WDA if it has the same bundle id with updatedWDABundleId. * Or reuse it if it has the default id without updatedWDABundleId. + * + * @returns The WDA URL used for caching on success, or `undefined` if caching was skipped. */ - async setupCaching(): Promise { + async setupCaching(): Promise { const status = await this.getStatus(0); if (!status || !status.build) { this.log.debug('WDA is currently not running. There is nothing to cache'); - return; + return undefined; } const {productBundleIdentifier, upgradedAt} = status.build as any; @@ -422,7 +424,7 @@ export class WebDriverAgent { this.log.info( `Will not reuse running WDA since it has different bundle id. The actual value is '${productBundleIdentifier}'.`, ); - return; + return undefined; } // for simulator if ( @@ -433,7 +435,7 @@ export class WebDriverAgent { this.log.info( `Will not reuse running WDA since its bundle id is not equal to the default value ${WDA_RUNNER_BUNDLE_ID}`, ); - return; + return undefined; } const actualUpgradeTimestamp = await getWDAUpgradeTimestamp(); @@ -448,16 +450,18 @@ export class WebDriverAgent { 'Will not reuse running WDA since it has different version in comparison to the one ' + `which is bundled with appium-xcuitest-driver module (${actualUpgradeTimestamp} != ${upgradedAt})`, ); - return; + return undefined; } + const cachedUrl = this.url.href; const message = util.hasValue(productBundleIdentifier) - ? `Will reuse previously cached WDA instance at '${this.url.href}' with '${productBundleIdentifier}'` - : `Will reuse previously cached WDA instance at '${this.url.href}'`; + ? `Will reuse previously cached WDA instance at '${cachedUrl}' with '${productBundleIdentifier}'` + : `Will reuse previously cached WDA instance at '${cachedUrl}'`; this.log.info( `${message}. Set the wdaLocalPort capability to a value different from ${this.url.port} if this is an undesired behavior.`, ); - this.webDriverAgentUrl = this.url.href; + this.webDriverAgentUrl = cachedUrl; + return cachedUrl; } private setupProxies(sessionId: string): void { diff --git a/test/unit/webdriveragent-specs.ts b/test/unit/webdriveragent-specs.ts index 2617b29c6..bf27b1892 100644 --- a/test/unit/webdriveragent-specs.ts +++ b/test/unit/webdriveragent-specs.ts @@ -251,7 +251,7 @@ describe('WebDriverAgent', function () { return null; }); - await wda.setupCaching(); + expect(await wda.setupCaching()).to.be.undefined; expect(wdaStub.calledOnce).to.be.true; expect(wda.webDriverAgentUrl === undefined).to.be.true; }); @@ -261,7 +261,7 @@ describe('WebDriverAgent', function () { return {build: {time: 'Jun 24 2018 17:08:21'}}; }); - await wda.setupCaching(); + expect(await wda.setupCaching()).to.equal('http://127.0.0.1:8100/'); expect(wdaStub.calledOnce).to.be.true; expect(wda.webDriverAgentUrl).to.equal('http://127.0.0.1:8100/'); }); @@ -276,7 +276,7 @@ describe('WebDriverAgent', function () { }; }); - await wda.setupCaching(); + expect(await wda.setupCaching()).to.be.undefined; expect(wdaStub.calledOnce).to.be.true; expect(wda.webDriverAgentUrl === undefined).to.be.true; }); @@ -291,7 +291,7 @@ describe('WebDriverAgent', function () { }; }); - await wda.setupCaching(); + expect(await wda.setupCaching()).to.be.undefined; expect(wdaStub.calledOnce).to.be.true; expect(wda.webDriverAgentUrl === undefined).to.be.true; }); @@ -312,7 +312,7 @@ describe('WebDriverAgent', function () { }; }); - await wda.setupCaching(); + expect(await wda.setupCaching()).to.equal('http://127.0.0.1:8100/'); expect(wdaStub.calledOnce).to.be.true; expect(wda.webDriverAgentUrl).to.equal('http://127.0.0.1:8100/'); }); @@ -323,7 +323,7 @@ describe('WebDriverAgent', function () { }); getTimestampStub.callsFake(async () => 2); - await wda.setupCaching(); + expect(await wda.setupCaching()).to.be.undefined; expect(wdaStub.calledOnce).to.be.true; expect(wda.webDriverAgentUrl === undefined).to.be.true; }); @@ -334,7 +334,7 @@ describe('WebDriverAgent', function () { }); getTimestampStub.callsFake(async () => 1); - await wda.setupCaching(); + expect(await wda.setupCaching()).to.equal('http://127.0.0.1:8100/'); expect(wdaStub.calledOnce).to.be.true; expect(wda.webDriverAgentUrl).to.equal('http://127.0.0.1:8100/'); }); @@ -345,7 +345,7 @@ describe('WebDriverAgent', function () { }); getTimestampStub.callsFake(async () => 1); - await wda.setupCaching(); + expect(await wda.setupCaching()).to.equal('http://127.0.0.1:8100/'); expect(wdaStub.calledOnce).to.be.true; expect(wda.webDriverAgentUrl).to.equal('http://127.0.0.1:8100/'); }); @@ -356,7 +356,7 @@ describe('WebDriverAgent', function () { }); getTimestampStub.callsFake(async () => null); - await wda.setupCaching(); + expect(await wda.setupCaching()).to.equal('http://127.0.0.1:8100/'); expect(wdaStub.calledOnce).to.be.true; expect(wda.webDriverAgentUrl).to.equal('http://127.0.0.1:8100/'); }); From f85ffce60a94763aeb1a83529472cb7d333df3ff Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sat, 16 May 2026 18:13:35 +0200 Subject: [PATCH 3/7] improve typing --- lib/types.ts | 6 ++++-- lib/webdriveragent.ts | 15 ++++++++++----- package.json | 1 + test/unit/webdriveragent-specs.ts | 6 ++++-- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/types.ts b/lib/types.ts index e748c3bd5..ed342a108 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,4 +1,6 @@ import {type HTTPHeaders} from '@appium/types'; +import type {Simctl} from 'node-simctl'; +import type {Devicectl} from 'node-devicectl'; // WebDriverAgentLib/Utilities/FBSettings.h export interface WDASettings { @@ -99,8 +101,8 @@ export interface WebDriverAgentArgs { export interface AppleDevice { udid: string; - simctl?: any; - devicectl?: any; + simctl?: Simctl; + devicectl?: Devicectl; [key: string]: any; } diff --git a/lib/webdriveragent.ts b/lib/webdriveragent.ts index 572e9a9ec..5c3e48571 100644 --- a/lib/webdriveragent.ts +++ b/lib/webdriveragent.ts @@ -22,6 +22,8 @@ import { } from './constants'; import {strongbox} from '@appium/strongbox'; import type {WebDriverAgentArgs, AppleDevice} from './types'; +import type {Simctl} from 'node-simctl'; +import type {Devicectl} from 'node-devicectl'; const WDA_LAUNCH_TIMEOUT = 60 * 1000; const WDA_AGENT_PORT = 8100; @@ -361,7 +363,11 @@ export class WebDriverAgent { if (this.usePreinstalledWDA) { this.log.info('Stopping the XCTest session'); try { - await this.device.simctl.terminateApp(this.bundleIdForXctest); + if (this.device.simctl) { + await this.device.simctl.terminateApp(this.bundleIdForXctest); + } else if (this.device.devicectl) { + await this.device.devicectl.terminateApp(this.bundleIdForXctest); + } } catch (e: any) { this.log.warn(e.message); } @@ -372,8 +378,7 @@ export class WebDriverAgent { } } else { this.log.debug( - 'Do not stop xcodebuild nor XCTest session ' + - 'since the WDA session is managed by outside this driver.', + 'Stopping neither xcodebuild nor XCTest session since WDA lifecycle is not managed by this driver', ); } @@ -662,7 +667,7 @@ export class WebDriverAgent { ): Promise { const {env} = opts; - await this.device.devicectl.launchApp(this.bundleIdForXctest, {env, terminateExisting: true}); + await (this.device.devicectl as Devicectl).launchApp(this.bundleIdForXctest, {env, terminateExisting: true}); } /** @@ -684,7 +689,7 @@ export class WebDriverAgent { if (this.isRealDevice) { await this._launchViaDevicectl({env: xctestEnv}); } else { - await this.device.simctl.exec('launch', { + await (this.device.simctl as Simctl).exec('launch', { args: ['--terminate-running-process', this.device.udid, this.bundleIdForXctest], env: xctestEnv, }); diff --git a/package.json b/package.json index 1d6ed42c3..26a0bb660 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "chai-as-promised": "^8.0.0", "conventional-changelog-conventionalcommits": "^9.0.0", "mocha": "^11.0.1", + "node-devicectl": "^1.4.0", "node-simctl": "^8.0.0", "prettier": "^3.0.0", "semantic-release": "^25.0.2", diff --git a/test/unit/webdriveragent-specs.ts b/test/unit/webdriveragent-specs.ts index bf27b1892..ec5433ada 100644 --- a/test/unit/webdriveragent-specs.ts +++ b/test/unit/webdriveragent-specs.ts @@ -6,14 +6,16 @@ import * as utils from '../../lib/utils'; import path from 'node:path'; import sinon from 'sinon'; import type {WebDriverAgentArgs} from '../../lib/types'; +import type {Simctl} from 'node-simctl'; +import type {Devicectl} from 'node-devicectl'; chai.use(chaiAsPromised); const fakeConstructorArgs: WebDriverAgentArgs = { device: { udid: 'some-sim-udid', - simctl: {}, - devicectl: {}, + simctl: {} as Simctl, + devicectl: {} as Devicectl, }, platformVersion: '9', host: 'me', From 00646ca7c788bfaf54c3e9a9119d8b2e3fb3b68c Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sun, 17 May 2026 17:54:05 +0200 Subject: [PATCH 4/7] format --- lib/webdriveragent.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/webdriveragent.ts b/lib/webdriveragent.ts index 5c3e48571..750c30a27 100644 --- a/lib/webdriveragent.ts +++ b/lib/webdriveragent.ts @@ -667,7 +667,10 @@ export class WebDriverAgent { ): Promise { const {env} = opts; - await (this.device.devicectl as Devicectl).launchApp(this.bundleIdForXctest, {env, terminateExisting: true}); + await (this.device.devicectl as Devicectl).launchApp(this.bundleIdForXctest, { + env, + terminateExisting: true, + }); } /** From e10c05bb1f238f4fa873d1fee5f887d1b921ede1 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sun, 17 May 2026 18:03:41 +0200 Subject: [PATCH 5/7] tune versions --- .github/workflows/functional-test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index d7647d57a..4291078fd 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -18,7 +18,7 @@ jobs: IOS_MODEL: 'iPhone 17' - HOST_OS: 'macos-15' XCODE_VERSION: '16.4' - IOS_VERSION: '18.4' + IOS_VERSION: '18.5' IOS_MODEL: 'iPhone 16 Plus' - HOST_OS: 'macos-14' XCODE_VERSION: '15.4' @@ -46,6 +46,7 @@ jobs: DEVICE_NAME: ${{matrix.test_targets.IOS_MODEL}} PLATFORM_VERSION: ${{matrix.test_targets.IOS_VERSION}} run: | + xcrun simctl list devices available open -Fn "$(xcode-select -p)/Applications/Simulator.app" udid=$(xcrun simctl list devices available -j | \ node -p "Object.entries(JSON.parse(fs.readFileSync(0)).devices).filter((x) => x[0].includes('$PLATFORM_VERSION'.replace('.', '-'))).reduce((acc, x) => [...acc, ...x[1]], []).find(({name}) => name === '$DEVICE_NAME').udid") From f46fc0443e81fe0dcfa8dabec8a69ef422954d1d Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sun, 17 May 2026 18:19:30 +0200 Subject: [PATCH 6/7] moar --- lib/types.ts | 1 - test/functional/helpers/simulator.ts | 2 +- test/functional/webdriveragent-e2e-specs.ts | 5 ++++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/types.ts b/lib/types.ts index ed342a108..6778739c1 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -103,7 +103,6 @@ export interface AppleDevice { udid: string; simctl?: Simctl; devicectl?: Devicectl; - [key: string]: any; } /** diff --git a/test/functional/helpers/simulator.ts b/test/functional/helpers/simulator.ts index 040367f86..166d4910e 100644 --- a/test/functional/helpers/simulator.ts +++ b/test/functional/helpers/simulator.ts @@ -22,7 +22,7 @@ export async function killAllSimulators(): Promise { export async function shutdownSimulator(device: AppleDevice): Promise { // stop XCTest processes if running to avoid unexpected side effects await resetTestProcesses(device.udid, true); - await device.shutdown(); + await (device.simctl as Simctl).shutdownDevice(); } export async function deleteDeviceWithRetry(udid: string): Promise { diff --git a/test/functional/webdriveragent-e2e-specs.ts b/test/functional/webdriveragent-e2e-specs.ts index 5d98329c1..8a27c42fd 100644 --- a/test/functional/webdriveragent-e2e-specs.ts +++ b/test/functional/webdriveragent-e2e-specs.ts @@ -67,7 +67,10 @@ describe('WebDriverAgent', function () { this.timeout(6 * 60 * 1000); beforeEach(async function () { await killAllSimulators(); - await device.run({startupTimeout: SIM_STARTUP_TIMEOUT_MS}); + await (device.simctl as Simctl).startBootMonitor({ + shouldPreboot: true, + timeout: SIM_STARTUP_TIMEOUT_MS + }); }); afterEach(async function () { try { From 2be6be3367a0114c965993d54f6157d2ef6eb6a2 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sun, 17 May 2026 18:23:40 +0200 Subject: [PATCH 7/7] format --- test/functional/webdriveragent-e2e-specs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/webdriveragent-e2e-specs.ts b/test/functional/webdriveragent-e2e-specs.ts index 8a27c42fd..7eee7eac7 100644 --- a/test/functional/webdriveragent-e2e-specs.ts +++ b/test/functional/webdriveragent-e2e-specs.ts @@ -69,7 +69,7 @@ describe('WebDriverAgent', function () { await killAllSimulators(); await (device.simctl as Simctl).startBootMonitor({ shouldPreboot: true, - timeout: SIM_STARTUP_TIMEOUT_MS + timeout: SIM_STARTUP_TIMEOUT_MS, }); }); afterEach(async function () {