appium 2.0.0-beta.9 → 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 (208) 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 +677 -449
  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 +70 -68
  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 +372 -192
  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 +545 -188
  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 +38 -19
  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 +310 -89
  175. package/package.json +84 -84
  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 -16
  199. package/build/postinstall.js +0 -90
  200. package/lib/cli/argparse-actions.js +0 -77
  201. package/lib/cli/npm.js +0 -183
  202. package/lib/cli/parser-helpers.js +0 -91
  203. package/lib/driver-config.js +0 -46
  204. package/lib/drivers.js +0 -84
  205. package/lib/extension-config.js +0 -209
  206. package/lib/plugin-config.js +0 -34
  207. package/lib/plugins.js +0 -11
  208. package/postinstall.js +0 -71
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,135 +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, driverBaseVersion) {
89
- log.info(driverVersion
90
- ? `Appium v${APPIUM_VER} creating new ${driverName} (v${driverVersion}) session`
91
- : `Appium v${APPIUM_VER} creating new ${driverName} session`
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`
92
175
  );
93
- log.info(`Checking BaseDriver versions for Appium and ${driverName}`);
94
- log.info(AppiumDriver.baseVersion
95
- ? `Appium's BaseDriver version is ${AppiumDriver.baseVersion}`
96
- : `Could not determine Appium's BaseDriver version`
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`
97
181
  );
98
- log.info(driverBaseVersion
99
- ? `${driverName}'s BaseDriver version is ${driverBaseVersion}`
100
- : `Could not determine ${driverName}'s BaseDriver version`
182
+ this.log.info(
183
+ driverBaseVersion
184
+ ? `${driverName}'s BaseDriver version is ${driverBaseVersion}`
185
+ : `Could not determine ${driverName}'s BaseDriver version`
101
186
  );
102
187
  }
103
188
 
104
189
  /**
105
- * This is just an alias for driver.js's method, which is necessary for
106
- * 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.
193
+ */
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`
107
206
  */
108
- _findMatchingDriver (...args) {
109
- return findMatchingDriver(...args);
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
+ }
110
219
  }
111
220
 
112
221
  /**
113
222
  * Create a new session
114
- * @param {Object} jsonwpCaps JSONWP formatted desired capabilities
115
- * @param {Object} reqCaps Required capabilities (JSONWP standard)
116
- * @param {Object} w3cCapabilities W3C capabilities
117
- * @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>}
118
227
  */
119
- async createSession (jsonwpCaps, reqCaps, w3cCapabilities) {
228
+ async createSession(jsonwpCaps, reqCaps, w3cCapabilities) {
120
229
  const defaultCapabilities = _.cloneDeep(this.args.defaultCapabilities);
121
230
  const defaultSettings = pullSettings(defaultCapabilities);
122
231
  jsonwpCaps = _.cloneDeep(jsonwpCaps);
123
- const jwpSettings = Object.assign({}, defaultSettings, pullSettings(jsonwpCaps));
232
+ const jwpSettings = {...defaultSettings, ...pullSettings(jsonwpCaps)};
124
233
  w3cCapabilities = _.cloneDeep(w3cCapabilities);
125
234
  // It is possible that the client only provides caps using JSONWP standard,
126
235
  // although firstMatch/alwaysMatch properties are still present.
127
236
  // In such case we assume the client understands W3C protocol and merge the given
128
237
  // JSONWP caps to W3C caps
129
- const w3cSettings = Object.assign({}, jwpSettings);
130
- Object.assign(w3cSettings, pullSettings((w3cCapabilities || {}).alwaysMatch || {}));
131
- for (const firstMatchEntry of ((w3cCapabilities || {}).firstMatch || [])) {
238
+ const w3cSettings = {
239
+ ...jwpSettings,
240
+ ...pullSettings((w3cCapabilities ?? {}).alwaysMatch ?? {}),
241
+ };
242
+ for (const firstMatchEntry of (w3cCapabilities ?? {}).firstMatch ?? []) {
132
243
  Object.assign(w3cSettings, pullSettings(firstMatchEntry));
133
244
  }
134
245
 
246
+ /** @type {string|undefined} */
135
247
  let protocol;
136
248
  let innerSessionId, dCaps;
137
249
  try {
138
250
  // Parse the caps into a format that the InnerDriver will accept
139
251
  const parsedCaps = parseCapsForInnerDriver(
140
252
  jsonwpCaps,
141
- w3cCapabilities,
253
+ promoteAppiumOptions(/** @type {W3CAppiumDriverCaps} */ (w3cCapabilities)),
142
254
  this.desiredCapConstraints,
143
- defaultCapabilities
255
+ defaultCapabilities ? promoteAppiumOptionsForObject(defaultCapabilities) : undefined
144
256
  );
145
257
 
146
- const {desiredCaps, processedJsonwpCapabilities, processedW3CCapabilities, error} = parsedCaps;
258
+ const {desiredCaps, processedJsonwpCapabilities, processedW3CCapabilities} =
259
+ /** @type {import('./utils').ParsedDriverCaps<AppiumDriverConstraints>} */ (parsedCaps);
147
260
  protocol = parsedCaps.protocol;
148
-
261
+ const error = /** @type {import('./utils').InvalidCaps<AppiumDriverConstraints>} */ (
262
+ parsedCaps
263
+ ).error;
149
264
  // If the parsing of the caps produced an error, throw it in here
150
265
  if (error) {
151
266
  throw error;
@@ -153,87 +268,118 @@ class AppiumDriver extends BaseDriver {
153
268
 
154
269
  const {
155
270
  driver: InnerDriver,
156
- version: driverVersion
157
- } = this._findMatchingDriver(this.driverConfig, desiredCaps);
271
+ version: driverVersion,
272
+ driverName,
273
+ } = this.driverConfig.findMatchingDriver(desiredCaps);
158
274
  this.printNewSessionAnnouncement(InnerDriver.name, driverVersion, InnerDriver.baseVersion);
159
275
 
160
276
  if (this.args.sessionOverride) {
161
277
  await this.deleteAllSessions();
162
278
  }
163
279
 
164
- let runningDriversData, otherPendingDriversData;
165
- 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));
166
290
 
167
291
  // We want to assign security values directly on the driver. The driver
168
292
  // should not read security values from `this.opts` because those values
169
293
  // could have been set by a malicious user via capabilities, whereas we
170
294
  // want a guarantee the values were set by the appium server admin
171
295
  if (this.args.relaxedSecurityEnabled) {
172
- log.info(`Applying relaxed security to '${InnerDriver.name}' as per ` +
173
- `server command line argument. All insecure features will be ` +
174
- `enabled unless explicitly disabled by --deny-insecure`);
175
- 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;
176
302
  }
177
303
 
178
304
  if (!_.isEmpty(this.args.denyInsecure)) {
179
- log.info('Explicitly preventing use of insecure features:');
180
- this.args.denyInsecure.map((a) => log.info(` ${a}`));
181
- 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;
182
308
  }
183
309
 
184
310
  if (!_.isEmpty(this.args.allowInsecure)) {
185
- log.info('Explicitly enabling use of insecure features:');
186
- this.args.allowInsecure.map((a) => log.info(` ${a}`));
187
- 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;
188
321
  }
189
322
 
190
323
  // This assignment is required for correct web sockets functionality inside the driver
191
- 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
+
192
332
  try {
193
- runningDriversData = await this.curSessionDataForDriver(InnerDriver);
333
+ runningDriversData = (await this.curSessionDataForDriver(InnerDriver)) ?? [];
194
334
  } catch (e) {
195
335
  throw new errors.SessionNotCreatedError(e.message);
196
336
  }
197
337
  await pendingDriversGuard.acquire(AppiumDriver.name, () => {
198
338
  this.pendingDrivers[InnerDriver.name] = this.pendingDrivers[InnerDriver.name] || [];
199
- otherPendingDriversData = this.pendingDrivers[InnerDriver.name].map((drv) => drv.driverData);
200
- 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);
201
343
  });
202
344
 
203
345
  try {
204
- [innerSessionId, dCaps] = await d.createSession(
346
+ [innerSessionId, dCaps] = await driverInstance.createSession(
205
347
  processedJsonwpCapabilities,
206
348
  reqCaps,
207
349
  processedW3CCapabilities,
208
350
  [...runningDriversData, ...otherPendingDriversData]
209
351
  );
210
- protocol = d.protocol;
211
- await sessionsListGuard.acquire(AppiumDriver.name, () => {
212
- this.sessions[innerSessionId] = d;
213
- });
352
+ protocol = driverInstance.protocol;
353
+ this.sessions[innerSessionId] = driverInstance;
214
354
  } finally {
215
355
  await pendingDriversGuard.acquire(AppiumDriver.name, () => {
216
- _.pull(this.pendingDrivers[InnerDriver.name], d);
356
+ _.pull(this.pendingDrivers[InnerDriver.name], driverInstance);
217
357
  });
218
358
  }
219
359
 
220
- this.attachUnexpectedShutdownHandler(d, innerSessionId);
360
+ this.attachUnexpectedShutdownHandler(driverInstance, innerSessionId);
221
361
 
222
- log.info(`New ${InnerDriver.name} session created successfully, session ` +
223
- `${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
+ );
224
366
 
225
367
  // set the New Command Timeout for the inner driver
226
- d.startNewCommandTimeout();
368
+ driverInstance.startNewCommandTimeout();
227
369
 
228
370
  // apply initial values to Appium settings (if provided)
229
- if (d.isW3CProtocol() && !_.isEmpty(w3cSettings)) {
230
- log.info(`Applying the initial values to Appium settings parsed from W3C caps: ` +
231
- JSON.stringify(w3cSettings));
232
- await d.updateSettings(w3cSettings);
233
- } else if (d.isMjsonwpProtocol() && !_.isEmpty(jwpSettings)) {
234
- log.info(`Applying the initial values to Appium settings parsed from MJSONWP caps: ` +
235
- JSON.stringify(jwpSettings));
236
- 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);
237
383
  }
