appium-remote-debugger 15.9.1 → 15.10.1

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 (81) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/build/lib/mixins/connect.js +4 -4
  3. package/build/lib/mixins/connect.js.map +1 -1
  4. package/build/lib/mixins/execute.d.ts.map +1 -1
  5. package/build/lib/mixins/execute.js +2 -2
  6. package/build/lib/mixins/execute.js.map +1 -1
  7. package/build/lib/mixins/message-handlers.d.ts.map +1 -1
  8. package/build/lib/mixins/message-handlers.js +3 -2
  9. package/build/lib/mixins/message-handlers.js.map +1 -1
  10. package/build/lib/mixins/misc.d.ts +2 -1
  11. package/build/lib/mixins/misc.d.ts.map +1 -1
  12. package/build/lib/mixins/misc.js +5 -4
  13. package/build/lib/mixins/misc.js.map +1 -1
  14. package/build/lib/mixins/navigate.d.ts.map +1 -1
  15. package/build/lib/mixins/navigate.js +5 -4
  16. package/build/lib/mixins/navigate.js.map +1 -1
  17. package/build/lib/remote-debugger-real-device.js.map +1 -1
  18. package/build/lib/remote-debugger.d.ts +2 -2
  19. package/build/lib/remote-debugger.d.ts.map +1 -1
  20. package/build/lib/remote-debugger.js +5 -7
  21. package/build/lib/remote-debugger.js.map +1 -1
  22. package/build/lib/rpc/rpc-client-real-device-shim.js +3 -3
  23. package/build/lib/rpc/rpc-client-real-device-shim.js.map +1 -1
  24. package/build/lib/rpc/rpc-client.d.ts.map +1 -1
  25. package/build/lib/rpc/rpc-client.js +16 -16
  26. package/build/lib/rpc/rpc-client.js.map +1 -1
  27. package/build/lib/rpc/rpc-message-handler.d.ts.map +1 -1
  28. package/build/lib/rpc/rpc-message-handler.js +3 -4
  29. package/build/lib/rpc/rpc-message-handler.js.map +1 -1
  30. package/build/lib/utils/errors.d.ts +13 -0
  31. package/build/lib/utils/errors.d.ts.map +1 -0
  32. package/build/lib/utils/errors.js +24 -0
  33. package/build/lib/utils/errors.js.map +1 -0
  34. package/build/lib/utils/index.d.ts +7 -0
  35. package/build/lib/utils/index.d.ts.map +1 -0
  36. package/build/lib/utils/index.js +25 -0
  37. package/build/lib/utils/index.js.map +1 -0
  38. package/build/lib/utils/inspector.d.ts +30 -0
  39. package/build/lib/utils/inspector.d.ts.map +1 -0
  40. package/build/lib/utils/inspector.js +91 -0
  41. package/build/lib/utils/inspector.js.map +1 -0
  42. package/build/lib/utils/javascript.d.ts +25 -0
  43. package/build/lib/utils/javascript.d.ts.map +1 -0
  44. package/build/lib/utils/javascript.js +90 -0
  45. package/build/lib/utils/javascript.js.map +1 -0
  46. package/build/lib/utils/module.d.ts +20 -0
  47. package/build/lib/utils/module.d.ts.map +1 -0
  48. package/build/lib/utils/module.js +36 -0
  49. package/build/lib/utils/module.js.map +1 -0
  50. package/build/lib/utils/object.d.ts +29 -0
  51. package/build/lib/utils/object.d.ts.map +1 -0
  52. package/build/lib/utils/object.js +54 -0
  53. package/build/lib/utils/object.js.map +1 -0
  54. package/build/lib/utils/platform.d.ts +7 -0
  55. package/build/lib/utils/platform.d.ts.map +1 -0
  56. package/build/lib/utils/platform.js +13 -0
  57. package/build/lib/utils/platform.js.map +1 -0
  58. package/lib/appium-ios-device.d.ts +12 -0
  59. package/lib/mixins/connect.ts +6 -6
  60. package/lib/mixins/execute.ts +2 -3
  61. package/lib/mixins/message-handlers.ts +4 -3
  62. package/lib/mixins/misc.ts +6 -5
  63. package/lib/mixins/navigate.ts +10 -5
  64. package/lib/remote-debugger-real-device.ts +1 -1
  65. package/lib/remote-debugger.ts +8 -10
  66. package/lib/rpc/rpc-client-real-device-shim.ts +3 -3
  67. package/lib/rpc/rpc-client.ts +16 -23
  68. package/lib/rpc/rpc-message-handler.ts +3 -4
  69. package/lib/utils/errors.ts +19 -0
  70. package/lib/utils/index.ts +16 -0
  71. package/lib/utils/inspector.ts +99 -0
  72. package/lib/utils/javascript.ts +89 -0
  73. package/lib/utils/module.ts +33 -0
  74. package/lib/utils/object.ts +55 -0
  75. package/lib/utils/platform.ts +10 -0
  76. package/package.json +9 -3
  77. package/build/lib/utils.d.ts +0 -167
  78. package/build/lib/utils.d.ts.map +0 -1
  79. package/build/lib/utils.js +0 -422
  80. package/build/lib/utils.js.map +0 -1
  81. package/lib/utils.ts +0 -443
