@appium/base-driver 10.2.0 → 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 (127) hide show
  1. package/LICENSE +201 -0
  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 +148 -239
  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 +69 -73
  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 +51 -128
  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 +32 -42
  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-status/status.d.ts +113 -158
  68. package/build/lib/jsonwp-status/status.d.ts.map +1 -1
  69. package/build/lib/jsonwp-status/status.js +10 -14
  70. package/build/lib/jsonwp-status/status.js.map +1 -1
  71. package/build/lib/protocol/bidi-commands.d.ts +31 -36
  72. package/build/lib/protocol/bidi-commands.d.ts.map +1 -1
  73. package/build/lib/protocol/bidi-commands.js +5 -5
  74. package/build/lib/protocol/bidi-commands.js.map +1 -1
  75. package/build/lib/protocol/errors.d.ts.map +1 -1
  76. package/build/lib/protocol/helpers.d.ts +7 -11
  77. package/build/lib/protocol/helpers.d.ts.map +1 -1
  78. package/build/lib/protocol/helpers.js +5 -9
  79. package/build/lib/protocol/helpers.js.map +1 -1
  80. package/build/lib/protocol/index.d.ts +4 -21
  81. package/build/lib/protocol/index.d.ts.map +1 -1
  82. package/build/lib/protocol/index.js.map +1 -1
  83. package/build/lib/protocol/protocol.d.ts +15 -1
  84. package/build/lib/protocol/protocol.d.ts.map +1 -1
  85. package/build/lib/protocol/protocol.js +50 -20
  86. package/build/lib/protocol/protocol.js.map +1 -1
  87. package/build/lib/protocol/routes.d.ts +8 -15
  88. package/build/lib/protocol/routes.d.ts.map +1 -1
  89. package/build/lib/protocol/routes.js +18 -33
  90. package/build/lib/protocol/routes.js.map +1 -1
  91. package/lib/basedriver/capabilities.ts +1 -1
  92. package/lib/basedriver/commands/event.ts +2 -2
  93. package/lib/basedriver/commands/execute.ts +2 -2
  94. package/lib/basedriver/commands/find.ts +2 -2
  95. package/lib/basedriver/commands/mixin.ts +1 -1
  96. package/lib/basedriver/commands/timeout.ts +2 -2
  97. package/lib/basedriver/{device-settings.js → device-settings.ts} +24 -35
  98. package/lib/basedriver/{helpers.js → helpers.ts} +208 -266
  99. package/lib/basedriver/logger.ts +3 -0
  100. package/lib/basedriver/validation.ts +2 -2
  101. package/lib/constants.ts +1 -1
  102. package/lib/express/crash.ts +15 -0
  103. package/lib/express/express-logging.ts +84 -0
  104. package/lib/express/{idempotency.js → idempotency.ts} +105 -89
  105. package/lib/express/logger.ts +3 -0
  106. package/lib/express/middleware.ts +187 -0
  107. package/lib/express/{server.js → server.ts} +175 -167
  108. package/lib/express/static.ts +77 -0
  109. package/lib/express/websocket.ts +81 -0
  110. package/lib/helpers/capabilities.ts +83 -0
  111. package/lib/jsonwp-status/{status.js → status.ts} +12 -15
  112. package/lib/protocol/{bidi-commands.js → bidi-commands.ts} +7 -5
  113. package/lib/protocol/errors.ts +1 -1
  114. package/lib/protocol/{helpers.js → helpers.ts} +8 -11
  115. package/lib/protocol/protocol.ts +57 -26
  116. package/lib/protocol/{routes.js → routes.ts} +29 -40
  117. package/package.json +11 -11
  118. package/tsconfig.json +3 -1
  119. package/lib/basedriver/logger.js +0 -4
  120. package/lib/express/crash.js +0 -11
  121. package/lib/express/express-logging.js +0 -60
  122. package/lib/express/logger.js +0 -4
  123. package/lib/express/middleware.js +0 -171
  124. package/lib/express/static.js +0 -76
  125. package/lib/express/websocket.js +0 -79
  126. package/lib/helpers/capabilities.js +0 -93
  127. /package/lib/protocol/{index.js → index.ts} +0 -0
@@ -1,11 +1,13 @@
1
1
  import _ from 'lodash';
2
2
  import path from 'node:path';
3
3
  import express from 'express';
