appium-chromedriver 8.1.0 → 8.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import {JWProxy, PROTOCOLS} from '@appium/base-driver';
3
3
  import cp from 'child_process';
4
4
  import {system, fs, logger, util} from '@appium/support';
5
5
  import {retryInterval, asyncmap} from 'asyncbox';
6
- import {SubProcess, exec} from 'teen_process';
6
+ import {SubProcess, exec, type ExecError} from 'teen_process';
7
7
  import B from 'bluebird';
8
8
  import {
9
9
  getChromeVersion,
@@ -18,6 +18,10 @@ import path from 'path';
18
18
  import {compareVersions} from 'compare-versions';
19
19
  import {ChromedriverStorageClient} from './storage-client/storage-client';
20
20
  import {toW3cCapNames, getCapValue, toW3cCapName} from './protocol-helpers';
21
+ import type {ADB} from 'appium-adb';
22
+ import type {ProxyOptions, HTTPMethod, HTTPBody} from '@appium/types';
23
+ import type {Request, Response} from 'express';
24
+ import type {ChromedriverOpts, ChromedriverVersionMapping} from './types';
21
25
 
22
26
  const NEW_CD_VERSION_FORMAT_MAJOR_VERSION = 73;
23
27
  const DEFAULT_HOST = '127.0.0.1';
@@ -25,17 +29,60 @@ const MIN_CD_VERSION_WITH_W3C_SUPPORT = 75;
25
29
  const DEFAULT_PORT = 9515;
26
30
  const CHROME_BUNDLE_ID = 'com.android.chrome';
27
31
  const WEBVIEW_SHELL_BUNDLE_ID = 'org.chromium.webview_shell';
28
- const WEBVIEW_BUNDLE_IDS = ['com.google.android.webview', 'com.android.webview'];
32
+ const WEBVIEW_BUNDLE_IDS = ['com.google.android.webview', 'com.android.webview'] as const;
29
33
  const VERSION_PATTERN = /([\d.]+)/;
30
34
 
31
35
  const CD_VERSION_TIMEOUT = 5000;
32
36
 
37
+ interface ChromedriverInfo {
38
+ executable: string;
39
+ version: string;
40
+ minChromeVersion: string | null;
41
+ }
42
+
43
+ interface NewSessionResponse {
44
+ capabilities?: Record<string, any>;
45
+ [key: string]: any;
46
+ }
47
+
48
+ type SessionCapabilities = Record<string, any>;
49
+
33
50
  export class Chromedriver extends events.EventEmitter {
34
- /**
35
- *
36
- * @param {import('./types').ChromedriverOpts} args
37
- */
38
- constructor(args = {}) {
51
+ static readonly EVENT_ERROR = 'chromedriver_error';
52
+ static readonly EVENT_CHANGED = 'stateChanged';
53
+ static readonly STATE_STOPPED = 'stopped';
54
+ static readonly STATE_STARTING = 'starting';
55
+ static readonly STATE_ONLINE = 'online';
56
+ static readonly STATE_STOPPING = 'stopping';
57
+ static readonly STATE_RESTARTING = 'restarting';
58
+
59
+ private readonly _log: any;
60
+ private readonly proxyHost: string;
61
+ private readonly proxyPort: number;
62
+ private readonly adb?: ADB;
63
+ private readonly cmdArgs?: string[];
64
+ private proc: SubProcess | null;
65
+ private readonly useSystemExecutable: boolean;
66
+ private chromedriver?: string;
67
+ private readonly executableDir: string;
68
+ private readonly mappingPath?: string;
69
+ private bundleId?: string;
70
+ private executableVerified: boolean;
71
+ state: string;
72
+ private readonly _execFunc: typeof exec;
73
+ jwproxy: JWProxy;
74
+ private readonly isCustomExecutableDir: boolean;
75
+ private readonly verbose?: boolean;
76
+ private readonly logPath?: string;
77
+ private readonly disableBuildCheck: boolean;
78
+ private readonly storageClient: ChromedriverStorageClient | null;
79
+ private readonly details?: ChromedriverOpts['details'];
80
+ private capabilities: SessionCapabilities;
81
+ private _desiredProtocol: keyof typeof PROTOCOLS | null;
82
+ private _driverVersion: string | null;
83
+ private _onlineStatus: Record<string, any> | null;
84
+
85
+ constructor(args: ChromedriverOpts = {}) {
39
86
  super();
40
87
 
41
88
  const {
@@ -58,13 +105,12 @@ export class Chromedriver extends events.EventEmitter {
58
105
  this._log = logger.getLogger(generateLogPrefix(this));
59
106
 
60
107
  this.proxyHost = host;
61
- this.proxyPort = port;
108
+ this.proxyPort = parseInt(String(port), 10);
62
109
  this.adb = adb;
63
110
  this.cmdArgs = cmdArgs;
64
111
  this.proc = null;
65
112
  this.useSystemExecutable = useSystemExecutable;
66
113
  this.chromedriver = executable;
67
- this.executableDir = executableDir;
68
114
  this.mappingPath = mappingPath;
69
115
  this.bundleId = bundleId;
70
116
  this.executableVerified = false;
@@ -73,8 +119,7 @@ export class Chromedriver extends events.EventEmitter {
73
119
  // to mock in unit test
74
120
  this._execFunc = exec;
75
121
 
76
- /** @type {Record<string, any>} */
77
- const proxyOpts = {
122
+ const proxyOpts: ProxyOptions = {
78
123
  server: this.proxyHost,
79
124
  port: this.proxyPort,
80
125
  log: this._log,
@@ -83,12 +128,13 @@ export class Chromedriver extends events.EventEmitter {
83
128
  proxyOpts.reqBasePath = reqBasePath;
84
129
  }
85
130
  this.jwproxy = new JWProxy(proxyOpts);
86
- if (this.executableDir) {
131
+ if (executableDir) {
87
132
  // Expects the user set the executable directory explicitly
133
+ this.executableDir = executableDir;
88
134
  this.isCustomExecutableDir = true;
89
135
  } else {
90
- this.isCustomExecutableDir = false;
91
136
  this.executableDir = getChromedriverDir();
137
+ this.isCustomExecutableDir = false;
92
138
  }
93
139
 
94
140
  this.verbose = verbose;
@@ -98,30 +144,260 @@ export class Chromedriver extends events.EventEmitter {
98
144
  ? new ChromedriverStorageClient({chromedriverDir: this.executableDir})
99
145
  : null;
100
146
  this.details = details;
101
- /** @type {any} */
102
147
  this.capabilities = {};
103
- /** @type {keyof PROTOCOLS | null} */
104
148
  this._desiredProtocol = null;
105
149
 
106
150
  // Store the running driver version
107
- /** @type {string|null} */
108
151
  this._driverVersion = null;
109
- /** @type {Record<string, any> | null} */
110
152
  this._onlineStatus = null;
111
153
  }
112
154
 
155
+ /**
156
+ * Gets the logger instance for this Chromedriver instance.
157
+ * @returns The logger instance.
158
+ */
113
159
  get log() {
114
160
  return this._log;
115
161
  }
116
162
 
117
163
  /**
118
- * @returns {string | null}
164
+ * Gets the version of the currently running Chromedriver.
165
+ * @returns The driver version string, or null if not yet determined.
119
166
  */
120
- get driverVersion() {
167
+ get driverVersion(): string | null {
121
168
  return this._driverVersion;
122
169
  }
123
170
 
124
- async getDriversMapping() {
171
+ /**
172
+ * Starts a new Chromedriver session with the given capabilities.
173
+ * @param caps - The session capabilities to use.
174
+ * @param emitStartingState - Whether to emit the starting state event (default: true).
175
+ * @returns A promise that resolves to the session capabilities returned by Chromedriver.
176
+ * @throws {Error} If Chromedriver fails to start or crashes during startup.
177
+ */
178
+ async start(caps: SessionCapabilities, emitStartingState = true): Promise<SessionCapabilities> {
179
+ this.capabilities = _.cloneDeep(caps);
180
+
181
+ // set the logging preferences to ALL the console logs
182
+ this.capabilities.loggingPrefs = _.cloneDeep(getCapValue(caps, 'loggingPrefs', {}));
183
+ if (_.isEmpty(this.capabilities.loggingPrefs.browser)) {
184
+ this.capabilities.loggingPrefs.browser = 'ALL';
185
+ }
186
+
187
+ if (emitStartingState) {
188
+ this.changeState(Chromedriver.STATE_STARTING);
189
+ }
190
+
191
+ const args = this.buildChromedriverArgs();
192
+ // what are the process stdout/stderr conditions wherein we know that
193
+ // the process has started to our satisfaction?
194
+ const startDetector = (stdout: string) => stdout.startsWith('Starting ');
195
+
196
+ let processIsAlive = false;
197
+ let webviewVersion: string | undefined;
198
+ try {
199
+ const chromedriverPath = await this.initChromedriverPath();
200
+ await this.killAll();
201
+
202
+ // set up our subprocess object
203
+ this.proc = new SubProcess(chromedriverPath, args);
204
+ processIsAlive = true;
205
+
206
+ // handle log output
207
+ for (const streamName of ['stderr', 'stdout'] as const) {
208
+ this.proc.on(`line-${streamName}`, (line: string) => {
209
+ // if the cd output is not printed, find the chrome version and print
210
+ // will get a response like
211
+ // DevTools response: {
212
+ // "Android-Package": "io.appium.sampleapp",
213
+ // "Browser": "Chrome/55.0.2883.91",
214
+ // "Protocol-Version": "1.2",
215
+ // "User-Agent": "...",
216
+ // "WebKit-Version": "537.36"
217
+ // }
218
+ if (!webviewVersion) {
219
+ const match = /"Browser": "([^"]+)"/.exec(line);
220
+ if (match) {
221
+ webviewVersion = match[1];
222
+ this.log.debug(`Webview version: '${webviewVersion}'`);
223
+ }
224
+ }
225
+
226
+ if (this.verbose) {
227
+ // give the output if it is requested
228
+ this.log.debug(`[${streamName.toUpperCase()}] ${line}`);
229
+ }
230
+ });
231
+ }
232
+
233
+ // handle out-of-bound exit by simply emitting a stopped state
234
+ this.proc.once('exit', (code: number | null, signal: string | null) => {
235
+ this._driverVersion = null;
236
+ this._desiredProtocol = null;
237
+ this._onlineStatus = null;
238
+ processIsAlive = false;
239
+ if (
240
+ this.state !== Chromedriver.STATE_STOPPED &&
241
+ this.state !== Chromedriver.STATE_STOPPING &&
242
+ this.state !== Chromedriver.STATE_RESTARTING
243
+ ) {
244
+ const msg = `Chromedriver exited unexpectedly with code ${code}, signal ${signal}`;
245
+ this.log.error(msg);
246
+ this.changeState(Chromedriver.STATE_STOPPED);
247
+ }
248
+ this.proc?.removeAllListeners();
249
+ this.proc = null;
250
+ });
251
+ this.log.info(`Spawning Chromedriver with: ${this.chromedriver} ${args.join(' ')}`);
252
+ // start subproc and wait for startDetector
253
+ await this.proc.start(startDetector);
254
+ await this.waitForOnline();
255
+ this.syncProtocol();
256
+ return await this.startSession();
257
+ } catch (e) {
258
+ const err = e as Error;
259
+ this.log.debug(err);
260
+ this.emit(Chromedriver.EVENT_ERROR, err);
261
+ // just because we had an error doesn't mean the chromedriver process
262
+ // finished; we should clean up if necessary
263
+ if (processIsAlive) {
264
+ await this.proc?.stop();
265
+ }
266
+ this.proc?.removeAllListeners();
267
+ this.proc = null;
268
+
269
+ let message = '';
270
+ // often the user's Chrome version is not supported by the version of Chromedriver
271
+ if (err.message.includes('Chrome version must be')) {
272
+ message +=
273
+ 'Unable to automate Chrome version because it is not supported by this version of Chromedriver.\n';
274
+ if (webviewVersion) {
275
+ message += `Chrome version on the device: ${webviewVersion}\n`;
276
+ }
277
+ const versionsSupportedByDriver =
278
+ /Chrome version must be (.+)/.exec(err.message)?.[1] || '';
279
+ if (versionsSupportedByDriver) {
280
+ message += `Chromedriver supports Chrome version(s): ${versionsSupportedByDriver}\n`;
281
+ }
282
+ message += 'Check the driver tutorial for troubleshooting.\n';
283
+ }
284
+
285
+ message += err.message;
286
+ throw this.log.errorWithException(message);
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Gets the current session ID if the driver is online.
292
+ * @returns The session ID string, or null if the driver is not online.
293
+ */
294
+ sessionId(): string | null {
295
+ return this.state === Chromedriver.STATE_ONLINE ? this.jwproxy.sessionId : null;
296
+ }
297
+
298
+ /**
299
+ * Restarts the Chromedriver session.
300
+ * The session will be stopped and then started again with the same capabilities.
301
+ * @returns A promise that resolves to the session capabilities returned by Chromedriver.
302
+ * @throws {Error} If the driver is not online or if restart fails.
303
+ */
304
+ async restart(): Promise<SessionCapabilities> {
305
+ this.log.info('Restarting chromedriver');
306
+ if (this.state !== Chromedriver.STATE_ONLINE) {
307
+ throw new Error("Can't restart when we're not online");
308
+ }
309
+ this.changeState(Chromedriver.STATE_RESTARTING);
310
+ await this.stop(false);
311
+ return await this.start(this.capabilities, false);
312
+ }
313
+
314
+ /**
315
+ * Stops the Chromedriver session and terminates the process.
316
+ * @param emitStates - Whether to emit state change events during shutdown (default: true).
317
+ * @returns A promise that resolves when the session has been stopped.
318
+ */
319
+ async stop(emitStates = true): Promise<void> {
320
+ if (emitStates) {
321
+ this.changeState(Chromedriver.STATE_STOPPING);
322
+ }
323
+ const runSafeStep = async (f: () => Promise<any> | any): Promise<void> => {
324
+ try {
325
+ return await f();
326
+ } catch (e) {
327
+ const err = e as Error;
328
+ this.log.warn(err.message);
329
+ this.log.debug(err.stack);
330
+ }
331
+ };
332
+ await runSafeStep(() => this.jwproxy.command('', 'DELETE'));
333
+ await runSafeStep(() => {
334
+ this.proc?.stop('SIGTERM', 20000);
335
+ this.proc?.removeAllListeners();
336
+ this.proc = null;
337
+ });
338
+ this.log.prefix = generateLogPrefix(this);
339
+ if (emitStates) {
340
+ this.changeState(Chromedriver.STATE_STOPPED);
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Sends a command to the Chromedriver server.
346
+ * @param url - The endpoint URL (e.g., '/url', '/session').
347
+ * @param method - The HTTP method to use ('POST', 'GET', or 'DELETE').
348
+ * @param body - Optional request body for POST requests.
349
+ * @returns A promise that resolves to the response from Chromedriver.
350
+ */
351
+ async sendCommand(url: string, method: HTTPMethod, body: HTTPBody = null): Promise<HTTPBody> {
352
+ return await this.jwproxy.command(url, method, body);
353
+ }
354
+
355
+ /**
356
+ * Proxies an HTTP request/response to the Chromedriver server.
357
+ * @param req - The incoming HTTP request object.
358
+ * @param res - The outgoing HTTP response object.
359
+ * @returns A promise that resolves when the proxying is complete.
360
+ */
361
+ async proxyReq(req: Request, res: Response): Promise<void> {
362
+ await this.jwproxy.proxyReqRes(req, res);
363
+ }
364
+
365
+ /**
366
+ * Checks if Chromedriver is currently able to automate webviews.
367
+ * Sometimes Chromedriver stops automating webviews; this method runs a simple
368
+ * command to determine the current state.
369
+ * @returns A promise that resolves to true if webviews are working, false otherwise.
370
+ */
371
+ async hasWorkingWebview(): Promise<boolean> {
372
+ try {
373
+ await this.jwproxy.command('/url', 'GET');
374
+ return true;
375
+ } catch {
376
+ return false;
377
+ }
378
+ }
379
+
380
+ // Private methods at the tail of the class
381
+
382
+ private buildChromedriverArgs(): string[] {
383
+ const args = [`--port=${this.proxyPort}`];
384
+ if (this.adb?.adbPort) {
385
+ args.push(`--adb-port=${this.adb.adbPort}`);
386
+ }
387
+ if (_.isArray(this.cmdArgs)) {
388
+ args.push(...this.cmdArgs);
389
+ }
390
+ if (this.logPath) {
391
+ args.push(`--log-path=${this.logPath}`);
392
+ }
393
+ if (this.disableBuildCheck) {
394
+ args.push('--disable-build-check');
395
+ }
396
+ args.push('--verbose');
397
+ return args;
398
+ }
399
+
400
+ private async getDriversMapping(): Promise<ChromedriverVersionMapping> {
125
401
  let mapping = _.cloneDeep(CHROMEDRIVER_CHROME_MAPPING);
126
402
  if (this.mappingPath) {
127
403
  this.log.debug(`Attempting to use Chromedriver->Chrome mapping from '${this.mappingPath}'`);
@@ -132,7 +408,7 @@ export class Chromedriver extends events.EventEmitter {
132
408
  try {
133
409
  mapping = JSON.parse(await fs.readFile(this.mappingPath, 'utf8'));
134
410
  } catch (e) {
135
- const err = /** @type {Error} */ (e);
411
+ const err = e as Error;
136
412
  this.log.warn(`Error parsing mapping from '${this.mappingPath}': ${err.message}`);
137
413
  this.log.info('Defaulting to the static Chromedriver->Chrome mapping');
138
414
  }
@@ -153,10 +429,7 @@ export class Chromedriver extends events.EventEmitter {
153
429
  return mapping;
154
430
  }
155
431
 
156
- /**
157
- * @param {ChromedriverVersionMapping} mapping
158
- */
159
- async getChromedrivers(mapping) {
432
+ private async getChromedrivers(mapping: ChromedriverVersionMapping): Promise<ChromedriverInfo[]> {
160
433
  // go through the versions available
161
434
  const executables = await fs.glob('*', {
162
435
  cwd: this.executableDir,
@@ -168,11 +441,16 @@ export class Chromedriver extends events.EventEmitter {
168
441
  `in '${this.executableDir}'`,
169
442
  );
170
443
  const cds = (
171
- await asyncmap(executables, async (executable) => {
172
- /**
173
- * @param {{message: string, stdout?: string, stderr?: string}} opts
174
- */
175
- const logError = ({message, stdout, stderr}) => {
444
+ await asyncmap(executables, async (executable: string) => {
445
+ const logError = ({
446
+ message,
447
+ stdout,
448
+ stderr,
449
+ }: {
450
+ message: string;
451
+ stdout?: string;
452
+ stderr?: string;
453
+ }): null => {
176
454
  let errMsg =
177
455
  `Cannot retrieve version number from '${path.basename(
178
456
  executable,
@@ -188,14 +466,14 @@ export class Chromedriver extends events.EventEmitter {
188
466
  return null;
189
467
  };
190
468
 
191
- let stdout;
192
- let stderr;
469
+ let stdout: string;
470
+ let stderr: string | undefined;
193
471
  try {
194
472
  ({stdout, stderr} = await this._execFunc(executable, ['--version'], {
195
473
  timeout: CD_VERSION_TIMEOUT,
196
474
  }));
197
475
  } catch (e) {
198
- const err = /** @type {import('teen_process').ExecError} */ (e);
476
+ const err = e as ExecError;
199
477
  if (
200
478
  !(err.message || '').includes('timed out') &&
201
479
  !(err.stdout || '').includes('Starting ChromeDriver')
@@ -213,15 +491,13 @@ export class Chromedriver extends events.EventEmitter {
213
491
  return logError({message: 'Cannot parse the version string', stdout, stderr});
214
492
  }
215
493
  let version = match[1];
216
- let minChromeVersion = mapping[version];
494
+ let minChromeVersion = mapping[version] || null;
217
495
  const coercedVersion = semver.coerce(version);
218
496
  if (coercedVersion) {
219
497
  // before 2019-03-06 versions were of the form major.minor
220
498
  if (coercedVersion.major < NEW_CD_VERSION_FORMAT_MAJOR_VERSION) {
221
- version = /** @type {keyof typeof mapping} */ (
222
- `${coercedVersion.major}.${coercedVersion.minor}`
223
- );
224
- minChromeVersion = mapping[version];
499
+ version = `${coercedVersion.major}.${coercedVersion.minor}`;
500
+ minChromeVersion = mapping[version] || null;
225
501
  }
226
502
  if (!minChromeVersion && coercedVersion.major >= NEW_CD_VERSION_FORMAT_MAJOR_VERSION) {
227
503
  // Assume the major Chrome version is the same as the corresponding driver major version
@@ -235,7 +511,7 @@ export class Chromedriver extends events.EventEmitter {
235
511
  };
236
512
  })
237
513
  )
238
- .filter((cd) => !!cd)
514
+ .filter((cd): cd is ChromedriverInfo => !!cd)
239
515
  .sort((a, b) => compareVersions(b.version, a.version));
240
516
  if (_.isEmpty(cds)) {
241
517
  this.log.info(`No Chromedrivers were found in '${this.executableDir}'`);
@@ -252,7 +528,7 @@ export class Chromedriver extends events.EventEmitter {
252
528
  return cds;
253
529
  }
254
530
 
255
- async getChromeVersion() {
531
+ private async getChromeVersion(): Promise<semver.SemVer | null> {
256
532
  // Try to retrieve the version from `details` property if it is set
257
533
  // The `info` item must contain the output of /json/version CDP command
258
534
  // where `Browser` field looks like `Chrome/72.0.3601.0``
@@ -267,7 +543,7 @@ export class Chromedriver extends events.EventEmitter {
267
543
  }
268
544
  }
269
545
 
270
- let chromeVersion;
546
+ let chromeVersion: string | undefined;
271
547
 
272
548
  // in case of WebView Browser Tester, simply try to find the underlying webview
273
549
  if (this.bundleId === WEBVIEW_SHELL_BUNDLE_ID) {
@@ -321,12 +597,7 @@ export class Chromedriver extends events.EventEmitter {
321
597
  return chromeVersion ? semver.coerce(chromeVersion) : null;
322
598
  }
323
599
 
324
- /**
325
- *
326
- * @param {ChromedriverVersionMapping} newMapping
327
- * @returns {Promise<void>}
328
- */
329
- async updateDriversMapping(newMapping) {
600
+ private async updateDriversMapping(newMapping: ChromedriverVersionMapping): Promise<void> {
330
601
  let shouldUpdateStaticMapping = true;
331
602
  if (!this.mappingPath) {
332
603
  this.log.warn('No mapping path provided');
@@ -337,7 +608,7 @@ export class Chromedriver extends events.EventEmitter {
337
608
  await fs.writeFile(this.mappingPath, JSON.stringify(newMapping, null, 2), 'utf8');
338
609
  shouldUpdateStaticMapping = false;
339
610
  } catch (e) {
340
- const err = /** @type {Error} */ (e);
611
+ const err = e as Error;
341
612
  this.log.warn(
342
613
  `Cannot store the updated chromedrivers mapping into '${this.mappingPath}'. ` +
343
614
  `This may reduce the performance of further executions. Original error: ${err.message}`,
@@ -349,12 +620,7 @@ export class Chromedriver extends events.EventEmitter {
349
620
  }
350
621
  }
351
622
 
352
- /**
353
- * When executableDir is given explicitly for non-adb environment,
354
- * this method will respect the executableDir rather than the system installed binary.
355
- * @returns {Promise<string>}
356
- */
357
- async getCompatibleChromedriver() {
623
+ private async getCompatibleChromedriver(): Promise<string> {
358
624
  if (!this.adb && !this.isCustomExecutableDir) {
359
625
  return await getChromedriverBinaryPath();
360
626
  }
@@ -365,11 +631,7 @@ export class Chromedriver extends events.EventEmitter {
365
631
  }
366
632
 
367
633
  let didStorageSync = false;
368
- /**
369
- *
370
- * @param {import('semver').SemVer} chromeVersion
371
- */
372
- const syncChromedrivers = async (chromeVersion) => {
634
+ const syncChromedrivers = async (chromeVersion: semver.SemVer): Promise<boolean> => {
373
635
  didStorageSync = true;
374
636
  if (!this.storageClient) {
375
637
  return false;
@@ -389,17 +651,16 @@ export class Chromedriver extends events.EventEmitter {
389
651
  const {version, minBrowserVersion} = retrievedMapping[x];
390
652
  acc[version] = minBrowserVersion;
391
653
  return acc;
392
- }, /** @type {ChromedriverVersionMapping} */ ({}));
654
+ }, {} as ChromedriverVersionMapping);
393
655
  Object.assign(mapping, synchronizedDriversMapping);
394
656
  await this.updateDriversMapping(mapping);
395
657
  return true;
396
658
  };
397
659
 
398
- do {
660
+ while (true) {
399
661
  const cds = await this.getChromedrivers(mapping);
400
662
 
401
- /** @type {ChromedriverVersionMapping} */
402
- const missingVersions = {};
663
+ const missingVersions: ChromedriverVersionMapping = {};
403
664
  for (const {version, minChromeVersion} of cds) {
404
665
  if (!minChromeVersion || mapping[version]) {
405
666
  continue;
@@ -473,7 +734,7 @@ export class Chromedriver extends events.EventEmitter {
473
734
  continue;
474
735
  }
475
736
  } catch (e) {
476
- const err = /** @type {Error} */ (e);
737
+ const err = e as Error;
477
738
  this.log.warn(
478
739
  `Cannot synchronize local chromedrivers with the remote storage: ${err.message}`,
479
740
  );
@@ -499,13 +760,12 @@ export class Chromedriver extends events.EventEmitter {
499
760
  ` capability.`,
500
761
  );
501
762
  return binPath;
502
- // eslint-disable-next-line no-constant-condition
503
- } while (true);
763
+ }
504
764
  }
505
765
 
506
- async initChromedriverPath() {
766
+ private async initChromedriverPath(): Promise<string> {
507
767
  if (this.executableVerified && this.chromedriver) {
508
- return /** @type {string} */ (this.chromedriver);
768
+ return this.chromedriver;
509
769
  }
510
770
 
511
771
  let chromedriver = this.chromedriver;
@@ -521,21 +781,15 @@ export class Chromedriver extends events.EventEmitter {
521
781
  if (!(await fs.exists(chromedriver))) {
522
782
  throw new Error(
523
783
  `Trying to use a chromedriver binary at the path ` +
524
- `${this.chromedriver}, but it doesn't exist!`,
784
+ `${chromedriver}, but it doesn't exist!`,
525
785
  );
526
786
  }
527
787
  this.executableVerified = true;
528
- this.log.info(`Set chromedriver binary as: ${this.chromedriver}`);
529
- return /** @type {string} */ (this.chromedriver);
788
+ this.log.info(`Set chromedriver binary as: ${chromedriver}`);
789
+ return chromedriver;
530
790
  }
531
791
 
532
- /**
533
- * Determines the driver communication protocol
534
- * based on various validation rules.
535
- *
536
- * @returns {keyof PROTOCOLS}
537
- */
538
- syncProtocol() {
792
+ private syncProtocol(): keyof typeof PROTOCOLS {
539
793
  if (this.driverVersion) {
540
794
  const coercedVersion = semver.coerce(this.driverVersion);
541
795
  if (!coercedVersion || coercedVersion.major < MIN_CD_VERSION_WITH_W3C_SUPPORT) {
@@ -571,158 +825,7 @@ export class Chromedriver extends events.EventEmitter {
571
825
  return this._desiredProtocol;
572
826
  }
573
827
 
574
- /**
575
- *
576
- * @param {SessionCapabilities} caps
577
- * @param {boolean} emitStartingState
578
- * @returns {Promise<SessionCapabilities>}
579
- */
580
- async start(caps, emitStartingState = true) {
581
- this.capabilities = _.cloneDeep(caps);
582
-
583
- // set the logging preferences to ALL the console logs
584
- this.capabilities.loggingPrefs = _.cloneDeep(getCapValue(caps, 'loggingPrefs', {}));
585
- if (_.isEmpty(this.capabilities.loggingPrefs.browser)) {
586
- this.capabilities.loggingPrefs.browser = 'ALL';
587
- }
588
-
589
- if (emitStartingState) {
590
- this.changeState(Chromedriver.STATE_STARTING);
591
- }
592
-
593
- const args = [`--port=${this.proxyPort}`];
594
- if (this.adb?.adbPort) {
595
- args.push(`--adb-port=${this.adb.adbPort}`);
596
- }
597
- if (_.isArray(this.cmdArgs)) {
598
- args.push(...this.cmdArgs);
599
- }
600
- if (this.logPath) {
601
- args.push(`--log-path=${this.logPath}`);
602
- }
603
- if (this.disableBuildCheck) {
604
- args.push('--disable-build-check');
605
- }
606
- args.push('--verbose');
607
- // what are the process stdout/stderr conditions wherein we know that
608
- // the process has started to our satisfaction?
609
- const startDetector = /** @param {string} stdout */ (stdout) => stdout.startsWith('Starting ');
610
-
611
- let processIsAlive = false;
612
- /** @type {string|undefined} */
613
- let webviewVersion;
614
- try {
615
- const chromedriverPath = await this.initChromedriverPath();
616
- await this.killAll();
617
-
618
- // set up our subprocess object
619
- this.proc = new SubProcess(chromedriverPath, args);
620
- processIsAlive = true;
621
-
622
- // handle log output
623
- for (const streamName of ['stderr', 'stdout']) {
624
- this.proc.on(`line-${streamName}`, (line) => {
625
- // if the cd output is not printed, find the chrome version and print
626
- // will get a response like
627
- // DevTools response: {
628
- // "Android-Package": "io.appium.sampleapp",
629
- // "Browser": "Chrome/55.0.2883.91",
630
- // "Protocol-Version": "1.2",
631
- // "User-Agent": "...",
632
- // "WebKit-Version": "537.36"
633
- // }
634
- if (!webviewVersion) {
635
- const match = /"Browser": "([^"]+)"/.exec(line);
636
- if (match) {
637
- webviewVersion = match[1];
638
- this.log.debug(`Webview version: '${webviewVersion}'`);
639
- }
640
- }
641
-
642
- if (this.verbose) {
643
- // give the output if it is requested
644
- this.log.debug(`[${streamName.toUpperCase()}] ${line}`);
645
- }
646
- });
647
- }
648
-
649
- // handle out-of-bound exit by simply emitting a stopped state
650
- this.proc.once('exit', (code, signal) => {
651
- this._driverVersion = null;
652
- this._desiredProtocol = null;
653
- this._onlineStatus = null;
654
- processIsAlive = false;
655
- if (
656
- this.state !== Chromedriver.STATE_STOPPED &&
657
- this.state !== Chromedriver.STATE_STOPPING &&
658
- this.state !== Chromedriver.STATE_RESTARTING
659
- ) {
660
- const msg = `Chromedriver exited unexpectedly with code ${code}, signal ${signal}`;
661
- this.log.error(msg);
662
- this.changeState(Chromedriver.STATE_STOPPED);
663
- }
664
- this.proc?.removeAllListeners();
665
- this.proc = null;
666
- });
667
- this.log.info(`Spawning Chromedriver with: ${this.chromedriver} ${args.join(' ')}`);
668
- // start subproc and wait for startDetector
669
- await this.proc.start(startDetector);
670
- await this.waitForOnline();
671
- this.syncProtocol();
672
- return await this.startSession();
673
- } catch (e) {
674
- const err = /** @type {Error} */ (e);
675
- this.log.debug(err);
676
- this.emit(Chromedriver.EVENT_ERROR, err);
677
- // just because we had an error doesn't mean the chromedriver process
678
- // finished; we should clean up if necessary
679
- if (processIsAlive) {
680
- await this.proc?.stop();
681
- }
682
- this.proc?.removeAllListeners();
683
- this.proc = null;
684
-
685
- let message = '';
686
- // often the user's Chrome version is not supported by the version of Chromedriver
687
- if (err.message.includes('Chrome version must be')) {
688
- message +=
689
- 'Unable to automate Chrome version because it is not supported by this version of Chromedriver.\n';
690
- if (webviewVersion) {
691
- message += `Chrome version on the device: ${webviewVersion}\n`;
692
- }
693
- const versionsSupportedByDriver =
694
- /Chrome version must be (.+)/.exec(err.message)?.[1] || '';
695
- if (versionsSupportedByDriver) {
696
- message += `Chromedriver supports Chrome version(s): ${versionsSupportedByDriver}\n`;
697
- }
698
- message += 'Check the driver tutorial for troubleshooting.\n';
699
- }
700
-
701
- message += err.message;
702
- throw this.log.errorWithException(message);
703
- }
704
- }
705
-
706
- sessionId() {
707
- return this.state === Chromedriver.STATE_ONLINE ? this.jwproxy.sessionId : null;
708
- }
709
-
710
- /**
711
- * Restarts the chromedriver session
712
- *
713
- * @returns {Promise<SessionCapabilities>}
714
- */
715
- async restart() {
716
- this.log.info('Restarting chromedriver');
717
- if (this.state !== Chromedriver.STATE_ONLINE) {
718
- throw new Error("Can't restart when we're not online");
719
- }
720
- this.changeState(Chromedriver.STATE_RESTARTING);
721
- await this.stop(false);
722
- return await this.start(this.capabilities, false);
723
- }
724
-
725
- async waitForOnline() {
828
+ private async waitForOnline(): Promise<void> {
726
829
  // we need to make sure that CD hasn't crashed
727
830
  let chromedriverStopped = false;
728
831
  await retryInterval(20, 200, async () => {
@@ -731,8 +834,7 @@ export class Chromedriver extends events.EventEmitter {
731
834
  chromedriverStopped = true;
732
835
  return;
733
836
  }
734
- /** @type {any} */
735
- const status = await this.getStatus();
837
+ const status: any = await this.getStatus();
736
838
  if (!_.isPlainObject(status) || !status.ready) {
737
839
  throw new Error(`The response to the /status API is not valid: ${JSON.stringify(status)}`);
738
840
  }
@@ -750,16 +852,11 @@ export class Chromedriver extends events.EventEmitter {
750
852
  }
751
853
  }
752
854
 
753
- async getStatus() {
855
+ private async getStatus(): Promise<any> {
754
856
  return await this.jwproxy.command('/status', 'GET');
755
857
  }
756
858
 
757
- /**
758
- * Starts a new session
759
- *
760
- * @returns {Promise<SessionCapabilities>}
761
- */
762
- async startSession() {
859
+ private async startSession(): Promise<SessionCapabilities> {
763
860
  const sessionCaps =
764
861
  this._desiredProtocol === PROTOCOLS.W3C
765
862
  ? {capabilities: {alwaysMatch: toW3cCapNames(this.capabilities)}}
@@ -768,75 +865,20 @@ export class Chromedriver extends events.EventEmitter {
768
865
  `Starting ${this._desiredProtocol} Chromedriver session with capabilities: ` +
769
866
  JSON.stringify(sessionCaps, null, 2),
770
867
  );
771
- const response = /** @type {NewSessionResponse} */ (
772
- await this.jwproxy.command('/session', 'POST', sessionCaps)
773
- );
868
+ const response = (await this.jwproxy.command('/session', 'POST', sessionCaps)) as NewSessionResponse;
774
869
  this.log.prefix = generateLogPrefix(this, this.jwproxy.sessionId);
775
870
  this.changeState(Chromedriver.STATE_ONLINE);
776
- return _.has(response, 'capabilities') ? response.capabilities : response;
777
- }
778
-
779
- async stop(emitStates = true) {
780
- if (emitStates) {
781
- this.changeState(Chromedriver.STATE_STOPPING);
782
- }
783
- /**
784
- *
785
- * @param {() => Promise<any>|any} f
786
- */
787
- const runSafeStep = async (f) => {
788
- try {
789
- return await f();
790
- } catch (e) {
791
- const err = /** @type {Error} */ (e);
792
- this.log.warn(err.message);
793
- this.log.debug(err.stack);
794
- }
795
- };
796
- await runSafeStep(() => this.jwproxy.command('', 'DELETE'));
797
- await runSafeStep(() => {
798
- this.proc?.stop('SIGTERM', 20000);
799
- this.proc?.removeAllListeners();
800
- this.proc = null;
801
- });
802
- this.log.prefix = generateLogPrefix(this);
803
- if (emitStates) {
804
- this.changeState(Chromedriver.STATE_STOPPED);
805
- }
871
+ return _.has(response, 'capabilities') && response.capabilities ? response.capabilities : (response as SessionCapabilities);
806
872
  }
807
873
 
808
- /**
809
- *
810
- * @param {string} state
811
- */
812
- changeState(state) {
874
+ private changeState(state: string): void {
813
875
  this.state = state;
814
876
  this.log.debug(`Changed state to '${state}'`);
815
877
  this.emit(Chromedriver.EVENT_CHANGED, {state});
816
878
  }
817
879
 
818
- /**
819
- *
820
- * @param {string} url
821
- * @param {'POST'|'GET'|'DELETE'} method
822
- * @param {any} body
823
- * @returns
824
- */
825
- async sendCommand(url, method, body) {
826
- return await this.jwproxy.command(url, method, body);
827
- }
828
-
829
- /**
830
- *
831
- * @param {any} req
832
- * @param {any} res
833
- */
834
- async proxyReq(req, res) {
835
- return await this.jwproxy.proxyReqRes(req, res);
836
- }
837
-
838
- async killAll() {
839
- let cmd = system.isWindows()
880
+ private async killAll(): Promise<void> {
881
+ const cmd = system.isWindows()
840
882
  ? `wmic process where "commandline like '%chromedriver.exe%--port=${this.proxyPort}%'" delete`
841
883
  : `pkill -15 -f "${this.chromedriver}.*--port=${this.proxyPort}"`;
842
884
  this.log.debug(`Killing any old chromedrivers, running: ${cmd}`);
@@ -858,52 +900,21 @@ export class Chromedriver extends events.EventEmitter {
858
900
  }
859
901
 
860
902
  try {
861
- for (let conn of await this.adb.getForwardList()) {
903
+ for (const conn of await this.adb.getForwardList()) {
862
904
  // chromedriver will ask ADB to forward a port like "deviceId tcp:port localabstract:webview_devtools_remote_port"
863
905
  if (!(conn.includes('webview_devtools') && (!udid || conn.includes(udid)))) {
864
906
  continue;
865
907
  }
866
908
 
867
- let params = conn.split(/\s+/);
909
+ const params = conn.split(/\s+/);
868
910
  if (params.length > 1) {
869
911
  await this.adb.removePortForward(params[1].replace(/[\D]*/, ''));
870
912
  }
871
913
  }
872
914
  } catch (e) {
873
- const err = /** @type {Error} */ (e);
915
+ const err = e as Error;
874
916
  this.log.warn(`Unable to clean forwarded ports. Error: '${err.message}'. Continuing.`);
875
917
  }
876
918
  }
877
919
  }
878
-
879
- /**
880
- * @returns {Promise<boolean>}
881
- */
882
- async hasWorkingWebview() {
883
- // sometimes chromedriver stops automating webviews. this method runs a
884
- // simple command to determine our state, and responds accordingly
885
- try {
886
- await this.jwproxy.command('/url', 'GET');
887
- return true;
888
- } catch {
889
- return false;
890
- }
891
- }
892
920
  }
893
-
894
- Chromedriver.EVENT_ERROR = 'chromedriver_error';
895
- Chromedriver.EVENT_CHANGED = 'stateChanged';
896
- Chromedriver.STATE_STOPPED = 'stopped';
897
- Chromedriver.STATE_STARTING = 'starting';
898
- Chromedriver.STATE_ONLINE = 'online';
899
- Chromedriver.STATE_STOPPING = 'stopping';
900
- Chromedriver.STATE_RESTARTING = 'restarting';
901
-
902
- /**
903
- * @typedef {import('./types').ChromedriverVersionMapping} ChromedriverVersionMapping
904
- */
905
-
906
- /**
907
- * @typedef {{capabilities: Record<string, any>}} NewSessionResponse
908
- * @typedef {Record<string, any>} SessionCapabilities
909
- */