@appium/base-driver 8.1.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.
Files changed (70) hide show
  1. package/build/lib/basedriver/capabilities.js +3 -1
  2. package/build/lib/basedriver/commands/index.js +2 -4
  3. package/build/lib/basedriver/driver.js +8 -10
  4. package/build/lib/basedriver/helpers.js +140 -82
  5. package/build/lib/express/express-logging.js +2 -2
  6. package/build/lib/index.js +126 -0
  7. package/build/lib/jsonwp-proxy/protocol-converter.js +2 -5
  8. package/build/lib/jsonwp-proxy/proxy.js +2 -5
  9. package/build/lib/protocol/errors.js +4 -2
  10. package/build/lib/protocol/helpers.js +3 -20
  11. package/build/lib/protocol/index.js +13 -1
  12. package/build/lib/protocol/protocol.js +26 -25
  13. package/build/lib/protocol/routes.js +1 -10
  14. package/build/test/basedriver/capability-specs.js +10 -10
  15. package/build/test/basedriver/commands/event-specs.js +10 -10
  16. package/build/test/basedriver/driver-e2e-specs.js +3 -3
  17. package/build/test/basedriver/driver-e2e-tests.js +6 -223
  18. package/build/test/basedriver/driver-specs.js +3 -3
  19. package/build/test/basedriver/driver-tests.js +6 -6
  20. package/build/test/basedriver/helpers-specs.js +5 -1
  21. package/build/test/basedriver/timeout-specs.js +7 -7
  22. package/build/test/basedriver/websockets-e2e-specs.js +5 -5
  23. package/build/test/express/server-e2e-specs.js +156 -0
  24. package/build/test/express/server-specs.js +151 -0
  25. package/build/test/express/static-specs.js +23 -0
  26. package/build/test/helpers.js +57 -0
  27. package/build/test/jsonwp-proxy/mock-request.js +93 -0
  28. package/build/test/jsonwp-proxy/protocol-converter-specs.js +173 -0
  29. package/build/test/jsonwp-proxy/proxy-e2e-specs.js +61 -0
  30. package/build/test/jsonwp-proxy/proxy-specs.js +294 -0
  31. package/build/test/jsonwp-proxy/url-specs.js +167 -0
  32. package/build/test/jsonwp-status/status-specs.js +36 -0
  33. package/build/test/protocol/errors-specs.js +388 -0
  34. package/build/test/protocol/fake-driver.js +168 -0
  35. package/build/test/protocol/helpers.js +27 -0
  36. package/build/test/protocol/protocol-e2e-specs.js +1182 -0
  37. package/build/test/protocol/routes-specs.js +82 -0
  38. package/build/test/protocol/validator-specs.js +151 -0
  39. package/index.d.ts +5 -3
  40. package/index.js +1 -62
  41. package/lib/basedriver/capabilities.js +3 -0
  42. package/lib/basedriver/commands/index.js +0 -2
  43. package/lib/basedriver/driver.js +6 -26
  44. package/lib/basedriver/helpers.js +202 -85
  45. package/lib/express/express-logging.js +1 -1
  46. package/lib/index.js +64 -0
  47. package/lib/jsonwp-proxy/protocol-converter.js +1 -5
  48. package/lib/jsonwp-proxy/proxy.js +1 -3
  49. package/lib/protocol/errors.js +1 -1
  50. package/lib/protocol/helpers.js +5 -25
  51. package/lib/protocol/index.js +3 -1
  52. package/lib/protocol/protocol.js +26 -31
  53. package/lib/protocol/routes.js +0 -3
  54. package/package.json +8 -16
  55. package/test/basedriver/capability-specs.js +1 -1
  56. package/test/basedriver/commands/event-specs.js +1 -1
  57. package/test/basedriver/driver-e2e-specs.js +1 -1
  58. package/test/basedriver/driver-e2e-tests.js +1 -179
  59. package/test/basedriver/driver-specs.js +1 -1
  60. package/test/basedriver/driver-tests.js +3 -3
  61. package/test/basedriver/helpers-specs.js +4 -0
  62. package/test/basedriver/timeout-specs.js +1 -1
  63. package/test/basedriver/websockets-e2e-specs.js +1 -1
  64. package/build/index.js +0 -120
  65. package/build/lib/basedriver/commands/execute-child.js +0 -137
  66. package/build/lib/basedriver/commands/execute.js +0 -119
  67. package/build/test/basedriver/fixtures/custom-element-finder-bad.js +0 -12
  68. package/build/test/basedriver/fixtures/custom-element-finder.js +0 -36
  69. package/lib/basedriver/commands/execute-child.js +0 -132
  70. package/lib/basedriver/commands/execute.js +0 -126
