@appium/base-driver 9.16.2 → 10.0.0-beta.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.
@@ -43,6 +43,11 @@ const {MJSONWP, W3C} = PROTOCOLS;
43
43
  const DEFAULT_LOG = logger.getLogger('Protocol Converter');
44
44
 
45
45
  class ProtocolConverter {
46
+ /**
47
+ *
48
+ * @param {ProxyFunction} proxyFunc
49
+ * @param {import('@appium/types').AppiumLogger | null} [log=null]
50
+ */
46
51
  constructor(proxyFunc, log = null) {
47
52
  this.proxyFunc = proxyFunc;
48
53
  this._downstreamProtocol = null;
@@ -67,7 +72,7 @@ class ProtocolConverter {
67
72
  * provided in the request, we need to do 3 proxies and combine the result
68
73
  *
69
74
  * @param {Object} body Request body
70
- * @return {Array} Array of W3C + MJSONWP compatible timeout objects
75
+ * @return {Object[]} Array of W3C + MJSONWP compatible timeout objects
71
76
  */
72
77
  getTimeoutRequestObjects(body) {
73
78
  if (this.downstreamProtocol === W3C && _.has(body, 'ms') && _.has(body, 'type')) {
@@ -99,9 +104,10 @@ class ProtocolConverter {
99
104
 
100
105
  /**
101
106
  * Proxy an array of timeout objects and merge the result
102
- * @param {String} url Endpoint url
103
- * @param {String} method Endpoint method
104
- * @param {Object} body Request body
107
+ * @param {string} url Endpoint url
108
+ * @param {string} method Endpoint method
109
+ * @param {import('@appium/types').HTTPBody} body Request body
110
+ * @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
105
111
  */
106
112
  async proxySetTimeouts(url, method, body) {
107
113
  let response, resBody;
@@ -127,9 +133,16 @@ class ProtocolConverter {
127
133
 
128
134
  // ...Otherwise, continue to the next timeouts call
129
135
  }
130
- return [response, resBody];
136
+ return [/** @type {import('@appium/types').ProxyResponse} */(response), resBody];
131
137
  }
132
138
 
139
+ /**
140
+ *
141
+ * @param {string} url
142
+ * @param {string} method
143
+ * @param {import('@appium/types').HTTPBody} body
144
+ * @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
145
+ */
133
146
  async proxySetWindow(url, method, body) {
134
147
  const bodyObj = util.safeJsonParse(body);
135
148
  if (_.isPlainObject(bodyObj)) {
@@ -160,6 +173,13 @@ class ProtocolConverter {
160
173
  return await this.proxyFunc(url, method, body);
161
174
  }
162
175
 
176
+ /**
177
+ *
178
+ * @param {string} url
179
+ * @param {string} method
180
+ * @param {import('@appium/types').HTTPBody} body
181
+ * @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
182
+ */
163
183
  async proxySetValue(url, method, body) {
164
184
  const bodyObj = util.safeJsonParse(body);
165
185
  if (_.isPlainObject(bodyObj) && (util.hasValue(bodyObj.text) || util.hasValue(bodyObj.value))) {
@@ -186,6 +206,13 @@ class ProtocolConverter {
186
206
  return await this.proxyFunc(url, method, body);
187
207
  }
188
208
 
209
+ /**
210
+ *
211
+ * @param {string} url
212
+ * @param {string} method
213
+ * @param {import('@appium/types').HTTPBody} body
214
+ * @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
215
+ */
189
216
  async proxySetFrame(url, method, body) {
190
217
  const bodyObj = util.safeJsonParse(body);
191
218
  return _.has(bodyObj, 'id') && _.isPlainObject(bodyObj.id)
@@ -196,6 +223,13 @@ class ProtocolConverter {
196
223
  : await this.proxyFunc(url, method, body);
197
224
  }
198
225
 
226
+ /**
227
+ *
228
+ * @param {string} url
229
+ * @param {string} method
230
+ * @param {import('@appium/types').HTTPBody} body
231
+ * @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
232
+ */
199
233
  async proxyPerformActions(url, method, body) {
200
234
  const bodyObj = util.safeJsonParse(body);
201
235
  return _.isPlainObject(bodyObj)
@@ -207,6 +241,12 @@ class ProtocolConverter {
207
241
  : await this.proxyFunc(url, method, body);
208
242
  }
209
243
 
244
+ /**
245
+ *
246
+ * @param {string} url
247
+ * @param {string} method
248
+ * @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
249
+ */
210
250
  async proxyReleaseActions(url, method) {
211
251
  return await this.proxyFunc(url, method);
212
252
  }
@@ -218,8 +258,8 @@ class ProtocolConverter {
218
258
  * @param {string} commandName
219
259
  * @param {string} url
220
260
  * @param {string} method
221
- * @param {?string|object} body
222
- * @returns The proxyfying result as [response, responseBody] tuple
261
+ * @param {import('@appium/types').HTTPBody} [body]
262
+ * @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
223
263
  */
224
264
  async convertAndProxy(commandName, url, method, body) {
225
265
  if (!this.downstreamProtocol) {
@@ -272,3 +312,7 @@ class ProtocolConverter {
272
312
  }
273
313
 
274
314
  export default ProtocolConverter;
315
+
316
+ /**
317
+ * @typedef {(url: string, method: string, body?: import('@appium/types').HTTPBody) => Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>} ProxyFunction
318
+ */
@@ -0,0 +1,52 @@
1
+ import axios from 'axios';
2
+ import { CancellationError } from 'bluebird';
3
+ import EventEmitter from 'node:events';
4
+
5
+ const CANCEL_EVENT = 'cancel';
6
+ const FINISH_EVENT = 'finish';
7
+
8
+ export class ProxyRequest {
9
+ private readonly _requestConfig: axios.RawAxiosRequestConfig;
10
+ private readonly _ee: EventEmitter;
11
+ private _resultPromise: Promise<any> | null;
12
+
13
+ constructor(requestConfig: axios.RawAxiosRequestConfig<any>) {
14
+ this._requestConfig = requestConfig;
15
+ this._ee = new EventEmitter();
16
+ this._resultPromise = null;
17
+ }
18
+
19
+ async execute(): Promise<axios.AxiosResponse> {
20
+ if (this._resultPromise) {
21
+ return await this._resultPromise;
22
+ }
23
+
24
+ try {
25
+ this._resultPromise = Promise.race([
26
+ this._makeRacingTimer(),
27
+ this._makeRequest(),
28
+ ]);
29
+ return await this._resultPromise;
30
+ } finally {
31
+ this._ee.emit(FINISH_EVENT);
32
+ this._ee.removeAllListeners();
33
+ }
34
+ }
35
+
36
+ cancel(): void {
37
+ this._ee.emit(CANCEL_EVENT);
38
+ }
39
+
40
+ private async _makeRequest(): Promise<axios.AxiosResponse> {
41
+ return await axios(this._requestConfig);
42
+ }
43
+
44
+ private async _makeRacingTimer(): Promise<void> {
45
+ return await new Promise((resolve, reject) => {
46
+ this._ee.once(FINISH_EVENT, resolve);
47
+ this._ee.once(CANCEL_EVENT, () => reject(new CancellationError(
48
+ 'The request has been cancelled'
49
+ )));
50
+ });
51
+ }
52
+ }
@@ -1,6 +1,5 @@
1
1
  import _ from 'lodash';
2
2
  import {logger, util} from '@appium/support';
3
- import axios from 'axios';
4
3
  import {getSummaryByCode} from '../jsonwp-status/status';
5
4
  import {
6
5
  errors,
@@ -17,7 +16,7 @@ import http from 'http';
17
16
  import https from 'https';
18
17
  import { match as pathToRegexMatch } from 'path-to-regexp';
19
18
  import nodeUrl from 'node:url';
20
-
19
+ import { ProxyRequest } from './proxy-request';
21
20
 
22
21
  const DEFAULT_LOG = logger.getLogger('WD Proxy');
23
22
  const DEFAULT_REQUEST_TIMEOUT = 240000;
@@ -37,7 +36,7 @@ const ALLOWED_OPTS = [
37
36
  'keepAlive',
38
37
  ];
39
38
 
40
- class JWProxy {
39
+ export class JWProxy {
41
40
  /** @type {string} */
42
41
  scheme;
43
42
  /** @type {string} */
@@ -52,13 +51,20 @@ class JWProxy {
52
51
  sessionId;
53
52
  /** @type {number} */
54
53
  timeout;
54
+ /** @type {Protocol | null | undefined} */
55
+ _downstreamProtocol;
56
+ /** @type {ProxyRequest[]} */
57
+ _activeRequests;
55
58
 
59
+ /**
60
+ * @param {import('@appium/types').ProxyOptions} [opts={}]
61
+ */
56
62
  constructor(opts = {}) {
57
- opts = _.pick(opts, ALLOWED_OPTS);
58
-
63
+ const filteredOpts = _.pick(opts, ALLOWED_OPTS);
59
64
  // omit 'log' in the defaults assignment here because 'log' is a getter and we are going to set
60
65
  // it to this._log (which lies behind the getter) further down
61
- const options = _.defaults(_.omit(opts, 'log'), {
66
+ /** @type {import('@appium/types').ProxyOptions} */
67
+ const options = _.defaults(_.omit(filteredOpts, 'log'), {
62
68
  scheme: 'http',
63
69
  server: 'localhost',
64
70
  port: 4444,
@@ -67,7 +73,7 @@ class JWProxy {
67
73
  sessionId: null,
68
74
  timeout: DEFAULT_REQUEST_TIMEOUT,
69
75
  });
70
- options.scheme = options.scheme.toLowerCase();
76
+ options.scheme = /** @type {string} */ (options.scheme).toLowerCase();
71
77
  Object.assign(this, options);
72
78
 
73
79
  this._activeRequests = [];
@@ -81,6 +87,8 @@ class JWProxy {
81
87
  this.httpsAgent = new https.Agent(agentOpts);
82
88
  this.protocolConverter = new ProtocolConverter(this.proxy.bind(this), opts.log);
83
89
  this._log = opts.log;
90
+
91
+ this.log.debug(`${this.constructor.name} options: ${JSON.stringify(options)}`);
84
92
  }
85
93
 
86
94
  get log() {
@@ -97,23 +105,32 @@ class JWProxy {
97
105
  * @returns {Promise<import('axios').AxiosResponse>}
98
106
  */
99
107
  async request(requestConfig) {
100
- const reqPromise = axios(requestConfig);
101
- this._activeRequests.push(reqPromise);
108
+ const req = new ProxyRequest(requestConfig);
109
+ this._activeRequests.push(req);
102
110
  try {
103
- return await reqPromise;
111
+ return await req.execute();
104
112
  } finally {
105
- _.pull(this._activeRequests, reqPromise);
113
+ _.pull(this._activeRequests, req);
106
114
  }
107
115
  }
108
116
 
117
+ /**
118
+ * @returns {number}
119
+ */
109
120
  getActiveRequestsCount() {
110
121
  return this._activeRequests.length;
111
122
  }
112
123
 
113
124
  cancelActiveRequests() {
125
+ for (const ar of this._activeRequests) {
126
+ ar.cancel();
127
+ }
114
128
  this._activeRequests = [];
115
129
  }
116
130
 
131
+ /**
132
+ * @param {Protocol | null | undefined} value
133
+ */
117
134
  set downstreamProtocol(value) {
118
135
  this._downstreamProtocol = value;
119
136
  this.protocolConverter.downstreamProtocol = value;
@@ -130,32 +147,8 @@ class JWProxy {
130
147
  * @returns {string}
131
148
  */
132
149
  getUrlForProxy(url, method) {
133
- const parsedUrl = nodeUrl.parse(url || '/');
134
- if (
135
- !parsedUrl.href || !parsedUrl.pathname
136
- || (parsedUrl.protocol && !['http:', 'https:'].includes(parsedUrl.protocol))
137
- ) {
138
- throw new Error(`Did not know how to proxy the url '${url}'`);
139
- }
140
- let pathname = this.reqBasePath && parsedUrl.pathname.startsWith(this.reqBasePath)
141
- ? parsedUrl.pathname.replace(this.reqBasePath, '')
142
- : parsedUrl.pathname;
143
- const match = COMMAND_WITH_SESSION_ID_MATCHER(pathname);
144
- // This is needed for the backward compatibility
145
- // if drivers don't set reqBasePath properly
146
- if (!this.reqBasePath) {
147
- if (match && _.isArray(match.params?.prefix)) {
148
- pathname = pathname.replace(`/${match.params?.prefix.join('/')}`, '');
149
- } else if (_.startsWith(pathname, '/wd/hub')) {
150
- pathname = pathname.replace('/wd/hub', '');
151
- }
152
- }
153
- const normalizedPathname = _.trimEnd(
154
- match && _.isArray(match.params?.command)
155
- ? `/${match.params.command.join('/')}`
156
- : pathname,
157
- '/'
158
- );
150
+ const parsedUrl = this._parseUrl(url);
151
+ const normalizedPathname = this._toNormalizedPathname(parsedUrl);
159
152
  const commandName = normalizedPathname
160
153
  ? routeToCommandName(
161
154
  normalizedPathname,
@@ -181,8 +174,8 @@ class JWProxy {
181
174
  *
182
175
  * @param {string} url
183
176
  * @param {string} method
184
- * @param {any} body
185
- * @returns {Promise<any>}
177
+ * @param {import('@appium/types').HTTPBody} [body=null]
178
+ * @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
186
179
  */
187
180
  async proxy(url, method, body = null) {
188
181
  method = method.toUpperCase();
@@ -255,8 +248,12 @@ class JWProxy {
255
248
  // Some servers, like chromedriver may return response code 200 for non-zero JSONWP statuses
256
249
  throwProxyError(data);
257
250
  }
258
- const res = {statusCode: status, headers, body: data};
259
- return [res, data];
251
+ const headersMap = /** @type {import('@appium/types').HTTPHeaders} */ (headers);
252
+ return [{
253
+ statusCode: status,
254
+ headers: headersMap,
255
+ body: data,
256
+ }, data];
260
257
  } catch (e) {
261
258
  // We only consider an error unexpected if this was not
262
259
  // an async request module error or if the response cannot be cast to
@@ -279,6 +276,11 @@ class JWProxy {
279
276
  }
280
277
  }
281
278
 
279
+ /**
280
+ *
281
+ * @param {Record<string, any>} resObj
282
+ * @returns {Protocol | undefined}
283
+ */
282
284
  getProtocolFromResBody(resObj) {
283
285
  if (_.isInteger(resObj.status)) {
284
286
  return MJSONWP;
@@ -289,6 +291,7 @@ class JWProxy {
289
291
  }
290
292
 
291
293
  /**
294
+ * @deprecated This method is not used anymore and will be removed
292
295
  *
293
296
  * @param {string} url
294
297
  * @param {import('@appium/types').HTTPMethod} method
@@ -322,10 +325,13 @@ class JWProxy {
322
325
  *
323
326
  * @param {string} url
324
327
  * @param {import('@appium/types').HTTPMethod} method
325
- * @param {any?} body
328
+ * @param {import('@appium/types').HTTPBody} [body=null]
329
+ * @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
326
330
  */
327
331
  async proxyCommand(url, method, body = null) {
328
- const commandName = this.requestToCommandName(url, method);
332
+ const parsedUrl = this._parseUrl(url);
333
+ const normalizedPathname = this._toNormalizedPathname(parsedUrl);
334
+ const commandName = normalizedPathname ? routeToCommandName(normalizedPathname, method) : '';
329
335
  if (!commandName) {
330
336
  return await this.proxy(url, method, body);
331
337
  }
@@ -338,8 +344,8 @@ class JWProxy {
338
344
  *
339
345
  * @param {string} url
340
346
  * @param {import('@appium/types').HTTPMethod} method
341
- * @param {any?} body
342
- * @returns {Promise<unknown>}
347
+ * @param {import('@appium/types').HTTPBody} [body=null]
348
+ * @returns {Promise<import('@appium/types').HTTPBody>}
343
349
  */
344
350
  async command(url, method, body = null) {
345
351
  let response;
@@ -393,20 +399,40 @@ class JWProxy {
393
399
  );
394
400
  }
395
401
 
402
+ /**
403
+ *
404
+ * @param {string} url
405
+ * @returns {string | null}
406
+ */
396
407
  getSessionIdFromUrl(url) {
397
408
  const match = url.match(/\/session\/([^/]+)/);
398
409
  return match ? match[1] : null;
399
410
  }
400
411
 
412
+ /**
413
+ *
414
+ * @param {import('express').Request} req
415
+ * @param {import('express').Response} res
416
+ */
401
417
  async proxyReqRes(req, res) {
402
418
  // ! this method must not throw any exceptions
403
419
  // ! make sure to call res.send before return
420
+ /** @type {number} */
404
421
  let statusCode;
422
+ /** @type {import('@appium/types').HTTPBody} */
405
423
  let resBodyObj;
406
424
  try {
407
425
  let response;
408
- [response, resBodyObj] = await this.proxyCommand(req.originalUrl, req.method, req.body);
409
- res.headers = response.headers;
426
+ [response, resBodyObj] = await this.proxyCommand(
427
+ req.originalUrl,
428
+ /** @type {import('@appium/types').HTTPMethod} */ (req.method),
429
+ req.body
430
+ );
431
+ for (const [name, value] of _.toPairs(response.headers)) {
432
+ if (!_.isNil(value)) {
433
+ res.setHeader(name, _.isBoolean(value) ? String(value) : value);
434
+ }
435
+ }
410
436
  statusCode = response.statusCode;
411
437
  } catch (err) {
412
438
  [statusCode, resBodyObj] = getResponseForW3CError(
@@ -438,11 +464,58 @@ class JWProxy {
438
464
  resBodyObj.value = formatResponseValue(resBodyObj.value);
439
465
  res.status(statusCode).send(JSON.stringify(formatStatus(resBodyObj)));
440
466
  }
467
+
468
+ /**
469
+ *
470
+ * @param {string} url
471
+ * @returns {ParsedUrl}
472
+ */
473
+ _parseUrl(url) {
474
+ const parsedUrl = nodeUrl.parse(url || '/');
475
+ if (
476
+ _.isNil(parsedUrl.href) || _.isNil(parsedUrl.pathname)
477
+ || (parsedUrl.protocol && !['http:', 'https:'].includes(parsedUrl.protocol))
478
+ ) {
479
+ throw new Error(`Did not know how to proxy the url '${url}'`);
480
+ }
481
+ return parsedUrl;
482
+ }
483
+
484
+ /**
485
+ *
486
+ * @param {ParsedUrl} parsedUrl
487
+ * @returns {string}
488
+ */
489
+ _toNormalizedPathname(parsedUrl) {
490
+ if (!_.isString(parsedUrl.pathname)) {
491
+ return '';
492
+ }
493
+ let pathname = this.reqBasePath && parsedUrl.pathname.startsWith(this.reqBasePath)
494
+ ? parsedUrl.pathname.replace(this.reqBasePath, '')
495
+ : parsedUrl.pathname;
496
+ const match = COMMAND_WITH_SESSION_ID_MATCHER(pathname);
497
+ // This is needed for the backward compatibility
498
+ // if drivers don't set reqBasePath properly
499
+ if (!this.reqBasePath) {
500
+ if (match && _.isArray(match.params?.prefix)) {
501
+ pathname = pathname.replace(`/${match.params?.prefix.join('/')}`, '');
502
+ } else if (_.startsWith(pathname, '/wd/hub')) {
503
+ pathname = pathname.replace('/wd/hub', '');
504
+ }
505
+ }
506
+ return _.trimEnd(
507
+ match && _.isArray(match.params?.command)
508
+ ? `/${match.params.command.join('/')}`
509
+ : pathname,
510
+ '/'
511
+ );
512
+ }
441
513
  }
442
514
 
443
- export {JWProxy};
444
515
  export default JWProxy;
445
516
 
446
517
  /**
447
518
  * @typedef {Error & {response: {data: import('type-fest').JsonObject, status: import('http-status-codes').StatusCodes}}} ProxyError
519
+ * @typedef {nodeUrl.UrlWithStringQuery} ParsedUrl
520
+ * @typedef {typeof PROTOCOLS[keyof typeof PROTOCOLS]} Protocol
448
521
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appium/base-driver",
3
- "version": "9.16.2",
3
+ "version": "10.0.0-beta.0",
4
4
  "description": "Base driver class for Appium drivers",
5
5
  "keywords": [
6
6
  "automation",
@@ -49,7 +49,7 @@
49
49
  "@colors/colors": "1.6.0",
50
50
  "async-lock": "1.4.1",
51
51
  "asyncbox": "3.0.0",
52
- "axios": "1.7.9",
52
+ "axios": "1.8.1",
53
53
  "bluebird": "3.7.2",
54
54
  "body-parser": "1.20.3",
55
55
  "express": "4.21.2",
@@ -62,7 +62,7 @@
62
62
  "path-to-regexp": "8.2.0",
63
63
  "serve-favicon": "2.5.0",
64
64
  "source-map-support": "0.5.21",
65
- "type-fest": "4.35.0",
65
+ "type-fest": "4.36.0",
66
66
  "validate.js": "0.13.1"
67
67
  },
68
68
  "optionalDependencies": {
@@ -73,9 +73,10 @@
73
73
  "npm": ">=8"
74
74
  },
75
75
  "publishConfig": {
76
- "access": "public"
76
+ "access": "public",
77
+ "tag": "beta"
77
78
  },
78
- "gitHead": "e473b05130bb90518a9bdb843c3f93ef74d5b874",
79
+ "gitHead": "16d16dd0f3bf9829cff2ada7525156c462970932",
79
80
  "tsd": {
80
81
  "directory": "test/types"
81
82
  }