@appium/base-driver 8.4.0 → 8.5.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.
@@ -13,11 +13,12 @@ export function LogMixin (Base) {
13
13
  * @implements {ILogCommands}
14
14
  */
15
15
  class LogCommands extends Base {
16
- /**
17
- * XXX: dubious
18
- * @type {Record<string,LogType<Driver>>}
19
- */
20
- supportedLogTypes;
16
+
17
+ constructor (...args) {
18
+ super(...args);
19
+ /** @type {Record<string, LogType<Driver>>} */
20
+ this.supportedLogTypes = this.supportedLogTypes ?? {};
21
+ }
21
22
 
22
23
  async getLogTypes () {
23
24
  this.log.debug('Retrieving supported log types');
@@ -26,19 +27,13 @@ export function LogMixin (Base) {
26
27
 
27
28
  /**
28
29
  * @this {Driver}
30
+ * @param {string} logType
29
31
  */
30
32
  async getLog (logType) {
31
33
  this.log.debug(`Retrieving '${logType}' logs`);
32
34
 
33
35
  if (!(await this.getLogTypes()).includes(logType)) {
34
- const logsTypesWithDescriptions = _.reduce(
35
- this.supportedLogTypes,
36
- (acc, value, key) => {
37
- acc[key] = value.description;
38
- return acc;
39
- },
40
- {},
41
- );
36
+ const logsTypesWithDescriptions = _.mapValues(this.supportedLogTypes, 'description');
42
37
  throw new Error(
43
38
  `Unsupported log type '${logType}'. ` +
44
39
  `Supported types: ${JSON.stringify(logsTypesWithDescriptions)}`,
@@ -491,7 +491,7 @@ export {DriverCore};
491
491
  * @typedef {import('@appium/types').W3CCapabilities} W3CCapabilities
492
492
  * @typedef {import('@appium/types').Driver} Driver
493
493
  * @typedef {import('@appium/types').Core} Core
494
- * @typedef {import('@appium/types').DriverOpts} DriverOpts
494
+ * @typedef {import('@appium/types').ServerArgs} DriverOpts
495
495
  * @typedef {import('@appium/types').EventHistory} EventHistory
496
496
  * @typedef {import('@appium/types').AppiumLogger} AppiumLogger
497
497
  */
@@ -5,7 +5,10 @@ import log from './logger';
5
5
  import { node, util } from '@appium/support';
6
6
  import { errors } from '../protocol/errors';
7
7
 
8
- const MAX_SETTINGS_SIZE = 20 * 1024 * 1024; // 20 MB
8
+ /**
9
+ * Maximum size (in bytes) of a given driver's settings object (which is internal to {@linkcode DriverSettings}).
10
+ */
11
+ export const MAX_SETTINGS_SIZE = 20 * 1024 * 1024; // 20 MB
9
12
 
10
13
  /**
11
14
  * @template {Record<string,unknown>} T
@@ -21,19 +24,18 @@ class DeviceSettings {
21
24
 
22
25
  /**
23
26
  * @protected
24
- * @type {import('@appium/types').SettingsUpdateListener<T>|undefined}
27
+ * @type {import('@appium/types').SettingsUpdateListener<T>}
25
28
  */
26
29
  _onSettingsUpdate;
27
30
 
28
31
  /**
29
- * `onSettingsUpdate` is _required_ if settings will ever be updated; otherwise
30
- * an error will occur at runtime.
32
+ * Creates a _shallow copy_ of the `defaultSettings` parameter!
31
33
  * @param {T} [defaultSettings]
32
34
  * @param {import('@appium/types').SettingsUpdateListener<T>} [onSettingsUpdate]
33
35
  */
34
36
  constructor (defaultSettings, onSettingsUpdate) {
35
37
  this._settings = /** @type {T} */({...(defaultSettings ?? {})});
36
- this._onSettingsUpdate = onSettingsUpdate;
38
+ this._onSettingsUpdate = onSettingsUpdate ?? (async () => {});
37
39
  }
38
40
 
39
41
  /**
@@ -51,12 +53,6 @@ class DeviceSettings {
51
53
  `object size exceeds the allowed limit of ${util.toReadableSizeString(MAX_SETTINGS_SIZE)}`);
52
54
  }
53
55
 
54
- if (!_.isFunction(this._onSettingsUpdate)) {
55
- log.errorAndThrow(`Unable to update settings; ` +
56
- `onSettingsUpdate method not found on '${this.constructor.name}'`);
57
- return;
58
- }
59
-
60
56
  const props = /** @type {(keyof T & string)[]} */(_.keys(newSettings));
61
57
  for (const prop of props) {
62
58
  if (!_.isUndefined(this._settings[prop])) {
@@ -65,7 +61,6 @@ class DeviceSettings {
65
61
  continue;
66
62
  }
67
63
  }
68
- // update setting only when there is updateSettings defined.
69
64
  await this._onSettingsUpdate(prop, newSettings[prop], this._settings[prop]);
70
65
  this._settings[prop] = newSettings[prop];
71
66
  }
@@ -262,7 +262,7 @@ async function configureApp (app, options = {}) {
262
262
  }
263
263
  logger.info(`The application at '${cachedPath}' does not exist anymore ` +
264
264
  `or its integrity has been damaged. Deleting it from the internal cache`);
265
- APPLICATIONS_CACHE.del(app);
265
+ APPLICATIONS_CACHE.delete(app);
266
266
  }
267
267
 
268
268
  let fileName = null;
@@ -347,7 +347,7 @@ async function configureApp (app, options = {}) {
347
347
  }
348
348
  logger.info(`The application at '${fullPath}' does not exist anymore ` +
349
349
  `or its integrity has been damaged. Deleting it from the cache`);
350
- APPLICATIONS_CACHE.del(app);
350
+ APPLICATIONS_CACHE.delete(app);
351
351
  }
352
352
  const tmpRoot = await tempDir.openDir();
353
353
  try {
@@ -78,14 +78,14 @@ function cacheResponse (key, req, res) {
78
78
  }
79
79
  if (writeError) {
80
80
  log.info(`Could not cache the response identified by '${key}': ${writeError.message}`);
81
- IDEMPOTENT_RESPONSES.del(key);
81
+ IDEMPOTENT_RESPONSES.delete(key);
82
82
  return responseStateListener.emit('ready', null);
83
83
  }
84
84
  if (!isResponseFullySent) {
85
85
  log.info(`Could not cache the response identified by '${key}', ` +
86
86
  `because it has not been completed`);
87
87
  log.info('Does the client terminate connections too early?');
88
- IDEMPOTENT_RESPONSES.del(key);
88
+ IDEMPOTENT_RESPONSES.delete(key);
89
89
  return responseStateListener.emit('ready', null);
90
90
  }
91
91
 
@@ -125,7 +125,7 @@ async function handleIdempotency (req, res, next) {
125
125
 
126
126
  const rerouteCachedResponse = async (cachedResPath) => {
127
127
  if (!await fs.exists(cachedResPath)) {
128
- IDEMPOTENT_RESPONSES.del(key);
128
+ IDEMPOTENT_RESPONSES.delete(key);
129
129
  log.warn(`Could not read the cached response identified by key '${key}'`);
130
130
  log.warn('The temporary storage is not accessible anymore');
131
131
  return next();
@@ -1,5 +1,6 @@
1
1
  import _ from 'lodash';
2
- import url from 'url';
2
+ import { URL } from 'url';
3
+ import B from 'bluebird';
3
4
 
4
5
  const DEFAULT_WS_PATHNAME_PREFIX = '/ws';
5
6
 
@@ -18,29 +19,28 @@ const DEFAULT_WS_PATHNAME_PREFIX = '/ws';
18
19
  * on how to configure the handler properly.
19
20
  */
20
21
  async function addWebSocketHandler (handlerPathname, handlerServer) { // eslint-disable-line require-await
21
- let isUpgradeListenerAssigned = true;
22
22
  if (_.isUndefined(this.webSocketsMapping)) {
23
23
  this.webSocketsMapping = {};
24
- isUpgradeListenerAssigned = false;
24
+ // https://github.com/websockets/ws/pull/885
25
+ this.on('upgrade', (request, socket, head) => {
26
+ let currentPathname;
27
+ try {
28
+ currentPathname = (new URL(request.url)).pathname;
29
+ } catch (ign) {
30
+ currentPathname = request.url;
31
+ }
32
+ for (const [pathname, wsServer] of _.toPairs(this.webSocketsMapping)) {
33
+ if (currentPathname === pathname) {
34
+ wsServer.handleUpgrade(request, socket, head, (ws) => {
35
+ wsServer.emit('connection', ws, request);
36
+ });
37
+ return;
38
+ }
39
+ }
40
+ socket.destroy();
41
+ });
25
42
  }
26
43
  this.webSocketsMapping[handlerPathname] = handlerServer;
27
- if (isUpgradeListenerAssigned) {
28
- return;
29
- }
30
-
31
- // https://github.com/websockets/ws/pull/885
32
- this.on('upgrade', (request, socket, head) => {
33
- const currentPathname = url.parse(request.url).pathname;
34
- for (const [pathname, wsServer] of _.toPairs(this.webSocketsMapping)) {
35
- if (currentPathname === pathname) {
36
- wsServer.handleUpgrade(request, socket, head, (ws) => {
37
- wsServer.emit('connection', ws, request);
38
- });
39
- return;
40
- }
41
- }
42
- socket.destroy();
43
- });
44
44
  }
45
45
 
46
46
  /**
@@ -59,13 +59,12 @@ async function getWebSocketHandlers (keysFilter = null) { // eslint-disable-line
59
59
  return {};
60
60
  }
61
61
 
62
- let result = {};
63
- for (const [pathname, wsServer] of _.toPairs(this.webSocketsMapping)) {
62
+ return _.toPairs(this.webSocketsMapping).reduce((acc, [pathname, wsServer]) => {
64
63
  if (!_.isString(keysFilter) || pathname.includes(keysFilter)) {
65
- result[pathname] = wsServer;
64
+ acc[pathname] = wsServer;
66
65
  }
67
- }
68
- return result;
66
+ return acc;
67
+ }, {});
69
68
  }
70
69
 
71
70
  /**
@@ -79,12 +78,16 @@ async function getWebSocketHandlers (keysFilter = null) { // eslint-disable-line
79
78
  * @returns {boolean} true if the handlerPathname was found and deleted
80
79
  */
81
80
  async function removeWebSocketHandler (handlerPathname) { // eslint-disable-line require-await
82
- if (!this.webSocketsMapping || !this.webSocketsMapping[handlerPathname]) {
81
+ const wsServer = this.webSocketsMapping?.[handlerPathname];
82
+ if (!wsServer) {
83
83
  return false;
84
84
  }
85
85
 
86
86
  try {
87
- this.webSocketsMapping[handlerPathname].close();
87
+ wsServer.close();
88
+ for (const client of (wsServer.clients || [])) {
89
+ client.terminate();
90
+ }
88
91
  return true;
89
92
  } catch (ign) {
90
93
  // ignore
@@ -106,11 +109,11 @@ async function removeAllWebSocketHandlers () {
106
109
  return false;
107
110
  }
108
111
 
109
- let result = false;
110
- for (const pathname of _.keys(this.webSocketsMapping)) {
111
- result = result || await this.removeWebSocketHandler(pathname);
112
- }
113
- return result;
112
+ return _.some(
113
+ await B.all(
114
+ _.keys(this.webSocketsMapping).map((pathname) => this.removeWebSocketHandler(pathname))
115
+ )
116
+ );
114
117
  }
115
118
 
116
119
  export {
@@ -3,7 +3,8 @@ import { logger, util } from '@appium/support';
3
3
  import axios from 'axios';
4
4
  import { getSummaryByCode } from '../jsonwp-status/status';
5
5
  import {
6
- errors, isErrorType, errorFromMJSONWPStatusCode, errorFromW3CJsonCode
6
+ errors, isErrorType, errorFromMJSONWPStatusCode, errorFromW3CJsonCode,
7
+ getResponseForW3CError,
7
8
  } from '../protocol/errors';
8
9
  import { routeToCommandName } from '../protocol';
9
10
  import { MAX_LOG_BODY_LENGTH, DEFAULT_BASE_PATH, PROTOCOLS } from '../constants';
@@ -312,15 +313,34 @@ class JWProxy {
312
313
  }
313
314
 
314
315
  async proxyReqRes (req, res) {
315
- const [response, resBodyObj] = await this.proxyCommand(req.originalUrl, req.method, req.body);
316
+ // ! this method must not throw any exceptions
317
+ // ! make sure to call res.send before return
318
+ let statusCode;
319
+ let resBodyObj;
320
+ try {
321
+ let response;
322
+ [response, resBodyObj] = await this.proxyCommand(req.originalUrl, req.method, req.body);
323
+ res.headers = response.headers;
324
+ statusCode = response.statusCode;
325
+ } catch (err) {
326
+ [statusCode, resBodyObj] = getResponseForW3CError(
327
+ isErrorType(err, errors.ProxyRequestError) ? err.getActualError() : err
328
+ );
329
+ }
330
+ res.set('content-type', 'application/json; charset=utf-8');
331
+ if (!_.isPlainObject(resBodyObj)) {
332
+ const error = new errors.UnknownError(
333
+ `The downstream server response with the status code ${statusCode} is not a valid JSON object: ` +
334
+ _.truncate(`${resBodyObj}`, {length: 300})
335
+ );
336
+ [statusCode, resBodyObj] = getResponseForW3CError(error);
337
+ }
316
338
 
317
- res.headers = response.headers;
318
- res.set('content-type', response.headers['content-type']);
319
339
  // if the proxied response contains a sessionId that the downstream
320
340
  // driver has generated, we don't want to return that to the client.
321
341
  // Instead, return the id from the request or from current session
322
- const reqSessionId = this.getSessionIdFromUrl(req.originalUrl);
323
342
  if (_.has(resBodyObj, 'sessionId')) {
343
+ const reqSessionId = this.getSessionIdFromUrl(req.originalUrl);
324
344
  if (reqSessionId) {
325
345
  this.log.info(`Replacing sessionId ${resBodyObj.sessionId} with ${reqSessionId}`);
326
346
  resBodyObj.sessionId = reqSessionId;
@@ -330,7 +350,7 @@ class JWProxy {
330
350
  }
331
351
  }
332
352
  resBodyObj.value = formatResponseValue(resBodyObj.value);
333
- res.status(response.statusCode).send(JSON.stringify(formatStatus(resBodyObj)));
353
+ res.status(statusCode).send(JSON.stringify(formatStatus(resBodyObj)));
334
354
  }
335
355
  }
336
356
 
@@ -690,6 +690,9 @@ const METHOD_MAP = {
690
690
  '/session/:sessionId/window/fullscreen': {
691
691
  POST: {command: 'fullScreenWindow'}
692
692
  },
693
+ '/session/:sessionId/window/new': {
694
+ POST: {command: 'createNewWindow', payloadParams: {optional: ['type']}}
695
+ },
693
696
  '/session/:sessionId/element/:elementId/property/:name': {
694
697
  GET: {command: 'getProperty'}
695
698
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appium/base-driver",
3
- "version": "8.4.0",
3
+ "version": "8.5.0",
4
4
  "description": "Base driver class for Appium drivers",
5
5
  "keywords": [
6
6
  "automation",
@@ -37,18 +37,18 @@
37
37
  "!build/test/e2e/fixtures"
38
38
  ],
39
39
  "scripts": {
40
- "build": "npm run build:distfiles && npm run build:test",
40
+ "build": "run-s build:*",
41
41
  "build:distfiles": "babel lib --root-mode=upward --out-dir=build/lib",
42
42
  "build:test": "babel test --root-mode=upward --out-dir=build/test --copy-files",
43
43
  "dev": "run-p \"build:distfiles -- --watch\" \"build:test -- --watch\"",
44
44
  "fix": "npm run lint -- --fix",
45
45
  "lint": "eslint -c ../../.eslintrc --ignore-path ../../.eslintignore .",
46
46
  "test": "npm run test:unit",
47
- "test:e2e": "mocha --require ../../test/setup-babel.js --timeout 20s --slow 10s \"./test/e2e/**/*.spec.js\"",
48
- "test:unit": "mocha --require ../../test/setup-babel.js \"./test/unit/**/*.spec.js\""
47
+ "test:e2e": "mocha --timeout 20s --slow 10s \"./test/e2e/**/*.spec.js\"",
48
+ "test:unit": "mocha \"./test/unit/**/*.spec.js\""
49
49
  },
50
50
  "dependencies": {
51
- "@appium/support": "^2.57.0",
51
+ "@appium/support": "^2.57.2",
52
52
  "@babel/runtime": "7.17.9",
53
53
  "@colors/colors": "1.5.0",
54
54
  "async-lock": "1.3.1",
@@ -60,13 +60,15 @@
60
60
  "express": "4.17.3",
61
61
  "http-status-codes": "2.2.0",
62
62
  "lodash": "4.17.21",
63
- "lru-cache": "7.7.3",
63
+ "lru-cache": "7.8.1",
64
64
  "method-override": "3.0.0",
65
65
  "morgan": "1.10.0",
66
66
  "serve-favicon": "2.5.0",
67
67
  "source-map-support": "0.5.21",
68
- "validate.js": "0.13.1",
69
- "ws": "7.5.7"
68
+ "validate.js": "0.13.1"
69
+ },
70
+ "devDependencies": {
71
+ "ws": "8.5.0"
70
72
  },
71
73
  "engines": {
72
74
  "node": ">=12",
@@ -76,5 +78,5 @@
76
78
  "access": "public"
77
79
  },
78
80
  "types": "./build/lib/index.d.ts",
79
- "gitHead": "3ec3a0efa590e22e264ffbd23316ee5714a12081"
81
+ "gitHead": "8b7906f757f23b7abc09a0acf41a3daa6671c2ec"
80
82
  }
@@ -279,11 +279,10 @@ function baseDriverE2ETests (DriverClass, defaultCaps = {}) {
279
279
  it('should be able to get settings object', function () {
280
280
  d.settings.getSettings().ignoreUnimportantViews.should.be.false;
281
281
  });
282
- it('should throw error when updateSettings method is not defined', async function () {
283
- await d.settings.update({ignoreUnimportantViews: true}).should.eventually
284
- .be.rejectedWith('onSettingsUpdate');
282
+ it('should not reject when `updateSettings` method is not provided', async function () {
283
+ await d.settings.update({ignoreUnimportantViews: true}).should.not.be.rejected;
285
284
  });
286
- it('should throw error for invalid update object', async function () {
285
+ it('should reject for invalid update object', async function () {
287
286
  await d.settings.update('invalid json').should.eventually
288
287
  .be.rejectedWith('JSON');
289
288
  });