appium 2.0.0-beta.2 → 2.0.0-beta.23

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 (122) hide show
  1. package/README.md +9 -9
  2. package/build/check-npm-pack-files.js +23 -0
  3. package/build/commands-yml/parse.js +319 -0
  4. package/build/commands-yml/validator.js +130 -0
  5. package/build/index.js +19 -0
  6. package/build/lib/appium-config.schema.json +0 -0
  7. package/build/lib/appium.js +160 -53
  8. package/build/lib/cli/args.js +115 -279
  9. package/build/lib/cli/driver-command.js +11 -1
  10. package/build/lib/cli/extension-command.js +60 -8
  11. package/build/lib/cli/extension.js +30 -7
  12. package/build/lib/cli/npm.js +43 -29
  13. package/build/lib/cli/parser.js +156 -89
  14. package/build/lib/cli/plugin-command.js +11 -1
  15. package/build/lib/cli/utils.js +29 -3
  16. package/build/lib/config-file.js +141 -0
  17. package/build/lib/config.js +53 -65
  18. package/build/lib/driver-config.js +42 -19
  19. package/build/lib/drivers.js +8 -4
  20. package/build/lib/ext-config-io.js +165 -0
  21. package/build/lib/extension-config.js +130 -61
  22. package/build/lib/grid-register.js +22 -24
  23. package/build/lib/logger.js +3 -3
  24. package/build/lib/logsink.js +11 -13
  25. package/build/lib/main.js +197 -77
  26. package/build/lib/plugin-config.js +21 -11
  27. package/build/lib/plugins.js +4 -2
  28. package/build/lib/schema/appium-config-schema.js +253 -0
  29. package/build/lib/schema/arg-spec.js +120 -0
  30. package/build/lib/schema/cli-args.js +188 -0
  31. package/build/lib/schema/cli-transformers.js +76 -0
  32. package/build/lib/schema/index.js +36 -0
  33. package/build/lib/schema/keywords.js +72 -0
  34. package/build/lib/schema/schema.js +357 -0
  35. package/build/lib/utils.js +44 -99
  36. package/build/postinstall.js +90 -0
  37. package/build/test/cli/cli-e2e-specs.js +221 -0
  38. package/build/test/cli/cli-helpers.js +86 -0
  39. package/build/test/cli/cli-specs.js +71 -0
  40. package/build/test/cli/fixtures/test-driver/package.json +27 -0
  41. package/build/test/cli/schema-args-specs.js +48 -0
  42. package/build/test/cli/schema-e2e-specs.js +47 -0
  43. package/build/test/config-e2e-specs.js +112 -0
  44. package/build/test/config-file-e2e-specs.js +209 -0
  45. package/build/test/config-file-specs.js +281 -0
  46. package/build/test/config-specs.js +159 -0
  47. package/build/test/driver-e2e-specs.js +435 -0
  48. package/build/test/driver-specs.js +321 -0
  49. package/build/test/ext-config-io-specs.js +181 -0
  50. package/build/test/extension-config-specs.js +365 -0
  51. package/build/test/fixtures/allow-feat.txt +5 -0
  52. package/build/test/fixtures/caps.json +3 -0
  53. package/build/test/fixtures/config/allow-insecure.txt +3 -0
  54. package/build/test/fixtures/config/appium.config.bad-nodeconfig.json +5 -0
  55. package/build/test/fixtures/config/appium.config.bad.json +32 -0
  56. package/build/test/fixtures/config/appium.config.ext-good.json +9 -0
  57. package/build/test/fixtures/config/appium.config.ext-unknown-props.json +10 -0
  58. package/build/test/fixtures/config/appium.config.good.js +40 -0
  59. package/build/test/fixtures/config/appium.config.good.json +33 -0
  60. package/build/test/fixtures/config/appium.config.good.yaml +30 -0
  61. package/build/test/fixtures/config/appium.config.invalid.json +31 -0
  62. package/build/test/fixtures/config/appium.config.security-array.json +5 -0
  63. package/build/test/fixtures/config/appium.config.security-delimited.json +5 -0
  64. package/build/test/fixtures/config/appium.config.security-path.json +5 -0
  65. package/build/test/fixtures/config/driver-fake.config.json +8 -0
  66. package/build/test/fixtures/config/nodeconfig.json +3 -0
  67. package/build/test/fixtures/config/plugin-fake.config.json +0 -0
  68. package/build/test/fixtures/default-args.js +35 -0
  69. package/build/test/fixtures/deny-feat.txt +5 -0
  70. package/build/test/fixtures/driver.schema.js +20 -0
  71. package/build/test/fixtures/extensions.yaml +27 -0
  72. package/build/test/fixtures/flattened-schema.js +504 -0
  73. package/build/test/fixtures/plugin.schema.js +20 -0
  74. package/build/test/fixtures/schema-with-extensions.js +28 -0
  75. package/build/test/grid-register-specs.js +74 -0
  76. package/build/test/helpers.js +75 -0
  77. package/build/test/logger-specs.js +76 -0
  78. package/build/test/npm-specs.js +20 -0
  79. package/build/test/parser-specs.js +314 -0
  80. package/build/test/plugin-e2e-specs.js +316 -0
  81. package/build/test/schema/arg-spec-specs.js +70 -0
  82. package/build/test/schema/cli-args-specs.js +431 -0
  83. package/build/test/schema/schema-specs.js +389 -0
  84. package/build/test/utils-specs.js +266 -0
  85. package/index.js +11 -0
  86. package/lib/appium-config.schema.json +278 -0
  87. package/lib/appium.js +207 -65
  88. package/lib/cli/args.js +174 -375
  89. package/lib/cli/driver-command.js +4 -0
  90. package/lib/cli/extension-command.js +70 -5
  91. package/lib/cli/extension.js +25 -5
  92. package/lib/cli/npm.js +86 -18
  93. package/lib/cli/parser.js +257 -79
  94. package/lib/cli/plugin-command.js +4 -0
  95. package/lib/cli/utils.js +21 -1
  96. package/lib/config-file.js +227 -0
  97. package/lib/config.js +84 -63
  98. package/lib/driver-config.js +66 -11
  99. package/lib/drivers.js +4 -1
  100. package/lib/ext-config-io.js +287 -0
  101. package/lib/extension-config.js +225 -67
  102. package/lib/grid-register.js +27 -24
  103. package/lib/logger.js +1 -1
  104. package/lib/logsink.js +10 -7
  105. package/lib/main.js +214 -77
  106. package/lib/plugin-config.js +35 -6
  107. package/lib/plugins.js +1 -0
  108. package/lib/schema/appium-config-schema.js +287 -0
  109. package/lib/schema/arg-spec.js +222 -0
  110. package/lib/schema/cli-args.js +285 -0
  111. package/lib/schema/cli-transformers.js +123 -0
  112. package/lib/schema/index.js +2 -0
  113. package/lib/schema/keywords.js +135 -0
  114. package/lib/schema/schema.js +577 -0
  115. package/lib/utils.js +42 -88
  116. package/package.json +55 -84
  117. package/postinstall.js +71 -0
  118. package/types/appium-config.d.ts +197 -0
  119. package/types/types.d.ts +201 -0
  120. package/CHANGELOG.md +0 -3515
  121. package/build/lib/cli/parser-helpers.js +0 -82
  122. 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,34 @@ 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;
