@appium/base-driver 8.2.2 → 8.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.
Files changed (61) hide show
  1. package/build/lib/basedriver/capabilities.js +3 -1
  2. package/build/lib/basedriver/commands/find.js +4 -11
  3. package/build/lib/basedriver/commands/log.js +3 -6
  4. package/build/lib/basedriver/commands/session.js +18 -27
  5. package/build/lib/basedriver/commands/settings.js +4 -8
  6. package/build/lib/basedriver/commands/timeout.js +10 -15
  7. package/build/lib/basedriver/device-settings.js +14 -2
  8. package/build/lib/basedriver/driver.js +25 -23
  9. package/build/lib/basedriver/helpers.js +140 -84
  10. package/build/lib/express/express-logging.js +2 -2
  11. package/build/lib/express/idempotency.js +2 -2
  12. package/build/lib/helpers/capabilities.js +39 -0
  13. package/build/lib/index.js +7 -7
  14. package/build/lib/jsonwp-proxy/protocol-converter.js +19 -16
  15. package/build/lib/jsonwp-proxy/proxy.js +20 -16
  16. package/build/lib/protocol/errors.js +4 -2
  17. package/build/lib/protocol/helpers.js +3 -20
  18. package/build/lib/protocol/protocol.js +44 -45
  19. package/build/lib/protocol/routes.js +67 -1
  20. package/build/test/basedriver/capabilities-specs.js +43 -1
  21. package/build/test/basedriver/capability-specs.js +126 -167
  22. package/build/test/basedriver/commands/log-specs.js +12 -5
  23. package/build/test/basedriver/driver-tests.js +11 -14
  24. package/build/test/basedriver/helpers-specs.js +5 -1
  25. package/build/test/basedriver/timeout-specs.js +7 -9
  26. package/build/test/express/server-e2e-specs.js +10 -5
  27. package/build/test/express/server-specs.js +22 -16
  28. package/build/test/express/static-specs.js +10 -5
  29. package/build/test/jsonwp-proxy/proxy-e2e-specs.js +1 -2
  30. package/build/test/jsonwp-proxy/proxy-specs.js +1 -6
  31. package/build/test/protocol/fake-driver.js +12 -15
  32. package/build/test/protocol/protocol-e2e-specs.js +49 -103
  33. package/build/test/protocol/routes-specs.js +2 -2
  34. package/lib/basedriver/capabilities.js +3 -0
  35. package/lib/basedriver/commands/find.js +3 -6
  36. package/lib/basedriver/commands/log.js +2 -4
  37. package/lib/basedriver/commands/session.js +21 -22
  38. package/lib/basedriver/commands/settings.js +3 -5
  39. package/lib/basedriver/commands/timeout.js +9 -10
  40. package/lib/basedriver/device-settings.js +10 -1
  41. package/lib/basedriver/driver.js +29 -16
  42. package/lib/basedriver/helpers.js +201 -83
  43. package/lib/express/express-logging.js +1 -1
  44. package/lib/express/idempotency.js +1 -1
  45. package/lib/helpers/capabilities.js +25 -0
  46. package/lib/index.js +6 -4
  47. package/lib/jsonwp-proxy/protocol-converter.js +15 -18
  48. package/lib/jsonwp-proxy/proxy.js +17 -15
  49. package/lib/protocol/errors.js +1 -1
  50. package/lib/protocol/helpers.js +5 -25
  51. package/lib/protocol/protocol.js +43 -54
  52. package/lib/protocol/routes.js +60 -1
  53. package/package.json +29 -22
  54. package/test/basedriver/capabilities-specs.js +34 -2
  55. package/test/basedriver/capability-specs.js +120 -146
  56. package/test/basedriver/commands/log-specs.js +12 -3
  57. package/test/basedriver/driver-tests.js +12 -7
  58. package/test/basedriver/helpers-specs.js +4 -0
  59. package/test/basedriver/timeout-specs.js +6 -11
  60. package/build/lib/protocol/sessions-cache.js +0 -88
  61. package/lib/protocol/sessions-cache.js +0 -74
@@ -15,16 +15,17 @@ const ZIP_MIME_TYPES = [
15
15
  'multipart/x-zip',
16
16
  ];
17
17
  const CACHED_APPS_MAX_AGE = 1000 * 60 * 60 * 24; // ms
