UNPKG

appium-webdriveragent

Version:
674 lines 29.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebDriverAgent = void 0; const asyncbox_1 = require("asyncbox"); const lodash_1 = __importDefault(require("lodash")); const path_1 = __importDefault(require("path")); const url_1 = __importDefault(require("url")); const bluebird_1 = __importDefault(require("bluebird")); const base_driver_1 = require("@appium/base-driver"); const support_1 = require("@appium/support"); const logger_1 = __importDefault(require("./logger")); const no_session_proxy_1 = require("./no-session-proxy"); const utils_1 = require("./utils"); const xcodebuild_1 = require("./xcodebuild"); const async_lock_1 = __importDefault(require("async-lock")); const teen_process_1 = require("teen_process"); const check_dependencies_1 = require("./check-dependencies"); const constants_1 = require("./constants"); const appium_ios_device_1 = require("appium-ios-device"); const strongbox_1 = require("@appium/strongbox"); const WDA_LAUNCH_TIMEOUT = 60 * 1000; const WDA_AGENT_PORT = 8100; const WDA_CF_BUNDLE_NAME = 'WebDriverAgentRunner-Runner'; const SHARED_RESOURCES_GUARD = new async_lock_1.default(); const RECENT_MODULE_VERSION_ITEM_NAME = 'recentWdaModuleVersion'; 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; this.args = lodash_1.default.clone(args); this.log = log ?? logger_1.default; this.device = args.device; this.platformVersion = args.platformVersion; this.platformName = args.platformName; this.iosSdkVersion = args.iosSdkVersion; this.host = args.host; this.isRealDevice = !!args.realDevice; this.idb = (args.device || {}).idb; this.wdaBundlePath = args.wdaBundlePath; this.setWDAPaths(args.bootstrapPath, args.agentPath); this.wdaLocalPort = args.wdaLocalPort; this.wdaRemotePort = ((this.isRealDevice ? args.wdaRemotePort : null) ?? args.wdaLocalPort) || WDA_AGENT_PORT; this.wdaBaseUrl = args.wdaBaseUrl || constants_1.WDA_BASE_URL; this.prebuildWDA = args.prebuildWDA; // this.args.webDriverAgentUrl guiarantees the capabilities acually // gave 'appium:webDriverAgentUrl' but 'this.webDriverAgentUrl' // could be used for caching WDA with xcodebuild. this.webDriverAgentUrl = args.webDriverAgentUrl; this.started = false; this.wdaConnectionTimeout = args.wdaConnectionTimeout; this.useXctestrunFile = args.useXctestrunFile; this.usePrebuiltWDA = args.usePrebuiltWDA; this.derivedDataPath = args.derivedDataPath; this.mjpegServerPort = args.mjpegServerPort; this.updatedWDABundleId = args.updatedWDABundleId; this.wdaLaunchTimeout = args.wdaLaunchTimeout || WDA_LAUNCH_TIMEOUT; this.usePreinstalledWDA = args.usePreinstalledWDA; this.xctestApiClient = null; this.updatedWDABundleIdSuffix = args.updatedWDABundleIdSuffix ?? constants_1.DEFAULT_TEST_BUNDLE_SUFFIX; this.xcodebuild = this.canSkipXcodebuild ? null : new xcodebuild_1.XcodeBuild(this.xcodeVersion, 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, 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 {boolean} Whether the session needs/has xcodebuild. */ get canSkipXcodebuild() { // Use this.args.webDriverAgentUrl to guarantee // the capabilities set gave the `appium:webDriverAgentUrl`. return this.usePreinstalledWDA || this.args.webDriverAgentUrl; } /** * Return bundle id for WebDriverAgent to launch the WDA. * The primary usage is with 'this.usePreinstalledWDA'. * It adds `.xctrunner` as suffix by default but 'this.updatedWDABundleIdSuffix' * lets skip it. * * @returns {string} Bundle ID for Xctest. */ get bundleIdForXctest() { return `${this.updatedWDABundleId ? this.updatedWDABundleId : constants_1.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 this.bootstrapPath = bootstrapPath || utils_1.BOOTSTRAP_PATH; this.log.info(`Using WDA path: '${this.bootstrapPath}'`); // for backward compatibility we need to be able to specify agentPath too this.agentPath = agentPath || path_1.default.resolve(this.bootstrapPath, 'WebDriverAgent.xcodeproj'); this.log.info(`Using WDA agent: '${this.agentPath}'`); } /** * @returns {Promise<void>} */ async cleanupObsoleteProcesses() { const obsoletePids = await (0, utils_1.getPIDsListeningOnPort)(/** @type {string} */ (this.url.port), (cmdLine) => cmdLine.includes('/WebDriverAgentRunner') && !cmdLine.toLowerCase().includes(this.device.udid.toLowerCase())); if (lodash_1.default.isEmpty(obsoletePids)) { 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`); try { await (0, teen_process_1.exec)('kill', obsoletePids); } catch (e) { this.log.warn(`Failed to kill obsolete cached process${obsoletePids.length === 1 ? '' : 'es'} '${obsoletePids}'. ` + `Original error: ${e.message}`); } } /** * Return boolean if WDA is running or not * @return {Promise<boolean>} True if WDA is running * @throws {Error} If there was invalid response code or body */ async isRunning() { return !!(await this.getStatus()); } /** * @returns {string} */ get basePath() { if (this.url.path === '/') { return ''; } return this.url.path || ''; } /** * Return current running WDA's status like below * { * "state": "success", * "os": { * "name": "iOS", * "version": "11.4", * "sdkVersion": "11.3" * }, * "ios": { * "simulatorVersion": "11.4", * "ip": "172.254.99.34" * }, * "build": { * "time": "Jun 24 2018 17:08:21", * "productBundleIdentifier": "com.facebook.WebDriverAgentRunner" * } * } * * @param {number} [timeoutMs=0] If the given timeoutMs is zero or negative number, * this function will return the response of `/status` immediately. If the given timeoutMs, * this function will try to get the response of `/status` up to the timeoutMs. * @return {Promise<import('@appium/types').StringRecord|null>} State Object * @throws {Error} If there was an error within timeoutMs timeout. * No error is raised if zero or negative number for the timeoutMs. */ async getStatus(timeoutMs = 0) { const noSessionProxy = new no_session_proxy_1.NoSessionProxy({ server: this.url.hostname, port: this.url.port, base: this.basePath, timeout: 3000, }); const sendGetStatus = async () => await /** @type import('@appium/types').StringRecord */ (noSessionProxy.command('/status', 'GET')); if (lodash_1.default.isNil(timeoutMs) || timeoutMs <= 0) { try { return await sendGetStatus(); } catch (err) { this.log.debug(`WDA is not listening at '${this.url.href}'. Original error:: ${err.message}`); return null; } } let lastError = null; let status = null; try { await (0, asyncbox_1.waitForCondition)(async () => { try { status = await sendGetStatus(); return true; } catch (err) { lastError = err; } return false; }, { waitMs: timeoutMs, intervalMs: 300, }); } catch (err) { 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; } /** * 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<void>} */ async uninstall() { try { const bundleIds = await this.device.getUserInstalledBundleIdsByBundleName(WDA_CF_BUNDLE_NAME); if (lodash_1.default.isEmpty(bundleIds)) { this.log.debug('No WDAs on the device.'); return; } this.log.debug(`Uninstalling WDAs: '${bundleIds}'`); for (const bundleId of bundleIds) { await this.device.removeApp(bundleId); } } catch (e) { this.log.debug(e); this.log.warn(`WebDriverAgent uninstall failed. Perhaps, it is already uninstalled? ` + `Original error: ${e.message}`); } } async _cleanupProjectIfFresh() { if (this.canSkipXcodebuild) { return; } const packageInfo = JSON.parse(await support_1.fs.readFile(path_1.default.join(utils_1.BOOTSTRAP_PATH, 'package.json'), 'utf8')); const box = (0, strongbox_1.strongbox)(packageInfo.name); let boxItem = box.getItem(RECENT_MODULE_VERSION_ITEM_NAME); if (!boxItem) { const timestampPath = path_1.default.resolve(process.env.HOME ?? '', constants_1.WDA_UPGRADE_TIMESTAMP_PATH); if (await support_1.fs.exists(timestampPath)) { // TODO: It is probably a bit ugly to hardcode the recent version string, // TODO: hovewer it should do the job as a temporary transition trick // TODO: to switch from a hardcoded file path to the strongbox usage. try { boxItem = await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, '5.0.0'); } catch (e) { this.log.warn(`The actual module version cannot be persisted: ${e.message}`); return; } } else { 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) { this.log.warn(`The actual module version cannot be persisted: ${e.message}`); } return; } } let recentModuleVersion = await boxItem.read(); try { recentModuleVersion = support_1.util.coerceVersion(recentModuleVersion, true); } catch (e) { 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`); await boxItem.write(packageInfo.version); return; } if (support_1.util.compareVersions(recentModuleVersion, '>=', packageInfo.version)) { this.log.info(`WebDriverAgent does not need a cleanup. The project sources are up to date ` + `(${recentModuleVersion} >= ${packageInfo.version})`); return; } this.log.info(`Cleaning up the WebDriverAgent project after the module upgrade has happened ` + `(${recentModuleVersion} < ${packageInfo.version})`); try { // @ts-ignore xcodebuild should be set await this.xcodebuild.cleanProject(); await boxItem.write(packageInfo.version); } catch (e) { this.log.warn(`Cannot perform WebDriverAgent project cleanup. Original error: ${e.message}`); } } /** * @typedef {Object} LaunchWdaViaDeviceCtlOptions * @property {Record<string, string|number>} [env] environment variables for the launching WDA process */ /** * Launch WDA with preinstalled package with 'xcrun devicectl device process launch'. * The WDA package must be prepared properly like published via * https://github.com/appium/WebDriverAgent/releases * with proper sign for this case. * * When we implement launching XCTest service via appium-ios-device, * this implementation can be replaced with it. * * @param {LaunchWdaViaDeviceCtlOptions} [opts={}] launching WDA with devicectl command options. * @return {Promise<void>} */ async _launchViaDevicectl(opts = {}) { const { env } = opts; await this.device.devicectl.launchApp(this.bundleIdForXctest, { env, terminateExisting: true }); } /** * Launch WDA with preinstalled package without xcodebuild. * @param {string} sessionId Launch WDA and establish the session with this sessionId * @return {Promise<import('@appium/types').StringRecord|null>} State Object * @throws {Error} If there was an error within timeoutMs timeout. * No error is raised if zero or negative number for the timeoutMs. */ async launchWithPreinstalledWDA(sessionId) { const xctestEnv = { USE_PORT: this.wdaLocalPort || WDA_AGENT_PORT, WDA_PRODUCT_BUNDLE_IDENTIFIER: this.bundleIdForXctest }; if (this.mjpegServerPort) { xctestEnv.MJPEG_SERVER_PORT = this.mjpegServerPort; } this.log.info('Launching WebDriverAgent on the device without xcodebuild'); if (this.isRealDevice) { // Current method to launch WDA process can be done via 'xcrun devicectl', // but it has limitation about the WDA preinstalled package. // https://github.com/appium/appium/issues/19206#issuecomment-2014182674 if (support_1.util.compareVersions(this.platformVersion, '>=', '17.0')) { await this._launchViaDevicectl({ env: xctestEnv }); } else { this.xctestApiClient = new appium_ios_device_1.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, ], env: xctestEnv, }); } this.setupProxies(sessionId); let status; try { status = await this.getStatus(this.wdaLaunchTimeout); } 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.`); } this.started = true; return status; } /** * Return current running WDA's status like below after launching WDA * { * "state": "success", * "os": { * "name": "iOS", * "version": "11.4", * "sdkVersion": "11.3" * }, * "ios": { * "simulatorVersion": "11.4", * "ip": "172.254.99.34" * }, * "build": { * "time": "Jun 24 2018 17:08:21", * "productBundleIdentifier": "com.facebook.WebDriverAgentRunner" * } * } * * @param {string} sessionId Launch WDA and establish the session with this sessionId * @return {Promise<any?>} State Object * @throws {Error} If there was invalid response code or body */ async launch(sessionId) { if (this.webDriverAgentUrl) { this.log.info(`Using provided WebdriverAgent at '${this.webDriverAgentUrl}'`); this.url = this.webDriverAgentUrl; this.setupProxies(sessionId); return await this.getStatus(); } if (this.usePreinstalledWDA) { return await this.launchWithPreinstalledWDA(sessionId); } this.log.info('Launching WebDriverAgent on the device'); this.setupProxies(sessionId); if (!this.useXctestrunFile && !await support_1.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 // It depends on user side if (this.idb || this.useXctestrunFile || this.usePrebuiltWDA) { this.log.info('Skipped WDA project cleanup according to the provided capabilities'); } else { const synchronizationKey = path_1.default.normalize(this.bootstrapPath); await SHARED_RESOURCES_GUARD.acquire(synchronizationKey, async () => await this._cleanupProjectIfFresh()); } // We need to provide WDA local port, because it might be occupied await (0, utils_1.resetTestProcesses)(this.device.udid, !this.isRealDevice); if (this.idb) { return await this.startWithIDB(); } // @ts-ignore xcodebuild should be set await this.xcodebuild.init(this.noSessionProxy); // Start the xcodebuild process if (this.prebuildWDA) { // @ts-ignore xcodebuild should be set await this.xcodebuild.prebuild(); } // @ts-ignore xcodebuild should be set return await this.xcodebuild.start(); } /** * @returns {Promise<void>} */ 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(); const env = { USE_PORT: this.wdaRemotePort, WDA_PRODUCT_BUNDLE_IDENTIFIER: this.bundleIdForXctest, }; if (this.mjpegServerPort) { env.MJPEG_SERVER_PORT = this.mjpegServerPort; } return await this.idb.runXCUITest(wdaBundleId, wdaBundleId, testBundleId, { env }); } /** * * @param {string} wdaBundlePath * @returns {Promise<string>} */ async parseBundleId(wdaBundlePath) { const infoPlistPath = path_1.default.join(wdaBundlePath, 'Info.plist'); const infoPlist = await support_1.plist.parsePlist(await support_1.fs.readFile(infoPlistPath)); if (!infoPlist.CFBundleIdentifier) { throw new Error(`Could not find bundle id in '${infoPlistPath}'`); } 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); if (!await this.device.isAppInstalled(wdaBundleId)) { await this.device.installApp(wdaBundlePath); } const testBundleId = await this.idb.installXCTestBundle(path_1.default.join(wdaBundlePath, 'PlugIns', 'WebDriverAgentRunner.xctest')); return { wdaBundleId, testBundleId, wdaBundlePath }; } /** * @returns {Promise<string>} */ async fetchWDABundle() { if (!this.derivedDataPath) { return await (0, check_dependencies_1.bundleWDASim)(/** @type {XcodeBuild} */ (this.xcodebuild)); } const wdaBundlePaths = await support_1.fs.glob(`${this.derivedDataPath}/**/*${constants_1.WDA_RUNNER_APP}/`, { absolute: true, }); if (lodash_1.default.isEmpty(wdaBundlePaths)) { throw new Error(`Could not find the WDA bundle in '${this.derivedDataPath}'`); } return wdaBundlePaths[0]; } /** * @returns {Promise<boolean>} */ async isSourceFresh() { const existsPromises = [ 'Resources', `Resources${path_1.default.sep}WebDriverAgent.bundle`, ].map((subPath) => support_1.fs.exists(path_1.default.resolve(/** @type {String} */ (this.bootstrapPath), subPath))); return (await bluebird_1.default.all(existsPromises)).some((v) => v === false); } /** * @param {string} sessionId * @returns {void} */ setupProxies(sessionId) { const proxyOpts = { log: this.log, server: this.url.hostname ?? undefined, port: parseInt(this.url.port ?? '', 10) || undefined, base: this.basePath, timeout: this.wdaConnectionTimeout, keepAlive: true, scheme: this.url.protocol ? this.url.protocol.replace(':', '') : 'http', }; if (this.args.reqBasePath) { proxyOpts.reqBasePath = this.args.reqBasePath; } this.jwproxy = new base_driver_1.JWProxy(proxyOpts); this.jwproxy.sessionId = sessionId; this.proxyReqRes = this.jwproxy.proxyReqRes.bind(this.jwproxy); this.noSessionProxy = new no_session_proxy_1.NoSessionProxy(proxyOpts); } /** * @returns {Promise<void>} */ async quit() { if (this.usePreinstalledWDA) { this.log.info('Stopping the XCTest session'); if (this.xctestApiClient) { this.xctestApiClient.stop(); this.xctestApiClient = null; } else { try { await this.device.simctl.terminateApp(this.bundleIdForXctest); } catch (e) { this.log.warn(e.message); } } } else if (!this.args.webDriverAgentUrl) { this.log.info('Shutting down sub-processes'); await this.xcodebuild?.quit(); await this.xcodebuild?.reset(); } else { this.log.debug('Do not stop xcodebuild nor XCTest session ' + 'since the WDA session is managed by outside this driver.'); } if (this.jwproxy) { this.jwproxy.sessionId = null; } this.started = false; if (!this.args.webDriverAgentUrl) { // if we populated the url ourselves (during `setupCaching` call, for instance) // then clean that up. If the url was supplied, we want to keep it this.webDriverAgentUrl = null; } } /** * @returns {import('url').UrlWithStringQuery} */ get url() { if (!this._url) { if (this.webDriverAgentUrl) { this._url = url_1.default.parse(this.webDriverAgentUrl); } else { const port = this.wdaLocalPort || WDA_AGENT_PORT; const { protocol, hostname } = url_1.default.parse(this.wdaBaseUrl || constants_1.WDA_BASE_URL); this._url = url_1.default.parse(`${protocol}//${hostname}:${port}`); } } return this._url; } /** * @param {string} _url * @returns {void} */ set url(_url) { this._url = url_1.default.parse(_url); } /** * @returns {boolean} */ get fullyStarted() { return this.started; } /** * @param {boolean} started * @returns {void}s */ set fullyStarted(started) { this.started = started ?? false; } /** * @returns {Promise<string|undefined>} */ async retrieveDerivedDataPath() { if (this.canSkipXcodebuild) { return; } 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<void>} */ async setupCaching() { const status = await this.getStatus(); if (!status || !status.build) { this.log.debug('WDA is currently not running. There is nothing to cache'); return; } const { productBundleIdentifier, upgradedAt, } = status.build; // for real device if (support_1.util.hasValue(productBundleIdentifier) && support_1.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 (support_1.util.hasValue(productBundleIdentifier) && !support_1.util.hasValue(this.updatedWDABundleId) && constants_1.WDA_RUNNER_BUNDLE_ID !== productBundleIdentifier) { this.log.info(`Will uninstall running WDA since its bundle id is not equal to the default value ${constants_1.WDA_RUNNER_BUNDLE_ID}`); return await this.uninstall(); } const actualUpgradeTimestamp = await (0, utils_1.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 && lodash_1.default.toLower(`${actualUpgradeTimestamp}`) !== lodash_1.default.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 = support_1.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.webDriverAgentUrl = this.url.href; } /** * Quit and uninstall running WDA. * @returns {Promise<void>} */ async quitAndUninstall() { await this.quit(); await this.uninstall(); } } exports.WebDriverAgent = WebDriverAgent; exports.default = WebDriverAgent; //# sourceMappingURL=webdriveragent.js.map