@@ -2,7 +2,7 @@ import _ from 'lodash';
2
2
  import path from 'path';
3
3
  import url from 'url';
4
4
  import logger from './logger';
5
- import { system, tempDir, fs, util, zip, net, timing } from '@appium/support';
5
+ import { tempDir, fs, util, zip, net, timing } from '@appium/support';
6
6
  import LRU from 'lru-cache';
7
7
  import AsyncLock from 'async-lock';
8
8
  import axios from 'axios';
@@ -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,
283
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,
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
 
@@ -322,38 +439,38 @@ async function unzipApp (zipPath, dstRoot, supportedAppExtensions) {
322
439
  try {
323
440
  logger.debug(`Unzipping '${zipPath}'`);
324
441
  const timer = new timing.Timer().start();
442
+ const useSystemUnzipEnv = process.env.APPIUM_PREFER_SYSTEM_UNZIP;
443
+ const useSystemUnzip = _.isEmpty(useSystemUnzipEnv)
444
+ || !['0', 'false'].includes(_.toLower(useSystemUnzipEnv));
325
445
  /**
326
446
  * Attempt to use use the system `unzip` (e.g., `/usr/bin/unzip`) due
327
447
  * to the significant performance improvement it provides over the native
328
448
  * JS "unzip" implementation.
329
449
  * @type {import('@appium/support/lib/zip').ExtractAllOptions}
330
450
  */
331
- const extractionOpts = {
332
- useSystemUnzip: !system.isWindows(),
333
- };
451
+ const extractionOpts = {useSystemUnzip};
334
452
  // https://github.com/appium/appium/issues/14100
335
453
  if (path.extname(zipPath) === IPA_EXT) {
336
454
  logger.debug(`Enforcing UTF-8 encoding on the extracted file names for '${path.basename(zipPath)}'`);
337
455
  extractionOpts.fileNamesEncoding = 'utf8';
338
456
  }
339
457
  await zip.extractAllTo(zipPath, tmpRoot, extractionOpts);
340
- const duration = timer.getDuration();
341
- const allExtractedItems = await fs.glob('**', {cwd: tmpRoot});
342
- logger.debug(`Extracted ${util.pluralize('item', allExtractedItems.length, true)} ` +
343
- `from '${zipPath}' in ${Math.round(duration.asMilliSeconds)}ms`);
344
- const allBundleItems = allExtractedItems
345
- .filter((relativePath) => supportedAppExtensions.includes(path.extname(relativePath)))
346
- // Get the top level match
347
- .sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
348
- if (_.isEmpty(allBundleItems)) {
349
- 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}' ` +
350
466
  util.pluralize('bundle', supportedAppExtensions.length, false) +
351
467
  ` in it. Make sure your archive contains at least one package having ` +
352
468
  `'${supportedAppExtensions}' ${util.pluralize('extension', supportedAppExtensions.length, false)}`);
353
469
  }
354
- const matchedBundle = _.first(allBundleItems);
355
- logger.debug(`Matched ${util.pluralize('item', allBundleItems.length, true)} in the extracted archive. ` +
356
- `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`);
357
474
  const dstPath = path.resolve(dstRoot, path.basename(matchedBundle));
358
475
  await fs.mv(path.resolve(tmpRoot, matchedBundle), dstPath, {mkdirp: true});