18
+ const MAX_CACHED_APPS = 1024;
18
19
  const APPLICATIONS_CACHE = new LRU({
19
- maxAge: CACHED_APPS_MAX_AGE, // expire after 24 hours
20
+ max: MAX_CACHED_APPS,
21
+ ttl: CACHED_APPS_MAX_AGE, // expire after 24 hours
20
22
  updateAgeOnGet: true,
21
- dispose: async (app, {fullPath}) => {
22
- if (!await fs.exists(fullPath)) {
23
- return;
23
+ dispose: (app, {fullPath}) => {
24
+ logger.info(`The application '${app}' cached at '${fullPath}' has ` +
25
+ `expired after ${CACHED_APPS_MAX_AGE}ms`);
26
+ if (fullPath) {
27
+ fs.rimraf(fullPath);
24
28
  }
25
-
26
- logger.info(`The application '${app}' cached at '${fullPath}' has expired`);
27
- await fs.rimraf(fullPath);
28
29
  },
29
30
  noDisposeOnSet: true,
30
31
  });
@@ -34,11 +35,11 @@ const DEFAULT_BASENAME = 'appium-app';
34
35
  const APP_DOWNLOAD_TIMEOUT_MS = 120 * 1000;
35
36
 
36
37
  process.on('exit', () => {
37
- if (APPLICATIONS_CACHE.itemCount === 0) {
38
+ if (APPLICATIONS_CACHE.size === 0) {
38
39
  return;
39
40
  }
40
41
 
41
- const appPaths = APPLICATIONS_CACHE.values()
42
+ const appPaths = [...APPLICATIONS_CACHE.values()]
42
43
  .map(({fullPath}) => fullPath);
43
44
  logger.debug(`Performing cleanup of ${appPaths.length} cached ` +
44
45
  util.pluralize('application', appPaths.length));
@@ -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,106 @@ 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 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 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 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
+ * If the downloaded app has `.zip` extension, this method will unzip it.
197
+ * The unzip does not work when `onPostProcess` is provided.
198
+ *
199
+ * @param {string} app Either a full path to the app or a remote URL
200
+ * @param {string|string[]|ConfigureAppOptions} options
201
+ * @returns The full path to the resulting application bundle
202
+ */
203
+ async function configureApp (app, options = {}) {
125
204
  if (!_.isString(app)) {
126
205
  // immediately shortcircuit if not given an app
127
206
  return;
128
207
  }
129
- if (!_.isArray(supportedAppExtensions)) {
130
- supportedAppExtensions = [supportedAppExtensions];
208
+
209
+ let supportedAppExtensions;
210
+ const {
211
+ onPostProcess,
212
+ } = _.isPlainObject(options) ? options : {};
213
+ if (_.isString(options)) {
214
+ supportedAppExtensions = [options];
215
+ } else if (_.isArray(options)) {
216
+ supportedAppExtensions = options;
217
+ } else if (_.isPlainObject(options)) {
218
+ supportedAppExtensions = options.supportedExtensions;
219
+ }
220
+ if (_.isEmpty(supportedAppExtensions)) {
221
+ throw new Error(`One or more supported app extensions must be provided`);
131
222
  }
132
223
 
133
224
  let newApp = app;
134
225
  let shouldUnzipApp = false;
135
- let archiveHash = null;
226
+ let packageHash = null;
227
+ let headers = null;
136
228
  const remoteAppProps = {
137
229
  lastModified: null,
138
230
  immutable: false,
@@ -141,11 +233,13 @@ async function configureApp (app, supportedAppExtensions) {
141
233
  const {protocol, pathname} = url.parse(newApp);
142
234
  const isUrl = ['http:', 'https:'].includes(protocol);
143
235
 
236
+ const cachedAppInfo = APPLICATIONS_CACHE.get(app);
237
+
144
238
  return await APPLICATIONS_CACHE_GUARD.acquire(app, async () => {
145
239
  if (isUrl) {
146
240
  // Use the app from remote URL
147
241
  logger.info(`Using downloadable app '${newApp}'`);
148
- const headers = await retrieveHeaders(newApp);
242
+ headers = await retrieveHeaders(newApp);
149
243
  if (!_.isEmpty(headers)) {
150
244
  if (headers['last-modified']) {
151
245
  remoteAppProps.lastModified = new Date(headers['last-modified']);
@@ -160,13 +254,14 @@ async function configureApp (app, supportedAppExtensions) {
160
254
  }
161
255
  logger.debug(`Cache-Control: ${headers['cache-control']}`);
162
256
  }
163
- const cachedPath = getCachedApplicationPath(app, remoteAppProps);
257
+ const cachedPath = getCachedApplicationPath(app, remoteAppProps, cachedAppInfo);
164
258
  if (cachedPath) {
165
- if (await fs.exists(cachedPath)) {
259
+ if (await isAppIntegrityOk(cachedPath, cachedAppInfo?.integrity)) {
166
260
  logger.info(`Reusing previously downloaded application at '${cachedPath}'`);
167
261
  return verifyAppExtension(cachedPath, supportedAppExtensions);
168
262
  }
169
- logger.info(`The application at '${cachedPath}' does not exist anymore. Deleting it from the cache`);
263
+ logger.info(`The application at '${cachedPath}' does not exist anymore ` +
264
+ `or its integrity has been damaged. Deleting it from the internal cache`);
170
265
  APPLICATIONS_CACHE.del(app);
171
266
  }
172
267
 
@@ -234,19 +329,24 @@ async function configureApp (app, supportedAppExtensions) {
234
329
  throw new Error(errorMessage);
235
330
  }
236
331
 
237
- if (shouldUnzipApp) {
332
+ const isPackageAFile = (await fs.stat(newApp)).isFile();
333
+ if (isPackageAFile) {
334
+ packageHash = await calculateFileIntegrity(newApp);
335
+ }
336
+
337
+ if (isPackageAFile && shouldUnzipApp && !_.isFunction(onPostProcess)) {
238
338
  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)) {
339
+ if (packageHash === cachedAppInfo?.packageHash) {
340
+ const {fullPath} = cachedAppInfo;
341
+ if (await isAppIntegrityOk(fullPath, cachedAppInfo?.integrity)) {
243
342
  if (archivePath !== app) {
244
343
  await fs.rimraf(archivePath);
245
344
  }
246
345
  logger.info(`Will reuse previously cached application at '${fullPath}'`);
247
346
  return verifyAppExtension(fullPath, supportedAppExtensions);
248
347
  }
249
- logger.info(`The application at '${fullPath}' does not exist anymore. Deleting it from the cache`);
348
+ logger.info(`The application at '${fullPath}' does not exist anymore ` +
349
+ `or its integrity has been damaged. Deleting it from the cache`);
250
350
  APPLICATIONS_CACHE.del(app);
251
351
  }
252
352
  const tmpRoot = await tempDir.openDir();
@@ -265,24 +365,43 @@ async function configureApp (app, supportedAppExtensions) {
265
365
  app = newApp;
266
366
  }
267
367
 
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
- }
368
+ const storeAppInCache = async (appPathToCache) => {
369
+ const cachedFullPath = cachedAppInfo?.fullPath;
370
+ if (cachedFullPath && cachedFullPath !== appPathToCache) {
371
+ await fs.rimraf(cachedFullPath);
372
+ }
373
+ const integrity = {};
374
+ if ((await fs.stat(appPathToCache)).isDirectory()) {
375
+ integrity.folder = await calculateFolderIntegrity(appPathToCache);
376
+ } else {
377
+ integrity.file = await calculateFileIntegrity(appPathToCache);
277
378
  }
278
379
  APPLICATIONS_CACHE.set(app, {
279
380
  ...remoteAppProps,
280
381
  timestamp: Date.now(),
281
- hash: archiveHash,
282
- fullPath: newApp,
382
+ packageHash,
383
+ integrity,
384
+ fullPath: appPathToCache,
385
+ });
386
+ return appPathToCache;
387
+ };
388
+
389
+ if (_.isFunction(onPostProcess)) {
390
+ const result = await onPostProcess({
391
+ cachedAppInfo: _.clone(cachedAppInfo),
392
+ isUrl,
393
+ headers: _.clone(headers),
394
+ appPath: newApp,
283
395
  });
396
+ return (!result?.appPath || app === result?.appPath || !await fs.exists(result?.appPath))
397
+ ? newApp
398
+ : await storeAppInCache(result.appPath);
284
399
  }
285
- return newApp;
400
+
401
+ verifyAppExtension(newApp, supportedAppExtensions);
402
+ return (app !== newApp && (packageHash || _.values(remoteAppProps).some(Boolean)))
403
+ ? await storeAppInCache(newApp)
404
+ : newApp;
286
405
  });
287
406
  }
288
407
 
@@ -338,23 +457,22 @@ async function unzipApp (zipPath, dstRoot, supportedAppExtensions) {
338
457
  extractionOpts.fileNamesEncoding = 'utf8';
339
458
  }
340
459
  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}' ` +
460
+ const globPattern = `**/*.+(${supportedAppExtensions.map((ext) => ext.replace(/^\./, '')).join('|')})`;
461
+ const sortedBundleItems = (await fs.glob(globPattern, {
462
+ cwd: tmpRoot,
463
+ strict: false,
464
+ // Get the top level match
465
+ })).sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
466
+ if (_.isEmpty(sortedBundleItems)) {
467
+ logger.errorAndThrow(`App unzipped OK, but we could not find any '${supportedAppExtensions}' ` +
351
468
  util.pluralize('bundle', supportedAppExtensions.length, false) +
352
469
  ` in it. Make sure your archive contains at least one package having ` +
353
470
  `'${supportedAppExtensions}' ${util.pluralize('extension', supportedAppExtensions.length, false)}`);
354
471
  }
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`);
472
+ logger.debug(`Extracted ${util.pluralize('bundle item', sortedBundleItems.length, true)} ` +
473
+ `from '${zipPath}' in ${Math.round(timer.getDuration().asMilliSeconds)}ms: ${sortedBundleItems}`);
474
+ const matchedBundle = _.first(sortedBundleItems);
475
+ logger.info(`Assuming '${matchedBundle}' is the correct bundle`);
358
476
  const dstPath = path.resolve(dstRoot, path.basename(matchedBundle));
359
477
  await fs.mv(path.resolve(tmpRoot, matchedBundle), dstPath, {mkdirp: true});
360
478
  return dstPath;
@@ -1,5 +1,5 @@
1
1
  import _ from 'lodash';
2
- import 'colors';
2
+ import '@colors/colors';
3
3
  import morgan from 'morgan';
4
4
  import log from './logger';
5
5
  import { MAX_LOG_BODY_LENGTH } from '../constants';
@@ -20,7 +20,7 @@ const MONITORED_METHODS = ['POST', 'PATCH'];
20
20
  const IDEMPOTENCY_KEY_HEADER = 'x-idempotency-key';
21
21
 
22
22
  process.on('exit', () => {
23
- const resPaths = IDEMPOTENT_RESPONSES.values()
23
+ const resPaths = [...IDEMPOTENT_RESPONSES.values()]
24
24
  .map(({response}) => response)
25
25
  .filter(Boolean);
26
26
  for (const resPath of resPaths) {
@@ -0,0 +1,25 @@
1
+ import _ from 'lodash';
2
+
3
+ function isW3cCaps (caps) {
4
+ if (!_.isPlainObject(caps)) {
5
+ return false;
6
+ }
7
+
8
+ const isFirstMatchValid = () => _.isArray(caps.firstMatch)
9
+ && !_.isEmpty(caps.firstMatch) && _.every(caps.firstMatch, _.isPlainObject);
10
+ const isAlwaysMatchValid = () => _.isPlainObject(caps.alwaysMatch);
11
+ if (_.has(caps, 'firstMatch') && _.has(caps, 'alwaysMatch')) {
12
+ return isFirstMatchValid() && isAlwaysMatchValid();
13
+ }
14
+ if (_.has(caps, 'firstMatch')) {
15
+ return isFirstMatchValid();
16
+ }
17
+ if (_.has(caps, 'alwaysMatch')) {
18
+ return isAlwaysMatchValid();
19
+ }
20
+ return false;
21
+ }
22
+
23
+ export {
24
+ isW3cCaps,
25
+ };
package/lib/index.js CHANGED
@@ -5,9 +5,9 @@ import * as driver from './basedriver/driver';
5
5
  import * as deviceSettings from './basedriver/device-settings';
6
6
 
7
7
  const { BaseDriver } = driver;
8
- const { DeviceSettings, BASEDRIVER_HANDLED_SETTINGS } = deviceSettings;
8
+ const { DeviceSettings } = deviceSettings;
9
9
 
10
- export { BaseDriver, DeviceSettings, BASEDRIVER_HANDLED_SETTINGS };
10
+ export { BaseDriver, DeviceSettings };
11
11
  export default BaseDriver;
12
12
 
13
13
 
@@ -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,10 +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
-
9
- const log = logger.getLogger('Protocol Converter');
10
-
11
7
 
12
8
  export const COMMAND_URLS_CONFLICTS = [
13
9
  {
@@ -42,20 +38,24 @@ export const COMMAND_URLS_CONFLICTS = [
42
38
  jsonwpConverter: (w3cUrl) => {
43
39
  const w3cPropertyRegex = /\/element\/([^/]+)\/property\/([^/]+)/;
44
40
  const jsonwpUrl = w3cUrl.replace(w3cPropertyRegex, '/element/$1/attribute/$2');
45
- log.info(`Converting W3C '${w3cUrl}' to '${jsonwpUrl}'`);
46
41
  return jsonwpUrl;
47
42
  },
48
43
  w3cConverter: (jsonwpUrl) => jsonwpUrl // Don't convert JSONWP URL to W3C. W3C accepts /attribute and /property
49
44
  }
50
45
  ];
51
-
52
46
  const {MJSONWP, W3C} = PROTOCOLS;
47
+ const DEFAULT_LOG = logger.getLogger('Protocol Converter');
53
48
 
54
49
 
55
50
  class ProtocolConverter {
56
- constructor (proxyFunc) {
51
+ constructor (proxyFunc, log = null) {
57
52
  this.proxyFunc = proxyFunc;
58
53
  this._downstreamProtocol = null;
54
+ this._log = log;
55
+ }
56
+
57
+ get log () {
58
+ return this._log ?? DEFAULT_LOG;
59
59
  }
60
60
 
61
61
  set downstreamProtocol (value) {
@@ -108,7 +108,7 @@ class ProtocolConverter {
108
108
  let response, resBody;
109
109
 
110
110
  const timeoutRequestObjects = this.getTimeoutRequestObjects(body);
111
- log.debug(`Will send the following request bodies to /timeouts: ${JSON.stringify(timeoutRequestObjects)}`);
111
+ this.log.debug(`Will send the following request bodies to /timeouts: ${JSON.stringify(timeoutRequestObjects)}`);
112
112
  for (const timeoutObj of timeoutRequestObjects) {
113
113
  [response, resBody] = await this.proxyFunc(url, method, timeoutObj);
114
114
 
@@ -131,14 +131,14 @@ class ProtocolConverter {
131
131
  const bodyObj = util.safeJsonParse(body);
132
132
  if (_.isPlainObject(bodyObj)) {
133
133
  if (this.downstreamProtocol === W3C && _.has(bodyObj, 'name') && !_.has(bodyObj, 'handle')) {
134
- log.debug(`Copied 'name' value '${bodyObj.name}' to 'handle' as per W3C spec`);
134
+ this.log.debug(`Copied 'name' value '${bodyObj.name}' to 'handle' as per W3C spec`);
135
135
  return await this.proxyFunc(url, method, {
136
136
  ...bodyObj,
137
137
  handle: bodyObj.name,
138
138
  });
139
139
  }
140
140
  if (this.downstreamProtocol === MJSONWP && _.has(bodyObj, 'handle') && !_.has(bodyObj, 'name')) {
141
- log.debug(`Copied 'handle' value '${bodyObj.handle}' to 'name' as per JSONWP spec`);
141
+ this.log.debug(`Copied 'handle' value '${bodyObj.handle}' to 'name' as per JSONWP spec`);
142
142
  return await this.proxyFunc(url, method, {
143
143
  ...bodyObj,
144
144
  name: bodyObj.handle,
@@ -157,12 +157,12 @@ class ProtocolConverter {
157
157
  value = _.isString(text)
158
158
  ? [...text]
159
159
  : (_.isArray(text) ? text : []);
160
- log.debug(`Added 'value' property ${JSON.stringify(value)} to 'setValue' request body`);
160
+ this.log.debug(`Added 'value' property ${JSON.stringify(value)} to 'setValue' request body`);
161
161
  } else if (!util.hasValue(text) && util.hasValue(value)) {
162
162
  text = _.isArray(value)
163
163
  ? value.join('')
164
164
  : (_.isString(value) ? value : '');
165
- log.debug(`Added 'text' property ${JSON.stringify(text)} to 'setValue' request body`);
165
+ this.log.debug(`Added 'text' property ${JSON.stringify(text)} to 'setValue' request body`);
166
166
  }
167
167
  return await this.proxyFunc(url, method, Object.assign({}, bodyObj, {
168
168
  text,
@@ -206,10 +206,7 @@ class ProtocolConverter {
206
206
  */
207
207
  async convertAndProxy (commandName, url, method, body) {
208
208
  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)];
209
+ return await this.proxyFunc(url, method, body);
213
210
  }
214
211
 
215
212
  // Same url, but different arguments
@@ -240,11 +237,11 @@ class ProtocolConverter {
240
237
  ? jsonwpConverter(url)
241
238
  : w3cConverter(url);
242
239
  if (rewrittenUrl === url) {
243
- log.debug(`Did not know how to rewrite the original URL '${url}' ` +
240
+ this.log.debug(`Did not know how to rewrite the original URL '${url}' ` +
244
241
  `for ${this.downstreamProtocol} protocol`);
245
242
  break;
246
243
  }
247
- log.info(`Rewrote the original URL '${url}' to '${rewrittenUrl}' ` +
244
+ this.log.info(`Rewrote the original URL '${url}' to '${rewrittenUrl}' ` +
248
245
  `for ${this.downstreamProtocol} protocol`);
249
246
  return await this.proxyFunc(rewrittenUrl, method, body);
250
247
  }