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.
@@ -0,0 +1,639 @@
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
+ import type {AndroidDriver, AndroidDriverOpts} from '../driver';
7
+ import type {AppState, TerminateAppOpts} from './types';
8
+ import type {UninstallOptions, InstallOptions} from 'appium-adb';
9
+
10
+ const APP_EXTENSIONS = ['.apk', '.apks'] as const;
11
+ const PACKAGE_INSTALL_TIMEOUT_MS = 90000;
12
+ // These constants are in sync with
13
+ // https://developer.apple.com/documentation/xctest/xcuiapplicationstate/xcuiapplicationstaterunningbackground?language=objc
14
+ export const APP_STATE = {
15
+ NOT_INSTALLED: 0,
16
+ NOT_RUNNING: 1,
17
+ RUNNING_IN_BACKGROUND: 3,
18
+ RUNNING_IN_FOREGROUND: 4,
19
+ } as const;
20
+
21
+ export interface IsAppInstalledOptions {
22
+ /**
23
+ * The user ID for which to check the package installation.
24
+ * The `current` user id is used by default.
25
+ */
26
+ user?: string;
27
+ }
28
+
29
+ /**
30
+ * Checks whether the specified application is installed on the device.
31
+ *
32
+ * @param appId The application package identifier to check.
33
+ * @param opts Optional parameters for the installation check.
34
+ * @returns `true` if the application is installed, `false` otherwise.
35
+ */
36
+ export async function isAppInstalled(
37
+ this: AndroidDriver,
38
+ appId: string,
39
+ opts: IsAppInstalledOptions = {},
40
+ ): Promise<boolean> {
41
+ return await this.adb.isAppInstalled(appId, opts);
42
+ }
43
+
44
+ /**
45
+ * Checks whether the specified application is installed on the device.
46
+ *
47
+ * @param appId Application package identifier
48
+ * @param user The user ID for which the package is installed.
49
+ * The `current` user id is used by default.
50
+ * @returns `true` if the application is installed, `false` otherwise.
51
+ */
52
+ export async function mobileIsAppInstalled(
53
+ this: AndroidDriver,
54
+ appId: string,
55
+ user?: string | number,
56
+ ): Promise<boolean> {
57
+ const _opts: IsAppInstalledOptions = {};
58
+ if (util.hasValue(user)) {
59
+ _opts.user = `${user}`;
60
+ }
61
+ return await this.isAppInstalled(appId, _opts);
62
+ }
63
+
64
+ /**
65
+ * Queries the current state of the specified application.
66
+ *
67
+ * The possible states are:
68
+ * - `APP_STATE.NOT_INSTALLED` (0): The application is not installed
69
+ * - `APP_STATE.NOT_RUNNING` (1): The application is installed but not running
70
+ * - `APP_STATE.RUNNING_IN_BACKGROUND` (3): The application is running in the background
71
+ * - `APP_STATE.RUNNING_IN_FOREGROUND` (4): The application is running in the foreground
72
+ *
73
+ * @param appId Application package identifier
74
+ * @returns The current state of the application as a numeric value.
75
+ */
76
+ export async function queryAppState(this: AndroidDriver, appId: string): Promise<AppState> {
77
+ this.log.info(`Querying the state of '${appId}'`);
78
+ if (!(await this.adb.isAppInstalled(appId))) {
79
+ return APP_STATE.NOT_INSTALLED;
80
+ }
81
+ if (!(await this.adb.isAppRunning(appId))) {
82
+ return APP_STATE.NOT_RUNNING;
83
+ }
84
+ const appIdRe = new RegExp(`\\b${_.escapeRegExp(appId)}/`);
85
+ for (const line of (await this.adb.dumpWindows()).split('\n')) {
86
+ if (appIdRe.test(line) && ['mCurrentFocus', 'mFocusedApp'].some((x) => line.includes(x))) {
87
+ return APP_STATE.RUNNING_IN_FOREGROUND;
88
+ }
89
+ }
90
+ return APP_STATE.RUNNING_IN_BACKGROUND;
91
+ }
92
+
93
+ /**
94
+ * Activates the specified application, bringing it to the foreground.
95
+ *
96
+ * This is equivalent to launching the application if it's not already running,
97
+ * or bringing it to the foreground if it's running in the background.
98
+ *
99
+ * @param appId Application package identifier
100
+ */
101
+ export async function activateApp(this: AndroidDriver, appId: string): Promise<void> {
102
+ return await this.adb.activateApp(appId);
103
+ }
104
+
105
+ /**
106
+ * Removes (uninstalls) the specified application from the device.
107
+ *
108
+ * @param appId The application package identifier to remove.
109
+ * @param opts Optional uninstall options. See {@link UninstallOptions} for available options.
110
+ * @returns `true` if the application was successfully uninstalled, `false` otherwise.
111
+ */
112
+ export async function removeApp(
113
+ this: AndroidDriver,
114
+ appId: string,
115
+ opts: Omit<UninstallOptions, 'appId'> = {},
116
+ ): Promise<boolean> {
117
+ return await this.adb.uninstallApk(appId, opts);
118
+ }
119
+
120
+ /**
121
+ * @param appId Application package identifier
122
+ * @param timeout The count of milliseconds to wait until the
123
+ * app is uninstalled.
124
+ * @param keepData Set to true in order to keep the
125
+ * application data and cache folders after uninstall.
126
+ * @param skipInstallCheck Whether to check if the app is installed prior to
127
+ * uninstalling it. By default this is checked.
128
+ */
129
+ export async function mobileRemoveApp(
130
+ this: AndroidDriver,
131
+ appId: string,
132
+ timeout?: number,
133
+ keepData?: boolean,
134
+ skipInstallCheck?: boolean,
135
+ ): Promise<boolean> {
136
+ return await this.removeApp(appId, {
137
+ timeout,
138
+ keepData,
139
+ skipInstallCheck,
140
+ });
141
+ }
142
+
143
+ /**
144
+ * Terminates the specified application.
145
+ *
146
+ * This method forcefully stops the application and waits for it to be terminated.
147
+ * It checks that all process IDs belonging to the application have been stopped.
148
+ *
149
+ * @param appId The application package identifier to terminate.
150
+ * @param options Optional termination options. See {@link TerminateAppOpts} for available options.
151
+ * @returns `true` if the application was successfully terminated, `false` if it was not running.
152
+ * @throws {Error} If the application is still running after the timeout period.
153
+ */
154
+ export async function terminateApp(
155
+ this: AndroidDriver,
156
+ appId: string,
157
+ options: TerminateAppOpts = {},
158
+ ): Promise<boolean> {
159
+ this.log.info(`Terminating '${appId}'`);
160
+ const pids = await this.adb.listAppProcessIds(appId);
161
+ if (_.isEmpty(pids)) {
162
+ this.log.info(`The app '${appId}' is not running`);
163
+ return false;
164
+ }
165
+ await this.adb.forceStop(appId);
166
+ const timeout =
167
+ util.hasValue(options.timeout) && !Number.isNaN(options.timeout)
168
+ ? parseInt(String(options.timeout), 10)
169
+ : 500;
170
+
171
+ if (timeout <= 0) {
172
+ this.log.info(
173
+ `'${appId}' has been terminated. Skipping checking of the application process state ` +
174
+ `since the timeout was set to ${timeout}ms`,
175
+ );
176
+ return true;
177
+ }
178
+
179
+ let currentPids: number[] = [];
180
+ try {
181
+ await waitForCondition(async () => {
182
+ if (await this.queryAppState(appId) <= APP_STATE.NOT_RUNNING) {
183
+ return true;
184
+ }
185
+ currentPids = await this.adb.listAppProcessIds(appId);
186
+ if (_.isEmpty(currentPids) || _.isEmpty(_.intersection(pids, currentPids))) {
187
+ this.log.info(
188
+ `The application '${appId}' was reported running, ` +
189
+ `although all process ids belonging to it have been changed: ` +
190
+ `(${JSON.stringify(pids)} -> ${JSON.stringify(currentPids)}). ` +
191
+ `Assuming the termination was successful.`
192
+ );
193
+ return true;
194
+ }
195
+ return false;
196
+ }, {
197
+ waitMs: timeout,
198
+ intervalMs: 100,
199
+ });
200
+ } catch {
201
+ if (!_.isEmpty(currentPids) && !_.isEmpty(_.difference(pids, currentPids))) {
202
+ this.log.warn(
203
+ `Some of processes belonging to the '${appId}' applcation are still running ` +
204
+ `after ${timeout}ms (${JSON.stringify(pids)} -> ${JSON.stringify(currentPids)})`
205
+ );
206
+ }
207
+ throw this.log.errorWithException(`'${appId}' is still running after ${timeout}ms timeout`);
208
+ }
209
+ this.log.info(`'${appId}' has been successfully terminated`);
210
+ return true;
211
+ }
212
+
213
+ /**
214
+ * Terminates the specified application.
215
+ *
216
+ * This method forcefully stops the application and waits for it to be terminated.
217
+ * It checks that all process IDs belonging to the application have been stopped.
218
+ *
219
+ * @param appId Application package identifier
220
+ * @param timeout The count of milliseconds to wait until the app is terminated.
221
+ * 500ms by default.
222
+ * @returns `true` if the application was successfully terminated, `false` if it was not running.
223
+ * @throws {Error} If the application is still running after the timeout period.
224
+ */
225
+ export async function mobileTerminateApp(
226
+ this: AndroidDriver,
227
+ appId: string,
228
+ timeout?: number | string,
229
+ ): Promise<boolean> {
230
+ return await this.terminateApp(appId, {
231
+ timeout,
232
+ });
233
+ }
234
+
235
+ /**
236
+ * Installs the specified application on the device.
237
+ *
238
+ * The application file will be configured and validated before installation.
239
+ * Supported file formats are: `.apk`, `.apks`.
240
+ *
241
+ * @param appPath The path to the application file to install.
242
+ * Can be a local file path or a URL.
243
+ * @param opts Optional installation options. See {@link InstallOptions} for available options.
244
+ */
245
+ export async function installApp(
246
+ this: AndroidDriver,
247
+ appPath: string,
248
+ opts: Omit<InstallOptions, 'appId'>,
249
+ ): Promise<void> {
250
+ const localPath = await this.helpers.configureApp(appPath, [...APP_EXTENSIONS]);
251
+ await this.adb.install(localPath, opts);
252
+ }
253
+
254
+ /**
255
+ * @param appPath
256
+ * @param checkVersion
257
+ * @param timeout The count of milliseconds to wait until the app is installed.
258
+ * 20000ms by default.
259
+ * @param allowTestPackages Set to true in order to allow test packages installation.
260
+ * `false` by default.
261
+ * @param useSdcard Set to true to install the app on sdcard instead of the device memory.
262
+ * `false` by default.
263
+ * @param grantPermissions Set to true in order to grant all the
264
+ * permissions requested in the application's manifest automatically after the installation is completed
265
+ * under Android 6+. `false` by default.
266
+ * @param replace Set it to false if you don't want the application to be upgraded/reinstalled
267
+ * if it is already present on the device. `true` by default.
268
+ * @param noIncremental Forcefully disables incremental installs if set to `true`.
269
+ * Read https://developer.android.com/preview/features#incremental for more details.
270
+ * `false` by default.
271
+ */
272
+ export async function mobileInstallApp(
273
+ this: AndroidDriver,
274
+ appPath: string,
275
+ checkVersion?: boolean,
276
+ timeout?: number,
277
+ allowTestPackages?: boolean,
278
+ useSdcard?: boolean,
279
+ grantPermissions?: boolean,
280
+ replace?: boolean,
281
+ noIncremental?: boolean,
282
+ ): Promise<void> {
283
+ const opts: Omit<InstallOptions, 'appId'> = {
284
+ timeout,
285
+ allowTestPackages,
286
+ useSdcard,
287
+ grantPermissions,
288
+ replace,
289
+ noIncremental,
290
+ };
291
+ if (checkVersion) {
292
+ const localPath = await this.helpers.configureApp(appPath, [...APP_EXTENSIONS]);
293
+ await this.adb.installOrUpgrade(localPath, null, {
294
+ ...opts,
295
+ enforceCurrentBuild: false,
296
+ });
297
+ return;
298
+ }
299
+
300
+ return await this.installApp(appPath, opts);
301
+ }
302
+
303
+ /**
304
+ * Clears the application data and cache for the specified application.
305
+ *
306
+ * This is equivalent to running `adb shell pm clear <appId>`.
307
+ * All user data, cache, and settings for the application will be removed.
308
+ *
309
+ * @param appId Application package identifier
310
+ */
311
+ export async function mobileClearApp(this: AndroidDriver, appId: string): Promise<void> {
312
+ await this.adb.clear(appId);
313
+ }
314
+
315
+ /**
316
+ * Retrieves the name of the currently focused activity.
317
+ *
318
+ * @returns The fully qualified name of the current activity (e.g., 'com.example.app.MainActivity').
319
+ */
320
+ export async function getCurrentActivity(this: AndroidDriver): Promise<string> {
321
+ return (await this.adb.getFocusedPackageAndActivity()).appActivity as string;
322
+ }
323
+
324
+ /**
325
+ * Retrieves the package name of the currently focused application.
326
+ *
327
+ * @returns The package identifier of the current application (e.g., 'com.example.app').
328
+ */
329
+ export async function getCurrentPackage(this: AndroidDriver): Promise<string> {
330
+ return (await this.adb.getFocusedPackageAndActivity()).appPackage as string;
331
+ }
332
+
333
+ /**
334
+ * Puts the application in the background for the specified duration.
335
+ *
336
+ * If a negative value is provided, the app will be sent to background and not restored.
337
+ * Otherwise, the app will be restored to the foreground after the specified duration.
338
+ *
339
+ * @param seconds The number of seconds to keep the app in the background.
340
+ * A negative value means to not restore the app after putting it to background.
341
+ * @returns `true` if the app was successfully restored, or the result of `startApp` if restoration was attempted.
342
+ */
343
+ export async function background(
344
+ this: AndroidDriver,
345
+ seconds: number,
346
+ ): Promise<string | true> {
347
+ if (seconds < 0) {
348
+ // if user passes in a negative seconds value, interpret that as the instruction
349
+ // to not bring the app back at all
350
+ await this.adb.goToHome();
351
+ return true;
352
+ }
353
+ const {appPackage, appActivity} = await this.adb.getFocusedPackageAndActivity();
354
+ await this.adb.goToHome();
355
+
356
+ // people can wait for a long time, so to be safe let's use the longSleep function and log
357
+ // progress periodically.
358
+ const sleepMs = seconds * 1000;
359
+ const thresholdMs = 30 * 1000; // use the spin-wait for anything over this threshold
360
+ // for our spin interval, use 1% of the total wait time, but nothing bigger than 30s
361
+ const intervalMs = _.min([30 * 1000, parseInt(String(sleepMs / 100), 10)]) || 1000;
362
+ const progressCb = ({elapsedMs, progress}: {elapsedMs: number; progress: number}) => {
363
+ const waitSecs = (elapsedMs / 1000).toFixed(0);
364
+ const progressPct = (progress * 100).toFixed(2);
365
+ this.log.debug(`Waited ${waitSecs}s so far (${progressPct}%)`);
366
+ };
367
+ await longSleep(sleepMs, {thresholdMs, intervalMs, progressCb});
368
+
369
+ let args: import('appium-adb').StartAppOptions;
370
+ if (this._cachedActivityArgs?.[`${appPackage}/${appActivity}`]) {
371
+ // the activity was started with `startActivity`, so use those args to restart
372
+ args = this._cachedActivityArgs[`${appPackage}/${appActivity}`];
373
+ } else {
374
+ try {
375
+ this.log.debug(`Activating app '${appPackage}' in order to restore it`);
376
+ await this.adb.activateApp(appPackage as string);
377
+ return true;
378
+ } catch {}
379
+ args =
380
+ (appPackage === this.opts.appPackage && appActivity === this.opts.appActivity) ||
381
+ (appPackage === this.opts.appWaitPackage &&
382
+ (this.opts.appWaitActivity || '').split(',').includes(String(appActivity)))
383
+ ? {
384
+ // the activity is the original session activity, so use the original args
385
+ pkg: this.opts.appPackage as string,
386
+ activity: this.opts.appActivity ?? undefined,
387
+ action: this.opts.intentAction,
388
+ category: this.opts.intentCategory,
389
+ flags: this.opts.intentFlags,
390
+ waitPkg: this.opts.appWaitPackage ?? undefined,
391
+ waitActivity: this.opts.appWaitActivity ?? undefined,
392
+ waitForLaunch: this.opts.appWaitForLaunch,
393
+ waitDuration: this.opts.appWaitDuration,
394
+ optionalIntentArguments: this.opts.optionalIntentArguments,
395
+ stopApp: false,
396
+ user: this.opts.userProfile,
397
+ }
398
+ : {
399
+ // the activity was started some other way, so use defaults
400
+ pkg: appPackage as string,
401
+ activity: appActivity ?? undefined,
402
+ waitPkg: appPackage ?? undefined,
403
+ waitActivity: appActivity ?? undefined,
404
+ stopApp: false,
405
+ };
406
+ }
407
+ args = _.pickBy(args, (value) => !_.isUndefined(value)) as import('appium-adb').StartAppOptions;
408
+ this.log.debug(`Bringing application back to foreground with arguments: ${JSON.stringify(args)}`);
409
+ return await this.adb.startApp(args);
410
+ }
411
+
412
+ /**
413
+ * Puts the app to background and waits the given number of seconds then restores the app
414
+ * if necessary. The call is blocking.
415
+ *
416
+ * @param seconds The amount of seconds to wait between putting the app to background and restoring it.
417
+ * Any negative value means to not restore the app after putting it to background.
418
+ */
419
+ export async function mobileBackgroundApp(
420
+ this: AndroidDriver,
421
+ seconds: number = -1,
422
+ ): Promise<void> {
423
+ await this.background(seconds);
424
+ }
425
+
426
+ /**
427
+ * Resets the Application Under Test (AUT).
428
+ *
429
+ * The reset behavior depends on the driver options:
430
+ * - If `fastReset` is enabled: Stops the app and clears its data
431
+ * - If `fullReset` is enabled: Uninstalls and reinstalls the app
432
+ * - If neither is enabled: Only stops the app
433
+ *
434
+ * @param opts Optional driver options. If not provided, uses the current session options.
435
+ * @throws {Error} If `appPackage` is not specified or if the app cannot be reset.
436
+ */
437
+ export async function resetAUT(
438
+ this: AndroidDriver,
439
+ opts: AndroidDriverOpts | null = null,
440
+ ): Promise<void> {
441
+ const {
442
+ app,
443
+ appPackage,
444
+ fastReset,
445
+ fullReset,
446
+ androidInstallTimeout = PACKAGE_INSTALL_TIMEOUT_MS,
447
+ autoGrantPermissions,
448
+ allowTestPackages,
449
+ } = opts ?? this.opts;
450
+
451
+ if (!appPackage) {
452
+ throw new Error("'appPackage' option is required");
453
+ }
454
+
455
+ const isInstalled = await this.adb.isAppInstalled(appPackage);
456
+
457
+ if (isInstalled) {
458
+ try {
459
+ await this.adb.forceStop(appPackage);
460
+ } catch {}
461
+ // fullReset has priority over fastReset
462
+ if (!fullReset && fastReset) {
463
+ const output = await this.adb.clear(appPackage);
464
+ if (_.isString(output) && output.toLowerCase().includes('failed')) {
465
+ throw new Error(
466
+ `Cannot clear the application data of '${appPackage}'. Original error: ${output}`,
467
+ );
468
+ }
469
+ // executing `shell pm clear` resets previously assigned application permissions as well
470
+ if (autoGrantPermissions) {
471
+ try {
472
+ await this.adb.grantAllPermissions(appPackage);
473
+ } catch (error) {
474
+ const err = error as Error;
475
+ this.log.error(`Unable to grant permissions requested. Original error: ${err.message}`);
476
+ }
477
+ }
478
+ this.log.debug(
479
+ `Performed fast reset on the installed '${appPackage}' application (stop and clear)`,
480
+ );
481
+ return;
482
+ }
483
+ }
484
+
485
+ if (!app) {
486
+ throw new Error(
487
+ `Either provide 'app' option to install '${appPackage}' or ` +
488
+ `consider setting 'noReset' to 'true' if '${appPackage}' is supposed to be preinstalled.`,
489
+ );
490
+ }
491
+
492
+ this.log.debug(`Running full reset on '${appPackage}' (reinstall)`);
493
+ if (isInstalled) {
494
+ await this.adb.uninstallApk(appPackage);
495
+ }
496
+ await this.adb.install(app, {
497
+ grantPermissions: autoGrantPermissions,
498
+ timeout: androidInstallTimeout,
499
+ allowTestPackages,
500
+ });
501
+ }
502
+
503
+ /**
504
+ * Installs the Application Under Test (AUT) on the device.
505
+ *
506
+ * If `fullReset` is enabled, this will perform a full reset (uninstall and reinstall).
507
+ * Otherwise, it will install or upgrade the app if needed. If `fastReset` is enabled
508
+ * and the app was already installed, it will perform a fast reset after installation.
509
+ *
510
+ * @param opts Optional driver options. If not provided, uses the current session options.
511
+ * @throws {Error} If `app` or `appPackage` options are not specified.
512
+ */
513
+ export async function installAUT(
514
+ this: AndroidDriver,
515
+ opts: AndroidDriverOpts | null = null,
516
+ ): Promise<void> {
517
+ const {
518
+ app,
519
+ appPackage,
520
+ fastReset,
521
+ fullReset,
522
+ androidInstallTimeout = PACKAGE_INSTALL_TIMEOUT_MS,
523
+ autoGrantPermissions,
524
+ allowTestPackages,
525
+ enforceAppInstall,
526
+ } = opts ?? this.opts;
527
+
528
+ if (!app || !appPackage) {
529
+ throw new Error("'app' and 'appPackage' options are required");
530
+ }
531
+
532
+ if (fullReset) {
533
+ await this.resetAUT(opts);
534
+ return;
535
+ }
536
+
537
+ const {appState, wasUninstalled} = await this.adb.installOrUpgrade(app, appPackage, {
538
+ grantPermissions: autoGrantPermissions,
539
+ timeout: androidInstallTimeout,
540
+ allowTestPackages,
541
+ enforceCurrentBuild: enforceAppInstall,
542
+ });
543
+
544
+ // There is no need to reset the newly installed app
545
+ const isInstalledOverExistingApp =
546
+ !wasUninstalled && appState !== this.adb.APP_INSTALL_STATE.NOT_INSTALLED;
547
+ if (fastReset && isInstalledOverExistingApp) {
548
+ this.log.info(`Performing fast reset on '${appPackage}'`);
549
+ await this.resetAUT(opts);
550
+ }
551
+ }
552
+
553
+ /**
554
+ * Installs multiple additional APK files on the device.
555
+ *
556
+ * All APKs are installed asynchronously in parallel. This is useful for installing
557
+ * dependencies or additional applications required for testing.
558
+ *
559
+ * @param otherApps An array of paths to APK files to install.
560
+ * Each path can be a local file path or a URL.
561
+ * @param opts Optional driver options. If not provided, uses the current session options.
562
+ */
563
+ export async function installOtherApks(
564
+ this: AndroidDriver,
565
+ otherApps: string[],
566
+ opts: AndroidDriverOpts | null = null,
567
+ ): Promise<void> {
568
+ const {
569
+ androidInstallTimeout = PACKAGE_INSTALL_TIMEOUT_MS,
570
+ autoGrantPermissions,
571
+ allowTestPackages,
572
+ } = opts ?? this.opts;
573
+
574
+ // Install all of the APK's asynchronously
575
+ await B.all(
576
+ otherApps.map((otherApp) => {
577
+ this.log.debug(`Installing app: ${otherApp}`);
578
+ return this.adb.installOrUpgrade(otherApp, undefined, {
579
+ grantPermissions: autoGrantPermissions,
580
+ timeout: androidInstallTimeout,
581
+ allowTestPackages,
582
+ });
583
+ }),
584
+ );
585
+ }
586
+
587
+ /**
588
+ * Uninstalls the specified packages from the device.
589
+ *
590
+ * If `appPackages` contains `'*'`, all third-party packages will be uninstalled
591
+ * (excluding packages in `filterPackages`).
592
+ *
593
+ * @param appPackages An array of package names to uninstall, or `['*']` to uninstall all third-party packages.
594
+ * @param filterPackages An array of package names to exclude from uninstallation.
595
+ * Only used when `appPackages` contains `'*'`.
596
+ */
597
+ export async function uninstallOtherPackages(
598
+ this: AndroidDriver,
599
+ appPackages: string[],
600
+ filterPackages: string[] = [],
601
+ ): Promise<void> {
602
+ if (appPackages.includes('*')) {
603
+ this.log.debug('Uninstall third party packages');
604
+ appPackages = await getThirdPartyPackages.bind(this)(filterPackages);
605
+ }
606
+
607
+ this.log.debug(`Uninstalling packages: ${appPackages}`);
608
+ await B.all(appPackages.map((appPackage) => this.adb.uninstallApk(appPackage)));
609
+ }
610
+
611
+ /**
612
+ * Retrieves a list of all third-party packages installed on the device.
613
+ *
614
+ * Third-party packages are those that are not part of the system installation.
615
+ * This is equivalent to running `adb shell pm list packages -3`.
616
+ *
617
+ * @param filterPackages An array of package names to exclude from the results.
618
+ * @returns An array of third-party package names, excluding those in `filterPackages`.
619
+ * Returns an empty array if the command fails.
620
+ */
621
+ export async function getThirdPartyPackages(
622
+ this: AndroidDriver,
623
+ filterPackages: string[] = [],
624
+ ): Promise<string[]> {
625
+ try {
626
+ const packagesString = await this.adb.shell(['pm', 'list', 'packages', '-3']);
627
+ const appPackagesArray = packagesString
628
+ .trim()
629
+ .replace(/package:/g, '')
630
+ .split(EOL);
631
+ this.log.debug(`'${appPackagesArray}' filtered with '${filterPackages}'`);
632
+ return _.difference(appPackagesArray, filterPackages);
633
+ } catch (err) {
634
+ const error = err as Error;
635
+ this.log.warn(`Unable to get packages with 'adb shell pm list packages -3': ${error.message}`);
636
+ return [];
637
+ }
638
+ }
639
+
@@ -1,7 +1,4 @@
1
1
  /* eslint-disable @typescript-eslint/no-unused-vars */
