appium-adb 14.0.4 → 14.0.5
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 +6 -0
- package/build/lib/tools/app-commands.d.ts +125 -150
- package/build/lib/tools/app-commands.d.ts.map +1 -1
- package/build/lib/tools/app-commands.js +194 -234
- package/build/lib/tools/app-commands.js.map +1 -1
- package/lib/tools/{app-commands.js → app-commands.ts} +279 -294
- package/package.json +1 -1
|
@@ -40,7 +40,7 @@ const support_1 = require("@appium/support");
|
|
|
40
40
|
const logger_js_1 = require("../logger.js");
|
|
41
41
|
const asyncbox_1 = require("asyncbox");
|
|
42
42
|
const node_path_1 = __importDefault(require("node:path"));
|
|
43
|
-
|
|
43
|
+
// Constants
|
|
44
44
|
exports.APP_INSTALL_STATE = {
|
|
45
45
|
UNKNOWN: 'unknown',
|
|
46
46
|
NOT_INSTALLED: 'notInstalled',
|
|
@@ -57,13 +57,13 @@ const MIN_API_LEVEL_WITH_PERMS_SUPPORT = 23;
|
|
|
57
57
|
const RESOLVER_ACTIVITY_NAME = 'android/com.android.internal.app.ResolverActivity';
|
|
58
58
|
const MAIN_ACTION = 'android.intent.action.MAIN';
|
|
59
59
|
const LAUNCHER_CATEGORY = 'android.intent.category.LAUNCHER';
|
|
60
|
+
// Public methods
|
|
60
61
|
/**
|
|
61
62
|
* Verify whether the given argument is a
|
|
62
63
|
* valid class name.
|
|
63
64
|
*
|
|
64
|
-
* @
|
|
65
|
-
* @
|
|
66
|
-
* @return {boolean} The result of Regexp.exec operation
|
|
65
|
+
* @param classString - The actual class name to be verified.
|
|
66
|
+
* @returns The result of Regexp.exec operation
|
|
67
67
|
* or _null_ if no matches are found.
|
|
68
68
|
*/
|
|
69
69
|
function isValidClass(classString) {
|
|
@@ -75,10 +75,9 @@ function isValidClass(classString) {
|
|
|
75
75
|
* given package. It is expected the package is already installed on
|
|
76
76
|
* the device under test.
|
|
77
77
|
*
|
|
78
|
-
* @
|
|
79
|
-
* @param
|
|
80
|
-
* @
|
|
81
|
-
* @return {Promise<string>} Fully qualified name of the launchable activity
|
|
78
|
+
* @param pkg - The target package identifier
|
|
79
|
+
* @param opts - Options for resolving the activity
|
|
80
|
+
* @returns Fully qualified name of the launchable activity
|
|
82
81
|
* @throws {Error} If there was an error while resolving the activity name
|
|
83
82
|
*/
|
|
84
83
|
async function resolveLaunchableActivity(pkg, opts = {}) {
|
|
@@ -97,10 +96,10 @@ async function resolveLaunchableActivity(pkg, opts = {}) {
|
|
|
97
96
|
try {
|
|
98
97
|
const tmpApp = await this.pullApk(pkg, tmpRoot);
|
|
99
98
|
const { apkActivity } = await this.packageAndLaunchActivityFromManifest(tmpApp);
|
|
100
|
-
return
|
|
99
|
+
return apkActivity;
|
|
101
100
|
}
|
|
102
101
|
catch (e) {
|
|
103
|
-
const err =
|
|
102
|
+
const err = e;
|
|
104
103
|
logger_js_1.log.debug(err.stack);
|
|
105
104
|
logger_js_1.log.warn(`Unable to resolve the launchable activity of '${pkg}'. ` +
|
|
106
105
|
`The very first match of the dumpsys output is going to be used. ` +
|
|
@@ -123,30 +122,28 @@ async function resolveLaunchableActivity(pkg, opts = {}) {
|
|
|
123
122
|
}
|
|
124
123
|
/**
|
|
125
124
|
* Forcefully stops the app and puts it in the "stopped" state.
|
|
126
|
-
* Android treats a
|
|
125
|
+
* Android treats a "stopped" app as if it was never launched since boot:
|
|
127
126
|
* - It cannot receive broadcast intents (except for explicit ones).
|
|
128
127
|
* - Scheduled jobs, alarms, and services are cancelled.
|
|
129
|
-
* - The app won
|
|
130
|
-
* It
|
|
128
|
+
* - The app won't restart until the user explicitly launches it again.
|
|
129
|
+
* It's the same as when a user swipes an app away from Settings → Apps → Force Stop.
|
|
131
130
|
*
|
|
132
|
-
* @
|
|
133
|
-
* @
|
|
134
|
-
* @return {Promise<string>} The output of the corresponding adb command.
|
|
131
|
+
* @param pkg - The package name to be stopped.
|
|
132
|
+
* @returns The output of the corresponding adb command.
|
|
135
133
|
*/
|
|
136
134
|
async function forceStop(pkg) {
|
|
137
135
|
return await this.shell(['am', 'force-stop', pkg]);
|
|
138
136
|
}
|
|
139
137
|
/**
|
|
140
|
-
* Gracefully kills the app
|
|
138
|
+
* Gracefully kills the app's process, similar to how Android would do it
|
|
141
139
|
* automatically when low on memory.
|
|
142
|
-
* It only kills the process, without changing the app
|
|
140
|
+
* It only kills the process, without changing the app's "stopped" state.
|
|
143
141
|
* Background services or broadcast receivers may restart soon after,
|
|
144
142
|
* if they are still scheduled or registered.
|
|
145
143
|
* No data or state (like alarms, jobs, etc.) are cleared.
|
|
146
144
|
*
|
|
147
|
-
* @
|
|
148
|
-
* @
|
|
149
|
-
* @return {Promise<string>} The output of the corresponding adb command.
|
|
145
|
+
* @param pkg - The package name to be stopped.
|
|
146
|
+
* @returns The output of the corresponding adb command.
|
|
150
147
|
*/
|
|
151
148
|
async function killPackage(pkg) {
|
|
152
149
|
return await this.shell(['am', 'kill', pkg]);
|
|
@@ -155,9 +152,8 @@ async function killPackage(pkg) {
|
|
|
155
152
|
* Clear the user data of the particular application on the device
|
|
156
153
|
* under test.
|
|
157
154
|
*
|
|
158
|
-
* @
|
|
159
|
-
* @
|
|
160
|
-
* @return {Promise<string>} The output of the corresponding adb command.
|
|
155
|
+
* @param pkg - The package name to be cleared.
|
|
156
|
+
* @returns The output of the corresponding adb command.
|
|
161
157
|
*/
|
|
162
158
|
async function clear(pkg) {
|
|
163
159
|
return await this.shell(['pm', 'clear', pkg]);
|
|
@@ -167,9 +163,8 @@ async function clear(pkg) {
|
|
|
167
163
|
* This method is only useful on Android 6.0+ and for applications
|
|
168
164
|
* that support components-based permissions setting.
|
|
169
165
|
*
|
|
170
|
-
* @
|
|
171
|
-
* @param
|
|
172
|
-
* @param {string} [apk] - The path to the actual apk file.
|
|
166
|
+
* @param pkg - The package name to be processed.
|
|
167
|
+
* @param apk - The path to the actual apk file.
|
|
173
168
|
* @throws {Error} If there was an error while granting permissions
|
|
174
169
|
*/
|
|
175
170
|
async function grantAllPermissions(pkg, apk) {
|
|
@@ -225,9 +220,8 @@ async function grantAllPermissions(pkg, apk) {
|
|
|
225
220
|
* This call is more performant than `grantPermission` one, since it combines
|
|
226
221
|
* multiple `adb shell` calls into a single command.
|
|
227
222
|
*
|
|
228
|
-
* @
|
|
229
|
-
* @param
|
|
230
|
-
* @param {Array<string>} permissions - The list of permissions to be granted.
|
|
223
|
+
* @param pkg - The package name to be processed.
|
|
224
|
+
* @param permissions - The list of permissions to be granted.
|
|
231
225
|
* @throws {Error} If there was an error while changing permissions.
|
|
232
226
|
*/
|
|
233
227
|
async function grantPermissions(pkg, permissions) {
|
|
@@ -240,7 +234,7 @@ async function grantPermissions(pkg, permissions) {
|
|
|
240
234
|
await this.shellChunks((perm) => ['pm', 'grant', pkg, perm], permissions);
|
|
241
235
|
}
|
|
242
236
|
catch (e) {
|
|
243
|
-
const err =
|
|
237
|
+
const err = e;
|
|
244
238
|
if (!IGNORED_PERM_ERRORS.some((pattern) => pattern.test(err.stderr || err.message))) {
|
|
245
239
|
throw err;
|
|
246
240
|
}
|
|
@@ -249,9 +243,8 @@ async function grantPermissions(pkg, permissions) {
|
|
|
249
243
|
/**
|
|
250
244
|
* Grant single permission for the particular package.
|
|
251
245
|
*
|
|
252
|
-
* @
|
|
253
|
-
* @param
|
|
254
|
-
* @param {string} permission - The full name of the permission to be granted.
|
|
246
|
+
* @param pkg - The package name to be processed.
|
|
247
|
+
* @param permission - The full name of the permission to be granted.
|
|
255
248
|
* @throws {Error} If there was an error while changing permissions.
|
|
256
249
|
*/
|
|
257
250
|
async function grantPermission(pkg, permission) {
|
|
@@ -259,7 +252,7 @@ async function grantPermission(pkg, permission) {
|
|
|
259
252
|
await this.shell(['pm', 'grant', pkg, permission]);
|
|
260
253
|
}
|
|
261
254
|
catch (e) {
|
|
262
|
-
const err =
|
|
255
|
+
const err = e;
|
|
263
256
|
if (!NOT_CHANGEABLE_PERM_ERROR.test(err.stderr || err.message)) {
|
|
264
257
|
throw err;
|
|
265
258
|
}
|
|
@@ -268,9 +261,8 @@ async function grantPermission(pkg, permission) {
|
|
|
268
261
|
/**
|
|
269
262
|
* Revoke single permission from the particular package.
|
|
270
263
|
*
|
|
271
|
-
* @
|
|
272
|
-
* @param
|
|
273
|
-
* @param {string} permission - The full name of the permission to be revoked.
|
|
264
|
+
* @param pkg - The package name to be processed.
|
|
265
|
+
* @param permission - The full name of the permission to be revoked.
|
|
274
266
|
* @throws {Error} If there was an error while changing permissions.
|
|
275
267
|
*/
|
|
276
268
|
async function revokePermission(pkg, permission) {
|
|
@@ -278,7 +270,7 @@ async function revokePermission(pkg, permission) {
|
|
|
278
270
|
await this.shell(['pm', 'revoke', pkg, permission]);
|
|
279
271
|
}
|
|
280
272
|
catch (e) {
|
|
281
|
-
const err =
|
|
273
|
+
const err = e;
|
|
282
274
|
if (!NOT_CHANGEABLE_PERM_ERROR.test(err.stderr || err.message)) {
|
|
283
275
|
throw err;
|
|
284
276
|
}
|
|
@@ -287,11 +279,10 @@ async function revokePermission(pkg, permission) {
|
|
|
287
279
|
/**
|
|
288
280
|
* Retrieve the list of granted permissions for the particular package.
|
|
289
281
|
*
|
|
290
|
-
* @
|
|
291
|
-
* @param
|
|
292
|
-
* @param {string?} [cmdOutput=null] - Optional parameter containing command output of
|
|
282
|
+
* @param pkg - The package name to be processed.
|
|
283
|
+
* @param cmdOutput - Optional parameter containing command output of
|
|
293
284
|
* _dumpsys package_ command. It may speed up the method execution.
|
|
294
|
-
* @
|
|
285
|
+
* @returns The list of granted permissions or an empty list.
|
|
295
286
|
* @throws {Error} If there was an error while changing permissions.
|
|
296
287
|
*/
|
|
297
288
|
async function getGrantedPermissions(pkg, cmdOutput = null) {
|
|
@@ -302,11 +293,10 @@ async function getGrantedPermissions(pkg, cmdOutput = null) {
|
|
|
302
293
|
/**
|
|
303
294
|
* Retrieve the list of denied permissions for the particular package.
|
|
304
295
|
*
|
|
305
|
-
* @
|
|
306
|
-
* @param
|
|
307
|
-
* @param {string?} [cmdOutput=null] - Optional parameter containing command output of
|
|
296
|
+
* @param pkg - The package name to be processed.
|
|
297
|
+
* @param cmdOutput - Optional parameter containing command output of
|
|
308
298
|
* _dumpsys package_ command. It may speed up the method execution.
|
|
309
|
-
* @
|
|
299
|
+
* @returns The list of denied permissions or an empty list.
|
|
310
300
|
*/
|
|
311
301
|
async function getDeniedPermissions(pkg, cmdOutput = null) {
|
|
312
302
|
logger_js_1.log.debug('Retrieving denied permissions');
|
|
@@ -316,11 +306,10 @@ async function getDeniedPermissions(pkg, cmdOutput = null) {
|
|
|
316
306
|
/**
|
|
317
307
|
* Retrieve the list of requested permissions for the particular package.
|
|
318
308
|
*
|
|
319
|
-
* @
|
|
320
|
-
* @param
|
|
321
|
-
* @param {string?} [cmdOutput=null] - Optional parameter containing command output of
|
|
309
|
+
* @param pkg - The package name to be processed.
|
|
310
|
+
* @param cmdOutput - Optional parameter containing command output of
|
|
322
311
|
* _dumpsys package_ command. It may speed up the method execution.
|
|
323
|
-
* @
|
|
312
|
+
* @returns The list of requested permissions or an empty list.
|
|
324
313
|
*/
|
|
325
314
|
async function getReqPermissions(pkg, cmdOutput = null) {
|
|
326
315
|
logger_js_1.log.debug('Retrieving requested permissions');
|
|
@@ -330,8 +319,7 @@ async function getReqPermissions(pkg, cmdOutput = null) {
|
|
|
330
319
|
/**
|
|
331
320
|
* Stop the particular package if it is running and clears its application data.
|
|
332
321
|
*
|
|
333
|
-
* @
|
|
334
|
-
* @param {string} pkg - The package name to be processed.
|
|
322
|
+
* @param pkg - The package name to be processed.
|
|
335
323
|
*/
|
|
336
324
|
async function stopAndClear(pkg) {
|
|
337
325
|
try {
|
|
@@ -339,16 +327,15 @@ async function stopAndClear(pkg) {
|
|
|
339
327
|
await this.clear(pkg);
|
|
340
328
|
}
|
|
341
329
|
catch (e) {
|
|
342
|
-
const err =
|
|
330
|
+
const err = e;
|
|
343
331
|
throw new Error(`Cannot stop and clear ${pkg}. Original error: ${err.message}`);
|
|
344
332
|
}
|
|
345
333
|
}
|
|
346
334
|
/**
|
|
347
335
|
* Get the package info from the installed application.
|
|
348
336
|
*
|
|
349
|
-
* @
|
|
350
|
-
* @
|
|
351
|
-
* @return {Promise<import('./types').AppInfo>} The parsed application information.
|
|
337
|
+
* @param pkg - The name of the installed package.
|
|
338
|
+
* @returns The parsed application information.
|
|
352
339
|
*/
|
|
353
340
|
async function getPackageInfo(pkg) {
|
|
354
341
|
logger_js_1.log.debug(`Getting package info for '${pkg}'`);
|
|
@@ -358,8 +345,9 @@ async function getPackageInfo(pkg) {
|
|
|
358
345
|
stdout = await this.shell(['dumpsys', 'package', pkg]);
|
|
359
346
|
}
|
|
360
347
|
catch (err) {
|
|
361
|
-
|
|
362
|
-
logger_js_1.log.
|
|
348
|
+
const error = err;
|
|
349
|
+
logger_js_1.log.debug(error.stack);
|
|
350
|
+
logger_js_1.log.warn(`Got an unexpected error while dumping package info: ${error.message}`);
|
|
363
351
|
return result;
|
|
364
352
|
}
|
|
365
353
|
const installedPattern = new RegExp(`^\\s*Package\\s+\\[${lodash_1.default.escapeRegExp(pkg)}\\][^:]+:$`, 'm');
|
|
@@ -380,10 +368,9 @@ async function getPackageInfo(pkg) {
|
|
|
380
368
|
/**
|
|
381
369
|
* Fetches base.apk of the given package to the local file system
|
|
382
370
|
*
|
|
383
|
-
* @
|
|
384
|
-
* @param
|
|
385
|
-
* @
|
|
386
|
-
* @returns {Promise<string>} Full path to the downloaded file
|
|
371
|
+
* @param pkg - The package identifier (must be already installed on the device)
|
|
372
|
+
* @param tmpDir - The destination folder path
|
|
373
|
+
* @returns Full path to the downloaded file
|
|
387
374
|
* @throws {Error} If there was an error while fetching the .apk
|
|
388
375
|
*/
|
|
389
376
|
async function pullApk(pkg, tmpDir) {
|
|
@@ -403,8 +390,7 @@ async function pullApk(pkg, tmpDir) {
|
|
|
403
390
|
* The action literally simulates
|
|
404
391
|
* clicking the corresponding application icon on the dashboard.
|
|
405
392
|
*
|
|
406
|
-
* @
|
|
407
|
-
* @param {string} appId - Application package identifier
|
|
393
|
+
* @param appId - Application package identifier
|
|
408
394
|
* @throws {Error} If the app cannot be activated
|
|
409
395
|
*/
|
|
410
396
|
async function activateApp(appId) {
|
|
@@ -424,7 +410,8 @@ async function activateApp(appId) {
|
|
|
424
410
|
logger_js_1.log.debug(`Command stdout: ${output}`);
|
|
425
411
|
}
|
|
426
412
|
catch (e) {
|
|
427
|
-
|
|
413
|
+
const error = e;
|
|
414
|
+
throw logger_js_1.log.errorWithException(`Cannot activate '${appId}'. Original error: ${error.message}`);
|
|
428
415
|
}
|
|
429
416
|
if (output.includes('monkey aborted')) {
|
|
430
417
|
throw logger_js_1.log.errorWithException(`Cannot activate '${appId}'. Are you sure it is installed?`);
|
|
@@ -456,15 +443,13 @@ async function activateApp(appId) {
|
|
|
456
443
|
/**
|
|
457
444
|
* Check whether the particular package is present on the device under test.
|
|
458
445
|
*
|
|
459
|
-
* @
|
|
460
|
-
* @param
|
|
461
|
-
* @
|
|
462
|
-
* @return {Promise<boolean>} True if the package is installed.
|
|
446
|
+
* @param pkg - The name of the package to check.
|
|
447
|
+
* @param opts - Options for checking installation
|
|
448
|
+
* @returns True if the package is installed.
|
|
463
449
|
*/
|
|
464
450
|
async function isAppInstalled(pkg, opts = {}) {
|
|
465
451
|
const { user, } = opts;
|
|
466
452
|
logger_js_1.log.debug(`Getting install status for ${pkg}`);
|
|
467
|
-
/** @type {boolean} */
|
|
468
453
|
let isInstalled;
|
|
469
454
|
if (await this.getApiLevel() < 26) {
|
|
470
455
|
try {
|
|
@@ -485,14 +470,14 @@ async function isAppInstalled(pkg, opts = {}) {
|
|
|
485
470
|
if (support_1.util.hasValue(user)) {
|
|
486
471
|
cmd.push('--user', user);
|
|
487
472
|
}
|
|
488
|
-
/** @type {string} */
|
|
489
473
|
let stdout;
|
|
490
474
|
try {
|
|
491
475
|
stdout = await this.shell(cmd);
|
|
492
476
|
}
|
|
493
477
|
catch (e) {
|
|
478
|
+
const error = e;
|
|
494
479
|
// https://github.com/appium/appium-uiautomator2-driver/issues/810
|
|
495
|
-
if (lodash_1.default.includes(
|
|
480
|
+
if (lodash_1.default.includes(error.stderr || error.stdout || error.message, 'access user') && lodash_1.default.isEmpty(user)) {
|
|
496
481
|
stdout = await this.shell([...cmd, '--user', '0']);
|
|
497
482
|
}
|
|
498
483
|
else {
|
|
@@ -507,10 +492,9 @@ async function isAppInstalled(pkg, opts = {}) {
|
|
|
507
492
|
/**
|
|
508
493
|
* Start the particular URI on the device under test.
|
|
509
494
|
*
|
|
510
|
-
* @
|
|
511
|
-
* @param
|
|
512
|
-
* @param
|
|
513
|
-
* @param {import('./types').StartUriOptions} [opts={}]
|
|
495
|
+
* @param uri - The name of URI to start.
|
|
496
|
+
* @param pkg - The name of the package to start the URI with.
|
|
497
|
+
* @param opts - Options for starting the URI
|
|
514
498
|
*/
|
|
515
499
|
async function startUri(uri, pkg = null, opts = {}) {
|
|
516
500
|
const { waitForLaunch = true, } = opts;
|
|
@@ -538,51 +522,49 @@ async function startUri(uri, pkg = null, opts = {}) {
|
|
|
538
522
|
/**
|
|
539
523
|
* Start the particular package/activity on the device under test.
|
|
540
524
|
*
|
|
541
|
-
* @
|
|
542
|
-
* @
|
|
543
|
-
* @return {Promise<string>} The output of the corresponding adb command.
|
|
525
|
+
* @param startAppOptions - Startup options mapping.
|
|
526
|
+
* @returns The output of the corresponding adb command.
|
|
544
527
|
* @throws {Error} If there is an error while executing the activity
|
|
545
528
|
*/
|
|
546
529
|
async function startApp(startAppOptions) {
|
|
547
530
|
if (!startAppOptions.pkg || !(startAppOptions.activity || startAppOptions.action)) {
|
|
548
531
|
throw new Error('pkg, and activity or intent action, are required to start an application');
|
|
549
532
|
}
|
|
550
|
-
|
|
551
|
-
if (
|
|
552
|
-
|
|
533
|
+
const options = lodash_1.default.clone(startAppOptions);
|
|
534
|
+
if (options.activity) {
|
|
535
|
+
options.activity = options.activity.replace('$', '\\$');
|
|
553
536
|
}
|
|
554
537
|
// initializing defaults
|
|
555
|
-
lodash_1.default.defaults(
|
|
556
|
-
waitPkg:
|
|
538
|
+
lodash_1.default.defaults(options, {
|
|
539
|
+
waitPkg: options.pkg,
|
|
557
540
|
waitForLaunch: true,
|
|
558
541
|
waitActivity: false,
|
|
559
542
|
retry: true,
|
|
560
543
|
stopApp: true
|
|
561
544
|
});
|
|
562
545
|
// preventing null waitpkg
|
|
563
|
-
|
|
546
|
+
options.waitPkg = options.waitPkg || options.pkg;
|
|
564
547
|
const apiLevel = await this.getApiLevel();
|
|
565
|
-
const cmd = buildStartCmd(
|
|
566
|
-
const intentName = `${
|
|
567
|
-
? ' ' +
|
|
548
|
+
const cmd = buildStartCmd(options, apiLevel);
|
|
549
|
+
const intentName = `${options.action}${options.optionalIntentArguments
|
|
550
|
+
? ' ' + options.optionalIntentArguments
|
|
568
551
|
: ''}`;
|
|
569
552
|
try {
|
|
570
553
|
const shellOpts = {};
|
|
571
|
-
if (lodash_1.default.isInteger(
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
shellOpts.timeout = startAppOptions.waitDuration;
|
|
554
|
+
if (options.waitDuration !== undefined && lodash_1.default.isInteger(options.waitDuration)
|
|
555
|
+
&& options.waitDuration >= 0) {
|
|
556
|
+
shellOpts.timeout = options.waitDuration;
|
|
575
557
|
}
|
|
576
558
|
const stdout = await this.shell(cmd, shellOpts);
|
|
577
559
|
if (stdout.includes('Error: Activity class') && stdout.includes('does not exist')) {
|
|
578
|
-
if (
|
|
560
|
+
if (options.retry && options.activity && !options.activity.startsWith('.')) {
|
|
579
561
|
logger_js_1.log.debug(`We tried to start an activity that doesn't exist, ` +
|
|
580
|
-
`retrying with '.${
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
return await this.startApp(
|
|
562
|
+
`retrying with '.${options.activity}' activity name`);
|
|
563
|
+
options.activity = `.${options.activity}`;
|
|
564
|
+
options.retry = false;
|
|
565
|
+
return await this.startApp(options);
|
|
584
566
|
}
|
|
585
|
-
throw new Error(`Activity name '${
|
|
567
|
+
throw new Error(`Activity name '${options.activity}' used to start the app doesn't ` +
|
|
586
568
|
`exist or cannot be launched! Make sure it exists and is a launchable activity`);
|
|
587
569
|
}
|
|
588
570
|
else if (stdout.includes('Error: Intent does not match any activities')
|
|
@@ -592,25 +574,26 @@ async function startApp(startAppOptions) {
|
|
|
592
574
|
}
|
|
593
575
|
else if (stdout.includes('java.lang.SecurityException')) {
|
|
594
576
|
// if the app is disabled on a real device it will throw a security exception
|
|
595
|
-
throw new Error(`The permission to start '${
|
|
577
|
+
throw new Error(`The permission to start '${options.activity}' activity has been denied.` +
|
|
596
578
|
`Make sure the activity/package names are correct.`);
|
|
597
579
|
}
|
|
598
|
-
if (
|
|
599
|
-
await this.waitForActivity(
|
|
580
|
+
if (options.waitActivity) {
|
|
581
|
+
await this.waitForActivity(options.waitPkg, options.waitActivity, options.waitDuration);
|
|
600
582
|
}
|
|
601
583
|
return stdout;
|
|
602
584
|
}
|
|
603
585
|
catch (e) {
|
|
604
|
-
const
|
|
586
|
+
const error = e;
|
|
587
|
+
const appDescriptor = options.pkg || intentName;
|
|
605
588
|
throw new Error(`Cannot start the '${appDescriptor}' application. ` +
|
|
606
589
|
`Consider checking the driver's troubleshooting documentation. ` +
|
|
607
|
-
`Original error: ${
|
|
590
|
+
`Original error: ${error.message}`);
|
|
608
591
|
}
|
|
609
592
|
}
|
|
610
593
|
/**
|
|
611
594
|
* Helper method to call `adb dumpsys window windows/displays`
|
|
612
|
-
*
|
|
613
|
-
* @returns
|
|
595
|
+
*
|
|
596
|
+
* @returns The output of the dumpsys command
|
|
614
597
|
*/
|
|
615
598
|
async function dumpWindows() {
|
|
616
599
|
const apiLevel = await this.getApiLevel();
|
|
@@ -622,8 +605,7 @@ async function dumpWindows() {
|
|
|
622
605
|
/**
|
|
623
606
|
* Get the name of currently focused package and activity.
|
|
624
607
|
*
|
|
625
|
-
* @
|
|
626
|
-
* @return {Promise<import('./types').PackageActivityInfo>}
|
|
608
|
+
* @returns The focused package and activity information
|
|
627
609
|
* @throws {Error} If there is an error while parsing the data.
|
|
628
610
|
*/
|
|
629
611
|
async function getFocusedPackageAndActivity() {
|
|
@@ -633,18 +615,16 @@ async function getFocusedPackageAndActivity() {
|
|
|
633
615
|
stdout = await this.dumpWindows();
|
|
634
616
|
}
|
|
635
617
|
catch (e) {
|
|
636
|
-
|
|
618
|
+
const error = e;
|
|
619
|
+
throw new Error(`Could not retrieve the currently focused package and activity. Original error: ${error.message}`);
|
|
637
620
|
}
|
|
638
621
|
const nullFocusedAppRe = /^\s*mFocusedApp=null/m;
|
|
639
622
|
// https://regex101.com/r/xZ8vF7/1
|
|
640
623
|
const focusedAppRe = new RegExp('^\\s*mFocusedApp.+Record\\{.*\\s([^\\s\\/\\}]+)\\/([^\\s\\/\\}\\,]+)\\,?(\\s[^\\s\\/\\}]+)*\\}', 'mg');
|
|
641
624
|
const nullCurrentFocusRe = /^\s*mCurrentFocus=null/m;
|
|
642
625
|
const currentFocusAppRe = new RegExp('^\\s*mCurrentFocus.+\\{.+\\s([^\\s\\/]+)\\/([^\\s]+)\\b', 'mg');
|
|
643
|
-
/** @type {import('./types').PackageActivityInfo[]} */
|
|
644
626
|
const focusedAppCandidates = [];
|
|
645
|
-
/** @type {import('./types').PackageActivityInfo[]} */
|
|
646
627
|
const currentFocusAppCandidates = [];
|
|
647
|
-
/** @type {[import('./types').PackageActivityInfo[], RegExp][]} */
|
|
648
628
|
const pairs = [
|
|
649
629
|
[focusedAppCandidates, focusedAppRe],
|
|
650
630
|
[currentFocusAppCandidates, currentFocusAppRe]
|
|
@@ -688,24 +668,22 @@ async function getFocusedPackageAndActivity() {
|
|
|
688
668
|
/**
|
|
689
669
|
* Wait for the given activity to be focused/non-focused.
|
|
690
670
|
*
|
|
691
|
-
* @
|
|
692
|
-
* @param
|
|
693
|
-
* @param {string} activity - The name of the activity, belonging to that package,
|
|
671
|
+
* @param pkg - The name of the package to wait for.
|
|
672
|
+
* @param activity - The name of the activity, belonging to that package,
|
|
694
673
|
* to wait for.
|
|
695
|
-
* @param
|
|
674
|
+
* @param waitForStop - Whether to wait until the activity is focused (true)
|
|
696
675
|
* or is not focused (false).
|
|
697
|
-
* @param
|
|
698
|
-
* @throws {
|
|
676
|
+
* @param waitMs - Number of milliseconds to wait before timeout occurs.
|
|
677
|
+
* @throws {Error} If timeout happens.
|
|
699
678
|
*/
|
|
700
679
|
async function waitForActivityOrNot(pkg, activity, waitForStop, waitMs = 20000) {
|
|
701
680
|
if (!pkg || !activity) {
|
|
702
681
|
throw new Error('Package and activity required.');
|
|
703
682
|
}
|
|
704
|
-
const splitNames = (
|
|
683
|
+
const splitNames = (names) => names.split(',').map(lodash_1.default.trim);
|
|
705
684
|
const allPackages = splitNames(pkg);
|
|
706
685
|
const allActivities = splitNames(activity);
|
|
707
|
-
const toFullyQualifiedActivityName = (
|
|
708
|
-
/** @type {Set<string>} */
|
|
686
|
+
const toFullyQualifiedActivityName = (prefix, suffix) => `${prefix}${suffix}`.replace(/\/\.?/g, '.').replace(/\.{2,}/g, '.');
|
|
709
687
|
const possibleActivityNamesSet = new Set();
|
|
710
688
|
for (const oneActivity of allActivities) {
|
|
711
689
|
if (oneActivity.startsWith('.')) {
|
|
@@ -741,7 +719,8 @@ async function waitForActivityOrNot(pkg, activity, waitForStop, waitMs = 20000)
|
|
|
741
719
|
({ appPackage, appActivity } = await this.getFocusedPackageAndActivity());
|
|
742
720
|
}
|
|
743
721
|
catch (e) {
|
|
744
|
-
|
|
722
|
+
const error = e;
|
|
723
|
+
logger_js_1.log.debug(error.message);
|
|
745
724
|
return false;
|
|
746
725
|
}
|
|
747
726
|
if (appActivity && appPackage) {
|
|
@@ -772,12 +751,11 @@ async function waitForActivityOrNot(pkg, activity, waitForStop, waitMs = 20000)
|
|
|
772
751
|
/**
|
|
773
752
|
* Wait for the given activity to be focused
|
|
774
753
|
*
|
|
775
|
-
* @
|
|
776
|
-
* @param
|
|
777
|
-
* @param {string} act - The name of the activity, belonging to that package,
|
|
754
|
+
* @param pkg - The name of the package to wait for.
|
|
755
|
+
* @param act - The name of the activity, belonging to that package,
|
|
778
756
|
* to wait for.
|
|
779
|
-
* @param
|
|
780
|
-
* @throws {
|
|
757
|
+
* @param waitMs - Number of milliseconds to wait before timeout occurs.
|
|
758
|
+
* @throws {Error} If timeout happens.
|
|
781
759
|
*/
|
|
782
760
|
async function waitForActivity(pkg, act, waitMs = 20000) {
|
|
783
761
|
await this.waitForActivityOrNot(pkg, act, false, waitMs);
|
|
@@ -785,24 +763,22 @@ async function waitForActivity(pkg, act, waitMs = 20000) {
|
|
|
785
763
|
/**
|
|
786
764
|
* Wait for the given activity to be non-focused.
|
|
787
765
|
*
|
|
788
|
-
* @
|
|
789
|
-
* @param
|
|
790
|
-
* @param {string} act - The name of the activity, belonging to that package,
|
|
766
|
+
* @param pkg - The name of the package to wait for.
|
|
767
|
+
* @param act - The name of the activity, belonging to that package,
|
|
791
768
|
* to wait for.
|
|
792
|
-
* @param
|
|
793
|
-
* @throws {
|
|
769
|
+
* @param waitMs - Number of milliseconds to wait before timeout occurs.
|
|
770
|
+
* @throws {Error} If timeout happens.
|
|
794
771
|
*/
|
|
795
772
|
async function waitForNotActivity(pkg, act, waitMs = 20000) {
|
|
796
773
|
await this.waitForActivityOrNot(pkg, act, true, waitMs);
|
|
797
774
|
}
|
|
798
|
-
// #region Private functions
|
|
799
775
|
/**
|
|
800
776
|
* Builds command line representation for the given
|
|
801
777
|
* application startup options
|
|
802
778
|
*
|
|
803
|
-
* @param
|
|
804
|
-
* @param
|
|
805
|
-
* @returns
|
|
779
|
+
* @param startAppOptions - Application options mapping
|
|
780
|
+
* @param apiLevel - The actual OS API level
|
|
781
|
+
* @returns The actual command line array
|
|
806
782
|
*/
|
|
807
783
|
function buildStartCmd(startAppOptions, apiLevel) {
|
|
808
784
|
const { user, waitForLaunch, pkg, activity, action, category, stopApp, flags, optionalIntentArguments, } = startAppOptions;
|
|
@@ -833,66 +809,12 @@ function buildStartCmd(startAppOptions, apiLevel) {
|
|
|
833
809
|
}
|
|
834
810
|
return cmd;
|
|
835
811
|
}
|
|
836
|
-
/**
|
|
837
|
-
*
|
|
838
|
-
* @param {string} value expect optionalIntentArguments to be a single string of the form:
|
|
839
|
-
* "-flag key"
|
|
840
|
-
* "-flag key value"
|
|
841
|
-
* or a combination of these (e.g., "-flag1 key1 -flag2 key2 value2")
|
|
842
|
-
* @returns {string[]}
|
|
843
|
-
*/
|
|
844
|
-
function parseOptionalIntentArguments(value) {
|
|
845
|
-
// take a string and parse out the part before any spaces, and anything after
|
|
846
|
-
// the first space
|
|
847
|
-
/** @type {(str: string) => string[]} */
|
|
848
|
-
const parseKeyValue = (str) => {
|
|
849
|
-
str = str.trim();
|
|
850
|
-
const spacePos = str.indexOf(' ');
|
|
851
|
-
if (spacePos < 0) {
|
|
852
|
-
return str.length ? [str] : [];
|
|
853
|
-
}
|
|
854
|
-
else {
|
|
855
|
-
return [str.substring(0, spacePos).trim(), str.substring(spacePos + 1).trim()];
|
|
856
|
-
}
|
|
857
|
-
};
|
|
858
|
-
// cycle through the optionalIntentArguments and pull out the arguments
|
|
859
|
-
// add a space initially so flags can be distinguished from arguments that
|
|
860
|
-
// have internal hyphens
|
|
861
|
-
let optionalIntentArguments = ` ${value}`;
|
|
862
|
-
const re = / (-[^\s]+) (.+)/;
|
|
863
|
-
/** @type {string[]} */
|
|
864
|
-
const result = [];
|
|
865
|
-
while (true) {
|
|
866
|
-
const args = re.exec(optionalIntentArguments);
|
|
867
|
-
if (!args) {
|
|
868
|
-
if (optionalIntentArguments.length) {
|
|
869
|
-
// no more flags, so the remainder can be treated as 'key' or 'key value'
|
|
870
|
-
result.push(...parseKeyValue(optionalIntentArguments));
|
|
871
|
-
}
|
|
872
|
-
// we are done
|
|
873
|
-
return result;
|
|
874
|
-
}
|
|
875
|
-
// take the flag and see if it is at the beginning of the string
|
|
876
|
-
// if it is not, then it means we have been through already, and
|
|
877
|
-
// what is before the flag is the argument for the previous flag
|
|
878
|
-
const flag = args[1];
|
|
879
|
-
const flagPos = optionalIntentArguments.indexOf(flag);
|
|
880
|
-
if (flagPos !== 0) {
|
|
881
|
-
const prevArgs = optionalIntentArguments.substring(0, flagPos);
|
|
882
|
-
result.push(...parseKeyValue(prevArgs));
|
|
883
|
-
}
|
|
884
|
-
// add the flag, as there are no more earlier arguments
|
|
885
|
-
result.push(flag);
|
|
886
|
-
// make optionalIntentArguments hold the remainder
|
|
887
|
-
optionalIntentArguments = args[2];
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
812
|
/**
|
|
891
813
|
* Parses the name of launchable package activity
|
|
892
814
|
* from dumpsys output.
|
|
893
815
|
*
|
|
894
|
-
* @param
|
|
895
|
-
* @returns
|
|
816
|
+
* @param dumpsys - The actual dumpsys output
|
|
817
|
+
* @returns Either the fully qualified
|
|
896
818
|
* activity name as a single list item or an empty list if nothing could be parsed.
|
|
897
819
|
* In Android 6 and older there is no reliable way to determine
|
|
898
820
|
* the category name for the given activity, so this API just
|
|
@@ -968,39 +890,22 @@ function parseLaunchableActivityNames(dumpsys) {
|
|
|
968
890
|
/**
|
|
969
891
|
* Check if the given string is a valid component name
|
|
970
892
|
*
|
|
971
|
-
* @param
|
|
972
|
-
* @
|
|
893
|
+
* @param classString - The string to verify
|
|
894
|
+
* @returns The result of Regexp.exec operation
|
|
973
895
|
* or _null_ if no matches are found
|
|
974
896
|
*/
|
|
975
897
|
function matchComponentName(classString) {
|
|
976
898
|
// some.package/some.package.Activity
|
|
977
899
|
return /^[\p{L}0-9./_]+$/u.exec(classString);
|
|
978
900
|
}
|
|
979
|
-
/**
|
|
980
|
-
* Escapes special characters in command line arguments.
|
|
981
|
-
* This is needed to avoid possible issues with how system `spawn`
|
|
982
|
-
* call handles them.
|
|
983
|
-
* See https://discuss.appium.io/t/how-to-modify-wd-proxy-and-uiautomator2-source-code-to-support-unicode/33466
|
|
984
|
-
* for more details.
|
|
985
|
-
*
|
|
986
|
-
* @param {string} arg Non-escaped argument string
|
|
987
|
-
* @returns The escaped argument
|
|
988
|
-
*/
|
|
989
|
-
function escapeShellArg(arg) {
|
|
990
|
-
arg = `${arg}`;
|
|
991
|
-
if (support_1.system.isWindows()) {
|
|
992
|
-
return /[&|^\s]/.test(arg) ? `"${arg.replace(/"/g, '""')}"` : arg;
|
|
993
|
-
}
|
|
994
|
-
return arg.replace(/&/g, '\\&');
|
|
995
|
-
}
|
|
996
901
|
/**
|
|
997
902
|
* Retrieves the list of permission names encoded in `dumpsys package` command output.
|
|
998
903
|
*
|
|
999
|
-
* @param
|
|
1000
|
-
* @param
|
|
1001
|
-
* @param
|
|
904
|
+
* @param dumpsysOutput - The actual command output.
|
|
905
|
+
* @param groupNames - The list of group names to list permissions for.
|
|
906
|
+
* @param grantedState - The expected state of `granted` attribute to filter with.
|
|
1002
907
|
* No filtering is done if the parameter is not set.
|
|
1003
|
-
* @returns
|
|
908
|
+
* @returns The list of matched permission names or an empty list if no matches were found.
|
|
1004
909
|
*/
|
|
1005
910
|
function extractMatchingPermissions(dumpsysOutput, groupNames, grantedState = null) {
|
|
1006
911
|
const groupPatternByName = (groupName) => new RegExp(`^(\\s*${lodash_1.default.escapeRegExp(groupName)} permissions:[\\s\\S]+)`, 'm');
|
|
@@ -1047,9 +952,8 @@ function extractMatchingPermissions(dumpsysOutput, groupNames, grantedState = nu
|
|
|
1047
952
|
/**
|
|
1048
953
|
* Broadcast a message to the given intent.
|
|
1049
954
|
*
|
|
1050
|
-
* @
|
|
1051
|
-
* @
|
|
1052
|
-
* @throws {error} If intent name is not a valid class name.
|
|
955
|
+
* @param intent - The name of the intent to broadcast to.
|
|
956
|
+
* @throws {Error} If intent name is not a valid class name.
|
|
1053
957
|
*/
|
|
1054
958
|
async function broadcast(intent) {
|
|
1055
959
|
if (!this.isValidClass(intent)) {
|
|
@@ -1061,9 +965,8 @@ async function broadcast(intent) {
|
|
|
1061
965
|
/**
|
|
1062
966
|
* Get the list of process ids for the particular package on the device under test.
|
|
1063
967
|
*
|
|
1064
|
-
* @
|
|
1065
|
-
* @
|
|
1066
|
-
* @returns {Promise<number[]>} The list of matched process IDs or an empty list.
|
|
968
|
+
* @param pkg - The package name
|
|
969
|
+
* @returns The list of matched process IDs or an empty list.
|
|
1067
970
|
*/
|
|
1068
971
|
async function listAppProcessIds(pkg) {
|
|
1069
972
|
logger_js_1.log.debug(`Getting IDs of all '${pkg}' package`);
|
|
@@ -1079,24 +982,81 @@ async function listAppProcessIds(pkg) {
|
|
|
1079
982
|
* Check whether the process with the particular name is running on the device
|
|
1080
983
|
* under test.
|
|
1081
984
|
*
|
|
1082
|
-
* @
|
|
1083
|
-
* @param {string} pkg - The id of the package to be checked.
|
|
985
|
+
* @param pkg - The id of the package to be checked.
|
|
1084
986
|
* @returns True if the given package is running.
|
|
1085
987
|
*/
|
|
1086
988
|
async function isAppRunning(pkg) {
|
|
1087
989
|
return !lodash_1.default.isEmpty(await this.listAppProcessIds(pkg));
|
|
1088
990
|
}
|
|
991
|
+
// Private methods
|
|
1089
992
|
/**
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1092
|
-
* @
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
* @
|
|
1097
|
-
* @property {boolean} [stopApp]
|
|
1098
|
-
* @property {string} [flags]
|
|
1099
|
-
* @property {string} [optionalIntentArguments]
|
|
993
|
+
* Parses optional intent arguments from a string.
|
|
994
|
+
*
|
|
995
|
+
* @param value - Expect optionalIntentArguments to be a single string of the form:
|
|
996
|
+
* "-flag key"
|
|
997
|
+
* "-flag key value"
|
|
998
|
+
* or a combination of these (e.g., "-flag1 key1 -flag2 key2 value2")
|
|
999
|
+
* @returns Parsed arguments array
|
|
1100
1000
|
*/
|
|
1101
|
-
|
|
1001
|
+
function parseOptionalIntentArguments(value) {
|
|
1002
|
+
// take a string and parse out the part before any spaces, and anything after
|
|
1003
|
+
// the first space
|
|
1004
|
+
const parseKeyValue = (str) => {
|
|
1005
|
+
str = str.trim();
|
|
1006
|
+
const spacePos = str.indexOf(' ');
|
|
1007
|
+
if (spacePos < 0) {
|
|
1008
|
+
return str.length ? [str] : [];
|
|
1009
|
+
}
|
|
1010
|
+
else {
|
|
1011
|
+
return [str.substring(0, spacePos).trim(), str.substring(spacePos + 1).trim()];
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
// cycle through the optionalIntentArguments and pull out the arguments
|
|
1015
|
+
// add a space initially so flags can be distinguished from arguments that
|
|
1016
|
+
// have internal hyphens
|
|
1017
|
+
let optionalIntentArguments = ` ${value}`;
|
|
1018
|
+
const re = / (-[^\s]+) (.+)/;
|
|
1019
|
+
const result = [];
|
|
1020
|
+
while (true) {
|
|
1021
|
+
const args = re.exec(optionalIntentArguments);
|
|
1022
|
+
if (!args) {
|
|
1023
|
+
if (optionalIntentArguments.length) {
|
|
1024
|
+
// no more flags, so the remainder can be treated as 'key' or 'key value'
|
|
1025
|
+
result.push(...parseKeyValue(optionalIntentArguments));
|
|
1026
|
+
}
|
|
1027
|
+
// we are done
|
|
1028
|
+
return result;
|
|
1029
|
+
}
|
|
1030
|
+
// take the flag and see if it is at the beginning of the string
|
|
1031
|
+
// if it is not, then it means we have been through already, and
|
|
1032
|
+
// what is before the flag is the argument for the previous flag
|
|
1033
|
+
const flag = args[1];
|
|
1034
|
+
const flagPos = optionalIntentArguments.indexOf(flag);
|
|
1035
|
+
if (flagPos !== 0) {
|
|
1036
|
+
const prevArgs = optionalIntentArguments.substring(0, flagPos);
|
|
1037
|
+
result.push(...parseKeyValue(prevArgs));
|
|
1038
|
+
}
|
|
1039
|
+
// add the flag, as there are no more earlier arguments
|
|
1040
|
+
result.push(flag);
|
|
1041
|
+
// make optionalIntentArguments hold the remainder
|
|
1042
|
+
optionalIntentArguments = args[2];
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Escapes special characters in command line arguments.
|
|
1047
|
+
* This is needed to avoid possible issues with how system `spawn`
|
|
1048
|
+
* call handles them.
|
|
1049
|
+
* See https://discuss.appium.io/t/how-to-modify-wd-proxy-and-uiautomator2-source-code-to-support-unicode/33466
|
|
1050
|
+
* for more details.
|
|
1051
|
+
*
|
|
1052
|
+
* @param arg - Non-escaped argument string
|
|
1053
|
+
* @returns The escaped argument
|
|
1054
|
+
*/
|
|
1055
|
+
function escapeShellArg(arg) {
|
|
1056
|
+
arg = `${arg}`;
|
|
1057
|
+
if (support_1.system.isWindows()) {
|
|
1058
|
+
return /[&|^\s]/.test(arg) ? `"${arg.replace(/"/g, '""')}"` : arg;
|
|
1059
|
+
}
|
|
1060
|
+
return arg.replace(/&/g, '\\&');
|
|
1061
|
+
}
|
|
1102
1062
|
//# sourceMappingURL=app-commands.js.map
|