From 9c3e69ec005f5e06c5228dcc629b01ad2d44ca54 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Mon, 27 Apr 2026 18:08:31 +0200 Subject: [PATCH 1/5] feat: Ditch bluebird and lodash --- Scripts/build-webdriveragent.mjs | 11 +- Scripts/fetch-prebuilt-wda.mjs | 9 +- lib/check-dependencies.ts | 33 +++-- lib/utils.ts | 41 +++--- lib/webdriveragent.ts | 233 +++++++++++++++++-------------- lib/xcodebuild.ts | 10 +- 6 files changed, 189 insertions(+), 148 deletions(-) diff --git a/Scripts/build-webdriveragent.mjs b/Scripts/build-webdriveragent.mjs index 7fd864385..cd82cd622 100644 --- a/Scripts/build-webdriveragent.mjs +++ b/Scripts/build-webdriveragent.mjs @@ -20,6 +20,11 @@ const WDA_BUNDLE_TV_PATH = path.join(DERIVED_DATA_PATH, 'Build', 'Products', 'De const TARGETS = ['runner', 'tv_runner']; const SDKS = ['sim', 'tv_sim']; +/** + * Build WebDriverAgent and pack the app bundle into a zip archive. + * + * @param {string} [xcodeVersion] Xcode version to include in archive name. + */ async function buildWebDriverAgent (xcodeVersion) { const target = process.env.TARGET; const sdk = process.env.SDK; @@ -77,10 +82,12 @@ async function buildWebDriverAgent (xcodeVersion) { } if (isMainModule) { - buildWebDriverAgent().catch((e) => { + try { + await buildWebDriverAgent(); + } catch (e) { LOG.error(e); process.exit(1); - }); + } } export default buildWebDriverAgent; diff --git a/Scripts/fetch-prebuilt-wda.mjs b/Scripts/fetch-prebuilt-wda.mjs index 0fa1df409..e576a0333 100644 --- a/Scripts/fetch-prebuilt-wda.mjs +++ b/Scripts/fetch-prebuilt-wda.mjs @@ -12,6 +12,9 @@ const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === __file const log = logger.getLogger('WDA'); +/** + * Download all prebuilt WebDriverAgent archives for the current package version. + */ async function fetchPrebuiltWebDriverAgentAssets () { const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8')); const tag = packageJson.version; @@ -61,10 +64,12 @@ async function fetchPrebuiltWebDriverAgentAssets () { } if (isMainModule) { - fetchPrebuiltWebDriverAgentAssets().catch((e) => { + try { + await fetchPrebuiltWebDriverAgentAssets(); + } catch (e) { log.error(e); process.exit(1); - }); + } } export default fetchPrebuiltWebDriverAgentAssets; diff --git a/lib/check-dependencies.ts b/lib/check-dependencies.ts index 44c204d2c..d4d3390b5 100644 --- a/lib/check-dependencies.ts +++ b/lib/check-dependencies.ts @@ -5,21 +5,9 @@ import {WDA_SCHEME, SDK_SIMULATOR, WDA_RUNNER_APP} from './constants'; import {BOOTSTRAP_PATH} from './utils'; import type {XcodeBuild} from './xcodebuild'; -async function buildWDASim(): Promise { - const args = [ - '-project', - path.join(BOOTSTRAP_PATH, 'WebDriverAgent.xcodeproj'), - '-scheme', - WDA_SCHEME, - '-sdk', - SDK_SIMULATOR, - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED="NO"', - 'GCC_TREAT_WARNINGS_AS_ERRORS=0', - ]; - await exec('xcodebuild', args); -} - +/** + * Ensure simulator WDA is built and return the resulting app bundle path. + */ export async function bundleWDASim(xcodebuild: XcodeBuild): Promise { const derivedDataPath = await xcodebuild.retrieveDerivedDataPath(); if (!derivedDataPath) { @@ -38,3 +26,18 @@ export async function bundleWDASim(xcodebuild: XcodeBuild): Promise { await buildWDASim(); return wdaBundlePath; } + +async function buildWDASim(): Promise { + const args = [ + '-project', + path.join(BOOTSTRAP_PATH, 'WebDriverAgent.xcodeproj'), + '-scheme', + WDA_SCHEME, + '-sdk', + SDK_SIMULATOR, + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED="NO"', + 'GCC_TREAT_WARNINGS_AS_ERRORS=0', + ]; + await exec('xcodebuild', args); +} diff --git a/lib/utils.ts b/lib/utils.ts index 5f8fbe271..fe9cb38a3 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -46,6 +46,20 @@ const getModuleRoot = _.memoize(function getModuleRoot(): string { export const BOOTSTRAP_PATH = getModuleRoot(); +/** + * Arguments for setting xctestrun file + */ +export interface XctestrunFileArgs { + deviceInfo: DeviceInfo; + sdkVersion: string; + bootstrapPath: string; + wdaRemotePort: number | string; + wdaBindingIP?: string; +} + +/** + * Find and terminate all processes matching the given pgrep pattern. + */ export async function killAppUsingPattern(pgrepPattern: string): Promise { const signals = [2, 15, 9]; for (const signal of signals) { @@ -66,13 +80,16 @@ export async function killAppUsingPattern(pgrepPattern: string): Promise { try { await waitForCondition( async () => { - const pidCheckPromises = matchedPids.map((pid) => - exec('kill', ['-0', pid]) + const pidCheckPromises = matchedPids.map(async (pid) => { + try { + await exec('kill', ['-0', pid]); // the process is still alive - .then(() => false) + return false; + } catch { // the process is dead - .catch(() => true), - ); + return true; + } + }); return (await B.all(pidCheckPromises)).every((x) => x === true); }, { @@ -96,6 +113,9 @@ export function isTvOS(platformName: string): boolean { return _.toLower(platformName) === _.toLower(PLATFORM_NAME_TVOS); } +/** + * Configure keychain access required for real-device code signing. + */ export async function setRealDeviceSecurity( keychainPath: string, keychainPassword: string, @@ -106,17 +126,6 @@ export async function setRealDeviceSecurity( await exec('security', ['set-keychain-settings', '-t', '3600', '-l', keychainPath]); } -/** - * Arguments for setting xctestrun file - */ -export interface XctestrunFileArgs { - deviceInfo: DeviceInfo; - sdkVersion: string; - bootstrapPath: string; - wdaRemotePort: number | string; - wdaBindingIP?: string; -} - /** * Creates xctestrun file per device & platform version. * We expects to have WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device diff --git a/lib/webdriveragent.ts b/lib/webdriveragent.ts index 69c8fc52d..27dcb8692 100644 --- a/lib/webdriveragent.ts +++ b/lib/webdriveragent.ts @@ -1,7 +1,6 @@ import {waitForCondition} from 'asyncbox'; import _ from 'lodash'; import path from 'node:path'; -import url from 'node:url'; import B from 'bluebird'; import {JWProxy} from '@appium/base-driver'; import {fs, util, plist} from '@appium/support'; @@ -34,41 +33,42 @@ 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 = '://'; export class WebDriverAgent { bootstrapPath: string; agentPath: string; readonly args: WebDriverAgentArgs; - private readonly log: AppiumLogger; readonly device: AppleDevice; readonly platformVersion?: string; readonly platformName?: string; readonly iosSdkVersion?: string; readonly host?: string; readonly isRealDevice: boolean; - private readonly wdaBundlePath?: string; - private readonly wdaLocalPort?: number; readonly wdaRemotePort: number; readonly wdaBaseUrl: string; readonly wdaBindingIP?: string; - private readonly prebuildWDA?: boolean; webDriverAgentUrl?: string; started: boolean; + updatedWDABundleId?: string; + noSessionProxy?: NoSessionProxy; + jwproxy?: JWProxy; + proxyReqRes?: any; + private readonly log: AppiumLogger; + private readonly wdaBundlePath?: string; + private readonly wdaLocalPort?: number; + private readonly prebuildWDA?: boolean; private readonly wdaConnectionTimeout?: number; private readonly useXctestrunFile?: boolean; private readonly usePrebuiltWDA?: boolean; private readonly derivedDataPath?: string; private readonly mjpegServerPort?: number; - updatedWDABundleId?: string; private readonly wdaLaunchTimeout: number; private readonly usePreinstalledWDA?: boolean; - private xctestApiClient?: Xctest | null; private readonly updatedWDABundleIdSuffix: string; + private xctestApiClient?: Xctest | null; private _xcodebuild?: XcodeBuild | null; - noSessionProxy?: NoSessionProxy; - jwproxy?: JWProxy; - proxyReqRes?: any; - private _url?: url.UrlWithStringQuery; + private _url?: URL; /** * Creates a new WebDriverAgent instance. @@ -185,6 +185,62 @@ export class WebDriverAgent { return `${this.updatedWDABundleId ? this.updatedWDABundleId : WDA_RUNNER_BUNDLE_ID}${this.updatedWDABundleIdSuffix}`; } + /** + * Gets the base path for the WebDriverAgent URL. + * @returns The base path (empty string if root path) + */ + get basePath(): string { + if (this.url.pathname === '/') { + return ''; + } + return this.url.pathname || ''; + } + + /** + * Gets the WebDriverAgent URL. + * Constructs the URL from webDriverAgentUrl if provided, otherwise + * builds it from wdaBaseUrl, wdaBindingIP, and wdaLocalPort. + * @returns The parsed URL object + */ + get url(): URL { + if (!this._url) { + if (this.webDriverAgentUrl) { + this._url = this.toUrl(this.webDriverAgentUrl); + } else { + const port = this.wdaLocalPort || WDA_AGENT_PORT; + const parsedBaseUrl = this.toUrl(this.wdaBaseUrl || WDA_BASE_URL); + this._url = new URL( + `${parsedBaseUrl.protocol}//${this.wdaBindingIP || parsedBaseUrl.hostname}:${port}`, + ); + } + } + return this._url; + } + + /** + * Gets whether WebDriverAgent has fully started. + * @returns `true` if WDA has started, `false` otherwise + */ + get fullyStarted(): boolean { + return this.started; + } + + /** + * Sets whether WebDriverAgent has fully started. + * @param started - `true` if WDA has started, `false` otherwise + */ + set fullyStarted(started: boolean) { + this.started = started ?? false; + } + + /** + * Sets the WebDriverAgent URL. + * @param _url - The URL string to parse and set + */ + set url(_url: string) { + this._url = this.toUrl(_url); + } + /** * Cleans up obsolete cached processes from previous WDA sessions * that are listening on the same port but belong to different devices. @@ -220,14 +276,6 @@ export class WebDriverAgent { } /** - * Gets the base path for the WebDriverAgent URL. - * @returns The base path (empty string if root path) - */ - get basePath(): string { - if (this.url.path === '/') { - return ''; - } - return this.url.path || ''; } /** @@ -313,52 +361,6 @@ export class WebDriverAgent { return (await B.all(existsPromises)).some((v) => v === false); } - 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 (_.isEmpty(wdaBundlePaths)) { - 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, - server: this.url.hostname ?? undefined, - port: parseInt(this.url.port ?? '', 10) || undefined, - base: this.basePath, - timeout: this.wdaConnectionTimeout, - keepAlive: true, - scheme: this.url.protocol ? this.url.protocol.replace(':', '') : 'http', - headers: this.args.extraRequestHeaders, - }; - if (this.args.reqBasePath) { - proxyOpts.reqBasePath = this.args.reqBasePath; - } - - this.jwproxy = new JWProxy(proxyOpts); - this.jwproxy.sessionId = sessionId; - this.proxyReqRes = this.jwproxy.proxyReqRes.bind(this.jwproxy); - - this.noSessionProxy = new NoSessionProxy(proxyOpts); - } - /** * Stops the WebDriverAgent session and cleans up resources. * Handles both preinstalled WDA and xcodebuild-based sessions. @@ -401,49 +403,6 @@ export class WebDriverAgent { } } - /** - * Gets the WebDriverAgent URL. - * Constructs the URL from webDriverAgentUrl if provided, otherwise - * builds it from wdaBaseUrl, wdaBindingIP, and wdaLocalPort. - * @returns The parsed URL object - */ - get url(): url.UrlWithStringQuery { - if (!this._url) { - if (this.webDriverAgentUrl) { - this._url = url.parse(this.webDriverAgentUrl); - } else { - const port = this.wdaLocalPort || WDA_AGENT_PORT; - const {protocol, hostname} = url.parse(this.wdaBaseUrl || WDA_BASE_URL); - this._url = url.parse(`${protocol}//${this.wdaBindingIP || hostname}:${port}`); - } - } - return this._url; - } - - /** - * Sets the WebDriverAgent URL. - * @param _url - The URL string to parse and set - */ - set url(_url: string) { - this._url = url.parse(_url); - } - - /** - * Gets whether WebDriverAgent has fully started. - * @returns `true` if WDA has started, `false` otherwise - */ - get fullyStarted(): boolean { - return this.started; - } - - /** - * Sets whether WebDriverAgent has fully started. - * @param started - `true` if WDA has started, `false` otherwise - */ - set fullyStarted(started: boolean) { - this.started = started ?? false; - } - /** * Retrieves the Xcode derived data path for WebDriverAgent. * @returns The derived data path, or `undefined` if xcodebuild is skipped @@ -523,6 +482,64 @@ export class WebDriverAgent { 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 (_.isEmpty(wdaBundlePaths)) { + 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, + server: this.url.hostname ?? undefined, + port: parseInt(this.url.port ?? '', 10) || undefined, + base: this.basePath, + timeout: this.wdaConnectionTimeout, + keepAlive: true, + scheme: this.url.protocol ? this.url.protocol.replace(':', '') : 'http', + headers: this.args.extraRequestHeaders, + }; + if (this.args.reqBasePath) { + proxyOpts.reqBasePath = this.args.reqBasePath; + } + + this.jwproxy = new JWProxy(proxyOpts); + this.jwproxy.sessionId = sessionId; + this.proxyReqRes = this.jwproxy.proxyReqRes.bind(this.jwproxy); + + this.noSessionProxy = new NoSessionProxy(proxyOpts); + } + + private toUrl(value: string): URL { + try { + return new URL(value); + } catch (err) { + // Keep compatibility with legacy parser behavior for values like "localhost:8100". + if (!value.includes(URL_PROTOCOL_SEPARATOR)) { + return new URL(`http://${value}`); + } + throw err; + } + } + private setWDAPaths(bootstrapPath?: string, agentPath?: string): void { // allow the user to specify a place for WDA. This is undocumented and // only here for the purposes of testing development of WDA diff --git a/lib/xcodebuild.ts b/lib/xcodebuild.ts index 754c87943..ef2dfc8d5 100644 --- a/lib/xcodebuild.ts +++ b/lib/xcodebuild.ts @@ -44,27 +44,28 @@ const xcodeLog = logger.getLogger('Xcode'); export class XcodeBuild { xcodebuild?: SubProcess; readonly device: AppleDevice; - private readonly log: AppiumLogger; readonly realDevice: boolean; readonly agentPath: string; readonly bootstrapPath: string; readonly platformVersion?: string; readonly platformName?: string; readonly iosSdkVersion?: string; + readonly xcodeSigningId: string; + usePrebuiltWDA?: boolean; + derivedDataPath?: string; + agentUrl?: string; + private readonly log: AppiumLogger; private readonly showXcodeLog?: boolean; private readonly xcodeConfigFile?: string; private readonly xcodeOrgId?: string; - readonly xcodeSigningId: string; private readonly keychainPath?: string; private readonly keychainPassword?: string; - usePrebuiltWDA?: boolean; private readonly useSimpleBuildTest?: boolean; private readonly useXctestrunFile?: boolean; private readonly launchTimeout?: number; private readonly wdaRemotePort?: number; private readonly wdaBindingIP?: string; private readonly updatedWDABundleId?: string; - derivedDataPath?: string; private readonly mjpegServerPort?: number; private readonly prebuildDelay: number; private readonly allowProvisioningDeviceRegistration?: boolean; @@ -75,7 +76,6 @@ export class XcodeBuild { private _derivedDataPathPromise?: Promise; private noSessionProxy?: NoSessionProxy; private xctestrunFilePath?: string; - agentUrl?: string; /** * Creates a new XcodeBuild instance. From 34efb7d9496d63fede5859efe062ee34b92cbd78 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Mon, 27 Apr 2026 18:11:07 +0200 Subject: [PATCH 2/5] ditch bluebird --- Scripts/fetch-prebuilt-wda.mjs | 3 +-- lib/utils.ts | 32 +++++++++++++++++--------------- lib/webdriveragent.ts | 3 +-- lib/xcodebuild.ts | 5 ++--- package.json | 4 +--- 5 files changed, 22 insertions(+), 25 deletions(-) diff --git a/Scripts/fetch-prebuilt-wda.mjs b/Scripts/fetch-prebuilt-wda.mjs index e576a0333..9cab60f1d 100644 --- a/Scripts/fetch-prebuilt-wda.mjs +++ b/Scripts/fetch-prebuilt-wda.mjs @@ -4,7 +4,6 @@ import { readFileSync } from 'node:fs'; import axios from 'axios'; import { logger, fs, mkdirp, net } from '@appium/support'; import _ from 'lodash'; -import B from 'bluebird'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -60,7 +59,7 @@ async function fetchPrebuiltWebDriverAgentAssets () { } // Wait for them all to finish - return await B.all(agentsDownloading); + return await Promise.all(agentsDownloading); } if (isMainModule) { diff --git a/lib/utils.ts b/lib/utils.ts index fe9cb38a3..6609d03de 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -5,7 +5,6 @@ import {fileURLToPath} from 'node:url'; import {log} from './logger'; import _ from 'lodash'; import {PLATFORM_NAME_TVOS} from './constants'; -import B from 'bluebird'; import _fs from 'node:fs'; import {waitForCondition} from 'asyncbox'; import {arch} from 'node:os'; @@ -90,7 +89,7 @@ export async function killAppUsingPattern(pgrepPattern: string): Promise { return true; } }); - return (await B.all(pidCheckPromises)).every((x) => x === true); + return (await Promise.all(pidCheckPromises)).every((x) => x === true); }, { waitMs: 1000, @@ -305,7 +304,7 @@ export async function resetTestProcesses(udid: string, isSimulator: boolean): Pr processPatterns.push(`xctest.*${udid}`); } log.debug(`Killing running processes '${processPatterns.join(', ')}' for the device ${udid}...`); - await B.all(processPatterns.map(killAppUsingPattern)); + await Promise.all(processPatterns.map(killAppUsingPattern)); } /** @@ -341,19 +340,22 @@ export async function getPIDsListeningOnPort( if (!_.isFunction(filteringFunc)) { return result; } - return await B.filter(result, async (pid) => { - let stdout: string; - try { - ({stdout} = await exec('ps', ['-p', pid, '-o', 'command'])); - } catch (e: any) { - if (e.code === 1) { - // The process does not exist anymore, there's nothing to filter - return false; + const filtered = await Promise.all( + result.map(async (pid) => { + let stdout: string; + try { + ({stdout} = await exec('ps', ['-p', pid, '-o', 'command'])); + } catch (e: any) { + if (e.code === 1) { + // The process does not exist anymore, there's nothing to filter + return null; + } + throw e; } - throw e; - } - return await filteringFunc(stdout); - }); + return (await filteringFunc(stdout)) ? pid : null; + }), + ); + return filtered.filter((pid): pid is string => Boolean(pid)); } // Private functions diff --git a/lib/webdriveragent.ts b/lib/webdriveragent.ts index 27dcb8692..66999572f 100644 --- a/lib/webdriveragent.ts +++ b/lib/webdriveragent.ts @@ -1,7 +1,6 @@ import {waitForCondition} from 'asyncbox'; import _ from 'lodash'; import path from 'node:path'; -import B from 'bluebird'; import {JWProxy} from '@appium/base-driver'; import {fs, util, plist} from '@appium/support'; import type {AppiumLogger, StringRecord} from '@appium/types'; @@ -358,7 +357,7 @@ export class WebDriverAgent { const existsPromises = ['Resources', `Resources${path.sep}WebDriverAgent.bundle`].map( (subPath) => fs.exists(path.resolve(this.bootstrapPath, subPath)), ); - return (await B.all(existsPromises)).some((v) => v === false); + return (await Promise.all(existsPromises)).some((v) => v === false); } /** diff --git a/lib/xcodebuild.ts b/lib/xcodebuild.ts index ef2dfc8d5..8bc20bc9a 100644 --- a/lib/xcodebuild.ts +++ b/lib/xcodebuild.ts @@ -3,7 +3,6 @@ import {SubProcess, exec} from 'teen_process'; import {logger, timing} from '@appium/support'; import type {AppiumLogger, StringRecord} from '@appium/types'; import {log as defaultLogger} from './logger'; -import B from 'bluebird'; import { setRealDeviceSecurity, setXctestrunFile, @@ -207,7 +206,7 @@ export class XcodeBuild { if (this.prebuildDelay > 0) { // pause a moment - await B.delay(this.prebuildDelay); + await new Promise((resolve) => setTimeout(resolve, this.prebuildDelay)); } } @@ -242,7 +241,7 @@ export class XcodeBuild { throw new Error('xcodebuild subprocess was not created'); } const xcodebuild = this.xcodebuild; - return await new B((resolve, reject) => { + return await new Promise((resolve, reject) => { xcodebuild.once('exit', (code, signal) => { xcodeLog.error(`xcodebuild exited with code '${code}' and signal '${signal}'`); xcodebuild.removeAllListeners(); diff --git a/package.json b/package.json index 644b46cbc..576ad6ae4 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "@appium/types": "^1.0.0-rc.1", "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", - "@types/bluebird": "^3.5.38", "@types/lodash": "^4.14.196", "@types/mocha": "^10.0.1", "@types/node": "^25.0.0", @@ -62,8 +61,8 @@ "chai": "^6.0.0", "chai-as-promised": "^8.0.0", "conventional-changelog-conventionalcommits": "^9.0.0", - "node-simctl": "^8.0.0", "mocha": "^11.0.1", + "node-simctl": "^8.0.0", "prettier": "^3.0.0", "semantic-release": "^25.0.2", "semver": "^7.3.7", @@ -80,7 +79,6 @@ "async-lock": "^1.0.0", "asyncbox": "^6.1.0", "axios": "^1.4.0", - "bluebird": "^3.5.5", "lodash": "^4.17.11", "teen_process": "^4.0.7" }, From 7af1f7bb9d754f8efbb1b131c7de4419aa2319e4 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Mon, 27 Apr 2026 18:19:12 +0200 Subject: [PATCH 3/5] ditch lodash --- Scripts/fetch-prebuilt-wda.mjs | 6 ++- lib/utils.ts | 67 ++++++++++++++++++++++++---- lib/webdriveragent.ts | 13 +++--- lib/xcodebuild.ts | 11 ++--- package.json | 2 - test/functional/helpers/simulator.ts | 3 +- test/unit/webdriveragent-specs.ts | 59 +++++++++++------------- 7 files changed, 101 insertions(+), 60 deletions(-) diff --git a/Scripts/fetch-prebuilt-wda.mjs b/Scripts/fetch-prebuilt-wda.mjs index 9cab60f1d..782cec9d4 100644 --- a/Scripts/fetch-prebuilt-wda.mjs +++ b/Scripts/fetch-prebuilt-wda.mjs @@ -3,7 +3,6 @@ import { fileURLToPath } from 'node:url'; import { readFileSync } from 'node:fs'; import axios from 'axios'; import { logger, fs, mkdirp, net } from '@appium/support'; -import _ from 'lodash'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -53,7 +52,10 @@ async function fetchPrebuiltWebDriverAgentAssets () { const url = asset.browser_download_url; log.info(`Downloading: ${url}`); try { - const nameOfAgent = _.last(url.split('/')); + const nameOfAgent = url.split('/').at(-1); + if (!nameOfAgent) { + continue; + } agentsDownloading.push(downloadAgent(url, path.join(webdriveragentsDir, nameOfAgent))); } catch { } } diff --git a/lib/utils.ts b/lib/utils.ts index 6609d03de..7934db20f 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -3,7 +3,6 @@ import {exec, SubProcess} from 'teen_process'; import path, {dirname} from 'node:path'; import {fileURLToPath} from 'node:url'; import {log} from './logger'; -import _ from 'lodash'; import {PLATFORM_NAME_TVOS} from './constants'; import _fs from 'node:fs'; import {waitForCondition} from 'asyncbox'; @@ -18,13 +17,18 @@ const currentFilename = const currentDirname = dirname(currentFilename); +let moduleRootCache: string | undefined; + /** * Calculates the path to the current module's root folder * * @returns {string} The full path to module root * @throws {Error} If the current module root folder cannot be determined */ -const getModuleRoot = _.memoize(function getModuleRoot(): string { +const getModuleRoot = function getModuleRoot(): string { + if (moduleRootCache) { + return moduleRootCache; + } let currentDir = currentDirname; let isAtFsRoot = false; while (!isAtFsRoot) { @@ -34,6 +38,7 @@ const getModuleRoot = _.memoize(function getModuleRoot(): string { _fs.existsSync(manifestPath) && JSON.parse(_fs.readFileSync(manifestPath, 'utf8')).name === 'appium-webdriveragent' ) { + moduleRootCache = currentDir; return currentDir; } } catch {} @@ -41,7 +46,7 @@ const getModuleRoot = _.memoize(function getModuleRoot(): string { isAtFsRoot = currentDir.length <= path.dirname(currentDir).length; } throw new Error('Cannot find the root folder of the appium-webdriveragent Node.js module'); -}); +}; export const BOOTSTRAP_PATH = getModuleRoot(); @@ -63,7 +68,7 @@ export async function killAppUsingPattern(pgrepPattern: string): Promise { const signals = [2, 15, 9]; for (const signal of signals) { const matchedPids = await getPIDsUsingPattern(pgrepPattern); - if (_.isEmpty(matchedPids)) { + if (matchedPids.length === 0) { return; } const args = [`-${signal}`, ...matchedPids]; @@ -72,7 +77,7 @@ export async function killAppUsingPattern(pgrepPattern: string): Promise { } catch (err: any) { log.debug(`kill ${args.join(' ')} -> ${err.message}`); } - if (signal === _.last(signals)) { + if (signal === signals[signals.length - 1]) { // there is no need to wait after SIGKILL return; } @@ -109,7 +114,7 @@ export async function killAppUsingPattern(pgrepPattern: string): Promise { * @returns Return true if the platformName is tvOS */ export function isTvOS(platformName: string): boolean { - return _.toLower(platformName) === _.toLower(PLATFORM_NAME_TVOS); + return platformName?.toLowerCase() === PLATFORM_NAME_TVOS.toLowerCase(); } /** @@ -148,7 +153,7 @@ export async function setXctestrunFile(args: XctestrunFileArgs): Promise wdaRemotePort, wdaBindingIP, ); - const newXctestRunContent = _.merge(xctestRunContent, updateWDAPort); + const newXctestRunContent = mergeObjects(xctestRunContent, updateWDAPort); await plist.updatePlistFile(xctestrunFilePath, newXctestRunContent, true); return xctestrunFilePath; @@ -293,6 +298,23 @@ export async function getWDAUpgradeTimestamp(): Promise { return mtime.getTime(); } +/** + * Escape regular expression metacharacters in a string. + */ +export function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** + * Truncate a string to the given length and append ellipsis if needed. + */ +export function truncateString(value: string, length: number): string { + if (value.length <= length) { + return value; + } + return `${value.slice(0, Math.max(0, length - 1))}…`; +} + /** * Kills running XCTest processes for the particular device. */ @@ -337,7 +359,7 @@ export async function getPIDsListeningOnPort( return result; } - if (!_.isFunction(filteringFunc)) { + if (typeof filteringFunc !== 'function') { return result; } const filtered = await Promise.all( @@ -370,7 +392,7 @@ async function getPIDsUsingPattern(pattern: string): Promise { return stdout .split(/\s+/) .map((x) => parseInt(x, 10)) - .filter(_.isInteger) + .filter(Number.isInteger) .map((x) => `${x}`); } catch (err: any) { log.debug( @@ -379,3 +401,30 @@ async function getPIDsUsingPattern(pattern: string): Promise { return []; } } + +function mergeObjects, U extends Record>( + target: T, + source: U, +): T & U { + const output: Record = {...target}; + for (const [key, sourceValue] of Object.entries(source)) { + const targetValue = output[key]; + if ( + isPlainObject(targetValue) && + isPlainObject(sourceValue) + ) { + output[key] = mergeObjects(targetValue, sourceValue); + continue; + } + output[key] = sourceValue; + } + return output as T & U; +} + +function isPlainObject(value: unknown): value is Record { + if (value == null || typeof value !== 'object' || Array.isArray(value)) { + return false; + } + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} diff --git a/lib/webdriveragent.ts b/lib/webdriveragent.ts index 66999572f..4fded8f5c 100644 --- a/lib/webdriveragent.ts +++ b/lib/webdriveragent.ts @@ -1,5 +1,4 @@ import {waitForCondition} from 'asyncbox'; -import _ from 'lodash'; import path from 'node:path'; import {JWProxy} from '@appium/base-driver'; import {fs, util, plist} from '@appium/support'; @@ -75,7 +74,7 @@ export class WebDriverAgent { * @param log - Optional logger instance */ constructor(args: WebDriverAgentArgs, log: AppiumLogger | null = null) { - this.args = _.clone(args); + this.args = {...args}; this.log = log ?? defaultLogger; this.device = args.device; @@ -252,7 +251,7 @@ export class WebDriverAgent { !cmdLine.toLowerCase().includes(this.device.udid.toLowerCase()), ); - if (_.isEmpty(obsoletePids)) { + if (obsoletePids.length === 0) { this.log.debug( `No obsolete cached processes from previous WDA sessions ` + `listening on port ${this.url.port} have been found`, @@ -455,7 +454,7 @@ export class WebDriverAgent { if ( actualUpgradeTimestamp && upgradedAt && - _.toLower(`${actualUpgradeTimestamp}`) !== _.toLower(`${upgradedAt}`) + `${actualUpgradeTimestamp}`.toLowerCase() !== `${upgradedAt}`.toLowerCase() ) { this.log.info( 'Will uninstall running WDA since it has different version in comparison to the one ' + @@ -499,7 +498,7 @@ export class WebDriverAgent { const wdaBundlePaths = await fs.glob(`${this.derivedDataPath}/**/*${WDA_RUNNER_APP}/`, { absolute: true, }); - if (_.isEmpty(wdaBundlePaths)) { + if (wdaBundlePaths.length === 0) { throw new Error(`Could not find the WDA bundle in '${this.derivedDataPath}'`); } return wdaBundlePaths[0]; @@ -588,7 +587,7 @@ export class WebDriverAgent { const sendGetStatus = async () => (await noSessionProxy.command('/status', 'GET')) as StringRecord; - if (_.isNil(timeoutMs) || timeoutMs <= 0) { + if (timeoutMs == null || timeoutMs <= 0) { try { return await sendGetStatus(); } catch (err: any) { @@ -635,7 +634,7 @@ export class WebDriverAgent { private async uninstall(): Promise { try { const bundleIds = await this.device.getUserInstalledBundleIdsByBundleName(WDA_CF_BUNDLE_NAME); - if (_.isEmpty(bundleIds)) { + if (bundleIds.length === 0) { this.log.debug('No WDAs on the device.'); return; } diff --git a/lib/xcodebuild.ts b/lib/xcodebuild.ts index 8bc20bc9a..0bcb0504b 100644 --- a/lib/xcodebuild.ts +++ b/lib/xcodebuild.ts @@ -9,8 +9,9 @@ import { killProcess, getWDAUpgradeTimestamp, isTvOS, + escapeRegExp, + truncateString, } from './utils'; -import _ from 'lodash'; import path from 'node:path'; import {WDA_RUNNER_BUNDLE_ID} from './constants'; import type {AppleDevice, XcodeBuildArgs} from './types'; @@ -29,7 +30,7 @@ const IGNORED_ERRORS = [ 'Failed to remove screenshot at path', ]; const IGNORED_ERRORS_PATTERN = new RegExp( - '(' + IGNORED_ERRORS.map((errStr) => _.escapeRegExp(errStr)).join('|') + ')', + '(' + IGNORED_ERRORS.map((errStr) => escapeRegExp(errStr)).join('|') + ')', ); const RUNNER_SCHEME_TV = 'WebDriverAgentRunner_tvOS'; @@ -118,7 +119,7 @@ export class XcodeBuild { this.mjpegServerPort = args.mjpegServerPort; - this.prebuildDelay = _.isNumber(args.prebuildDelay) ? args.prebuildDelay : PREBUILD_DELAY; + this.prebuildDelay = typeof args.prebuildDelay === 'number' ? args.prebuildDelay : PREBUILD_DELAY; this.allowProvisioningDeviceRegistration = args.allowProvisioningDeviceRegistration; @@ -182,7 +183,7 @@ export class XcodeBuild { const pattern = /^\s*BUILD_DIR\s+=\s+(\/.*)/m; const match = pattern.exec(stdout); if (!match) { - this.log.warn(`Cannot parse WDA build dir from ${_.truncate(stdout, {length: 300})}`); + this.log.warn(`Cannot parse WDA build dir from ${truncateString(stdout, 300)}`); return; } this.log.debug(`Parsed BUILD_DIR configuration value: '${match[1]}'`); @@ -414,7 +415,7 @@ export class XcodeBuild { }); let logXcodeOutput = !!this.showXcodeLog; - const logMsg = _.isBoolean(this.showXcodeLog) + const logMsg = typeof this.showXcodeLog === 'boolean' ? `Output from xcodebuild ${this.showXcodeLog ? 'will' : 'will not'} be logged` : 'Output from xcodebuild will only be logged if any errors are present there'; this.log.debug(`${logMsg}. To change this, use 'showXcodeLog' desired capability`); diff --git a/package.json b/package.json index 576ad6ae4..43e4cc434 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "@appium/types": "^1.0.0-rc.1", "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", - "@types/lodash": "^4.14.196", "@types/mocha": "^10.0.1", "@types/node": "^25.0.0", "appium-xcode": "^6.0.0", @@ -79,7 +78,6 @@ "async-lock": "^1.0.0", "asyncbox": "^6.1.0", "axios": "^1.4.0", - "lodash": "^4.17.11", "teen_process": "^4.0.7" }, "files": [ diff --git a/test/functional/helpers/simulator.ts b/test/functional/helpers/simulator.ts index 36a4df3e1..040367f86 100644 --- a/test/functional/helpers/simulator.ts +++ b/test/functional/helpers/simulator.ts @@ -1,4 +1,3 @@ -import _ from 'lodash'; import {Simctl} from 'node-simctl'; import {retryInterval} from 'asyncbox'; import {killAllSimulators as simKill} from 'appium-ios-simulator'; @@ -7,7 +6,7 @@ import type {AppleDevice} from '../../../lib/types'; export async function killAllSimulators(): Promise { const simctl = new Simctl(); - const allDevices = _.flatMap(_.values(await simctl.getDevices())); + const allDevices = Object.values(await simctl.getDevices()).flat(); const bootedDevices = allDevices.filter((device) => device.state === 'Booted'); for (const {udid} of bootedDevices) { diff --git a/test/unit/webdriveragent-specs.ts b/test/unit/webdriveragent-specs.ts index d311c12e8..844ebfab0 100644 --- a/test/unit/webdriveragent-specs.ts +++ b/test/unit/webdriveragent-specs.ts @@ -4,7 +4,6 @@ import {BOOTSTRAP_PATH} from '../../lib/utils'; import {WebDriverAgent} from '../../lib/webdriveragent'; import * as utils from '../../lib/utils'; import path from 'node:path'; -import _ from 'lodash'; import sinon from 'sinon'; import type {WebDriverAgentArgs, AppleDevice} from '../../lib/types'; @@ -35,37 +34,31 @@ describe('WebDriverAgent', function () { }); it('should have custom wda bootstrap and default agent if only bootstrap specified', function () { const agent = new WebDriverAgent( - _.defaults( - { - bootstrapPath: customBootstrapPath, - }, - fakeConstructorArgs, - ), + { + ...fakeConstructorArgs, + bootstrapPath: customBootstrapPath, + }, ); expect(agent.bootstrapPath).to.eql(customBootstrapPath); expect(agent.agentPath).to.eql(path.resolve(customBootstrapPath, 'WebDriverAgent.xcodeproj')); }); it('should have custom wda bootstrap and agent if both specified', function () { const agent = new WebDriverAgent( - _.defaults( - { - bootstrapPath: customBootstrapPath, - agentPath: customAgentPath, - }, - fakeConstructorArgs, - ), + { + ...fakeConstructorArgs, + bootstrapPath: customBootstrapPath, + agentPath: customAgentPath, + }, ); expect(agent.bootstrapPath).to.eql(customBootstrapPath); expect(agent.agentPath).to.eql(customAgentPath); }); it('should have custom derivedDataPath if specified', function () { const agent = new WebDriverAgent( - _.defaults( - { - derivedDataPath: customDerivedDataPath, - }, - fakeConstructorArgs, - ), + { + ...fakeConstructorArgs, + derivedDataPath: customDerivedDataPath, + }, ); if (agent.xcodebuild) { expect(agent.xcodebuild.derivedDataPath).to.eql(customDerivedDataPath); @@ -117,7 +110,7 @@ describe('WebDriverAgent', function () { expect(agent.url.port).to.eql('8100'); expect(agent.url.hostname).to.eql('127.0.0.1'); - expect(agent.url.path).to.eql('/aabbccdd'); + expect(agent.url.pathname).to.eql('/aabbccdd'); if (agent.jwproxy) { expect(agent.jwproxy.server).to.eql('127.0.0.1'); expect(agent.jwproxy.port).to.eql(8100); @@ -247,19 +240,19 @@ describe('WebDriverAgent', function () { wdaStub.callsFake(function () { return null; }); - wdaStubUninstall.callsFake(_.noop); + wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; expect(wdaStubUninstall.notCalled).to.be.true; - expect(_.isUndefined(wda.webDriverAgentUrl)).to.be.true; + expect(wda.webDriverAgentUrl === undefined).to.be.true; }); it('should not call uninstall since running WDA has only time', async function () { wdaStub.callsFake(function () { return {build: {time: 'Jun 24 2018 17:08:21'}}; }); - wdaStubUninstall.callsFake(_.noop); + wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; @@ -276,12 +269,12 @@ describe('WebDriverAgent', function () { }, }; }); - wdaStubUninstall.callsFake(_.noop); + wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; expect(wdaStubUninstall.calledOnce).to.be.true; - expect(_.isUndefined(wda.webDriverAgentUrl)).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 () { @@ -294,12 +287,12 @@ describe('WebDriverAgent', function () { }; }); - wdaStubUninstall.callsFake(_.noop); + wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; expect(wdaStubUninstall.calledOnce).to.be.true; - expect(_.isUndefined(wda.webDriverAgentUrl)).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 () { @@ -319,7 +312,7 @@ describe('WebDriverAgent', function () { }; }); - wdaStubUninstall.callsFake(_.noop); + wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; @@ -332,7 +325,7 @@ describe('WebDriverAgent', function () { return {build: {upgradedAt: '1'}}; }); getTimestampStub.callsFake(() => '2'); - wdaStubUninstall.callsFake(_.noop); + wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; @@ -344,7 +337,7 @@ describe('WebDriverAgent', function () { return {build: {upgradedAt: '1'}}; }); getTimestampStub.callsFake(() => '1'); - wdaStubUninstall.callsFake(_.noop); + wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; @@ -356,7 +349,7 @@ describe('WebDriverAgent', function () { return {build: {}}; }); getTimestampStub.callsFake(() => '1'); - wdaStubUninstall.callsFake(_.noop); + wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; @@ -368,7 +361,7 @@ describe('WebDriverAgent', function () { return {build: {upgradedAt: '1'}}; }); getTimestampStub.callsFake(() => null); - wdaStubUninstall.callsFake(_.noop); + wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; From aa8270a0c4d366af74e0efa398fc5f368ce60652 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Mon, 27 Apr 2026 18:20:45 +0200 Subject: [PATCH 4/5] format --- lib/utils.ts | 5 +---- lib/xcodebuild.ts | 10 ++++++---- test/unit/webdriveragent-specs.ts | 32 +++++++++++++------------------ 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/lib/utils.ts b/lib/utils.ts index 7934db20f..f94b442ce 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -409,10 +409,7 @@ function mergeObjects, U extends Record = {...target}; for (const [key, sourceValue] of Object.entries(source)) { const targetValue = output[key]; - if ( - isPlainObject(targetValue) && - isPlainObject(sourceValue) - ) { + if (isPlainObject(targetValue) && isPlainObject(sourceValue)) { output[key] = mergeObjects(targetValue, sourceValue); continue; } diff --git a/lib/xcodebuild.ts b/lib/xcodebuild.ts index 0bcb0504b..df89a52ca 100644 --- a/lib/xcodebuild.ts +++ b/lib/xcodebuild.ts @@ -119,7 +119,8 @@ export class XcodeBuild { this.mjpegServerPort = args.mjpegServerPort; - this.prebuildDelay = typeof args.prebuildDelay === 'number' ? args.prebuildDelay : PREBUILD_DELAY; + this.prebuildDelay = + typeof args.prebuildDelay === 'number' ? args.prebuildDelay : PREBUILD_DELAY; this.allowProvisioningDeviceRegistration = args.allowProvisioningDeviceRegistration; @@ -415,9 +416,10 @@ export class XcodeBuild { }); let logXcodeOutput = !!this.showXcodeLog; - const logMsg = typeof this.showXcodeLog === 'boolean' - ? `Output from xcodebuild ${this.showXcodeLog ? 'will' : 'will not'} be logged` - : 'Output from xcodebuild will only be logged if any errors are present there'; + const logMsg = + typeof this.showXcodeLog === 'boolean' + ? `Output from xcodebuild ${this.showXcodeLog ? 'will' : 'will not'} be logged` + : 'Output from xcodebuild will only be logged if any errors are present there'; this.log.debug(`${logMsg}. To change this, use 'showXcodeLog' desired capability`); const onStreamLine = (line: string) => { diff --git a/test/unit/webdriveragent-specs.ts b/test/unit/webdriveragent-specs.ts index 844ebfab0..c518ef34d 100644 --- a/test/unit/webdriveragent-specs.ts +++ b/test/unit/webdriveragent-specs.ts @@ -33,33 +33,27 @@ describe('WebDriverAgent', function () { expect(agent.agentPath).to.eql(defaultAgentPath); }); it('should have custom wda bootstrap and default agent if only bootstrap specified', function () { - const agent = new WebDriverAgent( - { - ...fakeConstructorArgs, - bootstrapPath: customBootstrapPath, - }, - ); + const agent = new WebDriverAgent({ + ...fakeConstructorArgs, + bootstrapPath: customBootstrapPath, + }); expect(agent.bootstrapPath).to.eql(customBootstrapPath); expect(agent.agentPath).to.eql(path.resolve(customBootstrapPath, 'WebDriverAgent.xcodeproj')); }); it('should have custom wda bootstrap and agent if both specified', function () { - const agent = new WebDriverAgent( - { - ...fakeConstructorArgs, - bootstrapPath: customBootstrapPath, - agentPath: customAgentPath, - }, - ); + const agent = new WebDriverAgent({ + ...fakeConstructorArgs, + bootstrapPath: customBootstrapPath, + agentPath: customAgentPath, + }); expect(agent.bootstrapPath).to.eql(customBootstrapPath); expect(agent.agentPath).to.eql(customAgentPath); }); it('should have custom derivedDataPath if specified', function () { - const agent = new WebDriverAgent( - { - ...fakeConstructorArgs, - derivedDataPath: customDerivedDataPath, - }, - ); + const agent = new WebDriverAgent({ + ...fakeConstructorArgs, + derivedDataPath: customDerivedDataPath, + }); if (agent.xcodebuild) { expect(agent.xcodebuild.derivedDataPath).to.eql(customDerivedDataPath); } From 375b3b0dbb565850365c2e7d4abb960c80ea2857 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Tue, 28 Apr 2026 18:44:15 +0200 Subject: [PATCH 5/5] Address comments --- lib/webdriveragent.ts | 16 ++++++++-------- test/unit/webdriveragent-specs.ts | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/lib/webdriveragent.ts b/lib/webdriveragent.ts index 4fded8f5c..1be2622f8 100644 --- a/lib/webdriveragent.ts +++ b/lib/webdriveragent.ts @@ -353,10 +353,10 @@ export class WebDriverAgent { * @returns `true` if source is fresh (all required files exist), `false` otherwise */ async isSourceFresh(): Promise { - const existsPromises = ['Resources', `Resources${path.sep}WebDriverAgent.bundle`].map( + const existsPromises = ['Resources', path.join('Resources', 'WebDriverAgent.bundle')].map( (subPath) => fs.exists(path.resolve(this.bootstrapPath, subPath)), ); - return (await Promise.all(existsPromises)).some((v) => v === false); + return (await Promise.all(existsPromises)).every((v) => v === true); } /** @@ -527,14 +527,14 @@ export class WebDriverAgent { } private toUrl(value: string): URL { + // Treat values without `://` as host/path inputs and normalize to http. + if (!value.includes(URL_PROTOCOL_SEPARATOR)) { + return new URL(`http://${value}`); + } try { return new URL(value); - } catch (err) { - // Keep compatibility with legacy parser behavior for values like "localhost:8100". - if (!value.includes(URL_PROTOCOL_SEPARATOR)) { - return new URL(`http://${value}`); - } - throw err; + } catch { + throw new Error(`Invalid URL: ${value}`); } } diff --git a/test/unit/webdriveragent-specs.ts b/test/unit/webdriveragent-specs.ts index c518ef34d..725822318 100644 --- a/test/unit/webdriveragent-specs.ts +++ b/test/unit/webdriveragent-specs.ts @@ -208,6 +208,24 @@ describe('WebDriverAgent', function () { expect(agent.noSessionProxy.scheme).to.eql('https'); } }); + + it('should accept scheme-less webDriverAgentUrl values', function () { + const args = Object.assign({}, fakeConstructorArgs); + args.webDriverAgentUrl = 'localhost:8100/aabbccdd'; + const agent = new WebDriverAgent(args); + expect(agent.url.href).to.eql('http://localhost:8100/aabbccdd'); + (agent as any).setupProxies('mysession'); + if (agent.jwproxy) { + expect(agent.jwproxy.scheme).to.eql('http'); + } + }); + + it('should throw for invalid webDriverAgentUrl with explicit scheme', function () { + const args = Object.assign({}, fakeConstructorArgs); + args.webDriverAgentUrl = 'http://'; + const agent = new WebDriverAgent(args); + expect(() => agent.url).to.throw(); + }); }); describe('setupCaching()', function () {