@appium/base-driver 10.1.2 → 10.2.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 (133) hide show
  1. package/build/lib/basedriver/capabilities.d.ts.map +1 -1
  2. package/build/lib/basedriver/capabilities.js +7 -7
  3. package/build/lib/basedriver/capabilities.js.map +1 -1
  4. package/build/lib/basedriver/commands/event.d.ts +1 -1
  5. package/build/lib/basedriver/commands/event.d.ts.map +1 -1
  6. package/build/lib/basedriver/commands/execute.d.ts +1 -1
  7. package/build/lib/basedriver/commands/execute.d.ts.map +1 -1
  8. package/build/lib/basedriver/commands/find.d.ts +1 -1
  9. package/build/lib/basedriver/commands/find.d.ts.map +1 -1
  10. package/build/lib/basedriver/commands/mixin.d.ts +1 -1
  11. package/build/lib/basedriver/commands/mixin.d.ts.map +1 -1
  12. package/build/lib/basedriver/commands/timeout.d.ts +1 -1
  13. package/build/lib/basedriver/commands/timeout.d.ts.map +1 -1
  14. package/build/lib/basedriver/device-settings.d.ts +14 -23
  15. package/build/lib/basedriver/device-settings.d.ts.map +1 -1
  16. package/build/lib/basedriver/device-settings.js +11 -26
  17. package/build/lib/basedriver/device-settings.js.map +1 -1
  18. package/build/lib/basedriver/helpers.d.ts +36 -57
  19. package/build/lib/basedriver/helpers.d.ts.map +1 -1
  20. package/build/lib/basedriver/helpers.js +160 -248
  21. package/build/lib/basedriver/helpers.js.map +1 -1
  22. package/build/lib/basedriver/logger.d.ts +1 -2
  23. package/build/lib/basedriver/logger.d.ts.map +1 -1
  24. package/build/lib/basedriver/logger.js +2 -2
  25. package/build/lib/basedriver/logger.js.map +1 -1
  26. package/build/lib/basedriver/validation.d.ts.map +1 -1
  27. package/build/lib/basedriver/validation.js +3 -3
  28. package/build/lib/basedriver/validation.js.map +1 -1
  29. package/build/lib/constants.d.ts +1 -1
  30. package/build/lib/constants.d.ts.map +1 -1
  31. package/build/lib/express/crash.d.ts +8 -2
  32. package/build/lib/express/crash.d.ts.map +1 -1
  33. package/build/lib/express/crash.js +6 -0
  34. package/build/lib/express/crash.js.map +1 -1
  35. package/build/lib/express/express-logging.d.ts +12 -2
  36. package/build/lib/express/express-logging.d.ts.map +1 -1
  37. package/build/lib/express/express-logging.js +34 -26
  38. package/build/lib/express/express-logging.js.map +1 -1
  39. package/build/lib/express/idempotency.d.ts +4 -10
  40. package/build/lib/express/idempotency.d.ts.map +1 -1
  41. package/build/lib/express/idempotency.js +71 -75
  42. package/build/lib/express/idempotency.js.map +1 -1
  43. package/build/lib/express/logger.d.ts +1 -2
  44. package/build/lib/express/logger.d.ts.map +1 -1
  45. package/build/lib/express/logger.js +2 -2
  46. package/build/lib/express/logger.js.map +1 -1
  47. package/build/lib/express/middleware.d.ts +37 -41
  48. package/build/lib/express/middleware.d.ts.map +1 -1
  49. package/build/lib/express/middleware.js +48 -60
  50. package/build/lib/express/middleware.js.map +1 -1
  51. package/build/lib/express/server.d.ts +57 -101
  52. package/build/lib/express/server.d.ts.map +1 -1
  53. package/build/lib/express/server.js +55 -133
  54. package/build/lib/express/server.js.map +1 -1
  55. package/build/lib/express/static.d.ts +10 -5
  56. package/build/lib/express/static.d.ts.map +1 -1
  57. package/build/lib/express/static.js +33 -43
  58. package/build/lib/express/static.js.map +1 -1
  59. package/build/lib/express/websocket.d.ts +22 -6
  60. package/build/lib/express/websocket.d.ts.map +1 -1
  61. package/build/lib/express/websocket.js +10 -15
  62. package/build/lib/express/websocket.js.map +1 -1
  63. package/build/lib/helpers/capabilities.d.ts +4 -16
  64. package/build/lib/helpers/capabilities.d.ts.map +1 -1
  65. package/build/lib/helpers/capabilities.js +36 -48
  66. package/build/lib/helpers/capabilities.js.map +1 -1
  67. package/build/lib/jsonwp-proxy/proxy.d.ts +4 -2
  68. package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
  69. package/build/lib/jsonwp-proxy/proxy.js +9 -4
  70. package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
  71. package/build/lib/jsonwp-status/status.d.ts +113 -158
  72. package/build/lib/jsonwp-status/status.d.ts.map +1 -1
  73. package/build/lib/jsonwp-status/status.js +10 -14
  74. package/build/lib/jsonwp-status/status.js.map +1 -1
  75. package/build/lib/protocol/bidi-commands.d.ts +31 -36
  76. package/build/lib/protocol/bidi-commands.d.ts.map +1 -1
  77. package/build/lib/protocol/bidi-commands.js +5 -5
  78. package/build/lib/protocol/bidi-commands.js.map +1 -1
  79. package/build/lib/protocol/errors.d.ts.map +1 -1
  80. package/build/lib/protocol/errors.js.map +1 -1
  81. package/build/lib/protocol/helpers.d.ts +7 -11
  82. package/build/lib/protocol/helpers.d.ts.map +1 -1
  83. package/build/lib/protocol/helpers.js +5 -9
  84. package/build/lib/protocol/helpers.js.map +1 -1
  85. package/build/lib/protocol/index.d.ts +4 -21
  86. package/build/lib/protocol/index.d.ts.map +1 -1
  87. package/build/lib/protocol/index.js.map +1 -1
  88. package/build/lib/protocol/protocol.d.ts +15 -1
  89. package/build/lib/protocol/protocol.d.ts.map +1 -1
  90. package/build/lib/protocol/protocol.js +50 -20
  91. package/build/lib/protocol/protocol.js.map +1 -1
  92. package/build/lib/protocol/routes.d.ts +8 -15
  93. package/build/lib/protocol/routes.d.ts.map +1 -1
  94. package/build/lib/protocol/routes.js +18 -33
  95. package/build/lib/protocol/routes.js.map +1 -1
  96. package/lib/basedriver/capabilities.ts +2 -2
  97. package/lib/basedriver/commands/event.ts +2 -2
  98. package/lib/basedriver/commands/execute.ts +2 -2
  99. package/lib/basedriver/commands/find.ts +2 -2
  100. package/lib/basedriver/commands/mixin.ts +1 -1
  101. package/lib/basedriver/commands/timeout.ts +2 -2
  102. package/lib/basedriver/{device-settings.js → device-settings.ts} +24 -35
  103. package/lib/basedriver/{helpers.js → helpers.ts} +215 -270
  104. package/lib/basedriver/logger.ts +3 -0
  105. package/lib/basedriver/validation.ts +2 -2
  106. package/lib/constants.ts +1 -1
  107. package/lib/express/crash.ts +15 -0
  108. package/lib/express/express-logging.ts +84 -0
  109. package/lib/express/{idempotency.js → idempotency.ts} +106 -90
  110. package/lib/express/logger.ts +3 -0
  111. package/lib/express/middleware.ts +187 -0
  112. package/lib/express/{server.js → server.ts} +177 -170
  113. package/lib/express/static.ts +77 -0
  114. package/lib/express/websocket.ts +81 -0
  115. package/lib/helpers/capabilities.ts +83 -0
  116. package/lib/jsonwp-proxy/proxy.js +7 -2
  117. package/lib/jsonwp-status/{status.js → status.ts} +12 -15
  118. package/lib/protocol/{bidi-commands.js → bidi-commands.ts} +7 -5
  119. package/lib/protocol/errors.ts +4 -4
  120. package/lib/protocol/{helpers.js → helpers.ts} +8 -11
  121. package/lib/protocol/protocol.ts +57 -26
  122. package/lib/protocol/{routes.js → routes.ts} +29 -40
  123. package/package.json +15 -15
  124. package/tsconfig.json +3 -1
  125. package/lib/basedriver/logger.js +0 -4
  126. package/lib/express/crash.js +0 -11
  127. package/lib/express/express-logging.js +0 -60
  128. package/lib/express/logger.js +0 -4
  129. package/lib/express/middleware.js +0 -171
  130. package/lib/express/static.js +0 -76
  131. package/lib/express/websocket.js +0 -79
  132. package/lib/helpers/capabilities.js +0 -93
  133. /package/lib/protocol/{index.js → index.ts} +0 -0
