appium-webdriveragent 10.5.4 → 11.0.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 (69) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/Scripts/{build-webdriveragent.js → build-webdriveragent.mjs} +13 -7
  3. package/Scripts/{fetch-prebuilt-wda.js → fetch-prebuilt-wda.mjs} +17 -9
  4. package/Scripts/update-wda-version.mjs +42 -0
  5. package/WebDriverAgentLib/Info.plist +2 -2
  6. package/build/index.d.ts +1 -1
  7. package/build/index.d.ts.map +1 -1
  8. package/build/index.js +1 -2
  9. package/build/index.js.map +1 -1
  10. package/build/lib/check-dependencies.d.ts +2 -7
  11. package/build/lib/check-dependencies.d.ts.map +1 -1
  12. package/build/lib/check-dependencies.js +0 -11
  13. package/build/lib/check-dependencies.js.map +1 -1
  14. package/build/lib/constants.d.ts +12 -12
  15. package/build/lib/constants.d.ts.map +1 -1
  16. package/build/lib/constants.js +14 -26
  17. package/build/lib/constants.js.map +1 -1
  18. package/build/lib/logger.d.ts +1 -2
  19. package/build/lib/logger.d.ts.map +1 -1
  20. package/build/lib/logger.js +2 -2
  21. package/build/lib/logger.js.map +1 -1
  22. package/build/lib/no-session-proxy.d.ts +5 -5
  23. package/build/lib/no-session-proxy.d.ts.map +1 -1
  24. package/build/lib/no-session-proxy.js +0 -1
  25. package/build/lib/no-session-proxy.js.map +1 -1
  26. package/build/lib/utils.d.ts +53 -96
  27. package/build/lib/utils.d.ts.map +1 -1
  28. package/build/lib/utils.js +124 -110
  29. package/build/lib/utils.js.map +1 -1
  30. package/build/lib/webdriveragent.d.ts +123 -170
  31. package/build/lib/webdriveragent.d.ts.map +1 -1
  32. package/build/lib/webdriveragent.js +319 -349
  33. package/build/lib/webdriveragent.js.map +1 -1
  34. package/build/lib/xcodebuild.d.ts +66 -77
  35. package/build/lib/xcodebuild.d.ts.map +1 -1
  36. package/build/lib/xcodebuild.js +123 -92
  37. package/build/lib/xcodebuild.js.map +1 -1
  38. package/build/test/functional/desired.d.ts +3 -0
  39. package/build/test/functional/desired.d.ts.map +1 -0
  40. package/build/test/functional/desired.js +9 -0
  41. package/build/test/functional/desired.js.map +1 -0
  42. package/build/test/functional/helpers/simulator.d.ts +5 -0
  43. package/build/test/functional/helpers/simulator.d.ts.map +1 -0
  44. package/build/test/functional/helpers/simulator.js +39 -0
  45. package/build/test/functional/helpers/simulator.js.map +1 -0
  46. package/build/test/functional/webdriveragent-e2e-specs.d.ts +2 -0
  47. package/build/test/functional/webdriveragent-e2e-specs.d.ts.map +1 -0
  48. package/build/test/functional/webdriveragent-e2e-specs.js +133 -0
  49. package/build/test/functional/webdriveragent-e2e-specs.js.map +1 -0
  50. package/build/test/unit/utils-specs.d.ts +2 -0
  51. package/build/test/unit/utils-specs.d.ts.map +1 -0
  52. package/build/test/unit/utils-specs.js +161 -0
  53. package/build/test/unit/utils-specs.js.map +1 -0
  54. package/build/test/unit/webdriveragent-specs.d.ts +2 -0
  55. package/build/test/unit/webdriveragent-specs.d.ts.map +1 -0
  56. package/build/test/unit/webdriveragent-specs.js +424 -0
  57. package/build/test/unit/webdriveragent-specs.js.map +1 -0
  58. package/build/tsconfig.tsbuildinfo +1 -0
  59. package/index.ts +1 -1
  60. package/lib/{check-dependencies.js → check-dependencies.ts} +4 -13
  61. package/lib/constants.ts +18 -0
  62. package/lib/logger.ts +3 -0
  63. package/lib/{no-session-proxy.js → no-session-proxy.ts} +4 -5
  64. package/lib/{utils.js → utils.ts} +101 -111
  65. package/lib/{webdriveragent.js → webdriveragent.ts} +353 -390
  66. package/lib/{xcodebuild.js → xcodebuild.ts} +145 -114
  67. package/package.json +7 -9
  68. package/lib/constants.js +0 -24
  69. package/lib/logger.js +0 -5
@@ -1,11 +1,12 @@
1
1
  import { waitForCondition } from 'asyncbox';
2
2
  import _ from 'lodash';
3
- import path from 'path';
4
- import url from 'url';
3
+ import path from 'node:path';
4
+ import url from 'node:url';
5
5
  import B from 'bluebird';
6
6
  import { JWProxy } from '@appium/base-driver';
7
7
  import { fs, util, plist } from '@appium/support';
8
- import defaultLogger from './logger';
8
+ import type { AppiumLogger, StringRecord } from '@appium/types';
9
+ import { log as defaultLogger } from './logger';
9
10
  import { NoSessionProxy } from './no-session-proxy';