238
384
  } catch (error) {
239
385
  return {
@@ -244,86 +390,115 @@ class AppiumDriver extends BaseDriver {
244
390
 
245
391
  return {
246
392
  protocol,
247
- value: [innerSessionId, dCaps, protocol]
393
+ value: [innerSessionId, dCaps, protocol],
248
394
  };
249
395
  }
250
396
 
251
- attachUnexpectedShutdownHandler (driver, innerSessionId) {
252
- const removeSessionFromMasterList = (cause = new Error('Unknown error')) => {
253
- log.warn(`Closing session, cause was '${cause.message}'`);
254
- 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`);
255
426
  delete this.sessions[innerSessionId];
427
+ delete this.sessionPlugins[innerSessionId];
256
428
  };
257
429
 
258
- // eslint-disable-next-line promise/prefer-await-to-then
259
- if (_.isFunction((driver.onUnexpectedShutdown || {}).then)) {
260
- // TODO: Remove this block after all the drivers use base driver above v 5.0.0
261
- // Remove the session on unexpected shutdown, so that we are in a position
262
- // to open another session later on.
263
- driver.onUnexpectedShutdown
264
- // eslint-disable-next-line promise/prefer-await-to-then
265
- .then(() => {
266
- // if we get here, we've had an unexpected shutdown, so error
267
- throw new Error('Unexpected shutdown');
268
- })
269
- .catch((e) => {
270
- // if we cancelled the unexpected shutdown promise, that means we
271
- // no longer care about it, and can safely ignore it
272
- if (!(e instanceof B.CancellationError)) {
273
- removeSessionFromMasterList(e);
274
- }
275
- }); // this is a cancellable promise
276
- } else if (_.isFunction(driver.onUnexpectedShutdown)) {
277
- // since base driver v 5.0.0
278
- driver.onUnexpectedShutdown(removeSessionFromMasterList);
430
+ if (_.isFunction(driver.onUnexpectedShutdown)) {
431
+ driver.onUnexpectedShutdown(onShutdown);
279
432
  } else {
280
- log.warn(`Failed to attach the unexpected shutdown listener. ` +
281
- `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
+ );
282
437
  }
283
438
  }
284
439
 
285
- async curSessionDataForDriver (InnerDriver) {
286
- const sessions = await sessionsListGuard.acquire(AppiumDriver.name, () => this.sessions);
287
- const data = _.values(sessions)
288
- .filter((s) => s.constructor.name === InnerDriver.name)
289
- .map((s) => s.driverData);
290
- 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) {
291
454
  if (!datum) {
292
- throw new Error(`Problem getting session data for driver type ` +
293
- `${InnerDriver.name}; does it implement 'get ` +
294
- `driverData'?`);
455
+ throw new Error(
456
+ `Problem getting session data for driver type ` +
457
+ `${InnerDriver.name}; does it implement 'get driverData'?`
458
+ );
295
459
  }
296
460
  }
297
461
  return data;
298
462
  }
