@appium/base-driver 10.6.0 → 10.7.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 (157) hide show
  1. package/build/lib/basedriver/capabilities.d.ts +1 -1
  2. package/build/lib/basedriver/capabilities.d.ts.map +1 -1
  3. package/build/lib/basedriver/capabilities.js +15 -7
  4. package/build/lib/basedriver/capabilities.js.map +1 -1
  5. package/build/lib/basedriver/commands/bidi.js +1 -1
  6. package/build/lib/basedriver/commands/event.js.map +1 -1
  7. package/build/lib/basedriver/commands/execute.js.map +1 -1
  8. package/build/lib/basedriver/commands/find.d.ts.map +1 -1
  9. package/build/lib/basedriver/commands/find.js +2 -1
  10. package/build/lib/basedriver/commands/find.js.map +1 -1
  11. package/build/lib/basedriver/commands/timeout.js +4 -4
  12. package/build/lib/basedriver/commands/timeout.js.map +1 -1
  13. package/build/lib/basedriver/core.d.ts.map +1 -1
  14. package/build/lib/basedriver/core.js +5 -2
  15. package/build/lib/basedriver/core.js.map +1 -1
  16. package/build/lib/basedriver/device-settings.d.ts.map +1 -1
  17. package/build/lib/basedriver/device-settings.js.map +1 -1
  18. package/build/lib/basedriver/driver.d.ts.map +1 -1
  19. package/build/lib/basedriver/driver.js +23 -24
  20. package/build/lib/basedriver/driver.js.map +1 -1
  21. package/build/lib/basedriver/extension-core.d.ts.map +1 -1
  22. package/build/lib/basedriver/extension-core.js +11 -5
  23. package/build/lib/basedriver/extension-core.js.map +1 -1
  24. package/build/lib/basedriver/helpers.d.ts.map +1 -1
  25. package/build/lib/basedriver/helpers.js +20 -4
  26. package/build/lib/basedriver/helpers.js.map +1 -1
  27. package/build/lib/basedriver/ipc.d.ts.map +1 -1
  28. package/build/lib/basedriver/ipc.js +6 -4
  29. package/build/lib/basedriver/ipc.js.map +1 -1
  30. package/build/lib/basedriver/validation.d.ts.map +1 -1
  31. package/build/lib/basedriver/validation.js +3 -2
  32. package/build/lib/basedriver/validation.js.map +1 -1
  33. package/build/lib/express/express-logging.d.ts +0 -1
  34. package/build/lib/express/express-logging.d.ts.map +1 -1
  35. package/build/lib/express/express-logging.js +9 -8
  36. package/build/lib/express/express-logging.js.map +1 -1
  37. package/build/lib/express/idempotency.js.map +1 -1
  38. package/build/lib/express/middleware.d.ts.map +1 -1
  39. package/build/lib/express/middleware.js.map +1 -1
  40. package/build/lib/express/server.d.ts +1 -1
  41. package/build/lib/express/server.d.ts.map +1 -1
  42. package/build/lib/express/server.js +19 -20
  43. package/build/lib/express/server.js.map +1 -1
  44. package/build/lib/express/websocket.d.ts.map +1 -1
  45. package/build/lib/express/websocket.js.map +1 -1
  46. package/build/lib/helpers/capabilities.d.ts.map +1 -1
  47. package/build/lib/helpers/capabilities.js.map +1 -1
  48. package/build/lib/helpers/levenshtein-match.d.ts.map +1 -1
  49. package/build/lib/helpers/levenshtein-match.js +4 -1
  50. package/build/lib/helpers/levenshtein-match.js.map +1 -1
  51. package/build/lib/index.d.ts +1 -1
  52. package/build/lib/index.d.ts.map +1 -1
  53. package/build/lib/index.js +3 -2
  54. package/build/lib/index.js.map +1 -1
  55. package/build/lib/jsonwp-proxy/protocol-converter.d.ts.map +1 -1
  56. package/build/lib/jsonwp-proxy/protocol-converter.js +14 -7
  57. package/build/lib/jsonwp-proxy/protocol-converter.js.map +1 -1
  58. package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
  59. package/build/lib/jsonwp-proxy/proxy.js +17 -11
  60. package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
  61. package/build/lib/protocol/errors.d.ts.map +1 -1
  62. package/build/lib/protocol/errors.js +13 -13
  63. package/build/lib/protocol/errors.js.map +1 -1
  64. package/build/lib/protocol/protocol.d.ts +1 -1
  65. package/build/lib/protocol/protocol.d.ts.map +1 -1
  66. package/build/lib/protocol/protocol.js +35 -18
  67. package/build/lib/protocol/protocol.js.map +1 -1
  68. package/build/lib/protocol/routes.d.ts.map +1 -1
  69. package/build/lib/protocol/routes.js +7 -5
  70. package/build/lib/protocol/routes.js.map +1 -1
  71. package/build/lib/test-pages/crash.d.ts.map +1 -0
  72. package/build/lib/test-pages/crash.js.map +1 -0
  73. package/build/lib/test-pages/env.d.ts +5 -0
  74. package/build/lib/test-pages/env.d.ts.map +1 -0
  75. package/build/lib/test-pages/env.js +12 -0
  76. package/build/lib/test-pages/env.js.map +1 -0
  77. package/build/lib/{express/static.d.ts → test-pages/handlers.d.ts} +1 -2
  78. package/build/lib/test-pages/handlers.d.ts.map +1 -0
  79. package/build/lib/{express/static.js → test-pages/handlers.js} +7 -17
  80. package/build/lib/test-pages/handlers.js.map +1 -0
  81. package/build/lib/test-pages/index.d.ts +6 -0
  82. package/build/lib/test-pages/index.d.ts.map +1 -0
  83. package/build/lib/test-pages/index.js +35 -0
  84. package/build/lib/test-pages/index.js.map +1 -0
  85. package/build/lib/test-pages/static-dir.d.ts +8 -0
  86. package/build/lib/test-pages/static-dir.d.ts.map +1 -0
  87. package/build/lib/test-pages/static-dir.js +24 -0
  88. package/build/lib/test-pages/static-dir.js.map +1 -0
  89. package/build/lib/test-pages/template.d.ts +3 -0
  90. package/build/lib/test-pages/template.d.ts.map +1 -0
  91. package/build/lib/test-pages/template.js +19 -0
  92. package/build/lib/test-pages/template.js.map +1 -0
  93. package/build/lib/utils.d.ts +0 -2
  94. package/build/lib/utils.d.ts.map +1 -1
  95. package/build/lib/utils.js +0 -16
  96. package/build/lib/utils.js.map +1 -1
  97. package/lib/basedriver/capabilities.ts +72 -66
  98. package/lib/basedriver/commands/bidi.ts +1 -1
  99. package/lib/basedriver/commands/event.ts +10 -5
  100. package/lib/basedriver/commands/execute.ts +12 -9
  101. package/lib/basedriver/commands/find.ts +20 -12
  102. package/lib/basedriver/commands/log.ts +2 -2
  103. package/lib/basedriver/commands/timeout.ts +17 -8
  104. package/lib/basedriver/core.ts +14 -14
  105. package/lib/basedriver/device-settings.ts +4 -8
  106. package/lib/basedriver/driver.ts +50 -40
  107. package/lib/basedriver/extension-core.ts +33 -17
  108. package/lib/basedriver/helpers.ts +57 -26
  109. package/lib/basedriver/ipc.ts +37 -18
  110. package/lib/basedriver/validation.ts +13 -6
  111. package/lib/express/express-logging.ts +14 -17
  112. package/lib/express/idempotency.ts +6 -6
  113. package/lib/express/middleware.ts +10 -12
  114. package/lib/express/server.ts +53 -61
  115. package/lib/express/websocket.ts +5 -7
  116. package/lib/helpers/capabilities.ts +5 -4
  117. package/lib/helpers/extension-command-name.ts +1 -1
  118. package/lib/helpers/levenshtein-match.ts +20 -11
  119. package/lib/index.js +2 -1
  120. package/lib/jsonwp-proxy/protocol-converter.ts +51 -27
  121. package/lib/jsonwp-proxy/proxy.ts +42 -42
  122. package/lib/protocol/errors.ts +47 -67
  123. package/lib/protocol/protocol.ts +116 -72
  124. package/lib/protocol/routes.ts +9 -9
  125. package/lib/test-pages/env.ts +9 -0
  126. package/lib/{express/static.ts → test-pages/handlers.ts} +7 -27
  127. package/lib/test-pages/index.ts +34 -0
  128. package/lib/test-pages/static-dir.ts +19 -0
  129. package/lib/test-pages/template.ts +17 -0
  130. package/lib/utils.ts +3 -23
  131. package/package.json +9 -10
  132. package/tsconfig.json +1 -0
  133. package/build/lib/express/crash.d.ts.map +0 -1
  134. package/build/lib/express/crash.js.map +0 -1
  135. package/build/lib/express/static.d.ts.map +0 -1
  136. package/build/lib/express/static.js.map +0 -1
  137. /package/build/lib/{express → test-pages}/crash.d.ts +0 -0
  138. /package/build/lib/{express → test-pages}/crash.js +0 -0
  139. /package/lib/{express → test-pages}/crash.ts +0 -0
  140. /package/{static → test-fixtures/static}/appium.png +0 -0
  141. /package/{static → test-fixtures/static}/favicon.ico +0 -0
  142. /package/{static → test-fixtures/static}/js/jquery.min.js +0 -0
  143. /package/{static → test-fixtures/static}/test/frameset.html +0 -0
  144. /package/{static → test-fixtures/static}/test/guinea-pig-app-banner.html +0 -0
  145. /package/{static → test-fixtures/static}/test/guinea-pig-scrollable.html +0 -0
  146. /package/{static → test-fixtures/static}/test/guinea-pig.html +0 -0
  147. /package/{static → test-fixtures/static}/test/guinea-pig2.html +0 -0
  148. /package/{static → test-fixtures/static}/test/guinea-pig3.html +0 -0
  149. /package/{static → test-fixtures/static}/test/guinea-pig4.html +0 -0
  150. /package/{static → test-fixtures/static}/test/guinea-pig5.html +0 -0
  151. /package/{static → test-fixtures/static}/test/iframes.html +0 -0
  152. /package/{static → test-fixtures/static}/test/shadow-dom.html +0 -0
  153. /package/{static → test-fixtures/static}/test/subframe1.html +0 -0
  154. /package/{static → test-fixtures/static}/test/subframe2.html +0 -0
  155. /package/{static → test-fixtures/static}/test/subframe3.html +0 -0
  156. /package/{static → test-fixtures/static}/test/touch.html +0 -0
  157. /package/{static → test-fixtures/static}/test/welcome.html +0 -0
