appium 2.0.0-beta.8 → 2.0.0-rc.1

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