@appium/base-driver 9.16.1 → 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 +109 -31
- 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 +121 -40
- 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
|
@@ -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,11 +16,11 @@ 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;
|
|
24
|
-
const COMMAND_WITH_SESSION_ID_MATCHER = pathToRegexMatch('/session/:sessionId{/*command}');
|
|
23
|
+
const COMMAND_WITH_SESSION_ID_MATCHER = pathToRegexMatch('{/*prefix}/session/:sessionId{/*command}');
|
|
25
24
|
|
|
26
25
|
const {MJSONWP, W3C} = PROTOCOLS;
|
|
27
26
|
|
|
@@ -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,23 +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
|
-
const 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
|
-
const normalizedPathname = _.trimEnd(
|
|
145
|
-
match && _.isArray(match.params?.command)
|
|
146
|
-
? `/${match.params.command.join('/')}`
|
|
147
|
-
: pathname,
|
|
148
|
-
'/'
|
|
149
|
-
);
|
|
150
|
+
const parsedUrl = this._parseUrl(url);
|
|
151
|
+
const normalizedPathname = this._toNormalizedPathname(parsedUrl);
|
|
150
152
|
const commandName = normalizedPathname
|
|
151
153
|
? routeToCommandName(
|
|
152
154
|
normalizedPathname,
|
|
@@ -172,8 +174,8 @@ class JWProxy {
|
|
|
172
174
|
*
|
|
173
175
|
* @param {string} url
|
|
174
176
|
* @param {string} method
|
|
175
|
-
* @param {
|
|
176
|
-
* @returns {Promise<
|
|
177
|
+
* @param {import('@appium/types').HTTPBody} [body=null]
|
|
178
|
+
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
|
|
177
179
|
*/
|
|
178
180
|
async proxy(url, method, body = null) {
|
|
179
181
|
method = method.toUpperCase();
|
|
@@ -246,8 +248,12 @@ class JWProxy {
|
|
|
246
248
|
// Some servers, like chromedriver may return response code 200 for non-zero JSONWP statuses
|
|
247
249
|
throwProxyError(data);
|
|
248
250
|
}
|
|
249
|
-
const
|
|
250
|
-
return [
|
|
251
|
+
const headersMap = /** @type {import('@appium/types').HTTPHeaders} */ (headers);
|
|
252
|
+
return [{
|
|
253
|
+
statusCode: status,
|
|
254
|
+
headers: headersMap,
|
|
255
|
+
body: data,
|
|
256
|
+
}, data];
|
|
251
257
|
} catch (e) {
|
|
252
258
|
// We only consider an error unexpected if this was not
|
|
253
259
|
// an async request module error or if the response cannot be cast to
|
|
@@ -270,6 +276,11 @@ class JWProxy {
|
|
|
270
276
|
}
|
|
271
277
|
}
|
|
272
278
|
|
|
279
|
+
/**
|
|
280
|
+
*
|
|
281
|
+
* @param {Record<string, any>} resObj
|
|
282
|
+
* @returns {Protocol | undefined}
|
|
283
|
+
*/
|
|
273
284
|
getProtocolFromResBody(resObj) {
|
|
274
285
|
if (_.isInteger(resObj.status)) {
|
|
275
286
|
return MJSONWP;
|
|
@@ -280,6 +291,7 @@ class JWProxy {
|
|
|
280
291
|
}
|
|
281
292
|
|
|
282
293
|
/**
|
|
294
|
+
* @deprecated This method is not used anymore and will be removed
|
|
283
295
|
*
|
|
284
296
|
* @param {string} url
|
|
285
297
|
* @param {import('@appium/types').HTTPMethod} method
|
|
@@ -313,10 +325,13 @@ class JWProxy {
|
|
|
313
325
|
*
|
|
314
326
|
* @param {string} url
|
|
315
327
|
* @param {import('@appium/types').HTTPMethod} method
|
|
316
|
-
* @param {
|
|
328
|
+
* @param {import('@appium/types').HTTPBody} [body=null]
|
|
329
|
+
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
|
|
317
330
|
*/
|
|
318
331
|
async proxyCommand(url, method, body = null) {
|
|
319
|
-
const
|
|
332
|
+
const parsedUrl = this._parseUrl(url);
|
|
333
|
+
const normalizedPathname = this._toNormalizedPathname(parsedUrl);
|
|
334
|
+
const commandName = normalizedPathname ? routeToCommandName(normalizedPathname, method) : '';
|
|
320
335
|
if (!commandName) {
|
|
321
336
|
return await this.proxy(url, method, body);
|
|
322
337
|
}
|
|
@@ -329,8 +344,8 @@ class JWProxy {
|
|
|
329
344
|
*
|
|
330
345
|
* @param {string} url
|
|
331
346
|
* @param {import('@appium/types').HTTPMethod} method
|
|
332
|
-
* @param {
|
|
333
|
-
* @returns {Promise<
|
|
347
|
+
* @param {import('@appium/types').HTTPBody} [body=null]
|
|
348
|
+
* @returns {Promise<import('@appium/types').HTTPBody>}
|
|
334
349
|
*/
|
|
335
350
|
async command(url, method, body = null) {
|
|
336
351
|
let response;
|
|
@@ -384,20 +399,40 @@ class JWProxy {
|
|
|
384
399
|
);
|
|
385
400
|
}
|
|
386
401
|
|
|
402
|
+
/**
|
|
403
|
+
*
|
|
404
|
+
* @param {string} url
|
|
405
|
+
* @returns {string | null}
|
|
406
|
+
*/
|
|
387
407
|
getSessionIdFromUrl(url) {
|
|
388
408
|
const match = url.match(/\/session\/([^/]+)/);
|
|
389
409
|
return match ? match[1] : null;
|
|
390
410
|
}
|
|
391
411
|
|
|
412
|
+
/**
|
|
413
|
+
*
|
|
414
|
+
* @param {import('express').Request} req
|
|
415
|
+
* @param {import('express').Response} res
|
|
416
|
+
*/
|
|
392
417
|
async proxyReqRes(req, res) {
|
|
393
418
|
// ! this method must not throw any exceptions
|
|
394
419
|
// ! make sure to call res.send before return
|
|
420
|
+
/** @type {number} */
|
|
395
421
|
let statusCode;
|
|
422
|
+
/** @type {import('@appium/types').HTTPBody} */
|
|
396
423
|
let resBodyObj;
|
|
397
424
|
try {
|
|
398
425
|
let response;
|
|
399
|
-
[response, resBodyObj] = await this.proxyCommand(
|
|
400
|
-
|
|
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
|
+
}
|
|
401
436
|
statusCode = response.statusCode;
|
|
402
437
|
} catch (err) {
|
|
403
438
|
[statusCode, resBodyObj] = getResponseForW3CError(
|
|
@@ -429,11 +464,57 @@ class JWProxy {
|
|
|
429
464
|
resBodyObj.value = formatResponseValue(resBodyObj.value);
|
|
430
465
|
res.status(statusCode).send(JSON.stringify(formatStatus(resBodyObj)));
|
|
431
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
|
+
}
|
|
432
512
|
}
|
|
433
513
|
|
|
434
|
-
export {JWProxy};
|
|
435
514
|
export default JWProxy;
|
|
436
515
|
|
|
437
516
|
/**
|
|
438
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
|
|
439
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
|
-
```
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
## jsonwp-status
|
|
2
|
-
|
|
3
|
-
Library of status codes for the Selenium [JSON Wire Protocol](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md).
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
### Usage
|
|
7
|
-
|
|
8
|
-
```
|
|
9
|
-
import { statusCodes } from 'appium-base-driver';
|
|
10
|
-
|
|
11
|
-
statusCodes.NoSuchContext;
|
|
12
|
-
// -> {code: 35, summary: 'No such context found'}
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
import { getSummaryByCode } from 'appium-base-driver';
|
|
17
|
-
|
|
18
|
-
getSummaryByCode(0);
|
|
19
|
-
// -> 'The command executed successfully.'
|
|
20
|
-
```
|
package/lib/protocol/README.md
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
## webdriver and mobile-json-wire protocols
|
|
2
|
-
|
|
3
|
-
An abstraction of the Mobile JSON Wire Protocol ([spec](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md)) and the W3C Wire Protocol ([spec](https://www.w3.org/TR/webdriver/) with Appium extensions (as specified [here](http://www.w3.org/TR/webdriver/#protocol-extensions)).
|
|
4
|
-
|
|
5
|
-
### Protocol Detection
|
|
6
|
-
|
|
7
|
-
In the event that a session is requested, and both MJSONWP _and_ W3C capabilities are provided, like this
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
{
|
|
11
|
-
"capabilities: {
|
|
12
|
-
"alwaysMatch": {...},
|
|
13
|
-
"firstMatch": [{...}, ...]
|
|
14
|
-
},
|
|
15
|
-
"desiredCapabilities": {...}
|
|
16
|
-
}
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
a W3C session will be served _unless_ the W3C Capabilities are incomplete. So if the `"desiredCapabilities"` object has more keys
|
|
20
|
-
then whatever the capabilities were matched for the W3C capabilities, then an MJSONWP session will be served instead
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
### Endpoints in the protocol
|
|
24
|
-
|
|
25
|
-
The Mobile JSON Wire Protocol package gives access to a number of endpoints documented [here](https://github.com/appium/appium-base-driver/blob/master/docs/mjsonwp/protocol-methods.md).
|
|
26
|
-
|
|
27
|
-
The W3C WebDriver Protocol package gives access to a number of endpoints documented in the [official documentation](https://www.w3.org/TR/webdriver/) and the
|
|
28
|
-
[simplified spec](https://github.com/jlipps/simple-wd-spec)
|
|
29
|
-
|
|
30
|
-
### Protocol
|
|
31
|
-
|
|
32
|
-
The basic class, subclassed by drivers that will use the protocol.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
### routeConfiguringFunction (driver)
|
|
36
|
-
|
|
37
|
-
This function gives drivers access to the protocol routes. It returns a function that itself will take an [Express](http://expressjs.com/) application.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
### isSessionCommand (command)
|
|
41
|
-
|
|
42
|
-
Checks if the `command` needs to have a session associated with it.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
### ALL_COMMANDS
|
|
46
|
-
|
|
47
|
-
An array of all the commands that will be dispatched to by the Mobile JSON Wire Proxy endpoints.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
### NO_SESSION_ID_COMMANDS
|
|
51
|
-
|
|
52
|
-
An array of commands that do not need a session associated with them.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
### Errors
|
|
56
|
-
|
|
57
|
-
This package exports a number of classes and methods related to Selenium error handling. There are error classes for each Selenium error type (see [JSONWP Errors](https://code.google.com/p/selenium/wiki/JsonWireProtocol#Response_Status_Codes), as well as the context errors in the [mobile spec](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md#webviews-and-other-contexts).
|
|
58
|
-
|
|
59
|
-
The list of errors, and their meanings, can be found [here for JSONWP](https://github.com/appium/appium-base-driver/blob/master/docs/mjsonwp/errors.md) and
|
|
60
|
-
[here for W3C Errors](https://www.w3.org/TR/webdriver/#handling-errors))
|
|
61
|
-
|
|
62
|
-
There are, in addition, two helper methods for dealing with errors
|
|
63
|
-
|
|
64
|
-
`isErrorType (err, type)`
|
|
65
|
-
|
|
66
|
-
- checks if the `err` object is a Mobile JSON Wire Protocol error of a particular type
|
|
67
|
-
- arguments
|
|
68
|
-
- `err` - the error object to test
|
|
69
|
-
- `type` - the error class to test against
|
|
70
|
-
- usage
|
|
71
|
-
```js
|
|
72
|
-
import { errors, isErrorType } from 'mobile-json-wire-protocol';
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
// do some stuff...
|
|
76
|
-
} catch (err) {
|
|
77
|
-
if (isErrorType(err, errors.InvalidCookieDomainError)) {
|
|
78
|
-
// process...
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
`errorFromCode (code, message)`
|
|
84
|
-
|
|
85
|
-
- retrieve the appropriate error for an error code, with the supplied message.
|
|
86
|
-
- arguments
|
|
87
|
-
- `code` - the integer error code for a Mobile JSON Wire Protocol error
|
|
88
|
-
- `message` - the message to be encapsulated in the error
|
|
89
|
-
- usage
|
|
90
|
-
```js
|
|
91
|
-
import { errors, errorFromCode } from 'mobile-json-wire-protocol';
|
|
92
|
-
|
|
93
|
-
let error = errorFromCode(6, 'an error has occurred');
|
|
94
|
-
|
|
95
|
-
console.log(error instanceof errors.NoSuchDriverError);
|
|
96
|
-
// => true
|
|
97
|
-
|
|
98
|
-
console.log(error.message === 'an error has occurred');
|
|
99
|
-
// => true
|
|
100
|
-
```
|