@appium/base-driver 10.2.1 → 10.3.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.
@@ -0,0 +1,284 @@
1
+ import type {AppiumLogger, HTTPBody, ProxyResponse} from '@appium/types';
2
+ import _ from 'lodash';
3
+ import {logger, util} from '@appium/support';
4
+ import {duplicateKeys} from '../basedriver/helpers';
5
+ import {MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY, PROTOCOLS} from '../constants';
6
+
7
+ export type ProxyFunction = (
8
+ url: string,
9
+ method: string,
10
+ body?: HTTPBody
11
+ ) => Promise<[ProxyResponse, HTTPBody]>;
12
+
13
+ export const COMMAND_URLS_CONFLICTS = [
14
+ {
15
+ commandNames: ['execute', 'executeAsync'],
16
+ jsonwpConverter: (url: string) =>
17
+ url.replace(/\/execute.*/, url.includes('async') ? '/execute_async' : '/execute'),
18
+ w3cConverter: (url: string) =>
19
+ url.replace(/\/execute.*/, url.includes('async') ? '/execute/async' : '/execute/sync'),
20
+ },
21
+ {
22
+ commandNames: ['getElementScreenshot'],
23
+ jsonwpConverter: (url: string) => url.replace(/\/element\/([^/]+)\/screenshot$/, '/screenshot/$1'),
24
+ w3cConverter: (url: string) => url.replace(/\/screenshot\/([^/]+)/, '/element/$1/screenshot'),
25
+ },
26
+ {
27
+ commandNames: ['getWindowHandles', 'getWindowHandle'],
28
+ jsonwpConverter(url: string) {
29
+ return url.endsWith('/window')
30
+ ? url.replace(/\/window$/, '/window_handle')
31
+ : url.replace(/\/window\/handle(s?)$/, '/window_handle$1');
32
+ },
33
+ w3cConverter(url: string) {
34
+ return url.endsWith('/window_handle')
35
+ ? url.replace(/\/window_handle$/, '/window')
36
+ : url.replace(/\/window_handles$/, '/window/handles');
37
+ },
38
+ },
39
+ {
40
+ commandNames: ['getProperty'],
41
+ jsonwpConverter: (w3cUrl: string) => {
42
+ const w3cPropertyRegex = /\/element\/([^/]+)\/property\/([^/]+)/;
43
+ return w3cUrl.replace(w3cPropertyRegex, '/element/$1/attribute/$2');
44
+ },
45
+ // Don't convert JSONWP URL to W3C. W3C accepts /attribute and /property
46
+ w3cConverter: (jsonwpUrl: string) => jsonwpUrl,
47
+ },
48
+ ] as const;
49
+
50
+ const {MJSONWP, W3C} = PROTOCOLS;
51
+ const DEFAULT_LOG = logger.getLogger('Protocol Converter');
52
+
53
+ export class ProtocolConverter {
54
+ private _downstreamProtocol: string | null | undefined = null;
55
+ private readonly _log: AppiumLogger | null;
56
+
57
+ /**
58
+ * @param proxyFunc - Function to perform the actual proxy request
59
+ * @param log - Logger instance, or null to use the default
60
+ */
61
+ constructor(
62
+ public proxyFunc: ProxyFunction,
63
+ log: AppiumLogger | null = null
64
+ ) {
65
+ this._log = log;
66
+ }
67
+
68
+ get log(): AppiumLogger {
69
+ return this._log ?? DEFAULT_LOG;
70
+ }
71
+
72
+ set downstreamProtocol(value: string | null | undefined) {
73
+ this._downstreamProtocol = value;
74
+ }
75
+
76
+ get downstreamProtocol(): string | null | undefined {
77
+ return this._downstreamProtocol;
78
+ }
79
+
80
+ /**
81
+ * Handle "crossing" endpoints for the case when upstream and downstream
82
+ * drivers operate different protocols.
83
+ */
84
+ async convertAndProxy(
85
+ commandName: string,
86
+ url: string,
87
+ method: string,
88
+ body?: HTTPBody
89
+ ): Promise<[ProxyResponse, HTTPBody]> {
90
+ if (!this.downstreamProtocol) {
91
+ return await this.proxyFunc(url, method, body);
92
+ }
93
+
94
+ // Same url, but different arguments
95
+ switch (commandName) {
96
+ case 'timeouts':
97
+ return await this.proxySetTimeouts(url, method, body);
98
+ case 'setWindow':
99
+ return await this.proxySetWindow(url, method, body);
100
+ case 'setValue':
101
+ return await this.proxySetValue(url, method, body);
102
+ case 'performActions':
103
+ return await this.proxyPerformActions(url, method, body);
104
+ case 'releaseActions':
105
+ return await this.proxyReleaseActions(url, method);
106
+ case 'setFrame':
107
+ return await this.proxySetFrame(url, method, body);
108
+ default:
109
+ break;
110
+ }
111
+
112
+ // Same arguments, but different URLs
113
+ for (const {commandNames, jsonwpConverter, w3cConverter} of COMMAND_URLS_CONFLICTS) {
114
+ if (!(commandNames as readonly string[]).includes(commandName)) {
115
+ continue;
116
+ }
117
+ const rewrittenUrl =
118
+ this.downstreamProtocol === MJSONWP ? jsonwpConverter(url) : w3cConverter(url);
119
+ if (rewrittenUrl === url) {
120
+ this.log.debug(
121
+ `Did not know how to rewrite the original URL '${url}' for ${this.downstreamProtocol} protocol`
122
+ );
123
+ break;
124
+ }
125
+ this.log.info(
126
+ `Rewrote the original URL '${url}' to '${rewrittenUrl}' for ${this.downstreamProtocol} protocol`
127
+ );
128
+ return await this.proxyFunc(rewrittenUrl, method, body);
129
+ }
130
+
131
+ // No matches found. Proceed normally
132
+ return await this.proxyFunc(url, method, body);
133
+ }
134
+
135
+ /**
136
+ * W3C /timeouts can take as many as 3 timeout types at once, MJSONWP /timeouts only takes one
137
+ * at a time. So if we're using W3C and proxying to MJSONWP and there's more than one timeout type
138
+ * provided in the request, we need to do 3 proxies and combine the result.
139
+ */
140
+ private getTimeoutRequestObjects(body: HTTPBody): Record<string, unknown>[] {
141
+ if (_.isNil(body)) {
142
+ return [];
143
+ }
144
+
145
+ const bodyObj = (util.safeJsonParse(body) as Record<string, unknown>) ?? {};
146
+ if (this.downstreamProtocol === W3C && _.has(bodyObj, 'ms') && _.has(bodyObj, 'type')) {
147
+ const typeToW3C = (x: string) => (x === 'page load' ? 'pageLoad' : x);
148
+ return [
149
+ {
150
+ [typeToW3C(bodyObj.type as string)]: bodyObj.ms,
151
+ },
152
+ ];
153
+ }
154
+
155
+ if (this.downstreamProtocol === MJSONWP && (!_.has(bodyObj, 'ms') || !_.has(bodyObj, 'type'))) {
156
+ const typeToJSONWP = (x: string) => (x === 'pageLoad' ? 'page load' : x);
157
+ return _.toPairs(bodyObj)
158
+ // Only transform the entry if ms value is a valid positive float number
159
+ .filter((pair) => /^\d+(?:[.,]\d*?)?$/.test(`${pair[1]}`))
160
+ .map((pair) => ({
161
+ type: typeToJSONWP(pair[0]),
162
+ ms: pair[1],
163
+ }));
164
+ }
165
+
166
+ return [bodyObj];
167
+ }
168
+
169
+ /**
170
+ * Proxy an array of timeout objects and merge the result.
171
+ */
172
+ private async proxySetTimeouts(
173
+ url: string,
174
+ method: string,
175
+ body?: HTTPBody
176
+ ): Promise<[ProxyResponse, HTTPBody]> {
177
+ const timeoutRequestObjects = this.getTimeoutRequestObjects(body);
178
+ if (timeoutRequestObjects.length === 0) {
179
+ return await this.proxyFunc(url, method, body);
180
+ }
181
+ this.log.debug(
182
+ `Will send the following request bodies to /timeouts: ${JSON.stringify(timeoutRequestObjects)}`
183
+ );
184
+
185
+ let response!: ProxyResponse;
186
+ let resBody!: HTTPBody;
187
+ for (const timeoutObj of timeoutRequestObjects) {
188
+ [response, resBody] = await this.proxyFunc(url, method, timeoutObj as HTTPBody);
189
+
190
+ // If we got a non-MJSONWP response, return the result, nothing left to do
191
+ if (this.downstreamProtocol !== MJSONWP) {
192
+ return [response, resBody];
193
+ }
194
+ // If we got an error, return the error right away
195
+ if (response.statusCode >= 400) {
196
+ return [response, resBody];
197
+ }
198
+ // ...Otherwise, continue to the next timeouts call
199
+ }
200
+ return [response, resBody];
201
+ }
202
+
203
+ private async proxySetWindow(
204
+ url: string,
205
+ method: string,
206
+ body: HTTPBody
207
+ ): Promise<[ProxyResponse, HTTPBody]> {
208
+ const bodyObj = util.safeJsonParse(body);
209
+ if (_.isPlainObject(bodyObj)) {
210
+ const obj = bodyObj as Record<string, unknown>;
211
+ if (this.downstreamProtocol === W3C && _.has(bodyObj, 'name') && !_.has(bodyObj, 'handle')) {
212
+ this.log.debug(`Copied 'name' value '${obj.name}' to 'handle' as per W3C spec`);
213
+ return await this.proxyFunc(url, method, {...obj, handle: obj.name});
214
+ }
215
+ if (
216
+ this.downstreamProtocol === MJSONWP &&
217
+ _.has(bodyObj, 'handle') &&
218
+ !_.has(bodyObj, 'name')
219
+ ) {
220
+ this.log.debug(`Copied 'handle' value '${obj.handle}' to 'name' as per JSONWP spec`);
221
+ return await this.proxyFunc(url, method, {...obj, name: obj.handle});
222
+ }
223
+ }
224
+ return await this.proxyFunc(url, method, body);
225
+ }
226
+
227
+ private async proxySetValue(
228
+ url: string,
229
+ method: string,
230
+ body: HTTPBody
231
+ ): Promise<[ProxyResponse, HTTPBody]> {
232
+ const bodyObj = util.safeJsonParse(body) as Record<string, unknown> | undefined;
233
+ if (_.isPlainObject(bodyObj) && (util.hasValue(bodyObj?.text) || util.hasValue(bodyObj?.value))) {
234
+ let {text, value} = bodyObj;
235
+ if (util.hasValue(text) && !util.hasValue(value)) {
236
+ value = _.isString(text) ? [...text] : _.isArray(text) ? text : [];
237
+ this.log.debug(`Added 'value' property to 'setValue' request body`);
238
+ } else if (!util.hasValue(text) && util.hasValue(value)) {
239
+ text = _.isArray(value) ? value.join('') : _.isString(value) ? value : '';
240
+ this.log.debug(`Added 'text' property to 'setValue' request body`);
241
+ }
242
+ return await this.proxyFunc(url, method, {...bodyObj, text, value});
243
+ }
244
+ return await this.proxyFunc(url, method, body);
245
+ }
246
+
247
+ private async proxySetFrame(
248
+ url: string,
249
+ method: string,
250
+ body: HTTPBody
251
+ ): Promise<[ProxyResponse, HTTPBody]> {
252
+ const bodyObj = util.safeJsonParse(body);
253
+ if (_.has(bodyObj, 'id') && _.isPlainObject(bodyObj.id)) {
254
+ return await this.proxyFunc(url, method, {
255
+ ...(bodyObj as object),
256
+ id: duplicateKeys(bodyObj.id as object, MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY),
257
+ });
258
+ }
259
+ return await this.proxyFunc(url, method, body);
260
+ }
261
+
262
+ private async proxyPerformActions(
263
+ url: string,
264
+ method: string,
265
+ body: HTTPBody
266
+ ): Promise<[ProxyResponse, HTTPBody]> {
267
+ const bodyObj = util.safeJsonParse(body);
268
+ if (_.isPlainObject(bodyObj)) {
269
+ return await this.proxyFunc(
270
+ url,
271
+ method,
272
+ duplicateKeys(bodyObj as object, MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY)
273
+ );
274
+ }
275
+ return await this.proxyFunc(url, method, body);
276
+ }
277
+
278
+ private async proxyReleaseActions(
279
+ url: string,
280
+ method: string
281
+ ): Promise<[ProxyResponse, HTTPBody]> {
282
+ return await this.proxyFunc(url, method);
283
+ }
284
+ }