4
+ import type {Express, RequestHandler} from 'express';
4
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);
@@ -128,7 +198,7 @@ export function configureServer({
128
198
  app.use(`${basePath}/produce_error`, produceError);
129
199
  app.use(`${basePath}/crash`, produceCrash);
130
200
 
131
- // 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.
132
202
  // When shouldUpgradeCallback is available, upgrades are handled directly on the HTTP server
133
203
  // to avoid Express middleware timeout issues with long-lived connections
134
204
  if (useLegacyUpgradeHandler) {
@@ -161,17 +231,17 @@ export function configureServer({
161
231
  }
162
232
 
163
233
  /**
164
- * Normalize base path string
165
- * @param {string} basePath
166
- * @returns {string}
234
+ * Normalize base path string (leading slash, no trailing slash).
235
+ *
236
+ * @param basePath - Raw base path
237
+ * @returns Normalized base path
167
238
  */
168
- export function normalizeBasePath(basePath) {
239
+ export function normalizeBasePath(basePath: string): string {
169
240
  if (!_.isString(basePath)) {
170
241
  throw new Error(`Invalid path prefix ${basePath}`);
171
242
  }
172
243
 
173
- // ensure the path prefix does not end in '/', since our method map
174
- // starts all paths with '/'
244
+ // ensure the path prefix does not end in '/', since our method map starts all paths with '/'
175
245
  basePath = basePath.replace(/\/$/, '');
176
246
 
177
247
  // likewise, ensure the path prefix does always START with /, unless the path
@@ -183,64 +253,78 @@ export function normalizeBasePath(basePath) {
183
253
  return basePath;
184
254
  }
185
255
 
186
- /**
187
- *
188
- * @param {import('express').Express} app
189
- * @param {Partial<import('@appium/types').ServerArgs>} [cliArgs]
190
- * @returns {Promise<http.Server>}
191
- */
192
- async function createServer (app, cliArgs) {
256
+ async function createServer(
257
+ app: Express,
258
+ cliArgs?: Partial<ServerArgs>
259
+ ): Promise<HttpServer> {
193
260
  const {sslCertificatePath, sslKeyPath} = cliArgs ?? {};
194
261
  if (!sslCertificatePath && !sslKeyPath) {
195
262
  return http.createServer(app);
196
263
  }
197
264
  if (!sslCertificatePath || !sslKeyPath) {
198
- 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
+ );
199
268
  }
200
269
 
201
270
  const certKey = [sslCertificatePath, sslKeyPath];
202
271
  const zipped = _.zip(
203
272
  await B.all(certKey.map((p) => fs.exists(p))),
204
273
  ['certificate', 'key'],
205
- certKey,
206
- );
274
+ certKey
275
+ ) as [boolean, string, string][];
207
276
  for (const [exists, desc, p] of zipped) {
208
277
  if (!exists) {
209
- 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
+ );
210
281
  }
211
282
  }
212
- 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];
213
286
  log.debug('Enabling TLS/SPDY on the server using the provided certificate');
214
287
 
215
- return require('spdy').createServer({
216
- cert,
217
- key,
218
- spdy: {
219
- plain: false,
220
- ssl: true,
221
- }
222
- }, 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
+ );
223
305
  }
224
306
 
225
307
  /**
226
- * Monkeypatches the `http.Server` instance and returns a {@linkcode AppiumServer}.
227
- * This function _mutates_ the `httpServer` parameter.
228
- * @param {ConfigureHttpOpts} opts
229
- * @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
230
313
  */
231
- function configureHttp({httpServer, reject, keepAliveTimeout, gracefulShutdownTimeout}) {
232
- /**
233
- * @type {AppiumServer}
234
- */
235
- 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;
236
321
  appiumServer.webSocketsMapping = {};
237
322
  appiumServer.addWebSocketHandler = addWebSocketHandler;
238
323
  appiumServer.removeWebSocketHandler = removeWebSocketHandler;
239
324
  appiumServer.removeAllWebSocketHandlers = removeAllWebSocketHandlers;
240
325
  appiumServer.getWebSocketHandlers = getWebSocketHandlers;
241
326
  appiumServer.isSecure = function isSecure() {
242
- // eslint-disable-next-line dot-notation
243
- return Boolean(this['_spdyState']?.secure);
327
+ return Boolean((this as unknown as {_spdyState?: {secure?: boolean}})._spdyState?.secure);
244
328
  };
245
329
 
246
330
  // This avoids Express middleware timeout issues with long-lived WebSocket connections
@@ -248,8 +332,8 @@ function configureHttp({httpServer, reject, keepAliveTimeout, gracefulShutdownTi
248
332
  // See: https://github.com/nodejs/node/pull/59824
249
333
  if (hasShouldUpgradeCallback(httpServer)) {
250
334
  // shouldUpgradeCallback only returns a boolean to indicate if the upgrade should proceed
251
- // eslint-disable-next-line dot-notation
252
- 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';
253
337
  appiumServer.on('upgrade', (req, socket, head) => {
254
338
  if (!tryHandleWebSocketUpgrade(req, socket, head, appiumServer.webSocketsMapping)) {
255
339
  socket.destroy();
@@ -261,73 +345,69 @@ function configureHttp({httpServer, reject, keepAliveTimeout, gracefulShutdownTi
261
345
  // all connections are closed and the `close` event is emitted
262
346
  const originalClose = appiumServer.close.bind(appiumServer);
263
347
  appiumServer.close = async () =>
264
- await new B((_resolve, _reject) => {
348
+ await new B<void>((_resolve, _reject) => {
265
349
  log.info('Closing Appium HTTP server');
266
350
  const timer = new timing.Timer().start();
267
351
  const onTimeout = setTimeout(() => {
268
- if (gracefulShutdownTimeout > 0) {
352
+ if ((gracefulShutdownTimeout ?? 0) > 0) {
269
353
  log.info(
270
354
  `Not all active connections have been closed within ${gracefulShutdownTimeout}ms. ` +
271
- `This timeout might be customized by the --shutdown-timeout command line ` +
272
- `argument. Closing the server anyway.`
355
+ `This timeout might be customized by the --shutdown-timeout command line ` +
356
+ `argument. Closing the server anyway.`
273
357
  );
274
358
  }
275
359
  process.exit(process.exitCode ?? 0);
276
- }, gracefulShutdownTimeout);
277
- httpServer.once('close', () => {
360
+ }, gracefulShutdownTimeout ?? 0);
361
+ const onClose = () => {
278
362
  log.info(
279
363
  `Appium HTTP server has been successfully closed after ` +
280
- `${timer.getDuration().asMilliSeconds.toFixed(0)}ms`
364
+ `${timer.getDuration().asMilliSeconds.toFixed(0)}ms`
281
365
  );
282
366
  clearTimeout(onTimeout);
283
367
  _resolve();
284
- });
285
- originalClose((/** @type {Error|undefined} */ err) => {
368
+ };
369
+ httpServer.once('close', onClose);
370
+ originalClose((err?: Error) => {
286
371
  if (err) {
372
+ clearTimeout(onTimeout);
373
+ httpServer.removeListener('close', onClose);
287
374
  _reject(err);
288
375
  }
289
376
  });
290
377
  });
291
378
 
292
- appiumServer.once(
293
- 'error',
294
- /** @param {NodeJS.ErrnoException} err */ (err) => {
295
- if (err.code === 'EADDRNOTAVAIL') {
296
- log.error(
297
- 'Could not start REST http interface listener. ' + 'Requested address is not available.'
298
- );
299
- } else {
300
- log.error(
301
- 'Could not start REST http interface listener. The requested ' +
302
- 'port may already be in use. Please make sure there is no ' +
303
- 'other instance of this server running already.'
304
- );
305
- }
306
- 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
+ );
307
391
  }
308
- );
392
+ reject(err);
393
+ });
309
394
 
310
395
  appiumServer.on('connection', (socket) => socket.setTimeout(keepAliveTimeout));
311
396
 
312
397
  return appiumServer;
313
398
  }
314
399
 
315
- /**
316
- * Starts an {@linkcode AppiumServer}
317
- * @param {StartServerOpts} opts
318
- * @returns {Promise<void>}
319
- */
320
400
  async function startServer({
321
401
  httpServer,
322
402
  port,
323
403
  hostname,
324
404
  keepAliveTimeout,
325
405
  requestTimeout,
326
- }) {
327
- // If the hostname is omitted, the server will accept
328
- // connections on any IP address
329
- /** @type {(port: number, hostname?: string) => B<http.Server>} */
330
- 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>;
331
411
  const startPromise = start(port, hostname);
332
412
  httpServer.keepAliveTimeout = keepAliveTimeout;
333
413
  if (_.isInteger(requestTimeout)) {
@@ -339,92 +419,20 @@ async function startServer({
339
419
  }
340
420
 
341
421
  /**
342
- * Checks if the provided server instance supports `shouldUpgradeCallback`.
343
- * This feature was added in Node.js v22.21.0 (LTS) and v24.9.0.
344
- * @param {import('http').Server} server - The HTTP server instance to check
345
- * @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
346
426
  */
347
- function hasShouldUpgradeCallback(server) {
427
+ function hasShouldUpgradeCallback(server: HttpServer): boolean {
348
428
  // Check if shouldUpgradeCallback is available on http.Server
349
429
  // This is a runtime check that works regardless of TypeScript types
350
430
  try {
351
- // Use bracket notation to access property that may not exist in type definitions
352
- // eslint-disable-next-line dot-notation
353
- return typeof server['shouldUpgradeCallback'] !== 'undefined';
431
+ return (
432
+ typeof (server as unknown as {shouldUpgradeCallback?: unknown}).shouldUpgradeCallback !==
433
+ 'undefined'
434
+ );
354
435
  } catch {
355
436
  return false;
356
437
  }
357
438
  }
358
-
359
- /**
360
- * Options for {@linkcode startServer}.
361
- * @typedef StartServerOpts
362
- * @property {import('http').Server} httpServer - HTTP server instance
363
- * @property {number} port - Port to run on
364
- * @property {number} keepAliveTimeout - Keep-alive timeout in milliseconds
365
- * @property {string} [hostname] - Optional hostname
366
- * @property {number} [requestTimeout] - The timeout value in milliseconds for
367
- * receiving the entire request from the client
368
- */
369
-
370
- /**
371
- * @typedef {import('@appium/types').AppiumServer} AppiumServer
372
- */
373
-
374
- /**
375
- * @typedef {import('@appium/types').MethodMap<import('@appium/types').ExternalDriver>} MethodMap
376
- */
377
-
378
- /**
379
- * Options for {@linkcode configureHttp}
380
- * @typedef ConfigureHttpOpts
381
- * @property {import('http').Server} httpServer - HTTP server instance
382
- * @property {(error?: any) => void} reject - Rejection function from `Promise` constructor
383
- * @property {number} keepAliveTimeout - Keep-alive timeout in milliseconds
384
- * @property {number} gracefulShutdownTimeout - For how long the server should delay its
385
- * shutdown before force-closing all open connections to it. Providing zero will force-close
386
- * the server without waiting for any connections.
387
- */
388
-
389
- /**
390
- * Options for {@linkcode server}
391
- * @typedef ServerOpts
392
- * @property {RouteConfiguringFunction} routeConfiguringFunction
393
- * @property {number} port
394
- * @property {import('@appium/types').ServerArgs} [cliArgs]
395
- * @property {string} [hostname]
396
- * @property {boolean} [allowCors]
397
- * @property {string} [basePath]
398
- * @property {MethodMap} [extraMethodMap]
399
- * @property {import('@appium/types').UpdateServerCallback[]} [serverUpdaters]
400
- * @property {number} [keepAliveTimeout]
401
- * @property {number} [requestTimeout]
402
- */
403
-
404
- /**
405
- * A function which configures routes
406
- * @callback RouteConfiguringFunction
407
- * @param {import('express').Express} app
408
- * @param {RouteConfiguringFunctionOpts} [opts]
409
- * @returns {void}
410
- */
411
-
412
- /**
413
- * Options for a {@linkcode RouteConfiguringFunction}
414
- * @typedef RouteConfiguringFunctionOpts
415
- * @property {string} [basePath]
416
- * @property {MethodMap} [extraMethodMap]
417
- */
418
-
419
- /**
420
- * Options for {@linkcode configureServer}
421
- * @typedef ConfigureServerOpts
422
- * @property {import('express').Express} app
423
- * @property {RouteConfiguringFunction} addRoutes
424
- * @property {boolean} [allowCors]
425
- * @property {string} [basePath]
426
- * @property {MethodMap} [extraMethodMap]
427
- * @property {import('@appium/types').StringRecord} [webSocketsMapping={}]
428
- * @property {boolean} [useLegacyUpgradeHandler=true] - Whether to use legacy Express middleware for WebSocket upgrades.
429
- * Set to false when using shouldUpgradeCallback on the HTTP server (Node.js >= 22.21.0 or >= 24.9.0).
430
- */
@@ -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
+ }