appium-uiautomator2-driver 7.2.3 → 7.4.0

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 (66) 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 +1 -5
  4. package/build/lib/commands/app-management.js.map +1 -1
  5. package/build/lib/commands/element.d.ts.map +1 -1
  6. package/build/lib/commands/element.js +2 -6
  7. package/build/lib/commands/element.js.map +1 -1
  8. package/build/lib/commands/gestures.d.ts.map +1 -1
  9. package/build/lib/commands/gestures.js +2 -6
  10. package/build/lib/commands/gestures.js.map +1 -1
  11. package/build/lib/commands/keyboard.d.ts.map +1 -1
  12. package/build/lib/commands/keyboard.js +0 -8
  13. package/build/lib/commands/keyboard.js.map +1 -1
  14. package/build/lib/commands/screenshot.js +7 -8
  15. package/build/lib/commands/screenshot.js.map +1 -1
  16. package/build/lib/commands/types.d.ts +1 -1
  17. package/build/lib/commands/types.d.ts.map +1 -1
  18. package/build/lib/css-converter.js +9 -12
  19. package/build/lib/css-converter.js.map +1 -1
  20. package/build/lib/driver.d.ts +31 -30
  21. package/build/lib/driver.d.ts.map +1 -1
  22. package/build/lib/driver.js +72 -43
  23. package/build/lib/driver.js.map +1 -1
  24. package/build/lib/uiautomator2.d.ts.map +1 -1
  25. package/build/lib/uiautomator2.js +1 -2
  26. package/build/lib/uiautomator2.js.map +1 -1
  27. package/build/lib/{helpers.d.ts → utils/app.d.ts} +1 -1
  28. package/build/lib/utils/app.d.ts.map +1 -0
  29. package/build/lib/{helpers.js → utils/app.js} +1 -1
  30. package/build/lib/utils/app.js.map +1 -0
  31. package/build/lib/utils/index.d.ts +5 -0
  32. package/build/lib/utils/index.d.ts.map +1 -0
  33. package/build/lib/utils/index.js +21 -0
  34. package/build/lib/utils/index.js.map +1 -0
  35. package/build/lib/utils/lang.d.ts +15 -0
  36. package/build/lib/utils/lang.d.ts.map +1 -0
  37. package/build/lib/utils/lang.js +37 -0
  38. package/build/lib/utils/lang.js.map +1 -0
  39. package/build/lib/utils/memoize.d.ts +11 -0
  40. package/build/lib/utils/memoize.d.ts.map +1 -0
  41. package/build/lib/utils/memoize.js +26 -0
  42. package/build/lib/utils/memoize.js.map +1 -0
  43. package/build/lib/utils/object.d.ts +6 -0
  44. package/build/lib/utils/object.d.ts.map +1 -0
  45. package/build/lib/utils/object.js +15 -0
  46. package/build/lib/utils/object.js.map +1 -0
  47. package/lib/commands/app-management.ts +1 -2
  48. package/lib/commands/element.ts +2 -3
  49. package/lib/commands/gestures.ts +2 -3
  50. package/lib/commands/keyboard.ts +0 -5
  51. package/lib/commands/screenshot.ts +7 -7
  52. package/lib/commands/types.ts +1 -1
  53. package/lib/constraints.ts +1 -1
  54. package/lib/css-converter.ts +9 -9
  55. package/lib/driver.ts +124 -91
  56. package/lib/execute-method-map.ts +1 -1
  57. package/lib/uiautomator2.ts +5 -6
  58. package/lib/utils/index.ts +4 -0
  59. package/lib/utils/lang.ts +34 -0
  60. package/lib/utils/memoize.ts +26 -0
  61. package/lib/utils/object.ts +14 -0
  62. package/npm-shrinkwrap.json +95 -29
  63. package/package.json +7 -5
  64. package/build/lib/helpers.d.ts.map +0 -1
  65. package/build/lib/helpers.js.map +0 -1
  66. /package/lib/{helpers.ts → utils/app.ts} +0 -0