@@ -15,7 +15,14 @@ import {isW3cCaps} from '../helpers/capabilities';
15
15
  import {log} from '../basedriver/logger';
16
16
  import {omitKeys} from '../utils';
17
17
  import {generateDriverLogPrefix} from '../basedriver/helpers';
18
- import type {Core, AppiumLogger, PayloadParams, MethodMap, Driver, DriverMethodDef} from '@appium/types';
18
+ import type {
19
+ Core,
20
+ AppiumLogger,
21
+ PayloadParams,
22
+ MethodMap,
23
+ Driver,
24
+ DriverMethodDef,
25
+ } from '@appium/types';
19
26
  import type {BaseDriver} from '../basedriver/driver';
20
27
  import type {Request, Response, Application} from 'express';
21
28
  import type {MultidimensionalReadonlyArray} from 'type-fest';
@@ -54,10 +61,10 @@ export function getSessionId(driver: Core<any>, req: Request): string | undefine
54
61
  const sessionId = req.params.sessionId[0];
55
62
  getLogger(driver, sessionId).warn(
56
63
  `Received malformed sessionId as array from the route: ${req.originalUrl}. ` +
57
- `This indicates the route definition issue. The route should start with '/session/:sessionId' (named parameter) ` +
58
- `instead of '/session/*sessionId' (wildcard). ` +
59
- `Using the first element as session id: ${sessionId}. ` +
60
- `Please fix the route definition to prevent this error.`
64
+ `This indicates the route definition issue. The route should start with '/session/:sessionId' (named parameter) ` +
65
+ `instead of '/session/*sessionId' (wildcard). ` +
66
+ `Using the first element as session id: ${sessionId}. ` +
67
+ `Please fix the route definition to prevent this error.`,
61
68
  );
