appium-remote-debugger 15.9.0 → 15.10.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.
- package/CHANGELOG.md +12 -0
- package/build/lib/mixins/connect.js +4 -4
- package/build/lib/mixins/connect.js.map +1 -1
- package/build/lib/mixins/execute.d.ts.map +1 -1
- package/build/lib/mixins/execute.js +2 -2
- package/build/lib/mixins/execute.js.map +1 -1
- package/build/lib/mixins/message-handlers.d.ts.map +1 -1
- package/build/lib/mixins/message-handlers.js +3 -2
- package/build/lib/mixins/message-handlers.js.map +1 -1
- package/build/lib/mixins/misc.d.ts +2 -1
- package/build/lib/mixins/misc.d.ts.map +1 -1
- package/build/lib/mixins/misc.js +5 -4
- package/build/lib/mixins/misc.js.map +1 -1
- package/build/lib/mixins/navigate.d.ts.map +1 -1
- package/build/lib/mixins/navigate.js +3 -2
- package/build/lib/mixins/navigate.js.map +1 -1
- package/build/lib/remote-debugger-real-device.js.map +1 -1
- package/build/lib/remote-debugger.d.ts.map +1 -1
- package/build/lib/remote-debugger.js +5 -7
- package/build/lib/remote-debugger.js.map +1 -1
- package/build/lib/rpc/rpc-client-real-device-shim.js +3 -3
- package/build/lib/rpc/rpc-client-real-device-shim.js.map +1 -1
- package/build/lib/rpc/rpc-client.d.ts.map +1 -1
- package/build/lib/rpc/rpc-client.js +16 -16
- package/build/lib/rpc/rpc-client.js.map +1 -1
- package/build/lib/rpc/rpc-message-handler.d.ts.map +1 -1
- package/build/lib/rpc/rpc-message-handler.js +3 -4
- package/build/lib/rpc/rpc-message-handler.js.map +1 -1
- package/build/lib/utils/async.d.ts +11 -0
- package/build/lib/utils/async.d.ts.map +1 -0
- package/build/lib/utils/async.js +28 -0
- package/build/lib/utils/async.js.map +1 -0
- package/build/lib/utils/errors.d.ts +13 -0
- package/build/lib/utils/errors.d.ts.map +1 -0
- package/build/lib/utils/errors.js +24 -0
- package/build/lib/utils/errors.js.map +1 -0
- package/build/lib/utils/index.d.ts +9 -0
- package/build/lib/utils/index.d.ts.map +1 -0
- package/build/lib/utils/index.js +27 -0
- package/build/lib/utils/index.js.map +1 -0
- package/build/lib/utils/inspector.d.ts +30 -0
- package/build/lib/utils/inspector.d.ts.map +1 -0
- package/build/lib/utils/inspector.js +91 -0
- package/build/lib/utils/inspector.js.map +1 -0
- package/build/lib/utils/javascript.d.ts +25 -0
- package/build/lib/utils/javascript.d.ts.map +1 -0
- package/build/lib/utils/javascript.js +90 -0
- package/build/lib/utils/javascript.js.map +1 -0
- package/build/lib/utils/module.d.ts +20 -0
- package/build/lib/utils/module.d.ts.map +1 -0
- package/build/lib/utils/module.js +36 -0
- package/build/lib/utils/module.js.map +1 -0
- package/build/lib/utils/object.d.ts +29 -0
- package/build/lib/utils/object.d.ts.map +1 -0
- package/build/lib/utils/object.js +54 -0
- package/build/lib/utils/object.js.map +1 -0
- package/build/lib/utils/platform.d.ts +7 -0
- package/build/lib/utils/platform.d.ts.map +1 -0
- package/build/lib/utils/platform.js +13 -0
- package/build/lib/utils/platform.js.map +1 -0
- package/lib/appium-ios-device.d.ts +12 -0
- package/lib/mixins/connect.ts +6 -6
- package/lib/mixins/execute.ts +2 -3
- package/lib/mixins/message-handlers.ts +4 -3
- package/lib/mixins/misc.ts +6 -5
- package/lib/mixins/navigate.ts +8 -3
- package/lib/remote-debugger-real-device.ts +1 -1
- package/lib/remote-debugger.ts +6 -8
- package/lib/rpc/rpc-client-real-device-shim.ts +3 -3
- package/lib/rpc/rpc-client.ts +16 -23
- package/lib/rpc/rpc-message-handler.ts +3 -4
- package/lib/utils/async.ts +32 -0
- package/lib/utils/errors.ts +19 -0
- package/lib/utils/index.ts +18 -0
- package/lib/utils/inspector.ts +99 -0
- package/lib/utils/javascript.ts +89 -0
- package/lib/utils/module.ts +33 -0
- package/lib/utils/object.ts +55 -0
- package/lib/utils/platform.ts +10 -0
- package/package.json +9 -3
- package/build/lib/utils.d.ts +0 -167
- package/build/lib/utils.d.ts.map +0 -1
- package/build/lib/utils.js +0 -422
- package/build/lib/utils.js.map +0 -1
- package/lib/utils.ts +0 -443
package/lib/rpc/rpc-client.ts
CHANGED
|
@@ -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
|
|
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
|
|
989
|
+
lock.acquire(String(pageIdKey), async () => await sleep(0)),
|
|
997
990
|
this._pageSelectionLock.acquire(
|
|
998
991
|
toPageSelectionKey(appIdKey, pageIdKey),
|
|
999
|
-
async () => await
|
|
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
|
|
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,32 @@
|
|
|
1
|
+
import {DelayCancellation} from './errors';
|
|
2
|
+
|
|
3
|
+
export type CancellableDelay = Promise<void> & {
|
|
4
|
+
cancel: () => void;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns a delay promise with a `cancel` method that rejects the promise.
|
|
9
|
+
*
|
|
10
|
+
* @param ms - Delay in milliseconds.
|
|
11
|
+
* @returns A cancellable delay promise.
|
|
12
|
+
*/
|
|
13
|
+
export function cancellableDelay(ms: number): CancellableDelay {
|
|
14
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
15
|
+
let rejectFn: ((error: Error) => void) | undefined;
|
|
16
|
+
|
|
17
|
+
const promise = new Promise<void>((resolve, reject) => {
|
|
18
|
+
rejectFn = reject;
|
|
19
|
+
timeoutId = setTimeout(resolve, ms);
|
|
20
|
+
}) as CancellableDelay;
|
|
21
|
+
|
|
22
|
+
promise.cancel = () => {
|
|
23
|
+
if (timeoutId) {
|
|
24
|
+
clearTimeout(timeoutId);
|
|
25
|
+
timeoutId = undefined;
|
|
26
|
+
}
|
|
27
|
+
rejectFn?.(new DelayCancellation());
|
|
28
|
+
rejectFn = undefined;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return promise;
|
|
32
|
+
}
|
|
@@ -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,18 @@
|
|
|
1
|
+
export {DelayCancellation, TimeoutError} from './errors';
|
|
2
|
+
export type {CancellableDelay} from './async';
|
|
3
|
+
export {cancellableDelay} from './async';
|
|
4
|
+
export {defaults, deepEqual, checkParams} from './object';
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
WEB_CONTENT_BUNDLE_ID,
|
|
8
|
+
appInfoFromDict,
|
|
9
|
+
pageArrayFromDict,
|
|
10
|
+
appIdsForBundle,
|
|
11
|
+
} from './inspector';
|
|
12
|
+
export {
|
|
13
|
+
RESPONSE_LOG_LENGTH,
|
|
14
|
+
simpleStringify,
|
|
15
|
+
convertJavascriptEvaluationResult,
|
|
16
|
+
} from './javascript';
|
|
17
|
+
export {getModuleRoot, getModuleProperties} from './module';
|
|
18
|
+
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.
|
|
7
|
+
"version": "15.10.0",
|
|
8
8
|
"author": "Appium Contributors",
|
|
9
9
|
"license": "Apache-2.0",
|
|
10
10
|
"repository": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@appium/base-driver": "^10.0.0-rc.1",
|
|
36
|
-
"@appium/support": "^7.
|
|
36
|
+
"@appium/support": "^7.2.2",
|
|
37
37
|
"appium-ios-device": "^3.0.0",
|
|
38
38
|
"async-lock": "^1.4.1",
|
|
39
39
|
"asyncbox": "^6.1.0",
|
|
@@ -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",
|
|
@@ -85,7 +91,7 @@
|
|
|
85
91
|
"prettier": "^3.0.0",
|
|
86
92
|
"semantic-release": "^25.0.2",
|
|
87
93
|
"serve-static": "^2.2.0",
|
|
88
|
-
"sinon": "^
|
|
94
|
+
"sinon": "^22.0.0",
|
|
89
95
|
"ts-node": "^10.9.1",
|
|
90
96
|
"typescript": "^6.0.3"
|
|
91
97
|
}
|