@@ -1,19 +1,11 @@
1
1
  import {RemoteMessages} from './remote-messages';
2
- import {waitForCondition} from 'asyncbox';
2
+ import {sleep, waitForCondition, withTimeout} from 'asyncbox';
3
3
  import {log} from '../logger';
4
4
  import RpcMessageHandler from './rpc-message-handler';
5
5
  import {util, timing} from '@appium/support';
6
6
  import {EventEmitter} from 'node:events';
7
7
  import AsyncLock from 'async-lock';
8
- import {
9
- convertJavascriptEvaluationResult,
10
- delay,
11
- defaults,
12
- isEmpty,
13
- isPlainObject,
14
- truncateString,
15
- withTimeout,
16
- } from '../utils';
8
+ import {convertJavascriptEvaluationResult, defaults} from '../utils';
17
9
  import type {StringRecord} from '@appium/types';
18
10
  import type {
19
11
  AppIdKey,
@@ -325,7 +317,7 @@ export class RpcClient {
325
317
  await waitForCondition(
326
318
  () => {
327
319
  target = this.getTarget(appIdKey, pageIdKey);
328
- return !isEmpty(target);
320
+ return !util.isEmpty(target);
329
321
  },
330
322
  {
331
323
  waitMs,
@@ -339,6 +331,7 @@ export class RpcClient {
339
331
  }
340
332
  throw new Error(
341
333
  `No targets could be matched for the app '${appIdKey}' and page '${pageIdKey}' after ${waitMs}ms`,
334
+ {cause: err},
342
335
  );
343
336
  }
344
337
  }
@@ -441,7 +434,7 @@ export class RpcClient {
441
434
  __selector: cmd.__selector,
442
435
  };
443
436
 
444
- const hasSocketData = isPlainObject(cmd.__argument?.WIRSocketDataKey);
437
+ const hasSocketData = util.isPlainObject(cmd.__argument?.WIRSocketDataKey);
445
438
  if (hasSocketData) {
446
439
  // make sure the message being sent has all the information that is needed
447
440
  const socketData = cmd.__argument.WIRSocketDataKey as StringRecord;
@@ -463,7 +456,7 @@ export class RpcClient {
463
456
  // a protocol change. Log and check during testing
464
457
  log.error(
465
458
  `Received error from send that is not being waited for (id: ${msgId}): ` +
466
- truncateString(JSON.stringify(err), DATA_LOG_LENGTH),
459
+ util.truncateString(JSON.stringify(err), DATA_LOG_LENGTH),
467
460
  );
468
461
  // reject, though it is very rare that this will be triggered, since
469
462
  // the promise is resolved directly after send. On the off chance,
@@ -479,7 +472,7 @@ export class RpcClient {
479
472
  return reject(err);
480
473
  }
481
474
  log.debug(
482
- `Received response from send (id: ${msgId}): '${truncateString(JSON.stringify(args), DATA_LOG_LENGTH)}'`,
475
+ `Received response from send (id: ${msgId}): '${util.truncateString(JSON.stringify(args), DATA_LOG_LENGTH)}'`,
483
476
  );
484
477
  resolve(args);
485
478
  },
@@ -492,7 +485,7 @@ export class RpcClient {
492
485
  );
493
486
  }
494
487
  log.debug(
495
- `Received data response from send (id: ${msgId}): '${truncateString(JSON.stringify(value), DATA_LOG_LENGTH)}'`,
488
+ `Received data response from send (id: ${msgId}): '${util.truncateString(JSON.stringify(value), DATA_LOG_LENGTH)}'`,
496
489
  );
497
490
  resolve(value);
498
491
  });
