appium 2.0.0-beta.2 → 2.0.0-beta.20

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/README.md +9 -9
  2. package/build/lib/appium-config.schema.json +0 -0
  3. package/build/lib/appium.js +157 -53
  4. package/build/lib/cli/argparse-actions.js +104 -0
  5. package/build/lib/cli/args.js +115 -279
  6. package/build/lib/cli/driver-command.js +11 -1
  7. package/build/lib/cli/extension-command.js +60 -8
  8. package/build/lib/cli/extension.js +30 -7
  9. package/build/lib/cli/npm.js +17 -14
  10. package/build/lib/cli/parser.js +152 -89
  11. package/build/lib/cli/plugin-command.js +11 -1
  12. package/build/lib/cli/utils.js +29 -3
  13. package/build/lib/config-file.js +141 -0
  14. package/build/lib/config.js +76 -61
  15. package/build/lib/driver-config.js +42 -19
  16. package/build/lib/drivers.js +8 -4
  17. package/build/lib/ext-config-io.js +165 -0
  18. package/build/lib/extension-config.js +130 -61
  19. package/build/lib/grid-register.js +22 -24
  20. package/build/lib/logger.js +3 -3
  21. package/build/lib/logsink.js +11 -13
  22. package/build/lib/main.js +197 -77
  23. package/build/lib/plugin-config.js +20 -10
  24. package/build/lib/plugins.js +4 -2
  25. package/build/lib/schema/appium-config-schema.js +252 -0
  26. package/build/lib/schema/arg-spec.js +120 -0
  27. package/build/lib/schema/cli-args.js +173 -0
  28. package/build/lib/schema/cli-transformers.js +76 -0
  29. package/build/lib/schema/index.js +36 -0
  30. package/build/lib/schema/keywords.js +62 -0
  31. package/build/lib/schema/schema.js +357 -0
  32. package/build/lib/utils.js +44 -99
  33. package/lib/appium-config.schema.json +277 -0
  34. package/lib/appium.js +201 -65
  35. package/lib/cli/argparse-actions.js +77 -0
  36. package/lib/cli/args.js +174 -375
  37. package/lib/cli/driver-command.js +4 -0
  38. package/lib/cli/extension-command.js +70 -5
  39. package/lib/cli/extension.js +25 -5
  40. package/lib/cli/npm.js +18 -12
  41. package/lib/cli/parser.js +254 -79
  42. package/lib/cli/plugin-command.js +4 -0
  43. package/lib/cli/utils.js +21 -1
  44. package/lib/config-file.js +227 -0
  45. package/lib/config.js +109 -62
  46. package/lib/driver-config.js +66 -11
  47. package/lib/drivers.js +4 -1
  48. package/lib/ext-config-io.js +287 -0
  49. package/lib/extension-config.js +225 -67
  50. package/lib/grid-register.js +27 -24
  51. package/lib/logger.js +1 -1
  52. package/lib/logsink.js +10 -7
  53. package/lib/main.js +211 -77
  54. package/lib/plugin-config.js +34 -5
  55. package/lib/plugins.js +1 -0
  56. package/lib/schema/appium-config-schema.js +286 -0
  57. package/lib/schema/arg-spec.js +218 -0
  58. package/lib/schema/cli-args.js +273 -0
  59. package/lib/schema/cli-transformers.js +123 -0
  60. package/lib/schema/index.js +2 -0
  61. package/lib/schema/keywords.js +119 -0
  62. package/lib/schema/schema.js +577 -0
  63. package/lib/utils.js +42 -88
  64. package/package.json +55 -80
  65. package/postinstall.js +71 -0
  66. package/types/appium-config.d.ts +197 -0
  67. package/types/types.d.ts +201 -0
  68. package/CHANGELOG.md +0 -3515
  69. package/build/lib/cli/parser-helpers.js +0 -82
  70. package/lib/cli/parser-helpers.js +0 -79
package/lib/appium.js CHANGED
@@ -2,11 +2,11 @@ import _ from 'lodash';
2
2
  import log from './logger';