@@ -1,11 +1,13 @@
1
1
  import _ from 'lodash';
2
- import path from 'path';
2
+ import path from 'node:path';
3
3
  import express from 'express';
4
- import http from 'http';
4
+ import type {Express, RequestHandler} from 'express';
5
+ import http from 'node:http';
6
+ import type {Server as HttpServer} from 'node:http';
5
7
  import favicon from 'serve-favicon';
6
8
  import bodyParser from 'body-parser';
7
9
  import methodOverride from 'method-override';
8
- import log from './logger';
10
+ import {log} from './logger';
9
11
  import {startLogFormatter, endLogFormatter} from './express-logging';
10
12
  import {
11
13
  allowCrossDomain,
@@ -18,7 +20,13 @@ import {
18
20
  catch404Handler,
19
21
  handleLogContext,
20
22
  } from './middleware';
21
- import {guineaPig, guineaPigScrollable, guineaPigAppBanner, welcome, STATIC_DIR} from './static';
23
+ import {
24
+ guineaPig,
25
+ guineaPigScrollable,
26
+ guineaPigAppBanner,
27
+ welcome,
28
+ STATIC_DIR,
29
+ } from './static';
22
30
  import {produceError, produceCrash} from './crash';
23
31
  import {
24
32
  addWebSocketHandler,
@@ -29,20 +37,81 @@ import {
29
37
  import B from 'bluebird';
30
38
  import {DEFAULT_BASE_PATH} from '../constants';
31
39
  import {fs, timing} from '@appium/support';
40
+ import type {
41
+ AppiumServer,
42
+ ServerArgs,
43
+ UpdateServerCallback,
44
+ MethodMap,
45
+ ExternalDriver,
46
+ StringRecord,
47
+ } from '@appium/types';
32
48
 
33
49
  const KEEP_ALIVE_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
34
50
 
51
+ /** Options for {@linkcode RouteConfiguringFunction} */
52
+ export interface RouteConfiguringFunctionOpts {
53
+ basePath?: string;
54
+ extraMethodMap?: MethodMap<ExternalDriver>;
55
+ }
56
+
57
+ /** A function which configures routes */
58
+ export type RouteConfiguringFunction = (
59
+ app: Express,
60
+ opts?: RouteConfiguringFunctionOpts
61
+ ) => void;
62
+
63
+ /** Options for {@linkcode server} */
64
+ export interface ServerOpts {
65
+ routeConfiguringFunction: RouteConfiguringFunction;
66
+ port: number;
67
+ cliArgs?: Partial<ServerArgs>;
68
+ hostname?: string;
69
+ allowCors?: boolean;
70
+ basePath?: string;
71
+ extraMethodMap?: MethodMap<ExternalDriver>;
72
+ serverUpdaters?: UpdateServerCallback[];
73
+ keepAliveTimeout?: number;
74
+ requestTimeout?: number;
75
+ }
76
+
77
+ /** Options for {@linkcode configureServer} */
78
+ export interface ConfigureServerOpts {
79
+ app: Express;
80
+ addRoutes: RouteConfiguringFunction;
81
+ allowCors?: boolean;
82
+ basePath?: string;
83
+ extraMethodMap?: MethodMap<ExternalDriver>;
84
+ webSocketsMapping?: StringRecord;
85
+ useLegacyUpgradeHandler?: boolean;
86
+ }
87
+
88
+ /** Options for {@linkcode configureHttp} */
89
+ export interface ConfigureHttpOpts {
90
+ httpServer: HttpServer;
91
+ reject: (error?: unknown) => void;
92
+ keepAliveTimeout: number;
93
+ gracefulShutdownTimeout?: number;
94
+ }
95
+
96
+ /** Options for {@linkcode startServer} */
97
+ export interface StartServerOpts {
98
+ httpServer: HttpServer;
99
+ port: number;
100
+ hostname?: string;
101
+ keepAliveTimeout: number;
102
+ requestTimeout?: number;
103
+ }
104
+
35
105
  /**
36
- *
37
- * @param {ServerOpts} opts
38
- * @returns {Promise<AppiumServer>}
106
+ * @param opts - Server options
107
+ * @returns Promise resolving to the Appium server instance
39
108
  */
40
- export async function server(opts) {
109
+ export async function server(opts: ServerOpts): Promise<AppiumServer> {
41
110
  const {
42
111
  routeConfiguringFunction,
43
112
  port,
44
113
  hostname,
45
- cliArgs = /** @type {import('@appium/types').ServerArgs} */ ({}),
114
+ cliArgs = {},
46
115
  allowCors = true,
47
116
  basePath = DEFAULT_BASE_PATH,
48
117
  extraMethodMap = {},
@@ -54,7 +123,7 @@ export async function server(opts) {
54
123
  const app = express();
55
124
  const httpServer = await createServer(app, cliArgs);
56
125
 
57
- return await new B(async (resolve, reject) => {
126
+ return await new B<AppiumServer>(async (resolve, reject) => {
58
127
  // we put an async function as the promise constructor because we want some things to happen in
59
128
  // serial (application of plugin updates, for example). But we still need to use a promise here
60
129
  // because some elements of server start failure only happen in httpServer listeners. So the
@@ -103,8 +172,9 @@ export async function server(opts) {
103
172
  }
104
173
 
105
174
  /**
106
- * Sets up some Express middleware and stuff
107
- * @param {ConfigureServerOpts} opts
175
+ * Sets up Express middleware and routes.
176
+ *
177
+ * @param opts - Configuration options
108
178
  */
109
179
  export function configureServer({
110
180
  app,
@@ -114,7 +184,7 @@ export function configureServer({
114
184
  extraMethodMap = {},
115
185
  webSocketsMapping = {},
116
186
  useLegacyUpgradeHandler = true,
117
- }) {
187
+ }: ConfigureServerOpts): void {
118
188
  basePath = normalizeBasePath(basePath);
119
189
 
120
190
  app.use(endLogFormatter);
@@ -122,14 +192,13 @@ export function configureServer({
122
192
 
123
193
  // set up static assets
124
194
  app.use(favicon(path.resolve(STATIC_DIR, 'favicon.ico')));
125
- // eslint-disable-next-line import/no-named-as-default-member
126
195
  app.use(express.static(STATIC_DIR));
127
196
 
128
197
  // crash routes, for testing
129
198
  app.use(`${basePath}/produce_error`, produceError);
130
199
  app.use(`${basePath}/crash`, produceCrash);
131
200
 
132
- // Only use legacy Express middleware for WebSocket upgrades if shouldUpgradeCallback is not available
201
+ // Only use legacy Express middleware for WebSocket upgrades if shouldUpgradeCallback is not available.
133
202
  // When shouldUpgradeCallback is available, upgrades are handled directly on the HTTP server
134
203
  // to avoid Express middleware timeout issues with long-lived connections
135
204
  if (useLegacyUpgradeHandler) {
@@ -162,17 +231,17 @@ export function configureServer({
162
231
  }
163
232
 
164
233
  /**
165
- * Normalize base path string
166
- * @param {string} basePath
167
- * @returns {string}
234
+ * Normalize base path string (leading slash, no trailing slash).
235
+ *
236
+ * @param basePath - Raw base path
237
+ * @returns Normalized base path
168
238
  */
169
- export function normalizeBasePath(basePath) {
239
+ export function normalizeBasePath(basePath: string): string {
170
240
  if (!_.isString(basePath)) {
171
241
  throw new Error(`Invalid path prefix ${basePath}`);
172
242
  }
173
243
 
174
- // ensure the path prefix does not end in '/', since our method map
175
- // starts all paths with '/'
244
+ // ensure the path prefix does not end in '/', since our method map starts all paths with '/'
176
245
  basePath = basePath.replace(/\/$/, '');
177
246
 
178
247
  // likewise, ensure the path prefix does always START with /, unless the path
@@ -184,64 +253,78 @@ export function normalizeBasePath(basePath) {
184
253
  return basePath;
185
254
  }
186
255
 
187
- /**
188
- *
189
- * @param {import('express').Express} app
190
- * @param {Partial<import('@appium/types').ServerArgs>} [cliArgs]
191
- * @returns {Promise<http.Server>}
192
- */
193
- async function createServer (app, cliArgs) {
256
+ async function createServer(
257
+ app: Express,
258
+ cliArgs?: Partial<ServerArgs>
259
+ ): Promise<HttpServer> {
194
260
  const {sslCertificatePath, sslKeyPath} = cliArgs ?? {};
195
261
  if (!sslCertificatePath && !sslKeyPath) {
196
262
  return http.createServer(app);
197
263
  }
198
264
  if (!sslCertificatePath || !sslKeyPath) {
199
- throw new Error(`Both certificate path and key path must be provided to enable TLS`);
265
+ throw new Error(
266
+ `Both certificate path and key path must be provided to enable TLS`
267
+ );
200
268
  }
201
269
 
202
270
  const certKey = [sslCertificatePath, sslKeyPath];
203
271
  const zipped = _.zip(
204
272
  await B.all(certKey.map((p) => fs.exists(p))),
205
273
  ['certificate', 'key'],
206
- certKey,
207
- );
274
+ certKey
275
+ ) as [boolean, string, string][];
208
276
  for (const [exists, desc, p] of zipped) {
209
277
  if (!exists) {
210
- throw new Error(`The provided SSL ${desc} at '${p}' does not exist or is not accessible`);
278
+ throw new Error(
279
+ `The provided SSL ${desc} at '${p}' does not exist or is not accessible`
280
+ );
211
281
  }
212
282
  }
213
- const [cert, key] = await B.all(certKey.map((p) => fs.readFile(p, 'utf8')));
283
+ const [cert, key] = await B.all(
284
+ certKey.map((p) => fs.readFile(p, 'utf8'))
285
+ ) as [string, string];
214
286
  log.debug('Enabling TLS/SPDY on the server using the provided certificate');
215
287
 
216
- return require('spdy').createServer({
217
- cert,
218
- key,
219
- spdy: {
220
- plain: false,
221
- ssl: true,
222
- }
223
- }, app);
288
+ const spdy = require('spdy') as {
289
+ createServer: (
290
+ options: {cert: string; key: string; spdy: {plain: boolean; ssl: boolean}},
291
+ requestListener: RequestHandler
292
+ ) => HttpServer;
293
+ };
294
+ return spdy.createServer(
295
+ {
296
+ cert,
297
+ key,
298
+ spdy: {
299
+ plain: false,
300
+ ssl: true,
301
+ },
302
+ },
303
+ app
304
+ );
224
305
  }
225
306
 
226
307
  /**
227
- * Monkeypatches the `http.Server` instance and returns a {@linkcode AppiumServer}.
228
- * This function _mutates_ the `httpServer` parameter.
229
- * @param {ConfigureHttpOpts} opts
230
- * @returns {AppiumServer}
308
+ * Attaches Appium-specific behavior to the HTTP server and returns it as {@linkcode AppiumServer}.
309
+ * Mutates the `httpServer` parameter.
310
+ *
311
+ * @param opts - Configuration options
312
+ * @returns The same server instance typed as AppiumServer
231
313
  */
232
- function configureHttp({httpServer, reject, keepAliveTimeout, gracefulShutdownTimeout}) {
233
- /**
234
- * @type {AppiumServer}
235
- */
236
- const appiumServer = /** @type {any} */ (httpServer);
314
+ function configureHttp({
315
+ httpServer,
316
+ reject,
317
+ keepAliveTimeout,
318
+ gracefulShutdownTimeout,
319
+ }: ConfigureHttpOpts): AppiumServer {
320
+ const appiumServer = httpServer as unknown as AppiumServer;
237
321
  appiumServer.webSocketsMapping = {};
238
322
  appiumServer.addWebSocketHandler = addWebSocketHandler;
239
323
  appiumServer.removeWebSocketHandler = removeWebSocketHandler;
240
324
  appiumServer.removeAllWebSocketHandlers = removeAllWebSocketHandlers;
241
325
  appiumServer.getWebSocketHandlers = getWebSocketHandlers;
242
326
  appiumServer.isSecure = function isSecure() {
243
- // eslint-disable-next-line dot-notation
244
- return Boolean(this['_spdyState']?.secure);
327
+ return Boolean((this as unknown as {_spdyState?: {secure?: boolean}})._spdyState?.secure);
245
328
  };
246
329
 
247
330
  // This avoids Express middleware timeout issues with long-lived WebSocket connections
@@ -249,8 +332,8 @@ function configureHttp({httpServer, reject, keepAliveTimeout, gracefulShutdownTi
249
332
  // See: https://github.com/nodejs/node/pull/59824
250
333
  if (hasShouldUpgradeCallback(httpServer)) {
251
334
  // shouldUpgradeCallback only returns a boolean to indicate if the upgrade should proceed
252
- // eslint-disable-next-line dot-notation
253
- appiumServer['shouldUpgradeCallback'] = (req) => _.toLower(req.headers?.upgrade) === 'websocket';
335
+ (appiumServer as unknown as {shouldUpgradeCallback?: (req: http.IncomingMessage) => boolean}).shouldUpgradeCallback = (req) =>
336
+ _.toLower(req.headers?.upgrade) === 'websocket';
254
337
  appiumServer.on('upgrade', (req, socket, head) => {
255
338
  if (!tryHandleWebSocketUpgrade(req, socket, head, appiumServer.webSocketsMapping)) {
256
339
  socket.destroy();
@@ -262,73 +345,69 @@ function configureHttp({httpServer, reject, keepAliveTimeout, gracefulShutdownTi
262
345
  // all connections are closed and the `close` event is emitted
263
346
  const originalClose = appiumServer.close.bind(appiumServer);
264
347
  appiumServer.close = async () =>
265
- await new B((_resolve, _reject) => {
348
+ await new B<void>((_resolve, _reject) => {
266
349
  log.info('Closing Appium HTTP server');
267
350
  const timer = new timing.Timer().start();
268
351
  const onTimeout = setTimeout(() => {
269
- if (gracefulShutdownTimeout > 0) {
352
+ if ((gracefulShutdownTimeout ?? 0) > 0) {
270
353
  log.info(
271
354
  `Not all active connections have been closed within ${gracefulShutdownTimeout}ms. ` +
272
- `This timeout might be customized by the --shutdown-timeout command line ` +
273
- `argument. Closing the server anyway.`
355
+ `This timeout might be customized by the --shutdown-timeout command line ` +
356
+ `argument. Closing the server anyway.`
274
357
  );
275
358
  }
276
359
  process.exit(process.exitCode ?? 0);
277
- }, gracefulShutdownTimeout);
278
- httpServer.once('close', () => {
360
+ }, gracefulShutdownTimeout ?? 0);
361
+ const onClose = () => {
279
362
  log.info(
280
363
  `Appium HTTP server has been successfully closed after ` +
281
- `${timer.getDuration().asMilliSeconds.toFixed(0)}ms`
364
+ `${timer.getDuration().asMilliSeconds.toFixed(0)}ms`
282
365
  );
283
366
  clearTimeout(onTimeout);
284
367
  _resolve();
285
- });
286
- originalClose((/** @type {Error|undefined} */ err) => {
368
+ };
369
+ httpServer.once('close', onClose);
370
+ originalClose((err?: Error) => {
287
371
  if (err) {
372
+ clearTimeout(onTimeout);
373
+ httpServer.removeListener('close', onClose);
288
374
  _reject(err);
289
375
  }
290
376
  });
291
377
  });
292
378
 
293
- appiumServer.once(
294
- 'error',
295
- /** @param {NodeJS.ErrnoException} err */ (err) => {
296
- if (err.code === 'EADDRNOTAVAIL') {
297
- log.error(
298
- 'Could not start REST http interface listener. ' + 'Requested address is not available.'
299
- );
300
- } else {
301
- log.error(
302
- 'Could not start REST http interface listener. The requested ' +
303
- 'port may already be in use. Please make sure there is no ' +
304
- 'other instance of this server running already.'
305
- );
306
- }
307
- reject(err);
379
+ appiumServer.once('error', (err: NodeJS.ErrnoException) => {
380
+ if (err.code === 'EADDRNOTAVAIL') {
381
+ log.error(
382
+ 'Could not start REST http interface listener. ' +
383
+ 'Requested address is not available.'
384
+ );
385
+ } else {
386
+ log.error(
387
+ 'Could not start REST http interface listener. The requested ' +
388
+ 'port may already be in use. Please make sure there is no ' +
389
+ 'other instance of this server running already.'
390
+ );
308
391
  }
309
- );
392
+ reject(err);
393
+ });
310
394
 
311
395
  appiumServer.on('connection', (socket) => socket.setTimeout(keepAliveTimeout));
312
396
 
313
397
  return appiumServer;
314
398
  }
315
399
 
316
- /**
317
- * Starts an {@linkcode AppiumServer}
318
- * @param {StartServerOpts} opts
319
- * @returns {Promise<void>}
320
- */
321
400
  async function startServer({
322
401
  httpServer,
323
402
  port,
324
403
  hostname,
325
404
  keepAliveTimeout,
326
405
  requestTimeout,
327
- }) {
328
- // If the hostname is omitted, the server will accept
329
- // connections on any IP address
330
- /** @type {(port: number, hostname?: string) => B<http.Server>} */
331
- const start = B.promisify(httpServer.listen, {context: httpServer});
406
+ }: StartServerOpts): Promise<void> {
407
+ // If the hostname is omitted, the server will accept connections on any IP address
408
+ const start = B.promisify(httpServer.listen, {
409
+ context: httpServer,
410
+ }) as (port: number, hostname?: string) => B<HttpServer>;
332
411
  const startPromise = start(port, hostname);
333
412
  httpServer.keepAliveTimeout = keepAliveTimeout;
334
413
  if (_.isInteger(requestTimeout)) {
@@ -340,92 +419,20 @@ async function startServer({
340
419
  }
341
420
 
342
421
  /**
343
- * Checks if the provided server instance supports `shouldUpgradeCallback`.
344
- * This feature was added in Node.js v22.21.0 (LTS) and v24.9.0.
345
- * @param {import('http').Server} server - The HTTP server instance to check
346
- * @returns {boolean}
422
+ * Checks if the server supports `shouldUpgradeCallback` (Node.js v22.21.0+ / v24.9.0+).
423
+ *
424
+ * @param server - The HTTP server instance
425
+ * @returns true if shouldUpgradeCallback is available
347
426
  */
348
- function hasShouldUpgradeCallback(server) {
427
+ function hasShouldUpgradeCallback(server: HttpServer): boolean {
349
428
  // Check if shouldUpgradeCallback is available on http.Server
350
429
  // This is a runtime check that works regardless of TypeScript types
351
430
  try {
352
- // Use bracket notation to access property that may not exist in type definitions
353
- // eslint-disable-next-line dot-notation
354
- return typeof server['shouldUpgradeCallback'] !== 'undefined';
431
+ return (
432
+ typeof (server as unknown as {shouldUpgradeCallback?: unknown}).shouldUpgradeCallback !==
433
+ 'undefined'
434
+ );
355
435
  } catch {
356
436
  return false;
357
437
  }
358
438
  }
359
-
360
- /**
361
- * Options for {@linkcode startServer}.
362
- * @typedef StartServerOpts
363
- * @property {import('http').Server} httpServer - HTTP server instance
364
- * @property {number} port - Port to run on
365
- * @property {number} keepAliveTimeout - Keep-alive timeout in milliseconds
366
- * @property {string} [hostname] - Optional hostname
367
- * @property {number} [requestTimeout] - The timeout value in milliseconds for
368
- * receiving the entire request from the client
369
- */
370
-
371
- /**
372
- * @typedef {import('@appium/types').AppiumServer} AppiumServer
373
- */
374
-
375
- /**
376
- * @typedef {import('@appium/types').MethodMap<import('@appium/types').ExternalDriver>} MethodMap
377
- */
378
-
379
- /**
380
- * Options for {@linkcode configureHttp}
381
- * @typedef ConfigureHttpOpts
382
- * @property {import('http').Server} httpServer - HTTP server instance
383
- * @property {(error?: any) => void} reject - Rejection function from `Promise` constructor
384
- * @property {number} keepAliveTimeout - Keep-alive timeout in milliseconds
385
- * @property {number} gracefulShutdownTimeout - For how long the server should delay its
386
- * shutdown before force-closing all open connections to it. Providing zero will force-close
387
- * the server without waiting for any connections.
388
- */
389
-
390
- /**
391
- * Options for {@linkcode server}
392
- * @typedef ServerOpts
393
- * @property {RouteConfiguringFunction} routeConfiguringFunction
394
- * @property {number} port
395
- * @property {import('@appium/types').ServerArgs} [cliArgs]
396
- * @property {string} [hostname]
397
- * @property {boolean} [allowCors]
398
- * @property {string} [basePath]
399
- * @property {MethodMap} [extraMethodMap]
400
- * @property {import('@appium/types').UpdateServerCallback[]} [serverUpdaters]
401
- * @property {number} [keepAliveTimeout]
402
- * @property {number} [requestTimeout]
403
- */
404
-
405
- /**
406
- * A function which configures routes
407
- * @callback RouteConfiguringFunction
408
- * @param {import('express').Express} app
409
- * @param {RouteConfiguringFunctionOpts} [opts]
410
- * @returns {void}
411
- */
412
-
413
- /**
414
- * Options for a {@linkcode RouteConfiguringFunction}
415
- * @typedef RouteConfiguringFunctionOpts
416
- * @property {string} [basePath]
417
- * @property {MethodMap} [extraMethodMap]
418
- */
419
-
420
- /**
421
- * Options for {@linkcode configureServer}
422
- * @typedef ConfigureServerOpts
423
- * @property {import('express').Express} app
424
- * @property {RouteConfiguringFunction} addRoutes
425
- * @property {boolean} [allowCors]
426
- * @property {string} [basePath]
427
- * @property {MethodMap} [extraMethodMap]
428
- * @property {import('@appium/types').StringRecord} [webSocketsMapping={}]
429
- * @property {boolean} [useLegacyUpgradeHandler=true] - Whether to use legacy Express middleware for WebSocket upgrades.
430
- * Set to false when using shouldUpgradeCallback on the HTTP server (Node.js >= 22.21.0 or >= 24.9.0).
431
- */
@@ -0,0 +1,77 @@
1
+ import path from 'node:path';
2
+ import {log} from './logger';
3
+ import _ from 'lodash';
4
+ import {fs} from '@appium/support';
5
+ import B from 'bluebird';
6
+ import type {Request, Response} from 'express';
7
+
8
+ export const STATIC_DIR = _.isNull(path.resolve(__dirname).match(/build[/\\]lib[/\\]express$/))
9
+ ? path.resolve(__dirname, '..', '..', 'static')
10
+ : path.resolve(__dirname, '..', '..', '..', 'static');
11
+
12
+ type TemplateParams = Record<string, unknown>;
13
+
14
+ /** Dynamic page mapped to /test/guinea-pig */
15
+ export async function guineaPig(req: Request, res: Response): Promise<void> {
16
+ await guineaPigTemplate(req, res, 'guinea-pig.html');
17
+ }
18
+
19
+ /** Dynamic page mapped to /test/guinea-pig-scrollable */
20
+ export async function guineaPigScrollable(req: Request, res: Response): Promise<void> {
21
+ await guineaPigTemplate(req, res, 'guinea-pig-scrollable.html');
22
+ }
23
+
24
+ /** Dynamic page mapped to /test/guinea-pig-app-banner */
25
+ export async function guineaPigAppBanner(req: Request, res: Response): Promise<void> {
26
+ await guineaPigTemplate(req, res, 'guinea-pig-app-banner.html');
27
+ }
28
+
29
+ /** Dynamic page mapped to /welcome */
30
+ export async function welcome(req: Request, res: Response): Promise<void> {
31
+ const params: TemplateParams = {message: "Let's browse!"};
32
+ log.debug(`Sending welcome response with params: ${JSON.stringify(params)}`);
33
+ const template = await getTemplate('welcome.html');
34
+ res.send(template(params));
35
+ }
36
+
37
+ async function guineaPigTemplate(
38
+ req: Request,
39
+ res: Response,
40
+ page: string
41
+ ): Promise<void> {
42
+ const delay = parseInt(
43
+ String(req.params.delay ?? (req.query?.delay ?? 0)),
44
+ 10
45
+ );
46
+ const throwError = String(req.params.throwError ?? req.query?.throwError ?? '');
47
+ const params: TemplateParams = {
48
+ throwError,
49
+ serverTime: new Date(),
50
+ userAgent: req.headers['user-agent'],
51
+ comment: 'None',
52
+ };
53
+ if (req.method === 'POST' && req.body && typeof req.body === 'object' && 'comments' in req.body) {
54
+ params.comment = String((req.body as {comments?: string}).comments ?? params.comment);
55
+ }
56
+ log.debug(`Sending guinea pig response with params: ${JSON.stringify(params)}`);
57
+ if (delay) {
58
+ log.debug(`Waiting ${delay}ms before responding`);
59
+ await B.delay(delay);
60
+ }
61
+ res.set('content-type', 'text/html');
62
+ res.cookie('guineacookie1', 'i am a cookie value', {path: '/'});
63
+ res.cookie('guineacookie2', 'cookié2', {path: '/'});
64
+ res.cookie('guineacookie3', 'cant access this', {
65
+ domain: '.blargimarg.com',
66
+ path: '/',
67
+ });
68
+ const template = await getTemplate(page);
69
+ res.send(template(params));
70
+ }
71
+
72
+ async function getTemplate(
73
+ templateName: string
74
+ ): Promise<(params: TemplateParams) => string> {
75
+ const content = await fs.readFile(path.resolve(STATIC_DIR, 'test', templateName));
76
+ return _.template(content.toString()) as (params: TemplateParams) => string;
77
+ }
@@ -0,0 +1,81 @@
1
+ import _ from 'lodash';
2
+ import B from 'bluebird';
3
+ import type {AppiumServer, WSServer} from '@appium/types';
4
+
5
+ export const DEFAULT_WS_PATHNAME_PREFIX = '/ws';
6
+
7
+ /**
8
+ * Adds a WebSocket handler to this server's mapping.
9
+ * @see AppiumServerExtension.addWebSocketHandler
10
+ */
11
+ export async function addWebSocketHandler(
12
+ this: AppiumServer,
13
+ handlerPathname: string,
14
+ handlerServer: WSServer
15
+ ): Promise<void> {
16
+ this.webSocketsMapping[handlerPathname] = handlerServer;
17
+ }
18
+
19
+ /**
20
+ * Returns WebSocket handlers for this server, optionally filtered by pathname.
21
+ * @see AppiumServerExtension.getWebSocketHandlers
22
+ */
23
+ export async function getWebSocketHandlers(
24
+ this: AppiumServer,
25
+ keysFilter: string | null = null
26
+ ): Promise<Record<string, WSServer>> {
27
+ return _.toPairs(this.webSocketsMapping).reduce<Record<string, WSServer>>(
28
+ (acc, [pathname, wsServer]) => {
29
+ if (!_.isString(keysFilter) || pathname.includes(keysFilter)) {
30
+ acc[pathname] = wsServer;
31
+ }
32
+ return acc;
33
+ },
34
+ {}
35
+ );
36
+ }
37
+
38
+ /**
39
+ * Removes a WebSocket handler by pathname.
40
+ * @see AppiumServerExtension.removeWebSocketHandler
41
+ */
42
+ export async function removeWebSocketHandler(
43
+ this: AppiumServer,
44
+ handlerPathname: string
45
+ ): Promise<boolean> {
46
+ const wsServer = this.webSocketsMapping?.[handlerPathname];
47
+ if (!wsServer) {
48
+ return false;
49
+ }
50
+
51
+ try {
52
+ wsServer.close();
53
+ for (const client of wsServer.clients || []) {
54
+ client.terminate();
55
+ }
56
+ return true;
57
+ } catch {
58
+ // ignore
59
+ } finally {
60
+ delete this.webSocketsMapping[handlerPathname];
61
+ }
62
+ return false;
63
+ }
64
+
65
+ /**
66
+ * Removes all WebSocket handlers from this server.
67
+ * @see AppiumServerExtension.removeAllWebSocketHandlers
68
+ */
69
+ export async function removeAllWebSocketHandlers(this: AppiumServer): Promise<boolean> {
70
+ if (_.isEmpty(this.webSocketsMapping)) {
71
+ return false;
72
+ }
73
+
74
+ return _.some(
75
+ await B.all(
76
+ _.keys(this.webSocketsMapping).map((pathname) =>
77
+ this.removeWebSocketHandler(pathname)
78
+ )
79
+ )
80
+ );
81
+ }