appium 2.0.0-beta.5 → 2.0.0-beta.53

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 (199) hide show
  1. package/README.md +156 -63
  2. package/build/lib/appium.d.ts +226 -0
  3. package/build/lib/appium.d.ts.map +1 -0
  4. package/build/lib/appium.js +694 -439
  5. package/build/lib/appium.js.map +1 -0
  6. package/build/lib/cli/args.d.ts +17 -0
  7. package/build/lib/cli/args.d.ts.map +1 -0
  8. package/build/lib/cli/args.js +253 -319
  9. package/build/lib/cli/args.js.map +1 -0
  10. package/build/lib/cli/driver-command.d.ts +102 -0
  11. package/build/lib/cli/driver-command.d.ts.map +1 -0
  12. package/build/lib/cli/driver-command.js +126 -81
  13. package/build/lib/cli/driver-command.js.map +1 -0
  14. package/build/lib/cli/extension-command.d.ts +385 -0
  15. package/build/lib/cli/extension-command.d.ts.map +1 -0
  16. package/build/lib/cli/extension-command.js +731 -383
  17. package/build/lib/cli/extension-command.js.map +1 -0
  18. package/build/lib/cli/extension.d.ts +23 -0
  19. package/build/lib/cli/extension.d.ts.map +1 -0
  20. package/build/lib/cli/extension.js +71 -54
  21. package/build/lib/cli/extension.js.map +1 -0
  22. package/build/lib/cli/parser.d.ts +84 -0
  23. package/build/lib/cli/parser.d.ts.map +1 -0
  24. package/build/lib/cli/parser.js +240 -128
  25. package/build/lib/cli/parser.js.map +1 -0
  26. package/build/lib/cli/plugin-command.d.ts +99 -0
  27. package/build/lib/cli/plugin-command.d.ts.map +1 -0
  28. package/build/lib/cli/plugin-command.js +120 -81
  29. package/build/lib/cli/plugin-command.js.map +1 -0
  30. package/build/lib/cli/utils.d.ts +29 -0
  31. package/build/lib/cli/utils.d.ts.map +1 -0
  32. package/build/lib/cli/utils.js +72 -51
  33. package/build/lib/cli/utils.js.map +1 -0
  34. package/build/lib/config-file.d.ts +100 -0
  35. package/build/lib/config-file.d.ts.map +1 -0
  36. package/build/lib/config-file.js +207 -0
  37. package/build/lib/config-file.js.map +1 -0
  38. package/build/lib/config.d.ts +49 -0
  39. package/build/lib/config.d.ts.map +1 -0
  40. package/build/lib/config.js +265 -201
  41. package/build/lib/config.js.map +1 -0
  42. package/build/lib/constants.d.ts +49 -0
  43. package/build/lib/constants.d.ts.map +1 -0
  44. package/build/lib/constants.js +66 -0
  45. package/build/lib/constants.js.map +1 -0
  46. package/build/lib/extension/driver-config.d.ts +82 -0
  47. package/build/lib/extension/driver-config.d.ts.map +1 -0
  48. package/build/lib/extension/driver-config.js +212 -0
  49. package/build/lib/extension/driver-config.js.map +1 -0
  50. package/build/lib/extension/extension-config.d.ts +249 -0
  51. package/build/lib/extension/extension-config.d.ts.map +1 -0
  52. package/build/lib/extension/extension-config.js +581 -0
  53. package/build/lib/extension/extension-config.js.map +1 -0
  54. package/build/lib/extension/index.d.ts +48 -0
  55. package/build/lib/extension/index.d.ts.map +1 -0
  56. package/build/lib/extension/index.js +105 -0
  57. package/build/lib/extension/index.js.map +1 -0
  58. package/build/lib/extension/manifest-migrations.d.ts +27 -0
  59. package/build/lib/extension/manifest-migrations.d.ts.map +1 -0
  60. package/build/lib/extension/manifest-migrations.js +118 -0
  61. package/build/lib/extension/manifest-migrations.js.map +1 -0
  62. package/build/lib/extension/manifest.d.ts +145 -0
  63. package/build/lib/extension/manifest.d.ts.map +1 -0
  64. package/build/lib/extension/manifest.js +521 -0
  65. package/build/lib/extension/manifest.js.map +1 -0
  66. package/build/lib/extension/package-changed.d.ts +11 -0
  67. package/build/lib/extension/package-changed.d.ts.map +1 -0
  68. package/build/lib/extension/package-changed.js +62 -0
  69. package/build/lib/extension/package-changed.js.map +1 -0
  70. package/build/lib/extension/plugin-config.d.ts +56 -0
  71. package/build/lib/extension/plugin-config.d.ts.map +1 -0
  72. package/build/lib/extension/plugin-config.js +102 -0
  73. package/build/lib/extension/plugin-config.js.map +1 -0
  74. package/build/lib/grid-register.d.ts +10 -0
  75. package/build/lib/grid-register.d.ts.map +1 -0
  76. package/build/lib/grid-register.js +122 -144
  77. package/build/lib/grid-register.js.map +1 -0
  78. package/build/lib/logger.d.ts +3 -0
  79. package/build/lib/logger.d.ts.map +1 -0
  80. package/build/lib/logger.js +5 -17
  81. package/build/lib/logger.js.map +1 -0
  82. package/build/lib/logsink.d.ts +4 -0
  83. package/build/lib/logsink.d.ts.map +1 -0
  84. package/build/lib/logsink.js +190 -187
  85. package/build/lib/logsink.js.map +1 -0
  86. package/build/lib/main.d.ts +62 -0
  87. package/build/lib/main.d.ts.map +1 -0
  88. package/build/lib/main.js +348 -228
  89. package/build/lib/main.js.map +1 -0
  90. package/build/lib/schema/arg-spec.d.ts +143 -0
  91. package/build/lib/schema/arg-spec.d.ts.map +1 -0
  92. package/build/lib/schema/arg-spec.js +164 -0
  93. package/build/lib/schema/arg-spec.js.map +1 -0
  94. package/build/lib/schema/cli-args.d.ts +19 -0
  95. package/build/lib/schema/cli-args.d.ts.map +1 -0
  96. package/build/lib/schema/cli-args.js +217 -0
  97. package/build/lib/schema/cli-args.js.map +1 -0
  98. package/build/lib/schema/cli-transformers.d.ts +5 -0
  99. package/build/lib/schema/cli-transformers.d.ts.map +1 -0
  100. package/build/lib/schema/cli-transformers.js +124 -0
  101. package/build/lib/schema/cli-transformers.js.map +1 -0
  102. package/build/lib/schema/index.d.ts +3 -0
  103. package/build/lib/schema/index.d.ts.map +1 -0
  104. package/build/lib/schema/index.js +19 -0
  105. package/build/lib/schema/index.js.map +1 -0
  106. package/build/lib/schema/keywords.d.ts +24 -0
  107. package/build/lib/schema/keywords.d.ts.map +1 -0
  108. package/build/lib/schema/keywords.js +128 -0
  109. package/build/lib/schema/keywords.js.map +1 -0
  110. package/build/lib/schema/schema.d.ts +259 -0
  111. package/build/lib/schema/schema.d.ts.map +1 -0
  112. package/build/lib/schema/schema.js +615 -0
  113. package/build/lib/schema/schema.js.map +1 -0
  114. package/build/lib/utils.d.ts +121 -0
  115. package/build/lib/utils.d.ts.map +1 -0
  116. package/build/lib/utils.js +351 -273
  117. package/build/lib/utils.js.map +1 -0
  118. package/build/tsconfig.tsbuildinfo +1 -0
  119. package/build/types/cli.d.ts +134 -0
  120. package/build/types/cli.d.ts.map +1 -0
  121. package/build/types/cli.js +3 -0
  122. package/build/types/cli.js.map +1 -0
  123. package/build/types/index.d.ts +15 -0
  124. package/build/types/index.d.ts.map +1 -0
  125. package/build/types/index.js +19 -0
  126. package/build/types/index.js.map +1 -0
  127. package/build/types/manifest/base.d.ts +135 -0
  128. package/build/types/manifest/base.d.ts.map +1 -0
  129. package/build/types/manifest/base.js +3 -0
  130. package/build/types/manifest/base.js.map +1 -0
  131. package/build/types/manifest/index.d.ts +19 -0
  132. package/build/types/manifest/index.d.ts.map +1 -0
  133. package/build/types/manifest/index.js +40 -0
  134. package/build/types/manifest/index.js.map +1 -0
  135. package/build/types/manifest/v3.d.ts +139 -0
  136. package/build/types/manifest/v3.d.ts.map +1 -0
  137. package/build/types/manifest/v3.js +3 -0
  138. package/build/types/manifest/v3.js.map +1 -0
  139. package/driver.d.ts +1 -0
  140. package/driver.js +14 -0
  141. package/index.js +11 -0
  142. package/lib/appium.js +535 -186
  143. package/lib/cli/args.js +267 -422
  144. package/lib/cli/driver-command.js +126 -23
  145. package/lib/cli/extension-command.js +679 -271
  146. package/lib/cli/extension.js +48 -17
  147. package/lib/cli/parser.js +263 -83
  148. package/lib/cli/plugin-command.js +115 -20
  149. package/lib/cli/utils.js +24 -10
  150. package/lib/config-file.js +220 -0
  151. package/lib/config.js +244 -110
  152. package/lib/constants.js +71 -0
  153. package/lib/extension/driver-config.js +250 -0
  154. package/lib/extension/extension-config.js +682 -0
  155. package/lib/extension/index.js +116 -0
  156. package/lib/extension/manifest-migrations.js +120 -0
  157. package/lib/extension/manifest.js +573 -0
  158. package/lib/extension/package-changed.js +64 -0
  159. package/lib/extension/plugin-config.js +112 -0
  160. package/lib/grid-register.js +49 -35
  161. package/lib/logger.js +1 -2
  162. package/lib/logsink.js +64 -38
  163. package/lib/main.js +321 -101
  164. package/lib/schema/arg-spec.js +229 -0
  165. package/lib/schema/cli-args.js +238 -0
  166. package/lib/schema/cli-transformers.js +119 -0
  167. package/lib/schema/index.js +2 -0
  168. package/lib/schema/keywords.js +136 -0
  169. package/lib/schema/schema.js +722 -0
  170. package/lib/utils.js +291 -167
  171. package/package.json +83 -84
  172. package/plugin.d.ts +1 -0
  173. package/plugin.js +13 -0
  174. package/scripts/autoinstall-extensions.js +237 -0
  175. package/support.d.ts +1 -0
  176. package/support.js +13 -0
  177. package/tsconfig.json +25 -0
  178. package/types/cli.ts +193 -0
  179. package/types/index.ts +20 -0
  180. package/types/manifest/README.md +30 -0
  181. package/types/manifest/base.ts +158 -0
  182. package/types/manifest/index.ts +27 -0
  183. package/types/manifest/v3.ts +161 -0
  184. package/CHANGELOG.md +0 -3515
  185. package/bin/ios-webkit-debug-proxy-launcher.js +0 -71
  186. package/build/lib/cli/npm.js +0 -208
  187. package/build/lib/cli/parser-helpers.js +0 -82
  188. package/build/lib/driver-config.js +0 -77
  189. package/build/lib/drivers.js +0 -96
  190. package/build/lib/extension-config.js +0 -253
  191. package/build/lib/plugin-config.js +0 -59
  192. package/build/lib/plugins.js +0 -14
  193. package/lib/cli/npm.js +0 -184
  194. package/lib/cli/parser-helpers.js +0 -79
  195. package/lib/driver-config.js +0 -46
  196. package/lib/drivers.js +0 -81
  197. package/lib/extension-config.js +0 -209
  198. package/lib/plugin-config.js +0 -34
  199. package/lib/plugins.js +0 -10
