appium-android-driver 5.14.7 → 6.0.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 (210) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/build/lib/commands/actions.d.ts +6 -224
  3. package/build/lib/commands/actions.d.ts.map +1 -1
  4. package/build/lib/commands/actions.js +306 -405
  5. package/build/lib/commands/actions.js.map +1 -1
  6. package/build/lib/commands/alert.d.ts +7 -9
  7. package/build/lib/commands/alert.d.ts.map +1 -1
  8. package/build/lib/commands/alert.js +24 -18
  9. package/build/lib/commands/alert.js.map +1 -1
  10. package/build/lib/commands/app-management.d.ts +7 -313
  11. package/build/lib/commands/app-management.d.ts.map +1 -1
  12. package/build/lib/commands/app-management.js +135 -293
  13. package/build/lib/commands/app-management.js.map +1 -1
  14. package/build/lib/commands/context.d.ts +8 -92
  15. package/build/lib/commands/context.d.ts.map +1 -1
  16. package/build/lib/commands/context.js +381 -439
  17. package/build/lib/commands/context.js.map +1 -1
  18. package/build/lib/commands/element.d.ts +8 -35
  19. package/build/lib/commands/element.d.ts.map +1 -1
  20. package/build/lib/commands/element.js +153 -136
  21. package/build/lib/commands/element.js.map +1 -1
  22. package/build/lib/commands/emu-console.d.ts +6 -48
  23. package/build/lib/commands/emu-console.d.ts.map +1 -1
  24. package/build/lib/commands/emu-console.js +19 -34
  25. package/build/lib/commands/emu-console.js.map +1 -1
  26. package/build/lib/commands/execute.d.ts +6 -5
  27. package/build/lib/commands/execute.d.ts.map +1 -1
  28. package/build/lib/commands/execute.js +77 -66
  29. package/build/lib/commands/execute.js.map +1 -1
  30. package/build/lib/commands/file-actions.d.ts +7 -128
  31. package/build/lib/commands/file-actions.d.ts.map +1 -1
  32. package/build/lib/commands/file-actions.js +183 -219
  33. package/build/lib/commands/file-actions.js.map +1 -1
  34. package/build/lib/commands/find.d.ts +8 -12
  35. package/build/lib/commands/find.d.ts.map +1 -1
  36. package/build/lib/commands/find.js +19 -23
  37. package/build/lib/commands/find.js.map +1 -1
  38. package/build/lib/commands/general.d.ts +9 -132
  39. package/build/lib/commands/general.d.ts.map +1 -1
  40. package/build/lib/commands/general.js +281 -312
  41. package/build/lib/commands/general.js.map +1 -1
  42. package/build/lib/commands/ime.d.ts +7 -10
  43. package/build/lib/commands/ime.d.ts.map +1 -1
  44. package/build/lib/commands/ime.js +47 -35
  45. package/build/lib/commands/ime.js.map +1 -1
  46. package/build/lib/commands/index.d.ts +27 -2
  47. package/build/lib/commands/index.d.ts.map +1 -1
  48. package/build/lib/commands/index.js +41 -19
  49. package/build/lib/commands/index.js.map +1 -1
  50. package/build/lib/commands/intent.d.ts +7 -417
  51. package/build/lib/commands/intent.d.ts.map +1 -1
  52. package/build/lib/commands/intent.js +104 -216
  53. package/build/lib/commands/intent.js.map +1 -1
  54. package/build/lib/commands/keyboard.d.ts +6 -5
  55. package/build/lib/commands/keyboard.d.ts.map +1 -1
  56. package/build/lib/commands/keyboard.js +16 -8
  57. package/build/lib/commands/keyboard.js.map +1 -1
  58. package/build/lib/commands/log.d.ts +7 -44
  59. package/build/lib/commands/log.d.ts.map +1 -1
  60. package/build/lib/commands/log.js +146 -108
  61. package/build/lib/commands/log.js.map +1 -1
  62. package/build/lib/commands/media-projection.d.ts +7 -143
  63. package/build/lib/commands/media-projection.d.ts.map +1 -1
  64. package/build/lib/commands/media-projection.js +113 -140
  65. package/build/lib/commands/media-projection.js.map +1 -1
  66. package/build/lib/commands/mixins.d.ts +740 -0
  67. package/build/lib/commands/mixins.d.ts.map +1 -0
  68. package/build/lib/commands/mixins.js +19 -0
  69. package/build/lib/commands/mixins.js.map +1 -0
  70. package/build/lib/commands/network.d.ts +7 -138
  71. package/build/lib/commands/network.d.ts.map +1 -1
  72. package/build/lib/commands/network.js +212 -254
  73. package/build/lib/commands/network.js.map +1 -1
  74. package/build/lib/commands/performance.d.ts +24 -70
  75. package/build/lib/commands/performance.d.ts.map +1 -1
  76. package/build/lib/commands/performance.js +144 -100
  77. package/build/lib/commands/performance.js.map +1 -1
  78. package/build/lib/commands/permissions.d.ts +8 -92
  79. package/build/lib/commands/permissions.d.ts.map +1 -1
  80. package/build/lib/commands/permissions.js +75 -87
  81. package/build/lib/commands/permissions.js.map +1 -1
  82. package/build/lib/commands/recordscreen.d.ts +7 -193
  83. package/build/lib/commands/recordscreen.d.ts.map +1 -1
  84. package/build/lib/commands/recordscreen.js +151 -182
  85. package/build/lib/commands/recordscreen.js.map +1 -1
  86. package/build/lib/commands/shell.d.ts +7 -7
  87. package/build/lib/commands/shell.d.ts.map +1 -1
  88. package/build/lib/commands/shell.js +40 -33
  89. package/build/lib/commands/shell.js.map +1 -1
  90. package/build/lib/commands/streamscreen.d.ts +9 -103
  91. package/build/lib/commands/streamscreen.d.ts.map +1 -1
  92. package/build/lib/commands/streamscreen.js +261 -218
  93. package/build/lib/commands/streamscreen.js.map +1 -1
  94. package/build/lib/commands/system-bars.d.ts +22 -90
  95. package/build/lib/commands/system-bars.d.ts.map +1 -1
  96. package/build/lib/commands/system-bars.js +76 -74
  97. package/build/lib/commands/system-bars.js.map +1 -1
  98. package/build/lib/commands/touch.d.ts +10 -29
  99. package/build/lib/commands/touch.d.ts.map +1 -1
  100. package/build/lib/commands/touch.js +301 -285
  101. package/build/lib/commands/touch.js.map +1 -1
  102. package/build/lib/commands/types.d.ts +978 -0
  103. package/build/lib/commands/types.d.ts.map +1 -0
  104. package/build/lib/commands/types.js +3 -0
  105. package/build/lib/commands/types.js.map +1 -0
  106. package/build/lib/constraints.d.ts +291 -0
  107. package/build/lib/constraints.d.ts.map +1 -0
  108. package/build/lib/{desired-caps.js → constraints.js} +103 -102
  109. package/build/lib/constraints.js.map +1 -0
  110. package/build/lib/driver.d.ts +68 -37
  111. package/build/lib/driver.d.ts.map +1 -1
  112. package/build/lib/driver.js +123 -80
  113. package/build/lib/driver.js.map +1 -1
  114. package/build/lib/helpers/android.d.ts +164 -0
  115. package/build/lib/helpers/android.d.ts.map +1 -0
  116. package/build/lib/helpers/android.js +819 -0
  117. package/build/lib/helpers/android.js.map +1 -0
  118. package/build/lib/helpers/index.d.ts +7 -0
  119. package/build/lib/helpers/index.d.ts.map +1 -0
  120. package/build/lib/helpers/index.js +29 -0
  121. package/build/lib/helpers/index.js.map +1 -0
  122. package/build/lib/helpers/types.d.ts +121 -0
  123. package/build/lib/helpers/types.d.ts.map +1 -0
  124. package/build/lib/helpers/types.js +3 -0
  125. package/build/lib/helpers/types.js.map +1 -0
  126. package/build/lib/helpers/unlock.d.ts +32 -0
  127. package/build/lib/helpers/unlock.d.ts.map +1 -0
  128. package/build/lib/helpers/unlock.js +273 -0
  129. package/build/lib/helpers/unlock.js.map +1 -0
  130. package/build/lib/helpers/webview.d.ts +74 -0
  131. package/build/lib/helpers/webview.d.ts.map +1 -0
  132. package/build/lib/helpers/webview.js +421 -0
  133. package/build/lib/helpers/webview.js.map +1 -0
  134. package/build/lib/index.d.ts +9 -0
  135. package/build/lib/index.d.ts.map +1 -0
  136. package/build/lib/index.js +37 -0
  137. package/build/lib/index.js.map +1 -0
  138. package/build/lib/method-map.d.ts +0 -8
  139. package/build/lib/method-map.d.ts.map +1 -1
  140. package/build/lib/method-map.js +63 -74
  141. package/build/lib/method-map.js.map +1 -1
  142. package/build/lib/stubs.d.ts +0 -1
  143. package/build/lib/stubs.d.ts.map +1 -1
  144. package/build/lib/stubs.js +1 -0
  145. package/build/lib/stubs.js.map +1 -1
  146. package/build/lib/utils.d.ts +1 -1
  147. package/build/lib/utils.d.ts.map +1 -1
  148. package/lib/commands/actions.js +351 -464
  149. package/lib/commands/alert.js +27 -17
  150. package/lib/commands/app-management.js +156 -314
  151. package/lib/commands/context.js +457 -441
  152. package/lib/commands/element.js +201 -157
  153. package/lib/commands/emu-console.js +25 -45
  154. package/lib/commands/execute.js +106 -90
  155. package/lib/commands/file-actions.js +222 -240
  156. package/lib/commands/find.ts +103 -0
  157. package/lib/commands/general.js +327 -339
  158. package/lib/commands/ime.js +50 -34
  159. package/lib/commands/{index.js → index.ts} +20 -24
  160. package/lib/commands/intent.js +108 -249
  161. package/lib/commands/keyboard.js +20 -8
  162. package/lib/commands/log.js +172 -116
  163. package/lib/commands/media-projection.js +134 -161
  164. package/lib/commands/mixins.ts +966 -0
  165. package/lib/commands/network.js +252 -281
  166. package/lib/commands/performance.js +203 -132
  167. package/lib/commands/permissions.js +108 -109
  168. package/lib/commands/recordscreen.js +212 -209
  169. package/lib/commands/shell.js +51 -40
  170. package/lib/commands/streamscreen.js +355 -289
  171. package/lib/commands/system-bars.js +92 -83
  172. package/lib/commands/touch.js +357 -294
  173. package/lib/commands/types.ts +1097 -0
  174. package/lib/{desired-caps.js → constraints.ts} +106 -103
  175. package/lib/{driver.js → driver.ts} +278 -132
  176. package/lib/helpers/android.ts +1143 -0
  177. package/lib/helpers/index.ts +6 -0
  178. package/lib/helpers/types.ts +134 -0
  179. package/lib/helpers/unlock.ts +329 -0
  180. package/lib/helpers/webview.ts +582 -0
  181. package/lib/index.ts +18 -0
  182. package/lib/method-map.js +87 -98
  183. package/lib/stubs.ts +0 -1
  184. package/package.json +26 -19
  185. package/build/index.js +0 -51
  186. package/build/lib/android-helpers.d.ts +0 -136
  187. package/build/lib/android-helpers.d.ts.map +0 -1
  188. package/build/lib/android-helpers.js +0 -855
  189. package/build/lib/android-helpers.js.map +0 -1
  190. package/build/lib/commands/coverage.d.ts +0 -5
  191. package/build/lib/commands/coverage.d.ts.map +0 -1
  192. package/build/lib/commands/coverage.js +0 -19
  193. package/build/lib/commands/coverage.js.map +0 -1
  194. package/build/lib/desired-caps.d.ts +0 -353
  195. package/build/lib/desired-caps.d.ts.map +0 -1
  196. package/build/lib/desired-caps.js.map +0 -1
  197. package/build/lib/unlock-helpers.d.ts +0 -38
  198. package/build/lib/unlock-helpers.d.ts.map +0 -1
  199. package/build/lib/unlock-helpers.js +0 -266
  200. package/build/lib/unlock-helpers.js.map +0 -1
  201. package/build/lib/webview-helpers.d.ts +0 -224
  202. package/build/lib/webview-helpers.d.ts.map +0 -1
  203. package/build/lib/webview-helpers.js +0 -528
  204. package/build/lib/webview-helpers.js.map +0 -1
  205. package/index.js +0 -24
  206. package/lib/android-helpers.js +0 -983
  207. package/lib/commands/coverage.js +0 -18
  208. package/lib/commands/find.js +0 -82
  209. package/lib/unlock-helpers.js +0 -278
  210. package/lib/webview-helpers.js +0 -602
