@appium/base-driver 9.16.2 → 9.16.3
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/README.md +0 -8
- package/build/lib/basedriver/helpers.d.ts +4 -3
- package/build/lib/basedriver/helpers.d.ts.map +1 -1
- package/build/lib/basedriver/helpers.js +17 -13
- package/build/lib/basedriver/helpers.js.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.d.ts +58 -17
- package/build/lib/jsonwp-proxy/protocol-converter.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.js +50 -7
- package/build/lib/jsonwp-proxy/protocol-converter.js.map +1 -1
- package/build/lib/jsonwp-proxy/proxy-request.d.ts +12 -0
- package/build/lib/jsonwp-proxy/proxy-request.d.ts.map +1 -0
- package/build/lib/jsonwp-proxy/proxy-request.js +48 -0
- package/build/lib/jsonwp-proxy/proxy-request.js.map +1 -0
- package/build/lib/jsonwp-proxy/proxy.d.ts +70 -25
- package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.js +108 -40
- package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
- package/lib/basedriver/helpers.js +17 -13
- package/lib/jsonwp-proxy/protocol-converter.js +51 -7
- package/lib/jsonwp-proxy/proxy-request.ts +52 -0
- package/lib/jsonwp-proxy/proxy.js +120 -48
- package/package.json +6 -6
- package/lib/basedriver/README.md +0 -36
- package/lib/express/README.md +0 -59
- package/lib/jsonwp-proxy/README.md +0 -52
- package/lib/jsonwp-status/README.md +0 -20
- package/lib/protocol/README.md +0 -100
|
@@ -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 {
|
|
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 {
|
|
103
|
-
* @param {
|
|
104
|
-
* @param {
|
|
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 {
|
|
222
|
-
* @returns
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
101
|
-
this._activeRequests.push(
|
|
108
|
+
const req = new ProxyRequest(requestConfig);
|
|
109
|
+
this._activeRequests.push(req);
|
|
102
110
|
try {
|
|
103
|
-
return await
|
|
111
|
+
return await req.execute();
|
|
104
112
|
} finally {
|
|
105
|
-
_.pull(this._activeRequests,
|
|
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 =
|
|
134
|
-
|
|
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 {
|
|
185
|
-
* @returns {Promise<
|
|
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
|
|
259
|
-
return [
|
|
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 {
|
|
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
|
|
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 {
|
|
342
|
-
* @returns {Promise<
|
|
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(
|
|
409
|
-
|
|
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,57 @@ 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
|
+
let result = pathname;
|
|
507
|
+
if (match) {
|
|
508
|
+
result = _.isArray(match.params?.command) ? `/${match.params.command.join('/')}` : '';
|
|
509
|
+
}
|
|
510
|
+
return _.trimEnd(result, '/');
|
|
511
|
+
}
|
|
441
512
|
}
|
|
442
513
|
|
|
443
|
-
export {JWProxy};
|
|
444
514
|
export default JWProxy;
|
|
445
515
|
|
|
446
516
|
/**
|
|
447
517
|
* @typedef {Error & {response: {data: import('type-fest').JsonObject, status: import('http-status-codes').StatusCodes}}} ProxyError
|
|
518
|
+
* @typedef {nodeUrl.UrlWithStringQuery} ParsedUrl
|
|
519
|
+
* @typedef {typeof PROTOCOLS[keyof typeof PROTOCOLS]} Protocol
|
|
448
520
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appium/base-driver",
|
|
3
|
-
"version": "9.16.
|
|
3
|
+
"version": "9.16.3",
|
|
4
4
|
"description": "Base driver class for Appium drivers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"automation",
|
|
@@ -44,12 +44,12 @@
|
|
|
44
44
|
"test:types": "tsd"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@appium/support": "^6.0.
|
|
48
|
-
"@appium/types": "^0.25.
|
|
47
|
+
"@appium/support": "^6.0.7",
|
|
48
|
+
"@appium/types": "^0.25.2",
|
|
49
49
|
"@colors/colors": "1.6.0",
|
|
50
50
|
"async-lock": "1.4.1",
|
|
51
51
|
"asyncbox": "3.0.0",
|
|
52
|
-
"axios": "1.
|
|
52
|
+
"axios": "1.8.2",
|
|
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.
|
|
65
|
+
"type-fest": "4.37.0",
|
|
66
66
|
"validate.js": "0.13.1"
|
|
67
67
|
},
|
|
68
68
|
"optionalDependencies": {
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"publishConfig": {
|
|
76
76
|
"access": "public"
|
|
77
77
|
},
|
|
78
|
-
"gitHead": "
|
|
78
|
+
"gitHead": "7c05f5c213e284b6be0f1ecf5550a40d84e0899e",
|
|
79
79
|
"tsd": {
|
|
80
80
|
"directory": "test/types"
|
|
81
81
|
}
|
package/lib/basedriver/README.md
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
appium-base-driver
|
|
2
|
-
===================
|
|
3
|
-
This is the parent class that all [appium](appium.io) drivers inherit from. Appium drivers themselves can either be started from the command line as standalone appium servers, or can be included by another module (appium) which then proxies commands to the appropriate driver based on [Desired Capabilities](https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md).
|
|
4
|
-
|
|
5
|
-
An appium driver is a module which processes [Mobile Json Wire Protocol](https://code.google.com/p/selenium/source/browse/spec-draft.md?repo=mobile) commands and controls a device accordingly. The commands can either come in over HTTP as json api requests, or they can be passed to the driver object programmatically as already-parsed json object (without the HTTP headers and junk).
|
|
6
|
-
|
|
7
|
-
The appium Base driver already includes the [mjsonwp](https://github.com/appium/appium-base-driver/blob/master/lib/mjsonwp/README.md) module, which is the HTTP server that converts incoming requests into json objects that get sent to the driver programmatically.
|
|
8
|
-
|
|
9
|
-
The appium Base driver already has all the REST api routes, validation, and error codes supplied by [mjsonwp](https://github.com/appium/appium-base-driver/blob/master/lib/mjsonwp/README.md).
|
|
10
|
-
|
|
11
|
-
Appium drivers are designed to have a *single testing session* per instantiation. This means that one Driver object should be attached to a single device and handle commands from a single client. The main appium driver handles multiple sessions and instantiates a new instance of the desired driver for each new session.
|
|
12
|
-
|
|
13
|
-
## Writing your own appium driver
|
|
14
|
-
|
|
15
|
-
Writing your own appium driver starts with inheriting and extending this Base driver module.
|
|
16
|
-
|
|
17
|
-
Appium Base driver has some properties that all drivers share:
|
|
18
|
-
|
|
19
|
-
- `driver.opts` - these are the options passed into the driver constructor. Your driver's constructor should take an object of options and pass it on the the Base driver by calling `super(opts)` in your constructor.
|
|
20
|
-
|
|
21
|
-
- `driver.desiredCapConstraints` - Base driver sets this property with a customer `setter` function so that when you create a driver, you can add an object which defines the validation contraints of which desired capabilities your new driver can handle. Of course each driver will have it's own specific desired capabilities. Look for examples on our other drivers.
|
|
22
|
-
|
|
23
|
-
- `driver.createSession(caps)` - this is the function which gets desired capabilities and creates a session. Make sure to call `super.createSession(caps)` so that things like `this.sessionId` and `this.caps` are populated, and the caps are validated against your `desiredCapConstraints`.
|
|
24
|
-
|
|
25
|
-
- `driver.caps` - these are the desired capabilities for the current session.
|
|
26
|
-
|
|
27
|
-
- `driver.sessionId` - this is the ID of the current session. It gets populated automaticall by `baseDriver.createSession`.
|
|
28
|
-
|
|
29
|
-
- `driver.proxyReqRes()` - used by mjsonwp module for proxying http commands to another process (like chromedriver or selendroid)
|
|
30
|
-
|
|
31
|
-
- `driver.jwpProxyAvoid` - used by mjsonwp module. You can specify what REST api routes which you want to SKIP the automatic proxy to another server (which is optional) and instead be handled by your driver.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Base driver exposes an event called `onUnexpectedShutdown` which is called when the driver is shut down unexpectedly (usually after invocation of the `startUnexpectedShutdown` method).
|
|
35
|
-
|
|
36
|
-
Your driver should also implement a startUnexpectedShutdown method?
|
package/lib/express/README.md
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
## appium-express
|
|
2
|
-
|
|
3
|
-
[Express](http://expressjs.com/) server tuned for to serve [Appium](http://appium.io/).
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
### Configuration
|
|
7
|
-
|
|
8
|
-
The `appium-express` server comes configured with:
|
|
9
|
-
|
|
10
|
-
1. appropriate logging formats
|
|
11
|
-
2. service of necessary static assets
|
|
12
|
-
3. allowance of cross-domain requests
|
|
13
|
-
4. default error handling
|
|
14
|
-
5. fix for invalid content types sent by certain clients
|
|
15
|
-
|
|
16
|
-
To configure routes, a function that takes an Express server is passed into the
|
|
17
|
-
server. This function can add whatever routes are wanted.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
### Usage
|
|
21
|
-
|
|
22
|
-
```js
|
|
23
|
-
import { server } from 'appium-base-driver';
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// configure the routes
|
|
27
|
-
function configureRoutes (app) {
|
|
28
|
-
app.get('/hello', (req, res) => {
|
|
29
|
-
res.header['content-type'] = 'text/html';
|
|
30
|
-
res.status(200).send('Hello');
|
|
31
|
-
});
|
|
32
|
-
app.get('/world', (req, res) => {
|
|
33
|
-
res.header['content-type'] = 'text/html';
|
|
34
|
-
res.status(200).send('World');
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const port = 5000;
|
|
39
|
-
const host = 'localhost';
|
|
40
|
-
|
|
41
|
-
const appiumServer = await server({
|
|
42
|
-
routeConfiguringFunction,
|
|
43
|
-
port,
|
|
44
|
-
host,
|
|
45
|
-
});
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
## Watch
|
|
50
|
-
|
|
51
|
-
```
|
|
52
|
-
npm run watch
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## Test
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
npm test
|
|
59
|
-
```
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
## appium-jsonwp-proxy
|
|
2
|
-
|
|
3
|
-
Proxy middleware for the Selenium [JSON Wire Protocol](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md). Allows
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
### Usage
|
|
7
|
-
|
|
8
|
-
The proxy is used by instantiating with the details of the Selenium server to which to proxy. The options for the constructor are passed as a hash with the following possible members:
|
|
9
|
-
|
|
10
|
-
- `scheme` - defaults to 'http'
|
|
11
|
-
- `server` - defaults to 'localhost'
|
|
12
|
-
- `port` - defaults to `4444`
|
|
13
|
-
- `base` - defaults to ''
|
|
14
|
-
- `sessionId` - the session id of the session on the remote server
|
|
15
|
-
- `reqBasePath` - the base path of the server which the request was originally sent to (defaults to '')
|
|
16
|
-
|
|
17
|
-
Once the proxy is created, there are two `async` methods:
|
|
18
|
-
|
|
19
|
-
`command (url, method, body)`
|
|
20
|
-
|
|
21
|
-
Sends a "command" to the proxied server, using the "url", which is the endpoing, with the HTTP method and optional body.
|
|
22
|
-
|
|
23
|
-
```js
|
|
24
|
-
import { JWProxy } from 'appium-base-driver';
|
|
25
|
-
|
|
26
|
-
let host = 'my.host.com';
|
|
27
|
-
let port = 4445;
|
|
28
|
-
|
|
29
|
-
let proxy = new JWProxy({server: host, port: port});
|
|
30
|
-
|
|
31
|
-
// get the Selenium server status
|
|
32
|
-
let seStatus = await proxy.command('/status', 'GET');
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
`proxyReqRes (req, res)`
|
|
36
|
-
|
|
37
|
-
Proxies a request and response to the proxied server. Used to handle the entire conversation of a request/response cycle.
|
|
38
|
-
|
|
39
|
-
```js
|
|
40
|
-
import { JWProxy } from 'appium-base-driver';
|
|
41
|
-
import http from 'http';
|
|
42
|
-
|
|
43
|
-
let host = 'my.host.com';
|
|
44
|
-
let port = 4445;
|
|
45
|
-
|
|
46
|
-
let proxy = new JWProxy({server: host, port: port});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
http.createServer(function (req, res) {
|
|
50
|
-
await proxy.proxyReqRes(res, res);
|
|
51
|
-
}).listen(9615);
|
|
52
|
-
```
|