appium-chromedriver 8.1.0 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/build/lib/chromedriver.d.ts +88 -117
- package/build/lib/chromedriver.d.ts.map +1 -1
- package/build/lib/chromedriver.js +273 -284
- package/build/lib/chromedriver.js.map +1 -1
- package/build/lib/constants.d.ts +18 -18
- package/build/lib/constants.d.ts.map +1 -1
- package/build/lib/constants.js.map +1 -1
- package/build/lib/protocol-helpers.d.ts +14 -13
- package/build/lib/protocol-helpers.d.ts.map +1 -1
- package/build/lib/protocol-helpers.js +13 -12
- package/build/lib/protocol-helpers.js.map +1 -1
- package/build/lib/utils.d.ts +55 -41
- package/build/lib/utils.d.ts.map +1 -1
- package/build/lib/utils.js +51 -54
- package/build/lib/utils.js.map +1 -1
- package/lib/{chromedriver.js → chromedriver.ts} +344 -333
- package/lib/{constants.js → constants.ts} +5 -4
- package/lib/protocol-helpers.ts +44 -0
- package/lib/utils.ts +185 -0
- package/package.json +2 -1
- package/lib/protocol-helpers.js +0 -44
- package/lib/utils.js +0 -189
|
@@ -61,29 +61,55 @@ const WEBVIEW_BUNDLE_IDS = ['com.google.android.webview', 'com.android.webview']
|
|
|
61
61
|
const VERSION_PATTERN = /([\d.]+)/;
|
|
62
62
|
const CD_VERSION_TIMEOUT = 5000;
|
|
63
63
|
class Chromedriver extends events_1.default.EventEmitter {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
static EVENT_ERROR = 'chromedriver_error';
|
|
65
|
+
static EVENT_CHANGED = 'stateChanged';
|
|
66
|
+
static STATE_STOPPED = 'stopped';
|
|
67
|
+
static STATE_STARTING = 'starting';
|
|
68
|
+
static STATE_ONLINE = 'online';
|
|
69
|
+
static STATE_STOPPING = 'stopping';
|
|
70
|
+
static STATE_RESTARTING = 'restarting';
|
|
71
|
+
_log;
|
|
72
|
+
proxyHost;
|
|
73
|
+
proxyPort;
|
|
74
|
+
adb;
|
|
75
|
+
cmdArgs;
|
|
76
|
+
proc;
|
|
77
|
+
useSystemExecutable;
|
|
78
|
+
chromedriver;
|
|
79
|
+
executableDir;
|
|
80
|
+
mappingPath;
|
|
81
|
+
bundleId;
|
|
82
|
+
executableVerified;
|
|
83
|
+
state;
|
|
84
|
+
_execFunc;
|
|
85
|
+
jwproxy;
|
|
86
|
+
isCustomExecutableDir;
|
|
87
|
+
verbose;
|
|
88
|
+
logPath;
|
|
89
|
+
disableBuildCheck;
|
|
90
|
+
storageClient;
|
|
91
|
+
details;
|
|
92
|
+
capabilities;
|
|
93
|
+
_desiredProtocol;
|
|
94
|
+
_driverVersion;
|
|
95
|
+
_onlineStatus;
|
|
68
96
|
constructor(args = {}) {
|
|
69
97
|
super();
|
|
70
98
|
const { host = DEFAULT_HOST, port = DEFAULT_PORT, useSystemExecutable = false, executable, executableDir, bundleId, mappingPath, cmdArgs, adb, verbose, logPath, disableBuildCheck, details, isAutodownloadEnabled = false, reqBasePath, } = args;
|
|
71
99
|
this._log = support_1.logger.getLogger((0, utils_1.generateLogPrefix)(this));
|
|
72
100
|
this.proxyHost = host;
|
|
73
|
-
this.proxyPort = port;
|
|
101
|
+
this.proxyPort = parseInt(String(port), 10);
|
|
74
102
|
this.adb = adb;
|
|
75
103
|
this.cmdArgs = cmdArgs;
|
|
76
104
|
this.proc = null;
|
|
77
105
|
this.useSystemExecutable = useSystemExecutable;
|
|
78
106
|
this.chromedriver = executable;
|
|
79
|
-
this.executableDir = executableDir;
|
|
80
107
|
this.mappingPath = mappingPath;
|
|
81
108
|
this.bundleId = bundleId;
|
|
82
109
|
this.executableVerified = false;
|
|
83
110
|
this.state = Chromedriver.STATE_STOPPED;
|
|
84
111
|
// to mock in unit test
|
|
85
112
|
this._execFunc = teen_process_1.exec;
|
|
86
|
-
/** @type {Record<string, any>} */
|
|
87
113
|
const proxyOpts = {
|
|
88
114
|
server: this.proxyHost,
|
|
89
115
|
port: this.proxyPort,
|
|
@@ -93,13 +119,14 @@ class Chromedriver extends events_1.default.EventEmitter {
|
|
|
93
119
|
proxyOpts.reqBasePath = reqBasePath;
|
|
94
120
|
}
|
|
95
121
|
this.jwproxy = new base_driver_1.JWProxy(proxyOpts);
|
|
96
|
-
if (
|
|
122
|
+
if (executableDir) {
|
|
97
123
|
// Expects the user set the executable directory explicitly
|
|
124
|
+
this.executableDir = executableDir;
|
|
98
125
|
this.isCustomExecutableDir = true;
|
|
99
126
|
}
|
|
100
127
|
else {
|
|
101
|
-
this.isCustomExecutableDir = false;
|
|
102
128
|
this.executableDir = (0, utils_1.getChromedriverDir)();
|
|
129
|
+
this.isCustomExecutableDir = false;
|
|
103
130
|
}
|
|
104
131
|
this.verbose = verbose;
|
|
105
132
|
this.logPath = logPath;
|
|
@@ -108,25 +135,236 @@ class Chromedriver extends events_1.default.EventEmitter {
|
|
|
108
135
|
? new storage_client_1.ChromedriverStorageClient({ chromedriverDir: this.executableDir })
|
|
109
136
|
: null;
|
|
110
137
|
this.details = details;
|
|
111
|
-
/** @type {any} */
|
|
112
138
|
this.capabilities = {};
|
|
113
|
-
/** @type {keyof PROTOCOLS | null} */
|
|
114
139
|
this._desiredProtocol = null;
|
|
115
140
|
// Store the running driver version
|
|
116
|
-
/** @type {string|null} */
|
|
117
141
|
this._driverVersion = null;
|
|
118
|
-
/** @type {Record<string, any> | null} */
|
|
119
142
|
this._onlineStatus = null;
|
|
120
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Gets the logger instance for this Chromedriver instance.
|
|
146
|
+
* @returns The logger instance.
|
|
147
|
+
*/
|
|
121
148
|
get log() {
|
|
122
149
|
return this._log;
|
|
123
150
|
}
|
|
124
151
|
/**
|
|
125
|
-
*
|
|
152
|
+
* Gets the version of the currently running Chromedriver.
|
|
153
|
+
* @returns The driver version string, or null if not yet determined.
|
|
126
154
|
*/
|
|
127
155
|
get driverVersion() {
|
|
128
156
|
return this._driverVersion;
|
|
129
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Starts a new Chromedriver session with the given capabilities.
|
|
160
|
+
* @param caps - The session capabilities to use.
|
|
161
|
+
* @param emitStartingState - Whether to emit the starting state event (default: true).
|
|
162
|
+
* @returns A promise that resolves to the session capabilities returned by Chromedriver.
|
|
163
|
+
* @throws {Error} If Chromedriver fails to start or crashes during startup.
|
|
164
|
+
*/
|
|
165
|
+
async start(caps, emitStartingState = true) {
|
|
166
|
+
this.capabilities = lodash_1.default.cloneDeep(caps);
|
|
167
|
+
// set the logging preferences to ALL the console logs
|
|
168
|
+
this.capabilities.loggingPrefs = lodash_1.default.cloneDeep((0, protocol_helpers_1.getCapValue)(caps, 'loggingPrefs', {}));
|
|
169
|
+
if (lodash_1.default.isEmpty(this.capabilities.loggingPrefs.browser)) {
|
|
170
|
+
this.capabilities.loggingPrefs.browser = 'ALL';
|
|
171
|
+
}
|
|
172
|
+
if (emitStartingState) {
|
|
173
|
+
this.changeState(Chromedriver.STATE_STARTING);
|
|
174
|
+
}
|
|
175
|
+
const args = this.buildChromedriverArgs();
|
|
176
|
+
// what are the process stdout/stderr conditions wherein we know that
|
|
177
|
+
// the process has started to our satisfaction?
|
|
178
|
+
const startDetector = (stdout) => stdout.startsWith('Starting ');
|
|
179
|
+
let processIsAlive = false;
|
|
180
|
+
let webviewVersion;
|
|
181
|
+
try {
|
|
182
|
+
const chromedriverPath = await this.initChromedriverPath();
|
|
183
|
+
await this.killAll();
|
|
184
|
+
// set up our subprocess object
|
|
185
|
+
this.proc = new teen_process_1.SubProcess(chromedriverPath, args);
|
|
186
|
+
processIsAlive = true;
|
|
187
|
+
// handle log output
|
|
188
|
+
for (const streamName of ['stderr', 'stdout']) {
|
|
189
|
+
this.proc.on(`line-${streamName}`, (line) => {
|
|
190
|
+
// if the cd output is not printed, find the chrome version and print
|
|
191
|
+
// will get a response like
|
|
192
|
+
// DevTools response: {
|
|
193
|
+
// "Android-Package": "io.appium.sampleapp",
|
|
194
|
+
// "Browser": "Chrome/55.0.2883.91",
|
|
195
|
+
// "Protocol-Version": "1.2",
|
|
196
|
+
// "User-Agent": "...",
|
|
197
|
+
// "WebKit-Version": "537.36"
|
|
198
|
+
// }
|
|
199
|
+
if (!webviewVersion) {
|
|
200
|
+
const match = /"Browser": "([^"]+)"/.exec(line);
|
|
201
|
+
if (match) {
|
|
202
|
+
webviewVersion = match[1];
|
|
203
|
+
this.log.debug(`Webview version: '${webviewVersion}'`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (this.verbose) {
|
|
207
|
+
// give the output if it is requested
|
|
208
|
+
this.log.debug(`[${streamName.toUpperCase()}] ${line}`);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
// handle out-of-bound exit by simply emitting a stopped state
|
|
213
|
+
this.proc.once('exit', (code, signal) => {
|
|
214
|
+
this._driverVersion = null;
|
|
215
|
+
this._desiredProtocol = null;
|
|
216
|
+
this._onlineStatus = null;
|
|
217
|
+
processIsAlive = false;
|
|
218
|
+
if (this.state !== Chromedriver.STATE_STOPPED &&
|
|
219
|
+
this.state !== Chromedriver.STATE_STOPPING &&
|
|
220
|
+
this.state !== Chromedriver.STATE_RESTARTING) {
|
|
221
|
+
const msg = `Chromedriver exited unexpectedly with code ${code}, signal ${signal}`;
|
|
222
|
+
this.log.error(msg);
|
|
223
|
+
this.changeState(Chromedriver.STATE_STOPPED);
|
|
224
|
+
}
|
|
225
|
+
this.proc?.removeAllListeners();
|
|
226
|
+
this.proc = null;
|
|
227
|
+
});
|
|
228
|
+
this.log.info(`Spawning Chromedriver with: ${this.chromedriver} ${args.join(' ')}`);
|
|
229
|
+
// start subproc and wait for startDetector
|
|
230
|
+
await this.proc.start(startDetector);
|
|
231
|
+
await this.waitForOnline();
|
|
232
|
+
this.syncProtocol();
|
|
233
|
+
return await this.startSession();
|
|
234
|
+
}
|
|
235
|
+
catch (e) {
|
|
236
|
+
const err = e;
|
|
237
|
+
this.log.debug(err);
|
|
238
|
+
this.emit(Chromedriver.EVENT_ERROR, err);
|
|
239
|
+
// just because we had an error doesn't mean the chromedriver process
|
|
240
|
+
// finished; we should clean up if necessary
|
|
241
|
+
if (processIsAlive) {
|
|
242
|
+
await this.proc?.stop();
|
|
243
|
+
}
|
|
244
|
+
this.proc?.removeAllListeners();
|
|
245
|
+
this.proc = null;
|
|
246
|
+
let message = '';
|
|
247
|
+
// often the user's Chrome version is not supported by the version of Chromedriver
|
|
248
|
+
if (err.message.includes('Chrome version must be')) {
|
|
249
|
+
message +=
|
|
250
|
+
'Unable to automate Chrome version because it is not supported by this version of Chromedriver.\n';
|
|
251
|
+
if (webviewVersion) {
|
|
252
|
+
message += `Chrome version on the device: ${webviewVersion}\n`;
|
|
253
|
+
}
|
|
254
|
+
const versionsSupportedByDriver = /Chrome version must be (.+)/.exec(err.message)?.[1] || '';
|
|
255
|
+
if (versionsSupportedByDriver) {
|
|
256
|
+
message += `Chromedriver supports Chrome version(s): ${versionsSupportedByDriver}\n`;
|
|
257
|
+
}
|
|
258
|
+
message += 'Check the driver tutorial for troubleshooting.\n';
|
|
259
|
+
}
|
|
260
|
+
message += err.message;
|
|
261
|
+
throw this.log.errorWithException(message);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Gets the current session ID if the driver is online.
|
|
266
|
+
* @returns The session ID string, or null if the driver is not online.
|
|
267
|
+
*/
|
|
268
|
+
sessionId() {
|
|
269
|
+
return this.state === Chromedriver.STATE_ONLINE ? this.jwproxy.sessionId : null;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Restarts the Chromedriver session.
|
|
273
|
+
* The session will be stopped and then started again with the same capabilities.
|
|
274
|
+
* @returns A promise that resolves to the session capabilities returned by Chromedriver.
|
|
275
|
+
* @throws {Error} If the driver is not online or if restart fails.
|
|
276
|
+
*/
|
|
277
|
+
async restart() {
|
|
278
|
+
this.log.info('Restarting chromedriver');
|
|
279
|
+
if (this.state !== Chromedriver.STATE_ONLINE) {
|
|
280
|
+
throw new Error("Can't restart when we're not online");
|
|
281
|
+
}
|
|
282
|
+
this.changeState(Chromedriver.STATE_RESTARTING);
|
|
283
|
+
await this.stop(false);
|
|
284
|
+
return await this.start(this.capabilities, false);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Stops the Chromedriver session and terminates the process.
|
|
288
|
+
* @param emitStates - Whether to emit state change events during shutdown (default: true).
|
|
289
|
+
* @returns A promise that resolves when the session has been stopped.
|
|
290
|
+
*/
|
|
291
|
+
async stop(emitStates = true) {
|
|
292
|
+
if (emitStates) {
|
|
293
|
+
this.changeState(Chromedriver.STATE_STOPPING);
|
|
294
|
+
}
|
|
295
|
+
const runSafeStep = async (f) => {
|
|
296
|
+
try {
|
|
297
|
+
return await f();
|
|
298
|
+
}
|
|
299
|
+
catch (e) {
|
|
300
|
+
const err = e;
|
|
301
|
+
this.log.warn(err.message);
|
|
302
|
+
this.log.debug(err.stack);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
await runSafeStep(() => this.jwproxy.command('', 'DELETE'));
|
|
306
|
+
await runSafeStep(() => {
|
|
307
|
+
this.proc?.stop('SIGTERM', 20000);
|
|
308
|
+
this.proc?.removeAllListeners();
|
|
309
|
+
this.proc = null;
|
|
310
|
+
});
|
|
311
|
+
this.log.prefix = (0, utils_1.generateLogPrefix)(this);
|
|
312
|
+
if (emitStates) {
|
|
313
|
+
this.changeState(Chromedriver.STATE_STOPPED);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Sends a command to the Chromedriver server.
|
|
318
|
+
* @param url - The endpoint URL (e.g., '/url', '/session').
|
|
319
|
+
* @param method - The HTTP method to use ('POST', 'GET', or 'DELETE').
|
|
320
|
+
* @param body - Optional request body for POST requests.
|
|
321
|
+
* @returns A promise that resolves to the response from Chromedriver.
|
|
322
|
+
*/
|
|
323
|
+
async sendCommand(url, method, body = null) {
|
|
324
|
+
return await this.jwproxy.command(url, method, body);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Proxies an HTTP request/response to the Chromedriver server.
|
|
328
|
+
* @param req - The incoming HTTP request object.
|
|
329
|
+
* @param res - The outgoing HTTP response object.
|
|
330
|
+
* @returns A promise that resolves when the proxying is complete.
|
|
331
|
+
*/
|
|
332
|
+
async proxyReq(req, res) {
|
|
333
|
+
await this.jwproxy.proxyReqRes(req, res);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Checks if Chromedriver is currently able to automate webviews.
|
|
337
|
+
* Sometimes Chromedriver stops automating webviews; this method runs a simple
|
|
338
|
+
* command to determine the current state.
|
|
339
|
+
* @returns A promise that resolves to true if webviews are working, false otherwise.
|
|
340
|
+
*/
|
|
341
|
+
async hasWorkingWebview() {
|
|
342
|
+
try {
|
|
343
|
+
await this.jwproxy.command('/url', 'GET');
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Private methods at the tail of the class
|
|
351
|
+
buildChromedriverArgs() {
|
|
352
|
+
const args = [`--port=${this.proxyPort}`];
|
|
353
|
+
if (this.adb?.adbPort) {
|
|
354
|
+
args.push(`--adb-port=${this.adb.adbPort}`);
|
|
355
|
+
}
|
|
356
|
+
if (lodash_1.default.isArray(this.cmdArgs)) {
|
|
357
|
+
args.push(...this.cmdArgs);
|
|
358
|
+
}
|
|
359
|
+
if (this.logPath) {
|
|
360
|
+
args.push(`--log-path=${this.logPath}`);
|
|
361
|
+
}
|
|
362
|
+
if (this.disableBuildCheck) {
|
|
363
|
+
args.push('--disable-build-check');
|
|
364
|
+
}
|
|
365
|
+
args.push('--verbose');
|
|
366
|
+
return args;
|
|
367
|
+
}
|
|
130
368
|
async getDriversMapping() {
|
|
131
369
|
let mapping = lodash_1.default.cloneDeep(utils_1.CHROMEDRIVER_CHROME_MAPPING);
|
|
132
370
|
if (this.mappingPath) {
|
|
@@ -140,7 +378,7 @@ class Chromedriver extends events_1.default.EventEmitter {
|
|
|
140
378
|
mapping = JSON.parse(await support_1.fs.readFile(this.mappingPath, 'utf8'));
|
|
141
379
|
}
|
|
142
380
|
catch (e) {
|
|
143
|
-
const err =
|
|
381
|
+
const err = e;
|
|
144
382
|
this.log.warn(`Error parsing mapping from '${this.mappingPath}': ${err.message}`);
|
|
145
383
|
this.log.info('Defaulting to the static Chromedriver->Chrome mapping');
|
|
146
384
|
}
|
|
@@ -161,9 +399,6 @@ class Chromedriver extends events_1.default.EventEmitter {
|
|
|
161
399
|
}
|
|
162
400
|
return mapping;
|
|
163
401
|
}
|
|
164
|
-
/**
|
|
165
|
-
* @param {ChromedriverVersionMapping} mapping
|
|
166
|
-
*/
|
|
167
402
|
async getChromedrivers(mapping) {
|
|
168
403
|
// go through the versions available
|
|
169
404
|
const executables = await support_1.fs.glob('*', {
|
|
@@ -174,10 +409,7 @@ class Chromedriver extends events_1.default.EventEmitter {
|
|
|
174
409
|
this.log.debug(`Found ${support_1.util.pluralize('executable', executables.length, true)} ` +
|
|
175
410
|
`in '${this.executableDir}'`);
|
|
176
411
|
const cds = (await (0, asyncbox_1.asyncmap)(executables, async (executable) => {
|
|
177
|
-
|
|
178
|
-
* @param {{message: string, stdout?: string, stderr?: string}} opts
|
|
179
|
-
*/
|
|
180
|
-
const logError = ({ message, stdout, stderr }) => {
|
|
412
|
+
const logError = ({ message, stdout, stderr, }) => {
|
|
181
413
|
let errMsg = `Cannot retrieve version number from '${path_1.default.basename(executable)}' Chromedriver binary. ` +
|
|
182
414
|
`Make sure it returns a valid version string in response to '--version' command line argument. ${message}`;
|
|
183
415
|
if (stdout) {
|
|
@@ -197,7 +429,7 @@ class Chromedriver extends events_1.default.EventEmitter {
|
|
|
197
429
|
}));
|
|
198
430
|
}
|
|
199
431
|
catch (e) {
|
|
200
|
-
const err =
|
|
432
|
+
const err = e;
|
|
201
433
|
if (!(err.message || '').includes('timed out') &&
|
|
202
434
|
!(err.stdout || '').includes('Starting ChromeDriver')) {
|
|
203
435
|
return logError(err);
|
|
@@ -211,13 +443,13 @@ class Chromedriver extends events_1.default.EventEmitter {
|
|
|
211
443
|
return logError({ message: 'Cannot parse the version string', stdout, stderr });
|
|
212
444
|
}
|
|
213
445
|
let version = match[1];
|
|
214
|
-
let minChromeVersion = mapping[version];
|
|
446
|
+
let minChromeVersion = mapping[version] || null;
|
|
215
447
|
const coercedVersion = semver.coerce(version);
|
|
216
448
|
if (coercedVersion) {
|
|
217
449
|
// before 2019-03-06 versions were of the form major.minor
|
|
218
450
|
if (coercedVersion.major < NEW_CD_VERSION_FORMAT_MAJOR_VERSION) {
|
|
219
|
-
version =
|
|
220
|
-
minChromeVersion = mapping[version];
|
|
451
|
+
version = `${coercedVersion.major}.${coercedVersion.minor}`;
|
|
452
|
+
minChromeVersion = mapping[version] || null;
|
|
221
453
|
}
|
|
222
454
|
if (!minChromeVersion && coercedVersion.major >= NEW_CD_VERSION_FORMAT_MAJOR_VERSION) {
|
|
223
455
|
// Assume the major Chrome version is the same as the corresponding driver major version
|
|
@@ -301,11 +533,6 @@ class Chromedriver extends events_1.default.EventEmitter {
|
|
|
301
533
|
// make sure it is semver, so later checks won't fail
|
|
302
534
|
return chromeVersion ? semver.coerce(chromeVersion) : null;
|
|
303
535
|
}
|
|
304
|
-
/**
|
|
305
|
-
*
|
|
306
|
-
* @param {ChromedriverVersionMapping} newMapping
|
|
307
|
-
* @returns {Promise<void>}
|
|
308
|
-
*/
|
|
309
536
|
async updateDriversMapping(newMapping) {
|
|
310
537
|
let shouldUpdateStaticMapping = true;
|
|
311
538
|
if (!this.mappingPath) {
|
|
@@ -318,7 +545,7 @@ class Chromedriver extends events_1.default.EventEmitter {
|
|
|
318
545
|
shouldUpdateStaticMapping = false;
|
|
319
546
|
}
|
|
320
547
|
catch (e) {
|
|
321
|
-
const err =
|
|
548
|
+
const err = e;
|
|
322
549
|
this.log.warn(`Cannot store the updated chromedrivers mapping into '${this.mappingPath}'. ` +
|
|
323
550
|
`This may reduce the performance of further executions. Original error: ${err.message}`);
|
|
324
551
|
}
|
|
@@ -327,11 +554,6 @@ class Chromedriver extends events_1.default.EventEmitter {
|
|
|
327
554
|
Object.assign(utils_1.CHROMEDRIVER_CHROME_MAPPING, newMapping);
|
|
328
555
|
}
|
|
329
556
|
}
|
|
330
|
-
/**
|
|
331
|
-
* When executableDir is given explicitly for non-adb environment,
|
|
332
|
-
* this method will respect the executableDir rather than the system installed binary.
|
|
333
|
-
* @returns {Promise<string>}
|
|
334
|
-
*/
|
|
335
557
|
async getCompatibleChromedriver() {
|
|
336
558
|
if (!this.adb && !this.isCustomExecutableDir) {
|
|
337
559
|
return await (0, utils_1.getChromedriverBinaryPath)();
|
|
@@ -341,10 +563,6 @@ class Chromedriver extends events_1.default.EventEmitter {
|
|
|
341
563
|
this.log.debug(`The most recent known Chrome version: ${lodash_1.default.values(mapping)[0]}`);
|
|
342
564
|
}
|
|
343
565
|
let didStorageSync = false;
|
|
344
|
-
/**
|
|
345
|
-
*
|
|
346
|
-
* @param {import('semver').SemVer} chromeVersion
|
|
347
|
-
*/
|
|
348
566
|
const syncChromedrivers = async (chromeVersion) => {
|
|
349
567
|
didStorageSync = true;
|
|
350
568
|
if (!this.storageClient) {
|
|
@@ -363,14 +581,13 @@ class Chromedriver extends events_1.default.EventEmitter {
|
|
|
363
581
|
const { version, minBrowserVersion } = retrievedMapping[x];
|
|
364
582
|
acc[version] = minBrowserVersion;
|
|
365
583
|
return acc;
|
|
366
|
-
},
|
|
584
|
+
}, {});
|
|
367
585
|
Object.assign(mapping, synchronizedDriversMapping);
|
|
368
586
|
await this.updateDriversMapping(mapping);
|
|
369
587
|
return true;
|
|
370
588
|
};
|
|
371
|
-
|
|
589
|
+
while (true) {
|
|
372
590
|
const cds = await this.getChromedrivers(mapping);
|
|
373
|
-
/** @type {ChromedriverVersionMapping} */
|
|
374
591
|
const missingVersions = {};
|
|
375
592
|
for (const { version, minChromeVersion } of cds) {
|
|
376
593
|
if (!minChromeVersion || mapping[version]) {
|
|
@@ -427,7 +644,7 @@ class Chromedriver extends events_1.default.EventEmitter {
|
|
|
427
644
|
}
|
|
428
645
|
}
|
|
429
646
|
catch (e) {
|
|
430
|
-
const err =
|
|
647
|
+
const err = e;
|
|
431
648
|
this.log.warn(`Cannot synchronize local chromedrivers with the remote storage: ${err.message}`);
|
|
432
649
|
this.log.debug(err.stack);
|
|
433
650
|
}
|
|
@@ -443,12 +660,11 @@ class Chromedriver extends events_1.default.EventEmitter {
|
|
|
443
660
|
this.log.debug(`If a specific version is required, specify it with the 'chromedriverExecutable'` +
|
|
444
661
|
` capability.`);
|
|
445
662
|
return binPath;
|
|
446
|
-
|
|
447
|
-
} while (true);
|
|
663
|
+
}
|
|
448
664
|
}
|
|
449
665
|
async initChromedriverPath() {
|
|
450
666
|
if (this.executableVerified && this.chromedriver) {
|
|
451
|
-
return
|
|
667
|
+
return this.chromedriver;
|
|
452
668
|
}
|
|
453
669
|
let chromedriver = this.chromedriver;
|
|
454
670
|
// the executable might be set (if passed in)
|
|
@@ -461,18 +677,12 @@ class Chromedriver extends events_1.default.EventEmitter {
|
|
|
461
677
|
}
|
|
462
678
|
if (!(await support_1.fs.exists(chromedriver))) {
|
|
463
679
|
throw new Error(`Trying to use a chromedriver binary at the path ` +
|
|
464
|
-
`${
|
|
680
|
+
`${chromedriver}, but it doesn't exist!`);
|
|
465
681
|
}
|
|
466
682
|
this.executableVerified = true;
|
|
467
|
-
this.log.info(`Set chromedriver binary as: ${
|
|
468
|
-
return
|
|
683
|
+
this.log.info(`Set chromedriver binary as: ${chromedriver}`);
|
|
684
|
+
return chromedriver;
|
|
469
685
|
}
|
|
470
|
-
/**
|
|
471
|
-
* Determines the driver communication protocol
|
|
472
|
-
* based on various validation rules.
|
|
473
|
-
*
|
|
474
|
-
* @returns {keyof PROTOCOLS}
|
|
475
|
-
*/
|
|
476
686
|
syncProtocol() {
|
|
477
687
|
if (this.driverVersion) {
|
|
478
688
|
const coercedVersion = semver.coerce(this.driverVersion);
|
|
@@ -504,142 +714,6 @@ class Chromedriver extends events_1.default.EventEmitter {
|
|
|
504
714
|
this._desiredProtocol = base_driver_1.PROTOCOLS.W3C;
|
|
505
715
|
return this._desiredProtocol;
|
|
506
716
|
}
|
|
507
|
-
/**
|
|
508
|
-
*
|
|
509
|
-
* @param {SessionCapabilities} caps
|
|
510
|
-
* @param {boolean} emitStartingState
|
|
511
|
-
* @returns {Promise<SessionCapabilities>}
|
|
512
|
-
*/
|
|
513
|
-
async start(caps, emitStartingState = true) {
|
|
514
|
-
this.capabilities = lodash_1.default.cloneDeep(caps);
|
|
515
|
-
// set the logging preferences to ALL the console logs
|
|
516
|
-
this.capabilities.loggingPrefs = lodash_1.default.cloneDeep((0, protocol_helpers_1.getCapValue)(caps, 'loggingPrefs', {}));
|
|
517
|
-
if (lodash_1.default.isEmpty(this.capabilities.loggingPrefs.browser)) {
|
|
518
|
-
this.capabilities.loggingPrefs.browser = 'ALL';
|
|
519
|
-
}
|
|
520
|
-
if (emitStartingState) {
|
|
521
|
-
this.changeState(Chromedriver.STATE_STARTING);
|
|
522
|
-
}
|
|
523
|
-
const args = [`--port=${this.proxyPort}`];
|
|
524
|
-
if (this.adb?.adbPort) {
|
|
525
|
-
args.push(`--adb-port=${this.adb.adbPort}`);
|
|
526
|
-
}
|
|
527
|
-
if (lodash_1.default.isArray(this.cmdArgs)) {
|
|
528
|
-
args.push(...this.cmdArgs);
|
|
529
|
-
}
|
|
530
|
-
if (this.logPath) {
|
|
531
|
-
args.push(`--log-path=${this.logPath}`);
|
|
532
|
-
}
|
|
533
|
-
if (this.disableBuildCheck) {
|
|
534
|
-
args.push('--disable-build-check');
|
|
535
|
-
}
|
|
536
|
-
args.push('--verbose');
|
|
537
|
-
// what are the process stdout/stderr conditions wherein we know that
|
|
538
|
-
// the process has started to our satisfaction?
|
|
539
|
-
const startDetector = /** @param {string} stdout */ (stdout) => stdout.startsWith('Starting ');
|
|
540
|
-
let processIsAlive = false;
|
|
541
|
-
/** @type {string|undefined} */
|
|
542
|
-
let webviewVersion;
|
|
543
|
-
try {
|
|
544
|
-
const chromedriverPath = await this.initChromedriverPath();
|
|
545
|
-
await this.killAll();
|
|
546
|
-
// set up our subprocess object
|
|
547
|
-
this.proc = new teen_process_1.SubProcess(chromedriverPath, args);
|
|
548
|
-
processIsAlive = true;
|
|
549
|
-
// handle log output
|
|
550
|
-
for (const streamName of ['stderr', 'stdout']) {
|
|
551
|
-
this.proc.on(`line-${streamName}`, (line) => {
|
|
552
|
-
// if the cd output is not printed, find the chrome version and print
|
|
553
|
-
// will get a response like
|
|
554
|
-
// DevTools response: {
|
|
555
|
-
// "Android-Package": "io.appium.sampleapp",
|
|
556
|
-
// "Browser": "Chrome/55.0.2883.91",
|
|
557
|
-
// "Protocol-Version": "1.2",
|
|
558
|
-
// "User-Agent": "...",
|
|
559
|
-
// "WebKit-Version": "537.36"
|
|
560
|
-
// }
|
|
561
|
-
if (!webviewVersion) {
|
|
562
|
-
const match = /"Browser": "([^"]+)"/.exec(line);
|
|
563
|
-
if (match) {
|
|
564
|
-
webviewVersion = match[1];
|
|
565
|
-
this.log.debug(`Webview version: '${webviewVersion}'`);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
if (this.verbose) {
|
|
569
|
-
// give the output if it is requested
|
|
570
|
-
this.log.debug(`[${streamName.toUpperCase()}] ${line}`);
|
|
571
|
-
}
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
// handle out-of-bound exit by simply emitting a stopped state
|
|
575
|
-
this.proc.once('exit', (code, signal) => {
|
|
576
|
-
this._driverVersion = null;
|
|
577
|
-
this._desiredProtocol = null;
|
|
578
|
-
this._onlineStatus = null;
|
|
579
|
-
processIsAlive = false;
|
|
580
|
-
if (this.state !== Chromedriver.STATE_STOPPED &&
|
|
581
|
-
this.state !== Chromedriver.STATE_STOPPING &&
|
|
582
|
-
this.state !== Chromedriver.STATE_RESTARTING) {
|
|
583
|
-
const msg = `Chromedriver exited unexpectedly with code ${code}, signal ${signal}`;
|
|
584
|
-
this.log.error(msg);
|
|
585
|
-
this.changeState(Chromedriver.STATE_STOPPED);
|
|
586
|
-
}
|
|
587
|
-
this.proc?.removeAllListeners();
|
|
588
|
-
this.proc = null;
|
|
589
|
-
});
|
|
590
|
-
this.log.info(`Spawning Chromedriver with: ${this.chromedriver} ${args.join(' ')}`);
|
|
591
|
-
// start subproc and wait for startDetector
|
|
592
|
-
await this.proc.start(startDetector);
|
|
593
|
-
await this.waitForOnline();
|
|
594
|
-
this.syncProtocol();
|
|
595
|
-
return await this.startSession();
|
|
596
|
-
}
|
|
597
|
-
catch (e) {
|
|
598
|
-
const err = /** @type {Error} */ (e);
|
|
599
|
-
this.log.debug(err);
|
|
600
|
-
this.emit(Chromedriver.EVENT_ERROR, err);
|
|
601
|
-
// just because we had an error doesn't mean the chromedriver process
|
|
602
|
-
// finished; we should clean up if necessary
|
|
603
|
-
if (processIsAlive) {
|
|
604
|
-
await this.proc?.stop();
|
|
605
|
-
}
|
|
606
|
-
this.proc?.removeAllListeners();
|
|
607
|
-
this.proc = null;
|
|
608
|
-
let message = '';
|
|
609
|
-
// often the user's Chrome version is not supported by the version of Chromedriver
|
|
610
|
-
if (err.message.includes('Chrome version must be')) {
|
|
611
|
-
message +=
|
|
612
|
-
'Unable to automate Chrome version because it is not supported by this version of Chromedriver.\n';
|
|
613
|
-
if (webviewVersion) {
|
|
614
|
-
message += `Chrome version on the device: ${webviewVersion}\n`;
|
|
615
|
-
}
|
|
616
|
-
const versionsSupportedByDriver = /Chrome version must be (.+)/.exec(err.message)?.[1] || '';
|
|
617
|
-
if (versionsSupportedByDriver) {
|
|
618
|
-
message += `Chromedriver supports Chrome version(s): ${versionsSupportedByDriver}\n`;
|
|
619
|
-
}
|
|
620
|
-
message += 'Check the driver tutorial for troubleshooting.\n';
|
|
621
|
-
}
|
|
622
|
-
message += err.message;
|
|
623
|
-
throw this.log.errorWithException(message);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
sessionId() {
|
|
627
|
-
return this.state === Chromedriver.STATE_ONLINE ? this.jwproxy.sessionId : null;
|
|
628
|
-
}
|
|
629
|
-
/**
|
|
630
|
-
* Restarts the chromedriver session
|
|
631
|
-
*
|
|
632
|
-
* @returns {Promise<SessionCapabilities>}
|
|
633
|
-
*/
|
|
634
|
-
async restart() {
|
|
635
|
-
this.log.info('Restarting chromedriver');
|
|
636
|
-
if (this.state !== Chromedriver.STATE_ONLINE) {
|
|
637
|
-
throw new Error("Can't restart when we're not online");
|
|
638
|
-
}
|
|
639
|
-
this.changeState(Chromedriver.STATE_RESTARTING);
|
|
640
|
-
await this.stop(false);
|
|
641
|
-
return await this.start(this.capabilities, false);
|
|
642
|
-
}
|
|
643
717
|
async waitForOnline() {
|
|
644
718
|
// we need to make sure that CD hasn't crashed
|
|
645
719
|
let chromedriverStopped = false;
|
|
@@ -649,7 +723,6 @@ class Chromedriver extends events_1.default.EventEmitter {
|
|
|
649
723
|
chromedriverStopped = true;
|
|
650
724
|
return;
|
|
651
725
|
}
|
|
652
|
-
/** @type {any} */
|
|
653
726
|
const status = await this.getStatus();
|
|
654
727
|
if (!lodash_1.default.isPlainObject(status) || !status.ready) {
|
|
655
728
|
throw new Error(`The response to the /status API is not valid: ${JSON.stringify(status)}`);
|
|
@@ -671,80 +744,24 @@ class Chromedriver extends events_1.default.EventEmitter {
|
|
|
671
744
|
async getStatus() {
|
|
672
745
|
return await this.jwproxy.command('/status', 'GET');
|
|
673
746
|
}
|
|
674
|
-
/**
|
|
675
|
-
* Starts a new session
|
|
676
|
-
*
|
|
677
|
-
* @returns {Promise<SessionCapabilities>}
|
|
678
|
-
*/
|
|
679
747
|
async startSession() {
|
|
680
748
|
const sessionCaps = this._desiredProtocol === base_driver_1.PROTOCOLS.W3C
|
|
681
749
|
? { capabilities: { alwaysMatch: (0, protocol_helpers_1.toW3cCapNames)(this.capabilities) } }
|
|
682
750
|
: { desiredCapabilities: this.capabilities };
|
|
683
751
|
this.log.info(`Starting ${this._desiredProtocol} Chromedriver session with capabilities: ` +
|
|
684
752
|
JSON.stringify(sessionCaps, null, 2));
|
|
685
|
-
const response =
|
|
753
|
+
const response = (await this.jwproxy.command('/session', 'POST', sessionCaps));
|
|
686
754
|
this.log.prefix = (0, utils_1.generateLogPrefix)(this, this.jwproxy.sessionId);
|
|
687
755
|
this.changeState(Chromedriver.STATE_ONLINE);
|
|
688
|
-
return lodash_1.default.has(response, 'capabilities') ? response.capabilities : response;
|
|
756
|
+
return lodash_1.default.has(response, 'capabilities') && response.capabilities ? response.capabilities : response;
|
|
689
757
|
}
|
|
690
|
-
async stop(emitStates = true) {
|
|
691
|
-
if (emitStates) {
|
|
692
|
-
this.changeState(Chromedriver.STATE_STOPPING);
|
|
693
|
-
}
|
|
694
|
-
/**
|
|
695
|
-
*
|
|
696
|
-
* @param {() => Promise<any>|any} f
|
|
697
|
-
*/
|
|
698
|
-
const runSafeStep = async (f) => {
|
|
699
|
-
try {
|
|
700
|
-
return await f();
|
|
701
|
-
}
|
|
702
|
-
catch (e) {
|
|
703
|
-
const err = /** @type {Error} */ (e);
|
|
704
|
-
this.log.warn(err.message);
|
|
705
|
-
this.log.debug(err.stack);
|
|
706
|
-
}
|
|
707
|
-
};
|
|
708
|
-
await runSafeStep(() => this.jwproxy.command('', 'DELETE'));
|
|
709
|
-
await runSafeStep(() => {
|
|
710
|
-
this.proc?.stop('SIGTERM', 20000);
|
|
711
|
-
this.proc?.removeAllListeners();
|
|
712
|
-
this.proc = null;
|
|
713
|
-
});
|
|
714
|
-
this.log.prefix = (0, utils_1.generateLogPrefix)(this);
|
|
715
|
-
if (emitStates) {
|
|
716
|
-
this.changeState(Chromedriver.STATE_STOPPED);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
/**
|
|
720
|
-
*
|
|
721
|
-
* @param {string} state
|
|
722
|
-
*/
|
|
723
758
|
changeState(state) {
|
|
724
759
|
this.state = state;
|
|
725
760
|
this.log.debug(`Changed state to '${state}'`);
|
|
726
761
|
this.emit(Chromedriver.EVENT_CHANGED, { state });
|
|
727
762
|
}
|
|
728
|
-
/**
|
|
729
|
-
*
|
|
730
|
-
* @param {string} url
|
|
731
|
-
* @param {'POST'|'GET'|'DELETE'} method
|
|
732
|
-
* @param {any} body
|
|
733
|
-
* @returns
|
|
734
|
-
*/
|
|
735
|
-
async sendCommand(url, method, body) {
|
|
736
|
-
return await this.jwproxy.command(url, method, body);
|
|
737
|
-
}
|
|
738
|
-
/**
|
|
739
|
-
*
|
|
740
|
-
* @param {any} req
|
|
741
|
-
* @param {any} res
|
|
742
|
-
*/
|
|
743
|
-
async proxyReq(req, res) {
|
|
744
|
-
return await this.jwproxy.proxyReqRes(req, res);
|
|
745
|
-
}
|
|
746
763
|
async killAll() {
|
|
747
|
-
|
|
764
|
+
const cmd = support_1.system.isWindows()
|
|
748
765
|
? `wmic process where "commandline like '%chromedriver.exe%--port=${this.proxyPort}%'" delete`
|
|
749
766
|
: `pkill -15 -f "${this.chromedriver}.*--port=${this.proxyPort}"`;
|
|
750
767
|
this.log.debug(`Killing any old chromedrivers, running: ${cmd}`);
|
|
@@ -765,51 +782,23 @@ class Chromedriver extends events_1.default.EventEmitter {
|
|
|
765
782
|
this.log.debug(`Cleaning any old adb forwarded port socket connections`);
|
|
766
783
|
}
|
|
767
784
|
try {
|
|
768
|
-
for (
|
|
785
|
+
for (const conn of await this.adb.getForwardList()) {
|
|
769
786
|
// chromedriver will ask ADB to forward a port like "deviceId tcp:port localabstract:webview_devtools_remote_port"
|
|
770
787
|
if (!(conn.includes('webview_devtools') && (!udid || conn.includes(udid)))) {
|
|
771
788
|
continue;
|
|
772
789
|
}
|
|
773
|
-
|
|
790
|
+
const params = conn.split(/\s+/);
|
|
774
791
|
if (params.length > 1) {
|
|
775
792
|
await this.adb.removePortForward(params[1].replace(/[\D]*/, ''));
|
|
776
793
|
}
|
|
777
794
|
}
|
|
778
795
|
}
|
|
779
796
|
catch (e) {
|
|
780
|
-
const err =
|
|
797
|
+
const err = e;
|
|
781
798
|
this.log.warn(`Unable to clean forwarded ports. Error: '${err.message}'. Continuing.`);
|
|
782
799
|
}
|
|
783
800
|
}
|
|
784
801
|
}
|
|
785
|
-
/**
|
|
786
|
-
* @returns {Promise<boolean>}
|
|
787
|
-
*/
|
|
788
|
-
async hasWorkingWebview() {
|
|
789
|
-
// sometimes chromedriver stops automating webviews. this method runs a
|
|
790
|
-
// simple command to determine our state, and responds accordingly
|
|
791
|
-
try {
|
|
792
|
-
await this.jwproxy.command('/url', 'GET');
|
|
793
|
-
return true;
|
|
794
|
-
}
|
|
795
|
-
catch {
|
|
796
|
-
return false;
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
802
|
}
|
|
800
803
|
exports.Chromedriver = Chromedriver;
|
|
801
|
-
Chromedriver.EVENT_ERROR = 'chromedriver_error';
|
|
802
|
-
Chromedriver.EVENT_CHANGED = 'stateChanged';
|
|
803
|
-
Chromedriver.STATE_STOPPED = 'stopped';
|
|
804
|
-
Chromedriver.STATE_STARTING = 'starting';
|
|
805
|
-
Chromedriver.STATE_ONLINE = 'online';
|
|
806
|
-
Chromedriver.STATE_STOPPING = 'stopping';
|
|
807
|
-
Chromedriver.STATE_RESTARTING = 'restarting';
|
|
808
|
-
/**
|
|
809
|
-
* @typedef {import('./types').ChromedriverVersionMapping} ChromedriverVersionMapping
|
|
810
|
-
*/
|
|
811
|
-
/**
|
|
812
|
-
* @typedef {{capabilities: Record<string, any>}} NewSessionResponse
|
|
813
|
-
* @typedef {Record<string, any>} SessionCapabilities
|
|
814
|
-
*/
|
|
815
804
|
//# sourceMappingURL=chromedriver.js.map
|