@@ -1,479 +1,495 @@
1
- import _ from 'lodash';
1
+ /* eslint-disable require-await */
2
+ // @ts-check
3
+ import {util} from '@appium/support';
2
4
  import Chromedriver from 'appium-chromedriver';
3
- import PortFinder from 'portfinder';
5
+ import {errors} from 'appium/driver';
4
6
  import B from 'bluebird';
5
- import { util } from '@appium/support';
6
- import { errors } from 'appium/driver';
7
+ import _ from 'lodash';
8
+ import PortFinder from 'portfinder';
7
9
  import {
8
- default as webviewHelpers,
9
- NATIVE_WIN, WEBVIEW_BASE, WEBVIEW_WIN, CHROMIUM_WIN, KNOWN_CHROME_PACKAGE_NAMES
10
- } from '../webview-helpers';
11
- import { APP_STATE } from '../android-helpers';
10
+ APP_STATE,
11
+ CHROMIUM_WIN,
12
+ KNOWN_CHROME_PACKAGE_NAMES,
13
+ NATIVE_WIN,
14
+ WEBVIEW_BASE,
15
+ WEBVIEW_WIN,
16
+ WebviewHelpers,
17
+ } from '../helpers';
18
+ import {mixin} from './mixins';
12
19
 
13
20
  const CHROMEDRIVER_AUTODOWNLOAD_FEATURE = 'chromedriver_autodownload';
14
21
 
