appium-android-driver 12.4.5 → 12.4.7

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.
@@ -1,533 +0,0 @@
1
- import {util} from '@appium/support';
2
- import {waitForCondition, longSleep} from 'asyncbox';
3
- import _ from 'lodash';
4
- import {EOL} from 'node:os';
5
- import B from 'bluebird';
6
-
7
- const APP_EXTENSIONS = ['.apk', '.apks'];
8
- const PACKAGE_INSTALL_TIMEOUT_MS = 90000;
9
- // These constants are in sync with
10
- // https://developer.apple.com/documentation/xctest/xcuiapplicationstate/xcuiapplicationstaterunningbackground?language=objc
11
- export const APP_STATE = /** @type {const} */ ({
12
- NOT_INSTALLED: 0,
13
- NOT_RUNNING: 1,
14
- RUNNING_IN_BACKGROUND: 3,
15
- RUNNING_IN_FOREGROUND: 4,
16
- });
17
-
18
- /**
19
- * @typedef {Object} IsAppInstalledOptions
20
- * @property {string} [user] - The user id
21
- */
22
-
23
- /**
24
- * @this {AndroidDriver}
25
- * @param {string} appId
26
- * @param {IsAppInstalledOptions} [opts={}]
27
- * @returns {Promise<boolean>}
28
- */
29
- export async function isAppInstalled(appId, opts = {}) {
30
- return await this.adb.isAppInstalled(appId, opts);
31
- }
32
-
33
- /**
34
- * @this {AndroidDriver}
35
- * @param {string} appId Application package identifier
36
- * @param {string | number} [user] The user ID for which the package is installed.
37
- * The `current` user id is used by default.
38
- * @returns {Promise<boolean>}
39
- */
40
- export async function mobileIsAppInstalled(appId, user) {
41
- const _opts = {};
42
- if (util.hasValue(user)) {
43
- _opts.user = `${user}`;
44
- }
45
- return await this.isAppInstalled(appId, _opts);
46
- }
47
-
48
- /**
49
- * @this {AndroidDriver}
50
- * @param {string} appId Application package identifier
51
- * @returns {Promise<import('./types').AppState>}
52
- */
53
- export async function queryAppState(appId) {
54
- this.log.info(`Querying the state of '${appId}'`);
55
- if (!(await this.adb.isAppInstalled(appId))) {
56
- return APP_STATE.NOT_INSTALLED;
57
- }
58
- if (!(await this.adb.isAppRunning(appId))) {
59
- return APP_STATE.NOT_RUNNING;
60
- }
61
- const appIdRe = new RegExp(`\\b${_.escapeRegExp(appId)}/`);
62
- for (const line of (await this.adb.dumpWindows()).split('\n')) {
63
- if (appIdRe.test(line) && ['mCurrentFocus', 'mFocusedApp'].some((x) => line.includes(x))) {
64
- return APP_STATE.RUNNING_IN_FOREGROUND;
65
- }
66
- }
67
- return APP_STATE.RUNNING_IN_BACKGROUND;
68
- }
69
-
70
- /**
71
- * @this {AndroidDriver}
72
- * @param {string} appId Application package identifier
73
- * @returns {Promise<void>}
74
- */
75
- export async function activateApp(appId) {
76
- return await this.adb.activateApp(appId);
77
- }
78
-
79
- /**
80
- * @this {AndroidDriver}
81
- * @param {string} appId
82
- * @param {Omit<import('appium-adb').UninstallOptions, 'appId'>} opts
83
- * @returns {Promise<boolean>}
84
- */
85
- export async function removeApp(appId, opts = {}) {
86
- return await this.adb.uninstallApk(appId, opts);
87
- }
88
-
89
- /**
90
- * @this {import('../driver').AndroidDriver}
91
- * @param {string} appId Application package identifier
92
- * @param {number} [timeout] The count of milliseconds to wait until the
93
- * app is uninstalled.
94
- * @param {boolean} [keepData] Set to true in order to keep the
95
- * application data and cache folders after uninstall.
96
- * @param {boolean} [skipInstallCheck] Whether to check if the app is installed prior to
97
- * uninstalling it. By default this is checked.
98
- * @returns {Promise<boolean>}
99
- */
100
- export async function mobileRemoveApp(appId, timeout, keepData, skipInstallCheck) {
101
- return await this.removeApp(appId, {
102
- timeout,
103
- keepData,
104
- skipInstallCheck,
105
- });
106
- }
107
-
108
- /**
109
- * @this {AndroidDriver}
110
- * @param {string} appId
111
- * @param {import('./types').TerminateAppOpts} [options={}]
112
- * @returns {Promise<boolean>}
113
- */
114
- export async function terminateApp(appId, options = {}) {
115
- this.log.info(`Terminating '${appId}'`);
116
- const pids = await this.adb.listAppProcessIds(appId);
117
- if (_.isEmpty(pids)) {
118
- this.log.info(`The app '${appId}' is not running`);
119
- return false;
120
- }
121
- await this.adb.forceStop(appId);
122
- const timeout =
123
- util.hasValue(options.timeout) && !Number.isNaN(options.timeout)
124
- ? parseInt(String(options.timeout), 10)
125
- : 500;
126
-
127
- if (timeout <= 0) {
128
- this.log.info(
129
- `'${appId}' has been terminated. Skipping checking of the application process state ` +
130
- `since the timeout was set to ${timeout}ms`,
131
- );
132
- return true;
133
- }
134
-
135
- /** @type {number[]} */
136
- let currentPids = [];
137
- try {
138
- await waitForCondition(async () => {
139
- if (await this.queryAppState(appId) <= APP_STATE.NOT_RUNNING) {
140
- return true;
141
- }
142
- currentPids = await this.adb.listAppProcessIds(appId);
143
- if (_.isEmpty(currentPids) || _.isEmpty(_.intersection(pids, currentPids))) {
144
- this.log.info(
145
- `The application '${appId}' was reported running, ` +
146
- `although all process ids belonging to it have been changed: ` +
147
- `(${JSON.stringify(pids)} -> ${JSON.stringify(currentPids)}). ` +
148
- `Assuming the termination was successful.`
149
- );
150
- return true;
151
- }
152
- return false;
153
- }, {
154
- waitMs: timeout,
155
- intervalMs: 100,
156
- });
157
- } catch {
158
- if (!_.isEmpty(currentPids) && !_.isEmpty(_.difference(pids, currentPids))) {
159
- this.log.warn(
160
- `Some of processes belonging to the '${appId}' applcation are still running ` +
161
- `after ${timeout}ms (${JSON.stringify(pids)} -> ${JSON.stringify(currentPids)})`
162
- );
163
- }
164
- throw this.log.errorWithException(`'${appId}' is still running after ${timeout}ms timeout`);
165
- }
166
- this.log.info(`'${appId}' has been successfully terminated`);
167
- return true;
168
- }
169
-
170
- /**
171
- * @this {AndroidDriver}
172
- * @param {string} appId Application package identifier
173
- * @param {number|string} [timeout] The count of milliseconds to wait until the app is terminated.
174
- * 500ms by default.
175
- * @returns {Promise<boolean>}
176
- */
177
- export async function mobileTerminateApp(appId, timeout) {
178
- return await this.terminateApp(appId, {
179
- timeout,
180
- });
181
- }
182
-
183
- /**
184
- * @this {AndroidDriver}
185
- * @param {string} appPath
186
- * @param {Omit<import('appium-adb').InstallOptions, 'appId'>} opts
187
- * @returns {Promise<void>}
188
- */
189
- export async function installApp(appPath, opts) {
190
- const localPath = await this.helpers.configureApp(appPath, APP_EXTENSIONS);
191
- await this.adb.install(localPath, opts);
192
- }
193
-
194
- /**
195
- * @this {AndroidDriver}
196
- * @param {string} appPath
197
- * @param {boolean} [checkVersion]
198
- * @param {number} [timeout] The count of milliseconds to wait until the app is installed.
199
- * 20000ms by default.
200
- * @param {boolean} [allowTestPackages] Set to true in order to allow test packages installation.
201
- * `false` by default.
202
- * @param {boolean} [useSdcard] Set to true to install the app on sdcard instead of the device memory.
203
- * `false` by default.
204
- * @param {boolean} [grantPermissions] Set to true in order to grant all the
205
- * permissions requested in the application's manifest automatically after the installation is completed
206
- * under Android 6+. `false` by default.
207
- * @param {boolean} [replace] Set it to false if you don't want the application to be upgraded/reinstalled
208
- * if it is already present on the device. `true` by default.
209
- * @param {boolean} [noIncremental] Forcefully disables incremental installs if set to `true`.
210
- * Read https://developer.android.com/preview/features#incremental for more details.
211
- * `false` by default.
212
- * @returns {Promise<void>}
213
- */
214
- export async function mobileInstallApp(
215
- appPath,
216
- checkVersion,
217
- timeout,
218
- allowTestPackages,
219
- useSdcard,
220
- grantPermissions,
221
- replace,
222
- noIncremental,
223
- ) {
224
- const opts = {
225
- timeout,
226
- allowTestPackages,
227
- useSdcard,
228
- grantPermissions,
229
- replace,
230
- noIncremental,
231
- };
232
- if (checkVersion) {
233
- const localPath = await this.helpers.configureApp(appPath, APP_EXTENSIONS);
234
- await this.adb.installOrUpgrade(localPath, null, {
235
- ...opts,
236
- enforceCurrentBuild: false,
237
- });
238
- return;
239
- }
240
-
241
- return await this.installApp(appPath, opts);
242
- }
243
-
244
- /**
245
- * @this {AndroidDriver}
246
- * @param {string} appId Application package identifier
247
- * @returns {Promise<void>}
248
- */
249
- export async function mobileClearApp(appId) {
250
- await this.adb.clear(appId);
251
- }
252
-
253
- /**
254
- * @this {AndroidDriver}
255
- * @returns {Promise<string>}
256
- */
257
- export async function getCurrentActivity() {
258
- return /** @type {string} */ ((await this.adb.getFocusedPackageAndActivity()).appActivity);
259
- }
260
-
261
- /**
262
- * @this {AndroidDriver}
263
- * @returns {Promise<string>}
264
- */
265
- export async function getCurrentPackage() {
266
- return /** @type {string} */ ((await this.adb.getFocusedPackageAndActivity()).appPackage);
267
- }
268
-
269
- /**
270
- * @this {AndroidDriver}
271
- * @param {number} seconds
272
- * @returns {Promise<string|true>}
273
- */
274
- export async function background(seconds) {
275
- if (seconds < 0) {
276
- // if user passes in a negative seconds value, interpret that as the instruction
277
- // to not bring the app back at all
278
- await this.adb.goToHome();
279
- return true;
280
- }
281
- let {appPackage, appActivity} = await this.adb.getFocusedPackageAndActivity();
282
- await this.adb.goToHome();
283
-
284
- // people can wait for a long time, so to be safe let's use the longSleep function and log
285
- // progress periodically.
286
- const sleepMs = seconds * 1000;
287
- const thresholdMs = 30 * 1000; // use the spin-wait for anything over this threshold
288
- // for our spin interval, use 1% of the total wait time, but nothing bigger than 30s
289
- const intervalMs = _.min([30 * 1000, parseInt(String(sleepMs / 100), 10)]);
290
- /**
291
- *
292
- * @param {{elapsedMs: number, progress: number}} param0
293
- */
294
- const progressCb = ({elapsedMs, progress}) => {
295
- const waitSecs = (elapsedMs / 1000).toFixed(0);
296
- const progressPct = (progress * 100).toFixed(2);
297
- this.log.debug(`Waited ${waitSecs}s so far (${progressPct}%)`);
298
- };
299
- await longSleep(sleepMs, {thresholdMs, intervalMs, progressCb});
300
-
301
- /** @type {import('appium-adb').StartAppOptions} */
302
- let args;
303
- if (this._cachedActivityArgs?.[`${appPackage}/${appActivity}`]) {
304
- // the activity was started with `startActivity`, so use those args to restart
305
- args = this._cachedActivityArgs[`${appPackage}/${appActivity}`];
306
- } else {
307
- try {
308
- this.log.debug(`Activating app '${appPackage}' in order to restore it`);
309
- await this.adb.activateApp(/** @type {string} */ (appPackage));
310
- return true;
311
- } catch {}
312
- args =
313
- (appPackage === this.opts.appPackage && appActivity === this.opts.appActivity) ||
314
- (appPackage === this.opts.appWaitPackage &&
315
- (this.opts.appWaitActivity || '').split(',').includes(String(appActivity)))
316
- ? {
317
- // the activity is the original session activity, so use the original args
318
- pkg: /** @type {string} */ (this.opts.appPackage),
319
- activity: this.opts.appActivity ?? undefined,
320
- action: this.opts.intentAction,
321
- category: this.opts.intentCategory,
322
- flags: this.opts.intentFlags,
323
- waitPkg: this.opts.appWaitPackage ?? undefined,
324
- waitActivity: this.opts.appWaitActivity ?? undefined,
325
- waitForLaunch: this.opts.appWaitForLaunch,
326
- waitDuration: this.opts.appWaitDuration,
327
- optionalIntentArguments: this.opts.optionalIntentArguments,
328
- stopApp: false,
329
- user: this.opts.userProfile,
330
- }
331
- : {
332
- // the activity was started some other way, so use defaults
333
- pkg: /** @type {string} */ (appPackage),
334
- activity: appActivity ?? undefined,
335
- waitPkg: appPackage ?? undefined,
336
- waitActivity: appActivity ?? undefined,
337
- stopApp: false,
338
- };
339
- }
340
- args = /** @type {import('appium-adb').StartAppOptions} */ (
341
- _.pickBy(args, (value) => !_.isUndefined(value))
342
- );
343
- this.log.debug(`Bringing application back to foreground with arguments: ${JSON.stringify(args)}`);
344
- return await this.adb.startApp(args);
345
- }
346
-
347
- /**
348
- * Puts the app to background and waits the given number of seconds then restores the app
349
- * if necessary. The call is blocking.
350
- *
351
- * @this {AndroidDriver}
352
- * @param {number} [seconds=-1] The amount of seconds to wait between putting the app to background and restoring it.
353
- * Any negative value means to not restore the app after putting it to background.
354
- * @returns {Promise<void>}
355
- */
356
- export async function mobileBackgroundApp (seconds = -1) {
357
- await this.background(seconds);
358
- }
359
-
360
- /**
361
- * @this {AndroidDriver}
362
- * @param {import('../driver').AndroidDriverOpts?} [opts=null]
363
- * @returns {Promise<void>}
364
- */
365
- export async function resetAUT(opts = null) {
366
- const {
367
- app,
368
- appPackage,
369
- fastReset,
370
- fullReset,
371
- androidInstallTimeout = PACKAGE_INSTALL_TIMEOUT_MS,
372
- autoGrantPermissions,
373
- allowTestPackages,
374
- } = opts ?? this.opts;
375
-
376
- if (!appPackage) {
377
- throw new Error("'appPackage' option is required");
378
- }
379
-
380
- const isInstalled = await this.adb.isAppInstalled(appPackage);
381
-
382
- if (isInstalled) {
383
- try {
384
- await this.adb.forceStop(appPackage);
385
- } catch {}
386
- // fullReset has priority over fastReset
387
- if (!fullReset && fastReset) {
388
- const output = await this.adb.clear(appPackage);
389
- if (_.isString(output) && output.toLowerCase().includes('failed')) {
390
- throw new Error(
391
- `Cannot clear the application data of '${appPackage}'. Original error: ${output}`,
392
- );
393
- }
394
- // executing `shell pm clear` resets previously assigned application permissions as well
395
- if (autoGrantPermissions) {
396
- try {
397
- await this.adb.grantAllPermissions(appPackage);
398
- } catch (error) {
399
- this.log.error(`Unable to grant permissions requested. Original error: ${error.message}`);
400
- }
401
- }
402
- this.log.debug(
403
- `Performed fast reset on the installed '${appPackage}' application (stop and clear)`,
404
- );
405
- return;
406
- }
407
- }
408
-
409
- if (!app) {
410
- throw new Error(
411
- `Either provide 'app' option to install '${appPackage}' or ` +
412
- `consider setting 'noReset' to 'true' if '${appPackage}' is supposed to be preinstalled.`,
413
- );
414
- }
415
-
416
- this.log.debug(`Running full reset on '${appPackage}' (reinstall)`);
417
- if (isInstalled) {
418
- await this.adb.uninstallApk(appPackage);
419
- }
420
- await this.adb.install(app, {
421
- grantPermissions: autoGrantPermissions,
422
- timeout: androidInstallTimeout,
423
- allowTestPackages,
424
- });
425
- }
426
-
427
- /**
428
- * @this {AndroidDriver}
429
- * @param {import('../driver').AndroidDriverOpts?} [opts=null]
430
- * @returns {Promise<void>}
431
- */
432
- export async function installAUT(opts = null) {
433
- const {
434
- app,
435
- appPackage,
436
- fastReset,
437
- fullReset,
438
- androidInstallTimeout = PACKAGE_INSTALL_TIMEOUT_MS,
439
- autoGrantPermissions,
440
- allowTestPackages,
441
- enforceAppInstall,
442
- } = opts ?? this.opts;
443
-
444
- if (!app || !appPackage) {
445
- throw new Error("'app' and 'appPackage' options are required");
446
- }
447
-
448
- if (fullReset) {
449
- await this.resetAUT(opts);
450
- return;
451
- }
452
-
453
- const {appState, wasUninstalled} = await this.adb.installOrUpgrade(app, appPackage, {
454
- grantPermissions: autoGrantPermissions,
455
- timeout: androidInstallTimeout,
456
- allowTestPackages,
457
- enforceCurrentBuild: enforceAppInstall,
458
- });
459
-
460
- // There is no need to reset the newly installed app
461
- const isInstalledOverExistingApp =
462
- !wasUninstalled && appState !== this.adb.APP_INSTALL_STATE.NOT_INSTALLED;
463
- if (fastReset && isInstalledOverExistingApp) {
464
- this.log.info(`Performing fast reset on '${appPackage}'`);
465
- await this.resetAUT(opts);
466
- }
467
- }
468
-
469
- /**
470
- * @this {AndroidDriver}
471
- * @param {string[]} otherApps
472
- * @param {import('../driver').AndroidDriverOpts?} [opts=null]
473
- * @returns {Promise<void>}
474
- */
475
- export async function installOtherApks(otherApps, opts = null) {
476
- const {
477
- androidInstallTimeout = PACKAGE_INSTALL_TIMEOUT_MS,
478
- autoGrantPermissions,
479
- allowTestPackages,
480
- } = opts ?? this.opts;
481
-
482
- // Install all of the APK's asynchronously
483
- await B.all(
484
- otherApps.map((otherApp) => {
485
- this.log.debug(`Installing app: ${otherApp}`);
486
- return this.adb.installOrUpgrade(otherApp, undefined, {
487
- grantPermissions: autoGrantPermissions,
488
- timeout: androidInstallTimeout,
489
- allowTestPackages,
490
- });
491
- }),
492
- );
493
- }
494
-
495
- /**
496
- * @this {AndroidDriver}
497
- * @param {string[]} appPackages
498
- * @param {string[]} [filterPackages=[]]
499
- * @returns {Promise<void>}
500
- */
501
- export async function uninstallOtherPackages(appPackages, filterPackages = []) {
502
- if (appPackages.includes('*')) {
503
- this.log.debug('Uninstall third party packages');
504
- appPackages = await getThirdPartyPackages.bind(this)(filterPackages);
505
- }
506
-
507
- this.log.debug(`Uninstalling packages: ${appPackages}`);
508
- await B.all(appPackages.map((appPackage) => this.adb.uninstallApk(appPackage)));
509
- }
510
-
511
- /**
512
- * @this {AndroidDriver}
513
- * @param {string[]} [filterPackages=[]]
514
- * @returns {Promise<string[]>}
515
- */
516
- export async function getThirdPartyPackages(filterPackages = []) {
517
- try {
518
- const packagesString = await this.adb.shell(['pm', 'list', 'packages', '-3']);
519
- const appPackagesArray = packagesString
520
- .trim()
521
- .replace(/package:/g, '')
522
- .split(EOL);
523
- this.log.debug(`'${appPackagesArray}' filtered with '${filterPackages}'`);
524
- return _.difference(appPackagesArray, filterPackages);
525
- } catch (err) {
526
- this.log.warn(`Unable to get packages with 'adb shell pm list packages -3': ${err.message}`);
527
- return [];
528
- }
529
- }
530
-
531
- /**
532
- * @typedef {import('../driver').AndroidDriver} AndroidDriver
533
- */