appium-android-driver 13.3.2 → 13.3.4
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.map +1 -1
- package/build/lib/commands/app-management.js +13 -12
- package/build/lib/commands/app-management.js.map +1 -1
- package/build/lib/commands/bidi/models.d.ts.map +1 -1
- package/build/lib/commands/bidi/models.js +1 -5
- package/build/lib/commands/bidi/models.js.map +1 -1
- package/build/lib/commands/bluetooth.d.ts.map +1 -1
- package/build/lib/commands/bluetooth.js +1 -5
- package/build/lib/commands/bluetooth.js.map +1 -1
- package/build/lib/commands/context/exports.d.ts.map +1 -1
- package/build/lib/commands/context/exports.js +11 -15
- package/build/lib/commands/context/exports.js.map +1 -1
- package/build/lib/commands/context/helpers.d.ts.map +1 -1
- package/build/lib/commands/context/helpers.js +43 -22
- package/build/lib/commands/context/helpers.js.map +1 -1
- package/build/lib/commands/device/common.d.ts.map +1 -1
- package/build/lib/commands/device/common.js +9 -8
- package/build/lib/commands/device/common.js.map +1 -1
- package/build/lib/commands/device/utils.d.ts.map +1 -1
- package/build/lib/commands/device/utils.js +2 -6
- package/build/lib/commands/device/utils.js.map +1 -1
- package/build/lib/commands/deviceidle.d.ts.map +1 -1
- package/build/lib/commands/deviceidle.js +2 -6
- package/build/lib/commands/deviceidle.js.map +1 -1
- package/build/lib/commands/execute.d.ts.map +1 -1
- package/build/lib/commands/execute.js +2 -6
- package/build/lib/commands/execute.js.map +1 -1
- package/build/lib/commands/file-actions.d.ts.map +1 -1
- package/build/lib/commands/file-actions.js +2 -3
- package/build/lib/commands/file-actions.js.map +1 -1
- package/build/lib/commands/find.d.ts.map +1 -1
- package/build/lib/commands/find.js +4 -7
- package/build/lib/commands/find.js.map +1 -1
- package/build/lib/commands/geolocation.d.ts.map +1 -1
- package/build/lib/commands/geolocation.js +3 -4
- package/build/lib/commands/geolocation.js.map +1 -1
- package/build/lib/commands/image-injection.d.ts.map +1 -1
- package/build/lib/commands/image-injection.js +1 -2
- package/build/lib/commands/image-injection.js.map +1 -1
- package/build/lib/commands/intent.d.ts.map +1 -1
- package/build/lib/commands/intent.js +14 -18
- package/build/lib/commands/intent.js.map +1 -1
- package/build/lib/commands/keyboard.d.ts.map +1 -1
- package/build/lib/commands/keyboard.js +6 -6
- package/build/lib/commands/keyboard.js.map +1 -1
- package/build/lib/commands/lock/exports.d.ts.map +1 -1
- package/build/lib/commands/lock/exports.js +1 -5
- package/build/lib/commands/lock/exports.js.map +1 -1
- package/build/lib/commands/lock/helpers.d.ts.map +1 -1
- package/build/lib/commands/lock/helpers.js +5 -9
- package/build/lib/commands/lock/helpers.js.map +1 -1
- package/build/lib/commands/log.d.ts.map +1 -1
- package/build/lib/commands/log.js +9 -9
- package/build/lib/commands/log.js.map +1 -1
- package/build/lib/commands/media-projection.d.ts.map +1 -1
- package/build/lib/commands/media-projection.js +3 -4
- package/build/lib/commands/media-projection.js.map +1 -1
- package/build/lib/commands/network.d.ts.map +1 -1
- package/build/lib/commands/network.js +12 -16
- package/build/lib/commands/network.js.map +1 -1
- package/build/lib/commands/nfc.d.ts.map +1 -1
- package/build/lib/commands/nfc.js +1 -5
- package/build/lib/commands/nfc.js.map +1 -1
- package/build/lib/commands/performance.d.ts.map +1 -1
- package/build/lib/commands/performance.js +7 -12
- package/build/lib/commands/performance.js.map +1 -1
- package/build/lib/commands/permissions.d.ts.map +1 -1
- package/build/lib/commands/permissions.js +21 -23
- package/build/lib/commands/permissions.js.map +1 -1
- package/build/lib/commands/recordscreen.d.ts.map +1 -1
- package/build/lib/commands/recordscreen.js +10 -11
- package/build/lib/commands/recordscreen.js.map +1 -1
- package/build/lib/commands/resources.d.ts.map +1 -1
- package/build/lib/commands/resources.js +6 -10
- package/build/lib/commands/resources.js.map +1 -1
- package/build/lib/commands/shell.d.ts.map +1 -1
- package/build/lib/commands/shell.js +7 -6
- package/build/lib/commands/shell.js.map +1 -1
- package/build/lib/commands/streamscreen.d.ts.map +1 -1
- package/build/lib/commands/streamscreen.js +14 -15
- package/build/lib/commands/streamscreen.js.map +1 -1
- package/build/lib/commands/system-bars.d.ts.map +1 -1
- package/build/lib/commands/system-bars.js +19 -21
- package/build/lib/commands/system-bars.js.map +1 -1
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js +2 -6
- package/build/lib/driver.js.map +1 -1
- package/build/lib/utils.d.ts.map +1 -1
- package/build/lib/utils.js +9 -12
- package/build/lib/utils.js.map +1 -1
- package/lib/commands/app-management.ts +19 -9
- package/lib/commands/bidi/models.ts +1 -2
- package/lib/commands/bluetooth.ts +1 -2
- package/lib/commands/context/exports.ts +12 -14
- package/lib/commands/context/helpers.ts +47 -22
- package/lib/commands/device/common.ts +10 -8
- package/lib/commands/device/utils.ts +2 -3
- package/lib/commands/deviceidle.ts +2 -3
- package/lib/commands/execute.ts +2 -3
- package/lib/commands/file-actions.ts +2 -3
- package/lib/commands/find.ts +4 -5
- package/lib/commands/geolocation.ts +3 -4
- package/lib/commands/image-injection.ts +1 -2
- package/lib/commands/intent.ts +14 -15
- package/lib/commands/keyboard.ts +6 -3
- package/lib/commands/lock/exports.ts +1 -2
- package/lib/commands/lock/helpers.ts +5 -6
- package/lib/commands/log.ts +9 -9
- package/lib/commands/media-projection.ts +3 -4
- package/lib/commands/network.ts +12 -13
- package/lib/commands/nfc.ts +1 -2
- package/lib/commands/performance.ts +7 -11
- package/lib/commands/permissions.ts +23 -20
- package/lib/commands/recordscreen.ts +10 -11
- package/lib/commands/resources.ts +7 -8
- package/lib/commands/shell.ts +7 -3
- package/lib/commands/streamscreen.ts +14 -16
- package/lib/commands/system-bars.ts +20 -20
- package/lib/driver.ts +2 -4
- package/lib/utils.ts +9 -9
- package/package.json +2 -7
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {retryInterval} from 'asyncbox';
|
|
2
|
-
import _ from 'lodash';
|
|
3
2
|
import type {ExecError} from 'teen_process';
|
|
4
3
|
import type {AndroidDriver} from '../driver';
|
|
5
4
|
import type {PerformanceDataType} from './types';
|
|
5
|
+
import {util} from '@appium/support';
|
|
6
6
|
|
|
7
7
|
export const NETWORK_KEYS = [
|
|
8
8
|
[
|
|
@@ -68,7 +68,7 @@ const RETRY_PAUSE_MS = 1000;
|
|
|
68
68
|
* The possible values are: 'cpuinfo', 'memoryinfo', 'batteryinfo', 'networkinfo'.
|
|
69
69
|
*/
|
|
70
70
|
export async function getPerformanceDataTypes(this: AndroidDriver): Promise<PerformanceDataType[]> {
|
|
71
|
-
return
|
|
71
|
+
return Object.keys(SUPPORTED_PERFORMANCE_DATA_TYPES) as PerformanceDataType[];
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
@@ -89,7 +89,7 @@ export async function getPerformanceData(
|
|
|
89
89
|
dataType: string,
|
|
90
90
|
retries: number = 2,
|
|
91
91
|
): Promise<any[][]> {
|
|
92
|
-
switch (
|
|
92
|
+
switch (dataType?.toLowerCase()) {
|
|
93
93
|
case 'batteryinfo':
|
|
94
94
|
return await getBatteryInfo.call(this, retries);
|
|
95
95
|
case 'cpuinfo':
|
|
@@ -187,7 +187,7 @@ export async function getMemoryInfo(
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
if (valDict.totalPrivateDirty && valDict.totalPrivateDirty !== 'nodex') {
|
|
190
|
-
const headers =
|
|
190
|
+
const headers = structuredClone(MEMORY_KEYS);
|
|
191
191
|
const values = headers.map((header) => valDict[header]);
|
|
192
192
|
return [headers, values];
|
|
193
193
|
}
|
|
@@ -371,11 +371,7 @@ export async function getNetworkTrafficInfo(
|
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
-
if (
|
|
375
|
-
!_.isEqual(pendingBytes, '') &&
|
|
376
|
-
!_.isUndefined(pendingBytes) &&
|
|
377
|
-
!_.isEqual(pendingBytes, 'nodex')
|
|
378
|
-
) {
|
|
374
|
+
if (pendingBytes !== '' && pendingBytes !== undefined && pendingBytes !== 'nodex') {
|
|
379
375
|
return returnValue;
|
|
380
376
|
} else {
|
|
381
377
|
throw new Error(`Unable to parse network traffic data: '${data}'`);
|
|
@@ -427,7 +423,7 @@ export async function getCPUInfo(
|
|
|
427
423
|
// `output` will be something like
|
|
428
424
|
// +0% 2209/io.appium.android.apis: 0.1% user + 0.2% kernel / faults: 70 minor
|
|
429
425
|
const usagesPattern = new RegExp(
|
|
430
|
-
`^.+\\/${
|
|
426
|
+
`^.+\\/${util.escapeRegExp(packageName)}:\\D+([\\d.]+)%\\s+user\\s+\\+\\s+([\\d.]+)%\\s+kernel`,
|
|
431
427
|
'm',
|
|
432
428
|
);
|
|
433
429
|
const match = usagesPattern.exec(output);
|
|
@@ -462,7 +458,7 @@ export async function getBatteryInfo(this: AndroidDriver, retries: number = 2):
|
|
|
462
458
|
const power = parseInt((data.split(':')[1] || '').trim(), 10);
|
|
463
459
|
|
|
464
460
|
if (!Number.isNaN(power)) {
|
|
465
|
-
return [
|
|
461
|
+
return [structuredClone(BATTERY_KEYS), [power.toString()]];
|
|
466
462
|
} else {
|
|
467
463
|
throw new Error(`Unable to parse battery data: '${data}'`);
|
|
468
464
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {errors} from 'appium/driver';
|
|
2
|
-
import _ from 'lodash';
|
|
3
2
|
import {ADB_SHELL_FEATURE} from '../utils';
|
|
4
3
|
import type {AndroidDriver} from '../driver';
|
|
4
|
+
import {util} from '@appium/support';
|
|
5
5
|
|
|
6
6
|
const ALL_PERMISSIONS_MAGIC = 'all';
|
|
7
7
|
const PM_ACTION = Object.freeze({
|
|
@@ -67,31 +67,32 @@ export async function mobileChangePermissions(
|
|
|
67
67
|
if (!appPackage) {
|
|
68
68
|
throw new errors.InvalidArgumentError(`A valid appPackage must be provided`);
|
|
69
69
|
}
|
|
70
|
-
action ??=
|
|
71
|
-
|
|
70
|
+
action ??=
|
|
71
|
+
target?.toLowerCase() === PERMISSION_TARGET.APPOPS ? APPOPS_ACTION.ALLOW : PM_ACTION.GRANT;
|
|
72
|
+
if (permissions == null) {
|
|
72
73
|
throw new errors.InvalidArgumentError(`'permissions' argument is required`);
|
|
73
74
|
}
|
|
74
|
-
if (
|
|
75
|
+
if (util.isEmpty(permissions)) {
|
|
75
76
|
throw new errors.InvalidArgumentError(`'permissions' argument must not be empty`);
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
switch (
|
|
79
|
+
switch (target?.toLowerCase()) {
|
|
79
80
|
case PERMISSION_TARGET.PM:
|
|
80
81
|
return await changePermissionsViaPm.bind(this)(
|
|
81
82
|
permissions,
|
|
82
83
|
appPackage,
|
|
83
|
-
|
|
84
|
+
action?.toLowerCase() as PMAction,
|
|
84
85
|
);
|
|
85
86
|
case PERMISSION_TARGET.APPOPS:
|
|
86
87
|
this.assertFeatureEnabled(ADB_SHELL_FEATURE);
|
|
87
88
|
return await changePermissionsViaAppops.bind(this)(
|
|
88
89
|
permissions,
|
|
89
90
|
appPackage,
|
|
90
|
-
|
|
91
|
+
action?.toLowerCase() as AppOpsAction,
|
|
91
92
|
);
|
|
92
93
|
default:
|
|
93
94
|
throw new errors.InvalidArgumentError(
|
|
94
|
-
`'target' argument must be one of: ${
|
|
95
|
+
`'target' argument must be one of: ${Object.values(PERMISSION_TARGET)}`,
|
|
95
96
|
);
|
|
96
97
|
}
|
|
97
98
|
}
|
|
@@ -111,7 +112,7 @@ export async function mobileGetPermissions(
|
|
|
111
112
|
): Promise<string[]> {
|
|
112
113
|
appPackage ??= this.opts.appPackage;
|
|
113
114
|
let actionFunc: (pkg: string) => Promise<string[]>;
|
|
114
|
-
switch (
|
|
115
|
+
switch (type?.toLowerCase()) {
|
|
115
116
|
case PERMISSIONS_TYPE.REQUESTED:
|
|
116
117
|
actionFunc = (pkg) => this.adb.getReqPermissions(pkg);
|
|
117
118
|
break;
|
|
@@ -124,7 +125,7 @@ export async function mobileGetPermissions(
|
|
|
124
125
|
default:
|
|
125
126
|
throw new errors.InvalidArgumentError(
|
|
126
127
|
`Unknown permissions type '${type}'. ` +
|
|
127
|
-
`Only ${JSON.stringify(
|
|
128
|
+
`Only ${JSON.stringify(Object.values(PERMISSIONS_TYPE))} types are supported`,
|
|
128
129
|
);
|
|
129
130
|
}
|
|
130
131
|
return await actionFunc(appPackage as string);
|
|
@@ -135,24 +136,26 @@ async function changePermissionsViaPm(
|
|
|
135
136
|
appPackage: string,
|
|
136
137
|
action: PMAction,
|
|
137
138
|
): Promise<void> {
|
|
138
|
-
if (!
|
|
139
|
+
if (!Object.values(PM_ACTION).includes(action)) {
|
|
139
140
|
throw new errors.InvalidArgumentError(
|
|
140
141
|
`Unknown action '${action}'. ` +
|
|
141
|
-
`Only ${JSON.stringify(
|
|
142
|
+
`Only ${JSON.stringify(Object.values(PM_ACTION))} actions are supported`,
|
|
142
143
|
);
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
let affectedPermissions =
|
|
146
|
-
if (
|
|
146
|
+
let affectedPermissions = Array.isArray(permissions) ? permissions : [permissions];
|
|
147
|
+
if (typeof permissions === 'string' && permissions.toLowerCase() === ALL_PERMISSIONS_MAGIC) {
|
|
147
148
|
const dumpsys = await this.adb.shell(['dumpsys', 'package', appPackage]);
|
|
148
149
|
const grantedPermissions = await this.adb.getGrantedPermissions(appPackage, dumpsys);
|
|
149
150
|
if (action === PM_ACTION.GRANT) {
|
|
150
151
|
const reqPermissons = await this.adb.getReqPermissions(appPackage, dumpsys);
|
|
151
|
-
affectedPermissions =
|
|
152
|
+
affectedPermissions = reqPermissons.filter(
|
|
153
|
+
(permission) => !grantedPermissions.includes(permission),
|
|
154
|
+
);
|
|
152
155
|
} else {
|
|
153
156
|
affectedPermissions = grantedPermissions;
|
|
154
157
|
}
|
|
155
|
-
if (
|
|
158
|
+
if (util.isEmpty(affectedPermissions)) {
|
|
156
159
|
this.log.info(`'${appPackage}' contains no permissions to ${action}`);
|
|
157
160
|
return;
|
|
158
161
|
}
|
|
@@ -172,13 +175,13 @@ async function changePermissionsViaAppops(
|
|
|
172
175
|
appPackage: string,
|
|
173
176
|
action: AppOpsAction,
|
|
174
177
|
): Promise<void> {
|
|
175
|
-
if (!
|
|
178
|
+
if (!Object.values(APPOPS_ACTION).includes(action)) {
|
|
176
179
|
throw new errors.InvalidArgumentError(
|
|
177
180
|
`Unknown action '${action}'. ` +
|
|
178
|
-
`Only ${JSON.stringify(
|
|
181
|
+
`Only ${JSON.stringify(Object.values(APPOPS_ACTION))} actions are supported`,
|
|
179
182
|
);
|
|
180
183
|
}
|
|
181
|
-
if (
|
|
184
|
+
if (typeof permissions === 'string' && permissions.toLowerCase() === ALL_PERMISSIONS_MAGIC) {
|
|
182
185
|
throw new errors.InvalidArgumentError(
|
|
183
186
|
`'${ALL_PERMISSIONS_MAGIC}' permission is only supported for ` +
|
|
184
187
|
`'${PERMISSION_TARGET.PM}' target. ` +
|
|
@@ -186,7 +189,7 @@ async function changePermissionsViaAppops(
|
|
|
186
189
|
);
|
|
187
190
|
}
|
|
188
191
|
|
|
189
|
-
const promises = (
|
|
192
|
+
const promises = (Array.isArray(permissions) ? permissions : [permissions]).map((permission) =>
|
|
190
193
|
this.adb.shell(['appops', 'set', appPackage, permission, action]),
|
|
191
194
|
);
|
|
192
195
|
await Promise.all(promises);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {fs, net, system, tempDir, timing, util} from '@appium/support';
|
|
2
2
|
import type {NetOptions, HttpUploadOptions} from '@appium/support';
|
|
3
3
|
import {waitForCondition} from 'asyncbox';
|
|
4
|
-
import _ from 'lodash';
|
|
5
4
|
import path from 'node:path';
|
|
6
5
|
import {exec} from 'teen_process';
|
|
7
6
|
import type {AndroidDriver} from '../driver';
|
|
@@ -63,7 +62,7 @@ export async function startRecordingScreen(
|
|
|
63
62
|
);
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
if (
|
|
65
|
+
if (this._screenRecordingProperties) {
|
|
67
66
|
// XXX: this doesn't need to be done in serial, does it?
|
|
68
67
|
const props = this._screenRecordingProperties;
|
|
69
68
|
for (const record of props.records || []) {
|
|
@@ -117,7 +116,7 @@ export async function stopRecordingScreen(
|
|
|
117
116
|
await verifyScreenRecordIsSupported(this.adb, this.isEmulator());
|
|
118
117
|
|
|
119
118
|
const props = this._screenRecordingProperties;
|
|
120
|
-
if (
|
|
119
|
+
if (props) {
|
|
121
120
|
props.stopped = true;
|
|
122
121
|
}
|
|
123
122
|
|
|
@@ -125,12 +124,12 @@ export async function stopRecordingScreen(
|
|
|
125
124
|
await terminateBackgroundScreenRecording(this.adb, false);
|
|
126
125
|
} catch (err) {
|
|
127
126
|
this.log.warn((err as Error).message);
|
|
128
|
-
if (
|
|
127
|
+
if (props) {
|
|
129
128
|
this.log.warn('The resulting video might be corrupted');
|
|
130
129
|
}
|
|
131
130
|
}
|
|
132
131
|
|
|
133
|
-
if (
|
|
132
|
+
if (!props) {
|
|
134
133
|
this.log.info(
|
|
135
134
|
`Screen recording has not been previously started by Appium. There is nothing to stop`,
|
|
136
135
|
);
|
|
@@ -148,7 +147,7 @@ export async function stopRecordingScreen(
|
|
|
148
147
|
props.recordingProcess = null;
|
|
149
148
|
}
|
|
150
149
|
|
|
151
|
-
if (
|
|
150
|
+
if (util.isEmpty(props.records)) {
|
|
152
151
|
throw this.log.errorWithException(
|
|
153
152
|
`No screen recordings have been stored on the device so far. ` +
|
|
154
153
|
`Are you sure the ${SCREENRECORD_BINARY} utility works as expected?`,
|
|
@@ -164,7 +163,7 @@ export async function stopRecordingScreen(
|
|
|
164
163
|
await this.adb.pull(pathOnDevice, relativePath, {timeout: ADB_PULL_TIMEOUT});
|
|
165
164
|
await this.adb.rimraf(pathOnDevice);
|
|
166
165
|
}
|
|
167
|
-
let resultFilePath =
|
|
166
|
+
let resultFilePath = localRecords.at(-1) as string;
|
|
168
167
|
if (localRecords.length > 1) {
|
|
169
168
|
this.log.info(`Got ${localRecords.length} screen recordings. Trying to merge them`);
|
|
170
169
|
try {
|
|
@@ -176,7 +175,7 @@ export async function stopRecordingScreen(
|
|
|
176
175
|
);
|
|
177
176
|
}
|
|
178
177
|
}
|
|
179
|
-
if (
|
|
178
|
+
if (util.isEmpty(options.remotePath)) {
|
|
180
179
|
const {size} = await fs.stat(resultFilePath);
|
|
181
180
|
this.log.debug(
|
|
182
181
|
`The size of the resulting screen recording is ${util.toReadableSizeString(size)}`,
|
|
@@ -196,7 +195,7 @@ async function uploadRecordedMedia(
|
|
|
196
195
|
remotePath?: string,
|
|
197
196
|
uploadOptions: StopScreenRecordingOpts = {},
|
|
198
197
|
): Promise<string> {
|
|
199
|
-
if (
|
|
198
|
+
if (util.isEmpty(remotePath)) {
|
|
200
199
|
return (await util.toInMemoryBase64(localFile)).toString();
|
|
201
200
|
}
|
|
202
201
|
|
|
@@ -322,14 +321,14 @@ async function mergeScreenRecords(this: AndroidDriver, mediaFiles: string[]): Pr
|
|
|
322
321
|
|
|
323
322
|
async function terminateBackgroundScreenRecording(adb: ADB, force = true): Promise<boolean> {
|
|
324
323
|
const screenrecordPids = await adb.getProcessIdsByName(SCREENRECORD_BINARY);
|
|
325
|
-
if (
|
|
324
|
+
if (util.isEmpty(screenrecordPids)) {
|
|
326
325
|
return false;
|
|
327
326
|
}
|
|
328
327
|
|
|
329
328
|
try {
|
|
330
329
|
await adb.shell(['kill', force ? '-15' : '-2', ...screenrecordPids.map(String)]);
|
|
331
330
|
await waitForCondition(
|
|
332
|
-
async () =>
|
|
331
|
+
async () => util.isEmpty(await adb.getProcessIdsByName(SCREENRECORD_BINARY)),
|
|
333
332
|
{
|
|
334
333
|
waitMs: PROCESS_SHUTDOWN_TIMEOUT,
|
|
335
334
|
intervalMs: 500,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {fs, tempDir} from '@appium/support';
|
|
1
|
+
import {fs, tempDir, util} from '@appium/support';
|
|
3
2
|
import type {StringRecord} from '@appium/types';
|
|
4
3
|
import type {AndroidDriver, AndroidDriverOpts} from '../driver';
|
|
5
4
|
import type {Locale} from './types';
|
|
@@ -24,8 +23,8 @@ export async function getStrings(
|
|
|
24
23
|
// and values of type string
|
|
25
24
|
const preprocessStringsMap = (mapping: StringRecord): StringRecord => {
|
|
26
25
|
const result: StringRecord = {};
|
|
27
|
-
for (const [key, value] of
|
|
28
|
-
result[key] =
|
|
26
|
+
for (const [key, value] of Object.entries(mapping)) {
|
|
27
|
+
result[key] = typeof value === 'string' ? value : JSON.stringify(value);
|
|
29
28
|
}
|
|
30
29
|
return result;
|
|
31
30
|
};
|
|
@@ -63,7 +62,7 @@ export async function ensureDeviceLocale(
|
|
|
63
62
|
} catch (e1) {
|
|
64
63
|
this.log.debug((e1 as Error).stack);
|
|
65
64
|
}
|
|
66
|
-
if (
|
|
65
|
+
if (suggestions.length > 0) {
|
|
67
66
|
errMsg += ` You may want to apply one of the following locales instead: ${suggestions}`;
|
|
68
67
|
}
|
|
69
68
|
throw new Error(errMsg, {cause: e});
|
|
@@ -114,10 +113,10 @@ async function fetchLocaleSuggestions(
|
|
|
114
113
|
const supportedLocales = await this.settingsApp.listSupportedLocales();
|
|
115
114
|
const suggestedLocales = supportedLocales.filter(
|
|
116
115
|
(locale) =>
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
language?.toLowerCase() === locale.language?.toLowerCase() ||
|
|
117
|
+
country?.toLowerCase() === locale.country?.toLowerCase(),
|
|
119
118
|
);
|
|
120
|
-
return
|
|
119
|
+
return util.isEmpty(suggestedLocales) ? supportedLocales : suggestedLocales;
|
|
121
120
|
}
|
|
122
121
|
|
|
123
122
|
function toLocaleAbbr({language, country, script}: Locale): string {
|
package/lib/commands/shell.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {util} from '@appium/support';
|
|
2
2
|
import {errors} from 'appium/driver';
|
|
3
|
-
import _ from 'lodash';
|
|
4
3
|
import {exec} from 'teen_process';
|
|
5
4
|
import type {ExecError} from 'teen_process';
|
|
6
5
|
import {ADB_SHELL_FEATURE} from '../utils';
|
|
@@ -36,11 +35,16 @@ export async function mobileShell<T extends boolean>(
|
|
|
36
35
|
): Promise<T extends true ? {stdout: string; stderr: string} : string> {
|
|
37
36
|
this.assertFeatureEnabled(ADB_SHELL_FEATURE);
|
|
38
37
|
|
|
39
|
-
if (
|
|
38
|
+
if (typeof command !== 'string') {
|
|
40
39
|
throw new errors.InvalidArgumentError(`The 'command' argument is mandatory`);
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
const adbArgs = [
|
|
42
|
+
const adbArgs = [
|
|
43
|
+
...this.adb.executable.defaultArgs,
|
|
44
|
+
'shell',
|
|
45
|
+
command,
|
|
46
|
+
...(Array.isArray(args) ? args : [args]),
|
|
47
|
+
];
|
|
44
48
|
this.log.debug(`Running '${this.adb.executable.path} ${util.quote(adbArgs)}'`);
|
|
45
49
|
try {
|
|
46
50
|
const {stdout, stderr} = await exec(this.adb.executable.path, adbArgs, {timeout});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {fs, logger, system, util} from '@appium/support';
|
|
2
2
|
import {waitForCondition} from 'asyncbox';
|
|
3
|
-
import _ from 'lodash';
|
|
4
3
|
import {spawn} from 'node:child_process';
|
|
5
4
|
import http from 'node:http';
|
|
6
5
|
import net from 'node:net';
|
|
@@ -89,10 +88,9 @@ export async function mobileStartScreenStreaming(
|
|
|
89
88
|
): Promise<void> {
|
|
90
89
|
this.assertFeatureEnabled(ADB_SCREEN_STREAMING_FEATURE);
|
|
91
90
|
|
|
92
|
-
if (
|
|
91
|
+
if (this._screenStreamingProps === undefined) {
|
|
93
92
|
await verifyStreamingRequirements(this.adb);
|
|
94
|
-
}
|
|
95
|
-
if (!_.isEmpty(this._screenStreamingProps)) {
|
|
93
|
+
} else {
|
|
96
94
|
this.log.info(
|
|
97
95
|
`The screen streaming session is already running. ` +
|
|
98
96
|
`Stop it first in order to start a new one.`,
|
|
@@ -246,8 +244,8 @@ export async function mobileStartScreenStreaming(
|
|
|
246
244
|
* If no streaming session is active, this method returns without error.
|
|
247
245
|
*/
|
|
248
246
|
export async function mobileStopScreenStreaming(this: AndroidDriver): Promise<void> {
|
|
249
|
-
if (
|
|
250
|
-
if (
|
|
247
|
+
if (!this._screenStreamingProps) {
|
|
248
|
+
if (this._screenStreamingProps !== undefined) {
|
|
251
249
|
this.log.debug(`Screen streaming is not running. There is nothing to stop`);
|
|
252
250
|
}
|
|
253
251
|
return;
|
|
@@ -287,7 +285,7 @@ export async function mobileStopScreenStreaming(this: AndroidDriver): Promise<vo
|
|
|
287
285
|
function createStreamingLogger(streamName: string, udid: string): AppiumLogger {
|
|
288
286
|
return logger.getLogger(
|
|
289
287
|
`${streamName}@` +
|
|
290
|
-
|
|
288
|
+
util.truncateString(udid, {
|
|
291
289
|
length: 8,
|
|
292
290
|
omission: '',
|
|
293
291
|
}),
|
|
@@ -295,7 +293,7 @@ function createStreamingLogger(streamName: string, udid: string): AppiumLogger {
|
|
|
295
293
|
}
|
|
296
294
|
|
|
297
295
|
async function verifyStreamingRequirements(adb: ADB): Promise<void> {
|
|
298
|
-
if (!
|
|
296
|
+
if (!(await adb.shell(['which', SCREENRECORD_BINARY])).trim()) {
|
|
299
297
|
throw new Error(
|
|
300
298
|
`The required '${SCREENRECORD_BINARY}' binary is not available on the device under test`,
|
|
301
299
|
);
|
|
@@ -319,11 +317,11 @@ async function verifyStreamingRequirements(adb: ADB): Promise<void> {
|
|
|
319
317
|
await Promise.all(gstreamerCheckPromises);
|
|
320
318
|
|
|
321
319
|
const moduleCheckPromises: Promise<void>[] = [];
|
|
322
|
-
for (const [name, modName] of
|
|
320
|
+
for (const [name, modName] of Object.entries(REQUIRED_GST_PLUGINS)) {
|
|
323
321
|
moduleCheckPromises.push(
|
|
324
322
|
(async () => {
|
|
325
323
|
const {stdout} = await exec(GST_INSPECT_BINARY, [name]);
|
|
326
|
-
if (!
|
|
324
|
+
if (!stdout?.includes(modName)) {
|
|
327
325
|
throw new Error(
|
|
328
326
|
`The required GStreamer plugin '${name}' from '${modName}' module is not installed. ` +
|
|
329
327
|
`See ${GST_TUTORIAL_URL} for more details on how to install it.`,
|
|
@@ -366,9 +364,9 @@ async function initDeviceStreamingProc(
|
|
|
366
364
|
opts: {width?: string | number; height?: string | number; bitRate?: string | number} = {},
|
|
367
365
|
): Promise<ReturnType<typeof spawn>> {
|
|
368
366
|
const {width, height, bitRate} = opts;
|
|
369
|
-
const adjustedWidth =
|
|
370
|
-
const adjustedHeight =
|
|
371
|
-
const adjustedBitrate =
|
|
367
|
+
const adjustedWidth = width === undefined ? deviceInfo.width : parseInt(String(width), 10);
|
|
368
|
+
const adjustedHeight = height === undefined ? deviceInfo.height : parseInt(String(height), 10);
|
|
369
|
+
const adjustedBitrate = bitRate === undefined ? DEFAULT_BITRATE : parseInt(String(bitRate), 10);
|
|
372
370
|
let screenRecordCmd =
|
|
373
371
|
SCREENRECORD_BINARY +
|
|
374
372
|
` --output-format=h264` +
|
|
@@ -396,7 +394,7 @@ async function initDeviceStreamingProc(
|
|
|
396
394
|
const deviceStreamingLogger = createStreamingLogger(SCREENRECORD_BINARY, deviceInfo.udid);
|
|
397
395
|
const errorsListener = (chunk: Buffer | string) => {
|
|
398
396
|
const stderr = chunk.toString();
|
|
399
|
-
if (
|
|
397
|
+
if (stderr?.trim()) {
|
|
400
398
|
deviceStreamingLogger.debug(stderr);
|
|
401
399
|
}
|
|
402
400
|
};
|
|
@@ -404,7 +402,7 @@ async function initDeviceStreamingProc(
|
|
|
404
402
|
|
|
405
403
|
const startupListener = (chunk: Buffer | string) => {
|
|
406
404
|
if (!isStarted) {
|
|
407
|
-
isStarted = !
|
|
405
|
+
isStarted = !util.isEmpty(chunk);
|
|
408
406
|
}
|
|
409
407
|
};
|
|
410
408
|
deviceStreaming.stdout.on('data', startupListener);
|
|
@@ -477,7 +475,7 @@ async function initGstreamerPipeline(
|
|
|
477
475
|
});
|
|
478
476
|
const gstreamerLogger = createStreamingLogger('gst', deviceInfo.udid);
|
|
479
477
|
const gstOutputListener = (stdout: string, stderr: string) => {
|
|
480
|
-
if (
|
|
478
|
+
if ((stderr || stdout)?.trim()) {
|
|
481
479
|
gstreamerLogger.debug(stderr || stdout);
|
|
482
480
|
}
|
|
483
481
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {errors} from 'appium/driver';
|
|
2
|
-
import _ from 'lodash';
|
|
3
2
|
import type {StringRecord} from '@appium/types';
|
|
4
3
|
import type {AndroidDriver} from '../driver';
|
|
5
4
|
import type {StatusBarCommand, WindowProperties} from './types';
|
|
5
|
+
import {util} from '@appium/support';
|
|
6
6
|
|
|
7
7
|
const WINDOW_TITLE_PATTERN = /^\s+Window\s#\d+\sWindow\{[0-9a-f]+\s\w+\s([\w-]+)\}:$/;
|
|
8
8
|
const FRAME_PATTERN = /\bm?[Ff]rame=\[([0-9.-]+),([0-9.-]+)\]\[([0-9.-]+),([0-9.-]+)\]/;
|
|
@@ -64,15 +64,15 @@ export async function mobilePerformStatusBarCommand(
|
|
|
64
64
|
component?: string,
|
|
65
65
|
): Promise<string> {
|
|
66
66
|
const toStatusBarCommandCallable =
|
|
67
|
-
(cmd: string, argsCallable?: () => string[] | string) => async (): Promise<string> =>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
(cmd: string, argsCallable?: () => string[] | string) => async (): Promise<string> => {
|
|
68
|
+
let argsCallableArray = argsCallable ? argsCallable() : [];
|
|
69
|
+
if (!Array.isArray(argsCallableArray)) {
|
|
70
|
+
argsCallableArray = [argsCallableArray];
|
|
71
|
+
}
|
|
72
|
+
return await this.adb.shell(['cmd', 'statusbar', cmd, ...argsCallableArray]);
|
|
73
|
+
};
|
|
74
74
|
const tileCommandArgsCallable = () => component as string;
|
|
75
|
-
const statusBarCommands =
|
|
75
|
+
const statusBarCommands = Object.fromEntries(
|
|
76
76
|
(
|
|
77
77
|
[
|
|
78
78
|
['expandNotifications', ['expand-notifications']],
|
|
@@ -90,7 +90,7 @@ export async function mobilePerformStatusBarCommand(
|
|
|
90
90
|
if (!action) {
|
|
91
91
|
throw new errors.InvalidArgumentError(
|
|
92
92
|
`The '${command}' status bar command is unknown. Only the following commands ` +
|
|
93
|
-
`are supported: ${
|
|
93
|
+
`are supported: ${Object.keys(statusBarCommands)}`,
|
|
94
94
|
);
|
|
95
95
|
}
|
|
96
96
|
return await action();
|
|
@@ -110,7 +110,7 @@ export function parseWindowProperties(
|
|
|
110
110
|
name: string,
|
|
111
111
|
props: string[],
|
|
112
112
|
): WindowProperties {
|
|
113
|
-
const result =
|
|
113
|
+
const result = structuredClone(DEFAULT_WINDOW_PROPERTIES);
|
|
114
114
|
const propLines = props.join('\n');
|
|
115
115
|
const frameMatch = FRAME_PATTERN.exec(propLines);
|
|
116
116
|
if (!frameMatch) {
|
|
@@ -142,29 +142,29 @@ export function parseWindowProperties(
|
|
|
142
142
|
export function parseWindows(this: AndroidDriver, lines: string): SystemBarsResult {
|
|
143
143
|
const windows: StringRecord<string[]> = {};
|
|
144
144
|
let currentWindowName: string | null = null;
|
|
145
|
-
for (const line of lines.split('\n').map(
|
|
146
|
-
const match = WINDOW_TITLE_PATTERN.exec(line);
|
|
145
|
+
for (const line of lines.split('\n').map((line) => String(line)?.trimEnd())) {
|
|
146
|
+
const match = WINDOW_TITLE_PATTERN.exec(line as any);
|
|
147
147
|
if (match) {
|
|
148
148
|
currentWindowName = match[1];
|
|
149
149
|
windows[currentWindowName] = [];
|
|
150
150
|
continue;
|
|
151
151
|
}
|
|
152
|
-
if (
|
|
152
|
+
if (String(line)?.trim()?.length === 0) {
|
|
153
153
|
currentWindowName = null;
|
|
154
154
|
continue;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
if (currentWindowName &&
|
|
157
|
+
if (currentWindowName && Array.isArray(windows[currentWindowName])) {
|
|
158
158
|
windows[currentWindowName].push(line);
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
-
if (
|
|
161
|
+
if (util.isEmpty(windows)) {
|
|
162
162
|
this.log.debug(lines);
|
|
163
163
|
throw new Error('Cannot parse any window information from the dumpsys output');
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
const result: SystemBarsResult = {};
|
|
167
|
-
for (const [name, props] of
|
|
167
|
+
for (const [name, props] of Object.entries(windows)) {
|
|
168
168
|
if (
|
|
169
169
|
name.startsWith(STATUS_BAR_WINDOW_NAME_PREFIX) ||
|
|
170
170
|
props.some((line: string) => STATUS_BAR_TYPE_PATTERN.test(line))
|
|
@@ -182,14 +182,14 @@ export function parseWindows(this: AndroidDriver, lines: string): SystemBarsResu
|
|
|
182
182
|
['statusBar', STATUS_BAR_WINDOW_NAME_PREFIX],
|
|
183
183
|
['navigationBar', NAVIGATION_BAR_WINDOW_NAME_PREFIX],
|
|
184
184
|
] as const
|
|
185
|
-
).filter(([name]) =>
|
|
185
|
+
).filter(([name]) => result[name as keyof SystemBarsResult] == null);
|
|
186
186
|
for (const [window, namePrefix] of unmatchedWindows) {
|
|
187
187
|
this.log.info(
|
|
188
188
|
`No windows have been found whose title matches to ` +
|
|
189
189
|
`'${namePrefix}'. Assuming it is invisible. ` +
|
|
190
|
-
`Only the following windows are available: ${
|
|
190
|
+
`Only the following windows are available: ${Object.keys(windows)}`,
|
|
191
191
|
);
|
|
192
|
-
result[window as keyof SystemBarsResult] =
|
|
192
|
+
result[window as keyof SystemBarsResult] = structuredClone(DEFAULT_WINDOW_PROPERTIES);
|
|
193
193
|
}
|
|
194
194
|
return result;
|
|
195
195
|
}
|
package/lib/driver.ts
CHANGED
|
@@ -7,7 +7,6 @@ import type {
|
|
|
7
7
|
StringRecord,
|
|
8
8
|
W3CDriverCaps,
|
|
9
9
|
} from '@appium/types';
|
|
10
|
-
import _ from 'lodash';
|
|
11
10
|
import type {ADB, LogcatListener} from 'appium-adb';
|
|
12
11
|
import type {default as AppiumChromedriver} from 'appium-chromedriver';
|
|
13
12
|
import {BaseDriver} from 'appium/driver';
|
|
@@ -462,7 +461,7 @@ class AndroidDriver
|
|
|
462
461
|
'accessibility id',
|
|
463
462
|
'-android uiautomator',
|
|
464
463
|
];
|
|
465
|
-
this.desiredCapConstraints =
|
|
464
|
+
this.desiredCapConstraints = structuredClone(ANDROID_DRIVER_CONSTRAINTS);
|
|
466
465
|
this.sessionChromedrivers = {};
|
|
467
466
|
this.jwpProxyActive = false;
|
|
468
467
|
|
|
@@ -483,8 +482,7 @@ class AndroidDriver
|
|
|
483
482
|
}
|
|
484
483
|
|
|
485
484
|
get isChromeSession(): boolean {
|
|
486
|
-
return
|
|
487
|
-
Object.keys(CHROME_BROWSER_PACKAGE_ACTIVITY),
|
|
485
|
+
return Object.keys(CHROME_BROWSER_PACKAGE_ACTIVITY).includes(
|
|
488
486
|
(this.opts.browserName || '').toLowerCase(),
|
|
489
487
|
);
|
|
490
488
|
}
|
package/lib/utils.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import _ from 'lodash';
|
|
2
1
|
import {errors} from 'appium/driver';
|
|
3
2
|
import type {LogEntry} from 'appium-adb';
|
|
4
3
|
import type {AndroidDriver} from './driver';
|
|
4
|
+
import {util} from '@appium/support';
|
|
5
5
|
|
|
6
6
|
export type {LogEntry};
|
|
7
7
|
|
|
@@ -29,8 +29,8 @@ export function requireArgs(
|
|
|
29
29
|
argNames: string | string[],
|
|
30
30
|
opts: Record<string, any>,
|
|
31
31
|
): Record<string, any> {
|
|
32
|
-
for (const argName of
|
|
33
|
-
if (!
|
|
32
|
+
for (const argName of Array.isArray(argNames) ? argNames : [argNames]) {
|
|
33
|
+
if (util.isPlainObject(opts) && !Object.hasOwn(opts, argName)) {
|
|
34
34
|
throw new errors.InvalidArgumentError(`'${argName}' argument must be provided`);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
@@ -48,9 +48,9 @@ export function parseArray(cap: string | string[]): string[] {
|
|
|
48
48
|
parsedCaps = JSON.parse(cap as string);
|
|
49
49
|
} catch {}
|
|
50
50
|
|
|
51
|
-
if (
|
|
51
|
+
if (Array.isArray(parsedCaps)) {
|
|
52
52
|
return parsedCaps;
|
|
53
|
-
} else if (
|
|
53
|
+
} else if (typeof cap === 'string') {
|
|
54
54
|
return [cap];
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -62,12 +62,12 @@ export function parseArray(cap: string | string[]): string[] {
|
|
|
62
62
|
* @returns
|
|
63
63
|
*/
|
|
64
64
|
export async function removeAllSessionWebSocketHandlers(this: AndroidDriver): Promise<void> {
|
|
65
|
-
if (!this.sessionId ||
|
|
65
|
+
if (!this.sessionId || typeof this.server?.getWebSocketHandlers !== 'function') {
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const activeHandlers = await this.server.getWebSocketHandlers(this.sessionId);
|
|
70
|
-
for (const pathname of
|
|
70
|
+
for (const pathname of Object.keys(activeHandlers)) {
|
|
71
71
|
await this.server.removeWebSocketHandler(pathname);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -78,8 +78,8 @@ export async function removeAllSessionWebSocketHandlers(this: AndroidDriver): Pr
|
|
|
78
78
|
* @returns
|
|
79
79
|
*/
|
|
80
80
|
export function nativeLogEntryToSeleniumEntry(x: LogEntryWithPrefix): LogEntry {
|
|
81
|
-
const msg =
|
|
82
|
-
return toLogRecord(x.timestamp ?? Date.now(),
|
|
81
|
+
const msg = util.isEmpty(x.prefix) ? x.message : `[${x.prefix}] ${x.message}`;
|
|
82
|
+
return toLogRecord(x.timestamp ?? Date.now(), msg.replace(COLOR_CODE_PATTERN, ''));
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/**
|