@appium/base-driver 10.5.1 → 10.6.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/build/lib/basedriver/capabilities.d.ts.map +1 -1
- package/build/lib/basedriver/capabilities.js +45 -45
- package/build/lib/basedriver/capabilities.js.map +1 -1
- package/build/lib/basedriver/commands/bidi.d.ts.map +1 -1
- package/build/lib/basedriver/commands/bidi.js +9 -13
- package/build/lib/basedriver/commands/bidi.js.map +1 -1
- package/build/lib/basedriver/commands/event.d.ts.map +1 -1
- package/build/lib/basedriver/commands/event.js +4 -7
- package/build/lib/basedriver/commands/event.js.map +1 -1
- package/build/lib/basedriver/commands/execute.js +3 -6
- package/build/lib/basedriver/commands/execute.js.map +1 -1
- package/build/lib/basedriver/commands/log.d.ts.map +1 -1
- package/build/lib/basedriver/commands/log.js +1 -5
- package/build/lib/basedriver/commands/log.js.map +1 -1
- package/build/lib/basedriver/commands/timeout.d.ts.map +1 -1
- package/build/lib/basedriver/commands/timeout.js +5 -9
- package/build/lib/basedriver/commands/timeout.js.map +1 -1
- package/build/lib/basedriver/core.js +12 -12
- package/build/lib/basedriver/core.js.map +1 -1
- package/build/lib/basedriver/device-settings.d.ts.map +1 -1
- package/build/lib/basedriver/device-settings.js +3 -7
- package/build/lib/basedriver/device-settings.js.map +1 -1
- package/build/lib/basedriver/driver.d.ts.map +1 -1
- package/build/lib/basedriver/driver.js +13 -16
- package/build/lib/basedriver/driver.js.map +1 -1
- package/build/lib/basedriver/extension-core.d.ts +4 -1
- package/build/lib/basedriver/extension-core.d.ts.map +1 -1
- package/build/lib/basedriver/extension-core.js +27 -9
- package/build/lib/basedriver/extension-core.js.map +1 -1
- package/build/lib/basedriver/helpers.d.ts.map +1 -1
- package/build/lib/basedriver/helpers.js +28 -30
- package/build/lib/basedriver/helpers.js.map +1 -1
- package/build/lib/basedriver/ipc.d.ts +36 -0
- package/build/lib/basedriver/ipc.d.ts.map +1 -0
- package/build/lib/basedriver/ipc.js +155 -0
- package/build/lib/basedriver/ipc.js.map +1 -0
- package/build/lib/basedriver/validation.js +25 -28
- package/build/lib/basedriver/validation.js.map +1 -1
- package/build/lib/express/express-logging.d.ts.map +1 -1
- package/build/lib/express/express-logging.js +2 -3
- package/build/lib/express/express-logging.js.map +1 -1
- package/build/lib/express/idempotency.js +3 -6
- package/build/lib/express/idempotency.js.map +1 -1
- package/build/lib/express/middleware.d.ts.map +1 -1
- package/build/lib/express/middleware.js +6 -10
- package/build/lib/express/middleware.js.map +1 -1
- package/build/lib/express/server.d.ts.map +1 -1
- package/build/lib/express/server.js +64 -54
- package/build/lib/express/server.js.map +1 -1
- package/build/lib/express/static.d.ts.map +1 -1
- package/build/lib/express/static.js +14 -7
- package/build/lib/express/static.js.map +1 -1
- package/build/lib/express/websocket.d.ts.map +1 -1
- package/build/lib/express/websocket.js +6 -9
- package/build/lib/express/websocket.js.map +1 -1
- package/build/lib/helpers/capabilities.d.ts.map +1 -1
- package/build/lib/helpers/capabilities.js +14 -17
- package/build/lib/helpers/capabilities.js.map +1 -1
- package/build/lib/helpers/extension-command-name.js +2 -5
- package/build/lib/helpers/extension-command-name.js.map +1 -1
- package/build/lib/helpers/levenshtein-match.d.ts.map +1 -1
- package/build/lib/helpers/levenshtein-match.js +2 -6
- package/build/lib/helpers/levenshtein-match.js.map +1 -1
- package/build/lib/index.d.ts +1 -0
- package/build/lib/index.d.ts.map +1 -1
- package/build/lib/index.js +3 -14
- package/build/lib/index.js.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.js +13 -17
- package/build/lib/jsonwp-proxy/protocol-converter.js.map +1 -1
- package/build/lib/jsonwp-proxy/proxy-request.d.ts +2 -2
- package/build/lib/jsonwp-proxy/proxy-request.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/proxy-request.js +25 -21
- package/build/lib/jsonwp-proxy/proxy-request.js.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.js +29 -26
- package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
- package/build/lib/protocol/errors.d.ts.map +1 -1
- package/build/lib/protocol/errors.js +25 -29
- package/build/lib/protocol/errors.js.map +1 -1
- package/build/lib/protocol/helpers.d.ts.map +1 -1
- package/build/lib/protocol/helpers.js +9 -8
- package/build/lib/protocol/helpers.js.map +1 -1
- package/build/lib/protocol/protocol.d.ts.map +1 -1
- package/build/lib/protocol/protocol.js +43 -48
- package/build/lib/protocol/protocol.js.map +1 -1
- package/build/lib/protocol/routes.d.ts +1 -1
- package/build/lib/protocol/routes.d.ts.map +1 -1
- package/build/lib/protocol/routes.js +9 -12
- package/build/lib/protocol/routes.js.map +1 -1
- package/build/lib/protocol/validators.d.ts.map +1 -1
- package/build/lib/protocol/validators.js +1 -5
- package/build/lib/protocol/validators.js.map +1 -1
- package/build/lib/utils.d.ts +16 -0
- package/build/lib/utils.d.ts.map +1 -0
- package/build/lib/utils.js +71 -0
- package/build/lib/utils.js.map +1 -0
- package/lib/basedriver/capabilities.ts +60 -55
- package/lib/basedriver/commands/bidi.ts +10 -10
- package/lib/basedriver/commands/event.ts +11 -10
- package/lib/basedriver/commands/execute.ts +3 -3
- package/lib/basedriver/commands/log.ts +3 -2
- package/lib/basedriver/commands/timeout.ts +5 -6
- package/lib/basedriver/core.ts +12 -12
- package/lib/basedriver/device-settings.ts +3 -4
- package/lib/basedriver/driver.ts +15 -13
- package/lib/basedriver/extension-core.ts +33 -7
- package/lib/basedriver/helpers.ts +28 -30
- package/lib/basedriver/ipc.ts +179 -0
- package/lib/basedriver/validation.ts +26 -26
- package/lib/express/express-logging.ts +3 -4
- package/lib/express/idempotency.ts +3 -3
- package/lib/express/middleware.ts +6 -8
- package/lib/express/server.ts +67 -61
- package/lib/express/static.ts +15 -7
- package/lib/express/websocket.ts +8 -10
- package/lib/helpers/capabilities.ts +18 -14
- package/lib/helpers/extension-command-name.ts +2 -2
- package/lib/helpers/levenshtein-match.ts +2 -5
- package/lib/index.js +1 -11
- package/lib/jsonwp-proxy/protocol-converter.ts +14 -15
- package/lib/jsonwp-proxy/proxy-request.ts +26 -26
- package/lib/jsonwp-proxy/proxy.ts +36 -37
- package/lib/protocol/errors.ts +29 -28
- package/lib/protocol/helpers.ts +9 -5
- package/lib/protocol/protocol.ts +44 -46
- package/lib/protocol/routes.ts +9 -9
- package/lib/protocol/validators.ts +1 -3
- package/lib/utils.ts +85 -0
- package/package.json +7 -9
package/lib/index.js
CHANGED
|
@@ -1,19 +1,9 @@
|
|
|
1
|
-
import B from 'bluebird';
|
|
2
|
-
|
|
3
|
-
try {
|
|
4
|
-
B.config({
|
|
5
|
-
cancellation: true,
|
|
6
|
-
});
|
|
7
|
-
} catch {
|
|
8
|
-
// sometimes during testing this somehow gets required twice and results in an error about
|
|
9
|
-
// cancellation not being able to be enabled after promise has been configured
|
|
10
|
-
}
|
|
11
|
-
|
|
12
1
|
// BaseDriver exports
|
|
13
2
|
export {ExtensionCore} from './basedriver/extension-core';
|
|
14
3
|
import {BaseDriver} from './basedriver/driver';
|
|
15
4
|
export {DriverCore} from './basedriver/core';
|
|
16
5
|
export {DeviceSettings} from './basedriver/device-settings';
|
|
6
|
+
export {AppiumIpc} from './basedriver/ipc';
|
|
17
7
|
|
|
18
8
|
export {BaseDriver};
|
|
19
9
|
export default BaseDriver;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type {AppiumLogger, HTTPBody, ProxyResponse} from '@appium/types';
|
|
2
|
-
import _ from 'lodash';
|
|
3
2
|
import {logger, util} from '@appium/support';
|
|
4
3
|
import {duplicateKeys} from '../basedriver/helpers';
|
|
5
4
|
import {MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY, PROTOCOLS} from '../constants';
|
|
@@ -138,12 +137,12 @@ export class ProtocolConverter {
|
|
|
138
137
|
* provided in the request, we need to do 3 proxies and combine the result.
|
|
139
138
|
*/
|
|
140
139
|
private getTimeoutRequestObjects(body: HTTPBody): Record<string, unknown>[] {
|
|
141
|
-
if (
|
|
140
|
+
if (body == null) {
|
|
142
141
|
return [];
|
|
143
142
|
}
|
|
144
143
|
|
|
145
144
|
const bodyObj = (util.safeJsonParse(body) as Record<string, unknown>) ?? {};
|
|
146
|
-
if (this.downstreamProtocol === W3C &&
|
|
145
|
+
if (this.downstreamProtocol === W3C && Object.hasOwn(bodyObj, 'ms') && Object.hasOwn(bodyObj, 'type')) {
|
|
147
146
|
const typeToW3C = (x: string) => (x === 'page load' ? 'pageLoad' : x);
|
|
148
147
|
return [
|
|
149
148
|
{
|
|
@@ -152,9 +151,9 @@ export class ProtocolConverter {
|
|
|
152
151
|
];
|
|
153
152
|
}
|
|
154
153
|
|
|
155
|
-
if (this.downstreamProtocol === MJSONWP && (!
|
|
154
|
+
if (this.downstreamProtocol === MJSONWP && (!Object.hasOwn(bodyObj, 'ms') || !Object.hasOwn(bodyObj, 'type'))) {
|
|
156
155
|
const typeToJSONWP = (x: string) => (x === 'pageLoad' ? 'page load' : x);
|
|
157
|
-
return
|
|
156
|
+
return Object.entries(bodyObj)
|
|
158
157
|
// Only transform the entry if ms value is a valid positive float number
|
|
159
158
|
.filter((pair) => /^\d+(?:[.,]\d*?)?$/.test(`${pair[1]}`))
|
|
160
159
|
.map((pair) => ({
|
|
@@ -206,16 +205,16 @@ export class ProtocolConverter {
|
|
|
206
205
|
body: HTTPBody
|
|
207
206
|
): Promise<[ProxyResponse, HTTPBody]> {
|
|
208
207
|
const bodyObj = util.safeJsonParse(body);
|
|
209
|
-
if (
|
|
208
|
+
if (util.isPlainObject(bodyObj)) {
|
|
210
209
|
const obj = bodyObj as Record<string, unknown>;
|
|
211
|
-
if (this.downstreamProtocol === W3C &&
|
|
210
|
+
if (this.downstreamProtocol === W3C && Object.hasOwn(bodyObj, 'name') && !Object.hasOwn(bodyObj, 'handle')) {
|
|
212
211
|
this.log.debug(`Copied 'name' value '${obj.name}' to 'handle' as per W3C spec`);
|
|
213
212
|
return await this.proxyFunc(url, method, {...obj, handle: obj.name});
|
|
214
213
|
}
|
|
215
214
|
if (
|
|
216
215
|
this.downstreamProtocol === MJSONWP &&
|
|
217
|
-
|
|
218
|
-
!
|
|
216
|
+
Object.hasOwn(bodyObj, 'handle') &&
|
|
217
|
+
!Object.hasOwn(bodyObj, 'name')
|
|
219
218
|
) {
|
|
220
219
|
this.log.debug(`Copied 'handle' value '${obj.handle}' to 'name' as per JSONWP spec`);
|
|
221
220
|
return await this.proxyFunc(url, method, {...obj, name: obj.handle});
|
|
@@ -230,13 +229,13 @@ export class ProtocolConverter {
|
|
|
230
229
|
body: HTTPBody
|
|
231
230
|
): Promise<[ProxyResponse, HTTPBody]> {
|
|
232
231
|
const bodyObj = util.safeJsonParse(body) as Record<string, unknown> | undefined;
|
|
233
|
-
if (
|
|
232
|
+
if (util.isPlainObject(bodyObj) && (util.hasValue(bodyObj?.text) || util.hasValue(bodyObj?.value))) {
|
|
234
233
|
let {text, value} = bodyObj;
|
|
235
234
|
if (util.hasValue(text) && !util.hasValue(value)) {
|
|
236
|
-
value =
|
|
235
|
+
value = typeof text === 'string' ? [...text] : Array.isArray(text) ? text : [];
|
|
237
236
|
this.log.debug(`Added 'value' property to 'setValue' request body`);
|
|
238
237
|
} else if (!util.hasValue(text) && util.hasValue(value)) {
|
|
239
|
-
text =
|
|
238
|
+
text = Array.isArray(value) ? value.join('') : typeof value === 'string' ? value : '';
|
|
240
239
|
this.log.debug(`Added 'text' property to 'setValue' request body`);
|
|
241
240
|
}
|
|
242
241
|
return await this.proxyFunc(url, method, {...bodyObj, text, value});
|
|
@@ -250,10 +249,10 @@ export class ProtocolConverter {
|
|
|
250
249
|
body: HTTPBody
|
|
251
250
|
): Promise<[ProxyResponse, HTTPBody]> {
|
|
252
251
|
const bodyObj = util.safeJsonParse(body);
|
|
253
|
-
if (
|
|
252
|
+
if (Object.hasOwn(bodyObj ?? {}, 'id') && util.isPlainObject((bodyObj as Record<string, unknown>).id)) {
|
|
254
253
|
return await this.proxyFunc(url, method, {
|
|
255
254
|
...(bodyObj as object),
|
|
256
|
-
id: duplicateKeys(bodyObj.id as object, MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY),
|
|
255
|
+
id: duplicateKeys((bodyObj as Record<string, unknown>).id as object, MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY),
|
|
257
256
|
});
|
|
258
257
|
}
|
|
259
258
|
return await this.proxyFunc(url, method, body);
|
|
@@ -265,7 +264,7 @@ export class ProtocolConverter {
|
|
|
265
264
|
body: HTTPBody
|
|
266
265
|
): Promise<[ProxyResponse, HTTPBody]> {
|
|
267
266
|
const bodyObj = util.safeJsonParse(body);
|
|
268
|
-
if (
|
|
267
|
+
if (util.isPlainObject(bodyObj)) {
|
|
269
268
|
return await this.proxyFunc(
|
|
270
269
|
url,
|
|
271
270
|
method,
|
|
@@ -1,19 +1,16 @@
|
|
|
1
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
2
|
|
|
8
3
|
export class ProxyRequest {
|
|
9
4
|
private readonly _requestConfig: axios.RawAxiosRequestConfig;
|
|
10
|
-
private
|
|
11
|
-
private
|
|
5
|
+
private _resultPromise: Promise<axios.AxiosResponse> | null;
|
|
6
|
+
private _abortController: AbortController | null;
|
|
7
|
+
private _cancelled: boolean;
|
|
12
8
|
|
|
13
9
|
constructor(requestConfig: axios.RawAxiosRequestConfig<any>) {
|
|
14
10
|
this._requestConfig = requestConfig;
|
|
15
|
-
this._ee = new EventEmitter();
|
|
16
11
|
this._resultPromise = null;
|
|
12
|
+
this._abortController = null;
|
|
13
|
+
this._cancelled = false;
|
|
17
14
|
}
|
|
18
15
|
|
|
19
16
|
async execute(): Promise<axios.AxiosResponse> {
|
|
@@ -21,32 +18,35 @@ export class ProxyRequest {
|
|
|
21
18
|
return await this._resultPromise;
|
|
22
19
|
}
|
|
23
20
|
|
|
21
|
+
const abortController = new AbortController();
|
|
22
|
+
this._abortController = abortController;
|
|
23
|
+
this._cancelled = false;
|
|
24
|
+
|
|
24
25
|
try {
|
|
25
|
-
this._resultPromise =
|
|
26
|
-
this._makeRacingTimer(),
|
|
27
|
-
this._makeRequest(),
|
|
28
|
-
]);
|
|
26
|
+
this._resultPromise = this._makeRequest(abortController.signal);
|
|
29
27
|
return await this._resultPromise;
|
|
30
28
|
} finally {
|
|
31
|
-
this.
|
|
32
|
-
this._ee.removeAllListeners();
|
|
29
|
+
this._abortController = null;
|
|
33
30
|
}
|
|
34
31
|
}
|
|
35
32
|
|
|
36
33
|
cancel(): void {
|
|
37
|
-
this.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
private async _makeRequest(): Promise<axios.AxiosResponse> {
|
|
41
|
-
return await axios(this._requestConfig);
|
|
34
|
+
this._cancelled = true;
|
|
35
|
+
this._abortController?.abort();
|
|
42
36
|
}
|
|
43
37
|
|
|
44
|
-
private async
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
50
|
-
})
|
|
38
|
+
private async _makeRequest(signal: AbortSignal): Promise<axios.AxiosResponse> {
|
|
39
|
+
try {
|
|
40
|
+
return await axios({
|
|
41
|
+
...this._requestConfig,
|
|
42
|
+
signal,
|
|
43
|
+
});
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (this._cancelled && axios.isCancel(err)) {
|
|
46
|
+
// The request was cancelled; do not propagate the error to callers.
|
|
47
|
+
return await new Promise<axios.AxiosResponse>(() => {});
|
|
48
|
+
}
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -6,7 +6,6 @@ import type {
|
|
|
6
6
|
ProxyOptions,
|
|
7
7
|
ProxyResponse,
|
|
8
8
|
} from '@appium/types';
|
|
9
|
-
import _ from 'lodash';
|
|
10
9
|
import {logger, util} from '@appium/support';
|
|
11
10
|
import {getSummaryByCode} from '../jsonwp-status/status';
|
|
12
11
|
import {
|
|
@@ -19,6 +18,7 @@ import {
|
|
|
19
18
|
import {isSessionCommand, routeToCommandName} from '../protocol';
|
|
20
19
|
import {MAX_LOG_BODY_LENGTH, DEFAULT_BASE_PATH, PROTOCOLS} from '../constants';
|
|
21
20
|
import {ProtocolConverter} from './protocol-converter';
|
|
21
|
+
import {omit, pick} from '../utils';
|
|
22
22
|
import {formatResponseValue, ensureW3cResponse} from '../protocol/helpers';
|
|
23
23
|
import http from 'node:http';
|
|
24
24
|
import https from 'node:https';
|
|
@@ -69,8 +69,11 @@ export class JWProxy {
|
|
|
69
69
|
private readonly _log: AppiumLogger | undefined;
|
|
70
70
|
|
|
71
71
|
constructor(opts: ProxyOptions = {}) {
|
|
72
|
-
const
|
|
73
|
-
|
|
72
|
+
const filteredOptsWithoutLog = omit(
|
|
73
|
+
pick(opts as Record<string, unknown>, ALLOWED_OPTS),
|
|
74
|
+
'log'
|
|
75
|
+
) as Omit<ProxyOptions, 'log'>;
|
|
76
|
+
const options = {
|
|
74
77
|
scheme: 'http',
|
|
75
78
|
server: 'localhost',
|
|
76
79
|
port: 4444,
|
|
@@ -78,7 +81,8 @@ export class JWProxy {
|
|
|
78
81
|
reqBasePath: DEFAULT_BASE_PATH,
|
|
79
82
|
sessionId: null,
|
|
80
83
|
timeout: DEFAULT_REQUEST_TIMEOUT,
|
|
81
|
-
|
|
84
|
+
...filteredOptsWithoutLog,
|
|
85
|
+
} as ProxyOptions & {
|
|
82
86
|
scheme: string;
|
|
83
87
|
server: string;
|
|
84
88
|
port: number;
|
|
@@ -153,7 +157,7 @@ export class JWProxy {
|
|
|
153
157
|
const requiresSessionId =
|
|
154
158
|
!commandName || (commandName && isSessionCommand(commandName));
|
|
155
159
|
const proxyPrefix = `${this.scheme}://${this.server}:${this.port}${this.base}`;
|
|
156
|
-
let proxySuffix = normalizedPathname ? `/${
|
|
160
|
+
let proxySuffix = normalizedPathname ? `/${normalizedPathname.replace(/^\/+/, '')}` : '';
|
|
157
161
|
if (parsedUrl.search) {
|
|
158
162
|
proxySuffix += parsedUrl.search;
|
|
159
163
|
}
|
|
@@ -179,7 +183,7 @@ export class JWProxy {
|
|
|
179
183
|
method = method.toUpperCase();
|
|
180
184
|
const newUrl = this.getUrlForProxy(url, method as HTTPMethod);
|
|
181
185
|
const truncateBody = (content: unknown): string =>
|
|
182
|
-
|
|
186
|
+
util.truncateString(typeof content === 'string' ? content : JSON.stringify(content), {
|
|
183
187
|
length: MAX_LOG_BODY_LENGTH,
|
|
184
188
|
});
|
|
185
189
|
const reqOpts: RawAxiosRequestConfig = {
|
|
@@ -242,7 +246,7 @@ export class JWProxy {
|
|
|
242
246
|
const {data, status, headers} = await this.request(reqOpts);
|
|
243
247
|
// `data` might be really big
|
|
244
248
|
// Be careful while handling it to avoid memory leaks
|
|
245
|
-
if (!
|
|
249
|
+
if (!util.isPlainObject(data)) {
|
|
246
250
|
// The response should be a valid JSON object
|
|
247
251
|
// If it cannot be coerced to an object then the response is wrong
|
|
248
252
|
throwProxyError(data);
|
|
@@ -252,23 +256,15 @@ export class JWProxy {
|
|
|
252
256
|
const isSessionCreationRequest = url.endsWith('/session') && method === 'POST';
|
|
253
257
|
if (isSessionCreationRequest) {
|
|
254
258
|
if (status === 200) {
|
|
255
|
-
const value =
|
|
256
|
-
|
|
257
|
-
| undefined;
|
|
258
|
-
const raw =
|
|
259
|
-
(data as Record<string, unknown>).sessionId ?? value?.sessionId;
|
|
259
|
+
const value = data.value as Record<string, unknown> | undefined;
|
|
260
|
+
const raw = data.sessionId ?? value?.sessionId;
|
|
260
261
|
this.sessionId =
|
|
261
262
|
typeof raw === 'string' ? raw : raw != null ? String(raw) : null;
|
|
262
263
|
}
|
|
263
|
-
this.downstreamProtocol = this.getProtocolFromResBody(
|
|
264
|
-
data as Record<string, unknown>
|
|
265
|
-
) ?? this.downstreamProtocol;
|
|
264
|
+
this.downstreamProtocol = this.getProtocolFromResBody(data) ?? this.downstreamProtocol;
|
|
266
265
|
this.log.info(`Determined the downstream protocol as '${this.downstreamProtocol}'`);
|
|
267
266
|
}
|
|
268
|
-
if (
|
|
269
|
-
_.has(data, 'status') &&
|
|
270
|
-
parseInt((data as Record<string, unknown>).status as string, 10) !== 0
|
|
271
|
-
) {
|
|
267
|
+
if (Object.hasOwn(data, 'status') && parseInt(data.status as string, 10) !== 0) {
|
|
272
268
|
throwProxyError(data);
|
|
273
269
|
}
|
|
274
270
|
return [
|
|
@@ -307,10 +303,10 @@ export class JWProxy {
|
|
|
307
303
|
* Detects the downstream protocol from a response body.
|
|
308
304
|
*/
|
|
309
305
|
getProtocolFromResBody(resObj: Record<string, unknown>): Protocol | undefined {
|
|
310
|
-
if (
|
|
306
|
+
if (Number.isInteger(resObj.status)) {
|
|
311
307
|
return MJSONWP;
|
|
312
308
|
}
|
|
313
|
-
if (
|
|
309
|
+
if (resObj.value !== undefined) {
|
|
314
310
|
return W3C;
|
|
315
311
|
}
|
|
316
312
|
}
|
|
@@ -363,10 +359,10 @@ export class JWProxy {
|
|
|
363
359
|
const status = parseInt(resBody.status as string, 10);
|
|
364
360
|
if (!isNaN(status) && status !== 0) {
|
|
365
361
|
let message: unknown = resBody.value;
|
|
366
|
-
if (
|
|
362
|
+
if (util.isPlainObject(message) && Object.hasOwn(message, 'message')) {
|
|
367
363
|
message = (message as Record<string, unknown>).message;
|
|
368
364
|
}
|
|
369
|
-
throw errorFromMJSONWPStatusCode(status,
|
|
365
|
+
throw errorFromMJSONWPStatusCode(status, util.isEmpty(message)
|
|
370
366
|
? getSummaryByCode(status)
|
|
371
367
|
: (message as string | {message: string}));
|
|
372
368
|
}
|
|
@@ -374,8 +370,8 @@ export class JWProxy {
|
|
|
374
370
|
if (response.statusCode < 300) {
|
|
375
371
|
return resBody.value;
|
|
376
372
|
}
|
|
377
|
-
if (
|
|
378
|
-
const value = resBody.value
|
|
373
|
+
if (util.isPlainObject(resBody.value) && resBody.value.error) {
|
|
374
|
+
const value = resBody.value;
|
|
379
375
|
throw errorFromW3CJsonCode(
|
|
380
376
|
value.error as string,
|
|
381
377
|
(value.message as string) ?? '',
|
|
@@ -387,7 +383,7 @@ export class JWProxy {
|
|
|
387
383
|
}
|
|
388
384
|
throw new errors.UnknownError(
|
|
389
385
|
`Did not know what to do with response code '${response.statusCode}' ` +
|
|
390
|
-
`and response body '${
|
|
386
|
+
`and response body '${util.truncateString(JSON.stringify(resBodyObj), {
|
|
391
387
|
length: 300,
|
|
392
388
|
})}'`
|
|
393
389
|
);
|
|
@@ -424,16 +420,16 @@ export class JWProxy {
|
|
|
424
420
|
);
|
|
425
421
|
}
|
|
426
422
|
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
427
|
-
if (!
|
|
423
|
+
if (!util.isPlainObject(resBodyObj)) {
|
|
428
424
|
const error = new errors.UnknownError(
|
|
429
425
|
`The downstream server response with the status code ${statusCode} is not a valid JSON object: ` +
|
|
430
|
-
|
|
426
|
+
util.truncateString(`${resBodyObj}`, {length: 300})
|
|
431
427
|
);
|
|
432
428
|
[statusCode, resBodyObj] = getResponseForW3CError(error);
|
|
433
429
|
}
|
|
434
430
|
|
|
435
431
|
const resBody = resBodyObj as Record<string, unknown>;
|
|
436
|
-
if (
|
|
432
|
+
if (Object.hasOwn(resBody, 'sessionId')) {
|
|
437
433
|
const reqSessionId = this.getSessionIdFromUrl(req.originalUrl);
|
|
438
434
|
if (reqSessionId) {
|
|
439
435
|
this.log.info(`Replacing sessionId ${resBody.sessionId} with ${reqSessionId}`);
|
|
@@ -459,7 +455,10 @@ export class JWProxy {
|
|
|
459
455
|
try {
|
|
460
456
|
return await req.execute();
|
|
461
457
|
} finally {
|
|
462
|
-
|
|
458
|
+
const reqIndex = this._activeRequests.indexOf(req);
|
|
459
|
+
if (reqIndex >= 0) {
|
|
460
|
+
this._activeRequests.splice(reqIndex, 1);
|
|
461
|
+
}
|
|
463
462
|
}
|
|
464
463
|
}
|
|
465
464
|
|
|
@@ -467,8 +466,8 @@ export class JWProxy {
|
|
|
467
466
|
// eslint-disable-next-line n/no-deprecated-api -- we need relative URL support
|
|
468
467
|
const parsedUrl = nodeUrl.parse(url || '/');
|
|
469
468
|
if (
|
|
470
|
-
|
|
471
|
-
|
|
469
|
+
parsedUrl.href == null ||
|
|
470
|
+
parsedUrl.pathname == null ||
|
|
472
471
|
(parsedUrl.protocol && !['http:', 'https:'].includes(parsedUrl.protocol))
|
|
473
472
|
) {
|
|
474
473
|
throw new Error(`Did not know how to proxy the url '${url}'`);
|
|
@@ -477,7 +476,7 @@ export class JWProxy {
|
|
|
477
476
|
}
|
|
478
477
|
|
|
479
478
|
private _toNormalizedPathname(parsedUrl: nodeUrl.UrlWithStringQuery): string {
|
|
480
|
-
if (
|
|
479
|
+
if (typeof parsedUrl.pathname !== 'string') {
|
|
481
480
|
return '';
|
|
482
481
|
}
|
|
483
482
|
let pathname =
|
|
@@ -488,20 +487,20 @@ export class JWProxy {
|
|
|
488
487
|
// This is needed for the backward compatibility
|
|
489
488
|
// if drivers don't set reqBasePath properly
|
|
490
489
|
if (!this.reqBasePath) {
|
|
491
|
-
if (match && match.params &&
|
|
490
|
+
if (match && match.params && Array.isArray((match.params as Record<string, unknown>).prefix)) {
|
|
492
491
|
pathname = pathname.replace(
|
|
493
492
|
`/${((match.params as Record<string, unknown>).prefix as string[]).join('/')}`,
|
|
494
493
|
''
|
|
495
494
|
);
|
|
496
|
-
} else if (
|
|
495
|
+
} else if (pathname.startsWith('/wd/hub')) {
|
|
497
496
|
pathname = pathname.replace('/wd/hub', '');
|
|
498
497
|
}
|
|
499
498
|
}
|
|
500
499
|
let result = pathname;
|
|
501
500
|
if (match && match.params) {
|
|
502
501
|
const command = (match.params as Record<string, unknown>).command;
|
|
503
|
-
result =
|
|
502
|
+
result = Array.isArray(command) ? `/${(command as string[]).join('/')}` : '';
|
|
504
503
|
}
|
|
505
|
-
return
|
|
504
|
+
return result.replace(/\/+$/, '');
|
|
506
505
|
}
|
|
507
506
|
}
|
package/lib/protocol/errors.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import _ from 'lodash';
|
|
2
1
|
import {util, logger} from '@appium/support';
|
|
3
2
|
import {StatusCodes as HTTPStatusCodes} from 'http-status-codes';
|
|
4
3
|
import type {ErrorBiDiCommandResponse, Class} from '@appium/types';
|
|
@@ -22,13 +21,13 @@ class BaseError extends Error {
|
|
|
22
21
|
|
|
23
22
|
private _formatStack(): void {
|
|
24
23
|
// eslint-disable-next-line no-prototype-builtins
|
|
25
|
-
if (Error.hasOwnProperty('captureStackTrace') &&
|
|
24
|
+
if (Error.hasOwnProperty('captureStackTrace') && util.isEmpty(this.stack)) {
|
|
26
25
|
Error.captureStackTrace(this, this.constructor);
|
|
27
26
|
}
|
|
28
|
-
if (
|
|
27
|
+
if (typeof this.cause?.stack !== 'string') {
|
|
29
28
|
return;
|
|
30
29
|
}
|
|
31
|
-
if (
|
|
30
|
+
if (util.isEmpty(this.stack)) {
|
|
32
31
|
this.stack = this.cause.stack;
|
|
33
32
|
return;
|
|
34
33
|
}
|
|
@@ -77,7 +76,7 @@ export class ProtocolError extends BaseError {
|
|
|
77
76
|
bidiErrObject(id: string | number): ErrorBiDiCommandResponse {
|
|
78
77
|
// if we don't have an id, the client didn't send one, so we have nothing to send back.
|
|
79
78
|
// send back zero rather than making something up
|
|
80
|
-
const intId = (
|
|
79
|
+
const intId = (Number.isInteger(id) ? id : (parseInt(`${id}`, 10) || 0)) as number;
|
|
81
80
|
return {
|
|
82
81
|
id: intId,
|
|
83
82
|
type: 'error',
|
|
@@ -957,30 +956,30 @@ export class ProxyRequestError extends BaseError {
|
|
|
957
956
|
) {
|
|
958
957
|
const [responseErrorObj, originalMessage] = ProxyRequestError._parseHttpResponse(httpResponseData);
|
|
959
958
|
super(
|
|
960
|
-
|
|
959
|
+
util.isEmpty(message)
|
|
961
960
|
? `Proxy request unsuccessful.${originalMessage ? (' ' + originalMessage) : ''}`
|
|
962
961
|
: message,
|
|
963
962
|
cause,
|
|
964
963
|
);
|
|
965
964
|
|
|
966
965
|
// If the response error is an object and value is an object, it's a W3C error (for JSONWP value is a string)
|
|
967
|
-
if (
|
|
968
|
-
this._w3cError = responseErrorObj.value;
|
|
966
|
+
if (util.isPlainObject(responseErrorObj.value) && Object.hasOwn(responseErrorObj.value, 'error')) {
|
|
967
|
+
this._w3cError = responseErrorObj.value as unknown as typeof this._w3cError;
|
|
969
968
|
this._w3cErrorStatus = httpStatus;
|
|
970
|
-
} else if (
|
|
971
|
-
this._jwpError = responseErrorObj;
|
|
969
|
+
} else if (Object.hasOwn(responseErrorObj, 'status')) {
|
|
970
|
+
this._jwpError = responseErrorObj as typeof this._jwpError;
|
|
972
971
|
}
|
|
973
972
|
}
|
|
974
973
|
|
|
975
974
|
private static _parseHttpResponse(data: any): [Record<string, any>, string] {
|
|
976
975
|
let responseErrorObj: Record<string, any> = util.safeJsonParse(data);
|
|
977
|
-
if (!
|
|
976
|
+
if (!util.isPlainObject(responseErrorObj)) {
|
|
978
977
|
responseErrorObj = {};
|
|
979
978
|
}
|
|
980
|
-
let errorMessage: string =
|
|
981
|
-
if (
|
|
979
|
+
let errorMessage: string = typeof data === 'string' ? data : '';
|
|
980
|
+
if (typeof responseErrorObj.value === 'string') {
|
|
982
981
|
errorMessage = responseErrorObj.value;
|
|
983
|
-
} else if (
|
|
982
|
+
} else if (typeof responseErrorObj.value?.message === 'string') {
|
|
984
983
|
errorMessage = responseErrorObj.value.message;
|
|
985
984
|
}
|
|
986
985
|
return [responseErrorObj, errorMessage];
|
|
@@ -991,7 +990,7 @@ export class ProxyRequestError extends BaseError {
|
|
|
991
990
|
// If it's MJSONWP error, returns actual error cause for request failure based on `jsonwp.status`
|
|
992
991
|
return errorFromMJSONWPStatusCode(this._jwpError.status, this._jwpError.value);
|
|
993
992
|
}
|
|
994
|
-
if (util.hasValue(this._w3cError) &&
|
|
993
|
+
if (util.hasValue(this._w3cError) && typeof this._w3cErrorStatus === 'number' && this._w3cErrorStatus >= 300) {
|
|
995
994
|
return errorFromW3CJsonCode(
|
|
996
995
|
this._w3cError.error,
|
|
997
996
|
this._w3cError.message || this.message,
|
|
@@ -1007,10 +1006,10 @@ function generateBadParametersMessage(
|
|
|
1007
1006
|
paramNames: string[]
|
|
1008
1007
|
): string {
|
|
1009
1008
|
const toArray = function <T> (x: T | T[]): T[] {
|
|
1010
|
-
if (
|
|
1009
|
+
if (x === undefined) {
|
|
1011
1010
|
return [];
|
|
1012
1011
|
}
|
|
1013
|
-
if (
|
|
1012
|
+
if (Array.isArray(x)) {
|
|
1014
1013
|
return x;
|
|
1015
1014
|
}
|
|
1016
1015
|
return [x];
|
|
@@ -1018,26 +1017,28 @@ function generateBadParametersMessage(
|
|
|
1018
1017
|
|
|
1019
1018
|
const requiredParamNames = toArray(paramRequirements.required);
|
|
1020
1019
|
const actualParamNames = toArray(paramNames);
|
|
1021
|
-
const missingRequiredParamNames =
|
|
1020
|
+
const missingRequiredParamNames = requiredParamNames.filter((name) => !actualParamNames.includes(name));
|
|
1022
1021
|
const resultLines: string[] = [];
|
|
1023
1022
|
resultLines.push(
|
|
1024
|
-
|
|
1023
|
+
util.isEmpty(missingRequiredParamNames)
|
|
1025
1024
|
? // This should not happen
|
|
1026
1025
|
'Some of the provided parameters are not known'
|
|
1027
1026
|
: `The following required parameter${
|
|
1028
1027
|
missingRequiredParamNames.length === 1 ? ' is' : 's are'
|
|
1029
1028
|
} missing: ${JSON.stringify(missingRequiredParamNames)}`,
|
|
1030
1029
|
);
|
|
1031
|
-
if (!
|
|
1030
|
+
if (!util.isEmpty(requiredParamNames)) {
|
|
1032
1031
|
resultLines.push(`Known required parameters are: ${JSON.stringify(requiredParamNames)}`);
|
|
1033
1032
|
}
|
|
1034
|
-
const optionalParamNames =
|
|
1035
|
-
|
|
1033
|
+
const optionalParamNames = toArray(paramRequirements.optional).filter(
|
|
1034
|
+
(name): name is string => typeof name === 'string' && !['sessionId', 'id'].includes(name)
|
|
1035
|
+
);
|
|
1036
|
+
if (!util.isEmpty(optionalParamNames)) {
|
|
1036
1037
|
resultLines.push(`Known optional parameters are: ${JSON.stringify(optionalParamNames)}`);
|
|
1037
1038
|
}
|
|
1038
1039
|
resultLines.push(
|
|
1039
1040
|
`You have provided${
|
|
1040
|
-
|
|
1041
|
+
util.isEmpty(actualParamNames) ? ' none' : ': ' + JSON.stringify(paramNames)
|
|
1041
1042
|
}`,
|
|
1042
1043
|
);
|
|
1043
1044
|
return resultLines.join('\n');
|
|
@@ -1087,7 +1088,7 @@ export const errors = {
|
|
|
1087
1088
|
ProxyRequestError,
|
|
1088
1089
|
} as const;
|
|
1089
1090
|
|
|
1090
|
-
const jsonwpErrorCodeMap: Record<string, Class<ProtocolError>> =
|
|
1091
|
+
const jsonwpErrorCodeMap: Record<string, Class<ProtocolError>> = Object.values(errors)
|
|
1091
1092
|
.reduce((acc: Record<string, Class<ProtocolError>>, ErrorClass: any) => {
|
|
1092
1093
|
if ('code' in ErrorClass) {
|
|
1093
1094
|
acc[ErrorClass.code()] = ErrorClass;
|
|
@@ -1095,7 +1096,7 @@ const jsonwpErrorCodeMap: Record<string, Class<ProtocolError>> = _.values(errors
|
|
|
1095
1096
|
return acc;
|
|
1096
1097
|
}, {});
|
|
1097
1098
|
|
|
1098
|
-
const w3cErrorCodeMap: Record<string, Class<ProtocolError>> =
|
|
1099
|
+
const w3cErrorCodeMap: Record<string, Class<ProtocolError>> = Object.values(errors)
|
|
1099
1100
|
.reduce((acc: Record<string, Class<ProtocolError>>, ErrorClass: any) => {
|
|
1100
1101
|
if ('error' in ErrorClass) {
|
|
1101
1102
|
acc[ErrorClass.error()] = ErrorClass;
|
|
@@ -1151,7 +1152,7 @@ export function errorFromMJSONWPStatusCode(code: number, value: string | {messag
|
|
|
1151
1152
|
* @return The error that is associated with the W3C error string
|
|
1152
1153
|
*/
|
|
1153
1154
|
export function errorFromW3CJsonCode(signature: string, message: string, stacktrace?: string): ProtocolError {
|
|
1154
|
-
const ErrorClass = w3cErrorCodeMap[
|
|
1155
|
+
const ErrorClass = w3cErrorCodeMap[signature.toLowerCase()] ?? UnknownError;
|
|
1155
1156
|
w3cLog.debug(`Matched W3C error code '${signature}' to ${ErrorClass.name}`);
|
|
1156
1157
|
const resultError = new ErrorClass(message);
|
|
1157
1158
|
resultError.stacktrace = stacktrace;
|
|
@@ -1177,12 +1178,12 @@ export function getResponseForW3CError(err: any): [number, { value: W3CError }]
|
|
|
1177
1178
|
];
|
|
1178
1179
|
|
|
1179
1180
|
// err is ProtocolError
|
|
1180
|
-
if (['error', 'w3cStatus'].every((prop) =>
|
|
1181
|
+
if (['error', 'w3cStatus'].every((prop) => Object.hasOwn(err, prop))) {
|
|
1181
1182
|
return protocolErrorToResponse(err);
|
|
1182
1183
|
}
|
|
1183
1184
|
|
|
1184
1185
|
// err is ProxyRequestError
|
|
1185
|
-
if (
|
|
1186
|
+
if (Object.hasOwn(err, 'getActualError') && typeof err.getActualError === 'function') {
|
|
1186
1187
|
return protocolErrorToResponse(err.getActualError());
|
|
1187
1188
|
}
|
|
1188
1189
|
|
package/lib/protocol/helpers.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {util} from '@appium/support';
|
|
2
2
|
import {duplicateKeys} from '../basedriver/helpers';
|
|
3
3
|
import {MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY} from '../constants';
|
|
4
4
|
|
|
@@ -11,7 +11,7 @@ import {MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY} from '../constants';
|
|
|
11
11
|
* @returns Either modified value or the same one if nothing has been modified
|
|
12
12
|
*/
|
|
13
13
|
export function formatResponseValue(resValue: object | undefined): object | null {
|
|
14
|
-
if (
|
|
14
|
+
if (resValue === undefined) {
|
|
15
15
|
// convert undefined to null
|
|
16
16
|
return null;
|
|
17
17
|
}
|
|
@@ -30,7 +30,11 @@ export function formatResponseValue(resValue: object | undefined): object | null
|
|
|
30
30
|
* @returns The fixed response body
|
|
31
31
|
*/
|
|
32
32
|
export function ensureW3cResponse(responseBody: Record<string, unknown>): Record<string, unknown> {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
if (!util.isPlainObject(responseBody)) {
|
|
34
|
+
return responseBody;
|
|
35
|
+
}
|
|
36
|
+
const result = {...responseBody};
|
|
37
|
+
delete result.status;
|
|
38
|
+
delete result.sessionId;
|
|
39
|
+
return result;
|
|
36
40
|
}
|