package/lib/driver.ts CHANGED
@@ -1,4 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
1
  import type {
3
2
  DefaultCreateSessionResult,
4
3
  DriverData,
@@ -10,13 +9,12 @@ import type {
10
9
  StringRecord,
11
10
  SessionCapabilities,
12
11
  } from '@appium/types';
13
- import {DEFAULT_ADB_PORT} from 'appium-adb';
12
+ import {DEFAULT_ADB_PORT, type ADB} from 'appium-adb';
14
13
  import {AndroidDriver, utils} from 'appium-android-driver';
15
14
  import {SETTINGS_HELPER_ID} from 'io.appium.settings';
16
15
  import {BaseDriver, DeviceSettings} from 'appium/driver';
17
16
  import {fs, mjpeg, util} from 'appium/support';
18
17
  import {retryInterval} from 'asyncbox';
19
- import _ from 'lodash';
20
18
  import os from 'node:os';
21
19
  import path from 'node:path';
22
20
  import {checkPortStatus, findAPortNotInUse} from 'portscanner';
@@ -24,7 +22,7 @@ import type {ExecError} from 'teen_process';
24
22
  import UIAUTOMATOR2_CONSTRAINTS, {type Uiautomator2Constraints} from './constraints';
25
23
  import {APKS_EXTENSION, APK_EXTENSION} from './extensions';
26
24
  import {newMethodMap} from './method-map';
27
- import {signApp} from './helpers';
25
+ import {assignDefaults, memoize, signApp} from './utils';
28
26
  import type {
29
27
  Uiautomator2Settings,
30
28
  Uiautomator2DeviceDetails,
@@ -241,8 +239,6 @@ const CHROME_NO_PROXY: RouteMatcher[] = [
241
239
  ['POST', new RegExp('^/session/[^/]+/se/log$')],
242
240
  ];
243
241
 
244
- const MEMOIZED_FUNCTIONS = ['getStatusBarHeight', 'getDevicePixelRatio'] as const;
245
-
246
242
  class AndroidUiautomator2Driver
247
243
  extends AndroidDriver
248
244
  implements ExternalDriver<Uiautomator2Constraints, string, StringRecord>
@@ -250,7 +246,7 @@ class AndroidUiautomator2Driver
250
246
  static newMethodMap = newMethodMap;
251
247
  static executeMethodMap = executeMethodMap;
252
248
 
253
- uiautomator2: UiAutomator2Server;
249
+ uiautomator2!: UiAutomator2Server;
254
250
 
255
251
  systemPort: number | undefined;
256
252
 
@@ -267,7 +263,7 @@ class AndroidUiautomator2Driver
267
263
  mobileGetActionHistory = mobileGetActionHistory;
268
264
  mobileScheduleAction = mobileScheduleAction;
269
265
  mobileUnscheduleAction = mobileUnscheduleAction;
270
- performActions = performActions;
266
+ performActions = performActions as AndroidDriver['performActions'];
271
267
  releaseActions = releaseActions;
272
268
 
273
269
  getAlertText = getAlertText;
@@ -281,23 +277,23 @@ class AndroidUiautomator2Driver
281
277
  mobileGetBatteryInfo = mobileGetBatteryInfo;
282
278
 
283
279
  active = active;
284
- getAttribute = getAttribute;
285
- elementEnabled = elementEnabled;
286
- elementDisplayed = elementDisplayed;
287
- elementSelected = elementSelected;
288
- getName = getName;
289
- getLocation = getLocation;
290
- getSize = getSize;
280
+ getAttribute = getAttribute as AndroidDriver['getAttribute'];
281
+ elementEnabled = elementEnabled as AndroidDriver['elementEnabled'];
282
+ elementDisplayed = elementDisplayed as AndroidDriver['elementDisplayed'];
283
+ elementSelected = elementSelected as AndroidDriver['elementSelected'];
284
+ getName = getName as AndroidDriver['getName'];
285
+ getLocation = getLocation as AndroidDriver['getLocation'];
286
+ getSize = getSize as AndroidDriver['getSize'];
291
287
  getElementRect = getElementRect;
292
288
  getElementScreenshot = getElementScreenshot;
293
- getText = getText;
294
- setValueImmediate = setValueImmediate;
295
- doSetElementValue = doSetElementValue;
296
- click = click;
289
+ getText = getText as AndroidDriver['getText'];
290
+ setValueImmediate = setValueImmediate as AndroidDriver['setValueImmediate'];
291
+ doSetElementValue = doSetElementValue as AndroidDriver['doSetElementValue'];
292
+ click = click as AndroidDriver['click'];
297
293
  clear = clear;
298
294
  mobileReplaceElementValue = mobileReplaceElementValue;
299
295
 
300
- doFindElementOrEls = doFindElementOrEls;
296
+ doFindElementOrEls = doFindElementOrEls as AndroidDriver['doFindElementOrEls'];
301
297
 
302
298
  mobileClickGesture = mobileClickGesture;
303
299
  mobileDoubleClickGesture = mobileDoubleClickGesture;
@@ -311,18 +307,18 @@ class AndroidUiautomator2Driver
311
307
  mobileScrollGesture = mobileScrollGesture;
312
308
  mobileSwipeGesture = mobileSwipeGesture;
313
309
 
314
- pressKeyCode = pressKeyCode;
315
- longPressKeyCode = longPressKeyCode;
310
+ pressKeyCode = pressKeyCode as AndroidDriver['pressKeyCode'];
311
+ longPressKeyCode = longPressKeyCode as AndroidDriver['longPressKeyCode'];
316
312
  mobilePressKey = mobilePressKey;
317
313
  mobileType = mobileType;
318
- doSendKeys = doSendKeys;
314
+ doSendKeys = doSendKeys as AndroidDriver['doSendKeys'];
319
315
  keyevent = keyevent;
320
316
 
321
317
  getPageSource = getPageSource;
322
318
  getOrientation = getOrientation;
323
319
  setOrientation = setOrientation;
324
- openNotifications = openNotifications;
325
- suspendChromedriverProxy = suspendChromedriverProxy as any;
320
+ openNotifications = openNotifications as AndroidDriver['openNotifications'];
321
+ suspendChromedriverProxy = suspendChromedriverProxy as AndroidDriver['suspendChromedriverProxy'];
326
322
  mobileGetDeviceInfo = mobileGetDeviceInfo;
327
323
  mobileResetAccessibilityCache = mobileResetAccessibilityCache;
328
324
  mobileListWindows = mobileListWindows;
@@ -331,7 +327,7 @@ class AndroidUiautomator2Driver
331
327
  getClipboard = getClipboard;
332
328
  setClipboard = setClipboard;
333
329
 
334
- setUrl = setUrl;
330
+ setUrl = setUrl as AndroidDriver['setUrl'];
335
331
  mobileDeepLink = mobileDeepLink;
336
332
  back = back;
337
333
 
@@ -342,10 +338,10 @@ class AndroidUiautomator2Driver
342
338
 
343
339
  getStatusBarHeight = getStatusBarHeight;
344
340
  getDevicePixelRatio = getDevicePixelRatio;
345
- getDisplayDensity = getDisplayDensity;
341
+ getDisplayDensity = getDisplayDensity as AndroidDriver['getDisplayDensity'];
346
342
  getViewPortRect = getViewPortRect;
347
- getWindowRect = getWindowRect;
348
- getWindowSize = getWindowSize;
343
+ getWindowRect = getWindowRect as AndroidDriver['getWindowRect'];
344
+ getWindowSize = getWindowSize as AndroidDriver['getWindowSize'];
349
345
  mobileViewPortRect = mobileViewPortRect;
350
346
 
351
347
  constructor(opts: InitialOpts = {} as InitialOpts, shouldValidateCaps = true) {
@@ -363,7 +359,7 @@ class AndroidUiautomator2Driver
363
359
  'css selector',
364
360
  '-android uiautomator',
365
361
  ];
366
- this.desiredCapConstraints = _.cloneDeep(UIAUTOMATOR2_CONSTRAINTS);
362
+ this.desiredCapConstraints = structuredClone(UIAUTOMATOR2_CONSTRAINTS);
367
363
  this.jwpProxyActive = false;
368
364
  this.jwpProxyAvoid = NO_PROXY;
369
365
  this._originalIme = null;
@@ -378,9 +374,8 @@ class AndroidUiautomator2Driver
378
374
  this.caps = {} as Uiautomator2DriverCaps;
379
375
  this.opts = opts as Uiautomator2DriverOpts;
380
376
  // memoize functions here, so that they are done on a per-instance basis
381
- for (const fn of MEMOIZED_FUNCTIONS) {
382
- this[fn] = _.memoize(this[fn]) as any;
383
- }
377
+ this.getStatusBarHeight = memoize(this.getStatusBarHeight);
378
+ this.getDevicePixelRatio = memoize(this.getDevicePixelRatio);
384
379
  }
385
380
 
386
381
  override get driverData() {
@@ -427,7 +422,7 @@ class AndroidUiautomator2Driver
427
422
  adbPort: DEFAULT_ADB_PORT,
428
423
  androidInstallTimeout: 90000,
429
424
  };
430
- _.defaults(this.opts, defaultOpts);
425
+ assignDefaults(this.opts as Record<string, unknown>, defaultOpts);
431
426
 
432
427
  this.opts.adbPort = this.opts.adbPort || DEFAULT_ADB_PORT;
433
428
  // get device udid for this session
@@ -439,15 +434,15 @@ class AndroidUiautomator2Driver
439
434
  // ADB instance
440
435
  this.adb = await this.createADB();
441
436
 
442
- if (this.isChromeSession) {
437
+ if (this.isChromeSession && this.opts.browserName) {
443
438
  this.log.info(`We're going to run a Chrome-based session`);
444
- const {pkg, activity: defaultActivity} = utils.getChromePkg(this.opts.browserName!);
439
+ const {pkg, activity: defaultActivity} = utils.getChromePkg(this.opts.browserName);
445
440
  let activity: string = defaultActivity;
446
441
  try {
447
442
  activity = await this.adb.resolveLaunchableActivity(pkg);
448
443
  } catch (e) {
449
444
  this.log.warn(
450
- `Using the default ${pkg} activity ${activity}. Original error: ${e.message}`,
445
+ `Using the default ${pkg} activity ${activity}. Original error: ${(e as Error).message}`,
451
446
  );
452
447
  }
453
448
  this.opts.appPackage = this.caps.appPackage = pkg;
@@ -504,7 +499,7 @@ class AndroidUiautomator2Driver
504
499
  pixelRatio,
505
500
  statBarHeight,
506
501
  viewportRect,
507
- deviceApiLevel: _.parseInt(apiVersion),
502
+ deviceApiLevel: Number.parseInt(String(apiVersion), 10),
508
503
  platformVersion,
509
504
  deviceManufacturer: manufacturer,
510
505
  deviceModel: model,
@@ -516,11 +511,16 @@ class AndroidUiautomator2Driver
516
511
  override async getSession(): Promise<SingularSessionData<Uiautomator2Constraints>> {
517
512
  const sessionData = await BaseDriver.prototype.getSession.call(this);
518
513
  this.log.debug('Getting session details from server to mix in');
519
- const uia2Data = (await this.uiautomator2!.jwproxy.command('/', 'GET', {})) as any;
514
+ const uia2Data = (await this.requireUiautomator2().jwproxy.command(
515
+ '/',
516
+ 'GET',
517
+ {},
518
+ )) as StringRecord;
520
519
  return {...sessionData, ...uia2Data};
521
520
  }
522
521
 
523
522
  async allocateSystemPort() {
523
+ const adb = this.requireAdb();
524
524
  const forwardPort = async (localPort: number) => {
525
525
  this.log.debug(
526
526
  `Forwarding UiAutomator2 Server port ${DEVICE_PORT} to local port ${localPort}`,
@@ -533,7 +533,7 @@ class AndroidUiautomator2Driver
533
533
  `old automation sessions on the same device must always be closed before starting new ones.`,
534
534
  );
535
535
  }
536
- await this.adb!.forwardPort(localPort, DEVICE_PORT);
536
+ await adb.forwardPort(localPort, DEVICE_PORT);
537
537
  };
538
538
 
539
539
  if (this.opts.systemPort) {
@@ -558,34 +558,35 @@ class AndroidUiautomator2Driver
558
558
  }
559
559
 
560
560
  async releaseSystemPort() {
561
- if (!this.systemPort || !this.adb) {
561
+ const adb = this.adb;
562
+ const systemPort = this.systemPort;
563
+ if (!systemPort || !adb) {
562
564
  return;
563
565
  }
564
566
 
565
567
  if (this.opts.systemPort) {
566
568
  // We assume if the systemPort is provided manually then it must be unique,
567
569
  // so there is no need for the explicit synchronization
568
- await this.adb.removePortForward(this.systemPort);
570
+ await adb.removePortForward(systemPort);
569
571
  } else {
570
- await DEVICE_PORT_ALLOCATION_GUARD(
571
- async () => await this.adb!.removePortForward(this.systemPort!),
572
- );
572
+ await DEVICE_PORT_ALLOCATION_GUARD(async () => await adb.removePortForward(systemPort));
573
573
  }
574
574
  }
575
575
 
576
576
  async allocateMjpegServerPort() {
577
577
  if (this.opts.mjpegServerPort) {
578
+ const adb = this.requireAdb();
578
579
  this.log.debug(
579
580
  `MJPEG broadcasting requested, forwarding MJPEG server port ${MJPEG_SERVER_DEVICE_PORT} ` +
580
581
  `to local port ${this.opts.mjpegServerPort}`,
581
582
  );
582
- await this.adb!.forwardPort(this.opts.mjpegServerPort, MJPEG_SERVER_DEVICE_PORT);
583
+ await adb.forwardPort(this.opts.mjpegServerPort, MJPEG_SERVER_DEVICE_PORT);
583
584
  }
584
585
  }
585
586
 
586
587
  async releaseMjpegServerPort() {
587
- if (this.opts.mjpegServerPort) {
588
- await this.adb!.removePortForward(this.opts.mjpegServerPort);
588
+ if (this.opts.mjpegServerPort && this.adb) {
589
+ await this.adb.removePortForward(this.opts.mjpegServerPort);
589
590
  }
590
591
  }
591
592
 
@@ -610,7 +611,7 @@ class AndroidUiautomator2Driver
610
611
  'Please check https://github.com/appium/appium/issues/13802 for more details. ' +
611
612
  'You could also set the "appium:ignoreHiddenApiPolicyError" capability to true in order to ' +
612
613
  'ignore this error, which might later lead to unexpected crashes or behavior of ' +
613
- `the automation server. Original error: ${err.message}`,
614
+ `the automation server. Original error: ${(err as Error).message}`,
614
615
  );
615
616
  }
616
617
  })(),
@@ -732,9 +733,19 @@ class AndroidUiautomator2Driver
732
733
  ): Promise<Uiautomator2SessionCaps> {
733
734
  const appInfo = await this.performSessionPreExecSetup();
734
735
  // set actual device name, udid, platform version, screen size, screen density, model and manufacturer details
736
+ const deviceName = this.adb?.curDeviceId;
737
+ const deviceUDID = this.opts.udid;
738
+ if (!deviceName) {
739
+ throw this.log.errorWithException(
740
+ 'Could not determine device name (ADB curDeviceId is empty)',
741
+ );
742
+ }
743
+ if (!deviceUDID) {
744
+ throw this.log.errorWithException('Device UDID is not set in session options');
745
+ }
735
746
  const sessionInfo: Uiautomator2SessionInfo = {
736
- deviceName: this.adb.curDeviceId!,
737
- deviceUDID: this.opts.udid!,
747
+ deviceName,
748
+ deviceUDID,
738
749
  };
739
750
  const capsWithSessionInfo = {
740
751
  ...caps,
@@ -742,9 +753,9 @@ class AndroidUiautomator2Driver
742
753
  };
743
754
  // Adding AUT info in the capabilities if it does not exist in caps
744
755
  if (appInfo) {
745
- for (const capName of ['appPackage', 'appActivity']) {
746
- if (!capsWithSessionInfo[capName] && appInfo[capName]) {
747
- capsWithSessionInfo[capName] = appInfo[capName];
756
+ for (const capName of ['appPackage', 'appActivity'] as const) {
757
+ if (!(capsWithSessionInfo as StringRecord)[capName] && appInfo[capName]) {
758
+ (capsWithSessionInfo as StringRecord)[capName] = appInfo[capName];
748
759
  }
749
760
  }
750
761
  }
@@ -755,7 +766,7 @@ class AndroidUiautomator2Driver
755
766
  try {
756
767
  return await this.getDeviceDetails();
757
768
  } catch (e) {
758
- this.log.warn(`Cannot fetch device details. Original error: ${e.message}`);
769
+ this.log.warn(`Cannot fetch device details. Original error: ${(e as Error).message}`);
759
770
  return {};
760
771
  }
761
772
  })();
@@ -786,7 +797,7 @@ class AndroidUiautomator2Driver
786
797
  } else {
787
798
  await this.uiautomator2.installServerApk(this.opts.uiautomator2ServerInstallTimeout);
788
799
  try {
789
- await this.adb!.addToDeviceIdleWhitelist(
800
+ await this.requireAdb().addToDeviceIdleWhitelist(
790
801
  SETTINGS_HELPER_ID,
791
802
  SERVER_PACKAGE_ID,
792
803
  SERVER_TEST_PACKAGE_ID,
@@ -829,21 +840,20 @@ class AndroidUiautomator2Driver
829
840
  await this.installOtherApks(otherApps);
830
841
  }
831
842
 
832
- if (this.opts.app) {
833
- if (
834
- (this.opts.noReset && !(await this.adb!.isAppInstalled(this.opts.appPackage!))) ||
835
- !this.opts.noReset
836
- ) {
843
+ if (this.opts.app && this.opts.appPackage) {
844
+ const adb = this.requireAdb();
845
+ const appPackage = this.opts.appPackage;
846
+ if ((this.opts.noReset && !(await adb.isAppInstalled(appPackage))) || !this.opts.noReset) {
837
847
  if (
838
848
  !this.opts.noSign &&
839
- !(await this.adb!.checkApkCert(this.opts.app, this.opts.appPackage!, {
849
+ !(await adb.checkApkCert(this.opts.app, appPackage, {
840
850
  requireDefaultCert: false,
841
851
  }))
842
852
  ) {
843
- await signApp(this.adb!, this.opts.app);
853
+ await signApp(adb, this.opts.app);
844
854
  }
845
855
  if (!this.opts.skipUninstall) {
846
- await this.adb!.uninstallApk(this.opts.appPackage!);
856
+ await adb.uninstallApk(appPackage);
847
857
  }
848
858
  await this.installAUT();
849
859
  } else {
@@ -865,28 +875,32 @@ class AndroidUiautomator2Driver
865
875
  }
866
876
 
867
877
  async ensureAppStarts() {
878
+ const adb = this.requireAdb();
879
+ const appPackage = this.opts.appPackage;
880
+ const appActivity = this.opts.appActivity;
881
+ if (!appPackage) {
882
+ throw this.log.errorWithException(
883
+ 'appPackage capability is required to start the application',
884
+ );
885
+ }
868
886
  // make sure we have an activity and package to wait for
869
- const appWaitPackage = this.opts.appWaitPackage || this.opts.appPackage;
870
- const appWaitActivity = this.opts.appWaitActivity || this.opts.appActivity;
887
+ const appWaitPackage = this.opts.appWaitPackage || appPackage;
888
+ const appWaitActivity = this.opts.appWaitActivity || appActivity;
871
889
  this.log.info(
872
- `Starting '${this.opts.appPackage}/${this.opts.appActivity}' ` +
873
- `and waiting for '${appWaitPackage}/${appWaitActivity}'`,
890
+ `Starting '${appPackage}${appActivity ? '/' + appActivity : ''}` +
891
+ `and waiting for '${appWaitPackage}${appWaitActivity ? '/' + appWaitActivity : ''}'`,
874
892
  );
875
893
 
876
- if (
877
- this.opts.noReset &&
878
- !this.opts.forceAppLaunch &&
879
- (await this.adb!.processExists(this.opts.appPackage!))
880
- ) {
894
+ if (this.opts.noReset && !this.opts.forceAppLaunch && (await adb.processExists(appPackage))) {
881
895
  this.log.info(
882
- `'${this.opts.appPackage}' is already running and noReset is enabled. ` +
896
+ `'${appPackage}' is already running and noReset is enabled. ` +
883
897
  `Set forceAppLaunch capability to true if the app must be forcefully restarted on session startup.`,
884
898
  );
885
899
  return;
886
900
  }
887
- await this.adb!.startApp({
888
- pkg: this.opts.appPackage!,
889
- activity: this.opts.appActivity,
901
+ await adb.startApp({
902
+ pkg: appPackage,
903
+ activity: appActivity,
890
904
  action: this.opts.intentAction || 'android.intent.action.MAIN',
891
905
  category: this.opts.intentCategory || 'android.intent.category.LAUNCHER',
892
906
  flags: this.opts.intentFlags || '0x10200000', // FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
@@ -906,7 +920,7 @@ class AndroidUiautomator2Driver
906
920
 
907
921
  const screenRecordingStopTasks = [
908
922
  async () => {
909
- if (!_.isEmpty(this._screenRecordingProperties)) {
923
+ if (this._screenRecordingProperties) {
910
924
  await this.stopRecordingScreen();
911
925
  }
912
926
  },
@@ -916,7 +930,7 @@ class AndroidUiautomator2Driver
916
930
  }
917
931
  },
918
932
  async () => {
919
- if (!_.isEmpty(this._screenStreamingProps)) {
933
+ if (this._screenStreamingProps) {
920
934
  await this.mobileStopScreenStreaming();
921
935
  }
922
936
  },
@@ -980,7 +994,7 @@ class AndroidUiautomator2Driver
980
994
  try {
981
995
  await this.adb.setIME(this._originalIme);
982
996
  } catch (e) {
983
- this.log.warn(`Cannot restore the original IME: ${e.message}`);
997
+ this.log.warn(`Cannot restore the original IME: ${(e as Error).message}`);
984
998
  }
985
999
  }
986
1000
  try {
@@ -1023,19 +1037,19 @@ class AndroidUiautomator2Driver
1023
1037
  // settings to the uiauto2 server already
1024
1038
  }
1025
1039
 
1026
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1027
- proxyActive(sessionId: string): boolean {
1040
+ override proxyActive(sessionId: string): boolean {
1041
+ void sessionId;
1028
1042
  // we always have an active proxy to the UiAutomator2 server
1029
1043
  return true;
1030
1044
  }
1031
1045
 
1032
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1033
- canProxy(sessionId: string): boolean {
1046
+ override canProxy(sessionId: string): boolean {
1047
+ void sessionId;
1034
1048
  // we can always proxy to the uiautomator2 server
1035
1049
  return true;
1036
1050
  }
1037
1051
 
1038
- getProxyAvoidList(): RouteMatcher[] {
1052
+ override getProxyAvoidList(): RouteMatcher[] {
1039
1053
  // we are maintaining two sets of NO_PROXY lists, one for chromedriver(CHROME_NO_PROXY)
1040
1054
  // and one for uiautomator2(NO_PROXY), based on current context will return related NO_PROXY list
1041
1055
  if (util.hasValue(this.chromedriver)) {
@@ -1054,24 +1068,43 @@ class AndroidUiautomator2Driver
1054
1068
  return this.jwpProxyAvoid;
1055
1069
  }
1056
1070
 
1057
- async updateSettings(settings: Uiautomator2Settings) {
1071
+ // @ts-expect-error narrower parameter type than the base class override allows
1072
+ override async updateSettings(settings: Uiautomator2Settings) {
1058
1073
  await this.settings.update(settings);
1059
- await this.uiautomator2!.jwproxy.command('/appium/settings', 'POST', {settings});
1074
+ await this.requireUiautomator2().jwproxy.command('/appium/settings', 'POST', {settings});
1060
1075
  }
1061
1076
 
1062
- async getSettings() {
1077
+ override async getSettings(): Promise<StringRecord> {
1063
1078
  const driverSettings = this.settings.getSettings();
1064
- const serverSettings = (await this.uiautomator2!.jwproxy.command(
1079
+ const serverSettings = (await this.requireUiautomator2().jwproxy.command(
1065
1080
  '/appium/settings',
1066
1081
  'GET',
1067
1082
  )) as Partial<Uiautomator2Settings>;
1068
- return {...driverSettings, ...serverSettings} as any;
1083
+ return {...driverSettings, ...serverSettings};
1069
1084
  }
1070
1085
 
1071
1086
  // needed to make the typechecker happy
1072
- async getAppiumSessionCapabilities(): Promise<SessionCapabilities<Uiautomator2Constraints>> {
1087
+ override async getAppiumSessionCapabilities(): Promise<
1088
+ SessionCapabilities<Uiautomator2Constraints>
1089
+ > {
1073
1090
  return (await super.getAppiumSessionCapabilities()) as SessionCapabilities<Uiautomator2Constraints>;
1074
1091
  }
1092
+
1093
+ private requireAdb(): ADB {
1094
+ const adb = this.adb;
1095
+ if (!adb) {
1096
+ throw this.log.errorWithException('ADB must be initialized before this operation');
1097
+ }
1098
+ return adb;
1099
+ }
1100
+
1101
+ private requireUiautomator2(): UiAutomator2Server {
1102
+ const server = this.uiautomator2;
1103
+ if (!server) {
1104
+ throw this.log.errorWithException('UiAutomator2 server is not initialized');
1105
+ }
1106
+ return server;
1107
+ }
1075
1108
  }
1076
1109
 
1077
1110
  export {AndroidUiautomator2Driver};
@@ -1,4 +1,4 @@
1
- import {ExecuteMethodMap} from '@appium/types';
1
+ import type {ExecuteMethodMap} from '@appium/types';
2
2
  import {AndroidDriver} from 'appium-android-driver';
3
3
 
4
4
  export const executeMethodMap = {
@@ -1,4 +1,3 @@
1
- import _ from 'lodash';
2
1
  import {JWProxy, errors} from 'appium/driver';
3
2
  import {sleep, waitForCondition} from 'asyncbox';
4
3
  import {
@@ -88,10 +87,10 @@ export class UiAutomator2Server {
88
87
  public readonly proxyReqRes: typeof UIA2Proxy.prototype.proxyReqRes;
89
88
  public readonly proxyCommand: typeof UIA2Proxy.prototype.command;
90
89
 
91
- private readonly host: string;
92
- private readonly systemPort: number;
93
- private readonly adb: ADB;
94
- private readonly disableWindowAnimation: boolean;
90
+ private readonly host!: string;
91
+ private readonly systemPort!: number;
92
+ private readonly adb!: ADB;
93
+ private readonly disableWindowAnimation!: boolean;
95
94
  private readonly disableSuppressAccessibilityService?: boolean;
96
95
  private readonly log: AppiumLogger;
97
96
  private instrumentationProcess: SubProcess | null = null;
@@ -409,7 +408,7 @@ export class UiAutomator2Server {
409
408
  if (this.disableWindowAnimation) {
410
409
  cmd.push('--no-window-animation');
411
410
  }
412
- if (_.isBoolean(this.disableSuppressAccessibilityService)) {
411
+ if (typeof this.disableSuppressAccessibilityService === 'boolean') {
413
412
  cmd.push(
414
413
  '-e',
415
414
  'DISABLE_SUPPRESS_ACCESSIBILITY_SERVICES',
@@ -0,0 +1,4 @@
1
+ export * from './app';
2
+ export * from './lang';
3
+ export * from './memoize';
4
+ export * from './object';
@@ -0,0 +1,34 @@
1
+ // TODO(appium server 3.4.1+): Replace local `isEmpty` / `escapeRegExp` with imports from `appium/support`
2
+ // once this driver declares that minimum server version.
3
+
4
+ /**
5
+ * Returns true when the value has no elements/properties.
6
+ *
7
+ * @param value - Value to check
8
+ * @returns `true` if the value is empty
9
+ */
10
+ export function isEmpty(value: unknown): boolean {
11
+ if (value == null) {
12
+ return true;
13
+ }
14
+ if (typeof value === 'string' || Array.isArray(value) || Buffer.isBuffer(value)) {
15
+ return value.length === 0;
16
+ }
17
+ if (value instanceof Map || value instanceof Set) {
18
+ return value.size === 0;
19
+ }
20
+ if (typeof value === 'object' || typeof value === 'function') {
21
+ return Object.keys(value).length === 0;
22
+ }
23
+ return true;
24
+ }
25
+
26
+ /**
27
+ * Escapes RegExp special characters in a string.
28
+ *
29
+ * @param value - Input string
30
+ * @returns Escaped string safe for RegExp source
31
+ */
32
+ export function escapeRegExp(value: string): string {
33
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
34
+ }
@@ -0,0 +1,26 @@
1
+ // TODO(appium server 3.4.1+): Replace local `memoize` with imports from `appium/support`
2
+ // once this driver declares that minimum server version.
3
+
4
+ /**
5
+ * Creates a memoized version of a function.
6
+ *
7
+ * @param fn - Function to memoize
8
+ * @param resolver - Optional cache key resolver. If omitted, the first argument is used as the cache key.
9
+ * @returns Memoized function with a mutable `.cache` map (compatible with lodash-style cache resets in tests).
10
+ */
11
+ export function memoize<Fn extends (...args: any[]) => any>(
12
+ fn: Fn,
13
+ resolver?: (...args: Parameters<Fn>) => unknown,
14
+ ): Fn & {cache: Map<unknown, ReturnType<Fn>>} {
15
+ const memoizedFn = function (this: unknown, ...args: Parameters<Fn>) {
16
+ const key = resolver ? resolver.apply(this, args) : args[0];
17
+ if (memoizedFn.cache.has(key)) {
18
+ return memoizedFn.cache.get(key) as ReturnType<Fn>;
19
+ }
20
+ const result = fn.apply(this, args);
21
+ memoizedFn.cache.set(key, result);
22
+ return result;
23
+ } as unknown as Fn & {cache: Map<unknown, ReturnType<Fn>>};
24
+ memoizedFn.cache = new Map<unknown, ReturnType<Fn>>();
25
+ return memoizedFn;
26
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Assigns own enumerable properties of `source` onto `target` only where `target[key] === undefined`
3
+ * (lodash `defaults` semantics).
4
+ */
5
+ export function assignDefaults<T extends Record<string, unknown>>(
6
+ target: T,
7
+ source: Record<string, unknown>,
8
+ ): void {
9
+ for (const key of Object.keys(source)) {
10
+ if (target[key] === undefined) {
11
+ (target as Record<string, unknown>)[key] = source[key];
12
+ }
13
+ }
14
+ }