3
3
  import { getBuildInfo, updateBuildInfo, APPIUM_VER } from './config';
4
4
  import { findMatchingDriver } from './drivers';
5
- import { BaseDriver, errors, isSessionCommand } from 'appium-base-driver';
6
- import B from 'bluebird';
5
+ import { BaseDriver, errors, isSessionCommand,
6
+ CREATE_SESSION_COMMAND } from '@appium/base-driver';
7
7
  import AsyncLock from 'async-lock';
8
8
  import { parseCapsForInnerDriver, pullSettings } from './utils';
9
- import { util } from 'appium-support';
9
+ import { util } from '@appium/support';
10
10
 
11
11
  const desiredCapabilityConstraints = {
12
12
  automationName: {
@@ -27,7 +27,7 @@ class AppiumDriver extends BaseDriver {
27
27
  // It is necessary to set `--tmp` here since it should be set to
28
28
  // process.env.APPIUM_TMP_DIR once at an initial point in the Appium lifecycle.
29
29
  // The process argument will be referenced by BaseDriver.
30
- // Please call appium-support.tempDir module to apply this benefit.
30
+ // Please call @appium/support.tempDir module to apply this benefit.
31
31
  if (args.tmpDir) {
32
32
  process.env.APPIUM_TMP_DIR = args.tmpDir;
33
33
  }
@@ -39,7 +39,7 @@ class AppiumDriver extends BaseDriver {
39
39
  // the main Appium Driver has no new command timeout
40
40
  this.newCommandTimeoutMs = 0;
41
41
 
42
- this.args = Object.assign({}, args);
42
+ this.args = {...args};
43
43
 
44
44
  // Access to sessions list must be guarded with a Semaphore, because
45
45
  // it might be changed by other async calls at any time
@@ -51,12 +51,17 @@ class AppiumDriver extends BaseDriver {
51
51
  // It is not recommended to access this property directly from the outside
52
52
  this.pendingDrivers = {};
53
53
 
54
- this.plugins = [];
54
+ this.pluginClasses = []; // list of which plugins are active
55
+ this.sessionPlugins = {}; // map of sessions to actual plugin instances per session
56
+ this.sessionlessPlugins = []; // some commands are sessionless, so we need a set of plugins for them
55
57
 
56
58
  // allow this to happen in the background, so no `await`
57
59
  updateBuildInfo();
58
60
  }
59
61
 
62
+ /** @type {DriverConfig|undefined} */
63
+ driverConfig;
64
+
60
65
  /**
61
66
  * Cancel commands queueing for the umbrella Appium driver
62
67
  */
@@ -85,11 +90,20 @@ class AppiumDriver extends BaseDriver {
85
90
  .map(([id, driver]) => ({id, capabilities: driver.caps}));
86
91
  }
87
92
 
88
- printNewSessionAnnouncement (driverName, driverVersion) {
89
- const introString = driverVersion
93
+ printNewSessionAnnouncement (driverName, driverVersion, driverBaseVersion) {
94
+ log.info(driverVersion
90
95
  ? `Appium v${APPIUM_VER} creating new ${driverName} (v${driverVersion}) session`
91
- : `Appium v${APPIUM_VER} creating new ${driverName} session`;
92
- log.info(introString);
96
+ : `Appium v${APPIUM_VER} creating new ${driverName} session`
97
+ );
98
+ log.info(`Checking BaseDriver versions for Appium and ${driverName}`);
99
+ log.info(AppiumDriver.baseVersion
100
+ ? `Appium's BaseDriver version is ${AppiumDriver.baseVersion}`
101
+ : `Could not determine Appium's BaseDriver version`
102
+ );
103
+ log.info(driverBaseVersion
104
+ ? `${driverName}'s BaseDriver version is ${driverBaseVersion}`
105
+ : `Could not determine ${driverName}'s BaseDriver version`
106
+ );
93
107
  }
94
108
 
95
109
  /**
@@ -100,6 +114,21 @@ class AppiumDriver extends BaseDriver {
100
114
  return findMatchingDriver(...args);
101
115
  }
102
116
 
117
+ /**
118
+ * Validate and assign CLI args for a driver or plugin
119
+ *
120
+ * If the extension has provided a schema, validation has already happened.
121
+ * @param {import('./ext-config-io').ExtensionType} extType 'driver' or 'plugin'
122
+ * @param {string} extName the name of the extension
123
+ * @param {Object} extInstance the driver or plugin instance
124
+ */
125
+ assignCliArgsToExtension (extType, extName, extInstance) {
126
+ const cliArgs = this.args[extType]?.[extName];
127
+ if (!_.isEmpty(cliArgs)) {
128
+ extInstance.cliArgs = cliArgs;
129
+ }
130
+ }
131
+
103
132
  /**
104
133
  * Create a new session
105
134
  * @param {Object} jsonwpCaps JSONWP formatted desired capabilities
@@ -144,16 +173,18 @@ class AppiumDriver extends BaseDriver {
144
173
 
145
174
  const {
146
175
  driver: InnerDriver,
147
- version: driverVersion
176
+ version: driverVersion,
177
+ driverName
148
178
  } = this._findMatchingDriver(this.driverConfig, desiredCaps);
149
- this.printNewSessionAnnouncement(InnerDriver.name, driverVersion);
179
+ this.printNewSessionAnnouncement(InnerDriver.name, driverVersion, InnerDriver.baseVersion);
150
180
 
151
181
  if (this.args.sessionOverride) {
152
182
  await this.deleteAllSessions();
153
183
  }
154
184
 
155
185
  let runningDriversData, otherPendingDriversData;
156
- const d = new InnerDriver(this.args);
186
+
187
+ const driverInstance = new InnerDriver(this.args, true);
157
188
 
158
189
  // We want to assign security values directly on the driver. The driver
159
190
  // should not read security values from `this.opts` because those values
@@ -163,23 +194,28 @@ class AppiumDriver extends BaseDriver {
163
194
  log.info(`Applying relaxed security to '${InnerDriver.name}' as per ` +
164
195
  `server command line argument. All insecure features will be ` +
165
196
  `enabled unless explicitly disabled by --deny-insecure`);
166
- d.relaxedSecurityEnabled = true;
197
+ driverInstance.relaxedSecurityEnabled = true;
167
198
  }
168
199
 
169
200
  if (!_.isEmpty(this.args.denyInsecure)) {
170
201
  log.info('Explicitly preventing use of insecure features:');
171
202
  this.args.denyInsecure.map((a) => log.info(` ${a}`));
172
- d.denyInsecure = this.args.denyInsecure;
203
+ driverInstance.denyInsecure = this.args.denyInsecure;
173
204
  }
174
205
 
175
206
  if (!_.isEmpty(this.args.allowInsecure)) {
176
207
  log.info('Explicitly enabling use of insecure features:');
177
208
  this.args.allowInsecure.map((a) => log.info(` ${a}`));
178
- d.allowInsecure = this.args.allowInsecure;
209
+ driverInstance.allowInsecure = this.args.allowInsecure;
179
210
  }
180
211
 
212
+ // Likewise, any driver-specific CLI args that were passed in should be assigned directly to
213
+ // the driver so that they cannot be mimicked by a malicious user sending in capabilities
214
+ this.assignCliArgsToExtension('driver', driverName, driverInstance);
215
+
216
+
181
217
  // This assignment is required for correct web sockets functionality inside the driver
182
- d.server = this.server;
218
+ driverInstance.server = this.server;
183
219
  try {
184
220
  runningDriversData = await this.curSessionDataForDriver(InnerDriver);
185
221
  } catch (e) {
@@ -188,43 +224,43 @@ class AppiumDriver extends BaseDriver {
188
224
  await pendingDriversGuard.acquire(AppiumDriver.name, () => {
189
225
  this.pendingDrivers[InnerDriver.name] = this.pendingDrivers[InnerDriver.name] || [];
190
226
  otherPendingDriversData = this.pendingDrivers[InnerDriver.name].map((drv) => drv.driverData);
191
- this.pendingDrivers[InnerDriver.name].push(d);
227
+ this.pendingDrivers[InnerDriver.name].push(driverInstance);
192
228
  });
193
229
 
194
230
  try {
195
- [innerSessionId, dCaps] = await d.createSession(
231
+ [innerSessionId, dCaps] = await driverInstance.createSession(
196
232
  processedJsonwpCapabilities,
197
233
  reqCaps,
198
234
  processedW3CCapabilities,
199
235
  [...runningDriversData, ...otherPendingDriversData]
200
236
  );
201
- protocol = d.protocol;
237
+ protocol = driverInstance.protocol;
202
238
  await sessionsListGuard.acquire(AppiumDriver.name, () => {
203
- this.sessions[innerSessionId] = d;
239
+ this.sessions[innerSessionId] = driverInstance;
204
240
  });
205
241
  } finally {
206
242
  await pendingDriversGuard.acquire(AppiumDriver.name, () => {
207
- _.pull(this.pendingDrivers[InnerDriver.name], d);
243
+ _.pull(this.pendingDrivers[InnerDriver.name], driverInstance);
208
244
  });
209
245
  }
210
246
 
211
- this.attachUnexpectedShutdownHandler(d, innerSessionId);
247
+ this.attachUnexpectedShutdownHandler(driverInstance, innerSessionId);
212
248
 
213
249
  log.info(`New ${InnerDriver.name} session created successfully, session ` +
214
250
  `${innerSessionId} added to master session list`);
215
251
 
216
252
  // set the New Command Timeout for the inner driver
217
- d.startNewCommandTimeout();
253
+ driverInstance.startNewCommandTimeout();
218
254
 
219
255
  // apply initial values to Appium settings (if provided)
220
- if (d.isW3CProtocol() && !_.isEmpty(w3cSettings)) {
256
+ if (driverInstance.isW3CProtocol() && !_.isEmpty(w3cSettings)) {
221
257
  log.info(`Applying the initial values to Appium settings parsed from W3C caps: ` +
222
258
  JSON.stringify(w3cSettings));
223
- await d.updateSettings(w3cSettings);
224
- } else if (d.isMjsonwpProtocol() && !_.isEmpty(jwpSettings)) {
259
+ await driverInstance.updateSettings(w3cSettings);
260
+ } else if (driverInstance.isMjsonwpProtocol() && !_.isEmpty(jwpSettings)) {
225
261
  log.info(`Applying the initial values to Appium settings parsed from MJSONWP caps: ` +
226
262
  JSON.stringify(jwpSettings));
227
- await d.updateSettings(jwpSettings);
263
+ await driverInstance.updateSettings(jwpSettings);
228
264
  }
229
265
  } catch (error) {
230
266
  return {
@@ -240,33 +276,31 @@ class AppiumDriver extends BaseDriver {
240
276
  }
241
277
 
242
278
  attachUnexpectedShutdownHandler (driver, innerSessionId) {
243
- const removeSessionFromMasterList = (cause = new Error('Unknown error')) => {
244
- log.warn(`Closing session, cause was '${cause.message}'`);
279
+ const onShutdown = (cause = new Error('Unknown error')) => {
280
+ log.warn(`Ending session, cause was '${cause.message}'`);
281
+
282
+ if (this.sessionPlugins[innerSessionId]) {
283
+ for (const plugin of this.sessionPlugins[innerSessionId]) {
284
+ if (_.isFunction(plugin.onUnexpectedShutdown)) {
285
+ log.debug(`Plugin ${plugin.name} defines an unexpected shutdown handler; calling it now`);
286
+ try {
287
+ plugin.onUnexpectedShutdown(driver, cause);
288
+ } catch (e) {
289
+ log.warn(`Got an error when running plugin ${plugin.name} shutdown handler: ${e}`);
290
+ }
291
+ } else {
292
+ log.debug(`Plugin ${plugin.name} does not define an unexpected shutdown handler`);
293
+ }
294
+ }
295
+ }
296
+
245
297
  log.info(`Removing session '${innerSessionId}' from our master session list`);
246
298
  delete this.sessions[innerSessionId];
299
+ delete this.sessionPlugins[innerSessionId];
247
300
  };
248
301
 
249
- // eslint-disable-next-line promise/prefer-await-to-then
250
- if (_.isFunction((driver.onUnexpectedShutdown || {}).then)) {
251
- // TODO: Remove this block after all the drivers use base driver above v 5.0.0
252
- // Remove the session on unexpected shutdown, so that we are in a position
253
- // to open another session later on.
254
- driver.onUnexpectedShutdown
255
- // eslint-disable-next-line promise/prefer-await-to-then
256
- .then(() => {
257
- // if we get here, we've had an unexpected shutdown, so error
258
- throw new Error('Unexpected shutdown');
259
- })
260
- .catch((e) => {
261
- // if we cancelled the unexpected shutdown promise, that means we
262
- // no longer care about it, and can safely ignore it
263
- if (!(e instanceof B.CancellationError)) {
264
- removeSessionFromMasterList(e);
265
- }
266
- }); // this is a cancellable promise
267
- } else if (_.isFunction(driver.onUnexpectedShutdown)) {
268
- // since base driver v 5.0.0
269
- driver.onUnexpectedShutdown(removeSessionFromMasterList);
302
+ if (_.isFunction(driver.onUnexpectedShutdown)) {
303
+ driver.onUnexpectedShutdown(onShutdown);
270
304
  } else {
271
305
  log.warn(`Failed to attach the unexpected shutdown listener. ` +
272
306
  `Is 'onUnexpectedShutdown' method available for '${driver.constructor.name}'?`);
@@ -308,6 +342,7 @@ class AppiumDriver extends BaseDriver {
308
342
  // make the session unavailable, because who knows what state it might
309
343
  // be in otherwise
310
344
  delete this.sessions[sessionId];
345
+ delete this.sessionPlugins[sessionId];
311
346
  });
312
347
  return {
313
348
  protocol,
@@ -346,11 +381,50 @@ class AppiumDriver extends BaseDriver {
346
381
  }
347
382
  }
348
383
 
349
- pluginsToHandleCmd (cmd) {
350
- return this.plugins.filter((p) =>
351
- p.commands === true ||
352
- (_.isArray(p.commands) && _.includes(p.commands, cmd))
353
- );
384
+ /**
385
+ * Get the appropriate plugins for a session (or sessionless plugins)
386
+ *
387
+ * @param {?string} sessionId - the sessionId (or null) to use to find plugins
388
+ * @returns {Array} - array of plugin instances
389
+ */
390
+ pluginsForSession (sessionId = null) {
391
+ if (sessionId) {
392
+ if (!this.sessionPlugins[sessionId]) {
393
+ this.sessionPlugins[sessionId] = this.createPluginInstances();
394
+ }
395
+ return this.sessionPlugins[sessionId];
396
+ }
397
+
398
+ if (_.isEmpty(this.sessionlessPlugins)) {
399
+ this.sessionlessPlugins = this.createPluginInstances();
400
+ }
401
+ return this.sessionlessPlugins;
402
+ }
403
+
404
+ /**
405
+ * To get plugins for a command, we either get the plugin instances associated with the
406
+ * particular command's session, or in the case of sessionless plugins, pull from the set of
407
+ * plugin instances reserved for sessionless commands (and we lazily create plugin instances on
408
+ * first use)
409
+ *
410
+ * @param {string} cmd - the name of the command to find a plugin to handle
411
+ * @param {?string} sessionId - the particular session for which to find a plugin, or null if
412
+ * sessionless
413
+ */
414
+ pluginsToHandleCmd (cmd, sessionId = null) {
415
+ // to handle a given command, a plugin should either implement that command as a plugin
416
+ // instance method or it should implement a generic 'handle' method
417
+ return this.pluginsForSession(sessionId)
418
+ .filter((p) => _.isFunction(p[cmd]) || _.isFunction(p.handle));
419
+ }
420
+
421
+ createPluginInstances () {
422
+ return this.pluginClasses.map((PluginClass) => {
423
+ const name = PluginClass.pluginName;
424
+ const plugin = new PluginClass(name);
425
+ this.assignCliArgsToExtension('plugin', name, plugin);
426
+ return plugin;
427
+ });
354
428
  }
355
429
 
356
430
  async executeCommand (cmd, ...args) {
@@ -367,14 +441,21 @@ class AppiumDriver extends BaseDriver {
367
441
  const isUmbrellaCmd = !isGetStatus && isAppiumDriverCommand(cmd);
368
442
  const isSessionCmd = !isGetStatus && !isUmbrellaCmd;
369
443
 
370
- // get any plugins which are registered as handling this command
371
- const plugins = this.pluginsToHandleCmd(cmd);
444
+ // if a plugin override proxying for this command and that is why we are here instead of just
445
+ // letting the protocol proxy the command entirely, determine that, get the request object for
446
+ // use later on, then clean up the args
447
+ const reqForProxy = _.last(args)?.reqForProxy;
448
+ if (reqForProxy) {
449
+ args.pop();
450
+ }
451
+
372
452
 
373
453
  // first do some error checking. If we're requesting a session command execution, then make
374
454
  // sure that session actually exists on the session driver, and set the session driver itself
375
455
  let sessionId = null;
376
456
  let dstSession = null;
377
457
  let protocol = null;
458
+ let driver = this;
378
459
  if (isSessionCmd) {
379
460
  sessionId = _.last(args);
380
461
  dstSession = await sessionsListGuard.acquire(AppiumDriver.name, () => this.sessions[sessionId]);
@@ -383,8 +464,12 @@ class AppiumDriver extends BaseDriver {
383
464
  }
384
465
  // now save the response protocol given that the session driver's protocol might differ
385
466
  protocol = dstSession.protocol;
467
+ driver = dstSession;
386
468
  }
387
469
 
470
+ // get any plugins which are registered as handling this command
471
+ const plugins = this.pluginsToHandleCmd(cmd, sessionId);
472
+
388
473
  // now we define a 'cmdHandledBy' object which will keep track of which plugins have handled this
389
474
  // command. we care about this because (a) multiple plugins can handle the same command, and
390
475
  // (b) there's no guarantee that a plugin will actually call the next() method which runs the
@@ -406,6 +491,18 @@ class AppiumDriver extends BaseDriver {
406
491
  // if we make it here, we know that the default behavior is handled
407
492
  cmdHandledBy.default = true;
408
493
 
494
+ if (reqForProxy) {
495
+ // we would have proxied this command had a plugin not handled it, so the default behavior
496
+ // is to do the proxy and retrieve the result internally so it can be passed to the plugin
497
+ // in case it calls 'await next()'. This requires that the driver have defined
498
+ // 'proxyCommand' and not just 'proxyReqRes'.
499
+ if (!dstSession.proxyCommand) {
500
+ throw new NoDriverProxyCommandError();
501
+ }
502
+ return await dstSession.proxyCommand(reqForProxy.originalUrl, reqForProxy.method,
503
+ reqForProxy.body);
504
+ }
505
+
409
506
  if (isGetStatus) {
410
507
  return await this.getStatus();
411
508
  }
@@ -421,17 +518,30 @@ class AppiumDriver extends BaseDriver {
421
518
  };
422
519
 
423
520
  // now take our default behavior, wrap it with any number of plugin behaviors, and run it
424
- const wrappedCmd = this.wrapCommandWithPlugins({cmd, args, plugins, cmdHandledBy, next: defaultBehavior});
521
+ const wrappedCmd = this.wrapCommandWithPlugins({
522
+ driver, cmd, args, plugins, cmdHandledBy, next: defaultBehavior
523
+ });
425
524
  const res = await this.executeWrappedCommand({wrappedCmd, protocol});
426
525
 
427
526
  // if we had plugins, make sure to log out the helpful report about which plugins ended up
428
527
  // handling the command and which didn't
429
- plugins.length && this.logPluginHandlerReport({cmd, cmdHandledBy});
528
+ this.logPluginHandlerReport(plugins, {cmd, cmdHandledBy});
529
+
530
+ // And finally, if the command was createSession, we want to migrate any plugins which were
531
+ // previously sessionless to use the new sessionId, so that plugins can share state between
532
+ // their createSession method and other instance methods
533
+ if (cmd === CREATE_SESSION_COMMAND && this.sessionlessPlugins.length && !res.error) {
534
+ const sessionId = _.first(res.value);
535
+ log.info(`Promoting ${this.sessionlessPlugins.length} sessionless plugins to be attached ` +
536
+ `to session ID ${sessionId}`);
537
+ this.sessionPlugins[sessionId] = this.sessionlessPlugins;
538
+ this.sessionlessPlugins = [];
539
+ }
430
540
 
431
541
  return res;
432
542
  }
433
543
 
434
- wrapCommandWithPlugins ({cmd, args, next, cmdHandledBy, plugins}) {
544
+ wrapCommandWithPlugins ({driver, cmd, args, next, cmdHandledBy, plugins}) {
435
545
  plugins.length && log.info(`Plugins which can handle cmd '${cmd}': ${plugins.map((p) => p.name)}`);
436
546
 
437
547
  // now we can go through each plugin and wrap `next` around its own handler, passing the *old*
@@ -444,14 +554,23 @@ class AppiumDriver extends BaseDriver {
444
554
  next = ((_next) => async () => {
445
555
  log.info(`Plugin ${plugin.name} is now handling cmd '${cmd}'`);
446
556
  cmdHandledBy[plugin.name] = true; // if we make it here, this plugin has attempted to handle cmd
447
- return await plugin.handle(_next, this, cmd, ...args);
557
+ // first attempt to handle the command via a command-specific handler on the plugin
558
+ if (plugin[cmd]) {
559
+ return await plugin[cmd](_next, driver, ...args);
560
+ }
561
+ // otherwise, call the generic 'handle' method
562
+ return await plugin.handle(_next, driver, cmd, ...args);
448
563
  })(next);
449
564
  }
450
565
 
451
566
  return next;
452
567
  }
453
568
 
454
- logPluginHandlerReport ({cmd, cmdHandledBy}) {
569
+ logPluginHandlerReport (plugins, {cmd, cmdHandledBy}) {
570
+ if (!plugins.length) {
571
+ return;
572
+ }
573
+
455
574
  // at the end of the day, we have an object representing which plugins ended up getting
456
575
  // their code run as part of handling this command. Because plugins can choose *not* to
457
576
  // pass control to other plugins or to the default driver behavior, this is information
@@ -461,7 +580,7 @@ class AppiumDriver extends BaseDriver {
461
580
  const didHandle = Object.keys(cmdHandledBy).filter((k) => cmdHandledBy[k]);
462
581
  const didntHandle = Object.keys(cmdHandledBy).filter((k) => !cmdHandledBy[k]);
463
582
  if (didntHandle.length > 0) {
464
- log.info(`Command '${cmd}' was not handled by the following beahviors or plugins, even ` +
583
+ log.info(`Command '${cmd}' was *not* handled by the following behaviours or plugins, even ` +
465
584
  `though they were registered to handle it: ${JSON.stringify(didntHandle)}. The ` +
466
585
  `command *was* handled by these: ${JSON.stringify(didHandle)}.`);
467
586
  }
@@ -491,7 +610,6 @@ class AppiumDriver extends BaseDriver {
491
610
  return res;
492
611
  }
493
612
 
494
-
495
613
  proxyActive (sessionId) {
496
614
  const dstSession = this.sessions[sessionId];
497
615
  return dstSession && _.isFunction(dstSession.proxyActive) && dstSession.proxyActive(sessionId);
@@ -514,4 +632,22 @@ function isAppiumDriverCommand (cmd) {
514
632
  return !isSessionCommand(cmd) || cmd === 'deleteSession';
515
633
  }
516
634
 
635
+ /**
636
+ * Thrown when Appium tried to proxy a command using a driver's `proxyCommand` method but the
637
+ * method did not exist
638
+ */
639
+ export class NoDriverProxyCommandError extends Error {
640
+ /**
641
+ * @type {Readonly<string>}
642
+ */
643
+ code = 'APPIUMERR_NO_DRIVER_PROXYCOMMAND';
644
+
645
+ constructor () {
646
+ super(`The default behavior for this command was to proxy, but the driver ` +
647
+ `did not have the 'proxyCommand' method defined. To fully support ` +
648
+ `plugins, drivers should have 'proxyCommand' set to a jwpProxy object's ` +
649
+ `'command()' method, in addition to the normal 'proxyReqRes'`);
650
+ }
651
+ }
652
+
517
653
  export { AppiumDriver };
@@ -0,0 +1,77 @@
1
+ import { Action } from 'argparse';
2
+
3
+
4
+ const DEFAULT_CAPS_ARG = '--default-capabilities';
5
+
6
+
7
+ class StoreDeprecatedAction extends Action {
8
+ constructor (options = {}) {
9
+ const opts = Object.assign({}, options);
10
+ let helpPrefix = '[DEPRECATED]';
11
+ if (opts.deprecated_for) {
12
+ helpPrefix = `[DEPRECATED, use ${opts.deprecated_for} instead]`;
13
+ delete opts.deprecated_for;
14
+ }
15
+ if (opts.help) {
16
+ opts.help = `${helpPrefix} - ${opts.help}`;
17
+ } else {
18
+ opts.help = helpPrefix;
19
+ }
20
+ super(opts);
21
+ }
22
+
23
+ call (parser, namespace, values) {
24
+ namespace[this.dest] = values;
25
+ }
26
+ }
27
+
28
+
29
+ class StoreDeprecatedTrueAction extends StoreDeprecatedAction {
30
+ constructor (options = {}) {
31
+ super(Object.assign({}, options, {const: true, nargs: 0}));
32
+ }
33
+
34
+ call (parser, namespace) {
35
+ namespace[this.dest] = this.const;
36
+ }
37
+ }
38
+
39
+
40
+ class StoreDeprecatedDefaultCapabilityAction extends StoreDeprecatedAction {
41
+ constructor (options = {}) {
42
+ super(Object.assign({}, options, {deprecated_for: DEFAULT_CAPS_ARG}));
43
+ }
44
+
45
+ _writeDefaultCap (namespace, value) {
46
+ namespace[this.dest] = value;
47
+ if (value === this.default) {
48
+ return;
49
+ }
50
+
51
+ if (!namespace.defaultCapabilities) {
52
+ namespace.defaultCapabilities = {};
53
+ }
54
+ namespace.defaultCapabilities[this.dest] = value;
55
+ }
56
+
57
+ call (parser, namespace, values) {
58
+ this._writeDefaultCap(namespace, values);
59
+ }
60
+ }
61
+
62
+
63
+ class StoreDeprecatedDefaultCapabilityTrueAction extends StoreDeprecatedDefaultCapabilityAction {
64
+ constructor (options = {}) {
65
+ super(Object.assign({}, options, {const: true, nargs: 0}));
66
+ }
67
+
68
+ call (parser, namespace) {
69
+ this._writeDefaultCap(namespace, this.const);
70
+ }
71
+ }
72
+
73
+ export {
74
+ StoreDeprecatedAction, StoreDeprecatedTrueAction,
75
+ StoreDeprecatedDefaultCapabilityAction, StoreDeprecatedDefaultCapabilityTrueAction,
76
+ DEFAULT_CAPS_ARG,
77
+ };