359
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 ADDED
@@ -0,0 +1,64 @@
1
+ // transpile:main
2
+
3
+ // BaseDriver exports
4
+ import * as driver from './basedriver/driver';
5
+ import * as deviceSettings from './basedriver/device-settings';
6
+
7
+ const { BaseDriver } = driver;
8
+ const { DeviceSettings, BASEDRIVER_HANDLED_SETTINGS } = deviceSettings;
9
+
10
+ export { BaseDriver, DeviceSettings, BASEDRIVER_HANDLED_SETTINGS };
11
+ export default BaseDriver;
12
+
13
+
14
+ // MJSONWP exports
15
+ import * as protocol from './protocol';
16
+ import {
17
+ DEFAULT_BASE_PATH, PROTOCOLS
18
+ } from './constants';
19
+
20
+ const {
21
+ Protocol, routeConfiguringFunction, errors, isErrorType,
22
+ errorFromMJSONWPStatusCode, errorFromW3CJsonCode, ALL_COMMANDS, METHOD_MAP,
23
+ routeToCommandName, NO_SESSION_ID_COMMANDS, isSessionCommand,
24
+ normalizeBasePath, determineProtocol, CREATE_SESSION_COMMAND,
25
+ DELETE_SESSION_COMMAND, GET_STATUS_COMMAND,
26
+ } = protocol;
27
+
28
+ export {
29
+ Protocol, routeConfiguringFunction, errors, isErrorType, PROTOCOLS,
30
+ errorFromMJSONWPStatusCode, errorFromW3CJsonCode, determineProtocol,
31
+ errorFromMJSONWPStatusCode as errorFromCode, ALL_COMMANDS, METHOD_MAP,
32
+ routeToCommandName, NO_SESSION_ID_COMMANDS, isSessionCommand,
33
+ DEFAULT_BASE_PATH, normalizeBasePath, CREATE_SESSION_COMMAND,
34
+ DELETE_SESSION_COMMAND, GET_STATUS_COMMAND,
35
+ };
36
+
37
+ // Express exports
38
+ import * as staticIndex from './express/static';
39
+ const { STATIC_DIR } = staticIndex;
40
+ export { STATIC_DIR };
41
+
42
+ import * as serverIndex from './express/server';
43
+ const { server } = serverIndex;
44
+ export { server };
45
+
46
+ // jsonwp-proxy exports
47
+ import * as proxyIndex from './jsonwp-proxy/proxy';
48
+ const { JWProxy } = proxyIndex;
49
+ export { JWProxy };
50
+
51
+ // jsonwp-status exports
52
+ import * as statusIndex from './jsonwp-status/status';
53
+ const { codes: statusCodes, getSummaryByCode } = statusIndex;
54
+ export { statusCodes, getSummaryByCode };
55
+
56
+ // W3C capabilities parser
57
+ import * as caps from './basedriver/capabilities';
58
+ const { processCapabilities, isStandardCap, validateCaps } = caps;
59
+ export { processCapabilities, isStandardCap, validateCaps };
60
+
61
+ // Web socket helpers
62
+ import * as ws from './express/websocket';
63
+ const { DEFAULT_WS_PATHNAME_PREFIX } = ws;
64
+ export { DEFAULT_WS_PATHNAME_PREFIX };
@@ -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
  };
@@ -1,7 +1,8 @@
1
1
  // transpile:main
2
2
 
3
3
  import {
4
- Protocol, isSessionCommand, routeConfiguringFunction, determineProtocol
4
+ Protocol, isSessionCommand, routeConfiguringFunction, determineProtocol,
5
+ CREATE_SESSION_COMMAND, DELETE_SESSION_COMMAND,
5
6
  } from './protocol';
6
7
  import {
7
8
  NO_SESSION_ID_COMMANDS, ALL_COMMANDS, METHOD_MAP,
@@ -15,4 +16,5 @@ export {
15
16
  Protocol, routeConfiguringFunction, errors, isErrorType,
16
17
  errorFromMJSONWPStatusCode, errorFromW3CJsonCode, ALL_COMMANDS, METHOD_MAP,
17
18
  routeToCommandName, NO_SESSION_ID_COMMANDS, isSessionCommand, determineProtocol,
19
+ CREATE_SESSION_COMMAND, DELETE_SESSION_COMMAND,
18
20
  };