62
69
  // This is to not log the message multiple times.
63
70
  req.params.sessionId = sessionId;
@@ -83,7 +90,7 @@ export function isSessionCommand(command: string): boolean {
83
90
  export function checkParams(
84
91
  paramSpec: PayloadParams,
85
92
  args: Record<string, any>,
86
- protocol?: keyof typeof PROTOCOLS
93
+ protocol?: keyof typeof PROTOCOLS,
87
94
  ): Record<string, any> {
88
95
  let requiredParams: string[][] = [];
89
96
  let optionalParams: string[] = [];
@@ -95,8 +102,7 @@ export function checkParams(
95
102
  requiredParams = structuredClone(
96
103
  (hasMultipleRequiredParamSets(paramSpec.required)
97
104
  ? paramSpec.required
98
- : [paramSpec.required]
99
- ) as string[][]
105
+ : [paramSpec.required]) as string[][],
100
106
  );
101
107
  }
102
108
  // optional parameters are just an array
@@ -126,7 +132,10 @@ export function checkParams(
126
132
 
127
133
  if (util.isEmpty(requiredParams)) {
128
134
  // if we don't have any required parameters, then just filter out unknown ones
129
- return pickKnownParams(args, actualParamNames.filter((name) => !optionalParams.includes(name)));
135
+ return pickKnownParams(
136
+ args,
137
+ actualParamNames.filter((name) => !optionalParams.includes(name)),
138
+ );
130
139
  }
131
140
 
132
141
  // go through the required parameters and check against our arguments
@@ -135,25 +144,30 @@ export function checkParams(
135
144
  if (!Array.isArray(requiredParamsSet)) {
136
145
  throw new Error(
137
146
  `The required parameter set item ${JSON.stringify(requiredParamsSet)} ` +
138
- `in ${JSON.stringify(paramSpec)} is not an array. ` +
139
- `This is a bug in the method map definition.`
147
+ `in ${JSON.stringify(paramSpec)} is not an array. ` +
148
+ `This is a bug in the method map definition.`,
140
149
  );
141
150
  }
142
151
  if (requiredParamsSet.every((name) => actualParamNames.includes(name))) {
143
152
  return pickKnownParams(
144
153
  args,
145
- actualParamNames.filter((name) => !requiredParamsSet.includes(name) && !optionalParams.includes(name))
154
+ actualParamNames.filter(
155
+ (name) => !requiredParamsSet.includes(name) && !optionalParams.includes(name),
156
+ ),
146
157
  );
147
158
  }
148
159
  if (!util.isEmpty(requiredParamsSet) && util.isEmpty(matchedReqParamSet)) {
149
160
  matchedReqParamSet = requiredParamsSet;
150
161
  }
151
162
  }
152
- throw new BadParametersError({
153
- ...paramSpec,
154
- required: matchedReqParamSet,
155
- optional: optionalParams,
156
- }, actualParamNames);
163
+ throw new BadParametersError(
164
+ {
165
+ ...paramSpec,
166
+ required: matchedReqParamSet,
167
+ optional: optionalParams,
168
+ },
169
+ actualParamNames,
170
+ );
157
171
  }
158
172
 
159
173
  /**
@@ -162,7 +176,11 @@ export function checkParams(
162
176
  * @param jsonObj - Parsed JSON request body
163
177
  * @param payloadParams - Route payload definition (required/optional/makeArgs)
164
178
  */
165
- export function makeArgs(requestParams: PayloadParams, jsonObj: any, payloadParams: PayloadParams): any[] {
179
+ export function makeArgs(
180
+ requestParams: Record<string, string | string[] | undefined>,
181
+ jsonObj: any,
182
+ payloadParams: PayloadParams,
183
+ ): any[] {
166
184
  // We want to pass the "url" parameters to the commands in reverse order
167
185
  // since the command will sometimes want to ignore, say, the sessionId.
168
186
  // This has the effect of putting sessionId last, which means in JS we can
@@ -224,14 +242,14 @@ export function validateExecuteMethodParams(params: any[], paramSpec?: PayloadPa
224
242
  if (!params || !Array.isArray(params) || params.length > 1) {
225
243
  throw new errors.InvalidArgumentError(
226
244
  `Did not get correct format of arguments for execute method. Expected zero or one ` +
227
- `arguments to execute script and instead received: ${JSON.stringify(params)}`
245
+ `arguments to execute script and instead received: ${JSON.stringify(params)}`,
228
246
  );
229
247
  }
230
248
  const args: Record<string, any> = params[0] ?? {};
231
249
  if (!util.isPlainObject(args)) {
232
250
  throw new errors.InvalidArgumentError(
233
251
  `Did not receive an appropriate execute method parameters object. It needs to be ` +
234
- `deserializable as a plain JS object`
252
+ `deserializable as a plain JS object`,
235
253
  );
236
254
  }
237
255
  const specToUse = {
@@ -268,14 +286,7 @@ export function routeConfiguringFunction(driver: Core<any>): RouteConfiguringFun
268
286
  for (const [method, spec] of Object.entries(methods)) {
269
287
  const isSessCommand = spec.command ? isSessionCommand(spec.command) : false;
270
288
  // set up the express route handler
271
- buildHandler(
272
- app,
273
- method,
274
- `${basePath}${path}`,
275
- spec,
276
- driver,
277
- isSessCommand
278
- );
289
+ buildHandler(app, method, `${basePath}${path}`, spec, driver, isSessCommand);
279
290
  }
280
291
  }
281
292
  };
@@ -309,10 +320,14 @@ export function driverShouldDoJwpProxy(driver: Core<any>, req: Request, command:
309
320
  return true;
310
321
  }
311
322
 
312
- function extractProtocol(driver: Core<any>, sessionId: string | null = null): keyof typeof PROTOCOLS {
313
- const dstDriver = typeof driver.driverForSession === 'function' && sessionId
314
- ? driver.driverForSession(sessionId)
315
- : driver;
323
+ function extractProtocol(
324
+ driver: Core<any>,
325
+ sessionId: string | null = null,
326
+ ): keyof typeof PROTOCOLS {
327
+ const dstDriver =
328
+ typeof driver.driverForSession === 'function' && sessionId
329
+ ? driver.driverForSession(sessionId)
330
+ : driver;
316
331
  if (dstDriver === driver) {
317
332
  // Shortcircuit if the driver instance is not an umbrella driver
318
333
  // or it is Fake driver instance, where `driver.driverForSession`
@@ -327,7 +342,7 @@ function extractProtocol(driver: Core<any>, sessionId: string | null = null): ke
327
342
  function getLogger(driver: Core<any>, sessionId: string | null = null): AppiumLogger {
328
343
  const dstDriver =
329
344
  sessionId && typeof driver.driverForSession === 'function'
330
- ? driver.driverForSession(sessionId) ?? driver
345
+ ? (driver.driverForSession(sessionId) ?? driver)
331
346
  : driver;
332
347
  if (typeof dstDriver.log?.info === 'function') {
333
348
  return dstDriver.log;
@@ -337,14 +352,15 @@ function getLogger(driver: Core<any>, sessionId: string | null = null): AppiumLo
337
352
  return logger.getLogger(logPrefix);
338
353
  }
339
354
 
340
- function wrapParams<T>(paramSets, jsonObj: T): T | Record<string, T> {
355
+ function wrapParams<T>(paramSets: PayloadParams, jsonObj: T): T | Record<string, T> {
341
356
  /* There are commands like performTouch which take a single parameter (primitive type or array).
342
357
  * Some drivers choose to pass this parameter as a value (eg. [action1, action2...]) while others to
343
358
  * wrap it within an object(eg' {gesture: [action1, action2...]}), which makes it hard to validate.
344
359
  * The wrap option in the spec enforce wrapping before validation, so that all params are wrapped at
345
360
  * the time they are validated and later passed to the commands.
346
361
  */
347
- return (Array.isArray(jsonObj) || typeof jsonObj !== 'object' || jsonObj === null) && paramSets.wrap
362
+ return (Array.isArray(jsonObj) || typeof jsonObj !== 'object' || jsonObj === null) &&
363
+ paramSets.wrap
348
364
  ? {[paramSets.wrap]: jsonObj}
349
365
  : jsonObj;
350
366
  }
@@ -353,14 +369,15 @@ function unwrapParams<T>(paramSets: PayloadParams, jsonObj: T): T | Record<strin
353
369
  /* There are commands like setNetworkConnection which send parameters wrapped inside a key such as
354
370
  * "parameters". This function unwraps them (eg. {"parameters": {"type": 1}} becomes {"type": 1}).
355
371
  */
356
- return typeof jsonObj === 'object' && jsonObj !== null && paramSets.unwrap && jsonObj[paramSets.unwrap]
357
- ? jsonObj[paramSets.unwrap]
358
- : jsonObj;
372
+ const unwrapped =
373
+ typeof jsonObj === 'object' && jsonObj !== null && paramSets.unwrap
374
+ ? (jsonObj as Record<string, T>)[paramSets.unwrap]
375
+ : undefined;
376
+ return unwrapped !== undefined ? unwrapped : jsonObj;
359
377
  }
360
378
 
361
-
362
379
  function hasMultipleRequiredParamSets(
363
- required: ReadonlyArray<string> | MultidimensionalReadonlyArray<string, 2> | undefined
380
+ required: ReadonlyArray<string> | MultidimensionalReadonlyArray<string, 2> | undefined,
364
381
  ): required is MultidimensionalReadonlyArray<string, 2> {
365
382
  return Boolean(required && Array.isArray(required?.[0]));
366
383
  }
@@ -379,7 +396,7 @@ function buildHandler(
379
396
  path: string,
380
397
  spec: DriverMethodDef<Driver>,
381
398
  driver: Core<any>,
382
- isSessCmd: boolean
399
+ isSessCmd: boolean,
383
400
  ): void {
384
401
  const asyncHandler = async (req: Request, res: Response) => {
385
402
  let jsonObj = req.body;
@@ -396,7 +413,7 @@ function buildHandler(
396
413
  getLogger(driver, sessionId).warn(
397
414
  `The ${method} ${path} endpoint has been deprecated and will be removed in a future ` +
398
415
  `version of Appium or your driver/plugin. Please use a different endpoint or contact the ` +
399
- `driver/plugin author to add explicit support for the endpoint before it is removed`
416
+ `driver/plugin author to add explicit support for the endpoint before it is removed`,
400
417
  );
401
418
  }
402
419
 
@@ -415,9 +432,15 @@ function buildHandler(
415
432
  // commands and generally would not want that command to be proxied instead of handled by the
416
433
  // plugin)
417
434
  let didPluginOverrideProxy = false;
418
- if (isSessCmd && !spec.neverProxy && spec.command && driverShouldDoJwpProxy(driver, req, spec.command)) {
435
+ if (
436
+ isSessCmd &&
437
+ !spec.neverProxy &&
438
+ spec.command &&
439
+ driverShouldDoJwpProxy(driver, req, spec.command)
440
+ ) {
419
441
  if (
420
- !('pluginsToHandleCmd' in driver) || typeof driver.pluginsToHandleCmd !== 'function' ||
442
+ !('pluginsToHandleCmd' in driver) ||
443
+ typeof driver.pluginsToHandleCmd !== 'function' ||
421
444
  driver.pluginsToHandleCmd(spec.command, sessionId).length === 0
422
445
  ) {
423
446
  await doJwpProxy(driver as BaseDriver<any>, req, res);
@@ -426,7 +449,7 @@ function buildHandler(
426
449
  getLogger(driver, sessionId).debug(
427
450
  `Would have proxied ` +
428
451
  `command directly, but a plugin exists which might require its value, so will let ` +
429
- `its value be collected internally and made part of plugin chain`
452
+ `its value be collected internally and made part of plugin chain`,
430
453
  );
431
454
  didPluginOverrideProxy = true;
432
455
  }
@@ -451,7 +474,7 @@ function buildHandler(
451
474
  // try to determine protocol by session creation args, so we can throw a
452
475
  // properly formatted error if arguments validation fails
453
476
  currentProtocol = determineProtocol(
454
- makeArgs(req.params, jsonObj, spec.payloadParams || {})
477
+ makeArgs(req.params, jsonObj, spec.payloadParams || {}),
455
478
  );
456
479
  }
457
480
 
@@ -465,15 +488,21 @@ function buildHandler(
465
488
  const args = makeArgs(req.params, jsonObj, spec.payloadParams || {});
466
489
  let driverRes: any;
467
490
  // validate command args according to MJSONWP
468
- if (validators[spec.command]) {
469
- validators[spec.command](...args);
491
+ const validator = (
492
+ validators as Record<string, ((...validatorArgs: any[]) => void) | undefined>
493
+ )[spec.command];
494
+ if (validator) {
495
+ validator(...args);
470
496
  }
471
497
 
472
498
  // run the driver command wrapped inside the argument validators
473
499
  getLogger(driver, sessionId).debug(
474
500
  `Calling %s.%s() with args: %s`,
475
- driver.constructor.name, spec.command,
476
- logger.markSensitive(util.truncateString(JSON.stringify(args), {length: MAX_LOG_BODY_LENGTH}))
501
+ driver.constructor.name,
502
+ spec.command,
503
+ logger.markSensitive(
504
+ util.truncateString(JSON.stringify(args), {length: MAX_LOG_BODY_LENGTH}),
505
+ ),
477
506
  );
478
507
 
479
508
  if (didPluginOverrideProxy) {
@@ -491,7 +520,8 @@ function buildHandler(
491
520
  // If `executeCommand` was overridden and the method returns an object
492
521
  // with a protocol and value/error property, re-assign the protocol
493
522
  if (util.isPlainObject(driverRes) && Object.hasOwn(driverRes, 'protocol')) {
494
- currentProtocol = (driverRes as {protocol?: keyof typeof PROTOCOLS}).protocol || currentProtocol;
523
+ currentProtocol =
524
+ (driverRes as {protocol?: keyof typeof PROTOCOLS}).protocol || currentProtocol;
495
525
  if (driverRes.error) {
496
526
  throw driverRes.error;
497
527
  }
@@ -502,7 +532,7 @@ function buildHandler(
502
532
  if (spec.command === CREATE_SESSION_COMMAND) {
503
533
  newSessionId = driverRes[0];
504
534
  getLogger(driver, newSessionId).debug(
505
- `Cached the protocol value '${currentProtocol}' for the new session ${newSessionId}`
535
+ `Cached the protocol value '${currentProtocol}' for the new session ${newSessionId}`,
506
536
  );
507
537
  if (currentProtocol === PROTOCOLS.MJSONWP) {
508
538
  driverRes = driverRes[1];
@@ -520,7 +550,7 @@ function buildHandler(
520
550
  getLogger(driver, sessionId).debug(
521
551
  `Received response: ${util.truncateString(JSON.stringify(driverRes), {
522
552
  length: MAX_LOG_BODY_LENGTH,
523
- })}`
553
+ })}`,
524
554
  );
525
555
  getLogger(driver, sessionId).debug('But deleting session, so not returning');
526
556
  driverRes = null;
@@ -538,7 +568,7 @@ function buildHandler(
538
568
  throw errorFromW3CJsonCode(
539
569
  driverRes.value.error,
540
570
  driverRes.value.message,
541
- driverRes.value.stacktrace
571
+ driverRes.value.stacktrace,
542
572
  );
543
573
  }
544
574
  }
@@ -546,38 +576,48 @@ function buildHandler(
546
576
  httpResBody.value = driverRes;
547
577
  getLogger(driver, sessionId || newSessionId).debug(
548
578
  `Responding ` +
549
- `to client with driver.${spec.command}() result: ${util.truncateString(JSON.stringify(driverRes), {
550
- length: MAX_LOG_BODY_LENGTH,
551
- })}`
579
+ `to client with driver.${spec.command}() result: ${util.truncateString(
580
+ JSON.stringify(driverRes),
581
+ {
582
+ length: MAX_LOG_BODY_LENGTH,
583
+ },
584
+ )}`,
552
585
  );
553
586
  } catch (err) {
554
587
  // if anything goes wrong, figure out what our response should be
555
588
  // based on the type of error that we encountered
556
- let actualErr;
557
- if (err instanceof Error || (Object.hasOwn(err, 'stack') && Object.hasOwn(err, 'message'))) {
589
+ let actualErr: Error;
590
+ if (err instanceof Error) {
558
591
  actualErr = err;
592
+ } else if (
593
+ typeof err === 'object' &&
594
+ err !== null &&
595
+ Object.hasOwn(err, 'stack') &&
596
+ Object.hasOwn(err, 'message')
597
+ ) {
598
+ actualErr = err as Error;
559
599
  } else {
560
600
  getLogger(driver, sessionId || newSessionId).warn(
561
601
  'The thrown error object does not seem to be a valid instance of the Error class. This ' +
562
- 'might be a genuine bug of a driver or a plugin.'
602
+ 'might be a genuine bug of a driver or a plugin.',
563
603
  );
564
604
  actualErr = new Error(`${err ?? 'unknown'}`);
565
605
  }
566
606
 
567
- currentProtocol =
568
- currentProtocol || extractProtocol(driver, sessionId || newSessionId);
607
+ currentProtocol = currentProtocol || extractProtocol(driver, sessionId || newSessionId);
569
608
 
570
- let errMsg = err.stacktrace || err.stack;
571
- if (!errMsg.includes(err.message)) {
609
+ const stacktrace = (err as {stacktrace?: string}).stacktrace;
610
+ let errMsg = stacktrace || actualErr.stack || '';
611
+ if (!errMsg.includes(actualErr.message)) {
572
612
  // if the message has more information, add it. but often the message
573
613
  // is the first part of the stack trace
574
- errMsg = `${err.message}${errMsg ? '\n' + errMsg : ''}`;
614
+ errMsg = `${actualErr.message}${errMsg ? '\n' + errMsg : ''}`;
575
615
  }
576
616
  if (isErrorType(err, errors.ProxyRequestError)) {
577
617
  actualErr = err.getActualError();
578
618
  } else {
579
619
  getLogger(driver, sessionId || newSessionId).debug(
580
- `Encountered internal error running command: ${errMsg}`
620
+ `Encountered internal error running command: ${errMsg}`,
581
621
  );
582
622
  }
583
623
 
@@ -586,7 +626,8 @@ function buildHandler(
586
626
 
587
627
  // decode the response, which is either a string or json
588
628
  if (typeof httpResBody === 'string') {
589
- res.status(httpStatus)
629
+ res
630
+ .status(httpStatus)
590
631
  .setHeader('content-type', 'application/json; charset=utf-8')
591
632
  .send(httpResBody);
592
633
  } else {
@@ -597,16 +638,17 @@ function buildHandler(
597
638
  }
598
639
  };
599
640
  // add the method to the app
600
- app[method.toLowerCase()](path, (req, res) => {
641
+ const registerRoute = (
642
+ app as Application & Record<string, (routePath: string, ...handlers: any[]) => void>
643
+ )[method.toLowerCase()].bind(app);
644
+ registerRoute(path, (req: Request, res: Response) => {
601
645
  void asyncHandler(req, res);
602
646
  });
603
647
  }
604
648
 
605
649
  async function doJwpProxy(driver: BaseDriver<any>, req: Request, res: Response): Promise<void> {
606
650
  const sessionId = getSessionId(driver, req) as string;
607
- getLogger(driver, sessionId).info(
608
- 'Driver proxy active, passing request on via HTTP proxy'
609
- );
651
+ getLogger(driver, sessionId).info('Driver proxy active, passing request on via HTTP proxy');
610
652
 
611
653
  // check that the inner driver has a proxy function
612
654
  if (!driver.canProxy(sessionId)) {
@@ -617,8 +659,10 @@ async function doJwpProxy(driver: BaseDriver<any>, req: Request, res: Response):
617
659
  } catch (err) {
618
660
  if (isErrorType(err, errors.ProxyRequestError)) {
619
661
  throw err;
620
- } else {
662
+ }
663
+ if (err instanceof Error) {
621
664
  throw new Error(`Could not proxy. Proxy error: ${err.message}`, {cause: err});
622
665
  }
666
+ throw new Error(`Could not proxy. Proxy error: ${String(err)}`, {cause: err});
623
667
  }
624
668
  }
@@ -14,7 +14,6 @@ const COMMAND_NAMES_CACHE = new LRUCache<string, string>({
14
14
  * `optional`.
15
15
  */
16
16
  export const METHOD_MAP = {
17
-
18
17
  // #region W3C WebDriver
19
18
  // https://www.w3.org/TR/webdriver2/
20
19
  '/session': {
@@ -244,8 +243,8 @@ export const METHOD_MAP = {
244
243
  'shrinkToFit',
245
244
  'pageRanges',
246
245
  ],
247
- }
248
- }
246
+ },
247
+ },
249
248
  },
250
249
  // #endregion
251
250
 
@@ -274,7 +273,7 @@ export const METHOD_MAP = {
274
273
  GET: {command: 'getOrientation'},
275
274
  POST: {
276
275
  command: 'setOrientation',
277
- payloadParams: {required: ['orientation']}
276
+ payloadParams: {required: ['orientation']},
278
277
  },
279
278
  },
280
279
  '/session/:sessionId/location': {
@@ -318,7 +317,7 @@ export const METHOD_MAP = {
318
317
  GET: {command: 'getAppiumSessions'},
319
318
  },
320
319
  '/session/:sessionId/appium/capabilities': {
321
- GET: {command: 'getAppiumSessionCapabilities'}
320
+ GET: {command: 'getAppiumSessionCapabilities'},
322
321
  },
323
322
  '/session/:sessionId/appium/settings': {
324
323
  POST: {command: 'updateSettings', payloadParams: {required: ['settings']}},
@@ -592,7 +591,7 @@ export const ALL_COMMANDS = Object.values(METHOD_MAP)
592
591
  export function routeToCommandName(
593
592
  endpoint: string,
594
593
  method?: HTTPMethod,
595
- basePath?: string
594
+ basePath?: string,
596
595
  ): string | undefined {
597
596
  const resolvedBasePath = basePath ?? DEFAULT_BASE_PATH;
598
597
  let normalizedEndpoint = resolvedBasePath
@@ -624,9 +623,10 @@ export function routeToCommandName(
624
623
  const routeMatcher = match(routePath);
625
624
  if (possiblePathnames.some((pp) => routeMatcher(pp))) {
626
625
  const spec = routeSpec as Record<string, DriverMethodDef<Driver>>;
627
- const commandForAnyMethod = () =>
628
- Object.keys(spec).map((key) => spec[key]?.command)[0];
629
- const commandName = normalizedMethod ? spec[normalizedMethod]?.command : commandForAnyMethod();
626
+ const commandForAnyMethod = () => Object.keys(spec).map((key) => spec[key]?.command)[0];
627
+ const commandName = normalizedMethod
628
+ ? spec[normalizedMethod]?.command
629
+ : commandForAnyMethod();
630
630
  if (commandName) {
631
631
  COMMAND_NAMES_CACHE.set(cacheKey, commandName);
632
632
  return commandName;
@@ -0,0 +1,9 @@
1
+ /** Environment variable that opts into deprecated built-in test pages on the Appium server. */
2
+ export const LEGACY_TEST_PAGES_ENV = 'APPIUM_ENABLE_LEGACY_TEST_PAGES';
3
+
4
+ const TRUTHY = new Set(['1', 'true', 'yes']);
5
+
6
+ /** @returns Whether built-in legacy test pages should be mounted on the Appium server. */
7
+ export function isLegacyTestPagesEnabled(): boolean {
8
+ return TRUTHY.has(String(process.env[LEGACY_TEST_PAGES_ENV] ?? '').toLowerCase());
9
+ }
@@ -1,11 +1,10 @@
1
1
  import path from 'node:path';
2
- import {log} from './logger';
2
+ import {log} from '../express/logger';
3
3
  import {fs} from '@appium/support';
4
4
  import {sleep} from 'asyncbox';
5
5
  import type {Request, Response} from 'express';
6
- import {compileLodashTemplate} from '../utils';
7
-
8
- export const STATIC_DIR = resolveStaticDir();
6
+ import {compileLodashTemplate} from './template';
7
+ import {TEST_FIXTURES_DIR} from './static-dir';
9
8
 
10
9
  type TemplateParams = Record<string, unknown>;
11
10
 
@@ -32,15 +31,8 @@ export async function welcome(req: Request, res: Response): Promise<void> {
32
31
  res.send(template(params));
33
32
  }
34
33
 
35
- async function guineaPigTemplate(
36
- req: Request,
37
- res: Response,
38
- page: string
39
- ): Promise<void> {
40
- const delay = parseInt(
41
- String(req.params.delay ?? (req.query?.delay ?? 0)),
42
- 10
43
- );
34
+ async function guineaPigTemplate(req: Request, res: Response, page: string): Promise<void> {
35
+ const delay = parseInt(String(req.params.delay ?? req.query?.delay ?? 0), 10);
44
36
  const throwError = String(req.params.throwError ?? req.query?.throwError ?? '');
45
37
  const params: TemplateParams = {
46
38
  throwError,
@@ -67,19 +59,7 @@ async function guineaPigTemplate(
67
59
  res.send(template(params));
68
60
  }
69
61
 
70
- async function getTemplate(
71
- templateName: string
72
- ): Promise<(params: TemplateParams) => string> {
73
- const content = await fs.readFile(path.resolve(STATIC_DIR, 'test', templateName));
62
+ async function getTemplate(templateName: string): Promise<(params: TemplateParams) => string> {
63
+ const content = await fs.readFile(path.resolve(TEST_FIXTURES_DIR, 'test', templateName));
74
64
  return compileLodashTemplate(content.toString());
75
65
  }
76
-
77
- function resolveStaticDir(): string {
78
- const fromDir = __dirname;
79
- const parts = path.resolve(fromDir).split(path.sep);
80
- const baseDriverIndex = parts.indexOf('base-driver');
81
- if (baseDriverIndex < 0) {
82
- throw new Error(`Could not find the module root folder in the path: ${fromDir}`);
83
- }
84
- return path.join(parts.slice(0, baseDriverIndex + 1).join(path.sep), 'static');
85
- }
@@ -0,0 +1,34 @@
1
+ import path from 'node:path';
2
+ import express from 'express';
3
+ import type {Express} from 'express';
4
+ import favicon from 'serve-favicon';
5
+ import {guineaPig, guineaPigScrollable, guineaPigAppBanner, welcome} from './handlers';
6
+ import {produceError, produceCrash} from './crash';
7
+ import {TEST_FIXTURES_DIR} from './static-dir';
8
+
9
+ export interface RegisterTestPagesOpts {
10
+ basePath: string;
11
+ }
12
+
13
+ /**
14
+ * Mount deprecated built-in test pages and crash routes on an Express app.
15
+ *
16
+ * @deprecated Built-in test pages on the Appium server are deprecated and will be removed in
17
+ * Appium 4. Driver CI should hard-copy needed fixtures and run a local test HTTP server.
18
+ * @internal
19
+ */
20
+ export function registerTestPages(app: Express, {basePath}: RegisterTestPagesOpts): void {
21
+ app.use(favicon(path.resolve(TEST_FIXTURES_DIR, 'favicon.ico')));
22
+ app.use(express.static(TEST_FIXTURES_DIR));
23
+
24
+ app.use(`${basePath}/produce_error`, produceError);
25
+ app.use(`${basePath}/crash`, produceCrash);
26
+
27
+ app.all('/welcome', welcome);
28
+ app.all('/test/guinea-pig', guineaPig);
29
+ app.all('/test/guinea-pig-scrollable', guineaPigScrollable);
30
+ app.all('/test/guinea-pig-app-banner', guineaPigAppBanner);
31
+ }
32
+
33
+ export {TEST_FIXTURES_DIR} from './static-dir';
34
+ export {isLegacyTestPagesEnabled} from './env';
@@ -0,0 +1,19 @@
1
+ import path from 'node:path';
2
+
3
+ /**
4
+ * Absolute path to bundled legacy test fixture static files.
5
+ *
6
+ * @deprecated Removed in Appium 4. Test fixture files will live in the appium/test-fixtures
7
+ * repository. Do not depend on this path in driver CI — hard-copy needed files locally.
8
+ */
9
+ export const TEST_FIXTURES_DIR = resolveTestFixturesDir();
10
+
11
+ function resolveTestFixturesDir(): string {
12
+ const fromDir = __dirname;
13
+ const parts = path.resolve(fromDir).split(path.sep);
14
+ const baseDriverIndex = parts.indexOf('base-driver');
15
+ if (baseDriverIndex < 0) {
16
+ throw new Error(`Could not find the module root folder in the path: ${fromDir}`);
17
+ }
18
+ return path.join(parts.slice(0, baseDriverIndex + 1).join(path.sep), 'test-fixtures', 'static');
19
+ }
@@ -0,0 +1,17 @@
1
+ /** Compile a lodash-style template string (`<%= expression %>`) into a render function. */
2
+ export function compileLodashTemplate(
3
+ template: string,
4
+ ): (params: Record<string, unknown>) => string {
5
+ const parts: string[] = [];
6
+ let lastIndex = 0;
7
+ const re = /<%=\s*([\s\S]+?)\s*%>/g;
8
+ let match;
9
+ while ((match = re.exec(template)) !== null) {
10
+ parts.push(JSON.stringify(template.slice(lastIndex, match.index)));
11
+ parts.push(`String(${match[1]})`);
12
+ lastIndex = match.index + match[0].length;
13
+ }
14
+ parts.push(JSON.stringify(template.slice(lastIndex)));
15
+ const fn = new Function('obj', `with (obj) { return ${parts.join(' + ')}; }`);
16
+ return (params) => fn(params) as string;
17
+ }
package/lib/utils.ts CHANGED
@@ -48,7 +48,7 @@ export function omitKeys<T extends Record<string, unknown>>(obj: T, keys: readon
48
48
  /** Return a shallow copy of `obj` containing only listed keys. */
49
49
  export function pick<T extends Record<string, unknown>>(
50
50
  obj: T,
51
- keys: readonly string[]
51
+ keys: readonly string[],
52
52
  ): Partial<T> {
53
53
  const keysToPick = new Set(keys);
54
54
  return Object.fromEntries(Object.entries(obj).filter(([k]) => keysToPick.has(k))) as Partial<T>;
@@ -57,29 +57,9 @@ export function pick<T extends Record<string, unknown>>(
57
57
  /** Return a shallow copy of `obj` whose entries pass `predicate`. */
58
58
  export function pickBy<T extends Record<string, unknown>>(
59
59
  obj: T,
60
- predicate: (value: T[keyof T], key: keyof T) => boolean
60
+ predicate: (value: T[keyof T], key: keyof T) => boolean,
61
61
  ): Partial<T> {
62
62
  return Object.fromEntries(
63
- Object.entries(obj).filter(([key, value]) =>
64
- predicate(value as T[keyof T], key as keyof T)
65
- )
63
+ Object.entries(obj).filter(([key, value]) => predicate(value as T[keyof T], key as keyof T)),
66
64
  ) as Partial<T>;
67
65
  }
68
-
69
- /** Compile a lodash-style template string (`<%= expression %>`) into a render function. */
70
- export function compileLodashTemplate(
71
- template: string
72
- ): (params: Record<string, unknown>) => string {
73
- const parts: string[] = [];
74
- let lastIndex = 0;
75
- const re = /<%=\s*([\s\S]+?)\s*%>/g;
76
- let match;
77
- while ((match = re.exec(template)) !== null) {
78
- parts.push(JSON.stringify(template.slice(lastIndex, match.index)));
79
- parts.push(`String(${match[1]})`);
80
- lastIndex = match.index + match[0].length;
81
- }
82
- parts.push(JSON.stringify(template.slice(lastIndex)));
83
- const fn = new Function('obj', `with (obj) { return ${parts.join(' + ')}; }`);
84
- return (params) => fn(params) as string;
85
- }