appium-android-driver 12.1.2 → 12.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/build/lib/commands/context/exports.d.ts +5 -0
- package/build/lib/commands/context/exports.d.ts.map +1 -1
- package/build/lib/commands/context/exports.js +17 -0
- package/build/lib/commands/context/exports.js.map +1 -1
- package/build/lib/commands/context/helpers.d.ts +7 -6
- package/build/lib/commands/context/helpers.d.ts.map +1 -1
- package/build/lib/commands/context/helpers.js +257 -252
- package/build/lib/commands/context/helpers.js.map +1 -1
- package/build/lib/driver.d.ts +6 -2
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js +2 -1
- package/build/lib/driver.js.map +1 -1
- package/build/lib/execute-method-map.d.ts +3 -0
- package/build/lib/execute-method-map.d.ts.map +1 -1
- package/build/lib/execute-method-map.js +3 -0
- package/build/lib/execute-method-map.js.map +1 -1
- package/lib/commands/context/exports.js +22 -0
- package/lib/commands/context/helpers.js +322 -312
- package/lib/driver.ts +3 -1
- package/lib/execute-method-map.ts +4 -0
- package/package.json +2 -2
|
@@ -68,6 +68,290 @@ const DEVTOOLS_PORT_ALLOCATION_GUARD = util.getLockFileGuard(
|
|
|
68
68
|
{timeout: 7, tryRecovery: true},
|
|
69
69
|
);
|
|
70
70
|
|
|
71
|
+
// #region Exported Functions
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
*
|
|
75
|
+
* @param {string} browser
|
|
76
|
+
* @returns {CHROME_BROWSER_PACKAGE_ACTIVITY[keyof CHROME_BROWSER_PACKAGE_ACTIVITY]}
|
|
77
|
+
*/
|
|
78
|
+
export function getChromePkg(browser) {
|
|
79
|
+
return (
|
|
80
|
+
CHROME_BROWSER_PACKAGE_ACTIVITY[browser.toLowerCase()] ||
|
|
81
|
+
CHROME_BROWSER_PACKAGE_ACTIVITY.default
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parse webview names for getContexts
|
|
87
|
+
*
|
|
88
|
+
* @this {AndroidDriver}
|
|
89
|
+
* @param {import('../types').WebviewsMapping[]} webviewsMapping
|
|
90
|
+
* @param {import('../types').GetWebviewsOpts} options
|
|
91
|
+
* @returns {string[]}
|
|
92
|
+
*/
|
|
93
|
+
export function parseWebviewNames(
|
|
94
|
+
webviewsMapping,
|
|
95
|
+
{ensureWebviewsHavePages = true, isChromeSession = false} = {},
|
|
96
|
+
) {
|
|
97
|
+
if (isChromeSession) {
|
|
98
|
+
return [CHROMIUM_WIN];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @type {string[]} */
|
|
102
|
+
const result = [];
|
|
103
|
+
for (const {webview, pages, proc, webviewName} of webviewsMapping) {
|
|
104
|
+
if (ensureWebviewsHavePages && !pages?.length) {
|
|
105
|
+
this.log.info(
|
|
106
|
+
`Skipping the webview '${webview}' at '${proc}' ` +
|
|
107
|
+
`since it has reported having zero pages`,
|
|
108
|
+
);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (webviewName) {
|
|
112
|
+
result.push(webviewName);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
this.log.debug(
|
|
116
|
+
`Found ${util.pluralize('webview', result.length, true)}: ${JSON.stringify(result)}`,
|
|
117
|
+
);
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get a list of available webviews mapping by introspecting processes with adb,
|
|
123
|
+
* where webviews are listed. It's possible to pass in a 'deviceSocket' arg, which
|
|
124
|
+
* limits the webview possibilities to the one running on the Chromium devtools
|
|
125
|
+
* socket we're interested in (see note on webviewsFromProcs). We can also
|
|
126
|
+
* direct this method to verify whether a particular webview process actually
|
|
127
|
+
* has any pages (if a process exists but no pages are found, Chromedriver will
|
|
128
|
+
* not actually be able to connect to it, so this serves as a guard for that
|
|
129
|
+
* strange failure mode). The strategy for checking whether any pages are
|
|
130
|
+
* active involves sending a request to the remote debug server on the device,
|
|
131
|
+
* hence it is also possible to specify the port on the host machine which
|
|
132
|
+
* should be used for this communication.
|
|
133
|
+
*
|
|
134
|
+
* @this {AndroidDriver}
|
|
135
|
+
* @param {import('../types').GetWebviewsOpts} [opts={}]
|
|
136
|
+
* @returns {Promise<import('../types').WebviewsMapping[]>}
|
|
137
|
+
*/
|
|
138
|
+
export async function getWebViewsMapping({
|
|
139
|
+
androidDeviceSocket = null,
|
|
140
|
+
ensureWebviewsHavePages = true,
|
|
141
|
+
webviewDevtoolsPort = null,
|
|
142
|
+
enableWebviewDetailsCollection = true,
|
|
143
|
+
waitForWebviewMs = 0,
|
|
144
|
+
} = {}) {
|
|
145
|
+
this.log.debug(`Getting a list of available webviews`);
|
|
146
|
+
|
|
147
|
+
if (!_.isNumber(waitForWebviewMs)) {
|
|
148
|
+
waitForWebviewMs = parseInt(`${waitForWebviewMs}`, 10) || 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** @type {import('../types').WebviewsMapping[]} */
|
|
152
|
+
let webviewsMapping;
|
|
153
|
+
const timer = new timing.Timer().start();
|
|
154
|
+
do {
|
|
155
|
+
webviewsMapping = await webviewsFromProcs.bind(this)(androidDeviceSocket);
|
|
156
|
+
|
|
157
|
+
if (webviewsMapping.length > 0) {
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.log.debug(`No webviews found in ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`);
|
|
162
|
+
await sleep(WEBVIEW_WAIT_INTERVAL_MS);
|
|
163
|
+
} while (timer.getDuration().asMilliSeconds < waitForWebviewMs);
|
|
164
|
+
|
|
165
|
+
await collectWebviewsDetails.bind(this)(webviewsMapping, {
|
|
166
|
+
ensureWebviewsHavePages,
|
|
167
|
+
enableWebviewDetailsCollection,
|
|
168
|
+
webviewDevtoolsPort,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
for (const webviewMapping of webviewsMapping) {
|
|
172
|
+
const {webview, info} = webviewMapping;
|
|
173
|
+
webviewMapping.webviewName = null;
|
|
174
|
+
|
|
175
|
+
let wvName = webview;
|
|
176
|
+
/** @type {{name: string; id: string | null} | undefined} */
|
|
177
|
+
let process;
|
|
178
|
+
if (!androidDeviceSocket) {
|
|
179
|
+
const pkgMatch = WEBVIEW_PKG_PATTERN.exec(webview);
|
|
180
|
+
try {
|
|
181
|
+
// web view name could either be suffixed with PID or the package name
|
|
182
|
+
// package names could not start with a digit
|
|
183
|
+
const pkg = pkgMatch ? pkgMatch[1] : await procFromWebview.bind(this)(webview);
|
|
184
|
+
wvName = `${WEBVIEW_BASE}${pkg}`;
|
|
185
|
+
const pidMatch = WEBVIEW_PID_PATTERN.exec(webview);
|
|
186
|
+
process = {
|
|
187
|
+
name: pkg,
|
|
188
|
+
id: pidMatch ? pidMatch[1] : null,
|
|
189
|
+
};
|
|
190
|
+
} catch (e) {
|
|
191
|
+
this.log.debug(e.stack);
|
|
192
|
+
this.log.warn(e.message);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
webviewMapping.webviewName = wvName;
|
|
198
|
+
const key = toDetailsCacheKey(this.adb, wvName);
|
|
199
|
+
if (info || process) {
|
|
200
|
+
WEBVIEWS_DETAILS_CACHE.set(key, {info, process});
|
|
201
|
+
} else if (WEBVIEWS_DETAILS_CACHE.has(key)) {
|
|
202
|
+
WEBVIEWS_DETAILS_CACHE.delete(key);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return webviewsMapping;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @this {AndroidDriver}
|
|
210
|
+
* @param {import('../../driver').AndroidDriverOpts} opts
|
|
211
|
+
* @param {string} curDeviceId
|
|
212
|
+
* @param {string} [context]
|
|
213
|
+
* @returns {Promise<Chromedriver>}
|
|
214
|
+
*/
|
|
215
|
+
export async function setupNewChromedriver(opts, curDeviceId, context) {
|
|
216
|
+
// @ts-ignore TODO: Remove the legacy
|
|
217
|
+
if (opts.chromeDriverPort) {
|
|
218
|
+
this.log.warn(
|
|
219
|
+
`The 'chromeDriverPort' capability is deprecated. Please use 'chromedriverPort' instead`,
|
|
220
|
+
);
|
|
221
|
+
// @ts-ignore TODO: Remove the legacy
|
|
222
|
+
opts.chromedriverPort = opts.chromeDriverPort;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (opts.chromedriverPort) {
|
|
226
|
+
this.log.debug(`Using user-specified port ${opts.chromedriverPort} for chromedriver`);
|
|
227
|
+
} else {
|
|
228
|
+
// if a single port wasn't given, we'll look for a free one
|
|
229
|
+
opts.chromedriverPort = await getChromedriverPort.bind(this)(opts.chromedriverPorts);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const details = context ? getWebviewDetails(this.adb, context) : undefined;
|
|
233
|
+
if (!_.isEmpty(details)) {
|
|
234
|
+
this.log.debug(
|
|
235
|
+
'Passing web view details to the Chromedriver constructor: ' +
|
|
236
|
+
JSON.stringify(details, null, 2),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** @type {import('appium-chromedriver').ChromedriverOpts} */
|
|
241
|
+
const chromedriverOpts = {
|
|
242
|
+
port: _.isNil(opts.chromedriverPort) ? undefined : String(opts.chromedriverPort),
|
|
243
|
+
executable: opts.chromedriverExecutable,
|
|
244
|
+
adb: this.adb,
|
|
245
|
+
cmdArgs: /** @type {string[] | undefined} */ (opts.chromedriverArgs),
|
|
246
|
+
verbose: !!opts.showChromedriverLog,
|
|
247
|
+
executableDir: opts.chromedriverExecutableDir,
|
|
248
|
+
mappingPath: opts.chromedriverChromeMappingFile,
|
|
249
|
+
// @ts-ignore this property exists
|
|
250
|
+
bundleId: opts.chromeBundleId,
|
|
251
|
+
useSystemExecutable: opts.chromedriverUseSystemExecutable,
|
|
252
|
+
disableBuildCheck: opts.chromedriverDisableBuildCheck,
|
|
253
|
+
// @ts-ignore this is ok
|
|
254
|
+
details,
|
|
255
|
+
isAutodownloadEnabled: isChromedriverAutodownloadEnabled.bind(this)(),
|
|
256
|
+
};
|
|
257
|
+
if (this.basePath) {
|
|
258
|
+
chromedriverOpts.reqBasePath = this.basePath;
|
|
259
|
+
}
|
|
260
|
+
const chromedriver = new Chromedriver(chromedriverOpts);
|
|
261
|
+
// make sure there are chromeOptions
|
|
262
|
+
opts.chromeOptions = opts.chromeOptions || {};
|
|
263
|
+
// try out any prefixed chromeOptions,
|
|
264
|
+
// and strip the prefix
|
|
265
|
+
for (const opt of _.keys(opts)) {
|
|
266
|
+
if (opt.endsWith(':chromeOptions')) {
|
|
267
|
+
this?.log?.warn(`Merging '${opt}' into 'chromeOptions'. This may cause unexpected behavior`);
|
|
268
|
+
_.merge(opts.chromeOptions, opts[opt]);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Ensure there are logging preferences
|
|
273
|
+
opts.chromeLoggingPrefs = opts.chromeLoggingPrefs ?? {};
|
|
274
|
+
|
|
275
|
+
// Strip the prefix and store it
|
|
276
|
+
for (const opt of _.keys(opts)) {
|
|
277
|
+
if (opt.endsWith(':loggingPrefs')) {
|
|
278
|
+
this.log.warn(`Merging '${opt}' into 'chromeLoggingPrefs'. This may cause unexpected behavior`);
|
|
279
|
+
_.merge(opts.chromeLoggingPrefs, opts[opt]);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const caps = /** @type {any} */ (createChromedriverCaps.bind(this)(opts, curDeviceId, details));
|
|
284
|
+
this.log.debug(
|
|
285
|
+
`Before starting chromedriver, androidPackage is '${caps.chromeOptions.androidPackage}'`,
|
|
286
|
+
);
|
|
287
|
+
const sessionCaps = await chromedriver.start(caps);
|
|
288
|
+
cacheChromedriverCaps.bind(this)(sessionCaps, context);
|
|
289
|
+
return chromedriver;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* @this {AndroidDriver}
|
|
294
|
+
* @template {Chromedriver} T
|
|
295
|
+
* @param {T} chromedriver
|
|
296
|
+
* @param {string} context
|
|
297
|
+
* @returns {Promise<T>}
|
|
298
|
+
*/
|
|
299
|
+
export async function setupExistingChromedriver(chromedriver, context) {
|
|
300
|
+
// check the status by sending a simple window-based command to ChromeDriver
|
|
301
|
+
// if there is an error, we want to recreate the ChromeDriver session
|
|
302
|
+
if (await chromedriver.hasWorkingWebview()) {
|
|
303
|
+
const cachedCaps = this._chromedriverCapsCache.get(context);
|
|
304
|
+
if (cachedCaps) {
|
|
305
|
+
cacheChromedriverCaps.bind(this)(cachedCaps, context);
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
this.log.debug('ChromeDriver is not associated with a window. Re-initializing the session.');
|
|
309
|
+
const sessionCaps = await chromedriver.restart();
|
|
310
|
+
cacheChromedriverCaps.bind(this)(sessionCaps, context);
|
|
311
|
+
}
|
|
312
|
+
return chromedriver;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* @this {AndroidDriver}
|
|
317
|
+
* @returns {boolean}
|
|
318
|
+
*/
|
|
319
|
+
export function shouldDismissChromeWelcome() {
|
|
320
|
+
return (
|
|
321
|
+
!!this.opts.chromeOptions &&
|
|
322
|
+
_.isArray(this.opts.chromeOptions.args) &&
|
|
323
|
+
this.opts.chromeOptions.args.includes('--no-first-run')
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* @this {AndroidDriver}
|
|
329
|
+
* @returns {Promise<void>}
|
|
330
|
+
*/
|
|
331
|
+
export async function dismissChromeWelcome() {
|
|
332
|
+
this.log.info('Trying to dismiss Chrome welcome');
|
|
333
|
+
let activity = await this.getCurrentActivity();
|
|
334
|
+
if (activity !== 'org.chromium.chrome.browser.firstrun.FirstRunActivity') {
|
|
335
|
+
this.log.info('Chrome welcome dialog never showed up! Continuing');
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
let el = await this.findElOrEls('id', 'com.android.chrome:id/terms_accept', false);
|
|
339
|
+
await this.click(/** @type {string} */ (el.ELEMENT));
|
|
340
|
+
try {
|
|
341
|
+
let el = await this.findElOrEls('id', 'com.android.chrome:id/negative_button', false);
|
|
342
|
+
await this.click(/** @type {string} */ (el.ELEMENT));
|
|
343
|
+
} catch (e) {
|
|
344
|
+
// DO NOTHING, THIS DEVICE DIDNT LAUNCH THE SIGNIN DIALOG
|
|
345
|
+
// IT MUST BE A NON GMS DEVICE
|
|
346
|
+
this.log.warn(
|
|
347
|
+
`This device did not show Chrome SignIn dialog, ${/** @type {Error} */ (e).message}`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// #endregion
|
|
353
|
+
// #region Internal Helper Functions
|
|
354
|
+
|
|
71
355
|
/**
|
|
72
356
|
* @returns {Promise<number>}
|
|
73
357
|
*/
|
|
@@ -130,23 +414,11 @@ async function cdpInfo(host, port) {
|
|
|
130
414
|
return cdpGetRequest(host, port, '/json/version');
|
|
131
415
|
}
|
|
132
416
|
|
|
133
|
-
/**
|
|
134
|
-
*
|
|
135
|
-
* @param {string} browser
|
|
136
|
-
* @returns {CHROME_BROWSER_PACKAGE_ACTIVITY[keyof CHROME_BROWSER_PACKAGE_ACTIVITY]}
|
|
137
|
-
*/
|
|
138
|
-
export function getChromePkg(browser) {
|
|
139
|
-
return (
|
|
140
|
-
CHROME_BROWSER_PACKAGE_ACTIVITY[browser.toLowerCase()] ||
|
|
141
|
-
CHROME_BROWSER_PACKAGE_ACTIVITY.default
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
417
|
/**
|
|
146
418
|
* Create Chromedriver capabilities based on the provided
|
|
147
419
|
* Appium capabilities
|
|
148
420
|
*
|
|
149
|
-
* @this {
|
|
421
|
+
* @this {AndroidDriver}
|
|
150
422
|
* @param {any} opts
|
|
151
423
|
* @param {string} deviceId
|
|
152
424
|
* @param {import('../types').WebViewDetails | null} [webViewDetails]
|
|
@@ -228,70 +500,34 @@ function createChromedriverCaps(opts, deviceId, webViewDetails) {
|
|
|
228
500
|
|
|
229
501
|
this.log.debug(
|
|
230
502
|
'Precalculated Chromedriver capabilities: ' + JSON.stringify(caps.chromeOptions, null, 2),
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
/** @type {string[]} */
|
|
234
|
-
const protectedCapNames = [];
|
|
235
|
-
for (const [opt, val] of _.toPairs(opts.chromeOptions)) {
|
|
236
|
-
if (_.isUndefined(caps.chromeOptions[opt])) {
|
|
237
|
-
caps.chromeOptions[opt] = val;
|
|
238
|
-
} else {
|
|
239
|
-
protectedCapNames.push(opt);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
if (!_.isEmpty(protectedCapNames)) {
|
|
243
|
-
this.log.info(
|
|
244
|
-
'The following Chromedriver capabilities cannot be overridden ' +
|
|
245
|
-
'by the provided chromeOptions:',
|
|
246
|
-
);
|
|
247
|
-
for (const optName of protectedCapNames) {
|
|
248
|
-
this.log.info(` ${optName} (${JSON.stringify(opts.chromeOptions[optName])})`);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return caps;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Parse webview names for getContexts
|
|
257
|
-
*
|
|
258
|
-
* @this {import('../../driver').AndroidDriver}
|
|
259
|
-
* @param {import('../types').WebviewsMapping[]} webviewsMapping
|
|
260
|
-
* @param {import('../types').GetWebviewsOpts} options
|
|
261
|
-
* @returns {string[]}
|
|
262
|
-
*/
|
|
263
|
-
export function parseWebviewNames(
|
|
264
|
-
webviewsMapping,
|
|
265
|
-
{ensureWebviewsHavePages = true, isChromeSession = false} = {},
|
|
266
|
-
) {
|
|
267
|
-
if (isChromeSession) {
|
|
268
|
-
return [CHROMIUM_WIN];
|
|
269
|
-
}
|
|
503
|
+
);
|
|
270
504
|
|
|
271
505
|
/** @type {string[]} */
|
|
272
|
-
const
|
|
273
|
-
for (const
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
);
|
|
279
|
-
continue;
|
|
506
|
+
const protectedCapNames = [];
|
|
507
|
+
for (const [opt, val] of _.toPairs(opts.chromeOptions)) {
|
|
508
|
+
if (_.isUndefined(caps.chromeOptions[opt])) {
|
|
509
|
+
caps.chromeOptions[opt] = val;
|
|
510
|
+
} else {
|
|
511
|
+
protectedCapNames.push(opt);
|
|
280
512
|
}
|
|
281
|
-
|
|
282
|
-
|
|
513
|
+
}
|
|
514
|
+
if (!_.isEmpty(protectedCapNames)) {
|
|
515
|
+
this.log.info(
|
|
516
|
+
'The following Chromedriver capabilities cannot be overridden ' +
|
|
517
|
+
'by the provided chromeOptions:',
|
|
518
|
+
);
|
|
519
|
+
for (const optName of protectedCapNames) {
|
|
520
|
+
this.log.info(` ${optName} (${JSON.stringify(opts.chromeOptions[optName])})`);
|
|
283
521
|
}
|
|
284
522
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
);
|
|
288
|
-
return result;
|
|
523
|
+
|
|
524
|
+
return caps;
|
|
289
525
|
}
|
|
290
526
|
|
|
291
527
|
/**
|
|
292
528
|
* Allocates a local port for devtools communication
|
|
293
529
|
*
|
|
294
|
-
* @this {
|
|
530
|
+
* @this {AndroidDriver}
|
|
295
531
|
* @param {string} socketName - The remote Unix socket name
|
|
296
532
|
* @param {number?} [webviewDevtoolsPort=null] - The local port number or null to apply
|
|
297
533
|
* autodetection
|
|
@@ -339,7 +575,7 @@ async function allocateDevtoolsChannel(socketName, webviewDevtoolsPort = null) {
|
|
|
339
575
|
* No error is thrown if CDP request fails - in such case no data will be
|
|
340
576
|
* recorded into the corresponding `webviewsMapping` item.
|
|
341
577
|
*
|
|
342
|
-
* @this {
|
|
578
|
+
* @this {AndroidDriver}
|
|
343
579
|
* @param {import('../types').WebviewProps[]} webviewsMapping The current webviews mapping
|
|
344
580
|
* !!! Each item of this array gets mutated (`info`/`pages` properties get added
|
|
345
581
|
* based on the provided `opts`) if the requested details have been
|
|
@@ -419,100 +655,13 @@ async function collectWebviewsDetails(webviewsMapping, opts = {}) {
|
|
|
419
655
|
this.log.debug(`CDP data collection completed`);
|
|
420
656
|
}
|
|
421
657
|
|
|
422
|
-
/**
|
|
423
|
-
* Get a list of available webviews mapping by introspecting processes with adb,
|
|
424
|
-
* where webviews are listed. It's possible to pass in a 'deviceSocket' arg, which
|
|
425
|
-
* limits the webview possibilities to the one running on the Chromium devtools
|
|
426
|
-
* socket we're interested in (see note on webviewsFromProcs). We can also
|
|
427
|
-
* direct this method to verify whether a particular webview process actually
|
|
428
|
-
* has any pages (if a process exists but no pages are found, Chromedriver will
|
|
429
|
-
* not actually be able to connect to it, so this serves as a guard for that
|
|
430
|
-
* strange failure mode). The strategy for checking whether any pages are
|
|
431
|
-
* active involves sending a request to the remote debug server on the device,
|
|
432
|
-
* hence it is also possible to specify the port on the host machine which
|
|
433
|
-
* should be used for this communication.
|
|
434
|
-
*
|
|
435
|
-
* @this {import('../../driver').AndroidDriver}
|
|
436
|
-
* @param {import('../types').GetWebviewsOpts} [opts={}]
|
|
437
|
-
* @returns {Promise<import('../types').WebviewsMapping[]>}
|
|
438
|
-
*/
|
|
439
|
-
export async function getWebViewsMapping({
|
|
440
|
-
androidDeviceSocket = null,
|
|
441
|
-
ensureWebviewsHavePages = true,
|
|
442
|
-
webviewDevtoolsPort = null,
|
|
443
|
-
enableWebviewDetailsCollection = true,
|
|
444
|
-
waitForWebviewMs = 0,
|
|
445
|
-
} = {}) {
|
|
446
|
-
this.log.debug(`Getting a list of available webviews`);
|
|
447
|
-
|
|
448
|
-
if (!_.isNumber(waitForWebviewMs)) {
|
|
449
|
-
waitForWebviewMs = parseInt(`${waitForWebviewMs}`, 10) || 0;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/** @type {import('../types').WebviewsMapping[]} */
|
|
453
|
-
let webviewsMapping;
|
|
454
|
-
const timer = new timing.Timer().start();
|
|
455
|
-
do {
|
|
456
|
-
webviewsMapping = await webviewsFromProcs.bind(this)(androidDeviceSocket);
|
|
457
|
-
|
|
458
|
-
if (webviewsMapping.length > 0) {
|
|
459
|
-
break;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
this.log.debug(`No webviews found in ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`);
|
|
463
|
-
await sleep(WEBVIEW_WAIT_INTERVAL_MS);
|
|
464
|
-
} while (timer.getDuration().asMilliSeconds < waitForWebviewMs);
|
|
465
|
-
|
|
466
|
-
await collectWebviewsDetails.bind(this)(webviewsMapping, {
|
|
467
|
-
ensureWebviewsHavePages,
|
|
468
|
-
enableWebviewDetailsCollection,
|
|
469
|
-
webviewDevtoolsPort,
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
for (const webviewMapping of webviewsMapping) {
|
|
473
|
-
const {webview, info} = webviewMapping;
|
|
474
|
-
webviewMapping.webviewName = null;
|
|
475
|
-
|
|
476
|
-
let wvName = webview;
|
|
477
|
-
/** @type {{name: string; id: string | null} | undefined} */
|
|
478
|
-
let process;
|
|
479
|
-
if (!androidDeviceSocket) {
|
|
480
|
-
const pkgMatch = WEBVIEW_PKG_PATTERN.exec(webview);
|
|
481
|
-
try {
|
|
482
|
-
// web view name could either be suffixed with PID or the package name
|
|
483
|
-
// package names could not start with a digit
|
|
484
|
-
const pkg = pkgMatch ? pkgMatch[1] : await procFromWebview.bind(this)(webview);
|
|
485
|
-
wvName = `${WEBVIEW_BASE}${pkg}`;
|
|
486
|
-
const pidMatch = WEBVIEW_PID_PATTERN.exec(webview);
|
|
487
|
-
process = {
|
|
488
|
-
name: pkg,
|
|
489
|
-
id: pidMatch ? pidMatch[1] : null,
|
|
490
|
-
};
|
|
491
|
-
} catch (e) {
|
|
492
|
-
this.log.debug(e.stack);
|
|
493
|
-
this.log.warn(e.message);
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
webviewMapping.webviewName = wvName;
|
|
499
|
-
const key = toDetailsCacheKey(this.adb, wvName);
|
|
500
|
-
if (info || process) {
|
|
501
|
-
WEBVIEWS_DETAILS_CACHE.set(key, {info, process});
|
|
502
|
-
} else if (WEBVIEWS_DETAILS_CACHE.has(key)) {
|
|
503
|
-
WEBVIEWS_DETAILS_CACHE.delete(key);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
return webviewsMapping;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
658
|
/**
|
|
510
659
|
* Take a webview name like WEBVIEW_4296 and use 'adb shell ps' to figure out
|
|
511
660
|
* which app package is associated with that webview. One of the reasons we
|
|
512
661
|
* want to do this is to make sure we're listing webviews for the actual AUT,
|
|
513
662
|
* not some other running app
|
|
514
663
|
*
|
|
515
|
-
* @this {
|
|
664
|
+
* @this {AndroidDriver}
|
|
516
665
|
* @param {string} webview
|
|
517
666
|
* @returns {Promise<string>}
|
|
518
667
|
*/
|
|
@@ -536,7 +685,7 @@ async function procFromWebview(webview) {
|
|
|
536
685
|
* See https://cs.chromium.org/chromium/src/chrome/browser/devtools/device/android_device_info_query.cc
|
|
537
686
|
* for more details
|
|
538
687
|
*
|
|
539
|
-
* @this {
|
|
688
|
+
* @this {AndroidDriver}
|
|
540
689
|
* @returns {Promise<string[]>} a list of matching webview socket names (including the leading '@')
|
|
541
690
|
*/
|
|
542
691
|
async function getPotentialWebviewProcs() {
|
|
@@ -585,7 +734,7 @@ async function getPotentialWebviewProcs() {
|
|
|
585
734
|
* that socket name (this is for apps which embed Chromium, which isn't the
|
|
586
735
|
* same as chrome-backed webviews).
|
|
587
736
|
*
|
|
588
|
-
* @this {
|
|
737
|
+
* @this {AndroidDriver}
|
|
589
738
|
* @param {string?} [deviceSocket=null] - the explictly-named device socket to use
|
|
590
739
|
* @returns {Promise<import('../types').WebviewProc[]>}
|
|
591
740
|
*/
|
|
@@ -626,7 +775,7 @@ async function webviewsFromProcs(deviceSocket = null) {
|
|
|
626
775
|
}
|
|
627
776
|
|
|
628
777
|
/**
|
|
629
|
-
* @this {
|
|
778
|
+
* @this {AndroidDriver}
|
|
630
779
|
* @param {import('../types').PortSpec} [portSpec]
|
|
631
780
|
* @returns {Promise<number>}
|
|
632
781
|
*/
|
|
@@ -674,7 +823,7 @@ async function getChromedriverPort(portSpec) {
|
|
|
674
823
|
}
|
|
675
824
|
|
|
676
825
|
/**
|
|
677
|
-
* @this {
|
|
826
|
+
* @this {AndroidDriver}
|
|
678
827
|
* @returns {boolean}
|
|
679
828
|
*/
|
|
680
829
|
function isChromedriverAutodownloadEnabled() {
|
|
@@ -689,167 +838,25 @@ function isChromedriverAutodownloadEnabled() {
|
|
|
689
838
|
}
|
|
690
839
|
|
|
691
840
|
/**
|
|
692
|
-
* @this {
|
|
693
|
-
* @param {import('../../driver').AndroidDriverOpts} opts
|
|
694
|
-
* @param {string} curDeviceId
|
|
695
|
-
* @param {string} [context]
|
|
696
|
-
* @returns {Promise<Chromedriver>}
|
|
697
|
-
*/
|
|
698
|
-
export async function setupNewChromedriver(opts, curDeviceId, context) {
|
|
699
|
-
// @ts-ignore TODO: Remove the legacy
|
|
700
|
-
if (opts.chromeDriverPort) {
|
|
701
|
-
this.log.warn(
|
|
702
|
-
`The 'chromeDriverPort' capability is deprecated. Please use 'chromedriverPort' instead`,
|
|
703
|
-
);
|
|
704
|
-
// @ts-ignore TODO: Remove the legacy
|
|
705
|
-
opts.chromedriverPort = opts.chromeDriverPort;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
if (opts.chromedriverPort) {
|
|
709
|
-
this.log.debug(`Using user-specified port ${opts.chromedriverPort} for chromedriver`);
|
|
710
|
-
} else {
|
|
711
|
-
// if a single port wasn't given, we'll look for a free one
|
|
712
|
-
opts.chromedriverPort = await getChromedriverPort.bind(this)(opts.chromedriverPorts);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
const details = context ? getWebviewDetails(this.adb, context) : undefined;
|
|
716
|
-
if (!_.isEmpty(details)) {
|
|
717
|
-
this.log.debug(
|
|
718
|
-
'Passing web view details to the Chromedriver constructor: ' +
|
|
719
|
-
JSON.stringify(details, null, 2),
|
|
720
|
-
);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
/** @type {import('appium-chromedriver').ChromedriverOpts} */
|
|
724
|
-
const chromedriverOpts = {
|
|
725
|
-
port: _.isNil(opts.chromedriverPort) ? undefined : String(opts.chromedriverPort),
|
|
726
|
-
executable: opts.chromedriverExecutable,
|
|
727
|
-
adb: this.adb,
|
|
728
|
-
cmdArgs: /** @type {string[] | undefined} */ (opts.chromedriverArgs),
|
|
729
|
-
verbose: !!opts.showChromedriverLog,
|
|
730
|
-
executableDir: opts.chromedriverExecutableDir,
|
|
731
|
-
mappingPath: opts.chromedriverChromeMappingFile,
|
|
732
|
-
// @ts-ignore this property exists
|
|
733
|
-
bundleId: opts.chromeBundleId,
|
|
734
|
-
useSystemExecutable: opts.chromedriverUseSystemExecutable,
|
|
735
|
-
disableBuildCheck: opts.chromedriverDisableBuildCheck,
|
|
736
|
-
// @ts-ignore this is ok
|
|
737
|
-
details,
|
|
738
|
-
isAutodownloadEnabled: isChromedriverAutodownloadEnabled.bind(this)(),
|
|
739
|
-
};
|
|
740
|
-
if (this.basePath) {
|
|
741
|
-
chromedriverOpts.reqBasePath = this.basePath;
|
|
742
|
-
}
|
|
743
|
-
const chromedriver = new Chromedriver(chromedriverOpts);
|
|
744
|
-
// make sure there are chromeOptions
|
|
745
|
-
opts.chromeOptions = opts.chromeOptions || {};
|
|
746
|
-
// try out any prefixed chromeOptions,
|
|
747
|
-
// and strip the prefix
|
|
748
|
-
for (const opt of _.keys(opts)) {
|
|
749
|
-
if (opt.endsWith(':chromeOptions')) {
|
|
750
|
-
this?.log?.warn(`Merging '${opt}' into 'chromeOptions'. This may cause unexpected behavior`);
|
|
751
|
-
_.merge(opts.chromeOptions, opts[opt]);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// Ensure there are logging preferences
|
|
756
|
-
opts.chromeLoggingPrefs = opts.chromeLoggingPrefs ?? {};
|
|
757
|
-
|
|
758
|
-
// Strip the prefix and store it
|
|
759
|
-
for (const opt of _.keys(opts)) {
|
|
760
|
-
if (opt.endsWith(':loggingPrefs')) {
|
|
761
|
-
this.log.warn(`Merging '${opt}' into 'chromeLoggingPrefs'. This may cause unexpected behavior`);
|
|
762
|
-
_.merge(opts.chromeLoggingPrefs, opts[opt]);
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
const caps = /** @type {any} */ (createChromedriverCaps.bind(this)(opts, curDeviceId, details));
|
|
767
|
-
this.log.debug(
|
|
768
|
-
`Before starting chromedriver, androidPackage is '${caps.chromeOptions.androidPackage}'`,
|
|
769
|
-
);
|
|
770
|
-
const sessionCaps = await chromedriver.start(caps);
|
|
771
|
-
updateBidiProxyUrl.bind(this)(sessionCaps, context);
|
|
772
|
-
return chromedriver;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
/**
|
|
776
|
-
* @this {import('../../driver').AndroidDriver}
|
|
777
|
-
* @template {Chromedriver} T
|
|
778
|
-
* @param {T} chromedriver
|
|
779
|
-
* @param {string} context
|
|
780
|
-
* @returns {Promise<T>}
|
|
781
|
-
*/
|
|
782
|
-
export async function setupExistingChromedriver(chromedriver, context) {
|
|
783
|
-
// check the status by sending a simple window-based command to ChromeDriver
|
|
784
|
-
// if there is an error, we want to recreate the ChromeDriver session
|
|
785
|
-
if (await chromedriver.hasWorkingWebview()) {
|
|
786
|
-
const cachedBidiProxyUrl = this._bidiProxyUrlCache.get(context);
|
|
787
|
-
if (cachedBidiProxyUrl) {
|
|
788
|
-
updateBidiProxyUrl.bind(this)({webSocketUrl: cachedBidiProxyUrl}, context);
|
|
789
|
-
}
|
|
790
|
-
} else {
|
|
791
|
-
this.log.debug('ChromeDriver is not associated with a window. Re-initializing the session.');
|
|
792
|
-
const sessionCaps = await chromedriver.restart();
|
|
793
|
-
updateBidiProxyUrl.bind(this)(sessionCaps, context);
|
|
794
|
-
}
|
|
795
|
-
return chromedriver;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
/**
|
|
799
|
-
* @this {import('../../driver').AndroidDriver}
|
|
841
|
+
* @this {AndroidDriver}
|
|
800
842
|
* @param {Record<string, any>} sessionCaps
|
|
801
843
|
* @param {string} context
|
|
802
844
|
* @returns {void}
|
|
803
845
|
*/
|
|
804
|
-
function
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
846
|
+
function cacheChromedriverCaps(sessionCaps, context) {
|
|
847
|
+
// Store full session capabilities in cache
|
|
848
|
+
this._chromedriverCapsCache.set(context, sessionCaps);
|
|
849
|
+
|
|
850
|
+
if (
|
|
851
|
+
this.opts?.chromedriverForwardBiDi
|
|
852
|
+
&& sessionCaps?.webSocketUrl
|
|
853
|
+
&& this._bidiProxyUrl !== sessionCaps.webSocketUrl
|
|
854
|
+
) {
|
|
811
855
|
this._bidiProxyUrl = sessionCaps.webSocketUrl;
|
|
812
856
|
this.log.debug(`Updated Bidi Proxy URL to ${this._bidiProxyUrl}`);
|
|
813
857
|
}
|
|
814
858
|
}
|
|
815
859
|
|
|
816
|
-
/**
|
|
817
|
-
* @this {import('../../driver').AndroidDriver}
|
|
818
|
-
* @returns {boolean}
|
|
819
|
-
*/
|
|
820
|
-
export function shouldDismissChromeWelcome() {
|
|
821
|
-
return (
|
|
822
|
-
!!this.opts.chromeOptions &&
|
|
823
|
-
_.isArray(this.opts.chromeOptions.args) &&
|
|
824
|
-
this.opts.chromeOptions.args.includes('--no-first-run')
|
|
825
|
-
);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
/**
|
|
829
|
-
* @this {import('../../driver').AndroidDriver}
|
|
830
|
-
* @returns {Promise<void>}
|
|
831
|
-
*/
|
|
832
|
-
export async function dismissChromeWelcome() {
|
|
833
|
-
this.log.info('Trying to dismiss Chrome welcome');
|
|
834
|
-
let activity = await this.getCurrentActivity();
|
|
835
|
-
if (activity !== 'org.chromium.chrome.browser.firstrun.FirstRunActivity') {
|
|
836
|
-
this.log.info('Chrome welcome dialog never showed up! Continuing');
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
let el = await this.findElOrEls('id', 'com.android.chrome:id/terms_accept', false);
|
|
840
|
-
await this.click(/** @type {string} */ (el.ELEMENT));
|
|
841
|
-
try {
|
|
842
|
-
let el = await this.findElOrEls('id', 'com.android.chrome:id/negative_button', false);
|
|
843
|
-
await this.click(/** @type {string} */ (el.ELEMENT));
|
|
844
|
-
} catch (e) {
|
|
845
|
-
// DO NOTHING, THIS DEVICE DIDNT LAUNCH THE SIGNIN DIALOG
|
|
846
|
-
// IT MUST BE A NON GMS DEVICE
|
|
847
|
-
this.log.warn(
|
|
848
|
-
`This device did not show Chrome SignIn dialog, ${/** @type {Error} */ (e).message}`,
|
|
849
|
-
);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
860
|
/**
|
|
854
861
|
* https://github.com/puppeteer/puppeteer/issues/2242#issuecomment-544219536
|
|
855
862
|
*
|
|
@@ -862,6 +869,9 @@ function isCompatibleCdpHost (host) {
|
|
|
862
869
|
|| Boolean(net.isIP(host));
|
|
863
870
|
}
|
|
864
871
|
|
|
872
|
+
// #endregion
|
|
873
|
+
|
|
865
874
|
/**
|
|
866
875
|
* @typedef {import('appium-adb').ADB} ADB
|
|
876
|
+
* @typedef {import('../../driver').AndroidDriver} AndroidDriver
|
|
867
877
|
*/
|