299
463
 
300
- async deleteSession (sessionId) {
464
+ /**
465
+ * @param {string} sessionId
466
+ */
467
+ async deleteSession(sessionId) {
301
468
  let protocol;
302
469
  try {
303
- let otherSessionsData = null;
304
- let dstSession = null;
305
- await sessionsListGuard.acquire(AppiumDriver.name, () => {
470
+ let otherSessionsData;
471
+ const dstSession = await sessionsListGuard.acquire(AppiumDriver.name, () => {
306
472
  if (!this.sessions[sessionId]) {
307
473
  return;
308
474
  }
309
475
  const curConstructorName = this.sessions[sessionId].constructor.name;
310
476
  otherSessionsData = _.toPairs(this.sessions)
311
- .filter(([key, value]) => value.constructor.name === curConstructorName && key !== sessionId)
312
- .map(([, value]) => value.driverData);
313
- 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];
314
482
  protocol = dstSession.protocol;
315
- log.info(`Removing session ${sessionId} from our master session list`);
483
+ this.log.info(`Removing session ${sessionId} from our master session list`);
316
484
  // regardless of whether the deleteSession completes successfully or not
317
485
  // make the session unavailable, because who knows what state it might
318
486
  // be in otherwise
319
487
  delete this.sessions[sessionId];
488
+ delete this.sessionPlugins[sessionId];
489
+ return dstSession;
320
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
+ }
321
496
  return {
322
497
  protocol,
323
498
  value: await dstSession.deleteSession(sessionId, otherSessionsData),
324
499
  };
325
500
  } catch (e) {
326
- log.error(`Had trouble ending session ${sessionId}: ${e.message}`);
501
+ this.log.error(`Had trouble ending session ${sessionId}: ${e.message}`);
327
502
  return {
328
503
  protocol,
329
504
  error: e,
@@ -331,37 +506,89 @@ class AppiumDriver extends BaseDriver {
331
506
  }
332
507
  }
333
508
 
334
- async deleteAllSessions (opts = {}) {
509
+ async deleteAllSessions(opts = {}) {
335
510
  const sessionsCount = _.size(this.sessions);
336
511
  if (0 === sessionsCount) {
337
- log.debug('There are no active sessions for cleanup');
512
+ this.log.debug('There are no active sessions for cleanup');
338
513
  return;
339
514
  }
340
515
 
341
- const {
342
- force = false,
343
- reason,
344
- } = opts;
345
- 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)}`);
346
518
  const cleanupPromises = force
347
- ? _.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
+ )
348
522
  : _.keys(this.sessions).map((id) => this.deleteSession(id));
349
523
  for (const cleanupPromise of cleanupPromises) {
350
524
  try {
351
525
  await cleanupPromise;
352
526
  } catch (e) {
353
- log.debug(e);
527
+ this.log.debug(e);
354
528
  }
355
529
  }
356
530
  }
357
531
 
358
- pluginsToHandleCmd (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) {
359
563
  // to handle a given command, a plugin should either implement that command as a plugin
360
564
  // instance method or it should implement a generic 'handle' method
361
- return this.plugins.filter((p) => _.isFunction(p[cmd]) || _.isFunction(p.handle));
565
+ return this.pluginsForSession(sessionId).filter(
566
+ (p) => _.isFunction(p[cmd]) || _.isFunction(p.handle)
567
+ );
568
+ }
569
+
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;
362
583
  }
363
584
 
364
- async executeCommand (cmd, ...args) {
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) {
365
592
  // We have basically three cases for how to handle commands:
366
593
  // 1. handle getStatus (we do this as a special out of band case so it doesn't get added to an
367
594
  // execution queue, and can be called while e.g. createSession is in progress)
@@ -371,28 +598,42 @@ class AppiumDriver extends BaseDriver {
371
598
  // The tricky part is that because we support command plugins, we need to wrap any of these
372
599
  // cases with plugin handling.
373
600
 
374
- const isGetStatus = cmd === 'getStatus';
375
- const isUmbrellaCmd = !isGetStatus && isAppiumDriverCommand(cmd);
376
- const isSessionCmd = !isGetStatus && !isUmbrellaCmd;
601
+ const isGetStatus = cmd === GET_STATUS_COMMAND;
602
+ const isUmbrellaCmd = isAppiumDriverCommand(cmd);
603
+ const isSessionCmd = isSessionCommand(cmd);
377
604
 
378
- // get any plugins which are registered as handling this command
379
- 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
+ }
380
612
 
381
613
  // first do some error checking. If we're requesting a session command execution, then make
382
614
  // sure that session actually exists on the session driver, and set the session driver itself
383
615
  let sessionId = null;
384
616
  let dstSession = null;
385
617
  let protocol = null;
618
+ /** @type {this | ExternalDriver} */
619
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
620
+ let driver = this;
386
621
  if (isSessionCmd) {
387
622
  sessionId = _.last(args);
388
- dstSession = await sessionsListGuard.acquire(AppiumDriver.name, () => this.sessions[sessionId]);
623
+ dstSession = this.sessions[sessionId];
389
624
  if (!dstSession) {
390
625
  throw new Error(`The session with id '${sessionId}' does not exist`);
391
626
  }
392
627
  // now save the response protocol given that the session driver's protocol might differ
393
628
  protocol = dstSession.protocol;
629
+ if (!isUmbrellaCmd) {
630
+ driver = dstSession;
631
+ }
394
632
  }
395
633
 
634
+ // get any plugins which are registered as handling this command
635
+ const plugins = this.pluginsToHandleCmd(cmd, sessionId);
636
+
396
637
  // now we define a 'cmdHandledBy' object which will keep track of which plugins have handled this
397
638
  // command. we care about this because (a) multiple plugins can handle the same command, and
398
639
  // (b) there's no guarantee that a plugin will actually call the next() method which runs the
@@ -409,11 +650,26 @@ class AppiumDriver extends BaseDriver {
409
650
  // if we're running with plugins, make sure we log that the default behavior is actually
410
651
  // happening so we can tell when the plugin call chain is unwrapping to the default behavior
411
652
  // if that's what happens
412
- plugins.length && log.info(`Executing default handling behavior for command '${cmd}'`);
653
+ plugins.length && this.log.info(`Executing default handling behavior for command '${cmd}'`);
413
654
 
414
655
  // if we make it here, we know that the default behavior is handled
415
656
  cmdHandledBy.default = true;
416
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
+
417
673
  if (isGetStatus) {
418
674
  return await this.getStatus();
419
675
  }
@@ -421,7 +677,7 @@ class AppiumDriver extends BaseDriver {
421
677
  if (isUmbrellaCmd) {
422
678
  // some commands, like deleteSession, we want to make sure to handle on *this* driver,
423
679
  // not the platform driver
424
- return await super.executeCommand(cmd, ...args);
680
+ return await BaseDriver.prototype.executeCommand.call(this, cmd, ...args);
425
681
  }
426
682
 
427
683
  // here we know that we are executing a session command, and have a valid session driver
@@ -430,19 +686,38 @@ class AppiumDriver extends BaseDriver {
430
686
 
431
687
  // now take our default behavior, wrap it with any number of plugin behaviors, and run it
432
688
  const wrappedCmd = this.wrapCommandWithPlugins({
433
- driver: dstSession, cmd, args, plugins, cmdHandledBy, next: defaultBehavior
689
+ driver,
690
+ cmd,
691
+ args,
692
+ plugins,
693
+ cmdHandledBy,
694
+ next: defaultBehavior,
434
695
  });
435
696
  const res = await this.executeWrappedCommand({wrappedCmd, protocol});
436
697
 
437
698
  // if we had plugins, make sure to log out the helpful report about which plugins ended up
438
699
  // handling the command and which didn't
439
- 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
+ }
440
714
 
441
715
  return res;
442
716
  }
443
717
 
444
- wrapCommandWithPlugins ({driver, cmd, args, next, cmdHandledBy, plugins}) {
445
- 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)}`);
446
721
 
447
722
  // now we can go through each plugin and wrap `next` around its own handler, passing the *old*
448
723
  // next in so that it can call it if it wants to
@@ -452,7 +727,7 @@ class AppiumDriver extends BaseDriver {
452
727
  // evaluated, otherwise we end up with infinite recursion of the last `next` to be defined.
453
728
  cmdHandledBy[plugin.name] = false; // we see a new plugin, so add it to the 'cmdHandledBy' object
454
729
  next = ((_next) => async () => {
455
- log.info(`Plugin ${plugin.name} is now handling cmd '${cmd}'`);
730
+ this.log.info(`Plugin ${plugin.name} is now handling cmd '${cmd}'`);
456
731
  cmdHandledBy[plugin.name] = true; // if we make it here, this plugin has attempted to handle cmd
457
732
  // first attempt to handle the command via a command-specific handler on the plugin
458
733
  if (plugin[cmd]) {
@@ -466,7 +741,11 @@ class AppiumDriver extends BaseDriver {
466
741
  return next;
467
742
  }
468
743
 
469
- logPluginHandlerReport ({cmd, cmdHandledBy}) {
744
+ logPluginHandlerReport(plugins, {cmd, cmdHandledBy}) {
745
+ if (!plugins.length) {
746
+ return;
747
+ }
748
+
470
749
  // at the end of the day, we have an object representing which plugins ended up getting
471
750
  // their code run as part of handling this command. Because plugins can choose *not* to
472
751
  // pass control to other plugins or to the default driver behavior, this is information
@@ -476,14 +755,18 @@ class AppiumDriver extends BaseDriver {
476
755
  const didHandle = Object.keys(cmdHandledBy).filter((k) => cmdHandledBy[k]);
477
756
  const didntHandle = Object.keys(cmdHandledBy).filter((k) => !cmdHandledBy[k]);
478
757
  if (didntHandle.length > 0) {
479
- log.info(`Command '${cmd}' was not handled by the following beahviors or plugins, even ` +
480
- `though they were registered to handle it: ${JSON.stringify(didntHandle)}. The ` +
481
- `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
+ );
482
763
  }
483
764
  }
484
765
 
485
- async executeWrappedCommand ({wrappedCmd, protocol}) {
486
- let cmdRes, cmdErr, res = {};
766
+ async executeWrappedCommand({wrappedCmd, protocol}) {
767
+ let cmdRes,
768
+ cmdErr,
769
+ res = {};
487
770
  try {
488
771
  // At this point, `wrappedCmd` defines a whole sequence of plugin handlers, culminating in
489
772
  // our default handler. Whatever it returns is what we're going to want to send back to the
@@ -506,18 +789,22 @@ class AppiumDriver extends BaseDriver {
506
789
  return res;
507
790
  }
508
791
 
509
-
510
- proxyActive (sessionId) {
792
+ proxyActive(sessionId) {
511
793
  const dstSession = this.sessions[sessionId];
512
794
  return dstSession && _.isFunction(dstSession.proxyActive) && dstSession.proxyActive(sessionId);
513
795
  }
514
796
 
515
- getProxyAvoidList (sessionId) {
797
+ /**
798
+ *
799
+ * @param {string} sessionId
800
+ * @returns {import('@appium/types').RouteMatcher[]}
801
+ */
802
+ getProxyAvoidList(sessionId) {
516
803
  const dstSession = this.sessions[sessionId];
517
804
  return dstSession ? dstSession.getProxyAvoidList() : [];
518
805
  }
519
806
 
520
- canProxy (sessionId) {
807
+ canProxy(sessionId) {
521
808
  const dstSession = this.sessions[sessionId];
522
809
  return dstSession && dstSession.canProxy(sessionId);
523
810
  }
@@ -525,8 +812,78 @@ class AppiumDriver extends BaseDriver {
525
812
 
526
813
  // help decide which commands should be proxied to sub-drivers and which
527
814
  // should be handled by this, our umbrella driver
528
- function isAppiumDriverCommand (cmd) {
529
- 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
+ }
530
837
  }
531
838
 
532
- 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
+ */