diff --git a/lib/check-dependencies.js b/lib/check-dependencies.js index ce145f5a1..21c68de60 100644 --- a/lib/check-dependencies.js +++ b/lib/check-dependencies.js @@ -3,6 +3,7 @@ import _ from 'lodash'; import { exec } from 'teen_process'; import path from 'path'; import XcodeBuild from './xcodebuild'; +import xcode from 'appium-xcode'; import { WDA_SCHEME, SDK_SIMULATOR, WDA_RUNNER_APP } from './constants'; @@ -22,17 +23,25 @@ async function buildWDASim () { } // eslint-disable-next-line require-await -async function checkForDependencies () { +export async function checkForDependencies () { log.debug('Dependencies are up to date'); return false; } -async function bundleWDASim (xcodebuild) { +/** + * + * @param {XcodeBuild} xcodebuild + * @returns {Promise} + */ +export async function bundleWDASim (xcodebuild) { if (xcodebuild && !_.isFunction(xcodebuild.retrieveDerivedDataPath)) { - xcodebuild = new XcodeBuild('', {}); + xcodebuild = new XcodeBuild(/** @type {import('appium-xcode').XcodeVersion} */ (await xcode.getVersion(true)), {}); } 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); if (await fs.exists(wdaBundlePath)) { return wdaBundlePath; @@ -40,5 +49,3 @@ async function bundleWDASim (xcodebuild) { await buildWDASim(); return wdaBundlePath; } - -export { checkForDependencies, bundleWDASim }; diff --git a/lib/webdriveragent.js b/lib/webdriveragent.js index a4b316637..c69666f4e 100644 --- a/lib/webdriveragent.js +++ b/lib/webdriveragent.js @@ -27,7 +27,19 @@ const WDA_CF_BUNDLE_NAME = 'WebDriverAgentRunner-Runner'; const SHARED_RESOURCES_GUARD = new AsyncLock(); const RECENT_MODULE_VERSION_ITEM_NAME = 'recentWdaModuleVersion'; -class WebDriverAgent { +export class WebDriverAgent { + /** @type {string} */ + bootstrapPath; + + /** @type {string} */ + agentPath; + + /** + * @param {import('appium-xcode').XcodeVersion} xcodeVersion + * // TODO: make args typed + * @param {import('@appium/types').StringRecord} [args={}] + * @param {import('@appium/types').AppiumLogger?} [log=null] + */ constructor (xcodeVersion, args = {}, log = null) { this.xcodeVersion = xcodeVersion; @@ -123,6 +135,10 @@ class WebDriverAgent { return `${this.updatedWDABundleId ? this.updatedWDABundleId : WDA_RUNNER_BUNDLE_ID}${this.updatedWDABundleIdSuffix}`; } + /** + * @param {string} [bootstrapPath] + * @param {string} [agentPath] + */ setWDAPaths (bootstrapPath, agentPath) { // allow the user to specify a place for WDA. This is undocumented and // only here for the purposes of testing development of WDA @@ -134,8 +150,11 @@ class WebDriverAgent { this.log.info(`Using WDA agent: '${this.agentPath}'`); } + /** + * @returns {Promise} + */ async cleanupObsoleteProcesses () { - const obsoletePids = await getPIDsListeningOnPort(this.url.port, + const obsoletePids = await getPIDsListeningOnPort(/** @type {string} */ (this.url.port), (cmdLine) => cmdLine.includes('/WebDriverAgentRunner') && !cmdLine.toLowerCase().includes(this.device.udid.toLowerCase())); @@ -164,6 +183,9 @@ class WebDriverAgent { return !!(await this.getStatus()); } + /** + * @returns {string} + */ get basePath () { if (this.url.path === '/') { return ''; @@ -243,6 +265,8 @@ class WebDriverAgent { * Uninstall WDAs from the test device. * Over Xcode 11, multiple WDA can be in the device since Xcode 11 generates different WDA. * Appium does not expect multiple WDAs are running on a device. + * + * @returns {Promise} */ async uninstall () { try { @@ -476,6 +500,9 @@ class WebDriverAgent { return await this.xcodebuild.start(); } + /** + * @returns {Promise} + */ async startWithIDB () { this.log.info('Will launch WDA with idb instead of xcodebuild since the corresponding flag is enabled'); const {wdaBundleId, testBundleId} = await this.prepareWDA(); @@ -490,6 +517,11 @@ class WebDriverAgent { return await this.idb.runXCUITest(wdaBundleId, wdaBundleId, testBundleId, {env}); } + /** + * + * @param {string} wdaBundlePath + * @returns {Promise} + */ async parseBundleId (wdaBundlePath) { const infoPlistPath = path.join(wdaBundlePath, 'Info.plist'); const infoPlist = await plist.parsePlist(await fs.readFile(infoPlistPath)); @@ -499,6 +531,9 @@ class WebDriverAgent { return infoPlist.CFBundleIdentifier; } + /** + * @returns {Promise<{wdaBundleId: string, testBundleId: string, wdaBundlePath: string}>} + */ async prepareWDA () { const wdaBundlePath = this.wdaBundlePath || await this.fetchWDABundle(); const wdaBundleId = await this.parseBundleId(wdaBundlePath); @@ -509,9 +544,12 @@ class WebDriverAgent { return {wdaBundleId, testBundleId, wdaBundlePath}; } + /** + * @returns {Promise} + */ async fetchWDABundle () { if (!this.derivedDataPath) { - return await bundleWDASim(this.xcodebuild); + return await bundleWDASim(/** @type {XcodeBuild} */ (this.xcodebuild)); } const wdaBundlePaths = await fs.glob(`${this.derivedDataPath}/**/*${WDA_RUNNER_APP}/`, { absolute: true, @@ -522,14 +560,21 @@ class WebDriverAgent { return wdaBundlePaths[0]; } + /** + * @returns {Promise} + */ async isSourceFresh () { const existsPromises = [ 'Resources', `Resources${path.sep}WebDriverAgent.bundle`, - ].map((subPath) => fs.exists(path.resolve(this.bootstrapPath, subPath))); + ].map((subPath) => fs.exists(path.resolve(/** @type {String} */ (this.bootstrapPath), subPath))); return (await B.all(existsPromises)).some((v) => v === false); } + /** + * @param {string} sessionId + * @returns {void} + */ setupProxies (sessionId) { const proxyOpts = { log: this.log, @@ -547,6 +592,9 @@ class WebDriverAgent { this.noSessionProxy = new NoSessionProxy(proxyOpts); } + /** + * @returns {Promise} + */ async quit () { if (this.usePreinstalledWDA) { this.log.info('Stopping the XCTest session'); @@ -582,6 +630,9 @@ class WebDriverAgent { } } + /** + * @returns {import('url').UrlWithStringQuery} + */ get url () { if (!this._url) { if (this.webDriverAgentUrl) { @@ -595,30 +646,44 @@ class WebDriverAgent { return this._url; } + /** + * @param {string} _url + * @returns {void} + */ set url (_url) { this._url = url.parse(_url); } + /** + * @returns {boolean} + */ get fullyStarted () { return this.started; } + /** + * @param {boolean} started + * @returns {void}s + */ set fullyStarted (started) { this.started = started ?? false; } + /** + * @returns {Promise} + */ async retrieveDerivedDataPath () { if (this.canSkipXcodebuild) { return; } - // @ts-ignore xcodebuild should be set - return await this.xcodebuild.retrieveDerivedDataPath(); + return await /** @type {XcodeBuild} */ (this.xcodebuild).retrieveDerivedDataPath(); } /** * Reuse running WDA if it has the same bundle id with updatedWDABundleId. * Or reuse it if it has the default id without updatedWDABundleId. * Uninstall it if the method faces an exception for the above situation. + * @returns {Promise} */ async setupCaching () { const status = await this.getStatus(); @@ -660,6 +725,7 @@ class WebDriverAgent { /** * Quit and uninstall running WDA. + * @returns {Promise} */ async quitAndUninstall () { await this.quit(); @@ -668,4 +734,3 @@ class WebDriverAgent { } export default WebDriverAgent; -export { WebDriverAgent }; diff --git a/lib/xcodebuild.js b/lib/xcodebuild.js index bdd484db2..2862cb6a0 100644 --- a/lib/xcodebuild.js +++ b/lib/xcodebuild.js @@ -33,15 +33,16 @@ const LIB_SCHEME_TV = 'WebDriverAgentLib_tvOS'; const xcodeLog = logger.getLogger('Xcode'); -class XcodeBuild { +export class XcodeBuild { /** @type {SubProcess} */ xcodebuild; /** - * @param {string} xcodeVersion + * @param {import('appium-xcode').XcodeVersion} xcodeVersion * @param {any} device - * @param {any} args - * @param {import('@appium/types').AppiumLogger?} log + * // TODO: make args typed + * @param {import('@appium/types').StringRecord} [args={}] + * @param {import('@appium/types').AppiumLogger?} [log=null] */ constructor (xcodeVersion, device, args = {}, log = null) { this.xcodeVersion = xcodeVersion; @@ -92,6 +93,11 @@ class XcodeBuild { this._didProcessExit = false; } + /** + * + * @param {any} noSessionProxy + * @returns {Promise} + */ async init (noSessionProxy) { this.noSessionProxy = noSessionProxy; @@ -120,6 +126,9 @@ class XcodeBuild { } } + /** + * @returns {Promise} + */ async retrieveDerivedDataPath () { if (this.derivedDataPath) { return this.derivedDataPath; @@ -154,6 +163,9 @@ class XcodeBuild { return await this._derivedDataPathPromise; } + /** + * @returns {Promise} + */ async reset () { // if necessary, reset the bundleId to original value if (this.realDevice && this.updatedWDABundleId) { @@ -161,6 +173,9 @@ class XcodeBuild { } } + /** + * @returns {Promise} + */ async prebuild () { // first do a build phase this.log.debug('Pre-building WDA before launching test'); @@ -173,6 +188,9 @@ class XcodeBuild { } } + /** + * @returns {Promise} + */ async cleanProject () { const libScheme = isTvOS(this.platformName) ? LIB_SCHEME_TV : LIB_SCHEME_IOS; const runnerScheme = isTvOS(this.platformName) ? RUNNER_SCHEME_TV : RUNNER_SCHEME_IOS; @@ -187,18 +205,24 @@ class XcodeBuild { } } + /** + * + * @param {boolean} [buildOnly=false] + * @returns {{cmd: string, args: string[]}} + */ getCommand (buildOnly = false) { - let cmd = 'xcodebuild'; - let args; + const cmd = 'xcodebuild'; + /** @type {string[]} */ + const args = []; // figure out the targets for xcodebuild const [buildCmd, testCmd] = this.useSimpleBuildTest ? ['build', 'test'] : ['build-for-testing', 'test-without-building']; if (buildOnly) { - args = [buildCmd]; + args.push(buildCmd); } else if (this.usePrebuiltWDA || this.useXctestrunFile) { - args = [testCmd]; + args.push(testCmd); } else { - args = [buildCmd, testCmd]; + args.push(buildCmd, testCmd); } if (this.allowProvisioningDeviceRegistration) { @@ -260,6 +284,10 @@ class XcodeBuild { return {cmd, args}; } + /** + * @param {boolean} [buildOnly=false] + * @returns {Promise} + */ async createSubProcess (buildOnly = false) { if (!this.useXctestrunFile && this.realDevice) { if (this.keychainPath && this.keychainPassword) { @@ -321,6 +349,11 @@ class XcodeBuild { return xcodebuild; } + + /** + * @param {boolean} [buildOnly=false] + * @returns {Promise} + */ async start (buildOnly = false) { this.xcodebuild = await this.createSubProcess(buildOnly); @@ -356,8 +389,7 @@ class XcodeBuild { const timer = new timing.Timer().start(); await this.xcodebuild.start(true); if (!buildOnly) { - let status = await this.waitForStart(timer); - resolve(status); + resolve(/** @type {import('@appium/types').StringRecord} */ (await this.waitForStart(timer))); } } catch (err) { let msg = `Unable to start WebDriverAgent: ${err}`; @@ -368,6 +400,11 @@ class XcodeBuild { }); } + /** + * + * @param {any} timer + * @returns {Promise} + */ async waitForStart (timer) { // try to connect once every 0.5 seconds, until `launchTimeout` is up this.log.debug(`Waiting up to ${this.launchTimeout}ms for WebDriverAgent to start`); @@ -412,10 +449,12 @@ class XcodeBuild { return currentStatus; } + /** + * @returns {Promise} + */ async quit () { await killProcess('xcodebuild', this.xcodebuild); } } -export { XcodeBuild }; export default XcodeBuild;