appium-remote-debugger 15.2.9 → 15.2.10

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.
@@ -19,6 +19,8 @@ import {
19
19
  getAdditionalBundleIds,
20
20
  } from './property-accessors';
21
21
  import { NEW_APP_CONNECTED_ERROR, EMPTY_PAGE_DICTIONARY_ERROR } from '../rpc/rpc-client';
22
+ import type { RemoteDebugger } from '../remote-debugger';
23
+ import type { AppDict, Page, AppIdKey, PageIdKey, AppPage } from '../types';
22
24
 
23
25
  const APP_CONNECT_TIMEOUT_MS = 0;
24
26
  const APP_CONNECT_INTERVAL_MS = 100;
@@ -32,11 +34,11 @@ const SAFARI_VIEW_BUNDLE_ID = 'com.apple.SafariViewService';
32
34
  const WILDCARD_BUNDLE_ID = '*';
33
35
 
34
36
  /**
35
- *
36
- * @this {RemoteDebugger}
37
- * @returns {Promise<void>}
37
+ * Sends a connection key request to the Web Inspector.
38
+ * This method only waits to ensure the socket connection works, as the response
39
+ * from Web Inspector can take a long time.
38
40
  */
39
- export async function setConnectionKey () {
41
+ export async function setConnectionKey(this: RemoteDebugger): Promise<void> {
40
42
  this.log.debug('Sending connection key request');
41
43
 
42
44
  // send but only wait to make sure the socket worked
@@ -45,12 +47,17 @@ export async function setConnectionKey () {
45
47
  }
46
48
 
47
49
  /**
50
+ * Establishes a connection to the remote debugger and initializes the RPC client.
51
+ * Sets up event listeners for debugger-level events and waits for applications
52
+ * to be reported if a timeout is specified.
48
53
  *
49
- * @this {RemoteDebugger}
50
- * @param {number} [timeout=APP_CONNECT_TIMEOUT_MS]
51
- * @returns {Promise<import('../types').AppDict>}
54
+ * @param timeout - Maximum time in milliseconds to wait for applications to be reported.
55
+ * Defaults to 0 (no waiting). If provided, the method will wait up to
56
+ * this duration for applications to appear in the app dictionary.
57
+ * @returns A promise that resolves to the application dictionary containing all
58
+ * connected applications.
52
59
  */
53
- export async function connect (timeout = APP_CONNECT_TIMEOUT_MS) {
60
+ export async function connect(this: RemoteDebugger, timeout: number = APP_CONNECT_TIMEOUT_MS): Promise<AppDict> {
54
61
  this.setup();
55
62
 
56
63
  // initialize the rpc client
@@ -91,7 +98,7 @@ export async function connect (timeout = APP_CONNECT_TIMEOUT_MS) {
91
98
  }
92
99
  }
93
100
  return this.appDict;
94
- } catch (err) {
101
+ } catch (err: any) {
95
102
  this.log.error(`Error setting connection key: ${err.message}`);
96
103
  await this.disconnect();
97
104
  throw err;
@@ -99,25 +106,36 @@ export async function connect (timeout = APP_CONNECT_TIMEOUT_MS) {
99
106
  }
100
107
 
101
108
  /**
102
- *
103
- * @this {RemoteDebugger}
104
- * @returns {Promise<void>}
109
+ * Disconnects from the remote debugger by closing the RPC client connection,
110
+ * emitting a disconnect event, and performing cleanup via teardown.
105
111
  */
106
- export async function disconnect () {
112
+ export async function disconnect(this: RemoteDebugger): Promise<void> {
107
113
  await getRcpClient(this)?.disconnect();
108
114
  this.emit(events.EVENT_DISCONNECT, true);
109
115
  this.teardown();
110
116
  }
111
117
 
112
118
  /**
119
+ * Selects an application from the available connected applications.
120
+ * Searches for an app matching the provided URL and bundle IDs, then returns
121
+ * all pages from the selected application.
113
122
  *
114
- * @this {RemoteDebugger}
115
- * @param {string?} [currentUrl=null]
116
- * @param {number} [maxTries=SELECT_APP_RETRIES]
117
- * @param {boolean} [ignoreAboutBlankUrl=false]
118
- * @returns {Promise<import('../types').Page[]>}
123
+ * @param currentUrl - Optional URL to match when selecting an application.
124
+ * If provided, the method will try to find an app containing
125
+ * a page with this URL.
126
+ * @param maxTries - Maximum number of retry attempts when searching for an app.
127
+ * Defaults to SELECT_APP_RETRIES (20).
128
+ * @param ignoreAboutBlankUrl - If true, pages with 'about:blank' URL will be
129
+ * excluded from the results. Defaults to false.
130
+ * @returns A promise that resolves to an array of Page objects from the selected
131
+ * application. Returns an empty array if no applications are connected.
119
132
  */
120
- export async function selectApp (currentUrl = null, maxTries = SELECT_APP_RETRIES, ignoreAboutBlankUrl = false) {
133
+ export async function selectApp(
134
+ this: RemoteDebugger,
135
+ currentUrl: string | null = null,
136
+ maxTries: number = SELECT_APP_RETRIES,
137
+ ignoreAboutBlankUrl: boolean = false
138
+ ): Promise<Page[]> {
121
139
  this.log.debug('Selecting application');
122
140
 
123
141
  const timer = new timing.Timer().start();
@@ -135,8 +153,7 @@ export async function selectApp (currentUrl = null, maxTries = SELECT_APP_RETRIE
135
153
  // translate the dictionary into a useful form, and return to sender
136
154
  this.log.debug(`Finally selecting app ${getAppIdKey(this)}`);
137
155
 
138
- /** @type {import('../types').Page[]} */
139
- const fullPageArray = [];
156
+ const fullPageArray: Page[] = [];
140
157
  for (const [app, info] of _.toPairs(getAppDict(this))) {
141
158
  if (!_.isArray(info.pageArray) || !info.isActive) {
142
159
  continue;
@@ -157,14 +174,22 @@ export async function selectApp (currentUrl = null, maxTries = SELECT_APP_RETRIE
157
174
  }
158
175
 
159
176
  /**
177
+ * Selects a specific page within an application and forwards socket setup.
178
+ * Optionally waits for the page to be ready based on the page load strategy.
160
179
  *
161
- * @this {RemoteDebugger}
162
- * @param {import('../types').AppIdKey} appIdKey
163
- * @param {import('../types').PageIdKey} pageIdKey
164
- * @param {boolean} [skipReadyCheck]
165
- * @returns {Promise<void>}
180
+ * @param appIdKey - The application identifier key. Will be prefixed with 'PID:'
181
+ * if not already present.
182
+ * @param pageIdKey - The page identifier key to select.
183
+ * @param skipReadyCheck - If true, skips the page readiness check. Defaults to false.
184
+ * When false, the method will wait for the page to be ready
185
+ * according to the configured page load strategy.
166
186
  */
167
- export async function selectPage (appIdKey, pageIdKey, skipReadyCheck = false) {
187
+ export async function selectPage(
188
+ this: RemoteDebugger,
189
+ appIdKey: AppIdKey,
190
+ pageIdKey: PageIdKey,
191
+ skipReadyCheck: boolean = false
192
+ ): Promise<void> {
168
193
  const fullAppIdKey = _.startsWith(`${appIdKey}`, 'PID:') ? `${appIdKey}` : `PID:${appIdKey}`;
169
194
  setAppIdKey(this, fullAppIdKey);
170
195
  setPageIdKey(this, pageIdKey);
@@ -175,7 +200,7 @@ export async function selectPage (appIdKey, pageIdKey, skipReadyCheck = false) {
175
200
 
176
201
  const pageReadinessDetector = skipReadyCheck ? undefined : {
177
202
  timeoutMs: this.pageLoadMs,
178
- readinessDetector: (/** @type {string} */ readyState) => this.isPageLoadingCompleted(readyState),
203
+ readinessDetector: (readyState: string) => this.isPageLoadingCompleted(readyState),
179
204
  };
180
205
  await this.requireRpcClient().selectPage(fullAppIdKey, pageIdKey, pageReadinessDetector);
181
206
 
@@ -183,16 +208,87 @@ export async function selectPage (appIdKey, pageIdKey, skipReadyCheck = false) {
183
208
  }
184
209
 
185
210
  /**
211
+ * Finds app keys based on assigned bundle IDs from the app dictionary.
212
+ * When bundleIds includes a wildcard ('*'), returns all app keys in the app dictionary.
213
+ * Also handles proxy applications that may act on behalf of other bundle IDs.
186
214
  *
187
- * @this {RemoteDebugger}
188
- * @param {string?} currentUrl
189
- * @param {number} maxTries
190
- * @param {boolean} ignoreAboutBlankUrl
191
- * @returns {Promise<import('../types').AppPage>}
215
+ * @param bundleIds - Array of bundle identifiers to match against. If the array
216
+ * contains a wildcard ('*'), all apps will be returned.
217
+ * @returns Array of application identifier keys that match the provided bundle IDs.
192
218
  */
193
- async function searchForApp (currentUrl, maxTries, ignoreAboutBlankUrl) {
194
- /** @type {string[]} */
195
- const bundleIds = _.compact(
219
+ export function getPossibleDebuggerAppKeys(this: RemoteDebugger, bundleIds: string[]): string[] {
220
+ const appDict = getAppDict(this);
221
+
222
+ if (bundleIds.includes(WILDCARD_BUNDLE_ID)) {
223
+ this.log.info(
224
+ 'Returning all apps because the list of matching bundle identifiers includes a wildcard'
225
+ );
226
+ return _.keys(appDict);
227
+ }
228
+
229
+ // go through the possible bundle identifiers
230
+ const possibleBundleIds = _.uniq([
231
+ WEB_CONTENT_BUNDLE_ID,
232
+ WEB_CONTENT_PROCESS_BUNDLE_ID,
233
+ SAFARI_VIEW_PROCESS_BUNDLE_ID,
234
+ SAFARI_VIEW_BUNDLE_ID,
235
+ ...bundleIds,
236
+ ]);
237
+ this.log.debug(
238
+ `Checking for apps with matching bundle identifiers: ${possibleBundleIds.join(', ')}`
239
+ );
240
+ const proxiedAppIds: string[] = [];
241
+ for (const bundleId of possibleBundleIds) {
242
+ // now we need to determine if we should pick a proxy for this instead
243
+ for (const appId of appIdsForBundle(bundleId, appDict)) {
244
+ if (proxiedAppIds.includes(appId)) {
245
+ continue;
246
+ }
247
+
248
+ proxiedAppIds.push(appId);
249
+ this.log.debug(`Found app id key '${appId}' for bundle '${bundleId}'`);
250
+ for (const [key, data] of _.toPairs(appDict)) {
251
+ if (data.isProxy && data.hostId === appId && !proxiedAppIds.includes(key)) {
252
+ this.log.debug(
253
+ `Found separate bundleId '${data.bundleId}' ` +
254
+ `acting as proxy for '${bundleId}', with app id '${key}'`
255
+ );
256
+ proxiedAppIds.push(key);
257
+ }
258
+ }
259
+ }
260
+ }
261
+
262
+ this.log.debug(
263
+ `You may also consider providing more values to 'additionalWebviewBundleIds' ` +
264
+ `capability to match other applications. Add a wildcard ('*') to match all apps.`
265
+ );
266
+
267
+ return _.uniq(proxiedAppIds);
268
+ }
269
+
270
+ /**
271
+ * Searches for an application matching the given criteria by retrying with
272
+ * exponential backoff. Attempts to connect to apps matching the bundle IDs
273
+ * and optionally filters by URL.
274
+ *
275
+ * @param currentUrl - Optional URL to match when searching for a page.
276
+ * If provided, only apps containing a page with this URL
277
+ * will be considered.
278
+ * @param maxTries - Maximum number of retry attempts.
279
+ * @param ignoreAboutBlankUrl - If true, pages with 'about:blank' URL will be
280
+ * ignored during the search.
281
+ * @returns A promise that resolves to an AppPage object containing the matched
282
+ * app ID key and page dictionary.
283
+ * @throws Error if no valid webapp can be connected after all retry attempts.
284
+ */
285
+ async function searchForApp(
286
+ this: RemoteDebugger,
287
+ currentUrl: string | null,
288
+ maxTries: number,
289
+ ignoreAboutBlankUrl: boolean
290
+ ): Promise<AppPage> {
291
+ const bundleIds: string[] = _.compact(
196
292
  [
197
293
  getBundleId(this),
198
294
  ...(getAdditionalBundleIds(this) ?? []),
@@ -200,9 +296,9 @@ async function searchForApp (currentUrl, maxTries, ignoreAboutBlankUrl) {
200
296
  ]
201
297
  );
202
298
  let retryCount = 0;
203
- return /** @type {import('../types').AppPage} */ (await retryInterval(maxTries, SELECT_APP_RETRY_SLEEP_MS, async () => {
299
+ return await retryInterval(maxTries, SELECT_APP_RETRY_SLEEP_MS, async () => {
204
300
  logApplicationDictionary.bind(this)();
205
- const possibleAppIds = getPossibleDebuggerAppKeys.bind(this)(/** @type {string[]} */ (bundleIds));
301
+ const possibleAppIds = getPossibleDebuggerAppKeys.bind(this)(bundleIds);
206
302
  this.log.debug(`Trying out the possible app ids: ${possibleAppIds.join(', ')} (try #${retryCount + 1} of ${maxTries})`);
207
303
  for (const attemptedAppIdKey of possibleAppIds) {
208
304
  const appInfo = getAppDict(this)[attemptedAppIdKey];
@@ -236,7 +332,7 @@ async function searchForApp (currentUrl, maxTries, ignoreAboutBlankUrl) {
236
332
  } else {
237
333
  this.log.debug('Received app, but no match was found. Trying again.');
238
334
  }
239
- } catch (err) {
335
+ } catch (err: any) {
240
336
  if (![NEW_APP_CONNECTED_ERROR, EMPTY_PAGE_DICTIONARY_ERROR].some((msg) => msg === err.message)) {
241
337
  this.log.debug(err.stack);
242
338
  }
@@ -247,18 +343,25 @@ async function searchForApp (currentUrl, maxTries, ignoreAboutBlankUrl) {
247
343
  throw new Error(
248
344
  `Could not connect to a valid webapp. Make sure it is debuggable and has at least one active page.`
249
345
  );
250
- }));
346
+ }) as Promise<AppPage>;
251
347
  }
252
348
 
253
349
  /**
350
+ * Searches through the application dictionary to find a page matching the given URL.
351
+ * Only considers active applications with non-empty page arrays.
254
352
  *
255
- * @this {RemoteDebugger}
256
- * @param {Record<string, import('../types').AppInfo>} appsDict
257
- * @param {string?} currentUrl
258
- * @param {boolean} [ignoreAboutBlankUrl]
259
- * @returns {import('../types').AppPage?}
353
+ * @param appsDict - The application dictionary to search through.
354
+ * @param currentUrl - Optional URL to match. If provided, only pages with this exact
355
+ * URL or with this URL followed by '/' will be considered.
356
+ * @param ignoreAboutBlankUrl - If true, pages with 'about:blank' URL will be ignored.
357
+ * @returns An AppPage object if a matching page is found, null otherwise.
260
358
  */
261
- function searchForPage (appsDict, currentUrl = null, ignoreAboutBlankUrl = false) {
359
+ function searchForPage(
360
+ this: RemoteDebugger,
361
+ appsDict: AppDict,
362
+ currentUrl: string | null = null,
363
+ ignoreAboutBlankUrl: boolean = false
364
+ ): AppPage | null {
262
365
  for (const appDict of _.values(appsDict)) {
263
366
  if (!appDict || !appDict.isActive || !appDict.pageArray || _.isEmpty(appDict.pageArray)) {
264
367
  continue;
@@ -278,10 +381,11 @@ function searchForPage (appsDict, currentUrl = null, ignoreAboutBlankUrl = false
278
381
  }
279
382
 
280
383
  /**
281
- * @this {RemoteDebugger}
282
- * @returns {void}
384
+ * Logs the current application dictionary to the debug log.
385
+ * Displays all applications, their properties, and their associated pages
386
+ * in a formatted structure.
283
387
  */
284
- function logApplicationDictionary () {
388
+ function logApplicationDictionary(this: RemoteDebugger): void {
285
389
  this.log.debug('Current applications available:');
286
390
  for (const [app, info] of _.toPairs(getAppDict(this))) {
287
391
  this.log.debug(` Application: "${app}"`);
@@ -302,67 +406,3 @@ function logApplicationDictionary () {
302
406
  }
303
407
  }
304
408
  }
305
-
306
- /**
307
- * Find app keys based on assigned bundleIds from appDict
308
- * When bundleIds includes a wildcard ('*'), returns all appKeys in appDict.
309
- *
310
- * @this {RemoteDebugger}
311
- * @param {string[]} bundleIds
312
- * @returns {string[]}
313
- */
314
- export function getPossibleDebuggerAppKeys(bundleIds) {
315
- const appDict = getAppDict(this);
316
-
317
- if (bundleIds.includes(WILDCARD_BUNDLE_ID)) {
318
- this.log.info(
319
- 'Returning all apps because the list of matching bundle identifiers includes a wildcard'
320
- );
321
- return _.keys(appDict);
322
- }
323
-
324
- // go through the possible bundle identifiers
325
- const possibleBundleIds = _.uniq([
326
- WEB_CONTENT_BUNDLE_ID,
327
- WEB_CONTENT_PROCESS_BUNDLE_ID,
328
- SAFARI_VIEW_PROCESS_BUNDLE_ID,
329
- SAFARI_VIEW_BUNDLE_ID,
330
- ...bundleIds,
331
- ]);
332
- this.log.debug(
333
- `Checking for apps with matching bundle identifiers: ${possibleBundleIds.join(', ')}`
334
- );
335
- /** @type {string[]} */
336
- const proxiedAppIds = [];
337
- for (const bundleId of possibleBundleIds) {
338
- // now we need to determine if we should pick a proxy for this instead
339
- for (const appId of appIdsForBundle(bundleId, appDict)) {
340
- if (proxiedAppIds.includes(appId)) {
341
- continue;
342
- }
343
-
344
- proxiedAppIds.push(appId);
345
- this.log.debug(`Found app id key '${appId}' for bundle '${bundleId}'`);
346
- for (const [key, data] of _.toPairs(appDict)) {
347
- if (data.isProxy && data.hostId === appId && !proxiedAppIds.includes(key)) {
348
- this.log.debug(
349
- `Found separate bundleId '${data.bundleId}' ` +
350
- `acting as proxy for '${bundleId}', with app id '${key}'`
351
- );
352
- proxiedAppIds.push(key);
353
- }
354
- }
355
- }
356
- }
357
-
358
- this.log.debug(
359
- `You may also consider providing more values to 'additionalWebviewBundleIds' ` +
360
- `capability to match other applications. Add a wildcard ('*') to match all apps.`
361
- );
362
-
363
- return _.uniq(proxiedAppIds);
364
- }
365
-
366
- /**
367
- * @typedef {import('../remote-debugger').RemoteDebugger} RemoteDebugger
368
- */
@@ -0,0 +1,272 @@
1
+ import { events } from './events';
2
+ import {
3
+ pageArrayFromDict,
4
+ appInfoFromDict,
5
+ } from '../utils';
6
+ import _ from 'lodash';
7
+ import {
8
+ setAppIdKey,
9
+ getAppDict,
10
+ getAppIdKey,
11
+ getBundleId,
12
+ getNavigatingToPage,
13
+ setCurrentState,
14
+ setConnectedDrivers,
15
+ getSkippedApps,
16
+ } from './property-accessors';
17
+ import type { RemoteDebugger } from '../remote-debugger';
18
+ import type { StringRecord } from '@appium/types';
19
+ import type { AppDict } from '../types';
20
+
21
+ /*
22
+ * Generic callbacks used throughout the lifecycle of the Remote Debugger.
23
+ * These will be added to the prototype.
24
+ */
25
+
26
+ /**
27
+ * Handles page change notifications from the remote debugger.
28
+ * Updates the page array for the specified application and emits a page change
29
+ * event if the pages have actually changed and navigation is not in progress.
30
+ *
31
+ * @param err - Error object if an error occurred, null or undefined otherwise.
32
+ * @param appIdKey - The application identifier key for which pages have changed.
33
+ * @param pageDict - Dictionary containing the new page information.
34
+ */
35
+ export async function onPageChange(
36
+ this: RemoteDebugger,
37
+ err: Error | null | undefined,
38
+ appIdKey: string,
39
+ pageDict: StringRecord
40
+ ): Promise<void> {
41
+ if (_.isEmpty(pageDict)) {
42
+ return;
43
+ }
44
+
45
+ const currentPages = pageArrayFromDict(pageDict);
46
+ // save the page dict for this app
47
+ if (getAppDict(this)[appIdKey]) {
48
+ const previousPages = getAppDict(this)[appIdKey].pageArray;
49
+ // we have a pre-existing pageDict
50
+ if (previousPages && _.isEqual(previousPages, currentPages)) {
51
+ this.log.debug(
52
+ `Received page change notice for app '${appIdKey}' ` +
53
+ `but the listing has not changed. Ignoring.`
54
+ );
55
+ return;
56
+ }
57
+ // keep track of the page dictionary
58
+ getAppDict(this)[appIdKey].pageArray = currentPages;
59
+ this.log.debug(
60
+ `Pages changed for ${appIdKey}: ${JSON.stringify(previousPages)} -> ${JSON.stringify(currentPages)}`
61
+ );
62
+ }
63
+
64
+ if (getNavigatingToPage(this)) {
65
+ // in the middle of navigating, so reporting a page change will cause problems
66
+ return;
67
+ }
68
+
69
+ this.emit(events.EVENT_PAGE_CHANGE, {
70
+ appIdKey: appIdKey.replace('PID:', ''),
71
+ pageArray: currentPages,
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Handles notifications when a new application connects to the remote debugger.
77
+ * Updates the application dictionary with the new application information.
78
+ *
79
+ * @param err - Error object if an error occurred, null or undefined otherwise.
80
+ * @param dict - Dictionary containing the new application information including
81
+ * the WIRApplicationIdentifierKey.
82
+ */
83
+ export async function onAppConnect(
84
+ this: RemoteDebugger,
85
+ err: Error | null | undefined,
86
+ dict: StringRecord
87
+ ): Promise<void> {
88
+ const appIdKey = dict.WIRApplicationIdentifierKey;
89
+ this.log.debug(`Notified that new application '${appIdKey}' has connected`);
90
+ updateAppsWithDict.bind(this)(dict);
91
+ }
92
+
93
+ /**
94
+ * Handles notifications when an application disconnects from the remote debugger.
95
+ * Removes the application from the dictionary and attempts to find a replacement
96
+ * if the disconnected app was the currently selected one. Emits a disconnect event
97
+ * if no applications remain.
98
+ *
99
+ * @param err - Error object if an error occurred, null or undefined otherwise.
100
+ * @param dict - Dictionary containing the disconnected application information
101
+ * including the WIRApplicationIdentifierKey.
102
+ */
103
+ export function onAppDisconnect(
104
+ this: RemoteDebugger,
105
+ err: Error | null | undefined,
106
+ dict: StringRecord
107
+ ): void {
108
+ const appIdKey = dict.WIRApplicationIdentifierKey;
109
+ this.log.debug(`Application '${appIdKey}' disconnected. Removing from app dictionary.`);
110
+ this.log.debug(`Current app is '${getAppIdKey(this)}'`);
111
+
112
+ // get rid of the entry in our app dictionary,
113
+ // since it is no longer available
114
+ delete getAppDict(this)[appIdKey];
115
+
116
+ // if the disconnected app is the one we are connected to, try to find another
117
+ if (getAppIdKey(this) === appIdKey) {
118
+ this.log.debug(`No longer have app id. Attempting to find new one.`);
119
+ setAppIdKey(this, getDebuggerAppKey.bind(this)(getBundleId(this) as string));
120
+ }
121
+
122
+ if (_.isEmpty(getAppDict(this))) {
123
+ // this means we no longer have any apps. what the what?
124
+ this.log.debug('Main app disconnected. Disconnecting altogether.');
125
+ this.emit(events.EVENT_DISCONNECT, true);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Handles notifications when an application's information is updated.
131
+ * Updates the application dictionary with the new information while preserving
132
+ * any existing page array data.
133
+ *
134
+ * @param err - Error object if an error occurred, null or undefined otherwise.
135
+ * @param dict - Dictionary containing the updated application information.
136
+ */
137
+ export async function onAppUpdate(
138
+ this: RemoteDebugger,
139
+ err: Error | null | undefined,
140
+ dict: StringRecord
141
+ ): Promise<void> {
142
+ this.log.debug(`Notified that an application has been updated`);
143
+ updateAppsWithDict.bind(this)(dict);
144
+ }
145
+
146
+ /**
147
+ * Handles notifications containing the list of connected drivers.
148
+ * Updates the internal connected drivers list with the received information.
149
+ *
150
+ * @param err - Error object if an error occurred, null or undefined otherwise.
151
+ * @param drivers - Dictionary containing the connected driver list with
152
+ * WIRDriverDictionaryKey.
153
+ */
154
+ export function onConnectedDriverList(
155
+ this: RemoteDebugger,
156
+ err: Error | null | undefined,
157
+ drivers: StringRecord
158
+ ): void {
159
+ setConnectedDrivers(this, drivers.WIRDriverDictionaryKey);
160
+ this.log.debug(`Received connected driver list: ${JSON.stringify(this.connectedDrivers)}`);
161
+ }
162
+
163
+ /**
164
+ * Handles notifications about the current automation availability state.
165
+ * This state changes when 'Remote Automation' setting in Safari's advanced settings
166
+ * is toggled. The state can be either WIRAutomationAvailabilityAvailable or
167
+ * WIRAutomationAvailabilityNotAvailable.
168
+ *
169
+ * @param err - Error object if an error occurred, null or undefined otherwise.
170
+ * @param state - Dictionary containing the automation availability state with
171
+ * WIRAutomationAvailabilityKey.
172
+ */
173
+ export function onCurrentState(
174
+ this: RemoteDebugger,
175
+ err: Error | null | undefined,
176
+ state: StringRecord
177
+ ): void {
178
+ setCurrentState(this, state.WIRAutomationAvailabilityKey);
179
+ // This state changes when 'Remote Automation' in 'Settings app' > 'Safari' > 'Advanced' > 'Remote Automation' changes
180
+ // WIRAutomationAvailabilityAvailable or WIRAutomationAvailabilityNotAvailable
181
+ this.log.debug(`Received connected automation availability state: ${JSON.stringify(this.currentState)}`);
182
+ }
183
+
184
+ /**
185
+ * Handles notifications containing the list of connected applications.
186
+ * Translates the received information into the application dictionary format,
187
+ * filtering out any applications that are in the skipped apps list.
188
+ *
189
+ * @param err - Error object if an error occurred, null or undefined otherwise.
190
+ * @param apps - Dictionary containing the connected applications list.
191
+ */
192
+ export async function onConnectedApplicationList(
193
+ this: RemoteDebugger,
194
+ err: Error | null | undefined,
195
+ apps: StringRecord
196
+ ): Promise<void> {
197
+ this.log.debug(`Received connected applications list: ${_.keys(apps).join(', ')}`);
198
+
199
+ // translate the received information into an easier-to-manage
200
+ // hash with app id as key, and app info as value
201
+ const newDict: AppDict = {};
202
+ for (const dict of _.values(apps)) {
203
+ const [id, entry] = appInfoFromDict(dict);
204
+ if (getSkippedApps(this).includes(entry.name)) {
205
+ continue;
206
+ }
207
+ newDict[id] = entry;
208
+ }
209
+ // update the object's list of apps
210
+ _.defaults(getAppDict(this), newDict);
211
+ }
212
+
213
+ /**
214
+ * Given a bundle ID, finds the correct remote debugger app identifier key
215
+ * that is currently connected. Also handles proxy applications that may act
216
+ * on behalf of the requested bundle ID.
217
+ *
218
+ * @param bundleId - The bundle identifier to search for.
219
+ * @returns The application identifier key if found, undefined otherwise.
220
+ * If a proxy application is found, returns the proxy's app ID instead.
221
+ */
222
+ export function getDebuggerAppKey(this: RemoteDebugger, bundleId: string): string | undefined {
223
+ let appId: string | undefined;
224
+ for (const [key, data] of _.toPairs(getAppDict(this))) {
225
+ if (data.bundleId === bundleId) {
226
+ appId = key;
227
+ break;
228
+ }
229
+ }
230
+ // now we need to determine if we should pick a proxy for this instead
231
+ if (appId) {
232
+ this.log.debug(`Found app id key '${appId}' for bundle '${bundleId}'`);
233
+ let proxyAppId: string | undefined;
234
+ for (const [key, data] of _.toPairs(getAppDict(this))) {
235
+ if (data.isProxy && data.hostId === appId) {
236
+ this.log.debug(`Found separate bundleId '${data.bundleId}' ` +
237
+ `acting as proxy for '${bundleId}', with app id '${key}'`);
238
+ // set the app id... the last one will be used, so just keep re-assigning
239
+ proxyAppId = key;
240
+ }
241
+ }
242
+ if (proxyAppId) {
243
+ appId = proxyAppId;
244
+ this.log.debug(`Using proxied app id '${appId}'`);
245
+ }
246
+ }
247
+
248
+ return appId;
249
+ }
250
+
251
+ /**
252
+ * Updates the application dictionary with information from the provided dictionary.
253
+ * Preserves existing page array data if the application already exists in the dictionary.
254
+ * Attempts to set the app ID key if one is not currently set.
255
+ *
256
+ * @param dict - Dictionary containing application information to add or update.
257
+ */
258
+ function updateAppsWithDict(this: RemoteDebugger, dict: StringRecord): void {
259
+ // get the dictionary entry into a nice form, and add it to the
260
+ // application dictionary
261
+ const [id, entry] = appInfoFromDict(dict);
262
+ if (getAppDict(this)[id]?.pageArray) {
263
+ // preserve the page dictionary for this entry
264
+ entry.pageArray = getAppDict(this)[id].pageArray;
265
+ }
266
+ getAppDict(this)[id] = entry;
267
+
268
+ // try to get the app id from our connected apps
269
+ if (!getAppIdKey(this)) {
270
+ setAppIdKey(this, getDebuggerAppKey.bind(this)(getBundleId(this) as string));
271
+ }
272
+ }