@@ -580,7 +573,7 @@ export class RpcClient {
580
573
  }
581
574
  const {appIdKey, pageIdKey, pageReadinessDetector} = pendingPageTargetDetails;
582
575
 
583
- if (!isPlainObject(this.targets[appIdKey])) {
576
+ if (!util.isPlainObject(this.targets[appIdKey])) {
584
577
  this.targets[appIdKey] = {
585
578
  lock: new AsyncLock({maxOccupationTime: this._targetCreationTimeoutMs}),
586
579
  } as PagesToTargets;
@@ -610,8 +603,8 @@ export class RpcClient {
610
603
 
611
604
  this._provisionedPages.add(pageIdKey);
612
605
  try {
613
- await this.targets[appIdKey].lock.acquire(pageIdKey, async () => {
614
- let wasInitialized = false;
606
+ await this.targets[appIdKey].lock.acquire(String(pageIdKey), async () => {
607
+ let wasInitialized: boolean;
615
608
  try {
616
609
  wasInitialized = await this._initializePage(appIdKey, pageIdKey, targetInfo.targetId);
617
610
  } finally {
@@ -666,7 +659,7 @@ export class RpcClient {
666
659
  }
667
660
 
668
661
  try {
669
- await this.targets[appIdKey].lock.acquire(pageIdKey, async () => {
662
+ await this.targets[appIdKey].lock.acquire(String(pageIdKey), async () => {
670
663
  let wasInitialized = false;
671
664
  try {
672
665
  if (this._provisionedPages.has(pageIdKey)) {
@@ -929,7 +922,7 @@ export class RpcClient {
929
922
  const [connectedAppIdKey, pageDict] = await this.send('connectToApp', {appIdKey});
930
923
  // sometimes the connect logic happens, but with an empty dictionary
931
924
  // which leads to the remote debugger getting disconnected, and into a loop
932
- if (isEmpty(pageDict)) {
925
+ if (util.isEmpty(pageDict)) {
933
926
  reject(new Error(EMPTY_PAGE_DICTIONARY_ERROR));
934
927
  } else {
935
928
  resolve([connectedAppIdKey, pageDict]);
@@ -993,10 +986,10 @@ export class RpcClient {
993
986
  const lock = appTargetsMap.lock;
994
987
  const timer = new timing.Timer().start();
995
988
  await Promise.all([
996
- lock.acquire(pageIdKey, async () => await delay(0)),
989
+ lock.acquire(String(pageIdKey), async () => await sleep(0)),
997
990
  this._pageSelectionLock.acquire(
998
991
  toPageSelectionKey(appIdKey, pageIdKey),
999
- async () => await delay(0),
992
+ async () => await sleep(0),
1000
993
  ),
1001
994
  ]);
1002
995
  const durationMs = timer.getDuration().asMilliSeconds;
@@ -1230,7 +1223,7 @@ export class RpcClient {
1230
1223
  );
1231
1224
  return;
1232
1225
  }
1233
- await delay(100);
1226
+ await sleep(100);
1234
1227
  }
1235
1228
  log.warn(
1236
1229
  `Page '${pageIdKey}' for app '${appIdKey}' is not ready after ` +
@@ -1,6 +1,5 @@
1
1
  import {EventEmitter} from 'node:events';
2
2
  import {log} from '../logger';
3
- import {isPlainObject, truncateString} from '../utils';
4
3
  import {util} from '@appium/support';
5
4
  import type {StringRecord} from '@appium/types';
6
5
 
@@ -106,8 +105,8 @@ export default class RpcMessageHandler extends EventEmitter {
106
105
  try {
107
106
  return JSON.parse(plist.__argument.WIRMessageDataKey.toString('utf8'));
108
107
  } catch (err: any) {
109
- log.error(`Unparseable message data: ${truncateString(JSON.stringify(plist), 100)}`);
110
- throw new Error(`Unable to parse message data: ${err.message}`);
108
+ log.error(`Unparseable message data: ${util.truncateString(JSON.stringify(plist), 100)}`);
109
+ throw new Error(`Unable to parse message data: ${err.message}`, {cause: err});
111
110
  }
112
111
  }
113
112
 
@@ -213,7 +212,7 @@ export default class RpcMessageHandler extends EventEmitter {
213
212
  return new Error(message);
214
213
  }
215
214
  if (dataKey.error) {
216
- if (isPlainObject(dataKey.error)) {
215
+ if (util.isPlainObject(dataKey.error)) {
217
216
  const dataKeyError = dataKey.error as DataErrorMessage;
218
217
  const error = new Error(defaultMessage);
219
218
  for (const key of Object.keys(dataKeyError)) {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Thrown when a cancellable delay is cancelled.
3
+ */
4
+ export class DelayCancellation extends Error {
5
+ constructor(message: string = 'Delay cancelled') {
6
+ super(message);
7
+ this.name = 'DelayCancellation';
8
+ }
9
+ }
10
+
11
+ /**
12
+ * Error thrown when an async operation exceeds the configured timeout.
13
+ */
14
+ export class TimeoutError extends Error {
15
+ constructor(message: string = 'Operation timed out') {
16
+ super(message);
17
+ this.name = 'TimeoutError';
18
+ }
19
+ }
@@ -0,0 +1,16 @@
1
+ export {DelayCancellation, TimeoutError} from './errors';
2
+ export {defaults, deepEqual, checkParams} from './object';
3
+
4
+ export {
5
+ WEB_CONTENT_BUNDLE_ID,
6
+ appInfoFromDict,
7
+ pageArrayFromDict,
8
+ appIdsForBundle,
9
+ } from './inspector';
10
+ export {
11
+ RESPONSE_LOG_LENGTH,
12
+ simpleStringify,
13
+ convertJavascriptEvaluationResult,
14
+ } from './javascript';
15
+ export {getModuleRoot, getModuleProperties} from './module';
16
+ export {canUseWebInspectorShim} from './platform';
@@ -0,0 +1,99 @@
1
+ import {util} from '@appium/support';
2
+ import type {StringRecord} from '@appium/types';
3
+ import type {AppInfo, AppDict, Page} from '../types';
4
+
5
+ export const WEB_CONTENT_BUNDLE_ID = 'com.apple.WebKit.WebContent';
6
+
7
+ const INACTIVE_APP_CODE = 0;
8
+
9
+ // values for the page `WIRTypeKey` entry
10
+ const ACCEPTED_PAGE_TYPES = [
11
+ 'WIRTypeWeb', // up to iOS 11.3
12
+ 'WIRTypeWebPage', // iOS 11.4
13
+ 'WIRTypePage', // iOS 11.4 webview
14
+ ];
15
+
16
+ /**
17
+ * Takes a dictionary from the remote debugger and converts it into a more
18
+ * manageable AppInfo object with understandable keys.
19
+ *
20
+ * @param dict - Dictionary from the remote debugger containing application information.
21
+ * @returns A tuple containing the application ID and the AppInfo object.
22
+ */
23
+ export function appInfoFromDict(dict: Record<string, any>): [string, AppInfo] {
24
+ const id = dict.WIRApplicationIdentifierKey;
25
+ const isProxy =
26
+ typeof dict.WIRIsApplicationProxyKey === 'string'
27
+ ? dict.WIRIsApplicationProxyKey.toLowerCase() === 'true'
28
+ : dict.WIRIsApplicationProxyKey;
29
+ // automation enabled can be either from the keys
30
+ // - WIRRemoteAutomationEnabledKey (boolean)
31
+ // - WIRAutomationAvailabilityKey (string or boolean)
32
+ let isAutomationEnabled: boolean | string = !!dict.WIRRemoteAutomationEnabledKey;
33
+ if (Object.hasOwn(dict, 'WIRAutomationAvailabilityKey')) {
34
+ if (typeof dict.WIRAutomationAvailabilityKey === 'string') {
35
+ isAutomationEnabled =
36
+ dict.WIRAutomationAvailabilityKey === 'WIRAutomationAvailabilityUnknown'
37
+ ? 'Unknown'
38
+ : dict.WIRAutomationAvailabilityKey === 'WIRAutomationAvailabilityAvailable';
39
+ } else {
40
+ isAutomationEnabled = !!dict.WIRAutomationAvailabilityKey;
41
+ }
42
+ }
43
+ const entry: AppInfo = {
44
+ id,
45
+ isProxy,
46
+ name: dict.WIRApplicationNameKey,
47
+ bundleId: dict.WIRApplicationBundleIdentifierKey,
48
+ hostId: dict.WIRHostApplicationIdentifierKey,
49
+ isActive: dict.WIRIsApplicationActiveKey !== INACTIVE_APP_CODE,
50
+ isAutomationEnabled,
51
+ };
52
+
53
+ return [id, entry];
54
+ }
55
+
56
+ /**
57
+ * Takes a dictionary from the remote debugger and converts it into an array
58
+ * of Page objects with understandable keys. Filters out non-web pages.
59
+ *
60
+ * @param pageDict - Dictionary from the remote debugger containing page information.
61
+ * @returns An array of Page objects representing the available pages.
62
+ */
63
+ export function pageArrayFromDict(pageDict: StringRecord): Page[] {
64
+ return (
65
+ Object.values(pageDict)
66
+ // count only WIRTypeWeb pages and ignore all others (WIRTypeJavaScript etc)
67
+ .filter(
68
+ (dict) => dict.WIRTypeKey === undefined || ACCEPTED_PAGE_TYPES.includes(dict.WIRTypeKey),
69
+ )
70
+ .map((dict) => ({
71
+ id: dict.WIRPageIdentifierKey,
72
+ title: dict.WIRTitleKey,
73
+ url: dict.WIRURLKey,
74
+ isKey: dict.WIRConnectionIdentifierKey !== undefined,
75
+ }))
76
+ );
77
+ }
78
+
79
+ /**
80
+ * Finds all application identifier keys that match the given bundle ID.
81
+ * If no matches are found and the bundle ID is not WEB_CONTENT_BUNDLE_ID,
82
+ * falls back to searching for WEB_CONTENT_BUNDLE_ID.
83
+ *
84
+ * @param bundleId - The bundle identifier to search for.
85
+ * @param appDict - The application dictionary to search in.
86
+ * @returns An array of unique application identifier keys matching the bundle ID.
87
+ */
88
+ export function appIdsForBundle(bundleId: string, appDict: AppDict): string[] {
89
+ const appIds: string[] = Object.entries(appDict)
90
+ .filter(([, data]) => data.bundleId === bundleId)
91
+ .map(([key]) => key);
92
+
93
+ // if nothing is found, try to get the generic app
94
+ if (appIds.length === 0 && bundleId !== WEB_CONTENT_BUNDLE_ID) {
95
+ return appIdsForBundle(WEB_CONTENT_BUNDLE_ID, appDict);
96
+ }
97
+
98
+ return util.uniq(appIds);
99
+ }
@@ -0,0 +1,89 @@
1
+ import {errorFromMJSONWPStatusCode} from '@appium/base-driver';
2
+ import {util} from '@appium/support';
3
+
4
+ export const RESPONSE_LOG_LENGTH = 100;
5
+
6
+ /**
7
+ * Converts a value to a best-effort JSON string for logging, removing noisy
8
+ * function properties from cloneable objects when possible.
9
+ *
10
+ * Falls back to `String(value)` when JSON serialization returns `undefined`
11
+ * or throws (for example, for functions, symbols, or circular structures).
12
+ *
13
+ * @param value - The value to stringify.
14
+ * @param multiline - If true, formats JSON output with indentation. Defaults to false.
15
+ * @returns A string representation suitable for logging.
16
+ */
17
+ export function simpleStringify(value: any, multiline: boolean = false): string {
18
+ const stringify = (val: any): string => {
19
+ try {
20
+ return multiline
21
+ ? (JSON.stringify(val, null, 2) ?? String(val))
22
+ : (JSON.stringify(val) ?? String(val));
23
+ } catch {
24
+ return String(val);
25
+ }
26
+ };
27
+
28
+ if (!value) {
29
+ return stringify(value);
30
+ }
31
+
32
+ let cleanValue = value;
33
+ if (typeof value === 'object' && value !== null) {
34
+ try {
35
+ cleanValue = removeNoisyProperties(structuredClone(value));
36
+ } catch {
37
+ // Fall back to the original value when cloning fails (e.g., non-cloneable graph entries).
38
+ cleanValue = value;
39
+ }
40
+ }
41
+ return stringify(cleanValue);
42
+ }
43
+
44
+ /**
45
+ * Converts the result from a JavaScript evaluation in the remote debugger
46
+ * into a usable format. Handles errors, serialization, and cleans up noisy
47
+ * function properties.
48
+ *
49
+ * @param res - The raw result from the remote debugger's JavaScript evaluation.
50
+ * @returns The cleaned and converted result value.
51
+ * @throws Error if the result is undefined, has an unexpected type, or contains
52
+ * an error status code.
53
+ */
54
+ export function convertJavascriptEvaluationResult(res: any): any {
55
+ if (res === undefined) {
56
+ throw new Error(
57
+ `Did not get OK result from remote debugger. Result was: ${util.truncateString(simpleStringify(res), RESPONSE_LOG_LENGTH)}`,
58
+ );
59
+ } else if (typeof res === 'string') {
60
+ try {
61
+ res = JSON.parse(res);
62
+ } catch {
63
+ // we might get a serialized object, but we might not
64
+ // if we get here, it is just a value
65
+ }
66
+ } else if ((typeof res !== 'object' && typeof res !== 'function') || res === null) {
67
+ throw new Error(`Result has unexpected type: (${typeof res}).`);
68
+ }
69
+
70
+ if (Object.hasOwn(res, 'status') && res.status !== 0) {
71
+ // we got some form of error.
72
+ throw errorFromMJSONWPStatusCode(res.status, res.value.message || res.value);
73
+ }
74
+
75
+ // with either have an object with a `value` property (even if `null`),
76
+ // or a plain object
77
+ const value = Object.hasOwn(res, 'value') ? res.value : res;
78
+ return removeNoisyProperties(value);
79
+ }
80
+
81
+ function removeNoisyProperties<T>(obj: T): T {
82
+ if (obj && typeof obj === 'object') {
83
+ const record = obj as Record<string, unknown>;
84
+ for (const property of ['ceil', 'clone', 'floor', 'round', 'scale', 'toString']) {
85
+ delete record[property];
86
+ }
87
+ }
88
+ return obj;
89
+ }
@@ -0,0 +1,33 @@
1
+ import {util, node} from '@appium/support';
2
+ import nodeFs from 'node:fs';
3
+ import path from 'node:path';
4
+ import type {StringRecord} from '@appium/types';
5
+
6
+ const MODULE_NAME = 'appium-remote-debugger';
7
+
8
+ function resolveModuleRoot(): string {
9
+ const root = node.getModuleRootSync(MODULE_NAME, __filename);
10
+ if (!root) {
11
+ throw new Error(`Cannot find the root folder of the ${MODULE_NAME} Node.js module`);
12
+ }
13
+ return root;
14
+ }
15
+
16
+ /**
17
+ * Calculates the path to the current module's root folder.
18
+ * The result is memoized for performance.
19
+ *
20
+ * @returns The full path to the module root directory.
21
+ * @throws Error if the module root folder cannot be determined.
22
+ */
23
+ export const getModuleRoot = util.memoize(resolveModuleRoot);
24
+
25
+ /**
26
+ * Reads and parses the package.json file from the module root.
27
+ *
28
+ * @returns The parsed package.json contents as a StringRecord.
29
+ */
30
+ export function getModuleProperties(): StringRecord {
31
+ const fullPath = path.resolve(getModuleRoot(), 'package.json');
32
+ return JSON.parse(nodeFs.readFileSync(fullPath, 'utf8'));
33
+ }
@@ -0,0 +1,55 @@
1
+ import {util} from '@appium/support';
2
+ import {isDeepStrictEqual} from 'node:util';
3
+ import type {StringRecord} from '@appium/types';
4
+
5
+ /**
6
+ * Creates a shallow object where undefined keys from `target` are filled
7
+ * from `defaultsObj`.
8
+ *
9
+ * @param target - The object with priority values.
10
+ * @param defaultsObj - The object providing fallback values.
11
+ * @returns A new object containing merged defaulted values.
12
+ */
13
+ export function defaults<T extends Record<string, any>, U extends Record<string, any>>(
14
+ target: T,
15
+ defaultsObj: U,
16
+ ): T & U {
17
+ const result = {...target} as T & U;
18
+ for (const [key, value] of Object.entries(defaultsObj)) {
19
+ if (result[key as keyof (T & U)] === undefined) {
20
+ (result as any)[key] = value;
21
+ }
22
+ }
23
+ return result;
24
+ }
25
+
26
+ /**
27
+ * Performs deep strict equality comparison.
28
+ *
29
+ * @param a - First value.
30
+ * @param b - Second value.
31
+ * @returns True when both values are deeply equal.
32
+ */
33
+ export function deepEqual(a: unknown, b: unknown): boolean {
34
+ return isDeepStrictEqual(a, b);
35
+ }
36
+
37
+ /**
38
+ * Validates that all parameters in the provided object have non-nil values.
39
+ * Throws an error if any parameters are missing (null or undefined).
40
+ *
41
+ * @template T - The type of the parameters object.
42
+ * @param params - An object containing parameters to validate.
43
+ * @returns The same parameters object if all values are valid.
44
+ * @throws Error if any parameters are missing, listing all missing parameter names.
45
+ */
46
+ export function checkParams<T extends StringRecord>(params: T): T {
47
+ // check if all parameters have a value
48
+ const errors = Object.entries(params)
49
+ .filter(([, value]) => value == null)
50
+ .map(([param]) => param);
51
+ if (errors.length) {
52
+ throw new Error(`Missing ${util.pluralize('parameter', errors.length)}: ${errors.join(', ')}`);
53
+ }
54
+ return params;
55
+ }
@@ -0,0 +1,10 @@
1
+ import {util} from '@appium/support';
2
+
3
+ /**
4
+ * Determines if the WebInspector shim can be used based on the provided iOS platform version.
5
+ * @param platformVersion - The iOS platform version string (e.g., "18.0", "17.5.1"), or undefined/null when unknown.
6
+ * @returns true if the WebInspector shim can be used, false otherwise
7
+ */
8
+ export function canUseWebInspectorShim(platformVersion: string | undefined | null): boolean {
9
+ return !!platformVersion && util.compareVersions(platformVersion, '>=', '18.0');
10
+ }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "keywords": [
5
5
  "appium"
6
6
  ],
7
- "version": "15.9.1",
7
+ "version": "15.10.1",
8
8
  "author": "Appium Contributors",
9
9
  "license": "Apache-2.0",
10
10
  "repository": {
@@ -33,10 +33,10 @@
33
33
  ],
34
34
  "dependencies": {
35
35
  "@appium/base-driver": "^10.0.0-rc.1",
36
- "@appium/support": "^7.0.0-rc.1",
36
+ "@appium/support": "^7.2.2",
37
37
  "appium-ios-device": "^3.0.0",
38
38
  "async-lock": "^1.4.1",
39
- "asyncbox": "^6.1.0",
39
+ "asyncbox": "^6.3.0",
40
40
  "glob": "^13.0.0",
41
41
  "teen_process": "^4.0.4"
42
42
  },
@@ -72,8 +72,14 @@
72
72
  "@appium/types": "^1.0.0-rc.1",
73
73
  "@semantic-release/changelog": "^6.0.1",
74
74
  "@semantic-release/git": "^10.0.1",
75
+ "@types/async-lock": "^1.4.2",
76
+ "@types/chai": "^5.2.3",
77
+ "@types/chai-as-promised": "^8.0.2",
78
+ "@types/finalhandler": "^1.2.4",
75
79
  "@types/mocha": "^10.0.1",
76
80
  "@types/node": "^25.0.0",
81
+ "@types/serve-static": "^2.2.0",
82
+ "@types/sinon": "^21.0.1",
77
83
  "appium-ios-simulator": "^8.0.0",
78
84
  "chai": "^6.0.0",
79
85
  "chai-as-promised": "^8.0.0",