10
11
  import {
11
12
  getWDAUpgradeTimestamp, resetTestProcesses, getPIDsListeningOnPort, BOOTSTRAP_PATH
@@ -20,6 +21,7 @@ import {
20
21
  } from './constants';
21
22
  import {Xctest} from 'appium-ios-device';
22
23
  import {strongbox} from '@appium/strongbox';
24
+ import type { WebDriverAgentArgs, AppleDevice } from './types';
23
25
 
24
26
  const WDA_LAUNCH_TIMEOUT = 60 * 1000;
25
27
  const WDA_AGENT_PORT = 8100;
@@ -28,23 +30,46 @@ const SHARED_RESOURCES_GUARD = new AsyncLock();
28
30
  const RECENT_MODULE_VERSION_ITEM_NAME = 'recentWdaModuleVersion';
29
31
 
30
32
  export class WebDriverAgent {
31
- /** @type {string} */
32
- bootstrapPath;
33
-
34
- /** @type {string} */
35
- agentPath;
36
-
37
- /** @type {any} @deprecated for removal */
38
- idb;
33
+ bootstrapPath: string;
34
+ agentPath: string;
35
+ readonly args: WebDriverAgentArgs;
36
+ private readonly log: AppiumLogger;
37
+ readonly device: AppleDevice;
38
+ readonly platformVersion?: string;
39
+ readonly platformName?: string;
40
+ readonly iosSdkVersion?: string;
41
+ readonly host?: string;
42
+ readonly isRealDevice: boolean;
43
+ private readonly wdaBundlePath?: string;
44
+ private readonly wdaLocalPort?: number;
45
+ readonly wdaRemotePort: number;
46
+ readonly wdaBaseUrl: string;
47
+ readonly wdaBindingIP?: string;
48
+ private readonly prebuildWDA?: boolean;
49
+ webDriverAgentUrl?: string;
50
+ started: boolean;
51
+ private readonly wdaConnectionTimeout?: number;
52
+ private readonly useXctestrunFile?: boolean;
53
+ private readonly usePrebuiltWDA?: boolean;
54
+ private readonly derivedDataPath?: string;
55
+ private readonly mjpegServerPort?: number;
56
+ updatedWDABundleId?: string;
57
+ private readonly wdaLaunchTimeout: number;
58
+ private readonly usePreinstalledWDA?: boolean;
59
+ private xctestApiClient?: Xctest | null;
60
+ private readonly updatedWDABundleIdSuffix: string;
61
+ private _xcodebuild?: XcodeBuild | null;
62
+ noSessionProxy?: NoSessionProxy;
63
+ jwproxy?: JWProxy;
64
+ proxyReqRes?: any;
65
+ private _url?: url.UrlWithStringQuery;
39
66
 
40
67
  /**
41
- * @param {import('appium-xcode').XcodeVersion | undefined} xcodeVersion @deprecated Will be removed as no actual usage.
42
- * @param {import('./types').WebDriverAgentArgs} args
43
- * @param {import('@appium/types').AppiumLogger?} [log=null]
68
+ * Creates a new WebDriverAgent instance.
69
+ * @param args - Configuration arguments for WebDriverAgent
70
+ * @param log - Optional logger instance
44
71
  */
45
- constructor (xcodeVersion, args, log = null) {
46
- this.xcodeVersion = xcodeVersion;
47
-
72
+ constructor (args: WebDriverAgentArgs, log: AppiumLogger | null = null) {
48
73
  this.args = _.clone(args);
49
74
  this.log = log ?? defaultLogger;
50
75
 
@@ -54,8 +79,6 @@ export class WebDriverAgent {
54
79
  this.iosSdkVersion = args.iosSdkVersion;
55
80
  this.host = args.host;
56
81
  this.isRealDevice = !!args.realDevice;
57
- /** @deprecated We'll stop supporting idb */
58
- this.idb = args.device.idb;
59
82
  this.wdaBundlePath = args.wdaBundlePath;
60
83
 
61
84
  this.setWDAPaths(args.bootstrapPath, args.agentPath);
@@ -88,9 +111,9 @@ export class WebDriverAgent {
88
111
  this.xctestApiClient = null;
89
112
  this.updatedWDABundleIdSuffix = args.updatedWDABundleIdSuffix ?? DEFAULT_TEST_BUNDLE_SUFFIX;
90
113
 
91
- this.xcodebuild = this.canSkipXcodebuild
114
+ this._xcodebuild = this.canSkipXcodebuild
92
115
  ? null
93
- : new XcodeBuild(this.xcodeVersion, this.device, {
116
+ : new XcodeBuild(this.device, {
94
117
  platformVersion: this.platformVersion,
95
118
  platformName: this.platformName,
96
119
  iosSdkVersion: this.iosSdkVersion,
@@ -120,46 +143,44 @@ export class WebDriverAgent {
120
143
 
121
144
  /**
122
145
  * Return true if the session does not need xcodebuild.
123
- * @returns {boolean} Whether the session needs/has xcodebuild.
146
+ * @returns Whether the session needs/has xcodebuild.
124
147
  */
125
- get canSkipXcodebuild () {
148
+ get canSkipXcodebuild (): boolean {
126
149
  // Use this.args.webDriverAgentUrl to guarantee
127
150
  // the capabilities set gave the `appium:webDriverAgentUrl`.
128
151
  return this.usePreinstalledWDA || !!this.args.webDriverAgentUrl;
129
152
  }
130
153
 
154
+ /**
155
+ * Get the xcodebuild instance. Throws if not initialized.
156
+ * @returns The XcodeBuild instance
157
+ * @throws Error if xcodebuild is not available
158
+ */
159
+ get xcodebuild (): XcodeBuild {
160
+ if (!this._xcodebuild) {
161
+ throw new Error('xcodebuild is not available');
162
+ }
163
+ return this._xcodebuild;
164
+ }
165
+
131
166
  /**
132
167
  * Return bundle id for WebDriverAgent to launch the WDA.
133
168
  * The primary usage is with 'this.usePreinstalledWDA'.
134
169
  * It adds `.xctrunner` as suffix by default but 'this.updatedWDABundleIdSuffix'
135
170
  * lets skip it.
136
171
  *
137
- * @returns {string} Bundle ID for Xctest.
172
+ * @returns Bundle ID for Xctest.
138
173
  */
139
- get bundleIdForXctest () {
174
+ get bundleIdForXctest (): string {
140
175
  return `${this.updatedWDABundleId ? this.updatedWDABundleId : WDA_RUNNER_BUNDLE_ID}${this.updatedWDABundleIdSuffix}`;
141
176
  }
142
177
 
143
178
  /**
144
- * @param {string} [bootstrapPath]
145
- * @param {string} [agentPath]
146
- */
147
- setWDAPaths (bootstrapPath, agentPath) {
148
- // allow the user to specify a place for WDA. This is undocumented and
149
- // only here for the purposes of testing development of WDA
150
- this.bootstrapPath = bootstrapPath || BOOTSTRAP_PATH;
151
- this.log.info(`Using WDA path: '${this.bootstrapPath}'`);
152
-
153
- // for backward compatibility we need to be able to specify agentPath too
154
- this.agentPath = agentPath || path.resolve(this.bootstrapPath, 'WebDriverAgent.xcodeproj');
155
- this.log.info(`Using WDA agent: '${this.agentPath}'`);
156
- }
157
-
158
- /**
159
- * @returns {Promise<void>}
179
+ * Cleans up obsolete cached processes from previous WDA sessions
180
+ * that are listening on the same port but belong to different devices.
160
181
  */
161
- async cleanupObsoleteProcesses () {
162
- const obsoletePids = await getPIDsListeningOnPort(/** @type {string} */ (this.url.port),
182
+ async cleanupObsoleteProcesses (): Promise<void> {
183
+ const obsoletePids = await getPIDsListeningOnPort(this.url.port as string,
163
184
  (cmdLine) => cmdLine.includes('/WebDriverAgentRunner') &&
164
185
  !cmdLine.toLowerCase().includes(this.device.udid.toLowerCase()));
165
186
 
@@ -173,268 +194,23 @@ export class WebDriverAgent {
173
194
  `from previous WDA sessions. Cleaning them up`);
174
195
  try {
175
196
  await exec('kill', obsoletePids);
176
- } catch (e) {
197
+ } catch (e: any) {
177
198
  this.log.warn(`Failed to kill obsolete cached process${obsoletePids.length === 1 ? '' : 'es'} '${obsoletePids}'. ` +
178
199
  `Original error: ${e.message}`);
179
200
  }
180
201
  }
181
202
 
182
203
  /**
183
- * Return boolean if WDA is running or not
184
- * @return {Promise<boolean>} True if WDA is running
185
- * @throws {Error} If there was invalid response code or body
204
+ * Gets the base path for the WebDriverAgent URL.
205
+ * @returns The base path (empty string if root path)
186
206
  */
187
- async isRunning () {
188
- return !!(await this.getStatus());
189
- }
190
-
191
- /**
192
- * @returns {string}
193
- */
194
- get basePath () {
207
+ get basePath (): string {
195
208
  if (this.url.path === '/') {
196
209
  return '';
197
210
  }
198
211
  return this.url.path || '';
199
212
  }
200
213
 
201
- /**
202
- * Return current running WDA's status like below
203
- * {
204
- * "state": "success",
205
- * "os": {
206
- * "name": "iOS",
207
- * "version": "11.4",
208
- * "sdkVersion": "11.3"
209
- * },
210
- * "ios": {
211
- * "simulatorVersion": "11.4",
212
- * "ip": "172.254.99.34"
213
- * },
214
- * "build": {
215
- * "time": "Jun 24 2018 17:08:21",
216
- * "productBundleIdentifier": "com.facebook.WebDriverAgentRunner"
217
- * }
218
- * }
219
- *
220
- * @param {number} [timeoutMs=0] If the given timeoutMs is zero or negative number,
221
- * this function will return the response of `/status` immediately. If the given timeoutMs,
222
- * this function will try to get the response of `/status` up to the timeoutMs.
223
- * @return {Promise<import('@appium/types').StringRecord|null>} State Object
224
- * @throws {Error} If there was an error within timeoutMs timeout.
225
- * No error is raised if zero or negative number for the timeoutMs.
226
- */
227
- async getStatus (timeoutMs = 0) {
228
- const noSessionProxy = new NoSessionProxy({
229
- server: this.url.hostname,
230
- port: this.url.port,
231
- base: this.basePath,
232
- timeout: 3000,
233
- });
234
-
235
- const sendGetStatus = async () => await /** @type import('@appium/types').StringRecord */ (noSessionProxy.command('/status', 'GET'));
236
-
237
- if (_.isNil(timeoutMs) || timeoutMs <= 0) {
238
- try {
239
- return await sendGetStatus();
240
- } catch (err) {
241
- this.log.debug(`WDA is not listening at '${this.url.href}'. Original error:: ${err.message}`);
242
- return null;
243
- }
244
- }
245
-
246
- let lastError = null;
247
- let status = null;
248
- try {
249
- await waitForCondition(async () => {
250
- try {
251
- status = await sendGetStatus();
252
- return true;
253
- } catch (err) {
254
- lastError = err;
255
- }
256
- return false;
257
- }, {
258
- waitMs: timeoutMs,
259
- intervalMs: 300,
260
- });
261
- } catch (err) {
262
- this.log.debug(`Failed to get the status endpoint in ${timeoutMs} ms. ` +
263
- `The last error while accessing ${this.url.href}: ${lastError}. Original error:: ${err.message}.`);
264
- throw new Error(`WDA was not ready in ${timeoutMs} ms.`);
265
- }
266
- return status;
267
- }
268
-
269
- /**
270
- * Uninstall WDAs from the test device.
271
- * Over Xcode 11, multiple WDA can be in the device since Xcode 11 generates different WDA.
272
- * Appium does not expect multiple WDAs are running on a device.
273
- *
274
- * @returns {Promise<void>}
275
- */
276
- async uninstall () {
277
- try {
278
- const bundleIds = await this.device.getUserInstalledBundleIdsByBundleName(WDA_CF_BUNDLE_NAME);
279
- if (_.isEmpty(bundleIds)) {
280
- this.log.debug('No WDAs on the device.');
281
- return;
282
- }
283
-
284
- this.log.debug(`Uninstalling WDAs: '${bundleIds}'`);
285
- for (const bundleId of bundleIds) {
286
- await this.device.removeApp(bundleId);
287
- }
288
- } catch (e) {
289
- this.log.debug(e);
290
- this.log.warn(`WebDriverAgent uninstall failed. Perhaps, it is already uninstalled? ` +
291
- `Original error: ${e.message}`);
292
- }
293
- }
294
-
295
- async _cleanupProjectIfFresh () {
296
- if (this.canSkipXcodebuild) {
297
- return;
298
- }
299
-
300
- const packageInfo = JSON.parse(await fs.readFile(path.join(BOOTSTRAP_PATH, 'package.json'), 'utf8'));
301
- const box = strongbox(packageInfo.name);
302
- let boxItem = box.getItem(RECENT_MODULE_VERSION_ITEM_NAME);
303
- if (!boxItem) {
304
- const timestampPath = path.resolve(process.env.HOME ?? '', WDA_UPGRADE_TIMESTAMP_PATH);
305
- if (await fs.exists(timestampPath)) {
306
- // TODO: It is probably a bit ugly to hardcode the recent version string,
307
- // TODO: hovewer it should do the job as a temporary transition trick
308
- // TODO: to switch from a hardcoded file path to the strongbox usage.
309
- try {
310
- boxItem = await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, '5.0.0');
311
- } catch (e) {
312
- this.log.warn(`The actual module version cannot be persisted: ${e.message}`);
313
- return;
314
- }
315
- } else {
316
- this.log.info('There is no need to perform the project cleanup. A fresh install has been detected');
317
- try {
318
- await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, packageInfo.version);
319
- } catch (e) {
320
- this.log.warn(`The actual module version cannot be persisted: ${e.message}`);
321
- }
322
- return;
323
- }
324
- }
325
-
326
- let recentModuleVersion = await boxItem.read();
327
- try {
328
- recentModuleVersion = util.coerceVersion(recentModuleVersion, true);
329
- } catch (e) {
330
- this.log.warn(`The persisted module version string has been damaged: ${e.message}`);
331
- this.log.info(`Updating it to '${packageInfo.version}' assuming the project clenup is not needed`);
332
- await boxItem.write(packageInfo.version);
333
- return;
334
- }
335
-
336
- if (util.compareVersions(recentModuleVersion, '>=', packageInfo.version)) {
337
- this.log.info(
338
- `WebDriverAgent does not need a cleanup. The project sources are up to date ` +
339
- `(${recentModuleVersion} >= ${packageInfo.version})`
340
- );
341
- return;
342
- }
343
-
344
- this.log.info(
345
- `Cleaning up the WebDriverAgent project after the module upgrade has happened ` +
346
- `(${recentModuleVersion} < ${packageInfo.version})`
347
- );
348
- try {
349
- // @ts-ignore xcodebuild should be set
350
- await this.xcodebuild.cleanProject();
351
- await boxItem.write(packageInfo.version);
352
- } catch (e) {
353
- this.log.warn(`Cannot perform WebDriverAgent project cleanup. Original error: ${e.message}`);
354
- }
355
- }
356
-
357
-
358
- /**
359
- * @typedef {Object} LaunchWdaViaDeviceCtlOptions
360
- * @property {Record<string, string|number>} [env] environment variables for the launching WDA process
361
- */
362
-
363
- /**
364
- * Launch WDA with preinstalled package with 'xcrun devicectl device process launch'.
365
- * The WDA package must be prepared properly like published via
366
- * https://github.com/appium/WebDriverAgent/releases
367
- * with proper sign for this case.
368
- *
369
- * When we implement launching XCTest service via appium-ios-device,
370
- * this implementation can be replaced with it.
371
- *
372
- * @param {LaunchWdaViaDeviceCtlOptions} [opts={}] launching WDA with devicectl command options.
373
- * @return {Promise<void>}
374
- */
375
- async _launchViaDevicectl(opts = {}) {
376
- const {env} = opts;
377
-
378
- await this.device.devicectl.launchApp(
379
- this.bundleIdForXctest, { env, terminateExisting: true }
380
- );
381
- }
382
-
383
- /**
384
- * Launch WDA with preinstalled package without xcodebuild.
385
- * @param {string} sessionId Launch WDA and establish the session with this sessionId
386
- * @return {Promise<import('@appium/types').StringRecord|null>} State Object
387
- * @throws {Error} If there was an error within timeoutMs timeout.
388
- * No error is raised if zero or negative number for the timeoutMs.
389
- */
390
- async launchWithPreinstalledWDA(sessionId) {
391
- const xctestEnv = {
392
- USE_PORT: this.wdaLocalPort || WDA_AGENT_PORT,
393
- WDA_PRODUCT_BUNDLE_IDENTIFIER: this.bundleIdForXctest
394
- };
395
- if (this.mjpegServerPort) {
396
- xctestEnv.MJPEG_SERVER_PORT = this.mjpegServerPort;
397
- }
398
- if (this.wdaBindingIP) {
399
- xctestEnv.USE_IP = this.wdaBindingIP;
400
- }
401
- this.log.info('Launching WebDriverAgent on the device without xcodebuild');
402
- if (this.isRealDevice) {
403
- // Current method to launch WDA process can be done via 'xcrun devicectl',
404
- // but it has limitation about the WDA preinstalled package.
405
- // https://github.com/appium/appium/issues/19206#issuecomment-2014182674
406
- if (this.platformVersion && util.compareVersions(this.platformVersion, '>=', '17.0')) {
407
- await this._launchViaDevicectl({env: xctestEnv});
408
- } else {
409
- this.xctestApiClient = new Xctest(this.device.udid, this.bundleIdForXctest, null, {env: xctestEnv});
410
- await this.xctestApiClient.start();
411
- }
412
- } else {
413
- await this.device.simctl.exec('launch', {
414
- args: [
415
- '--terminate-running-process',
416
- this.device.udid,
417
- this.bundleIdForXctest,
418
- ],
419
- env: xctestEnv,
420
- });
421
- }
422
-
423
- this.setupProxies(sessionId);
424
- let status;
425
- try {
426
- status = await this.getStatus(this.wdaLaunchTimeout);
427
- } catch {
428
- throw new Error(
429
- `Failed to start the preinstalled WebDriverAgent in ${this.wdaLaunchTimeout} ms. ` +
430
- `The WebDriverAgent might not be properly built or the device might be locked. ` +
431
- `The 'appium:wdaLaunchTimeout' capability modifies the timeout.`
432
- );
433
- }
434
- this.started = true;
435
- return status;
436
- }
437
-
438
214
  /**
439
215
  * Return current running WDA's status like below after launching WDA
440
216
  * {
@@ -454,11 +230,9 @@ export class WebDriverAgent {
454
230
  * }
455
231
  * }
456
232
  *
457
- * @param {string} sessionId Launch WDA and establish the session with this sessionId
458
- * @return {Promise<any?>} State Object
459
- * @throws {Error} If there was invalid response code or body
233
+ * @param sessionId Launch WDA and establish the session with this sessionId
460
234
  */
461
- async launch (sessionId) {
235
+ async launch (sessionId: string): Promise<StringRecord | null> {
462
236
  if (this.webDriverAgentUrl) {
463
237
  this.log.info(`Using provided WebdriverAgent at '${this.webDriverAgentUrl}'`);
464
238
  this.url = this.webDriverAgentUrl;
@@ -481,7 +255,7 @@ export class WebDriverAgent {
481
255
 
482
256
  // useXctestrunFile and usePrebuiltWDA use existing dependencies
483
257
  // It depends on user side
484
- if (this.idb || this.useXctestrunFile || this.usePrebuiltWDA) {
258
+ if (this.useXctestrunFile || this.usePrebuiltWDA) {
485
259
  this.log.info('Skipped WDA project cleanup according to the provided capabilities');
486
260
  } else {
487
261
  const synchronizationKey = path.normalize(this.bootstrapPath);
@@ -492,77 +266,43 @@ export class WebDriverAgent {
492
266
  // We need to provide WDA local port, because it might be occupied
493
267
  await resetTestProcesses(this.device.udid, !this.isRealDevice);
494
268
 
495
- if (this.idb) {
496
- return await this.startWithIDB();
269
+ if (!this.noSessionProxy) {
270
+ throw new Error('noSessionProxy is not available');
497
271
  }
498
-
499
- // @ts-ignore xcodebuild should be set
500
272
  await this.xcodebuild.init(this.noSessionProxy);
501
273
 
502
274
  // Start the xcodebuild process
503
275
  if (this.prebuildWDA) {
504
- // @ts-ignore xcodebuild should be set
505
276
  await this.xcodebuild.prebuild();
506
277
  }
507
- // @ts-ignore xcodebuild should be set
508
- return await this.xcodebuild.start();
278
+ return await this.xcodebuild.start() as StringRecord | null;
509
279
  }
510
280
 
511
281
  /**
512
- * @deprecated We'll stop supporting idb. Deprecated for removal.
513
- * @returns {Promise<void>}
282
+ * Checks if the WebDriverAgent source is fresh by verifying
283
+ * that required resource files exist.
284
+ * @returns `true` if source is fresh (all required files exist), `false` otherwise
514
285
  */
515
- async startWithIDB () {
516
- this.log.info('Will launch WDA with idb instead of xcodebuild since the corresponding flag is enabled');
517
- const {wdaBundleId, testBundleId} = await this.prepareWDA();
518
- const env = {
519
- USE_PORT: this.wdaRemotePort,
520
- WDA_PRODUCT_BUNDLE_IDENTIFIER: this.bundleIdForXctest,
521
- };
522
- if (this.mjpegServerPort) {
523
- env.MJPEG_SERVER_PORT = this.mjpegServerPort;
524
- }
525
- if (this.wdaBindingIP) {
526
- env.USE_IP = this.wdaBindingIP;
527
- }
528
-
529
- return await this.idb.runXCUITest(wdaBundleId, wdaBundleId, testBundleId, {env});
286
+ async isSourceFresh (): Promise<boolean> {
287
+ const existsPromises = [
288
+ 'Resources',
289
+ `Resources${path.sep}WebDriverAgent.bundle`,
290
+ ].map((subPath) => fs.exists(path.resolve(this.bootstrapPath, subPath)));
291
+ return (await B.all(existsPromises)).some((v) => v === false);
530
292
  }
531
293
 
532
- /**
533
- *
534
- * @param {string} wdaBundlePath
535
- * @returns {Promise<string>}
536
- */
537
- async parseBundleId (wdaBundlePath) {
294
+ private async parseBundleId (wdaBundlePath: string): Promise<string> {
538
295
  const infoPlistPath = path.join(wdaBundlePath, 'Info.plist');
539
296
  const infoPlist = await plist.parsePlist(await fs.readFile(infoPlistPath));
540
297
  if (!infoPlist.CFBundleIdentifier) {
541
298
  throw new Error(`Could not find bundle id in '${infoPlistPath}'`);
542
299
  }
543
- return infoPlist.CFBundleIdentifier;
300
+ return infoPlist.CFBundleIdentifier as string;
544
301
  }
545
302
 
546
- /**
547
- * @deprecated We'll stop using idb
548
- * @returns {Promise<{wdaBundleId: string, testBundleId: string, wdaBundlePath: string}>}
549
- */
550
- async prepareWDA () {
551
- const wdaBundlePath = this.wdaBundlePath || await this.fetchWDABundle();
552
- const wdaBundleId = await this.parseBundleId(wdaBundlePath);
553
- if (!await this.device.isAppInstalled(wdaBundleId)) {
554
- await this.device.installApp(wdaBundlePath);
555
- }
556
- const testBundleId = await this.idb.installXCTestBundle(path.join(wdaBundlePath, 'PlugIns', 'WebDriverAgentRunner.xctest'));
557
- return {wdaBundleId, testBundleId, wdaBundlePath};
558
- }
559
-
560
- /**
561
- * @returns {Promise<string>}
562
- */
563
- async fetchWDABundle () {
303
+ private async fetchWDABundle (): Promise<string> {
564
304
  if (!this.derivedDataPath) {
565
- return await bundleWDASim(/** @type {XcodeBuild} */ (this.xcodebuild));
305
+ return await bundleWDASim(this.xcodebuild);
566
306
  }
567
307
  const wdaBundlePaths = await fs.glob(`${this.derivedDataPath}/**/*${WDA_RUNNER_APP}/`, {
568
308
  absolute: true,
@@ -573,23 +313,8 @@ export class WebDriverAgent {
573
313
  return wdaBundlePaths[0];
574
314
  }
575
315
 
576
- /**
577
- * @returns {Promise<boolean>}
578
- */
579
- async isSourceFresh () {
580
- const existsPromises = [
581
- 'Resources',
582
- `Resources${path.sep}WebDriverAgent.bundle`,
583
- ].map((subPath) => fs.exists(path.resolve(/** @type {String} */ (this.bootstrapPath), subPath)));
584
- return (await B.all(existsPromises)).some((v) => v === false);
585
- }
586
-
587
- /**
588
- * @param {string} sessionId
589
- * @returns {void}
590
- */
591
- setupProxies (sessionId) {
592
- const proxyOpts = {
316
+ private setupProxies (sessionId: string): void {
317
+ const proxyOpts: any = {
593
318
  log: this.log,
594
319
  server: this.url.hostname ?? undefined,
595
320
  port: parseInt(this.url.port ?? '', 10) || undefined,
@@ -610,9 +335,10 @@ export class WebDriverAgent {
610
335
  }
611
336
 
612
337
  /**
613
- * @returns {Promise<void>}
338
+ * Stops the WebDriverAgent session and cleans up resources.
339
+ * Handles both preinstalled WDA and xcodebuild-based sessions.
614
340
  */
615
- async quit () {
341
+ async quit (): Promise<void> {
616
342
  if (this.usePreinstalledWDA) {
617
343
  this.log.info('Stopping the XCTest session');
618
344
  if (this.xctestApiClient) {
@@ -621,14 +347,16 @@ export class WebDriverAgent {
621
347
  } else {
622
348
  try {
623
349
  await this.device.simctl.terminateApp(this.bundleIdForXctest);
624
- } catch (e) {
350
+ } catch (e: any) {
625
351
  this.log.warn(e.message);
626
352
  }
627
353
  }
628
354
  } else if (!this.args.webDriverAgentUrl) {
629
355
  this.log.info('Shutting down sub-processes');
630
- await this.xcodebuild?.quit();
631
- await this.xcodebuild?.reset();
356
+ if (this._xcodebuild) {
357
+ await this.xcodebuild.quit();
358
+ await this.xcodebuild.reset();
359
+ }
632
360
  } else {
633
361
  this.log.debug('Do not stop xcodebuild nor XCTest session ' +
634
362
  'since the WDA session is managed by outside this driver.');
@@ -648,9 +376,12 @@ export class WebDriverAgent {
648
376
  }
649
377
 
650
378
  /**
651
- * @returns {import('url').UrlWithStringQuery}
379
+ * Gets the WebDriverAgent URL.
380
+ * Constructs the URL from webDriverAgentUrl if provided, otherwise
381
+ * builds it from wdaBaseUrl, wdaBindingIP, and wdaLocalPort.
382
+ * @returns The parsed URL object
652
383
  */
653
- get url () {
384
+ get url (): url.UrlWithStringQuery {
654
385
  if (!this._url) {
655
386
  if (this.webDriverAgentUrl) {
656
387
  this._url = url.parse(this.webDriverAgentUrl);
@@ -664,46 +395,47 @@ export class WebDriverAgent {
664
395
  }
665
396
 
666
397
  /**
667
- * @param {string} _url
668
- * @returns {void}
398
+ * Sets the WebDriverAgent URL.
399
+ * @param _url - The URL string to parse and set
669
400
  */
670
- set url (_url) {
401
+ set url (_url: string) {
671
402
  this._url = url.parse(_url);
672
403
  }
673
404
 
674
405
  /**
675
- * @returns {boolean}
406
+ * Gets whether WebDriverAgent has fully started.
407
+ * @returns `true` if WDA has started, `false` otherwise
676
408
  */
677
- get fullyStarted () {
409
+ get fullyStarted (): boolean {
678
410
  return this.started;
679
411
  }
680
412
 
681
413
  /**
682
- * @param {boolean} started
683
- * @returns {void}s
414
+ * Sets whether WebDriverAgent has fully started.
415
+ * @param started - `true` if WDA has started, `false` otherwise
684
416
  */
685
- set fullyStarted (started) {
417
+ set fullyStarted (started: boolean) {
686
418
  this.started = started ?? false;
687
419
  }
688
420
 
689
421
  /**
690
- * @returns {Promise<string|undefined>}
422
+ * Retrieves the Xcode derived data path for WebDriverAgent.
423
+ * @returns The derived data path, or `undefined` if xcodebuild is skipped
691
424
  */
692
- async retrieveDerivedDataPath () {
425
+ async retrieveDerivedDataPath (): Promise<string | undefined> {
693
426
  if (this.canSkipXcodebuild) {
694
427
  return;
695
428
  }
696
- return await /** @type {XcodeBuild} */ (this.xcodebuild).retrieveDerivedDataPath();
429
+ return await this.xcodebuild.retrieveDerivedDataPath();
697
430
  }
698
431
 
699
432
  /**
700
433
  * Reuse running WDA if it has the same bundle id with updatedWDABundleId.
701
434
  * Or reuse it if it has the default id without updatedWDABundleId.
702
435
  * Uninstall it if the method faces an exception for the above situation.
703
- * @returns {Promise<void>}
704
436
  */
705
- async setupCaching () {
706
- const status = await this.getStatus();
437
+ async setupCaching (): Promise<void> {
438
+ const status = await this.getStatus(0);
707
439
  if (!status || !status.build) {
708
440
  this.log.debug('WDA is currently not running. There is nothing to cache');
709
441
  return;
@@ -712,7 +444,7 @@ export class WebDriverAgent {
712
444
  const {
713
445
  productBundleIdentifier,
714
446
  upgradedAt,
715
- } = status.build;
447
+ } = status.build as any;
716
448
  // for real device
717
449
  if (util.hasValue(productBundleIdentifier) && util.hasValue(this.updatedWDABundleId) && this.updatedWDABundleId !== productBundleIdentifier) {
718
450
  this.log.info(`Will uninstall running WDA since it has different bundle id. The actual value is '${productBundleIdentifier}'.`);
@@ -742,12 +474,243 @@ export class WebDriverAgent {
742
474
 
743
475
  /**
744
476
  * Quit and uninstall running WDA.
745
- * @returns {Promise<void>}
746
477
  */
747
- async quitAndUninstall () {
478
+ async quitAndUninstall (): Promise<void> {
748
479
  await this.quit();
749
480
  await this.uninstall();
750
481
  }
751
- }
752
482
 
753
- export default WebDriverAgent;
483
+ private setWDAPaths (bootstrapPath?: string, agentPath?: string): void {
484
+ // allow the user to specify a place for WDA. This is undocumented and
485
+ // only here for the purposes of testing development of WDA
486
+ this.bootstrapPath = bootstrapPath || BOOTSTRAP_PATH;
487
+ this.log.info(`Using WDA path: '${this.bootstrapPath}'`);
488
+
489
+ // for backward compatibility we need to be able to specify agentPath too
490
+ this.agentPath = agentPath || path.resolve(this.bootstrapPath, 'WebDriverAgent.xcodeproj');
491
+ this.log.info(`Using WDA agent: '${this.agentPath}'`);
492
+ }
493
+
494
+ private async isRunning (): Promise<boolean> {
495
+ return !!(await this.getStatus());
496
+ }
497
+
498
+ /**
499
+ * Return current running WDA's status like below
500
+ * {
501
+ * "state": "success",
502
+ * "os": {
503
+ * "name": "iOS",
504
+ * "version": "11.4",
505
+ * "sdkVersion": "11.3"
506
+ * },
507
+ * "ios": {
508
+ * "simulatorVersion": "11.4",
509
+ * "ip": "172.254.99.34"
510
+ * },
511
+ * "build": {
512
+ * "time": "Jun 24 2018 17:08:21",
513
+ * "productBundleIdentifier": "com.facebook.WebDriverAgentRunner"
514
+ * }
515
+ * }
516
+ *
517
+ * @param timeoutMs If zero or negative, returns immediately. Otherwise, waits up to timeoutMs.
518
+ */
519
+ private async getStatus (timeoutMs: number = 0): Promise<StringRecord | null> {
520
+ const noSessionProxy = new NoSessionProxy({
521
+ server: this.url.hostname ?? undefined,
522
+ port: parseInt(this.url.port ?? '', 10) || undefined,
523
+ base: this.basePath,
524
+ timeout: 3000,
525
+ });
526
+
527
+ const sendGetStatus = async () => await noSessionProxy.command('/status', 'GET') as StringRecord;
528
+
529
+ if (_.isNil(timeoutMs) || timeoutMs <= 0) {
530
+ try {
531
+ return await sendGetStatus();
532
+ } catch (err: any) {
533
+ this.log.debug(`WDA is not listening at '${this.url.href}'. Original error:: ${err.message}`);
534
+ return null;
535
+ }
536
+ }
537
+
538
+ let lastError: any = null;
539
+ let status: StringRecord | null = null;
540
+ try {
541
+ await waitForCondition(async () => {
542
+ try {
543
+ status = await sendGetStatus();
544
+ return true;
545
+ } catch (err) {
546
+ lastError = err;
547
+ }
548
+ return false;
549
+ }, {
550
+ waitMs: timeoutMs,
551
+ intervalMs: 300,
552
+ });
553
+ } catch (err: any) {
554
+ this.log.debug(`Failed to get the status endpoint in ${timeoutMs} ms. ` +
555
+ `The last error while accessing ${this.url.href}: ${lastError}. Original error:: ${err.message}.`);
556
+ throw new Error(`WDA was not ready in ${timeoutMs} ms.`);
557
+ }
558
+ return status;
559
+ }
560
+
561
+ /**
562
+ * Uninstall WDAs from the test device.
563
+ * Over Xcode 11, multiple WDA can be in the device since Xcode 11 generates different WDA.
564
+ * Appium does not expect multiple WDAs are running on a device.
565
+ */
566
+ private async uninstall (): Promise<void> {
567
+ try {
568
+ const bundleIds = await this.device.getUserInstalledBundleIdsByBundleName(WDA_CF_BUNDLE_NAME);
569
+ if (_.isEmpty(bundleIds)) {
570
+ this.log.debug('No WDAs on the device.');
571
+ return;
572
+ }
573
+
574
+ this.log.debug(`Uninstalling WDAs: '${bundleIds}'`);
575
+ for (const bundleId of bundleIds) {
576
+ await this.device.removeApp(bundleId);
577
+ }
578
+ } catch (e: any) {
579
+ this.log.debug(e);
580
+ this.log.warn(`WebDriverAgent uninstall failed. Perhaps, it is already uninstalled? ` +
581
+ `Original error: ${e.message}`);
582
+ }
583
+ }
584
+
585
+ private async _cleanupProjectIfFresh (): Promise<void> {
586
+ if (this.canSkipXcodebuild) {
587
+ return;
588
+ }
589
+
590
+ const packageInfo = JSON.parse(await fs.readFile(path.join(BOOTSTRAP_PATH, 'package.json'), 'utf8'));
591
+ const box = strongbox(packageInfo.name);
592
+ let boxItem = box.getItem(RECENT_MODULE_VERSION_ITEM_NAME);
593
+ if (!boxItem) {
594
+ const timestampPath = path.resolve(process.env.HOME ?? '', WDA_UPGRADE_TIMESTAMP_PATH);
595
+ if (await fs.exists(timestampPath)) {
596
+ // TODO: It is probably a bit ugly to hardcode the recent version string,
597
+ // TODO: hovewer it should do the job as a temporary transition trick
598
+ // TODO: to switch from a hardcoded file path to the strongbox usage.
599
+ try {
600
+ boxItem = await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, '5.0.0');
601
+ } catch (e: any) {
602
+ this.log.warn(`The actual module version cannot be persisted: ${e.message}`);
603
+ return;
604
+ }
605
+ } else {
606
+ this.log.info('There is no need to perform the project cleanup. A fresh install has been detected');
607
+ try {
608
+ await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, packageInfo.version);
609
+ } catch (e: any) {
610
+ this.log.warn(`The actual module version cannot be persisted: ${e.message}`);
611
+ }
612
+ return;
613
+ }
614
+ }
615
+
616
+ let recentModuleVersion = await boxItem.read();
617
+ try {
618
+ recentModuleVersion = util.coerceVersion(recentModuleVersion, true);
619
+ } catch (e: any) {
620
+ this.log.warn(`The persisted module version string has been damaged: ${e.message}`);
621
+ this.log.info(`Updating it to '${packageInfo.version}' assuming the project clenup is not needed`);
622
+ await boxItem.write(packageInfo.version);
623
+ return;
624
+ }
625
+
626
+ if (util.compareVersions(recentModuleVersion, '>=', packageInfo.version)) {
627
+ this.log.info(
628
+ `WebDriverAgent does not need a cleanup. The project sources are up to date ` +
629
+ `(${recentModuleVersion} >= ${packageInfo.version})`
630
+ );
631
+ return;
632
+ }
633
+
634
+ this.log.info(
635
+ `Cleaning up the WebDriverAgent project after the module upgrade has happened ` +
636
+ `(${recentModuleVersion} < ${packageInfo.version})`
637
+ );
638
+ try {
639
+ await this.xcodebuild.cleanProject();
640
+ await boxItem.write(packageInfo.version);
641
+ } catch (e: any) {
642
+ this.log.warn(`Cannot perform WebDriverAgent project cleanup. Original error: ${e.message}`);
643
+ }
644
+ }
645
+
646
+ /**
647
+ * Launch WDA with preinstalled package with 'xcrun devicectl device process launch'.
648
+ * The WDA package must be prepared properly like published via
649
+ * https://github.com/appium/WebDriverAgent/releases
650
+ * with proper sign for this case.
651
+ *
652
+ * When we implement launching XCTest service via appium-ios-device,
653
+ * this implementation can be replaced with it.
654
+ *
655
+ * @param opts launching WDA with devicectl command options.
656
+ */
657
+ private async _launchViaDevicectl(opts: {env?: Record<string, string | number>} = {}): Promise<void> {
658
+ const {env} = opts;
659
+
660
+ await this.device.devicectl.launchApp(
661
+ this.bundleIdForXctest, { env, terminateExisting: true }
662
+ );
663
+ }
664
+
665
+ /**
666
+ * Launch WDA with preinstalled package without xcodebuild.
667
+ * @param sessionId Launch WDA and establish the session with this sessionId
668
+ */
669
+ private async launchWithPreinstalledWDA(sessionId: string): Promise<StringRecord | null> {
670
+ const xctestEnv: Record<string, string | number> = {
671
+ USE_PORT: this.wdaLocalPort || WDA_AGENT_PORT,
672
+ WDA_PRODUCT_BUNDLE_IDENTIFIER: this.bundleIdForXctest
673
+ };
674
+ if (this.mjpegServerPort) {
675
+ xctestEnv.MJPEG_SERVER_PORT = this.mjpegServerPort;
676
+ }
677
+ if (this.wdaBindingIP) {
678
+ xctestEnv.USE_IP = this.wdaBindingIP;
679
+ }
680
+ this.log.info('Launching WebDriverAgent on the device without xcodebuild');
681
+ if (this.isRealDevice) {
682
+ // Current method to launch WDA process can be done via 'xcrun devicectl',
683
+ // but it has limitation about the WDA preinstalled package.
684
+ // https://github.com/appium/appium/issues/19206#issuecomment-2014182674
685
+ if (this.platformVersion && util.compareVersions(this.platformVersion, '>=', '17.0')) {
686
+ await this._launchViaDevicectl({env: xctestEnv});
687
+ } else {
688
+ this.xctestApiClient = new Xctest(this.device.udid, this.bundleIdForXctest, null, {env: xctestEnv});
689
+ await this.xctestApiClient.start();
690
+ }
691
+ } else {
692
+ await this.device.simctl.exec('launch', {
693
+ args: [
694
+ '--terminate-running-process',
695
+ this.device.udid,
696
+ this.bundleIdForXctest,
697
+ ],
698
+ env: xctestEnv,
699
+ });
700
+ }
701
+
702
+ this.setupProxies(sessionId);
703
+ let status: StringRecord | null;
704
+ try {
705
+ status = await this.getStatus(this.wdaLaunchTimeout);
706
+ } catch {
707
+ throw new Error(
708
+ `Failed to start the preinstalled WebDriverAgent in ${this.wdaLaunchTimeout} ms. ` +
709
+ `The WebDriverAgent might not be properly built or the device might be locked. ` +
710
+ `The 'appium:wdaLaunchTimeout' capability modifies the timeout.`
711
+ );
712
+ }
713
+ this.started = true;
714
+ return status;
715
+ }
716
+ }