15
- let commands = {}, helpers = {}, extensions = {};
16
-
17
-
18
- /* -------------------------------
19
- * Actual MJSONWP command handlers
20
- * ------------------------------- */
21
- commands.getCurrentContext = async function getCurrentContext () { // eslint-disable-line require-await
22
- // if the current context is `null`, indicating no context
23
- // explicitly set, it is the default context
24
- return this.curContext || this.defaultContextName();
25
- };
26
-
27
- commands.getContexts = async function getContexts () {
28
- const webviewsMapping = await webviewHelpers.getWebViewsMapping(this.adb, this.opts);
29
- return this.assignContexts(webviewsMapping);
30
- };
31
-
32
- commands.setContext = async function setContext (name) {
33
- if (!util.hasValue(name)) {
34
- name = this.defaultContextName();
35
- } else if (name === WEBVIEW_WIN) {
36
- // handle setContext "WEBVIEW"
37
- name = this.defaultWebviewName();
38
- }
39
- // if we're already in the context we want, do nothing
40
- if (name === this.curContext) {
41
- return;
42
- }
43
-
44
- const webviewsMapping = await webviewHelpers.getWebViewsMapping(this.adb, this.opts);
45
- const contexts = this.assignContexts(webviewsMapping);
46
- // if the context we want doesn't exist, fail
47
- if (!_.includes(contexts, name)) {
48
- throw new errors.NoSuchContextError();
49
- }
50
-
51
- await this.switchContext(name, webviewsMapping);
52
- this.curContext = name;
53
- };
54
-
55
22
  /**
56
- * @typedef {Object} WebviewsMapping
57
- * @property {string} proc The name of the Devtools Unix socket
58
- * @property {string} webview The web view alias. Looks like `WEBVIEW_`
59
- * prefix plus PID or package name
60
- * @property {?Object} info Webview information as it is retrieved by
61
- * /json/version CDP endpoint
62
- * @property {?Array<Object>} pages Webview pages list as it is retrieved by
63
- * /json/list CDP endpoint
64
- * @propery {?string} webviewName An actual webview name for switching context.
65
- * This value becomes null when failing to find a PID for a webview.
66
- *
67
- * The following json demonstrates the example of WebviewsMapping object.
68
- * Note that `description` in `page` can be an empty string most likely when it comes to Mobile Chrome)
69
- * {
70
- * "proc": "@webview_devtools_remote_22138",
71
- * "webview": "WEBVIEW_22138",
72
- * "info": {
73
- * "Android-Package": "io.appium.settings",
74
- * "Browser": "Chrome/74.0.3729.185",
75
- * "Protocol-Version": "1.3",
76
- * "User-Agent": "Mozilla/5.0 (Linux; Android 10; Android SDK built for x86 Build/QSR1.190920.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.185 Mobile Safari/537.36",
77
- * "V8-Version": "7.4.288.28",
78
- * "WebKit-Version": "537.36 (@22955682f94ce09336197bfb8dffea991fa32f0d)",
79
- * "webSocketDebuggerUrl": "ws://127.0.0.1:10900/devtools/browser"
80
- * },
81
- * "pages": [
82
- * {
83
- * "description": "{\"attached\":true,\"empty\":false,\"height\":1458,\"screenX\":0,\"screenY\":336,\"visible\":true,\"width\":1080}",
84
- * "devtoolsFrontendUrl": "http://chrome-devtools-frontend.appspot.com/serve_rev/@22955682f94ce09336197bfb8dffea991fa32f0d/inspector.html?ws=127.0.0.1:10900/devtools/page/27325CC50B600D31B233F45E09487B1F",
85
- * "id": "27325CC50B600D31B233F45E09487B1F",
86
- * "title": "Releases · appium/appium · GitHub",
87
- * "type": "page",
88
- * "url": "https://github.com/appium/appium/releases",
89
- * "webSocketDebuggerUrl": "ws://127.0.0.1:10900/devtools/page/27325CC50B600D31B233F45E09487B1F"
90
- * }
91
- * ],
92
- * "webviewName": "WEBVIEW_com.io.appium.setting"
93
- * }
23
+ * @type {import('./mixins').ContextMixin & ThisType<import('../driver').AndroidDriver>}
24
+ * @satisfies {import('@appium/types').ExternalDriver}
94
25
  */
