appium-android-driver
Version:
Android UiAutomator and Chrome support for Appium
411 lines • 17.3 kB
JavaScript
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
;