From 6852ddd7674549f926afa246ec897e5a04b81f9b Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sun, 15 Feb 2026 21:39:14 -0800 Subject: [PATCH] fix: format --- .github/workflows/unit-test.yml | 2 + lib/check-dependencies.ts | 34 +- lib/constants.ts | 1 - lib/logger.ts | 2 +- lib/no-session-proxy.ts | 12 +- lib/types.ts | 7 +- lib/utils.ts | 132 ++-- lib/webdriveragent.ts | 323 +++++---- lib/xcodebuild.ts | 116 +-- package.json | 3 +- test/functional/desired.ts | 11 +- test/functional/helpers/simulator.ts | 16 +- test/functional/webdriveragent-e2e-specs.ts | 31 +- test/unit/utils-specs.ts | 163 +++-- test/unit/webdriveragent-specs.ts | 748 ++++++++++---------- 15 files changed, 897 insertions(+), 704 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 2c8eb438e..bd57c7beb 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -26,5 +26,7 @@ jobs: name: Install dev dependencies - run: npm run lint name: Run linter + - run: npm run format:check + name: Run Prettier check - run: npm run test name: Run unit tests diff --git a/lib/check-dependencies.ts b/lib/check-dependencies.ts index 9540eb963..44c204d2c 100644 --- a/lib/check-dependencies.ts +++ b/lib/check-dependencies.ts @@ -1,17 +1,18 @@ -import { fs } from '@appium/support'; -import { exec } from 'teen_process'; +import {fs} from '@appium/support'; +import {exec} from 'teen_process'; import path from 'node:path'; -import { - WDA_SCHEME, SDK_SIMULATOR, WDA_RUNNER_APP -} from './constants'; -import { BOOTSTRAP_PATH } from './utils'; -import type { XcodeBuild } from './xcodebuild'; +import {WDA_SCHEME, SDK_SIMULATOR, WDA_RUNNER_APP} from './constants'; +import {BOOTSTRAP_PATH} from './utils'; +import type {XcodeBuild} from './xcodebuild'; -async function buildWDASim (): Promise { +async function buildWDASim(): Promise { const args = [ - '-project', path.join(BOOTSTRAP_PATH, 'WebDriverAgent.xcodeproj'), - '-scheme', WDA_SCHEME, - '-sdk', SDK_SIMULATOR, + '-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', @@ -19,16 +20,21 @@ async function buildWDASim (): Promise { await exec('xcodebuild', args); } -export async function bundleWDASim (xcodebuild: XcodeBuild): Promise { +export async function bundleWDASim(xcodebuild: XcodeBuild): Promise { const derivedDataPath = await xcodebuild.retrieveDerivedDataPath(); if (!derivedDataPath) { throw new Error('Cannot retrieve the path to the Xcode derived data folder'); } - const wdaBundlePath = path.join(derivedDataPath, 'Build', 'Products', 'Debug-iphonesimulator', WDA_RUNNER_APP); + const wdaBundlePath = path.join( + derivedDataPath, + 'Build', + 'Products', + 'Debug-iphonesimulator', + WDA_RUNNER_APP, + ); if (await fs.exists(wdaBundlePath)) { return wdaBundlePath; } await buildWDASim(); return wdaBundlePath; } - diff --git a/lib/constants.ts b/lib/constants.ts index 42c761df0..bc5a68166 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -15,4 +15,3 @@ export const SDK_SIMULATOR = 'iphonesimulator'; export const SDK_DEVICE = 'iphoneos'; export const WDA_UPGRADE_TIMESTAMP_PATH = path.join('.appium', 'webdriveragent', 'upgrade.time'); - diff --git a/lib/logger.ts b/lib/logger.ts index aecf4e22a..567dbf353 100644 --- a/lib/logger.ts +++ b/lib/logger.ts @@ -1,3 +1,3 @@ -import { logger } from '@appium/support'; +import {logger} from '@appium/support'; export const log = logger.getLogger('WebDriverAgent'); diff --git a/lib/no-session-proxy.ts b/lib/no-session-proxy.ts index d16e68ac2..963624de4 100644 --- a/lib/no-session-proxy.ts +++ b/lib/no-session-proxy.ts @@ -1,19 +1,18 @@ -import { JWProxy } from '@appium/base-driver'; -import type { ProxyOptions } from '@appium/types'; - +import {JWProxy} from '@appium/base-driver'; +import type {ProxyOptions} from '@appium/types'; export class NoSessionProxy extends JWProxy { - constructor (opts: ProxyOptions = {}) { + constructor(opts: ProxyOptions = {}) { super(opts); } - override getUrlForProxy (url: string): string { + override getUrlForProxy(url: string): string { if (url === '') { url = '/'; } const proxyBase = `${this.scheme}://${this.server}:${this.port}${this.base}`; let remainingUrl = ''; - if ((new RegExp('^/')).test(url)) { + if (new RegExp('^/').test(url)) { remainingUrl = url; } else { throw new Error(`Did not know what to do with url '${url}'`); @@ -22,4 +21,3 @@ export class NoSessionProxy extends JWProxy { return proxyBase + remainingUrl; } } - diff --git a/lib/types.ts b/lib/types.ts index a2f8b7307..a592b37d4 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -21,7 +21,12 @@ export interface WDASettings { defaultAlertAction?: 'accept' | 'dismiss'; acceptAlertButtonSelector?: string; dismissAlertButtonSelector?: string; - screenshotOrientation?: 'auto' | 'portrait' | 'portraitUpsideDown' | 'landscapeRight' | 'landscapeLeft' + screenshotOrientation?: + | 'auto' + | 'portrait' + | 'portraitUpsideDown' + | 'landscapeRight' + | 'landscapeLeft'; waitForIdleTimeout?: number; animationCoolOffTimeout?: number; maxTypingFrequency?: number; diff --git a/lib/utils.ts b/lib/utils.ts index 893f0c3ea..bf48f998c 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,15 +1,15 @@ -import { fs, plist } from '@appium/support'; -import { exec, SubProcess } from 'teen_process'; -import path, { dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { log } from './logger'; +import {fs, plist} from '@appium/support'; +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 {PLATFORM_NAME_TVOS} from './constants'; import B from 'bluebird'; import _fs from 'node:fs'; -import { waitForCondition } from 'asyncbox'; -import { arch } from 'node:os'; -import type { DeviceInfo } from './types'; +import {waitForCondition} from 'asyncbox'; +import {arch} from 'node:os'; +import type {DeviceInfo} from './types'; // Get current filename - works in both CommonJS and ESM const currentFilename = @@ -25,14 +25,16 @@ const currentDirname = dirname(currentFilename); * @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 = _.memoize(function getModuleRoot(): string { let currentDir = currentDirname; let isAtFsRoot = false; while (!isAtFsRoot) { const manifestPath = path.join(currentDir, 'package.json'); try { - if (_fs.existsSync(manifestPath) && - JSON.parse(_fs.readFileSync(manifestPath, 'utf8')).name === 'appium-webdriveragent') { + if ( + _fs.existsSync(manifestPath) && + JSON.parse(_fs.readFileSync(manifestPath, 'utf8')).name === 'appium-webdriveragent' + ) { return currentDir; } } catch {} @@ -44,7 +46,7 @@ const getModuleRoot = _.memoize(function getModuleRoot (): string { export const BOOTSTRAP_PATH = getModuleRoot(); -export async function killAppUsingPattern (pgrepPattern: string): Promise { +export async function killAppUsingPattern(pgrepPattern: string): Promise { const signals = [2, 15, 9]; for (const signal of signals) { const matchedPids = await getPIDsUsingPattern(pgrepPattern); @@ -62,20 +64,22 @@ export async function killAppUsingPattern (pgrepPattern: string): Promise return; } try { - await waitForCondition(async () => { - const pidCheckPromises = matchedPids - .map((pid) => exec('kill', ['-0', pid]) - // the process is still alive - .then(() => false) - // the process is dead - .catch(() => true) + await waitForCondition( + async () => { + const pidCheckPromises = matchedPids.map((pid) => + exec('kill', ['-0', pid]) + // the process is still alive + .then(() => false) + // the process is dead + .catch(() => true), ); - return (await B.all(pidCheckPromises)) - .every((x) => x === true); - }, { - waitMs: 1000, - intervalMs: 100, - }); + return (await B.all(pidCheckPromises)).every((x) => x === true); + }, + { + waitMs: 1000, + intervalMs: 100, + }, + ); return; } catch { // try the next signal @@ -88,11 +92,14 @@ export async function killAppUsingPattern (pgrepPattern: string): Promise * @param platformName The name of the platorm * @returns Return true if the platformName is tvOS */ -export function isTvOS (platformName: string): boolean { +export function isTvOS(platformName: string): boolean { return _.toLower(platformName) === _.toLower(PLATFORM_NAME_TVOS); } -export async function setRealDeviceSecurity (keychainPath: string, keychainPassword: string): Promise { +export async function setRealDeviceSecurity( + keychainPath: string, + keychainPassword: string, +): Promise { log.debug('Setting security for iOS device'); await exec('security', ['-v', 'list-keychains', '-s', keychainPath]); await exec('security', ['-v', 'unlock-keychain', '-p', keychainPassword, keychainPath]); @@ -124,11 +131,15 @@ export interface XctestrunFileArgs { * or WebDriverAgentRunner_iphonesimulator${sdkVersion|platformVersion}-x86_64.xctestrun for simulator is not found @bootstrapPath, * then it will throw a file not found exception */ -export async function setXctestrunFile (args: XctestrunFileArgs): Promise { +export async function setXctestrunFile(args: XctestrunFileArgs): Promise { const {deviceInfo, sdkVersion, bootstrapPath, wdaRemotePort, wdaBindingIP} = args; const xctestrunFilePath = await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath); const xctestRunContent = await plist.parsePlistFile(xctestrunFilePath); - const updateWDAPort = getAdditionalRunContent(deviceInfo.platformName, wdaRemotePort, wdaBindingIP); + const updateWDAPort = getAdditionalRunContent( + deviceInfo.platformName, + wdaRemotePort, + wdaBindingIP, + ); const newXctestRunContent = _.merge(xctestRunContent, updateWDAPort); await plist.updatePlistFile(xctestrunFilePath, newXctestRunContent, true); @@ -142,16 +153,20 @@ export async function setXctestrunFile (args: XctestrunFileArgs): Promise { +export function getAdditionalRunContent( + platformName: string, + wdaRemotePort: number | string, + wdaBindingIP?: string, +): Record { const runner = `WebDriverAgentRunner${isTvOS(platformName) ? '_tvOS' : ''}`; return { [runner]: { EnvironmentVariables: { // USE_PORT must be 'string' USE_PORT: `${wdaRemotePort}`, - ...(wdaBindingIP ? { USE_IP: wdaBindingIP } : {}), - } - } + ...(wdaBindingIP ? {USE_IP: wdaBindingIP} : {}), + }, + }, }; } @@ -161,7 +176,11 @@ export function getAdditionalRunContent (platformName: string, wdaRemotePort: nu * @param sdkVersion - The Xcode SDK version of OS. * @param bootstrapPath - The folder path containing xctestrun file. */ -export async function getXctestrunFilePath (deviceInfo: DeviceInfo, sdkVersion: string, bootstrapPath: string): Promise { +export async function getXctestrunFilePath( + deviceInfo: DeviceInfo, + sdkVersion: string, + bootstrapPath: string, +): Promise { // First try the SDK path, for Xcode 10 (at least) const sdkBased: [string, string] = [ path.resolve(bootstrapPath, `${deviceInfo.udid}_${sdkVersion}.xctestrun`), @@ -178,7 +197,10 @@ export async function getXctestrunFilePath (deviceInfo: DeviceInfo, sdkVersion: log.info(`Using '${filePath}' as xctestrun file`); return filePath; } - const originalXctestrunFile = path.resolve(bootstrapPath, getXctestrunFileName(deviceInfo, version)); + const originalXctestrunFile = path.resolve( + bootstrapPath, + getXctestrunFileName(deviceInfo, version), + ); if (await fs.exists(originalXctestrunFile)) { // If this is first time run for given device, then first generate xctestrun file for device. // We need to have a xctestrun file **per device** because we cant not have same wda port for all devices. @@ -190,19 +212,18 @@ export async function getXctestrunFilePath (deviceInfo: DeviceInfo, sdkVersion: throw new Error( `If you are using 'useXctestrunFile' capability then you ` + - `need to have a xctestrun file (expected: ` + - `'${path.resolve(bootstrapPath, getXctestrunFileName(deviceInfo, sdkVersion))}')` + `need to have a xctestrun file (expected: ` + + `'${path.resolve(bootstrapPath, getXctestrunFileName(deviceInfo, sdkVersion))}')`, ); } - /** * Return the name of xctestrun file * @param deviceInfo * @param version - The Xcode SDK version of OS. * @return returns xctestrunFilePath for given device */ -export function getXctestrunFileName (deviceInfo: DeviceInfo, version: string): string { +export function getXctestrunFileName(deviceInfo: DeviceInfo, version: string): string { const archSuffix = deviceInfo.isRealDevice ? `os${version}-arm64` : `simulator${version}-${arch() === 'arm64' ? 'arm64' : 'x86_64'}`; @@ -212,7 +233,10 @@ export function getXctestrunFileName (deviceInfo: DeviceInfo, version: string): /** * Ensures the process is killed after the timeout */ -export async function killProcess (name: string, proc: SubProcess | null | undefined): Promise { +export async function killProcess( + name: string, + proc: SubProcess | null | undefined, +): Promise { if (!proc || !proc.isRunning) { return; } @@ -245,16 +269,16 @@ export async function killProcess (name: string, proc: SubProcess | null | undef /** * Generate a random integer in range [low, high). `low` is inclusive and `high` is exclusive. */ -export function randomInt (low: number, high: number): number { +export function randomInt(low: number, high: number): number { return Math.floor(Math.random() * (high - low) + low); } /** * Retrieves WDA upgrade timestamp. The manifest only gets modified on package upgrade. */ -export async function getWDAUpgradeTimestamp (): Promise { +export async function getWDAUpgradeTimestamp(): Promise { const packageManifest = path.resolve(getModuleRoot(), 'package.json'); - if (!await fs.exists(packageManifest)) { + if (!(await fs.exists(packageManifest))) { return null; } const {mtime} = await fs.stat(packageManifest); @@ -264,7 +288,7 @@ export async function getWDAUpgradeTimestamp (): Promise { /** * Kills running XCTest processes for the particular device. */ -export async function resetTestProcesses (udid: string, isSimulator: boolean): Promise { +export async function resetTestProcesses(udid: string, isSimulator: boolean): Promise { const processPatterns = [`xcodebuild.*${udid}`]; if (isSimulator) { processPatterns.push(`${udid}.*XCTRunner`); @@ -288,12 +312,15 @@ export async function resetTestProcesses (udid: string, isSimulator: boolean): P * from the resulting array. * @returns - the list of matched process ids. */ -export async function getPIDsListeningOnPort (port: string | number, filteringFunc: ((cmdline: string) => boolean | Promise) | null = null): Promise { +export async function getPIDsListeningOnPort( + port: string | number, + filteringFunc: ((cmdline: string) => boolean | Promise) | null = null, +): Promise { const result: string[] = []; try { // This only works since Mac OS X El Capitan const {stdout} = await exec('lsof', ['-ti', `tcp:${port}`]); - result.push(...(stdout.trim().split(/\n+/))); + result.push(...stdout.trim().split(/\n+/)); } catch (e: any) { if (e.code !== 1) { // code 1 means no processes. Other errors need reporting @@ -322,21 +349,22 @@ export async function getPIDsListeningOnPort (port: string | number, filteringFu // Private functions -async function getPIDsUsingPattern (pattern: string): Promise { +async function getPIDsUsingPattern(pattern: string): Promise { const args = [ '-if', // case insensitive, full cmdline match pattern, ]; try { const {stdout} = await exec('pgrep', args); - return stdout.split(/\s+/) + return stdout + .split(/\s+/) .map((x) => parseInt(x, 10)) .filter(_.isInteger) .map((x) => `${x}`); } catch (err: any) { - log.debug(`'pgrep ${args.join(' ')}' didn't detect any matching processes. Return code: ${err.code}`); + log.debug( + `'pgrep ${args.join(' ')}' didn't detect any matching processes. Return code: ${err.code}`, + ); return []; } } - - diff --git a/lib/webdriveragent.ts b/lib/webdriveragent.ts index e2061392a..954a15362 100644 --- a/lib/webdriveragent.ts +++ b/lib/webdriveragent.ts @@ -1,27 +1,33 @@ -import { waitForCondition } from 'asyncbox'; +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'; -import type { AppiumLogger, StringRecord } from '@appium/types'; -import { log as defaultLogger } from './logger'; -import { NoSessionProxy } from './no-session-proxy'; +import {JWProxy} from '@appium/base-driver'; +import {fs, util, plist} from '@appium/support'; +import type {AppiumLogger, StringRecord} from '@appium/types'; +import {log as defaultLogger} from './logger'; +import {NoSessionProxy} from './no-session-proxy'; import { - getWDAUpgradeTimestamp, resetTestProcesses, getPIDsListeningOnPort, BOOTSTRAP_PATH + getWDAUpgradeTimestamp, + resetTestProcesses, + getPIDsListeningOnPort, + BOOTSTRAP_PATH, } from './utils'; import {XcodeBuild} from './xcodebuild'; import AsyncLock from 'async-lock'; -import { exec } from 'teen_process'; -import { bundleWDASim } from './check-dependencies'; +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 + 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'; +import type {WebDriverAgentArgs, AppleDevice} from './types'; const WDA_LAUNCH_TIMEOUT = 60 * 1000; const WDA_AGENT_PORT = 8100; @@ -69,7 +75,7 @@ export class WebDriverAgent { * @param args - Configuration arguments for WebDriverAgent * @param log - Optional logger instance */ - constructor (args: WebDriverAgentArgs, log: AppiumLogger | null = null) { + constructor(args: WebDriverAgentArgs, log: AppiumLogger | null = null) { this.args = _.clone(args); this.log = log ?? defaultLogger; @@ -84,8 +90,8 @@ export class WebDriverAgent { this.setWDAPaths(args.bootstrapPath, args.agentPath); this.wdaLocalPort = args.wdaLocalPort; - this.wdaRemotePort = ((this.isRealDevice ? args.wdaRemotePort : null) ?? args.wdaLocalPort) - || WDA_AGENT_PORT; + this.wdaRemotePort = + ((this.isRealDevice ? args.wdaRemotePort : null) ?? args.wdaLocalPort) || WDA_AGENT_PORT; this.wdaBaseUrl = args.wdaBaseUrl || WDA_BASE_URL; this.wdaBindingIP = args.wdaBindingIP; this.prebuildWDA = args.prebuildWDA; @@ -112,40 +118,44 @@ export class WebDriverAgent { this.updatedWDABundleIdSuffix = args.updatedWDABundleIdSuffix ?? DEFAULT_TEST_BUNDLE_SUFFIX; this._xcodebuild = this.canSkipXcodebuild - ? null - : new XcodeBuild(this.device, { - platformVersion: this.platformVersion, - platformName: this.platformName, - iosSdkVersion: this.iosSdkVersion, - agentPath: this.agentPath, - bootstrapPath: this.bootstrapPath, - realDevice: this.isRealDevice, - showXcodeLog: args.showXcodeLog, - xcodeConfigFile: args.xcodeConfigFile, - xcodeOrgId: args.xcodeOrgId, - xcodeSigningId: args.xcodeSigningId, - keychainPath: args.keychainPath, - keychainPassword: args.keychainPassword, - useSimpleBuildTest: args.useSimpleBuildTest, - usePrebuiltWDA: args.usePrebuiltWDA, - updatedWDABundleId: this.updatedWDABundleId, - launchTimeout: this.wdaLaunchTimeout, - wdaRemotePort: this.wdaRemotePort, - wdaBindingIP: this.wdaBindingIP, - useXctestrunFile: this.useXctestrunFile, - derivedDataPath: args.derivedDataPath, - mjpegServerPort: this.mjpegServerPort, - allowProvisioningDeviceRegistration: args.allowProvisioningDeviceRegistration, - resultBundlePath: args.resultBundlePath, - resultBundleVersion: args.resultBundleVersion, - }, this.log); + ? null + : new XcodeBuild( + this.device, + { + platformVersion: this.platformVersion, + platformName: this.platformName, + iosSdkVersion: this.iosSdkVersion, + agentPath: this.agentPath, + bootstrapPath: this.bootstrapPath, + realDevice: this.isRealDevice, + showXcodeLog: args.showXcodeLog, + xcodeConfigFile: args.xcodeConfigFile, + xcodeOrgId: args.xcodeOrgId, + xcodeSigningId: args.xcodeSigningId, + keychainPath: args.keychainPath, + keychainPassword: args.keychainPassword, + useSimpleBuildTest: args.useSimpleBuildTest, + usePrebuiltWDA: args.usePrebuiltWDA, + updatedWDABundleId: this.updatedWDABundleId, + launchTimeout: this.wdaLaunchTimeout, + wdaRemotePort: this.wdaRemotePort, + wdaBindingIP: this.wdaBindingIP, + useXctestrunFile: this.useXctestrunFile, + derivedDataPath: args.derivedDataPath, + mjpegServerPort: this.mjpegServerPort, + allowProvisioningDeviceRegistration: args.allowProvisioningDeviceRegistration, + resultBundlePath: args.resultBundlePath, + resultBundleVersion: args.resultBundleVersion, + }, + this.log, + ); } /** * Return true if the session does not need xcodebuild. * @returns Whether the session needs/has xcodebuild. */ - get canSkipXcodebuild (): boolean { + get canSkipXcodebuild(): boolean { // Use this.args.webDriverAgentUrl to guarantee // the capabilities set gave the `appium:webDriverAgentUrl`. return this.usePreinstalledWDA || !!this.args.webDriverAgentUrl; @@ -156,7 +166,7 @@ export class WebDriverAgent { * @returns The XcodeBuild instance * @throws Error if xcodebuild is not available */ - get xcodebuild (): XcodeBuild { + get xcodebuild(): XcodeBuild { if (!this._xcodebuild) { throw new Error('xcodebuild is not available'); } @@ -171,7 +181,7 @@ export class WebDriverAgent { * * @returns Bundle ID for Xctest. */ - get bundleIdForXctest (): string { + get bundleIdForXctest(): string { return `${this.updatedWDABundleId ? this.updatedWDABundleId : WDA_RUNNER_BUNDLE_ID}${this.updatedWDABundleIdSuffix}`; } @@ -179,24 +189,33 @@ export class WebDriverAgent { * Cleans up obsolete cached processes from previous WDA sessions * that are listening on the same port but belong to different devices. */ - async cleanupObsoleteProcesses (): Promise { - const obsoletePids = await getPIDsListeningOnPort(this.url.port as string, - (cmdLine) => cmdLine.includes('/WebDriverAgentRunner') && - !cmdLine.toLowerCase().includes(this.device.udid.toLowerCase())); + async cleanupObsoleteProcesses(): Promise { + const obsoletePids = await getPIDsListeningOnPort( + this.url.port as string, + (cmdLine) => + cmdLine.includes('/WebDriverAgentRunner') && + !cmdLine.toLowerCase().includes(this.device.udid.toLowerCase()), + ); if (_.isEmpty(obsoletePids)) { - this.log.debug(`No obsolete cached processes from previous WDA sessions ` + - `listening on port ${this.url.port} have been found`); + this.log.debug( + `No obsolete cached processes from previous WDA sessions ` + + `listening on port ${this.url.port} have been found`, + ); return; } - this.log.info(`Detected ${obsoletePids.length} obsolete cached process${obsoletePids.length === 1 ? '' : 'es'} ` + - `from previous WDA sessions. Cleaning them up`); + this.log.info( + `Detected ${obsoletePids.length} obsolete cached process${obsoletePids.length === 1 ? '' : 'es'} ` + + `from previous WDA sessions. Cleaning them up`, + ); try { await exec('kill', obsoletePids); } catch (e: any) { - this.log.warn(`Failed to kill obsolete cached process${obsoletePids.length === 1 ? '' : 'es'} '${obsoletePids}'. ` + - `Original error: ${e.message}`); + this.log.warn( + `Failed to kill obsolete cached process${obsoletePids.length === 1 ? '' : 'es'} '${obsoletePids}'. ` + + `Original error: ${e.message}`, + ); } } @@ -204,7 +223,7 @@ export class WebDriverAgent { * Gets the base path for the WebDriverAgent URL. * @returns The base path (empty string if root path) */ - get basePath (): string { + get basePath(): string { if (this.url.path === '/') { return ''; } @@ -232,7 +251,7 @@ export class WebDriverAgent { * * @param sessionId Launch WDA and establish the session with this sessionId */ - async launch (sessionId: string): Promise { + async launch(sessionId: string): Promise { if (this.webDriverAgentUrl) { this.log.info(`Using provided WebdriverAgent at '${this.webDriverAgentUrl}'`); this.url = this.webDriverAgentUrl; @@ -248,9 +267,11 @@ export class WebDriverAgent { this.setupProxies(sessionId); - if (!this.useXctestrunFile && !await fs.exists(this.agentPath)) { - throw new Error(`Trying to use WebDriverAgent project at '${this.agentPath}' but the ` + - 'file does not exist'); + if (!this.useXctestrunFile && !(await fs.exists(this.agentPath))) { + throw new Error( + `Trying to use WebDriverAgent project at '${this.agentPath}' but the ` + + 'file does not exist', + ); } // useXctestrunFile and usePrebuiltWDA use existing dependencies @@ -259,8 +280,10 @@ export class WebDriverAgent { this.log.info('Skipped WDA project cleanup according to the provided capabilities'); } else { const synchronizationKey = path.normalize(this.bootstrapPath); - await SHARED_RESOURCES_GUARD.acquire(synchronizationKey, - async () => await this._cleanupProjectIfFresh()); + await SHARED_RESOURCES_GUARD.acquire( + synchronizationKey, + async () => await this._cleanupProjectIfFresh(), + ); } // We need to provide WDA local port, because it might be occupied @@ -275,7 +298,7 @@ export class WebDriverAgent { if (this.prebuildWDA) { await this.xcodebuild.prebuild(); } - return await this.xcodebuild.start() as StringRecord | null; + return (await this.xcodebuild.start()) as StringRecord | null; } /** @@ -283,15 +306,14 @@ export class WebDriverAgent { * that required resource files exist. * @returns `true` if source is fresh (all required files exist), `false` otherwise */ - async isSourceFresh (): Promise { - const existsPromises = [ - 'Resources', - `Resources${path.sep}WebDriverAgent.bundle`, - ].map((subPath) => fs.exists(path.resolve(this.bootstrapPath, subPath))); + async isSourceFresh(): Promise { + 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); } - private async parseBundleId (wdaBundlePath: string): Promise { + private async parseBundleId(wdaBundlePath: string): Promise { const infoPlistPath = path.join(wdaBundlePath, 'Info.plist'); const infoPlist = await plist.parsePlist(await fs.readFile(infoPlistPath)); if (!infoPlist.CFBundleIdentifier) { @@ -300,7 +322,7 @@ export class WebDriverAgent { return infoPlist.CFBundleIdentifier as string; } - private async fetchWDABundle (): Promise { + private async fetchWDABundle(): Promise { if (!this.derivedDataPath) { return await bundleWDASim(this.xcodebuild); } @@ -313,7 +335,7 @@ export class WebDriverAgent { return wdaBundlePaths[0]; } - private setupProxies (sessionId: string): void { + private setupProxies(sessionId: string): void { const proxyOpts: any = { log: this.log, server: this.url.hostname ?? undefined, @@ -338,7 +360,7 @@ export class WebDriverAgent { * Stops the WebDriverAgent session and cleans up resources. * Handles both preinstalled WDA and xcodebuild-based sessions. */ - async quit (): Promise { + async quit(): Promise { if (this.usePreinstalledWDA) { this.log.info('Stopping the XCTest session'); if (this.xctestApiClient) { @@ -357,8 +379,10 @@ export class WebDriverAgent { await this.xcodebuild.quit(); } } else { - this.log.debug('Do not stop xcodebuild nor XCTest session ' + - 'since the WDA session is managed by outside this driver.'); + this.log.debug( + 'Do not stop xcodebuild nor XCTest session ' + + 'since the WDA session is managed by outside this driver.', + ); } if (this.jwproxy) { @@ -380,7 +404,7 @@ export class WebDriverAgent { * builds it from wdaBaseUrl, wdaBindingIP, and wdaLocalPort. * @returns The parsed URL object */ - get url (): url.UrlWithStringQuery { + get url(): url.UrlWithStringQuery { if (!this._url) { if (this.webDriverAgentUrl) { this._url = url.parse(this.webDriverAgentUrl); @@ -397,7 +421,7 @@ export class WebDriverAgent { * Sets the WebDriverAgent URL. * @param _url - The URL string to parse and set */ - set url (_url: string) { + set url(_url: string) { this._url = url.parse(_url); } @@ -405,7 +429,7 @@ export class WebDriverAgent { * Gets whether WebDriverAgent has fully started. * @returns `true` if WDA has started, `false` otherwise */ - get fullyStarted (): boolean { + get fullyStarted(): boolean { return this.started; } @@ -413,7 +437,7 @@ export class WebDriverAgent { * Sets whether WebDriverAgent has fully started. * @param started - `true` if WDA has started, `false` otherwise */ - set fullyStarted (started: boolean) { + set fullyStarted(started: boolean) { this.started = started ?? false; } @@ -421,7 +445,7 @@ export class WebDriverAgent { * Retrieves the Xcode derived data path for WebDriverAgent. * @returns The derived data path, or `undefined` if xcodebuild is skipped */ - async retrieveDerivedDataPath (): Promise { + async retrieveDerivedDataPath(): Promise { if (this.canSkipXcodebuild) { return; } @@ -433,53 +457,70 @@ export class WebDriverAgent { * 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 { + 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; } - const { - productBundleIdentifier, - upgradedAt, - } = status.build as any; + const {productBundleIdentifier, upgradedAt} = status.build as any; // for real device - if (util.hasValue(productBundleIdentifier) && util.hasValue(this.updatedWDABundleId) && this.updatedWDABundleId !== productBundleIdentifier) { - this.log.info(`Will uninstall running WDA since it has different bundle id. The actual value is '${productBundleIdentifier}'.`); + if ( + util.hasValue(productBundleIdentifier) && + util.hasValue(this.updatedWDABundleId) && + this.updatedWDABundleId !== productBundleIdentifier + ) { + this.log.info( + `Will uninstall running WDA since it has different bundle id. The actual value is '${productBundleIdentifier}'.`, + ); return await this.uninstall(); } // for simulator - if (util.hasValue(productBundleIdentifier) && !util.hasValue(this.updatedWDABundleId) && 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}`); + if ( + util.hasValue(productBundleIdentifier) && + !util.hasValue(this.updatedWDABundleId) && + 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}`, + ); return await this.uninstall(); } const actualUpgradeTimestamp = await getWDAUpgradeTimestamp(); this.log.debug(`Upgrade timestamp of the currently bundled WDA: ${actualUpgradeTimestamp}`); this.log.debug(`Upgrade timestamp of the WDA on the device: ${upgradedAt}`); - if (actualUpgradeTimestamp && upgradedAt && _.toLower(`${actualUpgradeTimestamp}`) !== _.toLower(`${upgradedAt}`)) { - this.log.info('Will uninstall running WDA since it has different version in comparison to the one ' + - `which is bundled with appium-xcuitest-driver module (${actualUpgradeTimestamp} != ${upgradedAt})`); + if ( + actualUpgradeTimestamp && + upgradedAt && + _.toLower(`${actualUpgradeTimestamp}`) !== _.toLower(`${upgradedAt}`) + ) { + this.log.info( + 'Will uninstall 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(); } 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}'`; - this.log.info(`${message}. Set the wdaLocalPort capability to a value different from ${this.url.port} if this is an undesired behavior.`); + 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; } /** * Quit and uninstall running WDA. */ - async quitAndUninstall (): Promise { + async quitAndUninstall(): Promise { await this.quit(); await this.uninstall(); } - private setWDAPaths (bootstrapPath?: string, agentPath?: string): void { + 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 this.bootstrapPath = bootstrapPath || BOOTSTRAP_PATH; @@ -490,7 +531,7 @@ export class WebDriverAgent { this.log.info(`Using WDA agent: '${this.agentPath}'`); } - private async isRunning (): Promise { + private async isRunning(): Promise { return !!(await this.getStatus()); } @@ -515,7 +556,7 @@ export class WebDriverAgent { * * @param timeoutMs If zero or negative, returns immediately. Otherwise, waits up to timeoutMs. */ - private async getStatus (timeoutMs: number = 0): Promise { + private async getStatus(timeoutMs: number = 0): Promise { const noSessionProxy = new NoSessionProxy({ server: this.url.hostname ?? undefined, port: parseInt(this.url.port ?? '', 10) || undefined, @@ -523,13 +564,16 @@ export class WebDriverAgent { timeout: 3000, }); - const sendGetStatus = async () => await noSessionProxy.command('/status', 'GET') as StringRecord; + const sendGetStatus = async () => + (await noSessionProxy.command('/status', 'GET')) as StringRecord; if (_.isNil(timeoutMs) || timeoutMs <= 0) { try { return await sendGetStatus(); } catch (err: any) { - this.log.debug(`WDA is not listening at '${this.url.href}'. Original error:: ${err.message}`); + this.log.debug( + `WDA is not listening at '${this.url.href}'. Original error:: ${err.message}`, + ); return null; } } @@ -537,21 +581,26 @@ export class WebDriverAgent { let lastError: any = null; let status: StringRecord | null = null; try { - await waitForCondition(async () => { - try { - status = await sendGetStatus(); - return true; - } catch (err) { - lastError = err; - } - return false; - }, { - waitMs: timeoutMs, - intervalMs: 300, - }); + await waitForCondition( + async () => { + try { + status = await sendGetStatus(); + return true; + } catch (err) { + lastError = err; + } + return false; + }, + { + waitMs: timeoutMs, + intervalMs: 300, + }, + ); } catch (err: any) { - this.log.debug(`Failed to get the status endpoint in ${timeoutMs} ms. ` + - `The last error while accessing ${this.url.href}: ${lastError}. Original error:: ${err.message}.`); + this.log.debug( + `Failed to get the status endpoint in ${timeoutMs} ms. ` + + `The last error while accessing ${this.url.href}: ${lastError}. Original error:: ${err.message}.`, + ); throw new Error(`WDA was not ready in ${timeoutMs} ms.`); } return status; @@ -562,7 +611,7 @@ export class WebDriverAgent { * 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 { + private async uninstall(): Promise { try { const bundleIds = await this.device.getUserInstalledBundleIdsByBundleName(WDA_CF_BUNDLE_NAME); if (_.isEmpty(bundleIds)) { @@ -576,17 +625,21 @@ export class WebDriverAgent { } } catch (e: any) { this.log.debug(e); - this.log.warn(`WebDriverAgent uninstall failed. Perhaps, it is already uninstalled? ` + - `Original error: ${e.message}`); + this.log.warn( + `WebDriverAgent uninstall failed. Perhaps, it is already uninstalled? ` + + `Original error: ${e.message}`, + ); } } - private async _cleanupProjectIfFresh (): Promise { + private async _cleanupProjectIfFresh(): Promise { if (this.canSkipXcodebuild) { return; } - const packageInfo = JSON.parse(await fs.readFile(path.join(BOOTSTRAP_PATH, 'package.json'), 'utf8')); + const packageInfo = JSON.parse( + await fs.readFile(path.join(BOOTSTRAP_PATH, 'package.json'), 'utf8'), + ); const box = strongbox(packageInfo.name); let boxItem = box.getItem(RECENT_MODULE_VERSION_ITEM_NAME); if (!boxItem) { @@ -602,7 +655,9 @@ export class WebDriverAgent { return; } } else { - this.log.info('There is no need to perform the project cleanup. A fresh install has been detected'); + this.log.info( + 'There is no need to perform the project cleanup. A fresh install has been detected', + ); try { await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, packageInfo.version); } catch (e: any) { @@ -617,7 +672,9 @@ export class WebDriverAgent { recentModuleVersion = util.coerceVersion(recentModuleVersion, true); } catch (e: any) { this.log.warn(`The persisted module version string has been damaged: ${e.message}`); - this.log.info(`Updating it to '${packageInfo.version}' assuming the project clenup is not needed`); + this.log.info( + `Updating it to '${packageInfo.version}' assuming the project clenup is not needed`, + ); await boxItem.write(packageInfo.version); return; } @@ -625,14 +682,14 @@ export class WebDriverAgent { if (util.compareVersions(recentModuleVersion, '>=', packageInfo.version)) { this.log.info( `WebDriverAgent does not need a cleanup. The project sources are up to date ` + - `(${recentModuleVersion} >= ${packageInfo.version})` + `(${recentModuleVersion} >= ${packageInfo.version})`, ); return; } this.log.info( `Cleaning up the WebDriverAgent project after the module upgrade has happened ` + - `(${recentModuleVersion} < ${packageInfo.version})` + `(${recentModuleVersion} < ${packageInfo.version})`, ); try { await this.xcodebuild.cleanProject(); @@ -653,12 +710,12 @@ export class WebDriverAgent { * * @param opts launching WDA with devicectl command options. */ - private async _launchViaDevicectl(opts: {env?: Record} = {}): Promise { + private async _launchViaDevicectl( + opts: {env?: Record} = {}, + ): Promise { const {env} = opts; - await this.device.devicectl.launchApp( - this.bundleIdForXctest, { env, terminateExisting: true } - ); + await this.device.devicectl.launchApp(this.bundleIdForXctest, {env, terminateExisting: true}); } /** @@ -668,7 +725,7 @@ export class WebDriverAgent { private async launchWithPreinstalledWDA(sessionId: string): Promise { const xctestEnv: Record = { USE_PORT: this.wdaLocalPort || WDA_AGENT_PORT, - WDA_PRODUCT_BUNDLE_IDENTIFIER: this.bundleIdForXctest + WDA_PRODUCT_BUNDLE_IDENTIFIER: this.bundleIdForXctest, }; if (this.mjpegServerPort) { xctestEnv.MJPEG_SERVER_PORT = this.mjpegServerPort; @@ -684,16 +741,14 @@ export class WebDriverAgent { 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}); + this.xctestApiClient = new Xctest(this.device.udid, this.bundleIdForXctest, null, { + env: xctestEnv, + }); await this.xctestApiClient.start(); } } else { await this.device.simctl.exec('launch', { - args: [ - '--terminate-running-process', - this.device.udid, - this.bundleIdForXctest, - ], + args: ['--terminate-running-process', this.device.udid, this.bundleIdForXctest], env: xctestEnv, }); } @@ -705,8 +760,8 @@ export class WebDriverAgent { } catch { throw new Error( `Failed to start the preinstalled WebDriverAgent in ${this.wdaLaunchTimeout} ms. ` + - `The WebDriverAgent might not be properly built or the device might be locked. ` + - `The 'appium:wdaLaunchTimeout' capability modifies the timeout.` + `The WebDriverAgent might not be properly built or the device might be locked. ` + + `The 'appium:wdaLaunchTimeout' capability modifies the timeout.`, ); } this.started = true; diff --git a/lib/xcodebuild.ts b/lib/xcodebuild.ts index f1b723074..f21289906 100644 --- a/lib/xcodebuild.ts +++ b/lib/xcodebuild.ts @@ -1,19 +1,21 @@ -import { retryInterval } from 'asyncbox'; -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 {retryInterval} from 'asyncbox'; +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, - killProcess, getWDAUpgradeTimestamp, isTvOS + setRealDeviceSecurity, + setXctestrunFile, + killProcess, + getWDAUpgradeTimestamp, + isTvOS, } from './utils'; import _ from 'lodash'; import path from 'node:path'; -import { WDA_RUNNER_BUNDLE_ID } from './constants'; -import type { AppleDevice, XcodeBuildArgs } from './types'; -import type { NoSessionProxy } from './no-session-proxy'; - +import {WDA_RUNNER_BUNDLE_ID} from './constants'; +import type {AppleDevice, XcodeBuildArgs} from './types'; +import type {NoSessionProxy} from './no-session-proxy'; const DEFAULT_SIGNING_ID = 'iPhone Developer'; const PREBUILD_DELAY = 0; @@ -28,21 +30,17 @@ 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'; const LIB_SCHEME_TV = 'WebDriverAgentLib_tvOS'; -const REAL_DEVICES_CONFIG_DOCS_LINK = 'https://appium.github.io/appium-xcuitest-driver/latest/preparation/real-device-config/'; +const REAL_DEVICES_CONFIG_DOCS_LINK = + 'https://appium.github.io/appium-xcuitest-driver/latest/preparation/real-device-config/'; const xcodeLog = logger.getLogger('Xcode'); - export class XcodeBuild { xcodebuild?: SubProcess; readonly device: AppleDevice; @@ -85,7 +83,7 @@ export class XcodeBuild { * @param args - Configuration arguments for xcodebuild * @param log - Optional logger instance */ - constructor (device: AppleDevice, args: XcodeBuildArgs, log: AppiumLogger | null = null) { + constructor(device: AppleDevice, args: XcodeBuildArgs, log: AppiumLogger | null = null) { this.device = device; this.log = log ?? defaultLogger; @@ -137,7 +135,7 @@ export class XcodeBuild { * Sets up xctestrun file if needed. * @param noSessionProxy - The proxy instance for WDA communication */ - async init (noSessionProxy: NoSessionProxy): Promise { + async init(noSessionProxy: NoSessionProxy): Promise { this.noSessionProxy = noSessionProxy; if (this.useXctestrunFile) { @@ -145,15 +143,15 @@ export class XcodeBuild { isRealDevice: !!this.realDevice, udid: this.device.udid, platformVersion: this.platformVersion || '', - platformName: this.platformName || '' + platformName: this.platformName || '', }; this.xctestrunFilePath = await setXctestrunFile({ - deviceInfo, - sdkVersion: this.iosSdkVersion || '', - bootstrapPath: this.bootstrapPath, - wdaRemotePort: this.wdaRemotePort || 8100, - wdaBindingIP: this.wdaBindingIP - }); + deviceInfo, + sdkVersion: this.iosSdkVersion || '', + bootstrapPath: this.bootstrapPath, + wdaRemotePort: this.wdaRemotePort || 8100, + wdaBindingIP: this.wdaBindingIP, + }); return; } } @@ -163,7 +161,7 @@ export class XcodeBuild { * Uses cached value if available, otherwise queries xcodebuild for BUILD_DIR. * @returns The derived data path, or `undefined` if it cannot be determined */ - async retrieveDerivedDataPath (): Promise { + async retrieveDerivedDataPath(): Promise { if (this.derivedDataPath) { return this.derivedDataPath; } @@ -201,7 +199,7 @@ export class XcodeBuild { * Pre-builds WebDriverAgent before launching tests. * Performs a build-only operation and sets usePrebuiltWDA flag. */ - async prebuild (): Promise { + async prebuild(): Promise { // first do a build phase this.log.debug('Pre-building WDA before launching test'); this.usePrebuiltWDA = true; @@ -217,17 +215,15 @@ export class XcodeBuild { * Cleans the Xcode project to remove leftovers from previous installs. * Cleans both the library and runner schemes for the appropriate platform. */ - async cleanProject (): Promise { + async cleanProject(): Promise { const libScheme = isTvOS(this.platformName || '') ? LIB_SCHEME_TV : LIB_SCHEME_IOS; const runnerScheme = isTvOS(this.platformName || '') ? RUNNER_SCHEME_TV : RUNNER_SCHEME_IOS; for (const scheme of [libScheme, runnerScheme]) { - this.log.debug(`Cleaning the project scheme '${scheme}' to make sure there are no leftovers from previous installs`); - await exec('xcodebuild', [ - 'clean', - '-project', this.agentPath, - '-scheme', scheme, - ]); + this.log.debug( + `Cleaning the project scheme '${scheme}' to make sure there are no leftovers from previous installs`, + ); + await exec('xcodebuild', ['clean', '-project', this.agentPath, '-scheme', scheme]); } } @@ -237,7 +233,7 @@ export class XcodeBuild { * @returns The WDA status record if tests are run, `void` if build-only * @throws Error if xcodebuild fails or cannot start */ - async start (buildOnly: boolean = false): Promise { + async start(buildOnly: boolean = false): Promise { this.xcodebuild = await this.createSubProcess(buildOnly); // wrap the start procedure in a promise so that we can catch, and report, @@ -247,19 +243,22 @@ export class XcodeBuild { } const xcodebuild = this.xcodebuild; return await new B((resolve, reject) => { - xcodebuild.once('exit', (code, signal) => { + xcodebuild.once('exit', (code, signal) => { xcodeLog.error(`xcodebuild exited with code '${code}' and signal '${signal}'`); xcodebuild.removeAllListeners(); this._didProcessExit = true; if (this._didBuildFail || (!signal && code !== 0)) { - let errorMessage = `xcodebuild failed with code ${code}.` + + let errorMessage = + `xcodebuild failed with code ${code}.` + ` This usually indicates an issue with the local Xcode setup or WebDriverAgent` + ` project configuration or the driver-to-platform version mismatch.`; if (!this.showXcodeLog) { - errorMessage += ` Consider setting 'showXcodeLog' capability to true in` + + errorMessage += + ` Consider setting 'showXcodeLog' capability to true in` + ` order to check the Appium server log for build-related error messages.`; } else if (this.realDevice) { - errorMessage += ` Consider checking the WebDriverAgent configuration guide` + + errorMessage += + ` Consider checking the WebDriverAgent configuration guide` + ` for real iOS devices at ${REAL_DEVICES_CONFIG_DOCS_LINK}.`; } return reject(new Error(errorMessage)); @@ -293,16 +292,18 @@ export class XcodeBuild { /** * Stops the xcodebuild process and cleans up resources. */ - async quit (): Promise { + async quit(): Promise { await killProcess('xcodebuild', this.xcodebuild); } - private getCommand (buildOnly: boolean = false): {cmd: string; args: string[]} { + private getCommand(buildOnly: boolean = false): {cmd: string; args: string[]} { const cmd = 'xcodebuild'; const args: string[] = []; // figure out the targets for xcodebuild - const [buildCmd, testCmd] = this.useSimpleBuildTest ? ['build', 'test'] : ['build-for-testing', 'test-without-building']; + const [buildCmd, testCmd] = this.useSimpleBuildTest + ? ['build', 'test'] + : ['build-for-testing', 'test-without-building']; if (buildOnly) { args.push(buildCmd); } else if (this.usePrebuiltWDA || this.useXctestrunFile) { @@ -336,14 +337,17 @@ export class XcodeBuild { args.push('-destination', `id=${this.device.udid}`); let versionMatch: RegExpMatchArray | null = null; - if (this.platformVersion && (versionMatch = new RegExp(/^(\d+)\.(\d+)/).exec(this.platformVersion))) { + if ( + this.platformVersion && + (versionMatch = new RegExp(/^(\d+)\.(\d+)/).exec(this.platformVersion)) + ) { args.push( - `${isTvOS(this.platformName || '') ? 'TV' : 'IPHONE'}OS_DEPLOYMENT_TARGET=${versionMatch[1]}.${versionMatch[2]}` + `${isTvOS(this.platformName || '') ? 'TV' : 'IPHONE'}OS_DEPLOYMENT_TARGET=${versionMatch[1]}.${versionMatch[2]}`, ); } else { this.log.warn( `Cannot parse major and minor version numbers from platformVersion "${this.platformVersion}". ` + - 'Will build for the default platform instead' + 'Will build for the default platform instead', ); } @@ -375,7 +379,7 @@ export class XcodeBuild { return {cmd, args}; } - private async createSubProcess (buildOnly: boolean = false): Promise { + private async createSubProcess(buildOnly: boolean = false): Promise { if (!this.useXctestrunFile && this.realDevice) { if (this.keychainPath && this.keychainPassword) { await setRealDeviceSecurity(this.keychainPath, this.keychainPassword); @@ -383,8 +387,10 @@ export class XcodeBuild { } const {cmd, args} = this.getCommand(buildOnly); - this.log.debug(`Beginning ${buildOnly ? 'build' : 'test'} with command '${cmd} ${args.join(' ')}' ` + - `in directory '${this.bootstrapPath}'`); + this.log.debug( + `Beginning ${buildOnly ? 'build' : 'test'} with command '${cmd} ${args.join(' ')}' ` + + `in directory '${this.bootstrapPath}'`, + ); const env: Record = Object.assign({}, process.env, { USE_PORT: this.wdaRemotePort, WDA_PRODUCT_BUNDLE_IDENTIFIER: this.updatedWDABundleId || WDA_RUNNER_BUNDLE_ID, @@ -437,7 +443,7 @@ export class XcodeBuild { return xcodebuild; } - private async waitForStart (timer: timing.Timer): Promise { + private async waitForStart(timer: timing.Timer): Promise { // try to connect once every 0.5 seconds, until `launchTimeout` is up const timeout = this.launchTimeout || 60000; // Default to 60 seconds if not set this.log.debug(`Waiting up to ${timeout}ms for WebDriverAgent to start`); @@ -457,7 +463,7 @@ export class XcodeBuild { const proxyTimeout = noSessionProxy.timeout; noSessionProxy.timeout = 1000; try { - currentStatus = await noSessionProxy.command('/status', 'GET') as StringRecord; + currentStatus = (await noSessionProxy.command('/status', 'GET')) as StringRecord; if (currentStatus && currentStatus.ios && (currentStatus.ios as any).ip) { this.agentUrl = (currentStatus.ios as any).ip; } @@ -475,12 +481,14 @@ export class XcodeBuild { return currentStatus; } - this.log.debug(`WebDriverAgent successfully started after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); + this.log.debug( + `WebDriverAgent successfully started after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`, + ); } catch (err: any) { this.log.debug(err.stack); throw new Error( `We were not able to retrieve the /status response from the WebDriverAgent server after ${timeout}ms timeout.` + - `Try to increase the value of 'appium:wdaLaunchTimeout' capability as a possible workaround.` + `Try to increase the value of 'appium:wdaLaunchTimeout' capability as a possible workaround.`, ); } return currentStatus; diff --git a/package.json b/package.json index 9c7df846c..80532b1e8 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,9 @@ "dev": "npm run build -- --watch", "clean": "npm run build -- --clean", "lint": "eslint .", - "format": "prettier -w ./lib", "lint:fix": "npm run lint -- --fix", + "format": "prettier -w ./lib ./test", + "format:check": "prettier --check ./lib ./test", "prepare": "npm run build", "version": "npm run sync-wda-version", "test": "mocha --exit --timeout 1m \"./test/unit/**/*-specs.ts\"", diff --git a/test/functional/desired.ts b/test/functional/desired.ts index 0f5c8dbbd..99d46ae4d 100644 --- a/test/functional/desired.ts +++ b/test/functional/desired.ts @@ -1,7 +1,8 @@ -import { util } from '@appium/support'; +import {util} from '@appium/support'; export const PLATFORM_VERSION: string = process.env.PLATFORM_VERSION - ? process.env.PLATFORM_VERSION : '11.3'; -export const DEVICE_NAME: string = process.env.DEVICE_NAME - || (util.compareVersions(PLATFORM_VERSION, '>=', '13.0') ? 'iPhone X' : 'iPhone 6'); - + ? process.env.PLATFORM_VERSION + : '11.3'; +export const DEVICE_NAME: string = + process.env.DEVICE_NAME || + (util.compareVersions(PLATFORM_VERSION, '>=', '13.0') ? 'iPhone X' : 'iPhone 6'); diff --git a/test/functional/helpers/simulator.ts b/test/functional/helpers/simulator.ts index b79828c5d..36a4df3e1 100644 --- a/test/functional/helpers/simulator.ts +++ b/test/functional/helpers/simulator.ts @@ -1,11 +1,11 @@ import _ from 'lodash'; -import { Simctl } from 'node-simctl'; -import { retryInterval } from 'asyncbox'; -import { killAllSimulators as simKill } from 'appium-ios-simulator'; -import { resetTestProcesses } from '../../../lib/utils'; -import type { AppleDevice } from '../../../lib/types'; +import {Simctl} from 'node-simctl'; +import {retryInterval} from 'asyncbox'; +import {killAllSimulators as simKill} from 'appium-ios-simulator'; +import {resetTestProcesses} from '../../../lib/utils'; +import type {AppleDevice} from '../../../lib/types'; -export async function killAllSimulators (): Promise { +export async function killAllSimulators(): Promise { const simctl = new Simctl(); const allDevices = _.flatMap(_.values(await simctl.getDevices())); const bootedDevices = allDevices.filter((device) => device.state === 'Booted'); @@ -20,13 +20,13 @@ export async function killAllSimulators (): Promise { await simKill(); } -export async function shutdownSimulator (device: AppleDevice): 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(); } -export async function deleteDeviceWithRetry (udid: string): Promise { +export async function deleteDeviceWithRetry(udid: string): Promise { const simctl = new Simctl({udid}); try { await retryInterval(10, 1000, simctl.deleteDevice.bind(simctl)); diff --git a/test/functional/webdriveragent-e2e-specs.ts b/test/functional/webdriveragent-e2e-specs.ts index 3e2f8b5d7..5d98329c1 100644 --- a/test/functional/webdriveragent-e2e-specs.ts +++ b/test/functional/webdriveragent-e2e-specs.ts @@ -1,14 +1,14 @@ -import chai, { expect } from 'chai'; +import chai, {expect} from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import { Simctl } from 'node-simctl'; -import { getSimulator } from 'appium-ios-simulator'; -import { killAllSimulators, shutdownSimulator } from './helpers/simulator'; -import { SubProcess } from 'teen_process'; -import { PLATFORM_VERSION, DEVICE_NAME } from './desired'; -import { retryInterval } from 'asyncbox'; -import { WebDriverAgent } from '../../lib/webdriveragent'; +import {Simctl} from 'node-simctl'; +import {getSimulator} from 'appium-ios-simulator'; +import {killAllSimulators, shutdownSimulator} from './helpers/simulator'; +import {SubProcess} from 'teen_process'; +import {PLATFORM_VERSION, DEVICE_NAME} from './desired'; +import {retryInterval} from 'asyncbox'; +import {WebDriverAgent} from '../../lib/webdriveragent'; import axios from 'axios'; -import type { AppleDevice } from '../../lib/types'; +import type {AppleDevice} from '../../lib/types'; chai.use(chaiAsPromised); @@ -19,7 +19,7 @@ const SIM_STARTUP_TIMEOUT_MS = MOCHA_TIMEOUT_MS; const testUrl = 'http://localhost:8100/tree'; -function getStartOpts (device: AppleDevice) { +function getStartOpts(device: AppleDevice) { return { device, platformVersion: PLATFORM_VERSION, @@ -31,7 +31,6 @@ function getStartOpts (device: AppleDevice) { }; } - describe('WebDriverAgent', function () { this.timeout(MOCHA_TIMEOUT_MS); @@ -41,11 +40,7 @@ describe('WebDriverAgent', function () { before(async function () { simctl = new Simctl(); - simctl.udid = await simctl.createDevice( - SIM_DEVICE_NAME, - DEVICE_NAME, - PLATFORM_VERSION - ); + simctl.udid = await simctl.createDevice(SIM_DEVICE_NAME, DEVICE_NAME, PLATFORM_VERSION); device = await getSimulator(simctl.udid); // Prebuild WDA @@ -108,12 +103,10 @@ describe('WebDriverAgent', function () { return new SubProcess('xcodebuild', args, {detached: true}); }; - await expect(agent.launch('sessionId')) - .to.be.rejectedWith('xcodebuild failed'); + await expect(agent.launch('sessionId')).to.be.rejectedWith('xcodebuild failed'); await agent.quit(); }); }); }); }); - diff --git a/test/unit/utils-specs.ts b/test/unit/utils-specs.ts index 8308d6ccd..d7e3ef01c 100644 --- a/test/unit/utils-specs.ts +++ b/test/unit/utils-specs.ts @@ -1,13 +1,13 @@ -import chai, { expect } from 'chai'; +import chai, {expect} from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import { getXctestrunFilePath, getAdditionalRunContent, getXctestrunFileName } from '../../lib/utils'; -import { PLATFORM_NAME_IOS, PLATFORM_NAME_TVOS } from '../../lib/constants'; -import { fs } from '@appium/support'; +import {getXctestrunFilePath, getAdditionalRunContent, getXctestrunFileName} from '../../lib/utils'; +import {PLATFORM_NAME_IOS, PLATFORM_NAME_TVOS} from '../../lib/constants'; +import {fs} from '@appium/support'; import path from 'node:path'; -import { fail } from 'node:assert'; -import { arch } from 'node:os'; +import {fail} from 'node:assert'; +import {arch} from 'node:os'; import sinon from 'sinon'; -import type { DeviceInfo } from '../../lib/types'; +import type {DeviceInfo} from '../../lib/types'; chai.use(chaiAsPromised); @@ -16,7 +16,6 @@ function get_arch(): string { } describe('utils', function () { - describe('#getXctestrunFilePath', function () { const platformVersion = '12.0'; const sdkVersion = '12.2'; @@ -34,71 +33,140 @@ describe('utils', function () { }); it('should return sdk based path with udid', async function () { - sandbox.stub(fs, 'exists') + sandbox + .stub(fs, 'exists') .withArgs(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)) .resolves(true); sandbox.stub(fs, 'copyFile'); const deviceInfo: DeviceInfo = {isRealDevice: true, udid, platformVersion, platformName}; - await expect(getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath)) - .to.eventually.equal(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)); + await expect(getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath)).to.eventually.equal( + path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`), + ); sandbox.assert.notCalled(fs.copyFile); }); it('should return sdk based path without udid, copy them', async function () { const existsStub = sandbox.stub(fs, 'exists'); - existsStub.withArgs(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)).resolves(false); - existsStub.withArgs(path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphoneos${sdkVersion}-arm64.xctestrun`)).resolves(true); - sandbox.stub(fs, 'copyFile') + existsStub + .withArgs(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)) + .resolves(false); + existsStub + .withArgs( + path.resolve( + `${bootstrapPath}/WebDriverAgentRunner_iphoneos${sdkVersion}-arm64.xctestrun`, + ), + ) + .resolves(true); + sandbox + .stub(fs, 'copyFile') .withArgs( - path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphoneos${sdkVersion}-arm64.xctestrun`), - path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`) + path.resolve( + `${bootstrapPath}/WebDriverAgentRunner_iphoneos${sdkVersion}-arm64.xctestrun`, + ), + path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`), ) .resolves(); - const deviceInfo: DeviceInfo = {isRealDevice: true, udid, platformVersion, platformName: PLATFORM_NAME_IOS}; - await expect(getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath)) - .to.eventually.equal(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)); + const deviceInfo: DeviceInfo = { + isRealDevice: true, + udid, + platformVersion, + platformName: PLATFORM_NAME_IOS, + }; + await expect(getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath)).to.eventually.equal( + path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`), + ); }); it('should return platform based path', async function () { const existsStub = sandbox.stub(fs, 'exists'); - existsStub.withArgs(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)).resolves(false); - existsStub.withArgs(path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${sdkVersion}-${get_arch()}.xctestrun`)).resolves(false); - existsStub.withArgs(path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`)).resolves(true); + existsStub + .withArgs(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)) + .resolves(false); + existsStub + .withArgs( + path.resolve( + `${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${sdkVersion}-${get_arch()}.xctestrun`, + ), + ) + .resolves(false); + existsStub + .withArgs(path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`)) + .resolves(true); sandbox.stub(fs, 'copyFile'); - const deviceInfo: DeviceInfo = {isRealDevice: false, udid, platformVersion, platformName: PLATFORM_NAME_IOS}; - await expect(getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath)) - .to.eventually.equal(path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`)); + const deviceInfo: DeviceInfo = { + isRealDevice: false, + udid, + platformVersion, + platformName: PLATFORM_NAME_IOS, + }; + await expect(getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath)).to.eventually.equal( + path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`), + ); sandbox.assert.notCalled(fs.copyFile); }); it('should return platform based path without udid, copy them', async function () { const existsStub = sandbox.stub(fs, 'exists'); - existsStub.withArgs(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)).resolves(false); - existsStub.withArgs(path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${sdkVersion}-${get_arch()}.xctestrun`)).resolves(false); - existsStub.withArgs(path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`)).resolves(false); - existsStub.withArgs(path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${platformVersion}-${get_arch()}.xctestrun`)).resolves(true); - sandbox.stub(fs, 'copyFile') + existsStub + .withArgs(path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`)) + .resolves(false); + existsStub + .withArgs( + path.resolve( + `${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${sdkVersion}-${get_arch()}.xctestrun`, + ), + ) + .resolves(false); + existsStub + .withArgs(path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`)) + .resolves(false); + existsStub .withArgs( - path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${platformVersion}-${get_arch()}.xctestrun`), - path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`) + path.resolve( + `${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${platformVersion}-${get_arch()}.xctestrun`, + ), + ) + .resolves(true); + sandbox + .stub(fs, 'copyFile') + .withArgs( + path.resolve( + `${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${platformVersion}-${get_arch()}.xctestrun`, + ), + path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`), ) .resolves(); - const deviceInfo: DeviceInfo = {isRealDevice: false, udid, platformVersion, platformName: PLATFORM_NAME_IOS}; - await expect(getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath)) - .to.eventually.equal(path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`)); + const deviceInfo: DeviceInfo = { + isRealDevice: false, + udid, + platformVersion, + platformName: PLATFORM_NAME_IOS, + }; + await expect(getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath)).to.eventually.equal( + path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`), + ); }); it('should raise an exception because of no files', async function () { - const expected = path.resolve(`${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${sdkVersion}-${get_arch()}.xctestrun`); + const expected = path.resolve( + `${bootstrapPath}/WebDriverAgentRunner_iphonesimulator${sdkVersion}-${get_arch()}.xctestrun`, + ); sandbox.stub(fs, 'exists').resolves(false); - const deviceInfo: DeviceInfo = {isRealDevice: false, udid, platformVersion, platformName: PLATFORM_NAME_IOS}; + const deviceInfo: DeviceInfo = { + isRealDevice: false, + udid, + platformVersion, + platformName: PLATFORM_NAME_IOS, + }; try { await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath); fail(); } catch (err: any) { - expect(err.message).to.equal(`If you are using 'useXctestrunFile' capability then you need to have a xctestrun file (expected: '${expected}')`); + expect(err.message).to.equal( + `If you are using 'useXctestrunFile' capability then you need to have a xctestrun file (expected: '${expected}')`, + ); } }); }); @@ -106,14 +174,12 @@ describe('utils', function () { describe('#getAdditionalRunContent', function () { it('should return ios format', function () { const wdaPort = getAdditionalRunContent(PLATFORM_NAME_IOS, 8000); - expect(wdaPort.WebDriverAgentRunner - .EnvironmentVariables.USE_PORT).to.equal('8000'); + expect(wdaPort.WebDriverAgentRunner.EnvironmentVariables.USE_PORT).to.equal('8000'); }); it('should return tvos format', function () { const wdaPort = getAdditionalRunContent(PLATFORM_NAME_TVOS, '9000'); - expect(wdaPort.WebDriverAgentRunner_tvOS - .EnvironmentVariables.USE_PORT).to.equal('9000'); + expect(wdaPort.WebDriverAgentRunner_tvOS.EnvironmentVariables.USE_PORT).to.equal('9000'); }); }); @@ -126,7 +192,8 @@ describe('utils', function () { const deviceInfo: DeviceInfo = {isRealDevice: true, udid, platformVersion, platformName}; expect(getXctestrunFileName(deviceInfo, '10.2.0')).to.equal( - 'WebDriverAgentRunner_iphoneos10.2.0-arm64.xctestrun'); + 'WebDriverAgentRunner_iphoneos10.2.0-arm64.xctestrun', + ); }); it('should return ios format, simulator', function () { @@ -134,7 +201,8 @@ describe('utils', function () { const deviceInfo: DeviceInfo = {isRealDevice: false, udid, platformVersion, platformName}; expect(getXctestrunFileName(deviceInfo, '10.2.0')).to.equal( - `WebDriverAgentRunner_iphonesimulator10.2.0-${get_arch()}.xctestrun`); + `WebDriverAgentRunner_iphonesimulator10.2.0-${get_arch()}.xctestrun`, + ); }); it('should return tvos format, real device', function () { @@ -142,7 +210,8 @@ describe('utils', function () { const deviceInfo: DeviceInfo = {isRealDevice: true, udid, platformVersion, platformName}; expect(getXctestrunFileName(deviceInfo, '10.2.0')).to.equal( - 'WebDriverAgentRunner_tvOS_appletvos10.2.0-arm64.xctestrun'); + 'WebDriverAgentRunner_tvOS_appletvos10.2.0-arm64.xctestrun', + ); }); it('should return tvos format, simulator', function () { @@ -150,8 +219,8 @@ describe('utils', function () { const deviceInfo: DeviceInfo = {isRealDevice: false, udid, platformVersion, platformName}; expect(getXctestrunFileName(deviceInfo, '10.2.0')).to.equal( - `WebDriverAgentRunner_tvOS_appletvsimulator10.2.0-${get_arch()}.xctestrun`); + `WebDriverAgentRunner_tvOS_appletvsimulator10.2.0-${get_arch()}.xctestrun`, + ); }); }); }); - diff --git a/test/unit/webdriveragent-specs.ts b/test/unit/webdriveragent-specs.ts index e18d0db9d..1499c8077 100644 --- a/test/unit/webdriveragent-specs.ts +++ b/test/unit/webdriveragent-specs.ts @@ -1,12 +1,12 @@ -import chai, { expect } from 'chai'; +import chai, {expect} from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import { BOOTSTRAP_PATH } from '../../lib/utils'; -import { WebDriverAgent } from '../../lib/webdriveragent'; +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'; +import type {WebDriverAgentArgs, AppleDevice} from '../../lib/types'; chai.use(chaiAsPromised); @@ -15,11 +15,11 @@ const fakeConstructorArgs: WebDriverAgentArgs = { udid: 'some-sim-udid', simctl: {}, devicectl: {}, - idb: null + idb: null, }, platformVersion: '9', host: 'me', - realDevice: false + realDevice: false, }; const defaultAgentPath = path.resolve(BOOTSTRAP_PATH, 'WebDriverAgent.xcodeproj'); @@ -28,419 +28,447 @@ const customAgentPath = '/path/to/some/agent/WebDriverAgent.xcodeproj'; const customDerivedDataPath = '/path/to/some/agent/DerivedData/'; describe('WebDriverAgent', function () { - describe('Constructor', function () { - - it('should have a default wda agent if not specified', function () { - const agent = new WebDriverAgent(fakeConstructorArgs); - expect(agent.bootstrapPath).to.eql(BOOTSTRAP_PATH); - expect(agent.agentPath).to.eql(defaultAgentPath); - }); - it('should have custom wda bootstrap and default agent if only bootstrap specified', function () { - const agent = new WebDriverAgent(_.defaults({ - bootstrapPath: customBootstrapPath, - }, fakeConstructorArgs)); - 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)); - 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)); - if (agent.xcodebuild) { - expect(agent.xcodebuild.derivedDataPath).to.eql(customDerivedDataPath); - } - }); -}); - -describe('launch', function () { - it('should use webDriverAgentUrl override and return current status', async function () { - const override = 'http://mockurl:8100/'; - const args = Object.assign({}, fakeConstructorArgs); - args.webDriverAgentUrl = override; - const agent = new WebDriverAgent(args); - const wdaStub = sinon.stub(agent as any, 'getStatus'); - wdaStub.callsFake(function () { - return {build: 'data'}; + it('should have a default wda agent if not specified', function () { + const agent = new WebDriverAgent(fakeConstructorArgs); + expect(agent.bootstrapPath).to.eql(BOOTSTRAP_PATH); + expect(agent.agentPath).to.eql(defaultAgentPath); }); - - await expect(agent.launch('sessionId')).to.eventually.eql({build: 'data'}); - expect(agent.url.href).to.eql(override); - if (agent.jwproxy) { - expect(agent.jwproxy.server).to.eql('mockurl'); - expect(agent.jwproxy.port).to.eql(8100); - expect(agent.jwproxy.base).to.eql(''); - expect(agent.jwproxy.scheme).to.eql('http'); - } - if (agent.noSessionProxy) { - expect(agent.noSessionProxy.server).to.eql('mockurl'); - expect(agent.noSessionProxy.port).to.eql(8100); - expect(agent.noSessionProxy.base).to.eql(''); - expect(agent.noSessionProxy.scheme).to.eql('http'); - } - wdaStub.reset(); - }); -}); - -describe('use wda proxy url', function () { - it('should use webDriverAgentUrl wda proxy url', async function () { - const override = 'http://127.0.0.1:8100/aabbccdd'; - const args = Object.assign({}, fakeConstructorArgs); - args.webDriverAgentUrl = override; - const agent = new WebDriverAgent(args); - const wdaStub = sinon.stub(agent as any, 'getStatus'); - wdaStub.callsFake(function () { - return {build: 'data'}; + it('should have custom wda bootstrap and default agent if only bootstrap specified', function () { + const agent = new WebDriverAgent( + _.defaults( + { + bootstrapPath: customBootstrapPath, + }, + fakeConstructorArgs, + ), + ); + expect(agent.bootstrapPath).to.eql(customBootstrapPath); + expect(agent.agentPath).to.eql(path.resolve(customBootstrapPath, 'WebDriverAgent.xcodeproj')); }); - - await expect(agent.launch('sessionId')).to.eventually.eql({build: 'data'}); - - expect(agent.url.port).to.eql('8100'); - expect(agent.url.hostname).to.eql('127.0.0.1'); - expect(agent.url.path).to.eql('/aabbccdd'); - if (agent.jwproxy) { - expect(agent.jwproxy.server).to.eql('127.0.0.1'); - expect(agent.jwproxy.port).to.eql(8100); - expect(agent.jwproxy.base).to.eql('/aabbccdd'); - expect(agent.jwproxy.scheme).to.eql('http'); - } - if (agent.noSessionProxy) { - expect(agent.noSessionProxy.server).to.eql('127.0.0.1'); - expect(agent.noSessionProxy.port).to.eql(8100); - expect(agent.noSessionProxy.base).to.eql('/aabbccdd'); - expect(agent.noSessionProxy.scheme).to.eql('http'); - } - }); -}); - -describe('get url', function () { - it('should use default WDA listening url', function () { - const args = Object.assign({}, fakeConstructorArgs); - const agent = new WebDriverAgent(args); - expect(agent.url.href).to.eql('http://127.0.0.1:8100/'); - (agent as any).setupProxies('mysession'); - if (agent.jwproxy) { - expect(agent.jwproxy.scheme).to.eql('http'); - } - if (agent.noSessionProxy) { - expect(agent.noSessionProxy.scheme).to.eql('http'); - } - }); - it('should use default WDA listening url with emply base url', function () { - const wdaLocalPort = '9100'; - const wdaBaseUrl = ''; - - const args = Object.assign({}, fakeConstructorArgs); - args.wdaBaseUrl = wdaBaseUrl; - args.wdaLocalPort = parseInt(wdaLocalPort, 10); - - const agent = new WebDriverAgent(args); - expect(agent.url.href).to.eql('http://127.0.0.1:9100/'); - (agent as any).setupProxies('mysession'); - if (agent.jwproxy) { - expect(agent.jwproxy.scheme).to.eql('http'); - } - if (agent.noSessionProxy) { - expect(agent.noSessionProxy.scheme).to.eql('http'); - } - }); - it('should use customised WDA listening url', function () { - const wdaLocalPort = '9100'; - const wdaBaseUrl = 'http://mockurl'; - - const args = Object.assign({}, fakeConstructorArgs); - args.wdaBaseUrl = wdaBaseUrl; - args.wdaLocalPort = parseInt(wdaLocalPort, 10); - - const agent = new WebDriverAgent(args); - expect(agent.url.href).to.eql('http://mockurl:9100/'); - (agent as any).setupProxies('mysession'); - if (agent.jwproxy) { - expect(agent.jwproxy.scheme).to.eql('http'); - } - if (agent.noSessionProxy) { - expect(agent.noSessionProxy.scheme).to.eql('http'); - } - }); - it('should use customised WDA listening url with slash', function () { - const wdaLocalPort = '9100'; - const wdaBaseUrl = 'http://mockurl/'; - - const args = Object.assign({}, fakeConstructorArgs); - args.wdaBaseUrl = wdaBaseUrl; - args.wdaLocalPort = parseInt(wdaLocalPort, 10); - - const agent = new WebDriverAgent(args); - expect(agent.url.href).to.eql('http://mockurl:9100/'); - (agent as any).setupProxies('mysession'); - if (agent.jwproxy) { - expect(agent.jwproxy.scheme).to.eql('http'); - } - if (agent.noSessionProxy) { - expect(agent.noSessionProxy.scheme).to.eql('http'); - } - }); - it('should use the given webDriverAgentUrl and ignore other params', function () { - const args = Object.assign({}, fakeConstructorArgs); - args.wdaBaseUrl = 'http://mockurl/'; - args.wdaLocalPort = 9100; - args.webDriverAgentUrl = 'https://127.0.0.1:8100/'; - - const agent = new WebDriverAgent(args); - expect(agent.url.href).to.eql('https://127.0.0.1:8100/'); - }); - it('should set scheme to https for https webDriverAgentUrl', function () { - const args = Object.assign({}, fakeConstructorArgs); - args.webDriverAgentUrl = 'https://127.0.0.1:8100/'; - const agent = new WebDriverAgent(args); - (agent as any).setupProxies('mysession'); - if (agent.jwproxy) { - expect(agent.jwproxy.scheme).to.eql('https'); - } - if (agent.noSessionProxy) { - expect(agent.noSessionProxy.scheme).to.eql('https'); - } - }); -}); - -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, 'getStatus'); - wdaStubUninstall = sinon.stub(wda as any, 'uninstall'); - }); - - afterEach(function () { - for (const stub of [wdaStub, wdaStubUninstall, getTimestampStub]) { - if (stub) { - stub.reset(); + it('should have custom wda bootstrap and agent if both specified', function () { + const agent = new WebDriverAgent( + _.defaults( + { + bootstrapPath: customBootstrapPath, + agentPath: customAgentPath, + }, + fakeConstructorArgs, + ), + ); + 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, + ), + ); + if (agent.xcodebuild) { + expect(agent.xcodebuild.derivedDataPath).to.eql(customDerivedDataPath); } - } - }); - - it('should not call uninstall since no Running WDA', async function () { - wdaStub.callsFake(function () { - return null; }); - wdaStubUninstall.callsFake(_.noop); - - await wda.setupCaching(); - expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.notCalled).to.be.true; - expect(_.isUndefined(wda.webDriverAgentUrl)).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' }}; + describe('launch', function () { + it('should use webDriverAgentUrl override and return current status', async function () { + const override = 'http://mockurl:8100/'; + const args = Object.assign({}, fakeConstructorArgs); + args.webDriverAgentUrl = override; + const agent = new WebDriverAgent(args); + const wdaStub = sinon.stub(agent as any, 'getStatus'); + wdaStub.callsFake(function () { + return {build: 'data'}; + }); + + await expect(agent.launch('sessionId')).to.eventually.eql({build: 'data'}); + expect(agent.url.href).to.eql(override); + if (agent.jwproxy) { + expect(agent.jwproxy.server).to.eql('mockurl'); + expect(agent.jwproxy.port).to.eql(8100); + expect(agent.jwproxy.base).to.eql(''); + expect(agent.jwproxy.scheme).to.eql('http'); + } + if (agent.noSessionProxy) { + expect(agent.noSessionProxy.server).to.eql('mockurl'); + expect(agent.noSessionProxy.port).to.eql(8100); + expect(agent.noSessionProxy.base).to.eql(''); + expect(agent.noSessionProxy.scheme).to.eql('http'); + } + wdaStub.reset(); }); - wdaStubUninstall.callsFake(_.noop); - - 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 () { - wdaStub.callsFake(function () { - return {build: { time: 'Jun 24 2018 17:08:21', productBundleIdentifier: 'com.example.WebDriverAgent' }}; + describe('use wda proxy url', function () { + it('should use webDriverAgentUrl wda proxy url', async function () { + const override = 'http://127.0.0.1:8100/aabbccdd'; + const args = Object.assign({}, fakeConstructorArgs); + args.webDriverAgentUrl = override; + const agent = new WebDriverAgent(args); + const wdaStub = sinon.stub(agent as any, 'getStatus'); + wdaStub.callsFake(function () { + return {build: 'data'}; + }); + + await expect(agent.launch('sessionId')).to.eventually.eql({build: 'data'}); + + expect(agent.url.port).to.eql('8100'); + expect(agent.url.hostname).to.eql('127.0.0.1'); + expect(agent.url.path).to.eql('/aabbccdd'); + if (agent.jwproxy) { + expect(agent.jwproxy.server).to.eql('127.0.0.1'); + expect(agent.jwproxy.port).to.eql(8100); + expect(agent.jwproxy.base).to.eql('/aabbccdd'); + expect(agent.jwproxy.scheme).to.eql('http'); + } + if (agent.noSessionProxy) { + expect(agent.noSessionProxy.server).to.eql('127.0.0.1'); + expect(agent.noSessionProxy.port).to.eql(8100); + expect(agent.noSessionProxy.base).to.eql('/aabbccdd'); + expect(agent.noSessionProxy.scheme).to.eql('http'); + } }); - wdaStubUninstall.callsFake(_.noop); - - await wda.setupCaching(); - expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.calledOnce).to.be.true; - expect(_.isUndefined(wda.webDriverAgentUrl)).to.be.true; }); - it('should call uninstall once since bundle id is different with updatedWDABundleId capability', async function () { - wdaStub.callsFake(function () { - return {build: { time: 'Jun 24 2018 17:08:21', productBundleIdentifier: 'com.example.different.WebDriverAgent' }}; + describe('get url', function () { + it('should use default WDA listening url', function () { + const args = Object.assign({}, fakeConstructorArgs); + const agent = new WebDriverAgent(args); + expect(agent.url.href).to.eql('http://127.0.0.1:8100/'); + (agent as any).setupProxies('mysession'); + if (agent.jwproxy) { + expect(agent.jwproxy.scheme).to.eql('http'); + } + if (agent.noSessionProxy) { + expect(agent.noSessionProxy.scheme).to.eql('http'); + } }); + it('should use default WDA listening url with emply base url', function () { + const wdaLocalPort = '9100'; + const wdaBaseUrl = ''; - wdaStubUninstall.callsFake(_.noop); - - await wda.setupCaching(); - expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.calledOnce).to.be.true; - expect(_.isUndefined(wda.webDriverAgentUrl)).to.be.true; - }); - - it('should not call uninstall since bundle id is equal to updatedWDABundleId capability', async function () { - wda = new WebDriverAgent({ ...fakeConstructorArgs, updatedWDABundleId: 'com.example.WebDriverAgent' }); - wdaStub = sinon.stub(wda, 'getStatus'); - wdaStubUninstall = sinon.stub(wda as any, 'uninstall'); + const args = Object.assign({}, fakeConstructorArgs); + args.wdaBaseUrl = wdaBaseUrl; + args.wdaLocalPort = parseInt(wdaLocalPort, 10); - wdaStub.callsFake(function () { - return {build: { time: 'Jun 24 2018 17:08:21', productBundleIdentifier: 'com.example.WebDriverAgent' }}; + const agent = new WebDriverAgent(args); + expect(agent.url.href).to.eql('http://127.0.0.1:9100/'); + (agent as any).setupProxies('mysession'); + if (agent.jwproxy) { + expect(agent.jwproxy.scheme).to.eql('http'); + } + if (agent.noSessionProxy) { + expect(agent.noSessionProxy.scheme).to.eql('http'); + } }); + it('should use customised WDA listening url', function () { + const wdaLocalPort = '9100'; + const wdaBaseUrl = 'http://mockurl'; - wdaStubUninstall.callsFake(_.noop); - - 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/'); - }); + const args = Object.assign({}, fakeConstructorArgs); + args.wdaBaseUrl = wdaBaseUrl; + args.wdaLocalPort = parseInt(wdaLocalPort, 10); - it('should call uninstall if current revision differs from the bundled one', async function () { - wdaStub.callsFake(function () { - return {build: { upgradedAt: '1' }}; + const agent = new WebDriverAgent(args); + expect(agent.url.href).to.eql('http://mockurl:9100/'); + (agent as any).setupProxies('mysession'); + if (agent.jwproxy) { + expect(agent.jwproxy.scheme).to.eql('http'); + } + if (agent.noSessionProxy) { + expect(agent.noSessionProxy.scheme).to.eql('http'); + } }); - getTimestampStub.callsFake(() => '2'); - wdaStubUninstall.callsFake(_.noop); + it('should use customised WDA listening url with slash', function () { + const wdaLocalPort = '9100'; + const wdaBaseUrl = 'http://mockurl/'; - await wda.setupCaching(); - expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.calledOnce).to.be.true; - }); + const args = Object.assign({}, fakeConstructorArgs); + args.wdaBaseUrl = wdaBaseUrl; + args.wdaLocalPort = parseInt(wdaLocalPort, 10); - it('should not call uninstall if current revision is the same as the bundled one', async function () { - wdaStub.callsFake(function () { - return {build: { upgradedAt: '1' }}; + const agent = new WebDriverAgent(args); + expect(agent.url.href).to.eql('http://mockurl:9100/'); + (agent as any).setupProxies('mysession'); + if (agent.jwproxy) { + expect(agent.jwproxy.scheme).to.eql('http'); + } + if (agent.noSessionProxy) { + expect(agent.noSessionProxy.scheme).to.eql('http'); + } }); - getTimestampStub.callsFake(() => '1'); - wdaStubUninstall.callsFake(_.noop); - - await wda.setupCaching(); - expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.notCalled).to.be.true; - }); + it('should use the given webDriverAgentUrl and ignore other params', function () { + const args = Object.assign({}, fakeConstructorArgs); + args.wdaBaseUrl = 'http://mockurl/'; + args.wdaLocalPort = 9100; + args.webDriverAgentUrl = 'https://127.0.0.1:8100/'; - it('should not call uninstall if current revision cannot be retrieved from WDA status', async function () { - wdaStub.callsFake(function () { - return {build: {}}; + const agent = new WebDriverAgent(args); + expect(agent.url.href).to.eql('https://127.0.0.1:8100/'); }); - getTimestampStub.callsFake(() => '1'); - wdaStubUninstall.callsFake(_.noop); - - await wda.setupCaching(); - expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.notCalled).to.be.true; - }); - - it('should not call uninstall if current revision cannot be retrieved from the file system', async function () { - wdaStub.callsFake(function () { - return {build: { upgradedAt: '1' }}; + it('should set scheme to https for https webDriverAgentUrl', function () { + const args = Object.assign({}, fakeConstructorArgs); + args.webDriverAgentUrl = 'https://127.0.0.1:8100/'; + const agent = new WebDriverAgent(args); + (agent as any).setupProxies('mysession'); + if (agent.jwproxy) { + expect(agent.jwproxy.scheme).to.eql('https'); + } + if (agent.noSessionProxy) { + expect(agent.noSessionProxy.scheme).to.eql('https'); + } }); - getTimestampStub.callsFake(() => null); - wdaStubUninstall.callsFake(_.noop); - - await wda.setupCaching(); - expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.notCalled).to.be.true; }); - describe('uninstall', function () { - let device: AppleDevice; + describe('setupCaching()', function () { let wda: WebDriverAgent; - let deviceGetBundleIdsStub: sinon.SinonStub; - let deviceRemoveAppStub: sinon.SinonStub; + let wdaStub: sinon.SinonStub; + let wdaStubUninstall: sinon.SinonStub; + const getTimestampStub = sinon.stub(utils, 'getWDAUpgradeTimestamp'); beforeEach(function () { - device = { - getUserInstalledBundleIdsByBundleName: () => {}, - removeApp: () => {} - } as any; - wda = new WebDriverAgent({device} as WebDriverAgentArgs); - deviceGetBundleIdsStub = sinon.stub(device, 'getUserInstalledBundleIdsByBundleName'); - deviceRemoveAppStub = sinon.stub(device, 'removeApp'); + wda = new WebDriverAgent(fakeConstructorArgs); + wdaStub = sinon.stub(wda, 'getStatus'); + wdaStubUninstall = sinon.stub(wda as any, 'uninstall'); }); afterEach(function () { - for (const stub of [deviceGetBundleIdsStub, deviceRemoveAppStub]) { + for (const stub of [wdaStub, wdaStubUninstall, getTimestampStub]) { if (stub) { stub.reset(); } } }); - it('should not call uninstall', async function () { - deviceGetBundleIdsStub.callsFake(() => []); + it('should not call uninstall since no Running WDA', async function () { + wdaStub.callsFake(function () { + return null; + }); + wdaStubUninstall.callsFake(_.noop); - await (wda as any).uninstall(); - expect(deviceGetBundleIdsStub.calledOnce).to.be.true; - expect(deviceRemoveAppStub.notCalled).to.be.true; + await wda.setupCaching(); + expect(wdaStub.calledOnce).to.be.true; + expect(wdaStubUninstall.notCalled).to.be.true; + expect(_.isUndefined(wda.webDriverAgentUrl)).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)); + 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); - await (wda as any).uninstall(); - expect(deviceGetBundleIdsStub.calledOnce).to.be.true; - expect(deviceRemoveAppStub.calledOnce).to.be.true; - expect(uninstalledBundIds).to.eql(['com.appium.WDA1']); + 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 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']); + it('should call uninstall once since bundle id is not default without updatedWDABundleId capability', async function () { + wdaStub.callsFake(function () { + return { + build: { + time: 'Jun 24 2018 17:08:21', + productBundleIdentifier: 'com.example.WebDriverAgent', + }, + }; + }); + wdaStubUninstall.callsFake(_.noop); + + await wda.setupCaching(); + expect(wdaStub.calledOnce).to.be.true; + expect(wdaStubUninstall.calledOnce).to.be.true; + expect(_.isUndefined(wda.webDriverAgentUrl)).to.be.true; }); - }); -}); + it('should call uninstall once since bundle id is different with updatedWDABundleId capability', async function () { + wdaStub.callsFake(function () { + return { + build: { + time: 'Jun 24 2018 17:08:21', + productBundleIdentifier: 'com.example.different.WebDriverAgent', + }, + }; + }); + + wdaStubUninstall.callsFake(_.noop); + + await wda.setupCaching(); + expect(wdaStub.calledOnce).to.be.true; + expect(wdaStubUninstall.calledOnce).to.be.true; + expect(_.isUndefined(wda.webDriverAgentUrl)).to.be.true; + }); -describe('usePreinstalledWDA related functions', function () { - describe('bundleIdForXctest', function () { - it('should have xctrunner automatically', function () { - const args = Object.assign({}, fakeConstructorArgs); - args.updatedWDABundleId = 'io.appium.wda'; - const agent = new WebDriverAgent(args); - expect(agent.bundleIdForXctest).to.equal('io.appium.wda.xctrunner'); + it('should not call uninstall since bundle id is equal to updatedWDABundleId capability', async function () { + wda = new WebDriverAgent({ + ...fakeConstructorArgs, + updatedWDABundleId: 'com.example.WebDriverAgent', + }); + wdaStub = sinon.stub(wda, 'getStatus'); + wdaStubUninstall = sinon.stub(wda as any, 'uninstall'); + + wdaStub.callsFake(function () { + return { + build: { + time: 'Jun 24 2018 17:08:21', + productBundleIdentifier: 'com.example.WebDriverAgent', + }, + }; + }); + + wdaStubUninstall.callsFake(_.noop); + + 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 have xctrunner automatically with default bundle id', function () { - const args = Object.assign({}, fakeConstructorArgs); - const agent = new WebDriverAgent(args); - expect(agent.bundleIdForXctest).to.equal('com.facebook.WebDriverAgentRunner.xctrunner'); + it('should call uninstall if current revision differs from the bundled one', async function () { + wdaStub.callsFake(function () { + return {build: {upgradedAt: '1'}}; + }); + getTimestampStub.callsFake(() => '2'); + wdaStubUninstall.callsFake(_.noop); + + await wda.setupCaching(); + expect(wdaStub.calledOnce).to.be.true; + expect(wdaStubUninstall.calledOnce).to.be.true; }); - it('should allow an empty string as xctrunner suffix', function () { - const args = Object.assign({}, fakeConstructorArgs); - args.updatedWDABundleId = 'io.appium.wda'; - args.updatedWDABundleIdSuffix = ''; - const agent = new WebDriverAgent(args); - expect(agent.bundleIdForXctest).to.equal('io.appium.wda'); + it('should not call uninstall if current revision is the same as the bundled one', async function () { + wdaStub.callsFake(function () { + return {build: {upgradedAt: '1'}}; + }); + getTimestampStub.callsFake(() => '1'); + wdaStubUninstall.callsFake(_.noop); + + await wda.setupCaching(); + expect(wdaStub.calledOnce).to.be.true; + expect(wdaStubUninstall.notCalled).to.be.true; }); - it('should allow an empty string as xctrunner suffix with default bundle id', function () { - const args = Object.assign({}, fakeConstructorArgs); - args.updatedWDABundleIdSuffix = ''; - const agent = new WebDriverAgent(args); - expect(agent.bundleIdForXctest).to.equal('com.facebook.WebDriverAgentRunner'); + it('should not call uninstall if current revision cannot be retrieved from WDA status', async function () { + wdaStub.callsFake(function () { + return {build: {}}; + }); + getTimestampStub.callsFake(() => '1'); + wdaStubUninstall.callsFake(_.noop); + + await wda.setupCaching(); + expect(wdaStub.calledOnce).to.be.true; + expect(wdaStubUninstall.notCalled).to.be.true; }); - it('should have an arbitrary xctrunner suffix', function () { - const args = Object.assign({}, fakeConstructorArgs); - args.updatedWDABundleId = 'io.appium.wda'; - args.updatedWDABundleIdSuffix = '.customsuffix'; - const agent = new WebDriverAgent(args); - expect(agent.bundleIdForXctest).to.equal('io.appium.wda.customsuffix'); + it('should not call uninstall if current revision cannot be retrieved from the file system', async function () { + wdaStub.callsFake(function () { + return {build: {upgradedAt: '1'}}; + }); + getTimestampStub.callsFake(() => null); + wdaStubUninstall.callsFake(_.noop); + + 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']); + }); + }); }); -}); + describe('usePreinstalledWDA related functions', function () { + describe('bundleIdForXctest', function () { + it('should have xctrunner automatically', function () { + const args = Object.assign({}, fakeConstructorArgs); + args.updatedWDABundleId = 'io.appium.wda'; + const agent = new WebDriverAgent(args); + expect(agent.bundleIdForXctest).to.equal('io.appium.wda.xctrunner'); + }); + + it('should have xctrunner automatically with default bundle id', function () { + const args = Object.assign({}, fakeConstructorArgs); + const agent = new WebDriverAgent(args); + expect(agent.bundleIdForXctest).to.equal('com.facebook.WebDriverAgentRunner.xctrunner'); + }); + + it('should allow an empty string as xctrunner suffix', function () { + const args = Object.assign({}, fakeConstructorArgs); + args.updatedWDABundleId = 'io.appium.wda'; + args.updatedWDABundleIdSuffix = ''; + const agent = new WebDriverAgent(args); + expect(agent.bundleIdForXctest).to.equal('io.appium.wda'); + }); + + it('should allow an empty string as xctrunner suffix with default bundle id', function () { + const args = Object.assign({}, fakeConstructorArgs); + args.updatedWDABundleIdSuffix = ''; + const agent = new WebDriverAgent(args); + expect(agent.bundleIdForXctest).to.equal('com.facebook.WebDriverAgentRunner'); + }); + + it('should have an arbitrary xctrunner suffix', function () { + const args = Object.assign({}, fakeConstructorArgs); + args.updatedWDABundleId = 'io.appium.wda'; + args.updatedWDABundleIdSuffix = '.customsuffix'; + const agent = new WebDriverAgent(args); + expect(agent.bundleIdForXctest).to.equal('io.appium.wda.customsuffix'); + }); + }); + }); });