appium-android-driver 13.1.2 → 13.2.0

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 (83) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/build/lib/commands/app-management.d.ts.map +1 -1
  3. package/build/lib/commands/app-management.js +2 -3
  4. package/build/lib/commands/app-management.js.map +1 -1
  5. package/build/lib/commands/bidi/models.d.ts +7 -0
  6. package/build/lib/commands/bidi/models.d.ts.map +1 -1
  7. package/build/lib/commands/bidi/models.js +7 -0
  8. package/build/lib/commands/bidi/models.js.map +1 -1
  9. package/build/lib/commands/bidi/types.d.ts +6 -6
  10. package/build/lib/commands/bidi/types.d.ts.map +1 -1
  11. package/build/lib/commands/bluetooth.d.ts +1 -1
  12. package/build/lib/commands/bluetooth.d.ts.map +1 -1
  13. package/build/lib/commands/bluetooth.js.map +1 -1
  14. package/build/lib/commands/context/exports.d.ts.map +1 -1
  15. package/build/lib/commands/context/exports.js +14 -4
  16. package/build/lib/commands/context/exports.js.map +1 -1
  17. package/build/lib/commands/context/helpers.d.ts.map +1 -1
  18. package/build/lib/commands/context/helpers.js +1 -2
  19. package/build/lib/commands/context/helpers.js.map +1 -1
  20. package/build/lib/commands/device/common.d.ts.map +1 -1
  21. package/build/lib/commands/device/common.js +1 -2
  22. package/build/lib/commands/device/common.js.map +1 -1
  23. package/build/lib/commands/device/utils.d.ts.map +1 -1
  24. package/build/lib/commands/device/utils.js +1 -2
  25. package/build/lib/commands/device/utils.js.map +1 -1
  26. package/build/lib/commands/execute.d.ts +1 -1
  27. package/build/lib/commands/execute.d.ts.map +1 -1
  28. package/build/lib/commands/execute.js +0 -1
  29. package/build/lib/commands/execute.js.map +1 -1
  30. package/build/lib/commands/geolocation.js +3 -3
  31. package/build/lib/commands/geolocation.js.map +1 -1
  32. package/build/lib/commands/lock/exports.js +2 -2
  33. package/build/lib/commands/lock/exports.js.map +1 -1
  34. package/build/lib/commands/media-projection.d.ts.map +1 -1
  35. package/build/lib/commands/media-projection.js +0 -1
  36. package/build/lib/commands/media-projection.js.map +1 -1
  37. package/build/lib/commands/network.d.ts.map +1 -1
  38. package/build/lib/commands/network.js +21 -11
  39. package/build/lib/commands/network.js.map +1 -1
  40. package/build/lib/commands/performance.d.ts.map +1 -1
  41. package/build/lib/commands/performance.js +68 -68
  42. package/build/lib/commands/performance.js.map +1 -1
  43. package/build/lib/commands/permissions.d.ts +4 -4
  44. package/build/lib/commands/permissions.d.ts.map +1 -1
  45. package/build/lib/commands/permissions.js +2 -4
  46. package/build/lib/commands/permissions.js.map +1 -1
  47. package/build/lib/commands/recordscreen.js +1 -1
  48. package/build/lib/commands/recordscreen.js.map +1 -1
  49. package/build/lib/commands/streamscreen.d.ts.map +1 -1
  50. package/build/lib/commands/streamscreen.js +65 -45
  51. package/build/lib/commands/streamscreen.js.map +1 -1
  52. package/build/lib/commands/system-bars.d.ts +4 -4
  53. package/build/lib/commands/system-bars.d.ts.map +1 -1
  54. package/build/lib/commands/system-bars.js +0 -1
  55. package/build/lib/commands/system-bars.js.map +1 -1
  56. package/build/lib/driver.d.ts +7 -7
  57. package/build/lib/driver.d.ts.map +1 -1
  58. package/build/lib/driver.js +69 -69
  59. package/build/lib/driver.js.map +1 -1
  60. package/build/lib/utils.d.ts +6 -6
  61. package/build/lib/utils.d.ts.map +1 -1
  62. package/build/lib/utils.js.map +1 -1
  63. package/lib/commands/app-management.ts +2 -3
  64. package/lib/commands/bidi/models.ts +7 -0
  65. package/lib/commands/bidi/types.ts +10 -10
  66. package/lib/commands/bluetooth.ts +2 -2
  67. package/lib/commands/context/exports.ts +16 -4
  68. package/lib/commands/context/helpers.ts +1 -2
  69. package/lib/commands/device/common.ts +1 -2
  70. package/lib/commands/device/utils.ts +1 -2
  71. package/lib/commands/execute.ts +2 -6
  72. package/lib/commands/geolocation.ts +6 -10
  73. package/lib/commands/lock/exports.ts +2 -2
  74. package/lib/commands/media-projection.ts +10 -14
  75. package/lib/commands/network.ts +28 -22
  76. package/lib/commands/performance.ts +68 -68
  77. package/lib/commands/permissions.ts +12 -15
  78. package/lib/commands/recordscreen.ts +1 -1
  79. package/lib/commands/streamscreen.ts +77 -53
  80. package/lib/commands/system-bars.ts +5 -9
  81. package/lib/driver.ts +91 -91
  82. package/lib/utils.ts +7 -7
  83. package/package.json +1 -3
