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.
- package/CHANGELOG.md +12 -0
- package/build/lib/commands/app-management.d.ts +167 -113
- package/build/lib/commands/app-management.d.ts.map +1 -1
- package/build/lib/commands/app-management.js +147 -103
- package/build/lib/commands/app-management.js.map +1 -1
- package/build/lib/commands/find.d.ts +20 -3
- package/build/lib/commands/find.d.ts.map +1 -1
- package/build/lib/commands/find.js +10 -3
- package/build/lib/commands/find.js.map +1 -1
- package/build/lib/commands/performance.d.ts +80 -51
- package/build/lib/commands/performance.d.ts.map +1 -1
- package/build/lib/commands/performance.js +113 -89
- package/build/lib/commands/performance.js.map +1 -1
- package/build/lib/commands/shell.d.ts +21 -0
- package/build/lib/commands/shell.d.ts.map +1 -1
- package/build/lib/commands/shell.js +21 -0
- package/build/lib/commands/shell.js.map +1 -1
- package/build/lib/commands/streamscreen.d.ts +34 -64
- package/build/lib/commands/streamscreen.d.ts.map +1 -1
- package/build/lib/commands/streamscreen.js +41 -80
- package/build/lib/commands/streamscreen.js.map +1 -1
- package/lib/commands/app-management.ts +639 -0
- package/lib/commands/find.ts +20 -3
- package/lib/commands/{performance.js → performance.ts} +167 -108
- package/lib/commands/shell.ts +21 -0
- package/lib/commands/{streamscreen.js → streamscreen.ts} +86 -109
- package/package.json +2 -2
- package/lib/commands/app-management.js +0 -533
|
@@ -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
|
+
|
package/lib/commands/find.ts
CHANGED
|
@@ -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,
|