@appium/base-driver 8.2.2 → 8.2.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.
@@ -145,7 +145,10 @@ function parseCaps (caps, constraints = {}, shouldValidateCaps = true) {
145
145
  }
146
146
 
147
147
  // If an empty array as provided, we'll be forgiving and make it an array of one empty object
148
+ // In the future, reject 'firstMatch' argument if its array did not have one or more entries (#3.2)
148
149
  if (allFirstMatchCaps.length === 0) {
150
+ log.warn(`The firstMatch array in the given capabilities has no entries. Adding an empty entry fo rnow, ` +
151
+ `but it will require one or more entries as W3C spec.`);
149
152
  allFirstMatchCaps.push({});
150
153
  }
151
154
 
@@ -1,5 +1,5 @@
1
1
  import {
2
- Protocol, errors, determineProtocol
2
+ Protocol, errors, determineProtocol, DELETE_SESSION_COMMAND,
3
3
  } from '../protocol';
4
4
  import { fs } from '@appium/support';
5
5
  import { PROTOCOLS, DEFAULT_BASE_PATH } from '../constants';
@@ -311,7 +311,7 @@ class BaseDriver extends Protocol {
311
311
  // If creating a session determine if W3C or MJSONWP protocol was requested and remember the choice
312
312
  this.protocol = determineProtocol(...args);
313
313
  this.logEvent(EVENT_SESSION_INIT);
314
- } else if (cmd === 'deleteSession') {
314
+ } else if (cmd === DELETE_SESSION_COMMAND) {
315
315
  this.logEvent(EVENT_SESSION_QUIT_START);
316
316
  }
317
317
 