219
+
220
+ // Drivers/plugins might also want to know where they are hosted
221
+ driverInstance.serverHost = this.args.address;
222
+ driverInstance.serverPort = this.args.port;
223
+ driverInstance.serverPath = this.args.basePath;
224
+
183
225
  try {
184
226
  runningDriversData = await this.curSessionDataForDriver(InnerDriver);
185
227
  } catch (e) {
@@ -188,43 +230,43 @@ class AppiumDriver extends BaseDriver {
188
230
  await pendingDriversGuard.acquire(AppiumDriver.name, () => {
189
231
  this.pendingDrivers[InnerDriver.name] = this.pendingDrivers[InnerDriver.name] || [];
190
232
  otherPendingDriversData = this.pendingDrivers[InnerDriver.name].map((drv) => drv.driverData);
191
- this.pendingDrivers[InnerDriver.name].push(d);
233
+ this.pendingDrivers[InnerDriver.name].push(driverInstance);
192
234
  });
193
235
 
194
236
  try {
195
- [innerSessionId, dCaps] = await d.createSession(
237
+ [innerSessionId, dCaps] = await driverInstance.createSession(
196
238
  processedJsonwpCapabilities,
197
239
  reqCaps,
198
240
  processedW3CCapabilities,
199
241
  [...runningDriversData, ...otherPendingDriversData]
200
242
  );
201
- protocol = d.protocol;
243
+ protocol = driverInstance.protocol;
202
244
  await sessionsListGuard.acquire(AppiumDriver.name, () => {
203
- this.sessions[innerSessionId] = d;
245
+ this.sessions[innerSessionId] = driverInstance;
204
246
  });
205
247
  } finally {
206
248
  await pendingDriversGuard.acquire(AppiumDriver.name, () => {
207
- _.pull(this.pendingDrivers[InnerDriver.name], d);
249
+ _.pull(this.pendingDrivers[InnerDriver.name], driverInstance);
208
250
  });
209
251
  }
210
252
 
211
- this.attachUnexpectedShutdownHandler(d, innerSessionId);
253
+ this.attachUnexpectedShutdownHandler(driverInstance, innerSessionId);
212
254
 
213
255
  log.info(`New ${InnerDriver.name} session created successfully, session ` +
214
256
  `${innerSessionId} added to master session list`);
215
257
 
216
258
  // set the New Command Timeout for the inner driver
217
- d.startNewCommandTimeout();
259
+ driverInstance.startNewCommandTimeout();
218
260
 
219
261
  // apply initial values to Appium settings (if provided)
220
- if (d.isW3CProtocol() && !_.isEmpty(w3cSettings)) {
262
+ if (driverInstance.isW3CProtocol() && !_.isEmpty(w3cSettings)) {
221
263
  log.info(`Applying the initial values to Appium settings parsed from W3C caps: ` +
222
264
  JSON.stringify(w3cSettings));
223
- await d.updateSettings(w3cSettings);
224
- } else if (d.isMjsonwpProtocol() && !_.isEmpty(jwpSettings)) {
265
+ await driverInstance.updateSettings(w3cSettings);
266
+ } else if (driverInstance.isMjsonwpProtocol() && !_.isEmpty(jwpSettings)) {
225
267
  log.info(`Applying the initial values to Appium settings parsed from MJSONWP caps: ` +
226
268
  JSON.stringify(jwpSettings));
227
- await d.updateSettings(jwpSettings);
269
+ await driverInstance.updateSettings(jwpSettings);
228
270
  }
229
271
  } catch (error) {
230
272
  return {
@@ -240,33 +282,31 @@ class AppiumDriver extends BaseDriver {
240
282
  }
241
283
 
242
284
  attachUnexpectedShutdownHandler (driver, innerSessionId) {
243
- const removeSessionFromMasterList = (cause = new Error('Unknown error')) => {
244
- log.warn(`Closing session, cause was '${cause.message}'`);
285
+ const onShutdown = (cause = new Error('Unknown error')) => {
286
+ log.warn(`Ending session, cause was '${cause.message}'`);
287
+
288
+ if (this.sessionPlugins[innerSessionId]) {
289
+ for (const plugin of this.sessionPlugins[innerSessionId]) {
290
+ if (_.isFunction(plugin.onUnexpectedShutdown)) {
291
+ log.debug(`Plugin ${plugin.name} defines an unexpected shutdown handler; calling it now`);
292
+ try {
293
+ plugin.onUnexpectedShutdown(driver, cause);
294
+ } catch (e) {
295
+ log.warn(`Got an error when running plugin ${plugin.name} shutdown handler: ${e}`);
296
+ }
297
+ } else {
298
+ log.debug(`Plugin ${plugin.name} does not define an unexpected shutdown handler`);
299
+ }
300
+ }
301
+ }
302
+
245
303
  log.info(`Removing session '${innerSessionId}' from our master session list`);
246
304
  delete this.sessions[innerSessionId];
305
+ delete this.sessionPlugins[innerSessionId];
247
306
  };
248
307
 
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);
308
+ if (_.isFunction(driver.onUnexpectedShutdown)) {
309
+ driver.onUnexpectedShutdown(onShutdown);
270
310
  } else {
271
311
  log.warn(`Failed to attach the unexpected shutdown listener. ` +
272
312
  `Is 'onUnexpectedShutdown' method available for '${driver.constructor.name}'?`);
@@ -308,6 +348,7 @@ class AppiumDriver extends BaseDriver {
308
348
  // make the session unavailable, because who knows what state it might
309
349
  // be in otherwise
310
350
  delete this.sessions[sessionId];
351
+ delete this.sessionPlugins[sessionId];
311
352
  });
312
353
  return {
313
354
  protocol,
@@ -346,11 +387,50 @@ class AppiumDriver extends BaseDriver {
346
387
  }
347
388
  }
348
389
 
349
- pluginsToHandleCmd (cmd) {
350
- return this.plugins.filter((p) =>
351
- p.commands === true ||
352
- (_.isArray(p.commands) && _.includes(p.commands, cmd))
353
- );
390
+ /**
391
+ * Get the appropriate plugins for a session (or sessionless plugins)
392
+ *
393
+ * @param {?string} sessionId - the sessionId (or null) to use to find plugins
394
+ * @returns {Array} - array of plugin instances
395
+ */
396
+ pluginsForSession (sessionId = null) {
397
+ if (sessionId) {
398
+ if (!this.sessionPlugins[sessionId]) {
399
+ this.sessionPlugins[sessionId] = this.createPluginInstances();
400
+ }
401
+ return this.sessionPlugins[sessionId];
402
+ }
403
+
404
+ if (_.isEmpty(this.sessionlessPlugins)) {
405
+ this.sessionlessPlugins = this.createPluginInstances();
406
+ }
407
+ return this.sessionlessPlugins;
408
+ }
409
+
410
+ /**
411
+ * To get plugins for a command, we either get the plugin instances associated with the
412
+ * particular command's session, or in the case of sessionless plugins, pull from the set of
413
+ * plugin instances reserved for sessionless commands (and we lazily create plugin instances on
414
+ * first use)
415
+ *
416
+ * @param {string} cmd - the name of the command to find a plugin to handle
417
+ * @param {?string} sessionId - the particular session for which to find a plugin, or null if
418
+ * sessionless
419
+ */
420
+ pluginsToHandleCmd (cmd, sessionId = null) {
421
+ // to handle a given command, a plugin should either implement that command as a plugin
422
+ // instance method or it should implement a generic 'handle' method
423
+ return this.pluginsForSession(sessionId)
424
+ .filter((p) => _.isFunction(p[cmd]) || _.isFunction(p.handle));
425
+ }
426
+
427
+ createPluginInstances () {
428
+ return this.pluginClasses.map((PluginClass) => {
429
+ const name = PluginClass.pluginName;
430
+ const plugin = new PluginClass(name);
431
+ this.assignCliArgsToExtension('plugin', name, plugin);
432
+ return plugin;
433
+ });
354
434
  }
355
435
 
356
436
  async executeCommand (cmd, ...args) {
@@ -367,14 +447,21 @@ class AppiumDriver extends BaseDriver {
367
447
  const isUmbrellaCmd = !isGetStatus && isAppiumDriverCommand(cmd);
368
448
  const isSessionCmd = !isGetStatus && !isUmbrellaCmd;
369
449
 
370
- // get any plugins which are registered as handling this command
371
- const plugins = this.pluginsToHandleCmd(cmd);
450
+ // if a plugin override proxying for this command and that is why we are here instead of just
451
+ // letting the protocol proxy the command entirely, determine that, get the request object for
452
+ // use later on, then clean up the args
453
+ const reqForProxy = _.last(args)?.reqForProxy;
454
+ if (reqForProxy) {
455
+ args.pop();
456
+ }
457
+
372
458
 
373
459
  // first do some error checking. If we're requesting a session command execution, then make
374
460
  // sure that session actually exists on the session driver, and set the session driver itself
375
461
  let sessionId = null;
376
462
  let dstSession = null;
377
463
  let protocol = null;
464
+ let driver = this;
378
465
  if (isSessionCmd) {
379
466
  sessionId = _.last(args);
380
467
  dstSession = await sessionsListGuard.acquire(AppiumDriver.name, () => this.sessions[sessionId]);
@@ -383,8 +470,12 @@ class AppiumDriver extends BaseDriver {
383
470
  }
384
471
  // now save the response protocol given that the session driver's protocol might differ
385
472
  protocol = dstSession.protocol;
473
+ driver = dstSession;
386
474
  }
387
475
 
476
+ // get any plugins which are registered as handling this command
477
+ const plugins = this.pluginsToHandleCmd(cmd, sessionId);
478
+
388
479
  // now we define a 'cmdHandledBy' object which will keep track of which plugins have handled this
389
480
  // command. we care about this because (a) multiple plugins can handle the same command, and
390
481
  // (b) there's no guarantee that a plugin will actually call the next() method which runs the
@@ -406,6 +497,18 @@ class AppiumDriver extends BaseDriver {
406
497
  // if we make it here, we know that the default behavior is handled
407
498
  cmdHandledBy.default = true;
408
499
 
500
+ if (reqForProxy) {
501
+ // we would have proxied this command had a plugin not handled it, so the default behavior
502
+ // is to do the proxy and retrieve the result internally so it can be passed to the plugin
503
+ // in case it calls 'await next()'. This requires that the driver have defined
504
+ // 'proxyCommand' and not just 'proxyReqRes'.
505
+ if (!dstSession.proxyCommand) {
506
+ throw new NoDriverProxyCommandError();
507
+ }
508
+ return await dstSession.proxyCommand(reqForProxy.originalUrl, reqForProxy.method,
509
+ reqForProxy.body);
510
+ }
511
+
409
512
  if (isGetStatus) {
410
513
  return await this.getStatus();
411
514
  }
@@ -421,17 +524,30 @@ class AppiumDriver extends BaseDriver {
421
524
  };
422
525
 
423
526
  // 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});
527
+ const wrappedCmd = this.wrapCommandWithPlugins({
528
+ driver, cmd, args, plugins, cmdHandledBy, next: defaultBehavior
529
+ });
425
530
  const res = await this.executeWrappedCommand({wrappedCmd, protocol});
426
531
 
427
532
  // if we had plugins, make sure to log out the helpful report about which plugins ended up
428
533
  // handling the command and which didn't
429
- plugins.length && this.logPluginHandlerReport({cmd, cmdHandledBy});
534
+ this.logPluginHandlerReport(plugins, {cmd, cmdHandledBy});
535
+
536
+ // And finally, if the command was createSession, we want to migrate any plugins which were
537
+ // previously sessionless to use the new sessionId, so that plugins can share state between
538
+ // their createSession method and other instance methods
539
+ if (cmd === CREATE_SESSION_COMMAND && this.sessionlessPlugins.length && !res.error) {
540
+ const sessionId = _.first(res.value);
541
+ log.info(`Promoting ${this.sessionlessPlugins.length} sessionless plugins to be attached ` +
542
+ `to session ID ${sessionId}`);
543
+ this.sessionPlugins[sessionId] = this.sessionlessPlugins;
544
+ this.sessionlessPlugins = [];
545
+ }
430
546
 
431
547
  return res;
432
548
  }
433
549
 
434
- wrapCommandWithPlugins ({cmd, args, next, cmdHandledBy, plugins}) {
550
+ wrapCommandWithPlugins ({driver, cmd, args, next, cmdHandledBy, plugins}) {
435
551
  plugins.length && log.info(`Plugins which can handle cmd '${cmd}': ${plugins.map((p) => p.name)}`);
436
552
 
437
553
  // now we can go through each plugin and wrap `next` around its own handler, passing the *old*
@@ -444,14 +560,23 @@ class AppiumDriver extends BaseDriver {
444
560
  next = ((_next) => async () => {
445
561
  log.info(`Plugin ${plugin.name} is now handling cmd '${cmd}'`);
446
562
  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);
563
+ // first attempt to handle the command via a command-specific handler on the plugin
564
+ if (plugin[cmd]) {
565
+ return await plugin[cmd](_next, driver, ...args);
566
+ }
567
+ // otherwise, call the generic 'handle' method
568
+ return await plugin.handle(_next, driver, cmd, ...args);
448
569
  })(next);
449
570
  }
450
571
 
451
572
  return next;
452
573
  }
453
574
 
454
- logPluginHandlerReport ({cmd, cmdHandledBy}) {
575
+ logPluginHandlerReport (plugins, {cmd, cmdHandledBy}) {
576
+ if (!plugins.length) {
577
+ return;
578
+ }
579
+
455
580
  // at the end of the day, we have an object representing which plugins ended up getting
456
581
  // their code run as part of handling this command. Because plugins can choose *not* to
457
582
  // pass control to other plugins or to the default driver behavior, this is information
@@ -461,7 +586,7 @@ class AppiumDriver extends BaseDriver {
461
586
  const didHandle = Object.keys(cmdHandledBy).filter((k) => cmdHandledBy[k]);
462
587
  const didntHandle = Object.keys(cmdHandledBy).filter((k) => !cmdHandledBy[k]);
463
588
  if (didntHandle.length > 0) {
464
- log.info(`Command '${cmd}' was not handled by the following beahviors or plugins, even ` +
589
+ log.info(`Command '${cmd}' was *not* handled by the following behaviours or plugins, even ` +
465
590
  `though they were registered to handle it: ${JSON.stringify(didntHandle)}. The ` +
466
591
  `command *was* handled by these: ${JSON.stringify(didHandle)}.`);
467
592
  }
@@ -491,7 +616,6 @@ class AppiumDriver extends BaseDriver {
491
616
  return res;
492
617
  }
493
618
 
494
-
495
619
  proxyActive (sessionId) {
496
620
  const dstSession = this.sessions[sessionId];
497
621
  return dstSession && _.isFunction(dstSession.proxyActive) && dstSession.proxyActive(sessionId);
@@ -514,4 +638,22 @@ function isAppiumDriverCommand (cmd) {
514
638
  return !isSessionCommand(cmd) || cmd === 'deleteSession';
515
639
  }
516
640
 
641
+ /**
642
+ * Thrown when Appium tried to proxy a command using a driver's `proxyCommand` method but the
643
+ * method did not exist
644
+ */
645
+ export class NoDriverProxyCommandError extends Error {
646
+ /**
647
+ * @type {Readonly<string>}
648
+ */
649
+ code = 'APPIUMERR_NO_DRIVER_PROXYCOMMAND';
650
+
651
+ constructor () {
652
+ super(`The default behavior for this command was to proxy, but the driver ` +
653
+ `did not have the 'proxyCommand' method defined. To fully support ` +
654
+ `plugins, drivers should have 'proxyCommand' set to a jwpProxy object's ` +
655
+ `'command()' method, in addition to the normal 'proxyReqRes'`);
656
+ }
657
+ }
658
+
517
659
  export { AppiumDriver };