appium-remote-debugger 15.7.2 → 15.8.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 (70) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/build/lib/atoms.d.ts.map +1 -1
  3. package/build/lib/atoms.js +27 -28
  4. package/build/lib/atoms.js.map +1 -1
  5. package/build/lib/index.d.ts.map +1 -1
  6. package/build/lib/index.js +7 -0
  7. package/build/lib/index.js.map +1 -1
  8. package/build/lib/mixins/connect.d.ts.map +1 -1
  9. package/build/lib/mixins/connect.js +21 -25
  10. package/build/lib/mixins/connect.js.map +1 -1
  11. package/build/lib/mixins/execute.d.ts.map +1 -1
  12. package/build/lib/mixins/execute.js +2 -8
  13. package/build/lib/mixins/execute.js.map +1 -1
  14. package/build/lib/mixins/message-handlers.d.ts.map +1 -1
  15. package/build/lib/mixins/message-handlers.js +8 -12
  16. package/build/lib/mixins/message-handlers.js.map +1 -1
  17. package/build/lib/mixins/misc.d.ts.map +1 -1
  18. package/build/lib/mixins/misc.js +5 -39
  19. package/build/lib/mixins/misc.js.map +1 -1
  20. package/build/lib/mixins/navigate.d.ts.map +1 -1
  21. package/build/lib/mixins/navigate.js +20 -55
  22. package/build/lib/mixins/navigate.js.map +1 -1
  23. package/build/lib/mixins/property-accessors.d.ts +24 -0
  24. package/build/lib/mixins/property-accessors.d.ts.map +1 -1
  25. package/build/lib/mixins/property-accessors.js +24 -0
  26. package/build/lib/mixins/property-accessors.js.map +1 -1
  27. package/build/lib/remote-debugger.d.ts +38 -38
  28. package/build/lib/remote-debugger.d.ts.map +1 -1
  29. package/build/lib/remote-debugger.js +64 -69
  30. package/build/lib/remote-debugger.js.map +1 -1
  31. package/build/lib/rpc/remote-messages.d.ts.map +1 -1
  32. package/build/lib/rpc/remote-messages.js +7 -8
  33. package/build/lib/rpc/remote-messages.js.map +1 -1
  34. package/build/lib/rpc/rpc-client-real-device-shim.d.ts.map +1 -1
  35. package/build/lib/rpc/rpc-client-real-device-shim.js +3 -6
  36. package/build/lib/rpc/rpc-client-real-device-shim.js.map +1 -1
  37. package/build/lib/rpc/rpc-client-simulator.d.ts.map +1 -1
  38. package/build/lib/rpc/rpc-client-simulator.js +3 -5
  39. package/build/lib/rpc/rpc-client-simulator.js.map +1 -1
  40. package/build/lib/rpc/rpc-client.d.ts +27 -27
  41. package/build/lib/rpc/rpc-client.d.ts.map +1 -1
  42. package/build/lib/rpc/rpc-client.js +226 -224
  43. package/build/lib/rpc/rpc-client.js.map +1 -1
  44. package/build/lib/rpc/rpc-message-handler.js +7 -10
  45. package/build/lib/rpc/rpc-message-handler.js.map +1 -1
  46. package/build/lib/types.d.ts +19 -19
  47. package/build/lib/types.d.ts.map +1 -1
  48. package/build/lib/utils.d.ts +70 -4
  49. package/build/lib/utils.d.ts.map +1 -1
  50. package/build/lib/utils.js +171 -23
  51. package/build/lib/utils.js.map +1 -1
  52. package/lib/atoms.ts +31 -32
  53. package/lib/index.ts +7 -0
  54. package/lib/mixins/connect.ts +22 -23
  55. package/lib/mixins/execute.ts +3 -5
  56. package/lib/mixins/message-handlers.ts +9 -10
  57. package/lib/mixins/misc.ts +8 -7
  58. package/lib/mixins/navigate.ts +58 -63
  59. package/lib/mixins/property-accessors.ts +24 -0
  60. package/lib/remote-debugger.ts +74 -76
  61. package/lib/rpc/remote-messages.ts +10 -5
  62. package/lib/rpc/rpc-client-real-device-shim.ts +3 -3
  63. package/lib/rpc/rpc-client-simulator.ts +3 -5
  64. package/lib/rpc/rpc-client.ts +259 -247
  65. package/lib/rpc/rpc-message-handler.ts +7 -7
  66. package/lib/types.ts +24 -24
  67. package/lib/utils.ts +181 -23
  68. package/package.json +4 -8
  69. package/scripts/common.mjs +42 -37
  70. package/scripts/web_inspector_proxy.mjs +3 -5
