appium-android-driver
Version:
Android UiAutomator and Chrome support for Appium
742 lines • 31.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEVTOOLS_SOCKET_PATTERN = exports.WEBVIEW_BASE = exports.CHROMIUM_WIN = exports.WEBVIEW_WIN = exports.NATIVE_WIN = exports.KNOWN_CHROME_PACKAGE_NAMES = exports.CHROME_PACKAGE_NAME = exports.CHROME_BROWSER_PACKAGE_ACTIVITY = void 0;
exports.getChromePkg = getChromePkg;
exports.parseWebviewNames = parseWebviewNames;
exports.getWebViewsMapping = getWebViewsMapping;
exports.setupNewChromedriver = setupNewChromedriver;
exports.setupExistingChromedriver = setupExistingChromedriver;
exports.shouldDismissChromeWelcome = shouldDismissChromeWelcome;
exports.dismissChromeWelcome = dismissChromeWelcome;
const support_1 = require("@appium/support");
const lodash_1 = __importDefault(require("lodash"));
const axios_1 = __importDefault(require("axios"));
const node_net_1 = __importDefault(require("node:net"));
const portscanner_1 = require("portscanner");
const asyncbox_1 = require("asyncbox");
const bluebird_1 = __importDefault(require("bluebird"));
const node_os_1 = __importDefault(require("node:os"));
const node_path_1 = __importDefault(require("node:path"));
const node_http_1 = __importDefault(require("node:http"));
const appium_chromedriver_1 = require("appium-chromedriver");
const cache_1 = require("./cache");
const promises_1 = __importDefault(require("node:dns/promises"));
// https://cs.chromium.org/chromium/src/chrome/browser/devtools/device/android_device_info_query.cc
exports.CHROME_BROWSER_PACKAGE_ACTIVITY = ({
chrome: {
pkg: 'com.android.chrome',
activity: 'com.google.android.apps.chrome.Main',
},
chromium: {
pkg: 'org.chromium.chrome.shell',
activity: '.ChromeShellActivity',
},
chromebeta: {
pkg: 'com.chrome.beta',
activity: 'com.google.android.apps.chrome.Main',
},
browser: {
pkg: 'com.android.browser',
activity: 'com.android.browser.BrowserActivity',
},
'chromium-browser': {
pkg: 'org.chromium.chrome',
activity: 'com.google.android.apps.chrome.Main',
},
'chromium-webview': {
pkg: 'org.chromium.webview_shell',
activity: 'org.chromium.webview_shell.WebViewBrowserActivity',
},
default: {
pkg: 'com.android.chrome',
activity: 'com.google.android.apps.chrome.Main',
},
});
exports.CHROME_PACKAGE_NAME = 'com.android.chrome';
exports.KNOWN_CHROME_PACKAGE_NAMES = [
exports.CHROME_PACKAGE_NAME,
'com.chrome.beta',
'com.chrome.dev',
'com.chrome.canary',
];
const CHROMEDRIVER_AUTODOWNLOAD_FEATURE = 'chromedriver_autodownload';
const CROSSWALK_SOCKET_PATTERN = /@([\w.]+)_devtools_remote\b/;
const CHROMIUM_DEVTOOLS_SOCKET = 'chrome_devtools_remote';
exports.NATIVE_WIN = 'NATIVE_APP';
exports.WEBVIEW_WIN = 'WEBVIEW';
exports.CHROMIUM_WIN = 'CHROMIUM';
exports.WEBVIEW_BASE = `${exports.WEBVIEW_WIN}_`;
exports.DEVTOOLS_SOCKET_PATTERN = /@[\w.]+_devtools_remote_?([\w.]+_)?(\d+)?\b/;
const WEBVIEW_PID_PATTERN = new RegExp(`^${exports.WEBVIEW_BASE}(\\d+)`);
const WEBVIEW_PKG_PATTERN = new RegExp(`^${exports.WEBVIEW_BASE}([^\\d\\s][\\w.]*)`);
const WEBVIEW_WAIT_INTERVAL_MS = 200;
const CDP_REQ_TIMEOUT = 2000; // ms
const DEVTOOLS_PORTS_RANGE = [10900, 11000];
const DEVTOOLS_PORT_ALLOCATION_GUARD = support_1.util.getLockFileGuard(node_path_1.default.resolve(node_os_1.default.tmpdir(), 'android_devtools_port_guard'), { timeout: 7, tryRecovery: true });
/**
* @returns {Promise<number>}
*/
async function getFreePort() {
return await new Promise((resolve, reject) => {
const srv = node_net_1.default.createServer();
srv.listen(0, () => {
const address = srv.address();
let port;
if (lodash_1.default.has(address, 'port')) {
// @ts-ignore The above condition covers possible errors
port = address.port;
}
else {
reject(new Error('Cannot determine any free port number'));
}
srv.close(() => resolve(port));
});
});
}
/**
* https://chromedevtools.github.io/devtools-protocol/
*
* @param {string} host
* @param {number} port
* @param {string} endpoint
* @returns {Promise<object[]>}
*/
async function cdpGetRequest(host, port, endpoint) {
// Workaround for https://github.com/puppeteer/puppeteer/issues/2242, https://github.com/appium/appium/issues/20782
const compatibleHost = isCompatibleCdpHost(host) ? host : (await promises_1.default.lookup(host)).address;
return (await (0, axios_1.default)({
url: `http://${compatibleHost}:${port}${endpoint}`,
timeout: CDP_REQ_TIMEOUT,
// We need to set this from Node.js v19 onwards.
// Otherwise, in situation with multiple webviews,
// the preceding webview pages will be incorrectly retrieved as the current ones.
// https://nodejs.org/en/blog/announcements/v19-release-announce#https11-keepalive-by-default
httpAgent: new node_http_1.default.Agent({ keepAlive: false }),
})).data;
}
/**
* @param {string} host
* @param {number} port
* @returns {Promise<object[]>}
*/
async function cdpList(host, port) {
return cdpGetRequest(host, port, '/json/list');
}
/**
* @param {string} host
* @param {number} port
* @returns {Promise<object[]>}
*/
async function cdpInfo(host, port) {
return cdpGetRequest(host, port, '/json/version');
}
/**
*
* @param {string} browser
* @returns {import('type-fest').ValueOf<typeof CHROME_BROWSER_PACKAGE_ACTIVITY>}
*/
function getChromePkg(browser) {
return (exports.CHROME_BROWSER_PACKAGE_ACTIVITY[browser.toLowerCase()] ||
exports.CHROME_BROWSER_PACKAGE_ACTIVITY.default);
}
/**
* Create Chromedriver capabilities based on the provided
* Appium capabilities
*
* @this {import('../../driver').AndroidDriver}
* @param {any} opts
* @param {string} deviceId
* @param {import('../types').WebViewDetails | null} [webViewDetails]
* @returns {import('@appium/types').StringRecord}
*/
function createChromedriverCaps(opts, deviceId, webViewDetails) {
const caps = { chromeOptions: {} };
const androidPackage = opts.chromeOptions?.androidPackage ||
opts.appPackage ||
webViewDetails?.info?.['Android-Package'];
if (androidPackage) {
// chromedriver raises an invalid argument error when androidPackage is 'null'
caps.chromeOptions.androidPackage = androidPackage;
}
if (lodash_1.default.isBoolean(opts.chromeUseRunningApp)) {
caps.chromeOptions.androidUseRunningApp = opts.chromeUseRunningApp;
}
if (opts.chromeAndroidPackage) {
caps.chromeOptions.androidPackage = opts.chromeAndroidPackage;
}
if (opts.chromeAndroidActivity) {
caps.chromeOptions.androidActivity = opts.chromeAndroidActivity;
}
if (opts.chromeAndroidProcess) {
caps.chromeOptions.androidProcess = opts.chromeAndroidProcess;
}
else if (webViewDetails?.process?.name && webViewDetails?.process?.id) {
caps.chromeOptions.androidProcess = webViewDetails.process.name;
}
if (lodash_1.default.toLower(opts.browserName) === 'chromium-webview') {
caps.chromeOptions.androidActivity = opts.appActivity;
}
if (opts.pageLoadStrategy) {
caps.pageLoadStrategy = opts.pageLoadStrategy;
}
const isChrome = lodash_1.default.toLower(caps.chromeOptions.androidPackage) === 'chrome';
if (lodash_1.default.includes(exports.KNOWN_CHROME_PACKAGE_NAMES, caps.chromeOptions.androidPackage) || isChrome) {
// if we have extracted package from context name, it could come in as bare
// "chrome", and so we should make sure the details are correct, including
// not using an activity or process id
if (isChrome) {
caps.chromeOptions.androidPackage = exports.CHROME_PACKAGE_NAME;
}
delete caps.chromeOptions.androidActivity;
delete caps.chromeOptions.androidProcess;
}
// add device id from adb
caps.chromeOptions.androidDeviceSerial = deviceId;
if (lodash_1.default.isPlainObject(opts.loggingPrefs) || lodash_1.default.isPlainObject(opts.chromeLoggingPrefs)) {
if (opts.loggingPrefs) {
this.log.warn(`The 'loggingPrefs' cap is deprecated; use the 'chromeLoggingPrefs' cap instead`);
}
caps.loggingPrefs = opts.chromeLoggingPrefs || opts.loggingPrefs;
}
if (opts.enablePerformanceLogging) {
this.log.warn(`The 'enablePerformanceLogging' cap is deprecated; simply use ` +
`the 'chromeLoggingPrefs' cap instead, with a 'performance' key set to 'ALL'`);
const newPref = { performance: 'ALL' };
// don't overwrite other logging prefs that have been sent in if they exist
caps.loggingPrefs = caps.loggingPrefs ? Object.assign({}, caps.loggingPrefs, newPref) : newPref;
}
if (opts.chromeOptions?.Arguments) {
// merge `Arguments` and `args`
opts.chromeOptions.args = [...(opts.chromeOptions.args || []), ...opts.chromeOptions.Arguments];
delete opts.chromeOptions.Arguments;
}
this.log.debug('Precalculated Chromedriver capabilities: ' + JSON.stringify(caps.chromeOptions, null, 2));
/** @type {string[]} */
const protectedCapNames = [];
for (const [opt, val] of lodash_1.default.toPairs(opts.chromeOptions)) {
if (lodash_1.default.isUndefined(caps.chromeOptions[opt])) {
caps.chromeOptions[opt] = val;
}
else {
protectedCapNames.push(opt);
}
}
if (!lodash_1.default.isEmpty(protectedCapNames)) {
this.log.info('The following Chromedriver capabilities cannot be overridden ' +
'by the provided chromeOptions:');
for (const optName of protectedCapNames) {
this.log.info(` ${optName} (${JSON.stringify(opts.chromeOptions[optName])})`);
}
}
return caps;
}
/**
* Parse webview names for getContexts
*
* @this {import('../../driver').AndroidDriver}
* @param {import('../types').WebviewsMapping[]} webviewsMapping
* @param {import('../types').GetWebviewsOpts} options
* @returns {string[]}
*/
function parseWebviewNames(webviewsMapping, { ensureWebviewsHavePages = true, isChromeSession = false } = {}) {
if (isChromeSession) {
return [exports.CHROMIUM_WIN];
}
/** @type {string[]} */
const result = [];
for (const { webview, pages, proc, webviewName } of webviewsMapping) {
if (ensureWebviewsHavePages && !pages?.length) {
this.log.info(`Skipping the webview '${webview}' at '${proc}' ` +
`since it has reported having zero pages`);
continue;
}
if (webviewName) {
result.push(webviewName);
}
}
this.log.debug(`Found ${support_1.util.pluralize('webview', result.length, true)}: ${JSON.stringify(result)}`);
return result;
}
/**
* Allocates a local port for devtools communication
*
* @this {import('../../driver').AndroidDriver}
* @param {string} socketName - The remote Unix socket name
* @param {number?} [webviewDevtoolsPort=null] - The local port number or null to apply
* autodetection
* @returns {Promise<[string, number]>} The host name and the port number to connect to if the
* remote socket has been forwarded successfully
* @throws {Error} If there was an error while allocating the local port
*/
async function allocateDevtoolsChannel(socketName, webviewDevtoolsPort = null) {
// socket names come with '@', but this should not be a part of the abstract
// remote port, so remove it
const remotePort = socketName.replace(/^@/, '');
let [startPort, endPort] = DEVTOOLS_PORTS_RANGE;
if (webviewDevtoolsPort) {
endPort = webviewDevtoolsPort + (endPort - startPort);
startPort = webviewDevtoolsPort;
}
this.log.debug(`Forwarding remote port ${remotePort} to a local ` + `port in range ${startPort}..${endPort}`);
if (!webviewDevtoolsPort) {
this.log.debug(`You could use the 'webviewDevtoolsPort' capability to customize ` +
`the starting port number`);
}
const port = await DEVTOOLS_PORT_ALLOCATION_GUARD(async () => {
let localPort;
try {
localPort = await (0, portscanner_1.findAPortNotInUse)(startPort, endPort);
}
catch {
throw new Error(`Cannot find any free port to forward the Devtools socket ` +
`in range ${startPort}..${endPort}. You could set the starting port number ` +
`manually by providing the 'webviewDevtoolsPort' capability`);
}
await this.adb.adbExec(['forward', `tcp:${localPort}`, `localabstract:${remotePort}`]);
return localPort;
});
return [this.adb.adbHost ?? '127.0.0.1', port];
}
/**
* This is a wrapper for Chrome Debugger Protocol data collection.
* No error is thrown if CDP request fails - in such case no data will be
* recorded into the corresponding `webviewsMapping` item.
*
* @this {import('../../driver').AndroidDriver}
* @param {import('../types').WebviewProps[]} webviewsMapping The current webviews mapping
* !!! Each item of this array gets mutated (`info`/`pages` properties get added
* based on the provided `opts`) if the requested details have been
* successfully retrieved for it !!!
* @param {import('../types').DetailCollectionOptions} [opts={}] If both `ensureWebviewsHavePages` and
* `enableWebviewDetailsCollection` properties are falsy then no details collection
* is performed
* @returns {Promise<void>}
*/
async function collectWebviewsDetails(webviewsMapping, opts = {}) {
if (lodash_1.default.isEmpty(webviewsMapping)) {
return;
}
const { webviewDevtoolsPort = null, ensureWebviewsHavePages = null, enableWebviewDetailsCollection = null, } = opts;
if (!ensureWebviewsHavePages) {
this.log.info(`Not checking whether webviews have active pages; use the ` +
`'ensureWebviewsHavePages' cap to turn this check on`);
}
if (!enableWebviewDetailsCollection) {
this.log.info(`Not collecting web view details. Details collection might help ` +
`to make Chromedriver initialization more precise. Use the 'enableWebviewDetailsCollection' ` +
`cap to turn it on`);
}
if (!ensureWebviewsHavePages && !enableWebviewDetailsCollection) {
return;
}
// Connect to each devtools socket and retrieve web view details
this.log.debug(`Collecting CDP data of ${support_1.util.pluralize('webview', webviewsMapping.length, true)}`);
const detailCollectors = [];
for (const item of webviewsMapping) {
detailCollectors.push((async () => {
let port;
let host;
try {
[host, port] = /** @type {[string, number]} */ (await allocateDevtoolsChannel.bind(this)(item.proc, webviewDevtoolsPort));
if (enableWebviewDetailsCollection) {
item.info = await cdpInfo(host, port);
}
if (ensureWebviewsHavePages) {
item.pages = await cdpList(host, port);
}
}
catch (e) {
this.log.info(`CDP data for '${item.webview}' cannot be collected. Original error: ${e.message}`);
}
finally {
if (port) {
try {
await this.adb.removePortForward(port);
}
catch (e) {
this.log.debug(e);
}
}
}
})());
}
await bluebird_1.default.all(detailCollectors);
this.log.debug(`CDP data collection completed`);
}
/**
* Get a list of available webviews mapping by introspecting processes with adb,
* where webviews are listed. It's possible to pass in a 'deviceSocket' arg, which
* limits the webview possibilities to the one running on the Chromium devtools
* socket we're interested in (see note on webviewsFromProcs). We can also
* direct this method to verify whether a particular webview process actually
* has any pages (if a process exists but no pages are found, Chromedriver will
* not actually be able to connect to it, so this serves as a guard for that
* strange failure mode). The strategy for checking whether any pages are
* active involves sending a request to the remote debug server on the device,
* hence it is also possible to specify the port on the host machine which
* should be used for this communication.
*
* @this {import('../../driver').AndroidDriver}
* @param {import('../types').GetWebviewsOpts} [opts={}]
* @returns {Promise<import('../types').WebviewsMapping[]>}
*/
async function getWebViewsMapping({ androidDeviceSocket = null, ensureWebviewsHavePages = true, webviewDevtoolsPort = null, enableWebviewDetailsCollection = true, waitForWebviewMs = 0, } = {}) {
this.log.debug(`Getting a list of available webviews`);
if (!lodash_1.default.isNumber(waitForWebviewMs)) {
waitForWebviewMs = parseInt(`${waitForWebviewMs}`, 10) || 0;
}
/** @type {import('../types').WebviewsMapping[]} */
let webviewsMapping;
const timer = new support_1.timing.Timer().start();
do {
webviewsMapping = await webviewsFromProcs.bind(this)(androidDeviceSocket);
if (webviewsMapping.length > 0) {
break;
}
this.log.debug(`No webviews found in ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`);
await (0, asyncbox_1.sleep)(WEBVIEW_WAIT_INTERVAL_MS);
} while (timer.getDuration().asMilliSeconds < waitForWebviewMs);
await collectWebviewsDetails.bind(this)(webviewsMapping, {
ensureWebviewsHavePages,
enableWebviewDetailsCollection,
webviewDevtoolsPort,
});
for (const webviewMapping of webviewsMapping) {
const { webview, info } = webviewMapping;
webviewMapping.webviewName = null;
let wvName = webview;
/** @type {{name: string; id: string | null} | undefined} */
let process;
if (!androidDeviceSocket) {
const pkgMatch = WEBVIEW_PKG_PATTERN.exec(webview);
try {
// web view name could either be suffixed with PID or the package name
// package names could not start with a digit
const pkg = pkgMatch ? pkgMatch[1] : await procFromWebview.bind(this)(webview);
wvName = `${exports.WEBVIEW_BASE}${pkg}`;
const pidMatch = WEBVIEW_PID_PATTERN.exec(webview);
process = {
name: pkg,
id: pidMatch ? pidMatch[1] : null,
};
}
catch (e) {
this.log.debug(e.stack);
this.log.warn(e.message);
continue;
}
}
webviewMapping.webviewName = wvName;
const key = (0, cache_1.toDetailsCacheKey)(this.adb, wvName);
if (info || process) {
cache_1.WEBVIEWS_DETAILS_CACHE.set(key, { info, process });
}
else if (cache_1.WEBVIEWS_DETAILS_CACHE.has(key)) {
cache_1.WEBVIEWS_DETAILS_CACHE.delete(key);
}
}
return webviewsMapping;
}
/**
* Take a webview name like WEBVIEW_4296 and use 'adb shell ps' to figure out
* which app package is associated with that webview. One of the reasons we
* want to do this is to make sure we're listing webviews for the actual AUT,
* not some other running app
*
* @this {import('../../driver').AndroidDriver}
* @param {string} webview
* @returns {Promise<string>}
*/
async function procFromWebview(webview) {
const pidMatch = WEBVIEW_PID_PATTERN.exec(webview);
if (!pidMatch) {
throw new Error(`Could not find PID for webview '${webview}'`);
}
const pid = pidMatch[1];
this.log.debug(`${webview} mapped to pid ${pid}`);
this.log.debug(`Getting process name for webview '${webview}'`);
const pkg = await this.adb.getNameByPid(pid);
this.log.debug(`Got process name: '${pkg}'`);
return pkg;
}
/**
* This function gets a list of android system processes and returns ones
* that look like webviews
* See https://cs.chromium.org/chromium/src/chrome/browser/devtools/device/android_device_info_query.cc
* for more details
*
* @this {import('../../driver').AndroidDriver}
* @returns {Promise<string[]>} a list of matching webview socket names (including the leading '@')
*/
async function getPotentialWebviewProcs() {
const out = await this.adb.shell(['cat', '/proc/net/unix']);
/** @type {string[]} */
const names = [];
/** @type {string[]} */
const allMatches = [];
for (const line of out.split('\n')) {
// Num RefCount Protocol Flags Type St Inode Path
const [, , , flags, , st, , sockPath] = line.trim().split(/\s+/);
if (!sockPath) {
continue;
}
if (sockPath.startsWith('@')) {
allMatches.push(line.trim());
}
if (flags !== '00010000' || st !== '01') {
continue;
}
if (!exports.DEVTOOLS_SOCKET_PATTERN.test(sockPath)) {
continue;
}
names.push(sockPath);
}
if (lodash_1.default.isEmpty(names)) {
this.log.debug('Found no active devtools sockets');
if (!lodash_1.default.isEmpty(allMatches)) {
this.log.debug(`Other sockets are: ${JSON.stringify(allMatches, null, 2)}`);
}
}
else {
this.log.debug(`Parsed ${names.length} active devtools ${support_1.util.pluralize('socket', names.length, false)}: ` +
JSON.stringify(names));
}
// sometimes the webview process shows up multiple times per app
return lodash_1.default.uniq(names);
}
/**
* This function retrieves a list of system processes that look like webviews,
* and returns them along with the webview context name appropriate for it.
* If we pass in a deviceSocket, we only attempt to find webviews which match
* that socket name (this is for apps which embed Chromium, which isn't the
* same as chrome-backed webviews).
*
* @this {import('../../driver').AndroidDriver}
* @param {string?} [deviceSocket=null] - the explictly-named device socket to use
* @returns {Promise<import('../types').WebviewProc[]>}
*/
async function webviewsFromProcs(deviceSocket = null) {
const socketNames = await getPotentialWebviewProcs.bind(this)();
/** @type {{proc: string; webview: string}[]} */
const webviews = [];
for (const socketName of socketNames) {
if (deviceSocket === CHROMIUM_DEVTOOLS_SOCKET && socketName === `@${deviceSocket}`) {
webviews.push({
proc: socketName,
webview: exports.CHROMIUM_WIN,
});
continue;
}
const socketNameMatch = exports.DEVTOOLS_SOCKET_PATTERN.exec(socketName);
if (!socketNameMatch) {
continue;
}
const matchedSocketName = socketNameMatch[2];
const crosswalkMatch = CROSSWALK_SOCKET_PATTERN.exec(socketName);
if (!matchedSocketName && !crosswalkMatch) {
continue;
}
if ((deviceSocket && socketName === `@${deviceSocket}`) || !deviceSocket) {
webviews.push({
proc: socketName,
webview: matchedSocketName
? `${exports.WEBVIEW_BASE}${matchedSocketName}`
: // @ts-expect-error: XXX crosswalkMatch can absolutely be null
`${exports.WEBVIEW_BASE}${crosswalkMatch[1]}`,
});
}
}
return webviews;
}
/**
* @this {import('../../driver').AndroidDriver}
* @param {import('../types').PortSpec} [portSpec]
* @returns {Promise<number>}
*/
async function getChromedriverPort(portSpec) {
// if the user didn't give us any specific information about chromedriver
// port ranges, just find any free port
if (!portSpec) {
const port = await getFreePort();
this.log.debug(`A port was not given, using random free port: ${port}`);
return port;
}
// otherwise find the free port based on a list or range provided by the user
this.log.debug(`Finding a free port for chromedriver using spec ${JSON.stringify(portSpec)}`);
let foundPort = null;
for (const potentialPort of portSpec) {
/** @type {number} */
let port;
/** @type {number} */
let stopPort;
if (Array.isArray(potentialPort)) {
[port, stopPort] = potentialPort.map((p) => parseInt(String(p), 10));
}
else {
port = parseInt(String(potentialPort), 10); // ensure we have a number and not a string
stopPort = port;
}
this.log.debug(`Checking port range ${port}:${stopPort}`);
try {
foundPort = await (0, portscanner_1.findAPortNotInUse)(port, stopPort);
break;
}
catch {
this.log.debug(`Nothing in port range ${port}:${stopPort} was available`);
}
}
if (foundPort === null) {
throw new Error(`Could not find a free port for chromedriver using ` +
`chromedriverPorts spec ${JSON.stringify(portSpec)}`);
}
this.log.debug(`Using free port ${foundPort} for chromedriver`);
return foundPort;
}
/**
* @this {import('../../driver').AndroidDriver}
* @returns {boolean}
*/
function isChromedriverAutodownloadEnabled() {
if (this.isFeatureEnabled(CHROMEDRIVER_AUTODOWNLOAD_FEATURE)) {
return true;
}
this.log.debug(`Automated Chromedriver download is disabled. ` +
`Use '${CHROMEDRIVER_AUTODOWNLOAD_FEATURE}' server feature to enable it`);
return false;
}
/**
* @this {import('../../driver').AndroidDriver}
* @param {import('../../driver').AndroidDriverOpts} opts
* @param {string} curDeviceId
* @param {string} [context]
* @returns {Promise<Chromedriver>}
*/
async function setupNewChromedriver(opts, curDeviceId, context) {
// @ts-ignore TODO: Remove the legacy
if (opts.chromeDriverPort) {
this.log.warn(`The 'chromeDriverPort' capability is deprecated. Please use 'chromedriverPort' instead`);
// @ts-ignore TODO: Remove the legacy
opts.chromedriverPort = opts.chromeDriverPort;
}
if (opts.chromedriverPort) {
this.log.debug(`Using user-specified port ${opts.chromedriverPort} for chromedriver`);
}
else {
// if a single port wasn't given, we'll look for a free one
opts.chromedriverPort = await getChromedriverPort.bind(this)(opts.chromedriverPorts);
}
const details = context ? (0, cache_1.getWebviewDetails)(this.adb, context) : undefined;
if (!lodash_1.default.isEmpty(details)) {
this.log.debug('Passing web view details to the Chromedriver constructor: ' +
JSON.stringify(details, null, 2));
}
/** @type {import('appium-chromedriver').ChromedriverOpts} */
const chromedriverOpts = {
port: lodash_1.default.isNil(opts.chromedriverPort) ? undefined : String(opts.chromedriverPort),
executable: opts.chromedriverExecutable,
adb: this.adb,
cmdArgs: /** @type {string[] | undefined} */ (opts.chromedriverArgs),
verbose: !!opts.showChromedriverLog,
executableDir: opts.chromedriverExecutableDir,
mappingPath: opts.chromedriverChromeMappingFile,
// @ts-ignore this property exists
bundleId: opts.chromeBundleId,
useSystemExecutable: opts.chromedriverUseSystemExecutable,
disableBuildCheck: opts.chromedriverDisableBuildCheck,
// @ts-ignore this is ok
details,
isAutodownloadEnabled: isChromedriverAutodownloadEnabled.bind(this)(),
};
if (this.basePath) {
chromedriverOpts.reqBasePath = this.basePath;
}
const chromedriver = new appium_chromedriver_1.Chromedriver(chromedriverOpts);
// make sure there are chromeOptions
opts.chromeOptions = opts.chromeOptions || {};
// try out any prefixed chromeOptions,
// and strip the prefix
for (const opt of lodash_1.default.keys(opts)) {
if (opt.endsWith(':chromeOptions')) {
this?.log?.warn(`Merging '${opt}' into 'chromeOptions'. This may cause unexpected behavior`);
lodash_1.default.merge(opts.chromeOptions, opts[opt]);
}
}
// Ensure there are logging preferences
opts.chromeLoggingPrefs = opts.chromeLoggingPrefs ?? {};
// Strip the prefix and store it
for (const opt of lodash_1.default.keys(opts)) {
if (opt.endsWith(':loggingPrefs')) {
this.log.warn(`Merging '${opt}' into 'chromeLoggingPrefs'. This may cause unexpected behavior`);
lodash_1.default.merge(opts.chromeLoggingPrefs, opts[opt]);
}
}
const caps = /** @type {any} */ (createChromedriverCaps.bind(this)(opts, curDeviceId, details));
this.log.debug(`Before starting chromedriver, androidPackage is '${caps.chromeOptions.androidPackage}'`);
await chromedriver.start(caps);
return chromedriver;
}
/**
* @this {import('../../driver').AndroidDriver}
* @template {Chromedriver} T
* @param {T} chromedriver
* @returns {Promise<T>}
*/
async function setupExistingChromedriver(chromedriver) {
// check the status by sending a simple window-based command to ChromeDriver
// if there is an error, we want to recreate the ChromeDriver session
if (!(await chromedriver.hasWorkingWebview())) {
this.log.debug('ChromeDriver is not associated with a window. Re-initializing the session.');
await chromedriver.restart();
}
return chromedriver;
}
/**
* @this {import('../../driver').AndroidDriver}
* @returns {boolean}
*/
function shouldDismissChromeWelcome() {
return (!!this.opts.chromeOptions &&
lodash_1.default.isArray(this.opts.chromeOptions.args) &&
this.opts.chromeOptions.args.includes('--no-first-run'));
}
/**
* @this {import('../../driver').AndroidDriver}
* @returns {Promise<void>}
*/
async function dismissChromeWelcome() {
this.log.info('Trying to dismiss Chrome welcome');
let activity = await this.getCurrentActivity();
if (activity !== 'org.chromium.chrome.browser.firstrun.FirstRunActivity') {
this.log.info('Chrome welcome dialog never showed up! Continuing');
return;
}
let el = await this.findElOrEls('id', 'com.android.chrome:id/terms_accept', false);
await this.click(/** @type {string} */ (el.ELEMENT));
try {
let el = await this.findElOrEls('id', 'com.android.chrome:id/negative_button', false);
await this.click(/** @type {string} */ (el.ELEMENT));
}
catch (e) {
// DO NOTHING, THIS DEVICE DIDNT LAUNCH THE SIGNIN DIALOG
// IT MUST BE A NON GMS DEVICE
this.log.warn(`This device did not show Chrome SignIn dialog, ${ /** @type {Error} */(e).message}`);
}
}
/**
* https://github.com/puppeteer/puppeteer/issues/2242#issuecomment-544219536
*
* @param {string} host
* @returns {boolean}
*/
function isCompatibleCdpHost(host) {
return ['localhost', 'localhost.localdomain'].includes(host)
|| host.endsWith('.localhost')
|| Boolean(node_net_1.default.isIP(host));
}
/**
* @typedef {import('appium-adb').ADB} ADB
*/
//# sourceMappingURL=helpers.js.map