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.
Files changed (122) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/build/lib/commands/app-management.d.ts.map +1 -1
  3. package/build/lib/commands/app-management.js +13 -12
  4. package/build/lib/commands/app-management.js.map +1 -1
  5. package/build/lib/commands/bidi/models.d.ts.map +1 -1
  6. package/build/lib/commands/bidi/models.js +1 -5
  7. package/build/lib/commands/bidi/models.js.map +1 -1
  8. package/build/lib/commands/bluetooth.d.ts.map +1 -1
  9. package/build/lib/commands/bluetooth.js +1 -5
  10. package/build/lib/commands/bluetooth.js.map +1 -1
  11. package/build/lib/commands/context/exports.d.ts.map +1 -1
  12. package/build/lib/commands/context/exports.js +11 -15
  13. package/build/lib/commands/context/exports.js.map +1 -1
  14. package/build/lib/commands/context/helpers.d.ts.map +1 -1
  15. package/build/lib/commands/context/helpers.js +43 -22
  16. package/build/lib/commands/context/helpers.js.map +1 -1
  17. package/build/lib/commands/device/common.d.ts.map +1 -1
  18. package/build/lib/commands/device/common.js +9 -8
  19. package/build/lib/commands/device/common.js.map +1 -1
  20. package/build/lib/commands/device/utils.d.ts.map +1 -1
  21. package/build/lib/commands/device/utils.js +2 -6
  22. package/build/lib/commands/device/utils.js.map +1 -1
  23. package/build/lib/commands/deviceidle.d.ts.map +1 -1
  24. package/build/lib/commands/deviceidle.js +2 -6
  25. package/build/lib/commands/deviceidle.js.map +1 -1
  26. package/build/lib/commands/execute.d.ts.map +1 -1
  27. package/build/lib/commands/execute.js +2 -6
  28. package/build/lib/commands/execute.js.map +1 -1
  29. package/build/lib/commands/file-actions.d.ts.map +1 -1
  30. package/build/lib/commands/file-actions.js +2 -3
  31. package/build/lib/commands/file-actions.js.map +1 -1
  32. package/build/lib/commands/find.d.ts.map +1 -1
  33. package/build/lib/commands/find.js +4 -7
  34. package/build/lib/commands/find.js.map +1 -1
  35. package/build/lib/commands/geolocation.d.ts.map +1 -1
  36. package/build/lib/commands/geolocation.js +3 -4
  37. package/build/lib/commands/geolocation.js.map +1 -1
  38. package/build/lib/commands/image-injection.d.ts.map +1 -1
  39. package/build/lib/commands/image-injection.js +1 -2
  40. package/build/lib/commands/image-injection.js.map +1 -1
  41. package/build/lib/commands/intent.d.ts.map +1 -1
  42. package/build/lib/commands/intent.js +14 -18
  43. package/build/lib/commands/intent.js.map +1 -1
  44. package/build/lib/commands/keyboard.d.ts.map +1 -1
  45. package/build/lib/commands/keyboard.js +6 -6
  46. package/build/lib/commands/keyboard.js.map +1 -1
  47. package/build/lib/commands/lock/exports.d.ts.map +1 -1
  48. package/build/lib/commands/lock/exports.js +1 -5
  49. package/build/lib/commands/lock/exports.js.map +1 -1
  50. package/build/lib/commands/lock/helpers.d.ts.map +1 -1
  51. package/build/lib/commands/lock/helpers.js +5 -9
  52. package/build/lib/commands/lock/helpers.js.map +1 -1
  53. package/build/lib/commands/log.d.ts.map +1 -1
  54. package/build/lib/commands/log.js +9 -9
  55. package/build/lib/commands/log.js.map +1 -1
  56. package/build/lib/commands/media-projection.d.ts.map +1 -1
  57. package/build/lib/commands/media-projection.js +3 -4
  58. package/build/lib/commands/media-projection.js.map +1 -1
  59. package/build/lib/commands/network.d.ts.map +1 -1
  60. package/build/lib/commands/network.js +12 -16
  61. package/build/lib/commands/network.js.map +1 -1
  62. package/build/lib/commands/nfc.d.ts.map +1 -1
  63. package/build/lib/commands/nfc.js +1 -5
  64. package/build/lib/commands/nfc.js.map +1 -1
  65. package/build/lib/commands/performance.d.ts.map +1 -1
  66. package/build/lib/commands/performance.js +7 -12
  67. package/build/lib/commands/performance.js.map +1 -1
  68. package/build/lib/commands/permissions.d.ts.map +1 -1
  69. package/build/lib/commands/permissions.js +21 -23
  70. package/build/lib/commands/permissions.js.map +1 -1
  71. package/build/lib/commands/recordscreen.d.ts.map +1 -1
  72. package/build/lib/commands/recordscreen.js +10 -11
  73. package/build/lib/commands/recordscreen.js.map +1 -1
  74. package/build/lib/commands/resources.d.ts.map +1 -1
  75. package/build/lib/commands/resources.js +6 -10
  76. package/build/lib/commands/resources.js.map +1 -1
  77. package/build/lib/commands/shell.d.ts.map +1 -1
  78. package/build/lib/commands/shell.js +7 -6
  79. package/build/lib/commands/shell.js.map +1 -1
  80. package/build/lib/commands/streamscreen.d.ts.map +1 -1
  81. package/build/lib/commands/streamscreen.js +14 -15
  82. package/build/lib/commands/streamscreen.js.map +1 -1
  83. package/build/lib/commands/system-bars.d.ts.map +1 -1
  84. package/build/lib/commands/system-bars.js +19 -21
  85. package/build/lib/commands/system-bars.js.map +1 -1
  86. package/build/lib/driver.d.ts.map +1 -1
  87. package/build/lib/driver.js +2 -6
  88. package/build/lib/driver.js.map +1 -1
  89. package/build/lib/utils.d.ts.map +1 -1
  90. package/build/lib/utils.js +9 -12
  91. package/build/lib/utils.js.map +1 -1
  92. package/lib/commands/app-management.ts +19 -9
  93. package/lib/commands/bidi/models.ts +1 -2
  94. package/lib/commands/bluetooth.ts +1 -2
  95. package/lib/commands/context/exports.ts +12 -14
  96. package/lib/commands/context/helpers.ts +47 -22
  97. package/lib/commands/device/common.ts +10 -8
  98. package/lib/commands/device/utils.ts +2 -3
  99. package/lib/commands/deviceidle.ts +2 -3
  100. package/lib/commands/execute.ts +2 -3
  101. package/lib/commands/file-actions.ts +2 -3
  102. package/lib/commands/find.ts +4 -5
  103. package/lib/commands/geolocation.ts +3 -4
  104. package/lib/commands/image-injection.ts +1 -2
  105. package/lib/commands/intent.ts +14 -15
  106. package/lib/commands/keyboard.ts +6 -3
  107. package/lib/commands/lock/exports.ts +1 -2
  108. package/lib/commands/lock/helpers.ts +5 -6
  109. package/lib/commands/log.ts +9 -9
  110. package/lib/commands/media-projection.ts +3 -4
  111. package/lib/commands/network.ts +12 -13
  112. package/lib/commands/nfc.ts +1 -2
  113. package/lib/commands/performance.ts +7 -11
  114. package/lib/commands/permissions.ts +23 -20
  115. package/lib/commands/recordscreen.ts +10 -11
  116. package/lib/commands/resources.ts +7 -8
  117. package/lib/commands/shell.ts +7 -3
  118. package/lib/commands/streamscreen.ts +14 -16
  119. package/lib/commands/system-bars.ts +20 -20
  120. package/lib/driver.ts +2 -4
  121. package/lib/utils.ts +9 -9
  122. 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 _.keys(SUPPORTED_PERFORMANCE_DATA_TYPES) as PerformanceDataType[];
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 (_.toLower(dataType)) {
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 = _.clone(MEMORY_KEYS);
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
- `^.+\\/${_.escapeRegExp(packageName)}:\\D+([\\d.]+)%\\s+user\\s+\\+\\s+([\\d.]+)%\\s+kernel`,
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 [_.clone(BATTERY_KEYS), [power.toString()]];
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 ??= _.toLower(target) === PERMISSION_TARGET.APPOPS ? APPOPS_ACTION.ALLOW : PM_ACTION.GRANT;
71
- if (_.isNil(permissions)) {
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 (_.isEmpty(permissions)) {
75
+ if (util.isEmpty(permissions)) {
75
76
  throw new errors.InvalidArgumentError(`'permissions' argument must not be empty`);
76
77
  }
77
78
 
78
- switch (_.toLower(target)) {
79
+ switch (target?.toLowerCase()) {
79
80
  case PERMISSION_TARGET.PM:
80
81
  return await changePermissionsViaPm.bind(this)(
81
82
  permissions,
82
83
  appPackage,
83
- _.toLower(action) as PMAction,
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
- _.toLower(action) as AppOpsAction,
91
+ action?.toLowerCase() as AppOpsAction,
91
92
  );
92
93
  default:
93
94
  throw new errors.InvalidArgumentError(
94
- `'target' argument must be one of: ${_.values(PERMISSION_TARGET)}`,
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 (_.toLower(type)) {
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(_.values(PERMISSIONS_TYPE))} types are supported`,
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 (!_.values(PM_ACTION).includes(action)) {
139
+ if (!Object.values(PM_ACTION).includes(action)) {
139
140
  throw new errors.InvalidArgumentError(
140
141
  `Unknown action '${action}'. ` +
141
- `Only ${JSON.stringify(_.values(PM_ACTION))} actions are supported`,
142
+ `Only ${JSON.stringify(Object.values(PM_ACTION))} actions are supported`,
142
143
  );
143
144
  }
144
145
 
145
- let affectedPermissions = _.isArray(permissions) ? permissions : [permissions];
146
- if (_.isString(permissions) && _.toLower(permissions) === ALL_PERMISSIONS_MAGIC) {
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 = _.difference(reqPermissons, grantedPermissions);
152
+ affectedPermissions = reqPermissons.filter(
153
+ (permission) => !grantedPermissions.includes(permission),
154
+ );
152
155
  } else {
153
156
  affectedPermissions = grantedPermissions;
154
157
  }
155
- if (_.isEmpty(affectedPermissions)) {
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 (!_.values(APPOPS_ACTION).includes(action)) {
178
+ if (!Object.values(APPOPS_ACTION).includes(action)) {
176
179
  throw new errors.InvalidArgumentError(
177
180
  `Unknown action '${action}'. ` +
178
- `Only ${JSON.stringify(_.values(APPOPS_ACTION))} actions are supported`,
181
+ `Only ${JSON.stringify(Object.values(APPOPS_ACTION))} actions are supported`,
179
182
  );
180
183
  }
181
- if (_.isString(permissions) && _.toLower(permissions) === ALL_PERMISSIONS_MAGIC) {
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 = (_.isArray(permissions) ? permissions : [permissions]).map((permission) =>
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 (!_.isEmpty(this._screenRecordingProperties)) {
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 (!_.isEmpty(props)) {
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 (!_.isEmpty(props)) {
127
+ if (props) {
129
128
  this.log.warn('The resulting video might be corrupted');
130
129
  }
131
130
  }
132
131
 
133
- if (_.isEmpty(props)) {
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 (_.isEmpty(props.records)) {
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 = _.last(localRecords) as string;
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 (_.isEmpty(options.remotePath)) {
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 (_.isEmpty(remotePath)) {
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 (_.isEmpty(screenrecordPids)) {
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 () => _.isEmpty(await adb.getProcessIdsByName(SCREENRECORD_BINARY)),
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 _ from 'lodash';
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 _.toPairs(mapping)) {
28
- result[key] = _.isString(value) ? value : JSON.stringify(value);
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 (!_.isEmpty(suggestions)) {
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
- _.toLower(language) === _.toLower(locale.language) ||
118
- _.toLower(country) === _.toLower(locale.country),
116
+ language?.toLowerCase() === locale.language?.toLowerCase() ||
117
+ country?.toLowerCase() === locale.country?.toLowerCase(),
119
118
  );
120
- return _.isEmpty(suggestedLocales) ? supportedLocales : suggestedLocales;
119
+ return util.isEmpty(suggestedLocales) ? supportedLocales : suggestedLocales;
121
120
  }
122
121
 
123
122
  function toLocaleAbbr({language, country, script}: Locale): string {
@@ -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 (!_.isString(command)) {
38
+ if (typeof command !== 'string') {
40
39
  throw new errors.InvalidArgumentError(`The 'command' argument is mandatory`);
41
40
  }
42
41
 
43
- const adbArgs = [...this.adb.executable.defaultArgs, 'shell', command, ..._.castArray(args)];
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 (_.isUndefined(this._screenStreamingProps)) {
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 (_.isEmpty(this._screenStreamingProps)) {
250
- if (!_.isUndefined(this._screenStreamingProps)) {
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
- _.truncate(udid, {
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 (!_.trim(await adb.shell(['which', SCREENRECORD_BINARY]))) {
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 _.toPairs(REQUIRED_GST_PLUGINS)) {
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 (!_.includes(stdout, modName)) {
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 = _.isUndefined(width) ? deviceInfo.width : parseInt(String(width), 10);
370
- const adjustedHeight = _.isUndefined(height) ? deviceInfo.height : parseInt(String(height), 10);
371
- const adjustedBitrate = _.isUndefined(bitRate) ? DEFAULT_BITRATE : parseInt(String(bitRate), 10);
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 (_.trim(stderr)) {
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 = !_.isEmpty(chunk);
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 (_.trim(stderr || stdout)) {
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
- await this.adb.shell([
69
- 'cmd',
70
- 'statusbar',
71
- cmd,
72
- ...(argsCallable ? _.castArray(argsCallable()) : []),
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 = _.fromPairs(
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: ${_.keys(statusBarCommands)}`,
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 = _.cloneDeep(DEFAULT_WINDOW_PROPERTIES);
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(_.trimEnd)) {
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 (_.trim(line).length === 0) {
152
+ if (String(line)?.trim()?.length === 0) {
153
153
  currentWindowName = null;
154
154
  continue;
155
155
  }
156
156
 
157
- if (currentWindowName && _.isArray(windows[currentWindowName])) {
157
+ if (currentWindowName && Array.isArray(windows[currentWindowName])) {
158
158
  windows[currentWindowName].push(line);
159
159
  }
160
160
  }
161
- if (_.isEmpty(windows)) {
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 _.toPairs(windows)) {
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]) => _.isNil(result[name as keyof SystemBarsResult]));
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: ${_.keys(windows)}`,
190
+ `Only the following windows are available: ${Object.keys(windows)}`,
191
191
  );
192
- result[window as keyof SystemBarsResult] = _.cloneDeep(DEFAULT_WINDOW_PROPERTIES);
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 = _.cloneDeep(ANDROID_DRIVER_CONSTRAINTS);
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 _.includes(
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 _.isArray(argNames) ? argNames : [argNames]) {
33
- if (!_.has(opts, argName)) {
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 (_.isArray(parsedCaps)) {
51
+ if (Array.isArray(parsedCaps)) {
52
52
  return parsedCaps;
53
- } else if (_.isString(cap)) {
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 || !_.isFunction(this.server?.getWebSocketHandlers)) {
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 _.keys(activeHandlers)) {
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 = _.isEmpty(x.prefix) ? x.message : `[${x.prefix}] ${x.message}`;
82
- return toLogRecord(x.timestamp ?? Date.now(), _.replace(msg, COLOR_CODE_PATTERN, ''));
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
  /**