package/lib/appium.js CHANGED
@@ -1,14 +1,23 @@
1
+ /* eslint-disable no-unused-vars */
1
2
  import _ from 'lodash';
2
- import log from './logger';
3
- import { getBuildInfo, updateBuildInfo, APPIUM_VER } from './config';
4
- import { findMatchingDriver } from './drivers';
5
- import { BaseDriver, errors, isSessionCommand } from 'appium-base-driver';
6
- import B from 'bluebird';
3
+ import {getBuildInfo, updateBuildInfo, APPIUM_VER} from './config';
4
+ import {
5
+ BaseDriver,
6
+ DriverCore,
7
+ errors,
8
+ isSessionCommand,
9
+ CREATE_SESSION_COMMAND,
10
+ DELETE_SESSION_COMMAND,
11
+ GET_STATUS_COMMAND,
12
+ promoteAppiumOptions,
13
+ } from '@appium/base-driver';
7
14
  import AsyncLock from 'async-lock';
8
- import { parseCapsForInnerDriver, pullSettings } from './utils';
9
- import { util } from 'appium-support';
15
+ import {parseCapsForInnerDriver, pullSettings} from './utils';
16
+ import {util, node, logger} from '@appium/support';
17
+ import {getDefaultsForExtension} from './schema';
18
+ import {DRIVER_TYPE} from './constants';
10
19
 