@@ -352,7 +352,7 @@ class BaseDriver extends Protocol {
352
352
  // automatic session deletion in this.onCommandTimeout. Of course we don't
353
353
  // want to trigger the timer when the user is shutting down the session
354
354
  // intentionally
355
- if (this.isCommandsQueueEnabled && cmd !== 'deleteSession') {
355
+ if (this.isCommandsQueueEnabled && cmd !== DELETE_SESSION_COMMAND) {
356
356
  // resetting existing timeout
357
357
  this.startNewCommandTimeout();
358
358
  }
@@ -362,7 +362,7 @@ class BaseDriver extends Protocol {
362
362
  this._eventHistory.commands.push({cmd, startTime, endTime});
363
363
  if (cmd === 'createSession') {
364
364
  this.logEvent(EVENT_SESSION_START);
365
- } else if (cmd === 'deleteSession') {
365
+ } else if (cmd === DELETE_SESSION_COMMAND) {
366
366
  this.logEvent(EVENT_SESSION_QUIT_DONE);
367
367
  }
368
368
 
@@ -18,13 +18,14 @@ const CACHED_APPS_MAX_AGE = 1000 * 60 * 60 * 24; // ms
18
18
  const APPLICATIONS_CACHE = new LRU({
19
19
  maxAge: CACHED_APPS_MAX_AGE, // expire after 24 hours
20
20
  updateAgeOnGet: true,
21
- dispose: async (app, {fullPath}) => {
22
- if (!await fs.exists(fullPath)) {
23
- return;
24
- }
25
-
26
- logger.info(`The application '${app}' cached at '${fullPath}' has expired`);
27
- await fs.rimraf(fullPath);
21
+ dispose: (app, {fullPath}) => {
22
+ logger.info(`The application '${app}' cached at '${fullPath}' has ` +
23
+ `expired after ${CACHED_APPS_MAX_AGE}ms`);
24
+ setTimeout(async () => {
25
+ if (fullPath) {
26
+ await fs.rimraf(fullPath);
27
+ }
28
+ });
28
29
  },
29
30
  noDisposeOnSet: true,
30
31
  });
@@ -66,54 +67,57 @@ async function retrieveHeaders (link) {
66
67
  return {};
67
68
  }
68
69
 
69
- function getCachedApplicationPath (link, currentAppProps = {}) {
70
+ function getCachedApplicationPath (link, currentAppProps = {}, cachedAppInfo = {}) {
70
71
  const refresh = () => {
71
72
  logger.debug(`A fresh copy of the application is going to be downloaded from ${link}`);
72
73
  return null;
73
74
  };
74
75
 
75
- if (APPLICATIONS_CACHE.has(link)) {
76
- const {
77
- lastModified: currentModified,
78
- immutable: currentImmutable,
79
- // maxAge is in seconds
80
- maxAge: currentMaxAge,
81
- } = currentAppProps;
82
- const {
83
- // Date instance
84
- lastModified,
85
- // boolean
86
- immutable,
87
- // Unix time in milliseconds
88
- timestamp,
89
- fullPath,
90
- } = APPLICATIONS_CACHE.get(link);
91
- if (lastModified && currentModified) {
92
- if (currentModified.getTime() <= lastModified.getTime()) {
93
- logger.debug(`The application at ${link} has not been modified since ${lastModified}`);
94
- return fullPath;
95
- }
96
- logger.debug(`The application at ${link} has been modified since ${lastModified}`);
97
- return refresh();
98
- }
99
- if (immutable && currentImmutable) {
100
- logger.debug(`The application at ${link} is immutable`);
76
+ if (!_.isPlainObject(cachedAppInfo) || !_.isPlainObject(currentAppProps)) {
77
+ // if an invalid arg is passed then assume cache miss
78
+ return refresh();
79
+ }
80
+
81
+ const {
82
+ lastModified: currentModified,
83
+ immutable: currentImmutable,
84
+ // maxAge is in seconds
85
+ maxAge: currentMaxAge,
86
+ } = currentAppProps;
87
+ const {
88
+ // Date instance
89
+ lastModified,
90
+ // boolean
91
+ immutable,
92
+ // Unix time in milliseconds
93
+ timestamp,
94
+ fullPath,
95
+ } = cachedAppInfo;
96
+ if (lastModified && currentModified) {
97
+ if (currentModified.getTime() <= lastModified.getTime()) {
98
+ logger.debug(`The application at ${link} has not been modified since ${lastModified}`);
101
99
  return fullPath;
102
100
  }
103
- if (currentMaxAge && timestamp) {
104
- const msLeft = timestamp + currentMaxAge * 1000 - Date.now();
105
- if (msLeft > 0) {
106
- logger.debug(`The cached application '${path.basename(fullPath)}' will expire in ${msLeft / 1000}s`);
107
- return fullPath;
108
- }
109
- logger.debug(`The cached application '${path.basename(fullPath)}' has expired`);
101
+ logger.debug(`The application at ${link} has been modified since ${lastModified}`);
102
+ return refresh();
103
+ }
104
+ if (immutable && currentImmutable) {
105
+ logger.debug(`The application at ${link} is immutable`);
106
+ return fullPath;
107
+ }
108
+ if (currentMaxAge && timestamp) {
109
+ const msLeft = timestamp + currentMaxAge * 1000 - Date.now();
110
+ if (msLeft > 0) {
111
+ logger.debug(`The cached application '${path.basename(fullPath)}' will expire in ${msLeft / 1000}s`);
112
+ return fullPath;
110
113
  }
114
+ logger.debug(`The cached application '${path.basename(fullPath)}' has expired`);
111
115
  }
112
116
  return refresh();
113
117
  }
114
118
 
115
119
  function verifyAppExtension (app, supportedAppExtensions) {
116
- if (supportedAppExtensions.includes(path.extname(app))) {
120
+ if (supportedAppExtensions.map(_.toLower).includes(_.toLower(path.extname(app)))) {
117
121
  return app;
118
122
  }
119
123
  throw new Error(`New app path '${app}' did not have ` +
@@ -121,18 +125,104 @@ function verifyAppExtension (app, supportedAppExtensions) {
121
125
  supportedAppExtensions);
122
126
  }
123
127
 
124
- async function configureApp (app, supportedAppExtensions) {
128
+ async function calculateFolderIntegrity (folderPath) {
129
+ return (await fs.glob('**/*', {cwd: folderPath, strict: false, nosort: true})).length;
130
+ }
131
+
132
+ async function calculateFileIntegrity (filePath) {
133
+ return await fs.hash(filePath);
134
+ }
135
+
136
+ async function isAppIntegrityOk (currentPath, expectedIntegrity = {}) {
137
+ if (!await fs.exists(currentPath)) {
138
+ return false;
139
+ }
140
+
141
+ // Folder integrity check is simple:
142
+ // Verify the previous amount of files is not greater than the current one.
143
+ // We don't want to use equality comparison because of an assumption that the OS might
144
+ // create some unwanted service files/cached inside of that folder or its subfolders.
145
+ // Ofc, validating the hash sum of each file (or at least of file path) would be much
146
+ // more precise, but we don't need to be very precise here and also don't want to
147
+ // overuse RAM and have a performance drop.
148
+ return (await fs.stat(currentPath)).isDirectory()
149
+ ? await calculateFolderIntegrity(currentPath) >= expectedIntegrity?.folder
150
+ : await calculateFileIntegrity(currentPath) === expectedIntegrity?.file;
151
+ }
152
+
153
+ /**
154
+ * @typedef {Object} PostProcessOptions
155
+ * @property {?Object} cachedAppInfo The information about the previously cached app instance (if exists):
156
+ * - packageHash: SHA1 hash of the package if it is a file and not a folder
157
+ * - lastModified: Optional Date instance, the value of file's `Last-Modified` header
158
+ * - immutable: Optional boolean value. Contains true if the file has an `immutable` mark
159
+ * in `Cache-control` header
160
+ * - maxAge: Optional integer representation of `maxAge` parameter in `Cache-control` header
161
+ * - timestamp: The timestamp this item has been added to the cache (measured in Unix epoch
162
+ * milliseconds)
163
+ * - integrity: An object containing either `file` property with SHA1 hash of the file
164
+ * or `folder` property with total amount of cached files and subfolders
165
+ * - fullPath: the full path to the cached app
166
+ * @property {boolean} isUrl Whether the app has been downloaded from a remote URL
167
+ * @property {?Object} headers Optional headers object. Only present if `isUrl` is true and if the server
168
+ * responds to HEAD requests. All header names are normalized to lowercase.
169
+ * @property {string} appPath A string containing full path to the preprocessed application package (either
170
+ * downloaded or a local one)
171
+ */
172
+
173
+ /**
174
+ * @typedef {Object} PostProcessResult
175
+ * @property {string} appPath The full past to the post-processed application package on the
176
+ * local file system (might be a file or a folder path)
177
+ */
178
+
179
+ /**
180
+ * @typedef {Object} ConfigureAppOptions
181
+ * @property {(obj: PostProcessOptions) => (Promise<PostProcessResult|undefined>|PostProcessResult|undefined)} onPostProcess
182
+ * Optional function, which should be applied
183
+ * to the application after it is downloaded/preprocessed. This function may be async
184
+ * and is expected to accept single object parameter.
185
+ * The function is expected to either return a falsy value, which means the app must not be
186
+ * cached and a fresh copy of it is downloaded each time. If this function returns an object
187
+ * containing `appPath` property then the integrity of it will be verified and stored into
188
+ * the cache.
189
+ * @property {string[]} supportedExtensions List of supported application extensions (
190
+ * including starting dots). This property is mandatory and must not be empty.
191
+ */
192
+
193
+ /**
194
+ * Prepares an app to be used in an automated test. The app gets cached automatically
195
+ * if it is an archive or if it is downloaded from an URL.
196
+ *
197
+ * @param {string} app Either a full path to the app or a remote URL
198
+ * @param {string|string[]|ConfigureAppOptions} options
199
+ * @returns The full path to the resulting application bundle
200
+ */
201
+ async function configureApp (app, options = {}) {
125
202
  if (!_.isString(app)) {
126
203
  // immediately shortcircuit if not given an app
127
204
  return;
128
205
  }
129
- if (!_.isArray(supportedAppExtensions)) {
130
- supportedAppExtensions = [supportedAppExtensions];
206
+
207
+ let supportedAppExtensions;
208
+ const {
209
+ onPostProcess,
210
+ } = _.isPlainObject(options) ? options : {};
211
+ if (_.isString(options)) {
212
+ supportedAppExtensions = [options];
213
+ } else if (_.isArray(options)) {
214
+ supportedAppExtensions = options;
215
+ } else if (_.isPlainObject(options)) {
216
+ supportedAppExtensions = options.supportedExtensions;
217
+ }
218
+ if (_.isEmpty(supportedAppExtensions)) {
219
+ throw new Error(`One or more supported app extensions must be provided`);
131
220
  }
132
221
 
133
222
  let newApp = app;
134
223
  let shouldUnzipApp = false;
135
- let archiveHash = null;
224
+ let packageHash = null;
225
+ let headers = null;
136
226
  const remoteAppProps = {
137
227
  lastModified: null,
138
228
  immutable: false,
@@ -141,11 +231,13 @@ async function configureApp (app, supportedAppExtensions) {
141
231
  const {protocol, pathname} = url.parse(newApp);
142
232
  const isUrl = ['http:', 'https:'].includes(protocol);
143
233
 
234
+ const cachedAppInfo = APPLICATIONS_CACHE.get(app);
235
+
144
236
  return await APPLICATIONS_CACHE_GUARD.acquire(app, async () => {
145
237
  if (isUrl) {
146
238
  // Use the app from remote URL
147
239
  logger.info(`Using downloadable app '${newApp}'`);
148
- const headers = await retrieveHeaders(newApp);
240
+ headers = await retrieveHeaders(newApp);
149
241
  if (!_.isEmpty(headers)) {
150
242
  if (headers['last-modified']) {
151
243
  remoteAppProps.lastModified = new Date(headers['last-modified']);
@@ -160,13 +252,14 @@ async function configureApp (app, supportedAppExtensions) {
160
252
  }
161
253
  logger.debug(`Cache-Control: ${headers['cache-control']}`);
162
254
  }
163
- const cachedPath = getCachedApplicationPath(app, remoteAppProps);
255
+ const cachedPath = getCachedApplicationPath(app, remoteAppProps, cachedAppInfo);
164
256
  if (cachedPath) {
165
- if (await fs.exists(cachedPath)) {
257
+ if (await isAppIntegrityOk(cachedPath, cachedAppInfo?.integrity)) {
166
258
  logger.info(`Reusing previously downloaded application at '${cachedPath}'`);
167
259
  return verifyAppExtension(cachedPath, supportedAppExtensions);
168
260
  }
169
- logger.info(`The application at '${cachedPath}' does not exist anymore. Deleting it from the cache`);
261
+ logger.info(`The application at '${cachedPath}' does not exist anymore ` +
262
+ `or its integrity has been damaged. Deleting it from the internal cache`);
170
263
  APPLICATIONS_CACHE.del(app);
171
264
  }
172
265
 
@@ -234,19 +327,24 @@ async function configureApp (app, supportedAppExtensions) {
234
327
  throw new Error(errorMessage);
235
328
  }
236
329
 
237
- if (shouldUnzipApp) {
330
+ const isPackageAFile = (await fs.stat(newApp)).isFile();
331
+ if (isPackageAFile) {
332
+ packageHash = await calculateFileIntegrity(newApp);
333
+ }
334
+
335
+ if (isPackageAFile && shouldUnzipApp && !_.isFunction(onPostProcess)) {
238
336
  const archivePath = newApp;
239
- archiveHash = await fs.hash(archivePath);
240
- if (APPLICATIONS_CACHE.has(app) && archiveHash === APPLICATIONS_CACHE.get(app).hash) {
241
- const {fullPath} = APPLICATIONS_CACHE.get(app);
242
- if (await fs.exists(fullPath)) {
337
+ if (packageHash === cachedAppInfo?.packageHash) {
338
+ const {fullPath} = cachedAppInfo;
339
+ if (await isAppIntegrityOk(fullPath, cachedAppInfo?.integrity)) {
243
340
  if (archivePath !== app) {
244
341
  await fs.rimraf(archivePath);
245
342
  }
246
343
  logger.info(`Will reuse previously cached application at '${fullPath}'`);
247
344
  return verifyAppExtension(fullPath, supportedAppExtensions);
248
345
  }
249
- logger.info(`The application at '${fullPath}' does not exist anymore. Deleting it from the cache`);
346
+ logger.info(`The application at '${fullPath}' does not exist anymore ` +
347
+ `or its integrity has been damaged. Deleting it from the cache`);
250
348
  APPLICATIONS_CACHE.del(app);
251
349
  }
252
350
  const tmpRoot = await tempDir.openDir();
@@ -265,24 +363,43 @@ async function configureApp (app, supportedAppExtensions) {
265
363
  app = newApp;
266
364
  }
267
365
 
268
- verifyAppExtension(newApp, supportedAppExtensions);
269
-
270
- if (app !== newApp && (archiveHash || _.values(remoteAppProps).some(Boolean))) {
271
- if (APPLICATIONS_CACHE.has(app)) {
272
- const {fullPath} = APPLICATIONS_CACHE.get(app);
273
- // Clean up the obsolete entry first if needed
274
- if (fullPath !== newApp && await fs.exists(fullPath)) {
275
- await fs.rimraf(fullPath);
276
- }
366
+ const storeAppInCache = async (appPathToCache) => {
367
+ const cachedFullPath = cachedAppInfo?.fullPath;
368
+ if (cachedFullPath && cachedFullPath !== appPathToCache) {
369
+ await fs.rimraf(cachedFullPath);
370
+ }
371
+ const integrity = {};
372
+ if ((await fs.stat(appPathToCache)).isDirectory()) {
373
+ integrity.folder = await calculateFolderIntegrity(appPathToCache);
374
+ } else {
375
+ integrity.file = await calculateFileIntegrity(appPathToCache);
277
376
  }
278
377
  APPLICATIONS_CACHE.set(app, {
279
378
  ...remoteAppProps,
280
379
  timestamp: Date.now(),
281
- hash: archiveHash,
282
- fullPath: newApp,
380
+ packageHash,
381
+ integrity,
382
+ fullPath: appPathToCache,
383
+ });
384
+ return appPathToCache;
385
+ };
386
+
387
+ if (_.isFunction(onPostProcess)) {
388
+ const result = await onPostProcess({
389
+ cachedAppInfo: _.clone(cachedAppInfo),
390
+ isUrl,
391
+ headers: _.clone(headers),
392
+ appPath: newApp,
283
393
  });
394
+ return (!result?.appPath || app === result?.appPath || !await fs.exists(result?.appPath))
395
+ ? newApp
396
+ : await storeAppInCache(result.appPath);
284
397
  }
285
- return newApp;
398
+
399
+ verifyAppExtension(newApp, supportedAppExtensions);
400
+ return (app !== newApp && (packageHash || _.values(remoteAppProps).some(Boolean)))
401
+ ? await storeAppInCache(newApp)
402
+ : newApp;
286
403
  });
287
404
  }
288
405
 
@@ -338,23 +455,22 @@ async function unzipApp (zipPath, dstRoot, supportedAppExtensions) {
338
455
  extractionOpts.fileNamesEncoding = 'utf8';
339
456
  }
340
457
  await zip.extractAllTo(zipPath, tmpRoot, extractionOpts);
341
- const duration = timer.getDuration();
342
- const allExtractedItems = await fs.glob('**', {cwd: tmpRoot});
343
- logger.debug(`Extracted ${util.pluralize('item', allExtractedItems.length, true)} ` +
344
- `from '${zipPath}' in ${Math.round(duration.asMilliSeconds)}ms`);
345
- const allBundleItems = allExtractedItems
346
- .filter((relativePath) => supportedAppExtensions.includes(path.extname(relativePath)))
347
- // Get the top level match
348
- .sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
349
- if (_.isEmpty(allBundleItems)) {
350
- throw new Error(`App zip unzipped OK, but we could not find '${supportedAppExtensions}' ` +
458
+ const globPattern = `**/*.+(${supportedAppExtensions.map((ext) => ext.replace(/^\./, '')).join('|')})`;
459
+ const sortedBundleItems = (await fs.glob(globPattern, {
460
+ cwd: tmpRoot,
461
+ strict: false,
462
+ // Get the top level match
463
+ })).sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
464
+ if (_.isEmpty(sortedBundleItems)) {
465
+ logger.errorAndThrow(`App unzipped OK, but we could not find any '${supportedAppExtensions}' ` +
351
466
  util.pluralize('bundle', supportedAppExtensions.length, false) +
352
467
  ` in it. Make sure your archive contains at least one package having ` +
353
468
  `'${supportedAppExtensions}' ${util.pluralize('extension', supportedAppExtensions.length, false)}`);
354
469
  }
355
- const matchedBundle = _.first(allBundleItems);
356
- logger.debug(`Matched ${util.pluralize('item', allBundleItems.length, true)} in the extracted archive. ` +
357
- `Assuming '${matchedBundle}' is the correct bundle`);
470
+ logger.debug(`Extracted ${util.pluralize('bundle item', sortedBundleItems.length, true)} ` +
471
+ `from '${zipPath}' in ${Math.round(timer.getDuration().asMilliSeconds)}ms: ${sortedBundleItems}`);
472
+ const matchedBundle = _.first(sortedBundleItems);
473
+ logger.info(`Assuming '${matchedBundle}' is the correct bundle`);
358
474
  const dstPath = path.resolve(dstRoot, path.basename(matchedBundle));
359
475
  await fs.mv(path.resolve(tmpRoot, matchedBundle), dstPath, {mkdirp: true});
360
476
  return dstPath;
@@ -1,5 +1,5 @@
1
1
  import _ from 'lodash';
2
- import 'colors';
2
+ import '@dabh/colors';
3
3
  import morgan from 'morgan';
4
4
  import log from './logger';
5
5
  import { MAX_LOG_BODY_LENGTH } from '../constants';
package/lib/index.js CHANGED
@@ -21,7 +21,8 @@ const {
21
21
  Protocol, routeConfiguringFunction, errors, isErrorType,
22
22
  errorFromMJSONWPStatusCode, errorFromW3CJsonCode, ALL_COMMANDS, METHOD_MAP,
23
23
  routeToCommandName, NO_SESSION_ID_COMMANDS, isSessionCommand,
24
- normalizeBasePath, determineProtocol, CREATE_SESSION_COMMAND, DELETE_SESSION_COMMAND,
24
+ normalizeBasePath, determineProtocol, CREATE_SESSION_COMMAND,
25
+ DELETE_SESSION_COMMAND, GET_STATUS_COMMAND,
25
26
  } = protocol;
26
27
 
27
28
  export {
@@ -29,7 +30,8 @@ export {
29
30
  errorFromMJSONWPStatusCode, errorFromW3CJsonCode, determineProtocol,
30
31
  errorFromMJSONWPStatusCode as errorFromCode, ALL_COMMANDS, METHOD_MAP,
31
32
  routeToCommandName, NO_SESSION_ID_COMMANDS, isSessionCommand,
32
- DEFAULT_BASE_PATH, normalizeBasePath, CREATE_SESSION_COMMAND, DELETE_SESSION_COMMAND
33
+ DEFAULT_BASE_PATH, normalizeBasePath, CREATE_SESSION_COMMAND,
34
+ DELETE_SESSION_COMMAND, GET_STATUS_COMMAND,
33
35
  };
34
36
 
35
37
  // Express exports
@@ -4,7 +4,6 @@ import { duplicateKeys } from '../basedriver/helpers';
4
4
  import {
5
5
  MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY, PROTOCOLS
6
6
  } from '../constants';
7
- import { formatStatus } from '../protocol/helpers';
8
7
 
9
8
  const log = logger.getLogger('Protocol Converter');
10
9
 
@@ -206,10 +205,7 @@ class ProtocolConverter {
206
205
  */
207
206
  async convertAndProxy (commandName, url, method, body) {
208
207
  if (!this.downstreamProtocol) {
209
- // Patch calls with GENERIC protocol
210
- // to preserve the backward compatibility
211
- const [res, resBodyObj] = await this.proxyFunc(url, method, body);
212
- return [res, formatStatus(resBodyObj, res.statusCode)];
208
+ return await this.proxyFunc(url, method, body);
213
209
  }
214
210
 
215
211
  // Same url, but different arguments
@@ -9,7 +9,6 @@ import { routeToCommandName } from '../protocol';
9
9
  import { MAX_LOG_BODY_LENGTH, DEFAULT_BASE_PATH, PROTOCOLS } from '../constants';
10
10
  import ProtocolConverter from './protocol-converter';
11
11
  import { formatResponseValue, formatStatus } from '../protocol/helpers';
12
- import SESSIONS_CACHE from '../protocol/sessions-cache';
13
12
  import http from 'http';
14
13
  import https from 'https';
15
14
 
@@ -327,8 +326,7 @@ class JWProxy {
327
326
  }
328
327
  }
329
328
  resBodyObj.value = formatResponseValue(resBodyObj.value);
330
- formatStatus(resBodyObj, res.statusCode, SESSIONS_CACHE.getProtocol(reqSessionId));
331
- res.status(response.statusCode).send(JSON.stringify(resBodyObj));
329
+ res.status(response.statusCode).send(JSON.stringify(formatStatus(resBodyObj)));
332
330
  }
333
331
  }
334
332
 
@@ -657,7 +657,7 @@ class ProxyRequestError extends ES6Error {
657
657
 
658
658
  getActualError () {
659
659
  // If it's MJSONWP error, returns actual error cause for request failure based on `jsonwp.status`
660
- if (util.hasValue(this.jsonwp) && util.hasValue(this.jsonwp.status) && util.hasValue(this.jsonwp.value)) {
660
+ if (util.hasValue(this.jsonwp?.status) && util.hasValue(this.jsonwp?.value)) {
661
661
  return errorFromMJSONWPStatusCode(this.jsonwp.status, this.jsonwp.value);
662
662
  } else if (util.hasValue(this.w3c) && _.isNumber(this.w3cStatus) && this.w3cStatus >= 300) {
663
663
  return errorFromW3CJsonCode(this.w3c.error, this.w3c.message || this.message, this.w3c.stacktrace);
@@ -1,9 +1,6 @@
1
1
  import _ from 'lodash';
2
2
  import { duplicateKeys } from '../basedriver/helpers';
3
- import { MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY, PROTOCOLS } from '../constants';
4
-
5
- const JSONWP_SUCCESS_STATUS_CODE = 0;
6
- const JSONWP_UNKNOWN_ERROR_STATUS_CODE = 13;
3
+ import { MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY } from '../constants';
7
4
 
8
5
  /**
9
6
  * Preprocesses the resulting value for API responses,
@@ -26,33 +23,16 @@ function formatResponseValue (resValue) {
26
23
 
27
24
  /**
28
25
  * Properly formats the status for API responses,
29
- * so they are correct for both W3C and JSONWP protocols.
30
- * This method DOES mutate the `responseBody` argument if needed
26
+ * so they are correct for the W3C protocol.
31
27
  *
32
28
  * @param {Object} responseBody
33
- * @param {number} responseCode the HTTP response code
34
- * @param {?string} protocol The name of the protocol, either
35
- * `PROTOCOLS.W3C` or `PROTOCOLS.MJSONWP`
36
29
  * @returns {Object} The fixed response body
37
30
  */
38
- function formatStatus (responseBody, responseCode = 200, protocol = null) {
39
- if (!_.isPlainObject(responseBody)) {
40
- return responseBody;
41
- }
42
- const isError = _.has(responseBody.value, 'error') || responseCode >= 400;
43
- if ((protocol === PROTOCOLS.MJSONWP && !_.isInteger(responseBody.status))
44
- || (!protocol && !_.has(responseBody, 'status'))) {
45
- responseBody.status = isError
46
- ? JSONWP_UNKNOWN_ERROR_STATUS_CODE
47
- : JSONWP_SUCCESS_STATUS_CODE;
48
- } else if (protocol === PROTOCOLS.W3C && _.has(responseBody, 'status')) {
49
- delete responseBody.status;
50
- }
51
- return responseBody;
31
+ function formatStatus (responseBody) {
32
+ return _.isPlainObject(responseBody) ? _.omit(responseBody, ['status']) : responseBody;
52
33
  }
53
34
 
54
35
 
55
36
  export {
56
- MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY, formatResponseValue,
57
- JSONWP_SUCCESS_STATUS_CODE, formatStatus,
37
+ MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY, formatResponseValue, formatStatus
58
38
  };
@@ -2,7 +2,7 @@ import _ from 'lodash';
2
2
  import { util } from '@appium/support';
3
3
  import { validators } from './validators';
4
4
  import {
5
- errors, isErrorType, getResponseForW3CError, getResponseForJsonwpError,
5
+ errors, isErrorType, getResponseForW3CError,
6
6
  errorFromMJSONWPStatusCode, errorFromW3CJsonCode,
7
7
  } from './errors';
8
8
  import { METHOD_MAP, NO_SESSION_ID_COMMANDS } from './routes';
@@ -16,6 +16,7 @@ import SESSIONS_CACHE from './sessions-cache';
16
16
 
17
17
  const CREATE_SESSION_COMMAND = 'createSession';
18
18
  const DELETE_SESSION_COMMAND = 'deleteSession';
19
+ const GET_STATUS_COMMAND = 'getStatus';
19
20
 
20
21
  class Protocol {}
21
22
 
@@ -377,24 +378,7 @@ function buildHandler (app, method, path, spec, driver, isSessCmd) {
377
378
  .debug(`Encountered internal error running command: ${errMsg}`);
378
379
  }
379
380
 
380
- if (currentProtocol === PROTOCOLS.W3C) {
381
- [httpStatus, httpResBody] = getResponseForW3CError(actualErr);
382
- } else if (currentProtocol === PROTOCOLS.MJSONWP) {
383
- [httpStatus, httpResBody] = getResponseForJsonwpError(actualErr);
384
- } else {
385
- // If it's unknown what the protocol is (like if it's `getStatus` prior to `createSession`), merge the responses
386
- // together to be protocol-agnostic
387
- let jsonwpRes = getResponseForJsonwpError(actualErr);
388
- let w3cRes = getResponseForW3CError(actualErr);
389
-
390
- httpResBody = {
391
- ...jsonwpRes[1],
392
- ...w3cRes[1],
393
- };
394
-
395
- // Use the JSONWP status code (which is usually 500)
396
- httpStatus = jsonwpRes[0];
397
- }
381
+ [httpStatus, httpResBody] = getResponseForW3CError(actualErr);
398
382
  }
399
383
 
400
384
  // decode the response, which is either a string or json
@@ -415,7 +399,7 @@ function buildHandler (app, method, path, spec, driver, isSessCmd) {
415
399
  delete httpResBody.sessionId;
416
400
  }
417
401
 
418
- httpResBody = formatStatus(httpResBody, httpStatus, currentProtocol);
402
+ httpResBody = formatStatus(httpResBody);
419
403
  res.status(httpStatus).json(httpResBody);
420
404
  }
421
405
  };
@@ -433,7 +417,7 @@ function driverShouldDoJwpProxy (driver, req, command) {
433
417
 
434
418
  // we should never proxy deleteSession because we need to give the containing
435
419
  // driver an opportunity to clean itself up
436
- if (command === 'deleteSession') {
420
+ if (command === DELETE_SESSION_COMMAND) {
437
421
  return false;
438
422
  }
439
423
 
@@ -452,11 +436,10 @@ async function doJwpProxy (driver, req, res) {
452
436
 
453
437
  // check that the inner driver has a proxy function
454
438
  if (!driver.canProxy(req.params.sessionId)) {
455
- throw new Error('Trying to proxy to a JSONWP server but driver is unable to proxy');
439
+ throw new Error('Trying to proxy to a server but the driver is unable to proxy');
456
440
  }
457
441
  try {
458
- const proxiedRes = await driver.executeCommand('proxyReqRes', req, res, req.params.sessionId);
459
- if (proxiedRes && proxiedRes.error) throw proxiedRes.error; // eslint-disable-line curly
442
+ await driver.executeCommand('proxyReqRes', req, res, req.params.sessionId);
460
443
  } catch (err) {
461
444
  if (isErrorType(err, errors.ProxyRequestError)) {
462
445
  throw err;
@@ -469,5 +452,6 @@ async function doJwpProxy (driver, req, res) {
469
452
 
470
453
  export {
471
454
  Protocol, routeConfiguringFunction, isSessionCommand,
472
- driverShouldDoJwpProxy, determineProtocol, CREATE_SESSION_COMMAND, DELETE_SESSION_COMMAND,
455
+ driverShouldDoJwpProxy, determineProtocol, CREATE_SESSION_COMMAND,
456
+ DELETE_SESSION_COMMAND, GET_STATUS_COMMAND,
473
457
  };