@@ -1,13 +1,19 @@
1
1
  import {RemoteMessages} from './remote-messages';
2
2
  import {waitForCondition} from 'asyncbox';
3
3
  import {log} from '../logger';
4
- import _ from 'lodash';
5
- import B from 'bluebird';
6
4
  import RpcMessageHandler from './rpc-message-handler';
7
5
  import {util, timing} from '@appium/support';
8
6
  import {EventEmitter} from 'node:events';
9
7
  import AsyncLock from 'async-lock';
10
- import {convertJavascriptEvaluationResult} from '../utils';
8
+ import {
9
+ convertJavascriptEvaluationResult,
10
+ delay,
11
+ defaults,
12
+ isEmpty,
13
+ isPlainObject,
14
+ truncateString,
15
+ withTimeout,
16
+ } from '../utils';
11
17
  import type {StringRecord} from '@appium/types';
12
18
  import type {
13
19
  AppIdKey,
@@ -22,7 +28,7 @@ import type {
22
28
  RemoteCommandId,
23
29
  } from '../types';
24
30
 
25
- const DATA_LOG_LENGTH = {length: 200};
31
+ const DATA_LOG_LENGTH = 200;
26
32
  const MIN_WAIT_FOR_TARGET_TIMEOUT_MS = 30000;
27
33
  const DEFAULT_TARGET_CREATION_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
28
34
  const WAIT_FOR_TARGET_INTERVAL_MS = 100;
@@ -187,21 +193,21 @@ export class RpcClient {
187
193
  }
188
194
 
189
195
  /**
190
- * Sets the connection status.
196
+ * Gets the event emitter for target subscriptions.
191
197
  *
192
- * @param connected - The connection status to set.
198
+ * @returns The target subscriptions event emitter.
193
199
  */
194
- set isConnected(connected: boolean) {
195
- this.connected = !!connected;
200
+ get targetSubscriptions(): EventEmitter {
201
+ return this._targetSubscriptions;
196
202
  }
197
203
 
198
204
  /**
199
- * Gets the event emitter for target subscriptions.
205
+ * Sets the connection status.
200
206
  *
201
- * @returns The target subscriptions event emitter.
207
+ * @param connected - The connection status to set.
202
208
  */
203
- get targetSubscriptions(): EventEmitter {
204
- return this._targetSubscriptions;
209
+ set isConnected(connected: boolean) {
210
+ this.connected = !!connected;
205
211
  }
206
212
 
207
213
  /**
@@ -319,7 +325,7 @@ export class RpcClient {
319
325
  await waitForCondition(
320
326
  () => {
321
327
  target = this.getTarget(appIdKey, pageIdKey);
322
- return !_.isEmpty(target);
328
+ return !isEmpty(target);
323
329
  },
324
330
  {
325
331
  waitMs,
@@ -390,125 +396,129 @@ export class RpcClient {
390
396
  opts: RemoteCommandOpts,
391
397
  waitForResponse: TWaitForResponse = true as TWaitForResponse,
392
398
  ): Promise<TWaitForResponse extends true ? any : RemoteCommandOpts> {
393
- return await new B<any>(async (resolve, reject) => {
394
- // promise to be resolved whenever remote debugger
395
- // replies to our request
396
-
397
- // keep track of the messages coming and going using a simple sequential id
398
- const msgId = this.msgId++;
399
- // for target-base communication, everything is wrapped up
400
- const wrapperMsgId = this.msgId++;
401
- // acknowledge wrapper message
402
- this.messageHandler.on(wrapperMsgId.toString(), function (err: Error | null) {
403
- if (err) {
404
- reject(err);
405
- }
406
- });
399
+ return await new Promise<any>((resolve, reject) => {
400
+ void (async () => {
401
+ // promise to be resolved whenever remote debugger
402
+ // replies to our request
403
+
404
+ // keep track of the messages coming and going using a simple sequential id
405
+ const msgId = this.msgId++;
406
+ // for target-base communication, everything is wrapped up
407
+ const wrapperMsgId = this.msgId++;
408
+ // acknowledge wrapper message
409
+ this.messageHandler.on(wrapperMsgId.toString(), function (err: Error | null) {
410
+ if (err) {
411
+ reject(err);
412
+ }
413
+ });
407
414
 
408
- const appIdKey = opts.appIdKey;
409
- const pageIdKey = opts.pageIdKey;
410
- const targetId = opts.targetId ?? this.getTarget(appIdKey, pageIdKey);
415
+ const appIdKey = opts.appIdKey;
416
+ const pageIdKey = opts.pageIdKey;
417
+ const targetId = opts.targetId ?? this.getTarget(appIdKey, pageIdKey);
411
418
 
412
- // retrieve the correct command to send
413
- const fullOpts: RemoteCommandOpts & RemoteCommandId = _.defaults(
414
- {
415
- connId: this.connId,
416
- senderId: this.senderId,
417
- targetId,
418
- id: msgId.toString(),
419
- },
420
- opts,
421
- );
422
- let cmd: RawRemoteCommand;
423
- try {
424
- cmd = this.remoteMessages.getRemoteCommand(command, fullOpts);
425
- } catch (err: any) {
426
- log.error(err);
427
- return reject(err);
428
- }
419
+ // retrieve the correct command to send
420
+ const fullOpts: RemoteCommandOpts & RemoteCommandId = defaults(
421
+ {
422
+ connId: this.connId,
423
+ senderId: this.senderId,
424
+ targetId,
425
+ id: msgId.toString(),
426
+ },
427
+ opts,
428
+ );
429
+ let cmd: RawRemoteCommand;
430
+ try {
431
+ cmd = this.remoteMessages.getRemoteCommand(command, fullOpts);
432
+ } catch (err: any) {
433
+ log.error(err);
434
+ return reject(err);
435
+ }
429
436
 
430
- const finalCommand: RemoteCommand = {
431
- __argument: _.omit(cmd.__argument, ['WIRSocketDataKey']) as any,
432
- __selector: cmd.__selector,
433
- };
437
+ const finalCommandArgs = {...cmd.__argument};
438
+ delete finalCommandArgs.WIRSocketDataKey;
439
+ const finalCommand: RemoteCommand = {
440
+ __argument: finalCommandArgs as any,
441
+ __selector: cmd.__selector,
442
+ };
434
443
 
435
- const hasSocketData = _.isPlainObject(cmd.__argument?.WIRSocketDataKey);
436
- if (hasSocketData) {
437
- // make sure the message being sent has all the information that is needed
438
- const socketData = cmd.__argument.WIRSocketDataKey as StringRecord;
439
- if (!_.isInteger(socketData.id)) {
440
- // ! This must be a number
441
- socketData.id = wrapperMsgId;
444
+ const hasSocketData = isPlainObject(cmd.__argument?.WIRSocketDataKey);
445
+ if (hasSocketData) {
446
+ // make sure the message being sent has all the information that is needed
447
+ const socketData = cmd.__argument.WIRSocketDataKey as StringRecord;
448
+ if (!Number.isInteger(socketData.id)) {
449
+ // ! This must be a number
450
+ socketData.id = wrapperMsgId;
451
+ }
452
+ finalCommand.__argument.WIRSocketDataKey = Buffer.from(JSON.stringify(socketData));
442
453
  }
443
- finalCommand.__argument.WIRSocketDataKey = Buffer.from(JSON.stringify(socketData));
444
- }
445
454
 
446
- let messageHandled = true;
447
- if (!waitForResponse) {
448
- // the promise will be resolved as soon as the socket has been sent
449
- messageHandled = false;
450
- // do not log receipts
451
- this.messageHandler.once(msgId.toString(), (err: Error | null) => {
452
- if (err) {
453
- // we are not waiting for this, and if it errors it is most likely
454
- // a protocol change. Log and check during testing
455
- log.error(
456
- `Received error from send that is not being waited for (id: ${msgId}): ` +
457
- _.truncate(JSON.stringify(err), DATA_LOG_LENGTH),
458
- );
459
- // reject, though it is very rare that this will be triggered, since
460
- // the promise is resolved directly after send. On the off chance,
461
- // though, it will alert of a protocol change.
462
- reject(err);
463
- }
464
- });
465
- } else if (this.messageHandler.listenerCount(cmd.__selector)) {
466
- this.messageHandler.prependOnceListener(
467
- cmd.__selector,
468
- (err: Error | null, ...args: any[]) => {
455
+ let messageHandled = true;
456
+ if (!waitForResponse) {
457
+ // the promise will be resolved as soon as the socket has been sent
458
+ messageHandled = false;
459
+ // do not log receipts
460
+ this.messageHandler.once(msgId.toString(), (err: Error | null) => {
469
461
  if (err) {
470
- return reject(err);
462
+ // we are not waiting for this, and if it errors it is most likely
463
+ // a protocol change. Log and check during testing
464
+ log.error(
465
+ `Received error from send that is not being waited for (id: ${msgId}): ` +
466
+ truncateString(JSON.stringify(err), DATA_LOG_LENGTH),
467
+ );
468
+ // reject, though it is very rare that this will be triggered, since
469
+ // the promise is resolved directly after send. On the off chance,
470
+ // though, it will alert of a protocol change.
471
+ reject(err);
472
+ }
473
+ });
474
+ } else if (this.messageHandler.listenerCount(cmd.__selector)) {
475
+ this.messageHandler.prependOnceListener(
476
+ cmd.__selector,
477
+ (err: Error | null, ...args: any[]) => {
478
+ if (err) {
479
+ return reject(err);
480
+ }
481
+ log.debug(
482
+ `Received response from send (id: ${msgId}): '${truncateString(JSON.stringify(args), DATA_LOG_LENGTH)}'`,
483
+ );
484
+ resolve(args);
485
+ },
486
+ );
487
+ } else if (hasSocketData) {
488
+ this.messageHandler.once(msgId.toString(), (err: Error | null, value: any) => {
489
+ if (err) {
490
+ return reject(
491
+ new Error(`Remote debugger error with code '${(err as any).code}': ${err.message}`),
492
+ );
471
493
  }
472
494
  log.debug(
473
- `Received response from send (id: ${msgId}): '${_.truncate(JSON.stringify(args), DATA_LOG_LENGTH)}'`,
495
+ `Received data response from send (id: ${msgId}): '${truncateString(JSON.stringify(value), DATA_LOG_LENGTH)}'`,
474
496
  );
475
- resolve(args);
476
- },
477
- );
478
- } else if (hasSocketData) {
479
- this.messageHandler.once(msgId.toString(), (err: Error | null, value: any) => {
480
- if (err) {
481
- return reject(
482
- new Error(`Remote debugger error with code '${(err as any).code}': ${err.message}`),
483
- );
484
- }
485
- log.debug(
486
- `Received data response from send (id: ${msgId}): '${_.truncate(JSON.stringify(value), DATA_LOG_LENGTH)}'`,
487
- );
488
- resolve(value);
489
- });
490
- } else {
491
- // nothing else is handling things, so just resolve when the message is sent
492
- messageHandled = false;
493
- }
497
+ resolve(value);
498
+ });
499
+ } else {
500
+ // nothing else is handling things, so just resolve when the message is sent
501
+ messageHandled = false;
502
+ }
494
503
 
495
- const msg =
496
- `Sending '${cmd.__selector}' message` +
497
- (appIdKey ? ` to app '${appIdKey}'` : '') +
498
- (pageIdKey ? `, page '${pageIdKey}'` : '') +
499
- (targetId ? `, target '${targetId}'` : '') +
500
- ` (id: ${msgId}): '${command}'`;
501
- log.debug(msg);
502
- try {
503
- await this.sendMessage(finalCommand);
504
- if (!messageHandled) {
505
- // There are no handlers waiting for a response before resolving,
506
- // and no errors sending the message over the socket, so resolve
507
- resolve(fullOpts as any);
504
+ const msg =
505
+ `Sending '${cmd.__selector}' message` +
506
+ (appIdKey ? ` to app '${appIdKey}'` : '') +
507
+ (pageIdKey ? `, page '${pageIdKey}'` : '') +
508
+ (targetId ? `, target '${targetId}'` : '') +
509
+ ` (id: ${msgId}): '${command}'`;
510
+ log.debug(msg);
511
+ try {
512
+ await this.sendMessage(finalCommand);
513
+ if (!messageHandled) {
514
+ // There are no handlers waiting for a response before resolving,
515
+ // and no errors sending the message over the socket, so resolve
516
+ resolve(fullOpts as any);
517
+ }
518
+ } catch (err) {
519
+ return reject(err);
508
520
  }
509
- } catch (err) {
510
- return reject(err);
511
- }
521
+ })();
512
522
  });
513
523
  }
514
524
 
@@ -559,7 +569,7 @@ export class RpcClient {
559
569
  * @param targetInfo - Information about the created target.
560
570
  */
561
571
  async addTarget(err: Error | undefined, app: AppIdKey, targetInfo: TargetInfo): Promise<void> {
562
- if (_.isNil(targetInfo?.targetId)) {
572
+ if (targetInfo?.targetId == null) {
563
573
  log.info(`Received 'Target.targetCreated' event for app '${app}' with no target. Skipping`);
564
574
  return;
565
575
  }
@@ -570,7 +580,7 @@ export class RpcClient {
570
580
  }
571
581
  const {appIdKey, pageIdKey, pageReadinessDetector} = pendingPageTargetDetails;
572
582
 
573
- if (!_.isPlainObject(this.targets[appIdKey])) {
583
+ if (!isPlainObject(this.targets[appIdKey])) {
574
584
  this.targets[appIdKey] = {
575
585
  lock: new AsyncLock({maxOccupationTime: this._targetCreationTimeoutMs}),
576
586
  } as PagesToTargets;
@@ -634,7 +644,7 @@ export class RpcClient {
634
644
  log.debug(
635
645
  `Target created for app '${appIdKey}' and page '${pageIdKey}': ${JSON.stringify(targetInfo)}`,
636
646
  );
637
- if (_.has(this.targets[appIdKey], pageIdKey)) {
647
+ if (Object.hasOwn(this.targets[appIdKey], pageIdKey)) {
638
648
  const existingTarget = this.targets[appIdKey][pageIdKey] as TargetId;
639
649
  log.debug(
640
650
  `There is already a target for this app and page ('${existingTarget}'). ` +
@@ -727,7 +737,7 @@ export class RpcClient {
727
737
  * @param targetInfo - Information about the destroyed target.
728
738
  */
729
739
  async removeTarget(err: Error | undefined, app: AppIdKey, targetInfo: TargetInfo): Promise<void> {
730
- if (_.isNil(targetInfo?.targetId)) {
740
+ if (targetInfo?.targetId == null) {
731
741
  log.debug(`Received 'Target.targetDestroyed' event with no target. Skipping`);
732
742
  return;
733
743
  }
@@ -741,7 +751,7 @@ export class RpcClient {
741
751
 
742
752
  // we do not know the page, so go through and find the existing target
743
753
  const appTargetsMap = this.targets[app];
744
- for (const [page, targetId] of _.toPairs(appTargetsMap)) {
754
+ for (const [page, targetId] of Object.entries(appTargetsMap)) {
745
755
  if (targetId === oldTargetId) {
746
756
  log.debug(
747
757
  `Found provisional target for app '${app}'. ` +
@@ -761,7 +771,7 @@ export class RpcClient {
761
771
 
762
772
  // if there is no waiting provisional target, just get rid of the existing one
763
773
  const targets = this.targets[app];
764
- for (const [page, targetId] of _.toPairs(targets)) {
774
+ for (const [page, targetId] of Object.entries(targets)) {
765
775
  if (targetId === targetInfo.targetId) {
766
776
  delete targets[page];
767
777
  return;
@@ -837,7 +847,8 @@ export class RpcClient {
837
847
  }
838
848
  await this.send('setSenderKey', sendOpts);
839
849
  };
840
- await B.resolve(setupWebview()).timeout(
850
+ await withTimeout(
851
+ setupWebview(),
841
852
  timeoutMs,
842
853
  `Cannot set up page '${pageIdKey}' for app '${appIdKey}' within ${timeoutMs}ms`,
843
854
  );
@@ -877,6 +888,123 @@ export class RpcClient {
877
888
  });
878
889
  }
879
890
 
891
+ /**
892
+ * Connects to a specific application and returns its page dictionary.
893
+ *
894
+ * @param appIdKey - The application identifier key to connect to.
895
+ * @returns A promise that resolves to a tuple containing the connected app ID key
896
+ * and the page dictionary.
897
+ * @throws Error if a new application connects during the process or if the page
898
+ * dictionary is empty.
899
+ */
900
+ async selectApp(appIdKey: AppIdKey): Promise<[string, StringRecord]> {
901
+ return await new Promise<[string, StringRecord]>((resolve, reject) => {
902
+ // local callback, temporarily added as callback to
903
+ // `_rpc_applicationConnected:` remote debugger response
904
+ // to handle the initial connection
905
+ const onAppChange = (err: Error | null, dict: StringRecord) => {
906
+ if (err) {
907
+ return reject(err);
908
+ }
909
+ // from the dictionary returned, get the ids
910
+ const oldAppIdKey = dict.WIRHostApplicationIdentifierKey;
911
+ const correctAppIdKey = dict.WIRApplicationIdentifierKey;
912
+
913
+ // if this is a report of a proxy redirect from the remote debugger
914
+ // we want to update our dictionary and get a new app id
915
+ if (oldAppIdKey && correctAppIdKey !== oldAppIdKey) {
916
+ log.debug(
917
+ `We were notified we might have connected to the wrong app. ` +
918
+ `Using id ${correctAppIdKey} instead of ${oldAppIdKey}`,
919
+ );
920
+ }
921
+
922
+ reject(new Error(NEW_APP_CONNECTED_ERROR));
923
+ };
924
+ this.messageHandler.prependOnceListener('_rpc_applicationConnected:', onAppChange);
925
+
926
+ // do the actual connecting to the app
927
+ void (async () => {
928
+ try {
929
+ const [connectedAppIdKey, pageDict] = await this.send('connectToApp', {appIdKey});
930
+ // sometimes the connect logic happens, but with an empty dictionary
931
+ // which leads to the remote debugger getting disconnected, and into a loop
932
+ if (isEmpty(pageDict)) {
933
+ reject(new Error(EMPTY_PAGE_DICTIONARY_ERROR));
934
+ } else {
935
+ resolve([connectedAppIdKey, pageDict]);
936
+ }
937
+ } catch (err: any) {
938
+ log.warn(`Unable to connect to the app: ${err.message}`);
939
+ reject(err);
940
+ } finally {
941
+ this.messageHandler.off('_rpc_applicationConnected:', onAppChange);
942
+ }
943
+ })();
944
+ });
945
+ }
946
+
947
+ /**
948
+ * Handles execution context creation events by storing the context ID.
949
+ *
950
+ * @param err - Error if one occurred, undefined otherwise.
951
+ * @param context - The execution context information.
952
+ */
953
+ onExecutionContextCreated(err: Error | undefined, context: {id: number}): void {
954
+ // { id: 2, isPageContext: true, name: '', frameId: '0.1' }
955
+ // right now we have no way to map contexts to apps/pages
956
+ // so just store
957
+ this.contexts.push(context.id);
958
+ }
959
+
960
+ /**
961
+ * Handles garbage collection events by logging them.
962
+ * Garbage collection can affect operation timing.
963
+ */
964
+ onGarbageCollected(): void {
965
+ // just want to log that this is happening, as it can affect operation
966
+ log.debug(`Web Inspector garbage collected`);
967
+ }
968
+
969
+ /**
970
+ * Handles script parsing events by logging script information.
971
+ *
972
+ * @param err - Error if one occurred, undefined otherwise.
973
+ * @param scriptInfo - Information about the parsed script.
974
+ */
975
+ onScriptParsed(err: Error | undefined, scriptInfo: StringRecord): void {
976
+ // { scriptId: '13', url: '', startLine: 0, startColumn: 0, endLine: 82, endColumn: 3 }
977
+ log.debug(`Script parsed: ${JSON.stringify(scriptInfo)}`);
978
+ }
979
+
980
+ /**
981
+ * Waits for a page to be initialized by acquiring locks on both the page
982
+ * target lock and the page selection lock.
983
+ *
984
+ * @param appIdKey - The application identifier key.
985
+ * @param pageIdKey - The page identifier key.
986
+ * @throws Error if no targets are found for the application.
987
+ */
988
+ async waitForPage(appIdKey: AppIdKey, pageIdKey: PageIdKey): Promise<void> {
989
+ const appTargetsMap = this.targets[appIdKey];
990
+ if (!appTargetsMap) {
991
+ throw new Error(`No targets found for app '${appIdKey}'`);
992
+ }
993
+ const lock = appTargetsMap.lock;
994
+ const timer = new timing.Timer().start();
995
+ await Promise.all([
996
+ lock.acquire(pageIdKey, async () => await delay(0)),
997
+ this._pageSelectionLock.acquire(
998
+ toPageSelectionKey(appIdKey, pageIdKey),
999
+ async () => await delay(0),
1000
+ ),
1001
+ ]);
1002
+ const durationMs = timer.getDuration().asMilliSeconds;
1003
+ if (durationMs > 10) {
1004
+ log.debug(`Waited ${durationMs}ms until the page ${pageIdKey}@${appIdKey} is initialized`);
1005
+ }
1006
+ }
1007
+
880
1008
  /**
881
1009
  * Initializes a page by enabling various Web Inspector domains.
882
1010
  * Can perform either simple or full initialization based on configuration.
@@ -1028,95 +1156,6 @@ export class RpcClient {
1028
1156
  return true;
1029
1157
  }
1030
1158
 
1031
- /**
1032
- * Connects to a specific application and returns its page dictionary.
1033
- *
1034
- * @param appIdKey - The application identifier key to connect to.
1035
- * @returns A promise that resolves to a tuple containing the connected app ID key
1036
- * and the page dictionary.
1037
- * @throws Error if a new application connects during the process or if the page
1038
- * dictionary is empty.
1039
- */
1040
- async selectApp(appIdKey: AppIdKey): Promise<[string, StringRecord]> {
1041
- return await new B<[string, StringRecord]>((resolve, reject) => {
1042
- // local callback, temporarily added as callback to
1043
- // `_rpc_applicationConnected:` remote debugger response
1044
- // to handle the initial connection
1045
- const onAppChange = (err: Error | null, dict: StringRecord) => {
1046
- if (err) {
1047
- return reject(err);
1048
- }
1049
- // from the dictionary returned, get the ids
1050
- const oldAppIdKey = dict.WIRHostApplicationIdentifierKey;
1051
- const correctAppIdKey = dict.WIRApplicationIdentifierKey;
1052
-
1053
- // if this is a report of a proxy redirect from the remote debugger
1054
- // we want to update our dictionary and get a new app id
1055
- if (oldAppIdKey && correctAppIdKey !== oldAppIdKey) {
1056
- log.debug(
1057
- `We were notified we might have connected to the wrong app. ` +
1058
- `Using id ${correctAppIdKey} instead of ${oldAppIdKey}`,
1059
- );
1060
- }
1061
-
1062
- reject(new Error(NEW_APP_CONNECTED_ERROR));
1063
- };
1064
- this.messageHandler.prependOnceListener('_rpc_applicationConnected:', onAppChange);
1065
-
1066
- // do the actual connecting to the app
1067
- (async () => {
1068
- try {
1069
- const [connectedAppIdKey, pageDict] = await this.send('connectToApp', {appIdKey});
1070
- // sometimes the connect logic happens, but with an empty dictionary
1071
- // which leads to the remote debugger getting disconnected, and into a loop
1072
- if (_.isEmpty(pageDict)) {
1073
- reject(new Error(EMPTY_PAGE_DICTIONARY_ERROR));
1074
- } else {
1075
- resolve([connectedAppIdKey, pageDict]);
1076
- }
1077
- } catch (err: any) {
1078
- log.warn(`Unable to connect to the app: ${err.message}`);
1079
- reject(err);
1080
- } finally {
1081
- this.messageHandler.off('_rpc_applicationConnected:', onAppChange);
1082
- }
1083
- })();
1084
- });
1085
- }
1086
-
1087
- /**
1088
- * Handles execution context creation events by storing the context ID.
1089
- *
1090
- * @param err - Error if one occurred, undefined otherwise.
1091
- * @param context - The execution context information.
1092
- */
1093
- onExecutionContextCreated(err: Error | undefined, context: {id: number}): void {
1094
- // { id: 2, isPageContext: true, name: '', frameId: '0.1' }
1095
- // right now we have no way to map contexts to apps/pages
1096
- // so just store
1097
- this.contexts.push(context.id);
1098
- }
1099
-
1100
- /**
1101
- * Handles garbage collection events by logging them.
1102
- * Garbage collection can affect operation timing.
1103
- */
1104
- onGarbageCollected(): void {
1105
- // just want to log that this is happening, as it can affect operation
1106
- log.debug(`Web Inspector garbage collected`);
1107
- }
1108
-
1109
- /**
1110
- * Handles script parsing events by logging script information.
1111
- *
1112
- * @param err - Error if one occurred, undefined otherwise.
1113
- * @param scriptInfo - Information about the parsed script.
1114
- */
1115
- onScriptParsed(err: Error | undefined, scriptInfo: StringRecord): void {
1116
- // { scriptId: '13', url: '', startLine: 0, startColumn: 0, endLine: 82, endColumn: 3 }
1117
- log.debug(`Script parsed: ${JSON.stringify(scriptInfo)}`);
1118
- }
1119
-
1120
1159
  /**
1121
1160
  * Resumes a paused target.
1122
1161
  *
@@ -1169,7 +1208,7 @@ export class RpcClient {
1169
1208
  100,
1170
1209
  Math.trunc((pageReadinessDetector.timeoutMs - timer.getDuration().asMilliSeconds) * 0.8),
1171
1210
  );
1172
- const rawResult = await B.resolve(
1211
+ const rawResult = await withTimeout(
1173
1212
  this.send('Runtime.evaluate', {
1174
1213
  expression: 'document.readyState;',
1175
1214
  returnByValue: true,
@@ -1177,7 +1216,8 @@ export class RpcClient {
1177
1216
  pageIdKey,
1178
1217
  targetId,
1179
1218
  }),
1180
- ).timeout(commandTimeoutMs);
1219
+ commandTimeoutMs,
1220
+ );
1181
1221
  readyState = convertJavascriptEvaluationResult(rawResult);
1182
1222
  } catch (e: any) {
1183
1223
  log.debug(`Cannot determine page readiness: ${e.message}`);
@@ -1190,7 +1230,7 @@ export class RpcClient {
1190
1230
  );
1191
1231
  return;
1192
1232
  }
1193
- await B.delay(100);
1233
+ await delay(100);
1194
1234
  }
1195
1235
  log.warn(
1196
1236
  `Page '${pageIdKey}' for app '${appIdKey}' is not ready after ` +
@@ -1198,34 +1238,6 @@ export class RpcClient {
1198
1238
  );
1199
1239
  }
1200
1240
 
1201
- /**
1202
- * Waits for a page to be initialized by acquiring locks on both the page
1203
- * target lock and the page selection lock.
1204
- *
1205
- * @param appIdKey - The application identifier key.
1206
- * @param pageIdKey - The page identifier key.
1207
- * @throws Error if no targets are found for the application.
1208
- */
1209
- async waitForPage(appIdKey: AppIdKey, pageIdKey: PageIdKey): Promise<void> {
1210
- const appTargetsMap = this.targets[appIdKey];
1211
- if (!appTargetsMap) {
1212
- throw new Error(`No targets found for app '${appIdKey}'`);
1213
- }
1214
- const lock = appTargetsMap.lock;
1215
- const timer = new timing.Timer().start();
1216
- await Promise.all([
1217
- lock.acquire(pageIdKey, async () => await B.delay(0)),
1218
- this._pageSelectionLock.acquire(
1219
- toPageSelectionKey(appIdKey, pageIdKey),
1220
- async () => await B.delay(0),
1221
- ),
1222
- ]);
1223
- const durationMs = timer.getDuration().asMilliSeconds;
1224
- if (durationMs > 10) {
1225
- log.debug(`Waited ${durationMs}ms until the page ${pageIdKey}@${appIdKey} is initialized`);
1226
- }
1227
- }
1228
-
1229
1241
  /**
1230
1242
  * Gets the pending target details if there is a pending request for the given app.
1231
1243
  * Filters out non-page target types (e.g., 'frame').