@@ -1,6 +1,5 @@
1
1
  import {fs, logger, system, util} from '@appium/support';
2
2
  import {waitForCondition} from 'asyncbox';
3
- import B from 'bluebird';
4
3
  import _ from 'lodash';
5
4
  import {spawn} from 'node:child_process';
6
5
  import http from 'node:http';
@@ -139,65 +138,75 @@ export async function mobileStartScreenStreaming(
139
138
 
140
139
  let mjpegSocket: net.Socket | undefined;
141
140
  let mjpegServer: http.Server | undefined;
142
- try {
143
- await new B<void>((resolve, reject) => {
144
- mjpegSocket = net.createConnection(tcpPort, TCP_HOST, () => {
145
- this.log.info(`Successfully connected to MJPEG stream at tcp://${TCP_HOST}:${tcpPort}`);
146
- mjpegServer = http.createServer((req, res) => {
147
- const remoteAddress = extractRemoteAddress(req);
148
- const currentPathname = url.parse(String(req.url)).pathname;
141
+ let mjpegConnectTimeoutId: ReturnType<typeof setTimeout> | undefined;
142
+ const mjpegConnectTimeoutPromise = new Promise<never>((_resolve, reject) => {
143
+ mjpegConnectTimeoutId = setTimeout(
144
+ () =>
145
+ reject(
146
+ new Error(
147
+ `Cannot connect to the streaming server within ${STREAMING_STARTUP_TIMEOUT_MS}ms`,
148
+ ),
149
+ ),
150
+ STREAMING_STARTUP_TIMEOUT_MS,
151
+ );
152
+ });
153
+ const mjpegServerReadyPromise = new Promise<void>((resolve, reject) => {
154
+ mjpegSocket = net.createConnection(tcpPort, TCP_HOST, () => {
155
+ this.log.info(`Successfully connected to MJPEG stream at tcp://${TCP_HOST}:${tcpPort}`);
156
+ mjpegServer = http.createServer((req, res) => {
157
+ const remoteAddress = extractRemoteAddress(req);
158
+ const currentPathname = extractSafePathnameFromUrl(req.url);
159
+ this.log.info(
160
+ `Got an incoming screen broadcasting request from ${remoteAddress} ` +
161
+ `(${req.headers['user-agent'] || 'User Agent unknown'}) at ${currentPathname}`,
162
+ );
163
+
164
+ if (pathname && currentPathname !== pathname) {
149
165
  this.log.info(
150
- `Got an incoming screen broadcasting request from ${remoteAddress} ` +
151
- `(${req.headers['user-agent'] || 'User Agent unknown'}) at ${currentPathname}`,
166
+ 'Rejecting the broadcast request since it does not match the given pathname',
152
167
  );
153
-
154
- if (pathname && currentPathname !== pathname) {
155
- this.log.info(
156
- 'Rejecting the broadcast request since it does not match the given pathname',
157
- );
158
- res.writeHead(404, {
159
- Connection: 'close',
160
- 'Content-Type': 'text/plain; charset=utf-8',
161
- });
162
- res.write(`'${currentPathname}' did not match any known endpoints`);
163
- res.end();
164
- return;
165
- }
166
-
167
- this.log.info('Starting MJPEG broadcast');
168
- res.writeHead(200, {
169
- 'Cache-Control':
170
- 'no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0',
171
- Pragma: 'no-cache',
168
+ res.writeHead(404, {
172
169
  Connection: 'close',
173
- 'Content-Type': `multipart/x-mixed-replace; boundary=${BOUNDARY_STRING}`,
170
+ 'Content-Type': 'text/plain; charset=utf-8',
174
171
  });
172
+ res.write(`'${currentPathname}' did not match any known endpoints`);
173
+ res.end();
174
+ return;
175
+ }
175
176
 
176
- if (mjpegSocket) {
177
- mjpegSocket.pipe(res);
178
- }
177
+ this.log.info('Starting MJPEG broadcast');
178
+ res.writeHead(200, {
179
+ 'Cache-Control':
180
+ 'no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0',
181
+ Pragma: 'no-cache',
182
+ Connection: 'close',
183
+ 'Content-Type': `multipart/x-mixed-replace; boundary=${BOUNDARY_STRING}`,
179
184
  });
180
- mjpegServer.on('error', (e) => {
181
- this.log.warn(e);
182
- reject(e);
183
- });
184
- mjpegServer.on('close', () => {
185
- this.log.debug(`MJPEG server at http://${host}:${port} has been closed`);
186
- });
187
- mjpegServer.on('listening', () => {
188
- this.log.info(`Successfully started MJPEG server at http://${host}:${port}`);
189
- resolve();
190
- });
191
- mjpegServer.listen(port, host);
185
+
186
+ if (mjpegSocket) {
187
+ mjpegSocket.pipe(res);
188
+ }
192
189
  });
193
- mjpegSocket.on('error', (e) => {
194
- this.log.error(e);
190
+ mjpegServer.on('error', (e) => {
191
+ this.log.warn(e);
195
192
  reject(e);
196
193
  });
197
- }).timeout(
198
- STREAMING_STARTUP_TIMEOUT_MS,
199
- `Cannot connect to the streaming server within ${STREAMING_STARTUP_TIMEOUT_MS}ms`,
200
- );
194
+ mjpegServer.on('close', () => {
195
+ this.log.debug(`MJPEG server at http://${host}:${port} has been closed`);
196
+ });
197
+ mjpegServer.on('listening', () => {
198
+ this.log.info(`Successfully started MJPEG server at http://${host}:${port}`);
199
+ resolve();
200
+ });
201
+ mjpegServer.listen(port, host);
202
+ });
203
+ mjpegSocket.on('error', (e) => {
204
+ this.log.error(e);
205
+ reject(e);
206
+ });
207
+ });
208
+ try {
209
+ await Promise.race([mjpegServerReadyPromise, mjpegConnectTimeoutPromise]);
201
210
  } catch (e) {
202
211
  if (deviceStreamingProc.kill(0)) {
203
212
  deviceStreamingProc.kill();
@@ -212,6 +221,10 @@ export async function mobileStartScreenStreaming(
212
221
  mjpegServer.close();
213
222
  }
214
223
  throw e;
224
+ } finally {
225
+ if (mjpegConnectTimeoutId !== undefined) {
226
+ clearTimeout(mjpegConnectTimeoutId);
227
+ }
215
228
  }
216
229
 
217
230
  this._screenStreamingProps = {
@@ -303,7 +316,7 @@ async function verifyStreamingRequirements(adb: ADB): Promise<void> {
303
316
  })(),
304
317
  );
305
318
  }
306
- await B.all(gstreamerCheckPromises);
319
+ await Promise.all(gstreamerCheckPromises);
307
320
 
308
321
  const moduleCheckPromises: Promise<void>[] = [];
309
322
  for (const [name, modName] of _.toPairs(REQUIRED_GST_PLUGINS)) {
@@ -319,7 +332,7 @@ async function verifyStreamingRequirements(adb: ADB): Promise<void> {
319
332
  })(),
320
333
  );
321
334
  }
322
- await B.all(moduleCheckPromises);
335
+ await Promise.all(moduleCheckPromises);
323
336
  }
324
337
 
325
338
  const deviceInfoRegexes = [
@@ -499,6 +512,17 @@ async function initGstreamerPipeline(
499
512
  return gstreamerPipeline;
500
513
  }
501
514
 
515
+ /**
516
+ * WHATWG URL parsing can throw on malformed input; HTTP servers must not throw synchronously in request handlers.
517
+ */
518
+ function extractSafePathnameFromUrl(requestUrl: string | undefined): string {
519
+ try {
520
+ return new url.URL(String(requestUrl ?? '/'), 'http://127.0.0.1').pathname;
521
+ } catch {
522
+ return '/';
523
+ }
524
+ }
525
+
502
526
  /**
503
527
  * @privateRemarks This may need to be future-proofed, as `IncomingMessage.connection` is deprecated and its `socket` prop is likely private
504
528
  */
@@ -22,6 +22,11 @@ const DEFAULT_WINDOW_PROPERTIES: WindowProperties = {
22
22
  height: 0,
23
23
  };
24
24
 
25
+ interface SystemBarsResult {
26
+ statusBar?: WindowProperties;
27
+ navigationBar?: WindowProperties;
28
+ }
29
+
25
30
  /**
26
31
  * Gets the system bars (status bar and navigation bar) properties.
27
32
  *
@@ -86,8 +91,6 @@ export async function mobilePerformStatusBarCommand(
86
91
  return await action();
87
92
  }
88
93
 
89
- // #region Internal helpers
90
-
91
94
  /**
92
95
  * Parses window properties from adb dumpsys output
93
96
  *
@@ -185,10 +188,3 @@ export function parseWindows(this: AndroidDriver, lines: string): SystemBarsResu
185
188
  }
186
189
  return result;
187
190
  }
188
-
189
- // #endregion
190
-
191
- interface SystemBarsResult {
192
- statusBar?: WindowProperties;
193
- navigationBar?: WindowProperties;
194
- }
package/lib/driver.ts CHANGED
@@ -240,97 +240,6 @@ class AndroidDriver
240
240
  });
241
241
  opts: AndroidDriverOpts;
242
242
 
243
- constructor(opts: InitialOpts = {} as InitialOpts, shouldValidateCaps = true) {
244
- super(opts, shouldValidateCaps);
245
-
246
- this.locatorStrategies = [
247
- 'xpath',
248
- 'id',
249
- 'class name',
250
- 'accessibility id',
251
- '-android uiautomator',
252
- ];
253
- this.desiredCapConstraints = _.cloneDeep(ANDROID_DRIVER_CONSTRAINTS);
254
- this.sessionChromedrivers = {};
255
- this.jwpProxyActive = false;
256
-
257
- this.curContext = this.defaultContextName();
258
- this.opts = opts as AndroidDriverOpts;
259
- this._cachedActivityArgs = {};
260
- }
261
-
262
- get bidiProxyUrl(): string | null {
263
- return this.opts.chromedriverForwardBiDi ? this._bidiProxyUrl : null;
264
- }
265
-
266
- get settingsApp(): SettingsApp {
267
- if (!this._settingsApp || this._settingsApp.adb !== this.adb) {
268
- this._settingsApp = new SettingsApp({adb: this.adb});
269
- }
270
- return this._settingsApp;
271
- }
272
-
273
- isEmulator(): boolean {
274
- const possibleNames = [this.opts?.udid, this.adb?.curDeviceId];
275
- return !!this.opts?.avd || possibleNames.some((x) => EMULATOR_PATTERN.test(String(x)));
276
- }
277
-
278
- get isChromeSession(): boolean {
279
- return _.includes(
280
- Object.keys(CHROME_BROWSER_PACKAGE_ACTIVITY),
281
- (this.opts.browserName || '').toLowerCase(),
282
- );
283
- }
284
-
285
- override validateDesiredCaps(caps: any): caps is AndroidDriverCaps {
286
- if (!super.validateDesiredCaps(caps)) {
287
- return false;
288
- }
289
-
290
- if (caps.browserName) {
291
- if (caps.app) {
292
- // warn if the capabilities have both `app` and `browser, although this is common with selenium grid
293
- this.log.warn(
294
- `The desired capabilities should generally not include both an 'app' and a 'browserName'`,
295
- );
296
- }
297
- if (caps.appPackage) {
298
- throw this.log.errorWithException(
299
- `The desired should not include both of an 'appPackage' and a 'browserName'`,
300
- );
301
- }
302
- }
303
-
304
- if (caps.uninstallOtherPackages) {
305
- try {
306
- parseArray(caps.uninstallOtherPackages);
307
- } catch (e) {
308
- throw this.log.errorWithException(
309
- `Could not parse "uninstallOtherPackages" capability: ${(e as Error).message}`,
310
- );
311
- }
312
- }
313
-
314
- return true;
315
- }
316
-
317
- override async deleteSession(sessionId?: string | null) {
318
- await removeAllSessionWebSocketHandlers.bind(this)();
319
-
320
- try {
321
- this.adb?.logcat?.removeAllListeners();
322
- await this.adb?.stopLogcat();
323
- } catch (e) {
324
- this.log.warn(`Cannot stop the logcat process. Original error: ${e.message}`);
325
- }
326
-
327
- if (this._bidiServerLogListener) {
328
- this.log.unwrap().off('log', this._bidiServerLogListener);
329
- }
330
-
331
- await super.deleteSession(sessionId);
332
- }
333
-
334
243
  getContexts = getContexts;
335
244
  getCurrentContext = getCurrentContext;
336
245
  defaultContextName = defaultContextName;
@@ -542,6 +451,97 @@ class AndroidDriver
542
451
  reset = reset;
543
452
  closeApp = closeApp;
544
453
  launchApp = launchApp;
454
+
455
+ constructor(opts: InitialOpts = {} as InitialOpts, shouldValidateCaps = true) {
456
+ super(opts, shouldValidateCaps);
457
+
458
+ this.locatorStrategies = [
459
+ 'xpath',
460
+ 'id',
461
+ 'class name',
462
+ 'accessibility id',
463
+ '-android uiautomator',
464
+ ];
465
+ this.desiredCapConstraints = _.cloneDeep(ANDROID_DRIVER_CONSTRAINTS);
466
+ this.sessionChromedrivers = {};
467
+ this.jwpProxyActive = false;
468
+
469
+ this.curContext = this.defaultContextName();
470
+ this.opts = opts as AndroidDriverOpts;
471
+ this._cachedActivityArgs = {};
472
+ }
473
+
474
+ get bidiProxyUrl(): string | null {
475
+ return this.opts.chromedriverForwardBiDi ? this._bidiProxyUrl : null;
476
+ }
477
+
478
+ get settingsApp(): SettingsApp {
479
+ if (!this._settingsApp || this._settingsApp.adb !== this.adb) {
480
+ this._settingsApp = new SettingsApp({adb: this.adb});
481
+ }
482
+ return this._settingsApp;
483
+ }
484
+
485
+ get isChromeSession(): boolean {
486
+ return _.includes(
487
+ Object.keys(CHROME_BROWSER_PACKAGE_ACTIVITY),
488
+ (this.opts.browserName || '').toLowerCase(),
489
+ );
490
+ }
491
+
492
+ isEmulator(): boolean {
493
+ const possibleNames = [this.opts?.udid, this.adb?.curDeviceId];
494
+ return !!this.opts?.avd || possibleNames.some((x) => EMULATOR_PATTERN.test(String(x)));
495
+ }
496
+
497
+ override validateDesiredCaps(caps: any): caps is AndroidDriverCaps {
498
+ if (!super.validateDesiredCaps(caps)) {
499
+ return false;
500
+ }
501
+
502
+ if (caps.browserName) {
503
+ if (caps.app) {
504
+ // warn if the capabilities have both `app` and `browser, although this is common with selenium grid
505
+ this.log.warn(
506
+ `The desired capabilities should generally not include both an 'app' and a 'browserName'`,
507
+ );
508
+ }
509
+ if (caps.appPackage) {
510
+ throw this.log.errorWithException(
511
+ `The desired should not include both of an 'appPackage' and a 'browserName'`,
512
+ );
513
+ }
514
+ }
515
+
516
+ if (caps.uninstallOtherPackages) {
517
+ try {
518
+ parseArray(caps.uninstallOtherPackages);
519
+ } catch (e) {
520
+ throw this.log.errorWithException(
521
+ `Could not parse "uninstallOtherPackages" capability: ${(e as Error).message}`,
522
+ );
523
+ }
524
+ }
525
+
526
+ return true;
527
+ }
528
+
529
+ override async deleteSession(sessionId?: string | null) {
530
+ await removeAllSessionWebSocketHandlers.bind(this)();
531
+
532
+ try {
533
+ this.adb?.logcat?.removeAllListeners();
534
+ await this.adb?.stopLogcat();
535
+ } catch (e) {
536
+ this.log.warn(`Cannot stop the logcat process. Original error: ${e.message}`);
537
+ }
538
+
539
+ if (this._bidiServerLogListener) {
540
+ this.log.unwrap().off('log', this._bidiServerLogListener);
541
+ }
542
+
543
+ await super.deleteSession(sessionId);
544
+ }
545
545
  }
546
546
 
547
547
  export {AndroidDriver};
package/lib/utils.ts CHANGED
@@ -11,6 +11,13 @@ export const GET_SERVER_LOGS_FEATURE = 'get_server_logs';
11
11
  export const SET_STYLUS_HANDWRITING_FEATURE = 'set_stylus_handwriting';
12
12
  const COLOR_CODE_PATTERN = /\u001b\[(\d+(;\d+)*)?m/g; // eslint-disable-line no-control-regex
13
13
 
14
+ interface LogEntryWithPrefix {
15
+ message: string;
16
+ prefix?: string;
17
+ timestamp?: number;
18
+ level?: string;
19
+ }
20
+
14
21
  /**
15
22
  * Assert the presence of particular keys in the given object
16
23
  *
@@ -65,13 +72,6 @@ export async function removeAllSessionWebSocketHandlers(this: AndroidDriver): Pr
65
72
  }
66
73
  }
67
74
 
68
- interface LogEntryWithPrefix {
69
- message: string;
70
- prefix?: string;
71
- timestamp?: number;
72
- level?: string;
73
- }
74
-
75
75
  /**
76
76
  *
77
77
  * @param x
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-android-driver",
3
- "version": "13.1.2",
3
+ "version": "13.2.0",
4
4
  "description": "Android UiAutomator and Chrome support for Appium",
5
5
  "keywords": [
6
6
  "appium",
@@ -53,7 +53,6 @@
53
53
  "appium-chromedriver": "^8.2.25",
54
54
  "asyncbox": "^6.1.0",
55
55
  "axios": "^1.x",
56
- "bluebird": "^3.4.7",
57
56
  "io.appium.settings": "^7.0.4",
58
57
  "lodash": "^4.17.4",
59
58
  "lru-cache": "^11.1.0",
@@ -70,7 +69,6 @@
70
69
  "@appium/types": "^1.0.0-rc.1",
71
70
  "@semantic-release/changelog": "^6.0.1",
72
71
  "@semantic-release/git": "^10.0.1",
73
- "@types/bluebird": "^3.5.38",
74
72
  "@types/lodash": "^4.14.194",
75
73
  "@types/mocha": "^10.0.1",
76
74
  "@types/node": "^25.0.0",