2
- /**
3
- * @module
4
- */
5
2
 
6
3
  import _ from 'lodash';
7
4
  import {errors, isErrorType} from 'appium/driver';
@@ -9,6 +6,16 @@ import type {AndroidDriver} from '../driver';
9
6
  import type {Element} from '@appium/types';
10
7
  import type {FindElementOpts} from './types';
11
8
 
9
+ /**
10
+ * @param strategy The element location strategy to use (e.g., 'id', 'xpath', 'class name').
11
+ * @param selector The selector value to search for.
12
+ * @param mult If `true`, searches for multiple elements; if `false`, searches for a single element.
13
+ * @param context The context (e.g., webview) in which to search. Defaults to empty string (native context).
14
+ * @returns If `mult` is `false`, returns a single `Element` object.
15
+ * If `mult` is `true`, returns an array of `Element` objects (may be empty).
16
+ * @throws {Error} If `selector` is not provided.
17
+ * @throws {errors.NoSuchElementError} If a single element search fails and no element is found.
18
+ */
12
19
  export async function findElOrEls(
13
20
  this: AndroidDriver,
14
21
  strategy: string,
@@ -88,6 +95,16 @@ export async function findElOrEls(
88
95
  return element as Element;
89
96
  }
90
97
 
98
+ /**
99
+ * Performs the actual element search operation.
100
+ *
101
+ * This is an abstract method that must be implemented by subclasses or specific
102
+ * context handlers (e.g., native context, webview context).
103
+ *
104
+ * @param params The search parameters containing strategy, selector, context, and multiple flag.
105
+ * @returns A single `Element` if `params.multiple` is `false`, or an array of `Element` objects if `true`.
106
+ * @throws {errors.NotImplementedError} This method must be implemented by the specific context handler.
107
+ */
91
108
  export async function doFindElementOrEls(
92
109
  this: AndroidDriver,
93
110
  params: FindElementOpts,