95
-
96
- /**
97
- * Returns a webviewsMapping based on CDP endpoints
98
- *
99
- * @return {Array<WebviewsMapping>} webviewsMapping
100
- */
101
- commands.mobileGetContexts = async function mobileGetContexts () {
102
- const opts = {
103
- androidDeviceSocket: this.opts.androidDeviceSocket,
104
- ensureWebviewsHavePages: true,
105
- webviewDevtoolsPort: this.opts.webviewDevtoolsPort,
106
- enableWebviewDetailsCollection: true
107
- };
108
- return await webviewHelpers.getWebViewsMapping(this.adb, opts);
109
- };
110
-
111
- helpers.assignContexts = function assignContexts (webviewsMapping) {
112
- const opts = Object.assign({isChromeSession: this.isChromeSession}, this.opts);
113
- const webviews = webviewHelpers.parseWebviewNames(webviewsMapping, opts);
114
- this.contexts = [NATIVE_WIN, ...webviews];
115
- this.log.debug(`Available contexts: ${JSON.stringify(this.contexts)}`);
116
- return this.contexts;
117
- };
118
-
119
- helpers.switchContext = async function switchContext (name, webviewsMapping) {
120
- // We have some options when it comes to webviews. If we want a
121
- // Chromedriver webview, we can only control one at a time.
122
- if (this.isChromedriverContext(name)) {
123
- // start proxying commands directly to chromedriver
124
- await this.startChromedriverProxy(name, webviewsMapping);
125
- } else if (this.isChromedriverContext(this.curContext)) {
126
- // if we're moving to a non-chromedriver webview, and our current context
127
- // _is_ a chromedriver webview, if caps recreateChromeDriverSessions is set
128
- // to true then kill chromedriver session using stopChromedriverProxies or
129
- // else simply suspend proxying to the latter
130
- if (this.opts.recreateChromeDriverSessions) {
131
- this.log.debug('recreateChromeDriverSessions set to true; killing existing chromedrivers');
132
- await this.stopChromedriverProxies();
133
- } else {
134
- await this.suspendChromedriverProxy();
26
+ const ContextMixin = {
27
+ /* -------------------------------
28
+ * Actual MJSONWP command handlers
29
+ * ------------------------------- */
30
+ async getCurrentContext() {
31
+ // if the current context is `null`, indicating no context
32
+ // explicitly set, it is the default context
33
+ return this.curContext || this.defaultContextName();
34
+ },
35
+
36
+ async getContexts() {
37
+ const webviewsMapping = await WebviewHelpers.getWebViewsMapping(
38
+ /** @type {ADB} */ (this.adb),
39
+ this.opts
40
+ );
41
+ return this.assignContexts(webviewsMapping);
42
+ },
43
+
44
+ async setContext(name) {
45
+ if (!util.hasValue(name)) {
46
+ name = this.defaultContextName();
47
+ } else if (name === WEBVIEW_WIN) {
48
+ // handle setContext "WEBVIEW"
49
+ name = this.defaultWebviewName();
50
+ }
51
+ // if we're already in the context we want, do nothing
52
+ if (name === this.curContext) {
53
+ return;
135
54
  }
136
- } else {
137
- throw new Error(`Didn't know how to handle switching to context '${name}'`);
138
- }
139
- };
140
-
141
-
142
- /* ---------------------------------
143
- * On-object context-related helpers
144
- * --------------------------------- */
145
-
146
- // The reason this is a function and not just a constant is that both android-
147
- // driver and selendroid-driver use this logic, and each one returns
148
- // a different default context name
149
- helpers.defaultContextName = function defaultContextName () {
150
- return NATIVE_WIN;
151
- };
152
-
153
- helpers.defaultWebviewName = function defaultWebviewName () {
154
- return WEBVIEW_BASE + (this.opts.autoWebviewName || this.opts.appPackage);
155
- };
156
55
 
157
- helpers.isWebContext = function isWebContext () {
158
- return this.curContext !== null && this.curContext !== NATIVE_WIN;
159
- };
56
+ const webviewsMapping = await WebviewHelpers.getWebViewsMapping(
57
+ /** @type {ADB} */ (this.adb),
58
+ this.opts
59
+ );
60
+ const contexts = this.assignContexts(webviewsMapping);
61
+ // if the context we want doesn't exist, fail
62
+ if (!_.includes(contexts, name)) {
63
+ throw new errors.NoSuchContextError();
64
+ }
160
65
 
161
- // Turn on proxying to an existing Chromedriver session or a new one
162
- helpers.startChromedriverProxy = async function startChromedriverProxy (context, webviewsMapping) {
163
- this.log.debug(`Connecting to chrome-backed webview context '${context}'`);
164
-
165
- let cd;
166
- if (this.sessionChromedrivers[context]) {
167
- // in the case where we've already set up a chromedriver for a context,
168
- // we want to reconnect to it, not create a whole new one
169
- this.log.debug(`Found existing Chromedriver for context '${context}'. Using it.`);
170
- cd = this.sessionChromedrivers[context];
171
- await setupExistingChromedriver(this.log, cd);
172
- } else {
173
- let opts = _.cloneDeep(this.opts);
174
- opts.chromeUseRunningApp = true;
175
-
176
- // if requested, tell chromedriver to attach to the android package we have
177
- // associated with the context name, rather than the package of the AUT.
178
- // And turn this on by default for chrome--if chrome pops up with a webview
179
- // and someone wants to switch to it, we should let chromedriver connect to
180
- // chrome rather than staying stuck on the AUT
181
- if (opts.extractChromeAndroidPackageFromContextName || context === `${WEBVIEW_BASE}chrome`) {
182
- let androidPackage = context.match(`${WEBVIEW_BASE}(.+)`);
183
- if (androidPackage && androidPackage.length > 0) {
184
- opts.chromeAndroidPackage = androidPackage[1];
66
+ await this.switchContext(name, webviewsMapping);
67
+ this.curContext = name;
68
+ },
69
+
70
+ async mobileGetContexts() {
71
+ const opts = {
72
+ androidDeviceSocket: this.opts.androidDeviceSocket,
73
+ ensureWebviewsHavePages: true,
74
+ webviewDevtoolsPort: this.opts.webviewDevtoolsPort,
75
+ enableWebviewDetailsCollection: true,
76
+ };
77
+ return await WebviewHelpers.getWebViewsMapping(/** @type {ADB} */ (this.adb), opts);
78
+ },
79
+
80
+ assignContexts(webviewsMapping) {
81
+ const opts = Object.assign({isChromeSession: this.isChromeSession}, this.opts);
82
+ const webviews = WebviewHelpers.parseWebviewNames(webviewsMapping, opts);
83
+ this.contexts = [NATIVE_WIN, ...webviews];
84
+ this.log.debug(`Available contexts: ${JSON.stringify(this.contexts)}`);
85
+ return this.contexts;
86
+ },
87
+
88
+ async switchContext(name, webviewsMapping) {
89
+ // We have some options when it comes to webviews. If we want a
90
+ // Chromedriver webview, we can only control one at a time.
91
+ if (this.isChromedriverContext(name)) {
92
+ // start proxying commands directly to chromedriver
93
+ await this.startChromedriverProxy(name, webviewsMapping);
94
+ } else if (this.isChromedriverContext(this.curContext)) {
95
+ // if we're moving to a non-chromedriver webview, and our current context
96
+ // _is_ a chromedriver webview, if caps recreateChromeDriverSessions is set
97
+ // to true then kill chromedriver session using stopChromedriverProxies or
98
+ // else simply suspend proxying to the latter
99
+ if (this.opts.recreateChromeDriverSessions) {
100
+ this.log.debug('recreateChromeDriverSessions set to true; killing existing chromedrivers');
101
+ await this.stopChromedriverProxies();
102
+ } else {
103
+ this.suspendChromedriverProxy();
185
104
  }
186
- if (!opts.extractChromeAndroidPackageFromContextName) {
187
- if (_.has(this.opts, 'enableWebviewDetailsCollection') && !this.opts.enableWebviewDetailsCollection) {
188
- // When enableWebviewDetailsCollection capability is explicitly disabled, try to identify
189
- // chromeAndroidPackage based on contexts, known chrome variant packages and queryAppState result
190
- // since webviewsMapping does not have info object
191
- const contexts = webviewsMapping.map((wm) => wm.webviewName);
192
- for (const knownPackage of KNOWN_CHROME_PACKAGE_NAMES) {
193
- if (_.includes(contexts, `${WEBVIEW_BASE}${knownPackage}`)) {
194
- continue;
195
- }
196
- const appState = await this.queryAppState(knownPackage);
197
- if (_.includes([APP_STATE.RUNNING_IN_BACKGROUND, APP_STATE.RUNNING_IN_FOREGROUND], appState)) {
198
- opts.chromeAndroidPackage = knownPackage;
199
- this.log.debug(`Identified chromeAndroidPackage as '${opts.chromeAndroidPackage}' ` +
200
- `for context '${context}' by querying states of Chrome app packages`);
201
- break;
105
+ } else {
106
+ throw new Error(`Didn't know how to handle switching to context '${name}'`);
107
+ }
108
+ },
109
+
110
+ /* ---------------------------------
111
+ * On-object context-related helpers
112
+ * --------------------------------- */
113
+
114
+ // The reason this is a function and not just a constant is that both android-
115
+ // driver and selendroid-driver use this logic, and each one returns
116
+ // a different default context name
117
+ defaultContextName() {
118
+ return NATIVE_WIN;
119
+ },
120
+
121
+ defaultWebviewName() {
122
+ return WEBVIEW_BASE + (this.opts.autoWebviewName || this.opts.appPackage);
123
+ },
124
+
125
+ isWebContext() {
126
+ return this.curContext !== null && this.curContext !== NATIVE_WIN;
127
+ },
128
+
129
+ // Turn on proxying to an existing Chromedriver session or a new one
130
+ async startChromedriverProxy(context, webviewsMapping) {
131
+ this.log.debug(`Connecting to chrome-backed webview context '${context}'`);
132
+
133
+ let cd;
134
+ if (this.sessionChromedrivers[context]) {
135
+ // in the case where we've already set up a chromedriver for a context,
136
+ // we want to reconnect to it, not create a whole new one
137
+ this.log.debug(`Found existing Chromedriver for context '${context}'. Using it.`);
138
+ cd = this.sessionChromedrivers[context];
139
+ await this.setupExistingChromedriver(this.log, cd);
140
+ } else {
141
+ // XXX: this suppresses errors about putting arbitrary stuff on opts
142
+ const opts = /** @type {any} */ (_.cloneDeep(this.opts));
143
+ opts.chromeUseRunningApp = true;
144
+
145
+ // if requested, tell chromedriver to attach to the android package we have
146
+ // associated with the context name, rather than the package of the AUT.
147
+ // And turn this on by default for chrome--if chrome pops up with a webview
148
+ // and someone wants to switch to it, we should let chromedriver connect to
149
+ // chrome rather than staying stuck on the AUT
150
+ if (opts.extractChromeAndroidPackageFromContextName || context === `${WEBVIEW_BASE}chrome`) {
151
+ let androidPackage = context.match(`${WEBVIEW_BASE}(.+)`);
152
+ if (androidPackage && androidPackage.length > 0) {
153
+ opts.chromeAndroidPackage = androidPackage[1];
154
+ }
155
+ if (!opts.extractChromeAndroidPackageFromContextName) {
156
+ if (
157
+ _.has(this.opts, 'enableWebviewDetailsCollection') &&
158
+ !this.opts.enableWebviewDetailsCollection
159
+ ) {
160
+ // When enableWebviewDetailsCollection capability is explicitly disabled, try to identify
161
+ // chromeAndroidPackage based on contexts, known chrome variant packages and queryAppState result
162
+ // since webviewsMapping does not have info object
163
+ const contexts = webviewsMapping.map((wm) => wm.webviewName);
164
+ for (const knownPackage of KNOWN_CHROME_PACKAGE_NAMES) {
165
+ if (_.includes(contexts, `${WEBVIEW_BASE}${knownPackage}`)) {
166
+ continue;
167
+ }
168
+ const appState = await this.queryAppState(knownPackage);
169
+ if (
170
+ _.includes(
171
+ [APP_STATE.RUNNING_IN_BACKGROUND, APP_STATE.RUNNING_IN_FOREGROUND],
172
+ appState
173
+ )
174
+ ) {
175
+ opts.chromeAndroidPackage = knownPackage;
176
+ this.log.debug(
177
+ `Identified chromeAndroidPackage as '${opts.chromeAndroidPackage}' ` +
178
+ `for context '${context}' by querying states of Chrome app packages`
179
+ );
180
+ break;
181
+ }
202
182
  }
203
- }
204
- } else {
205
- for (const wm of webviewsMapping) {
206
- if (wm.webviewName === context && _.has(wm?.info, 'Android-Package')) {
207
- opts.chromeAndroidPackage = wm.info['Android-Package'];
208
- this.log.debug(`Identified chromeAndroidPackage as '${opts.chromeAndroidPackage}' ` +
209
- `for context '${context}' by CDP`);
210
- break;
183
+ } else {
184
+ for (const wm of webviewsMapping) {
185
+ if (wm.webviewName === context && _.has(wm?.info, 'Android-Package')) {
186
+ // XXX: should be a type guard here
187
+ opts.chromeAndroidPackage =
188
+ /** @type {NonNullable<import('./types').WebviewsMapping['info']>} */ (wm.info)[
189
+ 'Android-Package'
190
+ ];
191
+ this.log.debug(
192
+ `Identified chromeAndroidPackage as '${opts.chromeAndroidPackage}' ` +
193
+ `for context '${context}' by CDP`
194
+ );
195
+ break;
196
+ }
211
197
  }
212
198
  }
213
199
  }
214
200
  }
215
- }
216
201
 
217
- cd = await this.setupNewChromedriver(opts, this.adb.curDeviceId, this.adb, context);
218
- // bind our stop/exit handler, passing in context so we know which
219
- // one stopped unexpectedly
220
- cd.on(Chromedriver.EVENT_CHANGED, (msg) => {
202
+ cd = await this.setupNewChromedriver(
203
+ opts,
204
+ /** @type {string} */ (/** @type {ADB} */ (this.adb).curDeviceId),
205
+ /** @type {ADB} */ (this.adb),
206
+ context
207
+ );
208
+ // bind our stop/exit handler, passing in context so we know which
209
+ // one stopped unexpectedly
210
+ cd.on(Chromedriver.EVENT_CHANGED, (msg) => {
211
+ if (msg.state === Chromedriver.STATE_STOPPED) {
212
+ this.onChromedriverStop(context);
213
+ }
214
+ });
215
+ // save the chromedriver object under the context
216
+ this.sessionChromedrivers[context] = cd;
217
+ }
218
+ // hook up the local variables so we can proxy this biz
219
+ this.chromedriver = cd;
220
+ this.proxyReqRes = this.chromedriver.proxyReq.bind(this.chromedriver);
221
+ this.proxyCommand = /** @type {import('@appium/types').ExternalDriver['proxyCommand']} */ (
222
+ this.chromedriver.jwproxy.command.bind(this.chromedriver.jwproxy)
223
+ );
224
+ this.jwpProxyActive = true;
225
+ },
226
+
227
+ // Stop proxying to any Chromedriver
228
+ suspendChromedriverProxy() {
229
+ this.chromedriver = undefined;
230
+ this.proxyReqRes = undefined;
231
+ this.proxyCommand = undefined;
232
+ this.jwpProxyActive = false;
233
+ },
234
+
235
+ // Handle an out-of-band Chromedriver stop event
236
+ async onChromedriverStop(context) {
237
+ this.log.warn(`Chromedriver for context ${context} stopped unexpectedly`);
238
+ if (context === this.curContext) {
239
+ // we exited unexpectedly while automating the current context and so want
240
+ // to shut down the session and respond with an error
241
+ let err = new Error('Chromedriver quit unexpectedly during session');
242
+ await this.startUnexpectedShutdown(err);
243
+ } else {
244
+ // if a Chromedriver in the non-active context barfs, we don't really
245
+ // care, we'll just make a new one next time we need the context.
246
+ this.log.warn(
247
+ "Chromedriver quit unexpectedly, but it wasn't the active " + 'context, ignoring'
248
+ );
249
+ delete this.sessionChromedrivers[context];
250
+ }
251
+ },
252
+
253
+ // Intentionally stop all the chromedrivers currently active, and ignore
254
+ // their exit events
255
+ async stopChromedriverProxies() {
256
+ this.suspendChromedriverProxy(); // make sure we turn off the proxy flag
257
+ for (let context of _.keys(this.sessionChromedrivers)) {
258
+ let cd = this.sessionChromedrivers[context];
259
+ this.log.debug(`Stopping chromedriver for context ${context}`);
260
+ // stop listening for the stopped state event
261
+ cd.removeAllListeners(Chromedriver.EVENT_CHANGED);
262
+ try {
263
+ await cd.stop();
264
+ } catch (err) {
265
+ this.log.warn(`Error stopping Chromedriver: ${/** @type {Error} */ (err).message}`);
266
+ }
267
+ delete this.sessionChromedrivers[context];
268
+ }
269
+ },
270
+
271
+ isChromedriverContext(viewName) {
272
+ return _.includes(viewName, WEBVIEW_WIN) || viewName === CHROMIUM_WIN;
273
+ },
274
+
275
+ shouldDismissChromeWelcome() {
276
+ return (
277
+ !!this.opts.chromeOptions &&
278
+ _.isArray(this.opts.chromeOptions.args) &&
279
+ this.opts.chromeOptions.args.includes('--no-first-run')
280
+ );
281
+ },
282
+
283
+ async dismissChromeWelcome() {
284
+ this.log.info('Trying to dismiss Chrome welcome');
285
+ let activity = await this.getCurrentActivity();
286
+ if (activity !== 'org.chromium.chrome.browser.firstrun.FirstRunActivity') {
287
+ this.log.info('Chrome welcome dialog never showed up! Continuing');
288
+ return;
289
+ }
290
+ let el = await this.findElOrEls('id', 'com.android.chrome:id/terms_accept', false);
291
+ await this.click(/** @type {string} */ (el.ELEMENT));
292
+ try {
293
+ let el = await this.findElOrEls('id', 'com.android.chrome:id/negative_button', false);
294
+ await this.click(/** @type {string} */ (el.ELEMENT));
295
+ } catch (e) {
296
+ // DO NOTHING, THIS DEVICE DIDNT LAUNCH THE SIGNIN DIALOG
297
+ // IT MUST BE A NON GMS DEVICE
298
+ this.log.warn(
299
+ `This device did not show Chrome SignIn dialog, ${/** @type {Error} */ (e).message}`
300
+ );
301
+ }
302
+ },
303
+
304
+ async startChromeSession() {
305
+ this.log.info('Starting a chrome-based browser session');
306
+ // XXX: this suppresses errors about putting arbitrary stuff on opts
307
+ const opts = /** @type {any} */ (_.cloneDeep(this.opts));
308
+
309
+ const knownPackages = [
310
+ 'org.chromium.chrome.shell',
311
+ 'com.android.chrome',
312
+ 'com.chrome.beta',
313
+ 'org.chromium.chrome',
314
+ 'org.chromium.webview_shell',
315
+ ];
316
+
317
+ if (_.includes(knownPackages, this.opts.appPackage)) {
318
+ opts.chromeBundleId = this.opts.appPackage;
319
+ } else {
320
+ opts.chromeAndroidActivity = this.opts.appActivity;
321
+ }
322
+ this.chromedriver = await this.setupNewChromedriver(
323
+ opts,
324
+ /** @type {string} */ (/** @type {ADB} */ (this.adb).curDeviceId),
325
+ /** @type {ADB} */ (this.adb)
326
+ );
327
+ this.chromedriver.on(Chromedriver.EVENT_CHANGED, (msg) => {
221
328
  if (msg.state === Chromedriver.STATE_STOPPED) {
222
- this.onChromedriverStop(context);
329
+ this.onChromedriverStop(CHROMIUM_WIN);
223
330
  }
224
331
  });
225
- // save the chromedriver object under the context
226
- this.sessionChromedrivers[context] = cd;
227
- }
228
- // hook up the local variables so we can proxy this biz
229
- this.chromedriver = cd;
230
- this.proxyReqRes = this.chromedriver.proxyReq.bind(this.chromedriver);
231
- this.proxyCommand = this.chromedriver.jwproxy.command.bind(this.chromedriver.jwproxy);
232
- this.jwpProxyActive = true;
233
- };
234
-
235
- // Stop proxying to any Chromedriver
236
- helpers.suspendChromedriverProxy = function suspendChromedriverProxy () {
237
- this.chromedriver = null;
238
- this.proxyReqRes = null;
239
- this.proxyCommand = null;
240
- this.jwpProxyActive = false;
241
- };
242
332
 
243
- // Handle an out-of-band Chromedriver stop event
244
- helpers.onChromedriverStop = async function onChromedriverStop (context) {
245
- this.log.warn(`Chromedriver for context ${context} stopped unexpectedly`);
246
- if (context === this.curContext) {
247
- // we exited unexpectedly while automating the current context and so want
248
- // to shut down the session and respond with an error
249
- let err = new Error('Chromedriver quit unexpectedly during session');
250
- await this.startUnexpectedShutdown(err);
251
- } else {
252
- // if a Chromedriver in the non-active context barfs, we don't really
253
- // care, we'll just make a new one next time we need the context.
254
- this.log.warn("Chromedriver quit unexpectedly, but it wasn't the active " +
255
- 'context, ignoring');
256
- delete this.sessionChromedrivers[context];
257
- }
258
- };
259
-
260
- // Intentionally stop all the chromedrivers currently active, and ignore
261
- // their exit events
262
- helpers.stopChromedriverProxies = async function stopChromedriverProxies () {
263
- this.suspendChromedriverProxy(); // make sure we turn off the proxy flag
264
- for (let context of _.keys(this.sessionChromedrivers)) {
265
- let cd = this.sessionChromedrivers[context];
266
- this.log.debug(`Stopping chromedriver for context ${context}`);
267
- // stop listening for the stopped state event
268
- cd.removeAllListeners(Chromedriver.EVENT_CHANGED);
269
- try {
270
- await cd.stop();
271
- } catch (err) {
272
- this.log.warn(`Error stopping Chromedriver: ${err.message}`);
333
+ // Now that we have a Chrome session, we ensure that the context is
334
+ // appropriately set and that this chromedriver is added to the list
335
+ // of session chromedrivers so we can switch back and forth
336
+ this.curContext = CHROMIUM_WIN;
337
+ this.sessionChromedrivers[CHROMIUM_WIN] = this.chromedriver;
338
+ this.proxyReqRes = this.chromedriver.proxyReq.bind(this.chromedriver);
339
+ this.proxyCommand = /** @type {import('@appium/types').ExternalDriver['proxyCommand']} */ (
340
+ this.chromedriver.jwproxy.command.bind(this.chromedriver.jwproxy)
341
+ );
342
+ this.jwpProxyActive = true;
343
+
344
+ if (this.shouldDismissChromeWelcome()) {
345
+ // dismiss Chrome welcome dialog
346
+ await this.dismissChromeWelcome();
347
+ }
348
+ },
349
+
350
+ /* --------------------------
351
+ * Internal library functions
352
+ * -------------------------- */
353
+
354
+ async setupExistingChromedriver(log, chromedriver) {
355
+ // check the status by sending a simple window-based command to ChromeDriver
356
+ // if there is an error, we want to recreate the ChromeDriver session
357
+ if (!(await chromedriver.hasWorkingWebview())) {
358
+ log.debug('ChromeDriver is not associated with a window. ' + 'Re-initializing the session.');
359
+ await chromedriver.restart();
360
+ }
361
+ return chromedriver;
362
+ },
363
+
364
+ async getChromedriverPort(portSpec, log) {
365
+ const getPort = /** @type {(opts?: import('portfinder').PortFinderOptions) => B<number>} */ (
366
+ B.promisify(PortFinder.getPort, {context: PortFinder})
367
+ );
368
+
369
+ // if the user didn't give us any specific information about chromedriver
370
+ // port ranges, just find any free port
371
+ if (!portSpec) {
372
+ const port = await getPort();
373
+ log?.debug(`A port was not given, using random free port: ${port}`);
374
+ return port;
273
375
  }
274
- delete this.sessionChromedrivers[context];
275
- }
276
- };
277
-
278
- helpers.isChromedriverContext = function isChromedriverContext (viewName) {
279
- return _.includes(viewName, WEBVIEW_WIN) || viewName === CHROMIUM_WIN;
280
- };
281
-
282
- helpers.shouldDismissChromeWelcome = function shouldDismissChromeWelcome () {
283
- return !!this.opts.chromeOptions &&
284
- _.isArray(this.opts.chromeOptions.args) &&
285
- this.opts.chromeOptions.args.includes('--no-first-run');
286
- };
287
-
288
- helpers.dismissChromeWelcome = async function dismissChromeWelcome () {
289
- this.log.info('Trying to dismiss Chrome welcome');
290
- let activity = await this.getCurrentActivity();
291
- if (activity !== 'org.chromium.chrome.browser.firstrun.FirstRunActivity') {
292
- this.log.info('Chrome welcome dialog never showed up! Continuing');
293
- return;
294
- }
295
- let el = await this.findElOrEls('id', 'com.android.chrome:id/terms_accept', false);
296
- await this.click(el.ELEMENT);
297
- try {
298
- let el = await this.findElOrEls('id', 'com.android.chrome:id/negative_button', false);
299
- await this.click(el.ELEMENT);
300
- } catch (e) {
301
- // DO NOTHING, THIS DEVICE DIDNT LAUNCH THE SIGNIN DIALOG
302
- // IT MUST BE A NON GMS DEVICE
303
- this.log.warn(`This device did not show Chrome SignIn dialog, ${e.message}`);
304
- }
305
- };
306
376
 
307
- helpers.startChromeSession = async function startChromeSession () {
308
- this.log.info('Starting a chrome-based browser session');
309
- let opts = _.cloneDeep(this.opts);
310
-
311
- const knownPackages = [
312
- 'org.chromium.chrome.shell',
313
- 'com.android.chrome',
314
- 'com.chrome.beta',
315
- 'org.chromium.chrome',
316
- 'org.chromium.webview_shell',
317
- ];
318
-
319
- if (_.includes(knownPackages, this.opts.appPackage)) {
320
- opts.chromeBundleId = this.opts.appPackage;
321
- } else {
322
- opts.chromeAndroidActivity = this.opts.appActivity;
323
- }
324
- this.chromedriver = await this.setupNewChromedriver(opts, this.adb.curDeviceId, this.adb);
325
- this.chromedriver.on(Chromedriver.EVENT_CHANGED, (msg) => {
326
- if (msg.state === Chromedriver.STATE_STOPPED) {
327
- this.onChromedriverStop(CHROMIUM_WIN);
377
+ // otherwise find the free port based on a list or range provided by the user
378
+ log?.debug(`Finding a free port for chromedriver using spec ${JSON.stringify(portSpec)}`);
379
+ let foundPort = null;
380
+ for (const potentialPort of portSpec) {
381
+ /** @type {number} */
382
+ let port;
383
+ /** @type {number} */
384
+ let stopPort;
385
+ if (_.isArray(potentialPort)) {
386
+ [port, stopPort] = potentialPort.map((p) => parseInt(String(p), 10));
387
+ } else {
388
+ port = parseInt(String(potentialPort), 10); // ensure we have a number and not a string
389
+ stopPort = port;
390
+ }
391
+ try {
392
+ log?.debug(`Checking port range ${port}:${stopPort}`);
393
+ foundPort = await getPort({port, stopPort});
394
+ break;
395
+ } catch (e) {
396
+ log?.debug(`Nothing in port range ${port}:${stopPort} was available`);
397
+ }
328
398
  }
329
- });
330
-
331
- // Now that we have a Chrome session, we ensure that the context is
332
- // appropriately set and that this chromedriver is added to the list
333
- // of session chromedrivers so we can switch back and forth
334
- this.curContext = CHROMIUM_WIN;
335
- this.sessionChromedrivers[CHROMIUM_WIN] = this.chromedriver;
336
- this.proxyReqRes = this.chromedriver.proxyReq.bind(this.chromedriver);
337
- this.proxyCommand = this.chromedriver.jwproxy.command.bind(this.chromedriver.jwproxy);
338
- this.jwpProxyActive = true;
339
-
340
- if (this.shouldDismissChromeWelcome()) {
341
- // dismiss Chrome welcome dialog
342
- await this.dismissChromeWelcome();
343
- }
344
- };
345
399
 
400
+ if (foundPort === null) {
401
+ throw new Error(
402
+ `Could not find a free port for chromedriver using ` +
403
+ `chromedriverPorts spec ${JSON.stringify(portSpec)}`
404
+ );
405
+ }
346
406
 
347
- /* --------------------------
348
- * Internal library functions
349
- * -------------------------- */
407
+ log?.debug(`Using free port ${foundPort} for chromedriver`);
408
+ return foundPort;
409
+ },
350
410
 
351
- async function setupExistingChromedriver (log, chromedriver) {
352
- // check the status by sending a simple window-based command to ChromeDriver
353
- // if there is an error, we want to recreate the ChromeDriver session
354
- if (!await chromedriver.hasWorkingWebview()) {
355
- log.debug('ChromeDriver is not associated with a window. ' +
356
- 'Re-initializing the session.');
357
- await chromedriver.restart();
358
- }
359
- return chromedriver;
360
- }
411
+ isChromedriverAutodownloadEnabled() {
412
+ if (this.isFeatureEnabled(CHROMEDRIVER_AUTODOWNLOAD_FEATURE)) {
413
+ return true;
414
+ }
415
+ this?.log?.debug(
416
+ `Automated Chromedriver download is disabled. ` +
417
+ `Use '${CHROMEDRIVER_AUTODOWNLOAD_FEATURE}' server feature to enable it`
418
+ );
419
+ return false;
420
+ },
421
+
422
+ async setupNewChromedriver(opts, curDeviceId, adb, context) {
423
+ if (opts.chromeDriverPort) {
424
+ this?.log?.warn(
425
+ `The 'chromeDriverPort' capability is deprecated. Please use 'chromedriverPort' instead`
426
+ );
427
+ opts.chromedriverPort = opts.chromeDriverPort;
428
+ }
361
429
 
362
- /**
363
- * Find a free port to have Chromedriver listen on.
364
- *
365
- * @param {array} portSpec - Array which is a list of ports. A list item may
366
- * also itself be an array of length 2 specifying a start and end port of
367
- * a range. Some valid port specs:
368
- * - [8000, 8001, 8002]
369
- * - [[8000, 8005]]
370
- * - [8000, [9000, 9100]]
371
- * @param {Object?} log Logger instance
372
- *
373
- * @return {number} A free port
374
- */
375
- async function getChromedriverPort (portSpec, log = null) {
376
- const getPort = B.promisify(PortFinder.getPort, {context: PortFinder});
377
-
378
- // if the user didn't give us any specific information about chromedriver
379
- // port ranges, just find any free port
380
- if (!portSpec) {
381
- const port = await getPort();
382
- log?.debug(`A port was not given, using random free port: ${port}`);
383
- return port;
384
- }
385
-
386
- // otherwise find the free port based on a list or range provided by the user
387
- log?.debug(`Finding a free port for chromedriver using spec ${JSON.stringify(portSpec)}`);
388
- let foundPort = null;
389
- for (const potentialPort of portSpec) {
390
- let port, stopPort;
391
- if (_.isArray(potentialPort)) {
392
- ([port, stopPort] = potentialPort);
430
+ if (opts.chromedriverPort) {
431
+ this?.log?.debug(`Using user-specified port ${opts.chromedriverPort} for chromedriver`);
393
432
  } else {
394
- port = parseInt(potentialPort, 10); // ensure we have a number and not a string
395
- stopPort = port;
433
+ // if a single port wasn't given, we'll look for a free one
434
+ opts.chromedriverPort = await this.getChromedriverPort(opts.chromedriverPorts, this?.log);
396
435
  }
397
- try {
398
- log?.debug(`Checking port range ${port}:${stopPort}`);
399
- foundPort = await getPort({port, stopPort});
400
- break;
401
- } catch (e) {
402
- log?.debug(`Nothing in port range ${port}:${stopPort} was available`);
436
+
437
+ const details = context ? WebviewHelpers.getWebviewDetails(adb, context) : undefined;
438
+ if (!_.isEmpty(details)) {
439
+ this?.log?.debug(
440
+ 'Passing web view details to the Chromedriver constructor: ' +
441
+ JSON.stringify(details, null, 2)
442
+ );
403
443
  }
404
- }
405
-
406
- if (foundPort === null) {
407
- throw new Error(`Could not find a free port for chromedriver using ` +
408
- `chromedriverPorts spec ${JSON.stringify(portSpec)}`);
409
- }
410
-
411
- log?.debug(`Using free port ${foundPort} for chromedriver`);
412
- return foundPort;
413
- }
414
-
415
- helpers.isChromedriverAutodownloadEnabled = function isChromedriverAutodownloadEnabled () {
416
- if (this.isFeatureEnabled(CHROMEDRIVER_AUTODOWNLOAD_FEATURE)) {
417
- return true;
418
- }
419
- this?.log?.debug(`Automated Chromedriver download is disabled. ` +
420
- `Use '${CHROMEDRIVER_AUTODOWNLOAD_FEATURE}' server feature to enable it`);
421
- return false;
422
- };
423
444
 
424
- helpers.setupNewChromedriver = async function setupNewChromedriver (opts, curDeviceId, adb, context = null) {
425
- if (opts.chromeDriverPort) {
426
- this?.log?.warn(`The 'chromeDriverPort' capability is deprecated. Please use 'chromedriverPort' instead`);
427
- opts.chromedriverPort = opts.chromeDriverPort;
428
- }
429
-
430
- if (opts.chromedriverPort) {
431
- this?.log?.debug(`Using user-specified port ${opts.chromedriverPort} for chromedriver`);
432
- } else {
433
- // if a single port wasn't given, we'll look for a free one
434
- opts.chromedriverPort = await getChromedriverPort(opts.chromedriverPorts, this?.log);
435
- }
436
-
437
- const details = context ? webviewHelpers.getWebviewDetails(adb, context) : undefined;
438
- if (!_.isEmpty(details)) {
439
- this?.log?.debug('Passing web view details to the Chromedriver constructor: ' +
440
- JSON.stringify(details, null, 2));
441
- }
442
-
443
- const chromedriver = new Chromedriver({
444
- port: opts.chromedriverPort,
445
- executable: opts.chromedriverExecutable,
446
- adb,
447
- cmdArgs: opts.chromedriverArgs,
448
- verbose: !!opts.showChromedriverLog,
449
- executableDir: opts.chromedriverExecutableDir,
450
- mappingPath: opts.chromedriverChromeMappingFile,
451
- bundleId: opts.chromeBundleId,
452
- useSystemExecutable: opts.chromedriverUseSystemExecutable,
453
- disableBuildCheck: opts.chromedriverDisableBuildCheck,
454
- details,
455
- isAutodownloadEnabled: this?.isChromedriverAutodownloadEnabled?.()
456
- });
457
-
458
- // make sure there are chromeOptions
459
- opts.chromeOptions = opts.chromeOptions || {};
460
- // try out any prefixed chromeOptions,
461
- // and strip the prefix
462
- for (const opt of _.keys(opts)) {
463
- if (opt.endsWith(':chromeOptions')) {
464
- this?.log?.warn(`Merging '${opt}' into 'chromeOptions'. This may cause unexpected behavior`);
465
- _.merge(opts.chromeOptions, opts[opt]);
445
+ const chromedriver = new Chromedriver({
446
+ port: String(opts.chromedriverPort),
447
+ executable: opts.chromedriverExecutable,
448
+ // eslint-disable-next-line object-shorthand
449
+ adb: /** @type {any} */ (adb),
450
+ cmdArgs: /** @type {string[]} */ (opts.chromedriverArgs),
451
+ verbose: !!opts.showChromedriverLog,
452
+ executableDir: opts.chromedriverExecutableDir,
453
+ mappingPath: opts.chromedriverChromeMappingFile,
454
+ // @ts-expect-error arbitrary value on opts?
455
+ bundleId: opts.chromeBundleId,
456
+ useSystemExecutable: opts.chromedriverUseSystemExecutable,
457
+ disableBuildCheck: opts.chromedriverDisableBuildCheck,
458
+ // @ts-expect-error FIXME: chromedriver typing are probably too strict
459
+ details,
460
+ isAutodownloadEnabled: this?.isChromedriverAutodownloadEnabled?.(),
461
+ });
462
+
463
+ // make sure there are chromeOptions
464
+ opts.chromeOptions = opts.chromeOptions || {};
465
+ // try out any prefixed chromeOptions,
466
+ // and strip the prefix
467
+ for (const opt of _.keys(opts)) {
468
+ if (opt.endsWith(':chromeOptions')) {
469
+ this?.log?.warn(
470
+ `Merging '${opt}' into 'chromeOptions'. This may cause unexpected behavior`
471
+ );
472
+ // @ts-expect-error unsafe types
473
+ _.merge(opts.chromeOptions, opts[opt]);
474
+ }
466
475
  }
467
- }
468
476
 
469
- const caps = webviewHelpers.createChromedriverCaps(opts, curDeviceId, details);
470
- this?.log?.debug(`Before starting chromedriver, androidPackage is '${caps.chromeOptions.androidPackage}'`);
471
- await chromedriver.start(caps);
472
- return chromedriver;
477
+ const caps = /** @type {any} */ (
478
+ WebviewHelpers.createChromedriverCaps(opts, curDeviceId, details)
479
+ );
480
+ this?.log?.debug(
481
+ `Before starting chromedriver, androidPackage is '${caps.chromeOptions.androidPackage}'`
482
+ );
483
+ await chromedriver.start(caps);
484
+ return chromedriver;
485
+ },
473
486
  };
474
- const setupNewChromedriver = helpers.setupNewChromedriver;
475
487
 
488
+ mixin(ContextMixin);
489
+
490
+ export default ContextMixin;
491
+ export const setupNewChromedriver = ContextMixin.setupNewChromedriver;
476
492
 
477
- Object.assign(extensions, commands, helpers);
478
- export { commands, helpers, setupNewChromedriver };
479
- export default extensions;
493
+ /**
494
+ * @typedef {import('appium-adb').ADB} ADB
495
+ */