11
- const desiredCapabilityConstraints = {
20
+ const desiredCapabilityConstraints = /** @type {const} */ ({
12
21
  automationName: {
13
22
  presence: true,
14
23
  isString: true,
@@ -17,109 +26,212 @@ const desiredCapabilityConstraints = {
17
26
  presence: true,
18
27
  isString: true,
19
28
  },
20
- };
29
+ });
30
+ /**
31
+ * @typedef {typeof desiredCapabilityConstraints} AppiumDriverConstraints
32
+ */
21
33
 
22
34
  const sessionsListGuard = new AsyncLock();
23
35
  const pendingDriversGuard = new AsyncLock();
24
36
 
25
- class AppiumDriver extends BaseDriver {
26
- constructor (args) {
27
- // It is necessary to set `--tmp` here since it should be set to
28
- // process.env.APPIUM_TMP_DIR once at an initial point in the Appium lifecycle.
29
- // The process argument will be referenced by BaseDriver.
30
- // Please call appium-support.tempDir module to apply this benefit.
31
- if (args.tmpDir) {
32
- process.env.APPIUM_TMP_DIR = args.tmpDir;
33
- }
37
+ /**
38
+ * @implements {SessionHandler}
39
+ */
40
+ class AppiumDriver extends DriverCore {
41
+ /**
42
+ * Access to sessions list must be guarded with a Semaphore, because
43
+ * it might be changed by other async calls at any time
44
+ * It is not recommended to access this property directly from the outside
45
+ * @type {Record<string,ExternalDriver>}
46
+ */
47
+ sessions = {};
34
48
 
35
- super(args);
49
+ /**
50
+ * Access to pending drivers list must be guarded with a Semaphore, because
51
+ * it might be changed by other async calls at any time
52
+ * It is not recommended to access this property directly from the outside
53
+ * @type {Record<string,ExternalDriver[]>}
54
+ */
55
+ pendingDrivers = {};
36
56
 
37
- this.desiredCapConstraints = desiredCapabilityConstraints;
57
+ /**
58
+ * Note that {@linkcode AppiumDriver} has no `newCommandTimeout` method.
59
+ * `AppiumDriver` does not set and observe its own timeouts; individual
60
+ * sessions (managed drivers) do instead.
61
+ */
62
+ newCommandTimeoutMs = 0;
38
63
 
39
- // the main Appium Driver has no new command timeout
40
- this.newCommandTimeoutMs = 0;
64
+ /**
65
+ * List of active plugins
66
+ * @type {Map<PluginClass,string>}
67
+ */
68
+ pluginClasses;
41
69
 
42
- this.args = Object.assign({}, args);
70
+ /**
71
+ * map of sessions to actual plugin instances per session
72
+ * @type {Record<string,InstanceType<PluginClass>[]>}
73
+ */
74
+ sessionPlugins = {};
43
75
 
44
- // Access to sessions list must be guarded with a Semaphore, because
45
- // it might be changed by other async calls at any time
46
- // It is not recommended to access this property directly from the outside
47
- this.sessions = {};
76
+ /**
77
+ * some commands are sessionless, so we need a set of plugins for them
78
+ * @type {InstanceType<PluginClass>[]}
79
+ */
80
+ sessionlessPlugins = [];
81
+
82
+ /** @type {DriverConfig} */
83
+ driverConfig;
84
+
85
+ /** @type {AppiumServer} */
86
+ server;
87
+
88
+ desiredCapConstraints = desiredCapabilityConstraints;
89
+
90
+ /** @type {DriverOpts} */
91
+ args;
92
+
93
+ /**
94
+ * @param {DriverOpts} opts
95
+ */
96
+ constructor(opts) {
97
+ // It is necessary to set `--tmp` here since it should be set to
98
+ // process.env.APPIUM_TMP_DIR once at an initial point in the Appium lifecycle.
99
+ // The process argument will be referenced by BaseDriver.
100
+ // Please call @appium/support.tempDir module to apply this benefit.
101
+ if (opts.tmpDir) {
102
+ process.env.APPIUM_TMP_DIR = opts.tmpDir;
103
+ }
48
104
 
49
- // Access to pending drivers list must be guarded with a Semaphore, because
50
- // it might be changed by other async calls at any time
51
- // It is not recommended to access this property directly from the outside
52
- this.pendingDrivers = {};
105
+ super(opts);
53
106
 
54
- this.plugins = [];
107
+ this.args = {...opts};
55
108
 
56
109
  // allow this to happen in the background, so no `await`
57
- updateBuildInfo();
110
+ (async () => {
111
+ try {
112
+ await updateBuildInfo();
113
+ } catch (e) {
114
+ // make sure we catch any possible errors to avoid unhandled rejections
115
+ this.log.debug(`Cannot fetch Appium build info: ${e.message}`);
116
+ }
117
+ })();
118
+ }
119
+
120
+ /**
121
+ * Retrieves logger instance for the current umbrella driver instance
122
+ */
123
+ get log() {
124
+ if (!this._log) {
125
+ const instanceName = `${this.constructor.name}@${node.getObjectId(this).substring(0, 4)}`;
126
+ this._log = logger.getLogger(instanceName);
127
+ }
128
+ return this._log;
58
129
  }
59
130
 
60
131
  /**
61
132
  * Cancel commands queueing for the umbrella Appium driver
62
133
  */
63
- get isCommandsQueueEnabled () {
134
+ get isCommandsQueueEnabled() {
64
135
  return false;
65
136
  }
66
137
 
67
- sessionExists (sessionId) {
138
+ sessionExists(sessionId) {
68
139
  const dstSession = this.sessions[sessionId];
69
140
  return dstSession && dstSession.sessionId !== null;
70
141
  }
71
142
 
72
- driverForSession (sessionId) {
143
+ driverForSession(sessionId) {
73
144
  return this.sessions[sessionId];
74
145
  }
75
146
 
76
- async getStatus () { // eslint-disable-line require-await
147
+ // eslint-disable-next-line require-await
148
+ async getStatus() {
77
149
  return {
78
150
  build: _.clone(getBuildInfo()),
79
151
  };
80
152
  }
81
153
 
82
- async getSessions () {
83
- const sessions = await sessionsListGuard.acquire(AppiumDriver.name, () => this.sessions);
84
- return _.toPairs(sessions)
85
- .map(([id, driver]) => ({id, capabilities: driver.caps}));
154
+ // eslint-disable-next-line require-await
155
+ async getSessions() {
156
+ return _.toPairs(this.sessions).map(([id, driver]) => ({
157
+ id,
158
+ capabilities: driver.caps,
159
+ }));
86
160
  }
87
161
 
88
- printNewSessionAnnouncement (driverName, driverVersion) {
89
- const introString = driverVersion
90
- ? `Appium v${APPIUM_VER} creating new ${driverName} (v${driverVersion}) session`
91
- : `Appium v${APPIUM_VER} creating new ${driverName} session`;
92
- log.info(introString);
162
+ printNewSessionAnnouncement(driverName, driverVersion, driverBaseVersion) {
163
+ this.log.info(
164
+ driverVersion
165
+ ? `Appium v${APPIUM_VER} creating new ${driverName} (v${driverVersion}) session`
166
+ : `Appium v${APPIUM_VER} creating new ${driverName} session`
167
+ );
168
+ this.log.info(`Checking BaseDriver versions for Appium and ${driverName}`);
169
+ this.log.info(
170
+ AppiumDriver.baseVersion
171
+ ? `Appium's BaseDriver version is ${AppiumDriver.baseVersion}`
172
+ : `Could not determine Appium's BaseDriver version`
173
+ );
174
+ this.log.info(
175
+ driverBaseVersion
176
+ ? `${driverName}'s BaseDriver version is ${driverBaseVersion}`
177
+ : `Could not determine ${driverName}'s BaseDriver version`
178
+ );
93
179
  }
94
180
 
95
181
  /**
96
- * This is just an alias for driver.js's method, which is necessary for
97
- * mocking in the test suite
182
+ * Retrieves all CLI arguments for a specific plugin.
183
+ * @param {string} extName - Plugin name
184
+ * @returns {StringRecord} Arguments object. If none, an empty object.
98
185
  */
99
- _findMatchingDriver (...args) {
100
- return findMatchingDriver(...args);
186
+ getCliArgsForPlugin(extName) {
187
+ return /** @type {StringRecord} */ (this.args.plugin?.[extName] ?? {});
188
+ }
189
+
190
+ /**
191
+ * Retrieves CLI args for a specific driver.
192
+ *
193
+ * _Any arg which is equal to its default value will not be present in the returned object._
194
+ *
195
+ * _Note that this behavior currently (May 18 2022) differs from how plugins are handled_ (see {@linkcode AppiumDriver.getCliArgsForPlugin}).
196
+ * @param {string} extName - Driver name
197
+ * @returns {StringRecord|undefined} Arguments object. If none, `undefined`
198
+ */
199
+ getCliArgsForDriver(extName) {
200
+ const allCliArgsForExt = /** @type {StringRecord|undefined} */ (this.args.driver?.[extName]);
201
+
202
+ if (!_.isEmpty(allCliArgsForExt)) {
203
+ const defaults = getDefaultsForExtension(DRIVER_TYPE, extName);
204
+ const cliArgs = _.isEmpty(defaults)
205
+ ? allCliArgsForExt
206
+ : _.omitBy(allCliArgsForExt, (value, key) => _.isEqual(defaults[key], value));
207
+ if (!_.isEmpty(cliArgs)) {
208
+ return cliArgs;
209
+ }
210
+ }
101
211
  }
102
212
 
103
213
  /**
104
214
  * Create a new session
105
- * @param {Object} jsonwpCaps JSONWP formatted desired capabilities
106
- * @param {Object} reqCaps Required capabilities (JSONWP standard)
107
- * @param {Object} w3cCapabilities W3C capabilities
108
- * @return {Array} Unique session ID and capabilities
215
+ * @param {W3CCapabilities<AppiumDriverConstraints>} jsonwpCaps JSONWP formatted desired capabilities
216
+ * @param {W3CCapabilities<AppiumDriverConstraints>} reqCaps Required capabilities (JSONWP standard)
217
+ * @param {W3CCapabilities<AppiumDriverConstraints>} w3cCapabilities W3C capabilities
218
+ * @param {DriverData[]} [driverData]
109
219
  */
110
- async createSession (jsonwpCaps, reqCaps, w3cCapabilities) {
220
+ async createSession(jsonwpCaps, reqCaps, w3cCapabilities, driverData) {
111
221
  const defaultCapabilities = _.cloneDeep(this.args.defaultCapabilities);
112
222
  const defaultSettings = pullSettings(defaultCapabilities);
113
223
  jsonwpCaps = _.cloneDeep(jsonwpCaps);
114
- const jwpSettings = Object.assign({}, defaultSettings, pullSettings(jsonwpCaps));
224
+ const jwpSettings = {...defaultSettings, ...pullSettings(jsonwpCaps)};
115
225
  w3cCapabilities = _.cloneDeep(w3cCapabilities);
116
226
  // It is possible that the client only provides caps using JSONWP standard,
117
227
  // although firstMatch/alwaysMatch properties are still present.
118
228
  // In such case we assume the client understands W3C protocol and merge the given
119
229
  // JSONWP caps to W3C caps
120
- const w3cSettings = Object.assign({}, jwpSettings);
121
- Object.assign(w3cSettings, pullSettings((w3cCapabilities || {}).alwaysMatch || {}));
122
- for (const firstMatchEntry of ((w3cCapabilities || {}).firstMatch || [])) {
230
+ const w3cSettings = {
231
+ ...jwpSettings,
232
+ ...pullSettings((w3cCapabilities ?? {}).alwaysMatch ?? {}),
233
+ };
234
+ for (const firstMatchEntry of (w3cCapabilities ?? {}).firstMatch ?? []) {
123
235
  Object.assign(w3cSettings, pullSettings(firstMatchEntry));
124
236
  }
125
237
 
@@ -129,14 +241,17 @@ class AppiumDriver extends BaseDriver {
129
241
  // Parse the caps into a format that the InnerDriver will accept
130
242
  const parsedCaps = parseCapsForInnerDriver(
131
243
  jsonwpCaps,
132
- w3cCapabilities,
244
+ promoteAppiumOptions(w3cCapabilities),
133
245
  this.desiredCapConstraints,
134
246
  defaultCapabilities
135
247
  );
136
248
 
137
- const {desiredCaps, processedJsonwpCapabilities, processedW3CCapabilities, error} = parsedCaps;
249
+ const {desiredCaps, processedJsonwpCapabilities, processedW3CCapabilities} =
250
+ /** @type {import('./utils').ParsedDriverCaps<AppiumDriverConstraints>} */ (parsedCaps);
138
251
  protocol = parsedCaps.protocol;
139
-
252
+ const error = /** @type {import('./utils').InvalidCaps<AppiumDriverConstraints>} */ (
253
+ parsedCaps
254
+ ).error;
140
255
  // If the parsing of the caps produced an error, throw it in here
141
256
  if (error) {
142
257
  throw error;
@@ -144,87 +259,118 @@ class AppiumDriver extends BaseDriver {
144
259
 
145
260
  const {
146
261
  driver: InnerDriver,
147
- version: driverVersion
148
- } = this._findMatchingDriver(this.driverConfig, desiredCaps);
149
- this.printNewSessionAnnouncement(InnerDriver.name, driverVersion);
262
+ version: driverVersion,
263
+ driverName,
264
+ } = this.driverConfig.findMatchingDriver(desiredCaps);
265
+ this.printNewSessionAnnouncement(InnerDriver.name, driverVersion, InnerDriver.baseVersion);
150
266
 
151
267
  if (this.args.sessionOverride) {
152
268
  await this.deleteAllSessions();
153
269
  }
154
270
 
155
- let runningDriversData, otherPendingDriversData;
156
- const d = new InnerDriver(this.args);
271
+ /**
272
+ * @type {DriverData[]}
273
+ */
274
+ let runningDriversData = [];
275
+ /**
276
+ * @type {DriverData[]}
277
+ */
278
+ let otherPendingDriversData = [];
279
+
280
+ const driverInstance = new InnerDriver(this.args, true);
157
281
 
158
282
  // We want to assign security values directly on the driver. The driver
159
283
  // should not read security values from `this.opts` because those values
160
284
  // could have been set by a malicious user via capabilities, whereas we
161
285
  // want a guarantee the values were set by the appium server admin
162
286
  if (this.args.relaxedSecurityEnabled) {
163
- log.info(`Applying relaxed security to '${InnerDriver.name}' as per ` +
164
- `server command line argument. All insecure features will be ` +
165
- `enabled unless explicitly disabled by --deny-insecure`);
166
- d.relaxedSecurityEnabled = true;
287
+ this.log.info(
288
+ `Applying relaxed security to '${InnerDriver.name}' as per ` +
289
+ `server command line argument. All insecure features will be ` +
290
+ `enabled unless explicitly disabled by --deny-insecure`
291
+ );
292
+ driverInstance.relaxedSecurityEnabled = true;
167
293
  }
168
294
 
169
295
  if (!_.isEmpty(this.args.denyInsecure)) {
170
- log.info('Explicitly preventing use of insecure features:');
171
- this.args.denyInsecure.map((a) => log.info(` ${a}`));
172
- d.denyInsecure = this.args.denyInsecure;
296
+ this.log.info('Explicitly preventing use of insecure features:');
297
+ this.args.denyInsecure.map((a) => this.log.info(` ${a}`));
298
+ driverInstance.denyInsecure = this.args.denyInsecure;
173
299
  }
174
300
 
175
301
  if (!_.isEmpty(this.args.allowInsecure)) {
176
- log.info('Explicitly enabling use of insecure features:');
177
- this.args.allowInsecure.map((a) => log.info(` ${a}`));
178
- d.allowInsecure = this.args.allowInsecure;
302
+ this.log.info('Explicitly enabling use of insecure features:');
303
+ this.args.allowInsecure.map((a) => this.log.info(` ${a}`));
304
+ driverInstance.allowInsecure = this.args.allowInsecure;
305
+ }
306
+
307
+ // Likewise, any driver-specific CLI args that were passed in should be assigned directly to
308
+ // the driver so that they cannot be mimicked by a malicious user sending in capabilities
309
+ const cliArgs = this.getCliArgsForDriver(driverName);
310
+ if (!_.isUndefined(cliArgs)) {
311
+ driverInstance.cliArgs = cliArgs;
179
312
  }
180
313
 
181
314
  // This assignment is required for correct web sockets functionality inside the driver
182
- d.server = this.server;
315
+ // Drivers/plugins might also want to know where they are hosted
316
+
317
+ // XXX: temporary hack to work around #16747
318
+ driverInstance.server = this.server;
319
+ driverInstance.serverHost = this.args.address;
320
+ driverInstance.serverPort = this.args.port;
321
+ driverInstance.serverPath = this.args.basePath;
322
+
183
323
  try {
184
- runningDriversData = await this.curSessionDataForDriver(InnerDriver);
324
+ runningDriversData = (await this.curSessionDataForDriver(InnerDriver)) ?? [];
185
325
  } catch (e) {
186
326
  throw new errors.SessionNotCreatedError(e.message);
187
327
  }
188
328
  await pendingDriversGuard.acquire(AppiumDriver.name, () => {
189
329
  this.pendingDrivers[InnerDriver.name] = this.pendingDrivers[InnerDriver.name] || [];
190
- otherPendingDriversData = this.pendingDrivers[InnerDriver.name].map((drv) => drv.driverData);
191
- this.pendingDrivers[InnerDriver.name].push(d);
330
+ otherPendingDriversData = _.compact(
331
+ this.pendingDrivers[InnerDriver.name].map((drv) => drv.driverData)
332
+ );
333
+ this.pendingDrivers[InnerDriver.name].push(driverInstance);
192
334
  });
193
335
 
194
336
  try {
195
- [innerSessionId, dCaps] = await d.createSession(
337
+ [innerSessionId, dCaps] = await driverInstance.createSession(
196
338
  processedJsonwpCapabilities,
197
339
  reqCaps,
198
340
  processedW3CCapabilities,
199
341
  [...runningDriversData, ...otherPendingDriversData]
200
342
  );
201
- protocol = d.protocol;
202
- await sessionsListGuard.acquire(AppiumDriver.name, () => {
203
- this.sessions[innerSessionId] = d;
204
- });
343
+ protocol = driverInstance.protocol;
344
+ this.sessions[innerSessionId] = driverInstance;
205
345
  } finally {
206
346
  await pendingDriversGuard.acquire(AppiumDriver.name, () => {
207
- _.pull(this.pendingDrivers[InnerDriver.name], d);
347
+ _.pull(this.pendingDrivers[InnerDriver.name], driverInstance);
208
348
  });
209
349
  }
210
350
 
211
- this.attachUnexpectedShutdownHandler(d, innerSessionId);
351
+ this.attachUnexpectedShutdownHandler(driverInstance, innerSessionId);
212
352
 
213
- log.info(`New ${InnerDriver.name} session created successfully, session ` +
214
- `${innerSessionId} added to master session list`);
353
+ this.log.info(
354
+ `New ${InnerDriver.name} session created successfully, session ` +
355
+ `${innerSessionId} added to master session list`
356
+ );
215
357
 
216
358
  // set the New Command Timeout for the inner driver
217
- d.startNewCommandTimeout();
359
+ driverInstance.startNewCommandTimeout();
218
360
 
219
361
  // apply initial values to Appium settings (if provided)
220
- if (d.isW3CProtocol() && !_.isEmpty(w3cSettings)) {
221
- log.info(`Applying the initial values to Appium settings parsed from W3C caps: ` +
222
- JSON.stringify(w3cSettings));
223
- await d.updateSettings(w3cSettings);
224
- } else if (d.isMjsonwpProtocol() && !_.isEmpty(jwpSettings)) {
225
- log.info(`Applying the initial values to Appium settings parsed from MJSONWP caps: ` +
226
- JSON.stringify(jwpSettings));
227
- await d.updateSettings(jwpSettings);
362
+ if (driverInstance.isW3CProtocol() && !_.isEmpty(w3cSettings)) {
363
+ this.log.info(
364
+ `Applying the initial values to Appium settings parsed from W3C caps: ` +
365
+ JSON.stringify(w3cSettings)
366
+ );
367
+ await driverInstance.updateSettings(w3cSettings);
368
+ } else if (driverInstance.isMjsonwpProtocol() && !_.isEmpty(jwpSettings)) {
369
+ this.log.info(
370
+ `Applying the initial values to Appium settings parsed from MJSONWP caps: ` +
371
+ JSON.stringify(jwpSettings)
372
+ );
373
+ await driverInstance.updateSettings(jwpSettings);
228
374
  }
229
375
  } catch (error) {
230
376
  return {
@@ -235,86 +381,114 @@ class AppiumDriver extends BaseDriver {
235
381
 
236
382
  return {
237
383
  protocol,
238
- value: [innerSessionId, dCaps, protocol]
384
+ value: [innerSessionId, dCaps, protocol],
239
385
  };
240
386
  }
241
387
 
242
- attachUnexpectedShutdownHandler (driver, innerSessionId) {
243
- const removeSessionFromMasterList = (cause = new Error('Unknown error')) => {
244
- log.warn(`Closing session, cause was '${cause.message}'`);
245
- log.info(`Removing session '${innerSessionId}' from our master session list`);
388
+ /**
389
+ *
390
+ * @param {Driver} driver
391
+ * @param {string} innerSessionId
392
+ */
393
+ attachUnexpectedShutdownHandler(driver, innerSessionId) {
394
+ const onShutdown = (cause = new Error('Unknown error')) => {
395
+ this.log.warn(`Ending session, cause was '${cause.message}'`);
396
+
397
+ if (this.sessionPlugins[innerSessionId]) {
398
+ for (const plugin of this.sessionPlugins[innerSessionId]) {
399
+ if (_.isFunction(plugin.onUnexpectedShutdown)) {
400
+ this.log.debug(
401
+ `Plugin ${plugin.name} defines an unexpected shutdown handler; calling it now`
402
+ );
403
+ try {
404
+ plugin.onUnexpectedShutdown(driver, cause);
405
+ } catch (e) {
406
+ this.log.warn(
407
+ `Got an error when running plugin ${plugin.name} shutdown handler: ${e}`
408
+ );
409
+ }
410
+ } else {
411
+ this.log.debug(`Plugin ${plugin.name} does not define an unexpected shutdown handler`);
412
+ }
413
+ }
414
+ }
415
+
416
+ this.log.info(`Removing session '${innerSessionId}' from our master session list`);
246
417
  delete this.sessions[innerSessionId];
418
+ delete this.sessionPlugins[innerSessionId];
247
419
  };
248
420
 
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);
421
+ if (_.isFunction(driver.onUnexpectedShutdown)) {
422
+ driver.onUnexpectedShutdown(onShutdown);
270
423
  } else {
271
- log.warn(`Failed to attach the unexpected shutdown listener. ` +
272
- `Is 'onUnexpectedShutdown' method available for '${driver.constructor.name}'?`);
424
+ this.log.warn(
425
+ `Failed to attach the unexpected shutdown listener. ` +
426
+ `Is 'onUnexpectedShutdown' method available for '${driver.constructor.name}'?`
427
+ );
273
428
  }
274
429
  }
275
430
 
276
- async curSessionDataForDriver (InnerDriver) {
277
- const sessions = await sessionsListGuard.acquire(AppiumDriver.name, () => this.sessions);
278
- const data = _.values(sessions)
279
- .filter((s) => s.constructor.name === InnerDriver.name)
280
- .map((s) => s.driverData);
281
- for (let datum of data) {
431
+ /**
432
+ *
433
+ * @param {DriverClass} InnerDriver
434
+ * @returns {Promise<DriverData[]>}}
435
+ */
436
+ // eslint-disable-next-line require-await
437
+ async curSessionDataForDriver(InnerDriver) {
438
+ const data = _.compact(
439
+ _.values(this.sessions)
440
+ .filter((s) => s.constructor.name === InnerDriver.name)
441
+ .map((s) => s.driverData)
442
+ );
443
+ for (const datum of data) {
282
444
  if (!datum) {
283
- throw new Error(`Problem getting session data for driver type ` +
284
- `${InnerDriver.name}; does it implement 'get ` +
285
- `driverData'?`);
445
+ throw new Error(
446
+ `Problem getting session data for driver type ` +
447
+ `${InnerDriver.name}; does it implement 'get driverData'?`
448
+ );
286
449
  }
287
450
  }
288
451
  return data;
289
452
  }
290
453
 
291
- async deleteSession (sessionId) {
454
+ /**
455
+ * @param {string} sessionId
456
+ */
457
+ async deleteSession(sessionId) {
292
458
  let protocol;
293
459
  try {
294
- let otherSessionsData = null;
295
- let dstSession = null;
296
- await sessionsListGuard.acquire(AppiumDriver.name, () => {
460
+ let otherSessionsData;
461
+ const dstSession = await sessionsListGuard.acquire(AppiumDriver.name, () => {
297
462
  if (!this.sessions[sessionId]) {
298
463
  return;
299
464
  }
300
465
  const curConstructorName = this.sessions[sessionId].constructor.name;
301
466
  otherSessionsData = _.toPairs(this.sessions)
302
- .filter(([key, value]) => value.constructor.name === curConstructorName && key !== sessionId)
303
- .map(([, value]) => value.driverData);
304
- dstSession = this.sessions[sessionId];
467
+ .filter(
468
+ ([key, value]) => value.constructor.name === curConstructorName && key !== sessionId
469
+ )
470
+ .map(([, value]) => value.driverData);
471
+ const dstSession = this.sessions[sessionId];
305
472
  protocol = dstSession.protocol;
306
- log.info(`Removing session ${sessionId} from our master session list`);
473
+ this.log.info(`Removing session ${sessionId} from our master session list`);
307
474
  // regardless of whether the deleteSession completes successfully or not
308
475
  // make the session unavailable, because who knows what state it might
309
476
  // be in otherwise
310
477
  delete this.sessions[sessionId];
478
+ delete this.sessionPlugins[sessionId];
479
+ return dstSession;
311
480
  });
481
+ // this may not be correct, but if `dstSession` was falsy, the call to `deleteSession()` would
482
+ // throw anyway.
483
+ if (!dstSession) {
484
+ throw new Error('Session not found');
485
+ }
312
486
  return {
313
487
  protocol,
314
488
  value: await dstSession.deleteSession(sessionId, otherSessionsData),
315
489
  };
316
490
  } catch (e) {
317
- log.error(`Had trouble ending session ${sessionId}: ${e.message}`);
491
+ this.log.error(`Had trouble ending session ${sessionId}: ${e.message}`);
318
492
  return {
319
493
  protocol,
320
494
  error: e,
@@ -322,38 +496,89 @@ class AppiumDriver extends BaseDriver {
322
496
  }
323
497
  }
324
498
 
325
- async deleteAllSessions (opts = {}) {
499
+ async deleteAllSessions(opts = {}) {
326
500
  const sessionsCount = _.size(this.sessions);
327
501
  if (0 === sessionsCount) {
328
- log.debug('There are no active sessions for cleanup');
502
+ this.log.debug('There are no active sessions for cleanup');
329
503
  return;
330
504
  }
331
505
 
332
- const {
333
- force = false,
334
- reason,
335
- } = opts;
336
- log.debug(`Cleaning up ${util.pluralize('active session', sessionsCount, true)}`);
506
+ const {force = false, reason} = opts;
507
+ this.log.debug(`Cleaning up ${util.pluralize('active session', sessionsCount, true)}`);
337
508
  const cleanupPromises = force
338
- ? _.values(this.sessions).map((drv) => drv.startUnexpectedShutdown(reason && new Error(reason)))
509
+ ? _.values(this.sessions).map((drv) =>
510
+ drv.startUnexpectedShutdown(reason && new Error(reason))
511
+ )
339
512
  : _.keys(this.sessions).map((id) => this.deleteSession(id));
340
513
  for (const cleanupPromise of cleanupPromises) {
341
514
  try {
342
515
  await cleanupPromise;
343
516
  } catch (e) {
344
- log.debug(e);
517
+ this.log.debug(e);
345
518
  }
346
519
  }
347
520
  }
348
521
 
349
- pluginsToHandleCmd (cmd) {
350
- return this.plugins.filter((p) =>
351
- p.commands === true ||
352
- (_.isArray(p.commands) && _.includes(p.commands, cmd))
522
+ /**
523
+ * Get the appropriate plugins for a session (or sessionless plugins)
524
+ *
525
+ * @param {?string} sessionId - the sessionId (or null) to use to find plugins
526
+ * @returns {Array} - array of plugin instances
527
+ */
528
+ pluginsForSession(sessionId = null) {
529
+ if (sessionId) {
530
+ if (!this.sessionPlugins[sessionId]) {
531
+ this.sessionPlugins[sessionId] = this.createPluginInstances();
532
+ }
533
+ return this.sessionPlugins[sessionId];
534
+ }
535
+
536
+ if (_.isEmpty(this.sessionlessPlugins)) {
537
+ this.sessionlessPlugins = this.createPluginInstances();
538
+ }
539
+ return this.sessionlessPlugins;
540
+ }
541
+
542
+ /**
543
+ * To get plugins for a command, we either get the plugin instances associated with the
544
+ * particular command's session, or in the case of sessionless plugins, pull from the set of
545
+ * plugin instances reserved for sessionless commands (and we lazily create plugin instances on
546
+ * first use)
547
+ *
548
+ * @param {string} cmd - the name of the command to find a plugin to handle
549
+ * @param {?string} sessionId - the particular session for which to find a plugin, or null if
550
+ * sessionless
551
+ */
552
+ pluginsToHandleCmd(cmd, sessionId = null) {
553
+ // to handle a given command, a plugin should either implement that command as a plugin
554
+ // instance method or it should implement a generic 'handle' method
555
+ return this.pluginsForSession(sessionId).filter(
556
+ (p) => _.isFunction(p[cmd]) || _.isFunction(p.handle)
353
557
  );
354
558
  }
355
559
 
356
- async executeCommand (cmd, ...args) {
560
+ /**
561
+ * Creates instances of all of the enabled Plugin classes
562
+ * @returns {Plugin[]}
563
+ */
564
+ createPluginInstances() {
565
+ /** @type {Plugin[]} */
566
+ const pluginInstances = [];
567
+ for (const [PluginClass, name] of this.pluginClasses.entries()) {
568
+ const cliArgs = this.getCliArgsForPlugin(name);
569
+ const plugin = new PluginClass(name, cliArgs);
570
+ pluginInstances.push(plugin);
571
+ }
572
+ return pluginInstances;
573
+ }
574
+
575
+ /**
576
+ *
577
+ * @param {string} cmd
578
+ * @param {...any} args
579
+ * @returns {Promise<{value: any, error?: Error, protocol: string} | import('type-fest').AsyncReturnType<Driver['executeCommand']>>}
580
+ */
581
+ async executeCommand(cmd, ...args) {
357
582
  // We have basically three cases for how to handle commands:
358
583
  // 1. handle getStatus (we do this as a special out of band case so it doesn't get added to an
359
584
  // execution queue, and can be called while e.g. createSession is in progress)
@@ -363,28 +588,41 @@ class AppiumDriver extends BaseDriver {
363
588
  // The tricky part is that because we support command plugins, we need to wrap any of these
364
589
  // cases with plugin handling.
365
590
 
366
- const isGetStatus = cmd === 'getStatus';
367
- const isUmbrellaCmd = !isGetStatus && isAppiumDriverCommand(cmd);
368
- const isSessionCmd = !isGetStatus && !isUmbrellaCmd;
591
+ const isGetStatus = cmd === GET_STATUS_COMMAND;
592
+ const isUmbrellaCmd = isAppiumDriverCommand(cmd);
593
+ const isSessionCmd = isSessionCommand(cmd);
369
594
 
370
- // get any plugins which are registered as handling this command
371
- const plugins = this.pluginsToHandleCmd(cmd);
595
+ // if a plugin override proxying for this command and that is why we are here instead of just
596
+ // letting the protocol proxy the command entirely, determine that, get the request object for
597
+ // use later on, then clean up the args
598
+ const reqForProxy = _.last(args)?.reqForProxy;
599
+ if (reqForProxy) {
600
+ args.pop();
601
+ }
372
602
 
373
603
  // first do some error checking. If we're requesting a session command execution, then make
374
604
  // sure that session actually exists on the session driver, and set the session driver itself
375
605
  let sessionId = null;
376
606
  let dstSession = null;
377
607
  let protocol = null;
608
+ /** @type {this | ExternalDriver} */
609
+ let driver = this;
378
610
  if (isSessionCmd) {
379
611
  sessionId = _.last(args);
380
- dstSession = await sessionsListGuard.acquire(AppiumDriver.name, () => this.sessions[sessionId]);
612
+ dstSession = this.sessions[sessionId];
381
613
  if (!dstSession) {
382
614
  throw new Error(`The session with id '${sessionId}' does not exist`);
383
615
  }
384
616
  // now save the response protocol given that the session driver's protocol might differ
385
617
  protocol = dstSession.protocol;
618
+ if (!isUmbrellaCmd) {
619
+ driver = dstSession;
620
+ }
386
621
  }
387
622
 
623
+ // get any plugins which are registered as handling this command
624
+ const plugins = this.pluginsToHandleCmd(cmd, sessionId);
625
+
388
626
  // now we define a 'cmdHandledBy' object which will keep track of which plugins have handled this
389
627
  // command. we care about this because (a) multiple plugins can handle the same command, and
390
628
  // (b) there's no guarantee that a plugin will actually call the next() method which runs the
@@ -401,11 +639,26 @@ class AppiumDriver extends BaseDriver {
401
639
  // if we're running with plugins, make sure we log that the default behavior is actually
402
640
  // happening so we can tell when the plugin call chain is unwrapping to the default behavior
403
641
  // if that's what happens
404
- plugins.length && log.info(`Executing default handling behavior for command '${cmd}'`);
642
+ plugins.length && this.log.info(`Executing default handling behavior for command '${cmd}'`);
405
643
 
406
644
  // if we make it here, we know that the default behavior is handled
407
645
  cmdHandledBy.default = true;
408
646
 
647
+ if (reqForProxy) {
648
+ // we would have proxied this command had a plugin not handled it, so the default behavior
649
+ // is to do the proxy and retrieve the result internally so it can be passed to the plugin
650
+ // in case it calls 'await next()'. This requires that the driver have defined
651
+ // 'proxyCommand' and not just 'proxyReqRes'.
652
+ if (!dstSession.proxyCommand) {
653
+ throw new NoDriverProxyCommandError();
654
+ }
655
+ return await dstSession.proxyCommand(
656
+ reqForProxy.originalUrl,
657
+ reqForProxy.method,
658
+ reqForProxy.body
659
+ );
660
+ }
661
+
409
662
  if (isGetStatus) {
410
663
  return await this.getStatus();
411
664
  }
@@ -413,7 +666,7 @@ class AppiumDriver extends BaseDriver {
413
666
  if (isUmbrellaCmd) {
414
667
  // some commands, like deleteSession, we want to make sure to handle on *this* driver,
415
668
  // not the platform driver
416
- return await super.executeCommand(cmd, ...args);
669
+ return await BaseDriver.prototype.executeCommand.call(this, cmd, ...args);
417
670
  }
418
671
 
419
672
  // here we know that we are executing a session command, and have a valid session driver
@@ -421,18 +674,39 @@ class AppiumDriver extends BaseDriver {
421
674
  };
422
675
 
423
676
  // 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});
677
+ const wrappedCmd = this.wrapCommandWithPlugins({
678
+ driver,
679
+ cmd,
680
+ args,
681
+ plugins,
682
+ cmdHandledBy,
683
+ next: defaultBehavior,
684
+ });
425
685
  const res = await this.executeWrappedCommand({wrappedCmd, protocol});
426
686
 
427
687
  // if we had plugins, make sure to log out the helpful report about which plugins ended up
428
688
  // handling the command and which didn't
429
- plugins.length && this.logPluginHandlerReport({cmd, cmdHandledBy});
689
+ this.logPluginHandlerReport(plugins, {cmd, cmdHandledBy});
690
+
691
+ // And finally, if the command was createSession, we want to migrate any plugins which were
692
+ // previously sessionless to use the new sessionId, so that plugins can share state between
693
+ // their createSession method and other instance methods
694
+ if (cmd === CREATE_SESSION_COMMAND && this.sessionlessPlugins.length && !res.error) {
695
+ const sessionId = _.first(res.value);
696
+ this.log.info(
697
+ `Promoting ${this.sessionlessPlugins.length} sessionless plugins to be attached ` +
698
+ `to session ID ${sessionId}`
699
+ );
700
+ this.sessionPlugins[sessionId] = this.sessionlessPlugins;
701
+ this.sessionlessPlugins = [];
702
+ }
430
703
 
431
704
  return res;
432
705
  }
433
706
 
434
- wrapCommandWithPlugins ({cmd, args, next, cmdHandledBy, plugins}) {
435
- plugins.length && log.info(`Plugins which can handle cmd '${cmd}': ${plugins.map((p) => p.name)}`);
707
+ wrapCommandWithPlugins({driver, cmd, args, next, cmdHandledBy, plugins}) {
708
+ plugins.length &&
709
+ this.log.info(`Plugins which can handle cmd '${cmd}': ${plugins.map((p) => p.name)}`);
436
710
 
437
711
  // now we can go through each plugin and wrap `next` around its own handler, passing the *old*
438
712
  // next in so that it can call it if it wants to
@@ -442,16 +716,25 @@ class AppiumDriver extends BaseDriver {
442
716
  // evaluated, otherwise we end up with infinite recursion of the last `next` to be defined.
443
717
  cmdHandledBy[plugin.name] = false; // we see a new plugin, so add it to the 'cmdHandledBy' object
444
718
  next = ((_next) => async () => {
445
- log.info(`Plugin ${plugin.name} is now handling cmd '${cmd}'`);
719
+ this.log.info(`Plugin ${plugin.name} is now handling cmd '${cmd}'`);
446
720
  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);
721
+ // first attempt to handle the command via a command-specific handler on the plugin
722
+ if (plugin[cmd]) {
723
+ return await plugin[cmd](_next, driver, ...args);
724
+ }
725
+ // otherwise, call the generic 'handle' method
726
+ return await plugin.handle(_next, driver, cmd, ...args);
448
727
  })(next);
449
728
  }
450
729
 
451
730
  return next;
452
731
  }
453
732
 
454
- logPluginHandlerReport ({cmd, cmdHandledBy}) {
733
+ logPluginHandlerReport(plugins, {cmd, cmdHandledBy}) {
734
+ if (!plugins.length) {
735
+ return;
736
+ }
737
+
455
738
  // at the end of the day, we have an object representing which plugins ended up getting
456
739
  // their code run as part of handling this command. Because plugins can choose *not* to
457
740
  // pass control to other plugins or to the default driver behavior, this is information
@@ -461,14 +744,18 @@ class AppiumDriver extends BaseDriver {
461
744
  const didHandle = Object.keys(cmdHandledBy).filter((k) => cmdHandledBy[k]);
462
745
  const didntHandle = Object.keys(cmdHandledBy).filter((k) => !cmdHandledBy[k]);
463
746
  if (didntHandle.length > 0) {
464
- log.info(`Command '${cmd}' was not handled by the following beahviors or plugins, even ` +
465
- `though they were registered to handle it: ${JSON.stringify(didntHandle)}. The ` +
466
- `command *was* handled by these: ${JSON.stringify(didHandle)}.`);
747
+ this.log.info(
748
+ `Command '${cmd}' was *not* handled by the following behaviours or plugins, even ` +
749
+ `though they were registered to handle it: ${JSON.stringify(didntHandle)}. The ` +
750
+ `command *was* handled by these: ${JSON.stringify(didHandle)}.`
751
+ );
467
752
  }
468
753
  }
469
754
 
470
- async executeWrappedCommand ({wrappedCmd, protocol}) {
471
- let cmdRes, cmdErr, res = {};
755
+ async executeWrappedCommand({wrappedCmd, protocol}) {
756
+ let cmdRes,
757
+ cmdErr,
758
+ res = {};
472
759
  try {
473
760
  // At this point, `wrappedCmd` defines a whole sequence of plugin handlers, culminating in
474
761
  // our default handler. Whatever it returns is what we're going to want to send back to the
@@ -491,18 +778,22 @@ class AppiumDriver extends BaseDriver {
491
778
  return res;
492
779
  }
493
780
 
494
-
495
- proxyActive (sessionId) {
781
+ proxyActive(sessionId) {
496
782
  const dstSession = this.sessions[sessionId];
497
783
  return dstSession && _.isFunction(dstSession.proxyActive) && dstSession.proxyActive(sessionId);
498
784
  }
499
785
 
500
- getProxyAvoidList (sessionId) {
786
+ /**
787
+ *
788
+ * @param {string} sessionId
789
+ * @returns {import('@appium/types').RouteMatcher[]}
790
+ */
791
+ getProxyAvoidList(sessionId) {
501
792
  const dstSession = this.sessions[sessionId];
502
793
  return dstSession ? dstSession.getProxyAvoidList() : [];
503
794
  }
504
795
 
505
- canProxy (sessionId) {
796
+ canProxy(sessionId) {
506
797
  const dstSession = this.sessions[sessionId];
507
798
  return dstSession && dstSession.canProxy(sessionId);
508
799
  }
@@ -510,8 +801,66 @@ class AppiumDriver extends BaseDriver {
510
801
 
511
802
  // help decide which commands should be proxied to sub-drivers and which
512
803
  // should be handled by this, our umbrella driver
513
- function isAppiumDriverCommand (cmd) {
514
- return !isSessionCommand(cmd) || cmd === 'deleteSession';
804
+ function isAppiumDriverCommand(cmd) {
805
+ return !isSessionCommand(cmd) || cmd === DELETE_SESSION_COMMAND;
806
+ }
807
+
808
+ /**
809
+ * Thrown when Appium tried to proxy a command using a driver's `proxyCommand` method but the
810
+ * method did not exist
811
+ */
812
+ export class NoDriverProxyCommandError extends Error {
813
+ /**
814
+ * @type {Readonly<string>}
815
+ */
816
+ code = 'APPIUMERR_NO_DRIVER_PROXYCOMMAND';
817
+
818
+ constructor() {
819
+ super(
820
+ `The default behavior for this command was to proxy, but the driver ` +
821
+ `did not have the 'proxyCommand' method defined. To fully support ` +
822
+ `plugins, drivers should have 'proxyCommand' set to a jwpProxy object's ` +
823
+ `'command()' method, in addition to the normal 'proxyReqRes'`
824
+ );
825
+ }
515
826
  }
516
827
 
517
- export { AppiumDriver };
828
+ export {AppiumDriver};
829
+
830
+ /**
831
+ * @typedef {import('@appium/types').ExternalDriver} ExternalDriver
832
+ * @typedef {import('@appium/types').Driver} Driver
833
+ * @typedef {import('@appium/types').DriverClass} DriverClass
834
+ * @typedef {import('@appium/types').DriverData} DriverData
835
+ * @typedef {import('@appium/types').ServerArgs} DriverOpts
836
+ * @typedef {import('@appium/types').Constraints} Constraints
837
+ * @typedef {import('@appium/types').AppiumServer} AppiumServer
838
+ * @typedef {import('@appium/types').ExtensionType} ExtensionType
839
+ * @typedef {import('./extension/driver-config').DriverConfig} DriverConfig
840
+ * @typedef {import('@appium/types').Plugin} Plugin
841
+ * @typedef {import('@appium/types').PluginClass} PluginClass
842
+ * @typedef {import('@appium/types').PluginType} PluginType
843
+ * @typedef {import('@appium/types').DriverType} DriverType
844
+ * @typedef {import('@appium/types').StringRecord} StringRecord
845
+ * @typedef {import('@appium/types').SessionHandler<SessionHandlerResult<any[]>,SessionHandlerResult<void>>} SessionHandler
846
+ */
847
+
848
+ /**
849
+ * Used by {@linkcode AppiumDriver.createSession} and {@linkcode AppiumDriver.deleteSession} to describe
850
+ * result.
851
+ * @template V
852
+ * @typedef SessionHandlerResult
853
+ * @property {V} [value]
854
+ * @property {Error} [error]
855
+ * @property {string} [protocol]
856
+ */
857
+
858
+ /**
859
+ * @template {Constraints} C
860
+ * @typedef {import('@appium/types').W3CCapabilities<C>} W3CCapabilities
861
+ */
862
+
863
+ /**
864
+ * @template {Constraints} C
865
+ * @typedef {import('@appium/types').Capabilities<C>} Capabilities
866
+ */