UNPKG

appium-android-driver

Version:

Android UiAutomator and Chrome support for Appium

411 lines 17.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.getCurrentContext = getCurrentContext; exports.getContexts = getContexts; exports.setContext = setContext; exports.mobileGetContexts = mobileGetContexts; exports.assignContexts = assignContexts; exports.switchContext = switchContext; exports.defaultContextName = defaultContextName; exports.defaultWebviewName = defaultWebviewName; exports.isWebContext = isWebContext; exports.getWindowHandle = getWindowHandle; exports.getWindowHandles = getWindowHandles; exports.setWindow = setWindow; exports.startChromedriverProxy = startChromedriverProxy; exports.suspendChromedriverProxy = suspendChromedriverProxy; exports.onChromedriverStop = onChromedriverStop; exports.stopChromedriverProxies = stopChromedriverProxies; exports.isChromedriverContext = isChromedriverContext; exports.notifyBiDiContextChange = notifyBiDiContextChange; exports.startChromeSession = startChromeSession; const support_1 = require("@appium/support"); const appium_chromedriver_1 = require("appium-chromedriver"); const driver_1 = require("appium/driver"); const lodash_1 = __importDefault(require("lodash")); const helpers_1 = require("./helpers"); const app_management_1 = require("../app-management"); const constants_1 = require("../bidi/constants"); const models_1 = require("../bidi/models"); // https://github.com/appium/appium/issues/20710 const DEFAULT_NATIVE_WINDOW_HANDLE = '1'; /** * @this {AndroidDriver} * @returns {Promise<string>} */ async function getCurrentContext() { // if the current context is `null`, indicating no context // explicitly set, it is the default context return this.curContext || this.defaultContextName(); } /** * @this {AndroidDriver} * @returns {Promise<string[]>} */ async function getContexts() { const webviewsMapping = await helpers_1.getWebViewsMapping.bind(this)(this.opts); return this.assignContexts(webviewsMapping); } /** * @this {AndroidDriver} * @param {string?} name * @returns {Promise<void>} */ async function setContext(name) { let newContext = name; if (!support_1.util.hasValue(newContext)) { newContext = this.defaultContextName(); } else if (newContext === helpers_1.WEBVIEW_WIN) { // handle setContext "WEBVIEW" newContext = this.defaultWebviewName(); } // if we're already in the context we want, do nothing if (newContext === this.curContext) { return; } const webviewsMapping = await helpers_1.getWebViewsMapping.bind(this)(this.opts); const contexts = this.assignContexts(webviewsMapping); // if the context we want doesn't exist, fail if (!lodash_1.default.includes(contexts, newContext)) { throw new driver_1.errors.NoSuchContextError(); } await this.switchContext(newContext, webviewsMapping); this.curContext = newContext; await this.notifyBiDiContextChange(); } /** * @this {AndroidDriver} * @param {number} [waitForWebviewMs] * @returns {Promise<import('../types').WebviewsMapping[]>} */ async function mobileGetContexts(waitForWebviewMs) { const _opts = { androidDeviceSocket: this.opts.androidDeviceSocket, ensureWebviewsHavePages: true, webviewDevtoolsPort: this.opts.webviewDevtoolsPort, enableWebviewDetailsCollection: true, waitForWebviewMs: waitForWebviewMs || 0, }; return await helpers_1.getWebViewsMapping.bind(this)(_opts); } /** * @this {AndroidDriver} * @param {import('../types').WebviewsMapping[]} webviewsMapping * @returns {string[]} */ function assignContexts(webviewsMapping) { const opts = Object.assign({ isChromeSession: this.isChromeSession }, this.opts); const webviews = helpers_1.parseWebviewNames.bind(this)(webviewsMapping, opts); this.contexts = [helpers_1.NATIVE_WIN, ...webviews]; this.log.debug(`Available contexts: ${JSON.stringify(this.contexts)}`); return this.contexts; } /** * @this {AndroidDriver} * @param {string} name * @param {import('../types').WebviewsMapping[]} webviewsMapping * @returns {Promise<void>} */ async function switchContext(name, webviewsMapping) { // We have some options when it comes to webviews. If we want a // Chromedriver webview, we can only control one at a time. if (this.isChromedriverContext(name)) { // start proxying commands directly to chromedriver await this.startChromedriverProxy(name, webviewsMapping); } else if (this.isChromedriverContext(this.curContext)) { // if we're moving to a non-chromedriver webview, and our current context // _is_ a chromedriver webview, if caps recreateChromeDriverSessions is set // to true then kill chromedriver session using stopChromedriverProxies or // else simply suspend proxying to the latter if (this.opts.recreateChromeDriverSessions) { this.log.debug('recreateChromeDriverSessions set to true; killing existing chromedrivers'); await this.stopChromedriverProxies(); } else { this.suspendChromedriverProxy(); } } else { throw new Error(`Didn't know how to handle switching to context '${name}'`); } } /** * @this {AndroidDriver} * @returns {string} */ function defaultContextName() { return helpers_1.NATIVE_WIN; } /** * @this {AndroidDriver} * @returns {string} */ function defaultWebviewName() { return helpers_1.WEBVIEW_BASE + (this.opts.autoWebviewName || this.opts.appPackage); } /** * @this {AndroidDriver} * @returns {boolean} */ function isWebContext() { return this.curContext !== null && this.curContext !== helpers_1.NATIVE_WIN; } /** * @this {AndroidDriver} * @returns {Promise<string>} */ async function getWindowHandle() { if (!this.isWebContext()) { return DEFAULT_NATIVE_WINDOW_HANDLE; } const chromedriver = /** @type {Chromedriver} */ (this.chromedriver); const isJwp = chromedriver.jwproxy.downstreamProtocol === driver_1.PROTOCOLS.MJSONWP; const endpoint = isJwp ? '/window_handle' : '/window/handle'; return /** @type {string} */ (await chromedriver.jwproxy.command(endpoint, 'GET')); } /** * @this {AndroidDriver} * @returns {Promise<string[]>} */ async function getWindowHandles() { if (!this.isWebContext()) { return [DEFAULT_NATIVE_WINDOW_HANDLE]; } const chromedriver = /** @type {Chromedriver} */ (this.chromedriver); const isJwp = chromedriver.jwproxy.downstreamProtocol === driver_1.PROTOCOLS.MJSONWP; const endpoint = isJwp ? '/window_handles' : '/window/handles'; return /** @type {string[]} */ (await chromedriver.jwproxy.command(endpoint, 'GET')); } /** * @this {AndroidDriver} * @param {string} handle * @returns {Promise<void>} */ async function setWindow(handle) { if (!this.isWebContext()) { return; } const chromedriver = /** @type {Chromedriver} */ (this.chromedriver); const isJwp = chromedriver.jwproxy.downstreamProtocol === driver_1.PROTOCOLS.MJSONWP; const paramName = isJwp ? 'name' : 'handle'; await chromedriver.jwproxy.command('/window', 'POST', { [paramName]: handle }); } /** * Turn on proxying to an existing Chromedriver session or a new one * * @this {AndroidDriver} * @param {string} context * @param {import('../types').WebviewsMapping[]} webviewsMapping * @returns {Promise<void>} */ async function startChromedriverProxy(context, webviewsMapping) { this.log.debug(`Connecting to chrome-backed webview context '${context}'`); let cd; if (this.sessionChromedrivers[context]) { // in the case where we've already set up a chromedriver for a context, // we want to reconnect to it, not create a whole new one this.log.debug(`Found existing Chromedriver for context '${context}'. Using it.`); cd = this.sessionChromedrivers[context]; await helpers_1.setupExistingChromedriver.bind(this)(cd); } else { // XXX: this suppresses errors about putting arbitrary stuff on opts const opts = /** @type {any} */ (lodash_1.default.cloneDeep(this.opts)); opts.chromeUseRunningApp = true; // if requested, tell chromedriver to attach to the android package we have // associated with the context name, rather than the package of the AUT. // And turn this on by default for chrome--if chrome pops up with a webview // and someone wants to switch to it, we should let chromedriver connect to // chrome rather than staying stuck on the AUT if (opts.extractChromeAndroidPackageFromContextName || context === `${helpers_1.WEBVIEW_BASE}chrome`) { let androidPackage = context.match(`${helpers_1.WEBVIEW_BASE}(.+)`); if (androidPackage && androidPackage.length > 0) { opts.chromeAndroidPackage = androidPackage[1]; } if (!opts.extractChromeAndroidPackageFromContextName) { if (lodash_1.default.has(this.opts, 'enableWebviewDetailsCollection') && !this.opts.enableWebviewDetailsCollection) { // When enableWebviewDetailsCollection capability is explicitly disabled, try to identify // chromeAndroidPackage based on contexts, known chrome variant packages and queryAppState result // since webviewsMapping does not have info object const contexts = webviewsMapping.map((wm) => wm.webviewName); for (const knownPackage of helpers_1.KNOWN_CHROME_PACKAGE_NAMES) { if (lodash_1.default.includes(contexts, `${helpers_1.WEBVIEW_BASE}${knownPackage}`)) { continue; } const appState = await this.queryAppState(knownPackage); if (lodash_1.default.includes([app_management_1.APP_STATE.RUNNING_IN_BACKGROUND, app_management_1.APP_STATE.RUNNING_IN_FOREGROUND], appState)) { opts.chromeAndroidPackage = knownPackage; this.log.debug(`Identified chromeAndroidPackage as '${opts.chromeAndroidPackage}' ` + `for context '${context}' by querying states of Chrome app packages`); break; } } } else { for (const wm of webviewsMapping) { if (wm.webviewName === context && lodash_1.default.has(wm?.info, 'Android-Package')) { // XXX: should be a type guard here opts.chromeAndroidPackage = /** @type {NonNullable<import('../types').WebviewsMapping['info']>} */ (wm.info)['Android-Package']; this.log.debug(`Identified chromeAndroidPackage as '${opts.chromeAndroidPackage}' ` + `for context '${context}' by CDP`); break; } } } } } cd = await helpers_1.setupNewChromedriver.bind(this)(opts, /** @type {string} */ (this.adb.curDeviceId), context); // bind our stop/exit handler, passing in context so we know which // one stopped unexpectedly cd.on(appium_chromedriver_1.Chromedriver.EVENT_CHANGED, (msg) => { if (msg.state === appium_chromedriver_1.Chromedriver.STATE_STOPPED) { this.onChromedriverStop(context); } }); // save the chromedriver object under the context this.sessionChromedrivers[context] = cd; } // hook up the local variables so we can proxy this biz this.chromedriver = cd; // @ts-ignore chromedriver is defined this.proxyReqRes = this.chromedriver.proxyReq.bind(this.chromedriver); this.proxyCommand = /** @type {import('@appium/types').ExternalDriver['proxyCommand']} */ ( // @ts-ignore chromedriver is defined this.chromedriver.jwproxy.command.bind(this.chromedriver.jwproxy)); this.jwpProxyActive = true; } /** * Stop proxying to any Chromedriver * * @this {AndroidDriver} * @returns {void} */ function suspendChromedriverProxy() { this.chromedriver = undefined; this.proxyReqRes = undefined; this.proxyCommand = undefined; this.jwpProxyActive = false; } /** * Handle an out-of-band Chromedriver stop event * * @this {AndroidDriver} * @param {string} context * @returns {Promise<void>} */ async function onChromedriverStop(context) { this.log.warn(`Chromedriver for context ${context} stopped unexpectedly`); if (context === this.curContext) { // we exited unexpectedly while automating the current context and so want // to shut down the session and respond with an error let err = new Error('Chromedriver quit unexpectedly during session'); await this.startUnexpectedShutdown(err); } else { // if a Chromedriver in the non-active context barfs, we don't really // care, we'll just make a new one next time we need the context. this.log.warn("Chromedriver quit unexpectedly, but it wasn't the active " + 'context, ignoring'); delete this.sessionChromedrivers[context]; } } /** * Intentionally stop all the chromedrivers currently active, and ignore * their exit events * * @this {AndroidDriver} * @returns {Promise<void>} */ async function stopChromedriverProxies() { this.suspendChromedriverProxy(); // make sure we turn off the proxy flag for (let context of lodash_1.default.keys(this.sessionChromedrivers)) { let cd = this.sessionChromedrivers[context]; this.log.debug(`Stopping chromedriver for context ${context}`); // stop listening for the stopped state event cd.removeAllListeners(appium_chromedriver_1.Chromedriver.EVENT_CHANGED); try { await cd.stop(); } catch (err) { this.log.warn(`Error stopping Chromedriver: ${ /** @type {Error} */(err).message}`); } delete this.sessionChromedrivers[context]; } } /** * @this {AndroidDriver} * @param {string} viewName * @returns {boolean} */ function isChromedriverContext(viewName) { return lodash_1.default.includes(viewName, helpers_1.WEBVIEW_WIN) || viewName === helpers_1.CHROMIUM_WIN; } /** * https://github.com/appium/appium/issues/20741 * * @this {AndroidDriver} * @returns {Promise<void>} */ async function notifyBiDiContextChange() { const name = await this.getCurrentContext(); this.eventEmitter.emit(constants_1.BIDI_EVENT_NAME, (0, models_1.makeContextUpdatedEvent)(lodash_1.default.toLower(String(this.opts.automationName)), name)); this.eventEmitter.emit(constants_1.BIDI_EVENT_NAME, (0, models_1.makeObsoleteContextUpdatedEvent)(name)); } /** * @this {AndroidDriver} * @returns {Promise<void>} */ async function startChromeSession() { this.log.info('Starting a chrome-based browser session'); // XXX: this suppresses errors about putting arbitrary stuff on opts const opts = /** @type {any} */ (lodash_1.default.cloneDeep(this.opts)); const knownPackages = [ 'org.chromium.chrome.shell', 'com.android.chrome', 'com.chrome.beta', 'org.chromium.chrome', 'org.chromium.webview_shell', ]; if (lodash_1.default.includes(knownPackages, this.opts.appPackage)) { opts.chromeBundleId = this.opts.appPackage; } else { opts.chromeAndroidActivity = this.opts.appActivity; } this.chromedriver = await helpers_1.setupNewChromedriver.bind(this)(opts, /** @type {string} */ (this.adb.curDeviceId)); // @ts-ignore chromedriver is defined this.chromedriver.on(appium_chromedriver_1.Chromedriver.EVENT_CHANGED, (msg) => { if (msg.state === appium_chromedriver_1.Chromedriver.STATE_STOPPED) { this.onChromedriverStop(helpers_1.CHROMIUM_WIN); } }); // Now that we have a Chrome session, we ensure that the context is // appropriately set and that this chromedriver is added to the list // of session chromedrivers so we can switch back and forth this.curContext = helpers_1.CHROMIUM_WIN; // @ts-ignore chromedriver is defined this.sessionChromedrivers[helpers_1.CHROMIUM_WIN] = this.chromedriver; // @ts-ignore chromedriver should be defined this.proxyReqRes = this.chromedriver.proxyReq.bind(this.chromedriver); this.proxyCommand = /** @type {import('@appium/types').ExternalDriver['proxyCommand']} */ ( // @ts-ignore chromedriver is defined this.chromedriver.jwproxy.command.bind(this.chromedriver.jwproxy)); this.jwpProxyActive = true; if (helpers_1.shouldDismissChromeWelcome.bind(this)()) { // dismiss Chrome welcome dialog await helpers_1.dismissChromeWelcome.bind(this)(); } } /** * @typedef {import('appium-adb').ADB} ADB * @typedef {import('../../driver').AndroidDriver} AndroidDriver */ //# sourceMappingURL=exports.js.map