@appium/base-driver 10.2.2 → 10.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/build/lib/basedriver/capabilities.d.ts +4 -0
  2. package/build/lib/basedriver/capabilities.d.ts.map +1 -1
  3. package/build/lib/basedriver/capabilities.js +4 -0
  4. package/build/lib/basedriver/capabilities.js.map +1 -1
  5. package/build/lib/basedriver/commands/execute.js +7 -17
  6. package/build/lib/basedriver/commands/execute.js.map +1 -1
  7. package/build/lib/basedriver/core.d.ts +24 -24
  8. package/build/lib/basedriver/core.d.ts.map +1 -1
  9. package/build/lib/basedriver/core.js +29 -29
  10. package/build/lib/basedriver/core.js.map +1 -1
  11. package/build/lib/basedriver/driver.d.ts.map +1 -1
  12. package/build/lib/basedriver/driver.js +7 -2
  13. package/build/lib/basedriver/driver.js.map +1 -1
  14. package/build/lib/basedriver/extension-core.d.ts +1 -1
  15. package/build/lib/basedriver/extension-core.d.ts.map +1 -1
  16. package/build/lib/basedriver/extension-core.js +1 -1
  17. package/build/lib/basedriver/extension-core.js.map +1 -1
  18. package/build/lib/basedriver/helpers.d.ts.map +1 -1
  19. package/build/lib/basedriver/helpers.js +2 -2
  20. package/build/lib/basedriver/helpers.js.map +1 -1
  21. package/build/lib/helpers/levenshtein-match.d.ts +27 -0
  22. package/build/lib/helpers/levenshtein-match.d.ts.map +1 -0
  23. package/build/lib/helpers/levenshtein-match.js +61 -0
  24. package/build/lib/helpers/levenshtein-match.js.map +1 -0
  25. package/build/lib/jsonwp-proxy/protocol-converter.d.ts +1 -1
  26. package/build/lib/jsonwp-proxy/protocol-converter.d.ts.map +1 -1
  27. package/build/lib/jsonwp-proxy/protocol-converter.js +3 -3
  28. package/build/lib/jsonwp-proxy/protocol-converter.js.map +1 -1
  29. package/build/lib/jsonwp-proxy/proxy.d.ts +53 -98
  30. package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
  31. package/build/lib/jsonwp-proxy/proxy.js +102 -145
  32. package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
  33. package/build/lib/protocol/errors.d.ts +45 -45
  34. package/build/lib/protocol/errors.d.ts.map +1 -1
  35. package/build/lib/protocol/errors.js +162 -162
  36. package/build/lib/protocol/errors.js.map +1 -1
  37. package/build/lib/protocol/protocol.d.ts +35 -0
  38. package/build/lib/protocol/protocol.d.ts.map +1 -1
  39. package/build/lib/protocol/protocol.js +105 -77
  40. package/build/lib/protocol/protocol.js.map +1 -1
  41. package/build/lib/protocol/routes.d.ts +6 -0
  42. package/build/lib/protocol/routes.d.ts.map +1 -1
  43. package/build/lib/protocol/routes.js +6 -0
  44. package/build/lib/protocol/routes.js.map +1 -1
  45. package/lib/basedriver/capabilities.ts +4 -0
  46. package/lib/basedriver/commands/execute.ts +7 -18
  47. package/lib/basedriver/core.ts +34 -34
  48. package/lib/basedriver/driver.ts +9 -2
  49. package/lib/basedriver/extension-core.ts +1 -1
  50. package/lib/basedriver/helpers.ts +21 -21
  51. package/lib/helpers/levenshtein-match.ts +74 -0
  52. package/lib/jsonwp-proxy/protocol-converter.ts +4 -4
  53. package/lib/jsonwp-proxy/proxy.ts +506 -0
  54. package/lib/protocol/errors.ts +281 -246
  55. package/lib/protocol/protocol.ts +121 -93
  56. package/lib/protocol/routes.ts +6 -0
  57. package/package.json +10 -10
  58. package/tsconfig.json +6 -0
  59. package/lib/jsonwp-proxy/proxy.js +0 -493
@@ -8,9 +8,9 @@ import type {
8
8
  IExecuteCommands,
9
9
  StringRecord,
10
10
  } from '@appium/types';
11
+ import {rankLevenshteinCandidates} from '../../helpers/levenshtein-match';
11
12
  import {mixin} from './mixin';
12
13
  import type {BaseDriver} from '../driver';
13
- import {distance} from 'fastest-levenshtein';
14
14
 
15
15
  declare module '../driver' {
16
16
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -34,30 +34,19 @@ const ExecuteCommands: IExecuteCommands = {
34
34
  `The current driver version does not define any execute methods.`
35
35
  );
36
36
  }
37
- const matchesMap: StringRecord<string[]> = availableScripts
38
- .map((name) => [distance(script, name), name])
39
- .reduce((acc, [key, value]) => {
40
- if (key in acc) {
41
- acc[key].push(value);
42
- } else {
43
- acc[key] = [value];
44
- }
45
- return acc;
46
- }, {});
47
- const sortedMatches = _.flatten(
48
- _.keys(matchesMap)
49
- .sort((a, b) => parseInt(a, 10) - parseInt(b, 10))
50
- .map((x) => matchesMap[x])
51
- );
37
+ const {sorted: sortedMatches, suggestion} = rankLevenshteinCandidates(script, availableScripts);
52
38
  throw new errors.UnsupportedOperationError(
53
- `Unsupported execute method '${script}', did you mean '${sortedMatches[0]}'? ` +
39
+ (suggestion
40
+ ? `Unsupported execute method '${script}', did you mean '${suggestion}'? `
41
+ : `Unsupported execute method '${script}'. `) +
54
42
  `Make sure the installed ${Driver.name} is up-to-date. ` +
55
43
  `Execute methods available in the current driver version are: ` +
56
44
  sortedMatches.join(', ')
57
45
  );
58
46
  }
59
47
  const args = validateExecuteMethodParams(protoArgs as any[], commandMetadata.params);
60
- const command = this[commandMetadata.command] as DriverCommand;
48
+ const commandName = commandMetadata.command as keyof BaseDriver<C>;
49
+ const command = this[commandName] as DriverCommand;
61
50
  return await command.call(this, ...args);
62
51
  },
63
52
  };
@@ -77,19 +77,10 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
77
77
 
78
78
  noCommandTimer: NodeJS.Timeout | null;
79
79
 
80
- protected _eventHistory: EventHistory;
81
-
82
- /**
83
- * TODO: remove this._log and use this.log instead
84
- */
85
- protected _log: AppiumLogger;
86
-
87
80
  shutdownUnexpectedly: boolean;
88
81
 
89
82
  shouldValidateCaps: boolean;
90
83
 
91
- protected commandsQueueGuard: AsyncLock;
92
-
93
84
  /**
94
85
  * settings should be instantiated by drivers which extend BaseDriver, but
95
86
  * we set it to an empty DeviceSettings instance here to make sure that the
@@ -100,6 +91,15 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
100
91
 
101
92
  protocol?: Protocol;
102
93
 
94
+ protected _eventHistory: EventHistory;
95
+
96
+ /**
97
+ * TODO: remove this._log and use this.log instead
98
+ */
99
+ protected _log: AppiumLogger;
100
+
101
+ protected commandsQueueGuard: AsyncLock;
102
+
103
103
  constructor(opts: InitialOpts = <InitialOpts>{}, shouldValidateCaps = true) {
104
104
  super();
105
105
  this._log = this.log; // TODO: remove references to this._log and use this.log instead
@@ -136,19 +136,6 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
136
136
  this.settings = new DeviceSettings();
137
137
  }
138
138
 
139
- /**
140
- * Set a callback handler if needed to execute a custom piece of code
141
- * when the driver is shut down unexpectedly. Multiple calls to this method
142
- * will cause the handler to be executed multiple times
143
- *
144
- * @param handler The code to be executed on unexpected shutdown.
145
- * The function may accept one argument, which is the actual error instance, which
146
- * caused the driver to shut down.
147
- */
148
- onUnexpectedShutdown(handler: (...args: any[]) => void) {
149
- this.eventEmitter.on(ON_UNEXPECTED_SHUTDOWN_EVENT, handler);
150
- }
151
-
152
139
  /**
153
140
  * This property is used by AppiumDriver to store the data of the
154
141
  * specific driver sessions. This data can be later used to adjust
@@ -182,6 +169,31 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
182
169
  return _.cloneDeep(this._eventHistory);
183
170
  }
184
171
 
172
+ /**
173
+ * If this driver has requested proxying of bidi connections to an upstream bidi endpoint, this
174
+ * method should be overridden to return the URL of that websocket, to indicate that bidi
175
+ * proxying is enabled. Otherwise, a null return will indicate that bidi proxying should not be
176
+ * active and bidi commands will be handled by this driver.
177
+ *
178
+ * @returns {string | null}
179
+ */
180
+ get bidiProxyUrl(): string | null {
181
+ return null;
182
+ }
183
+
184
+ /**
185
+ * Set a callback handler if needed to execute a custom piece of code
186
+ * when the driver is shut down unexpectedly. Multiple calls to this method
187
+ * will cause the handler to be executed multiple times
188
+ *
189
+ * @param handler The code to be executed on unexpected shutdown.
190
+ * The function may accept one argument, which is the actual error instance, which
191
+ * caused the driver to shut down.
192
+ */
193
+ onUnexpectedShutdown(handler: (...args: any[]) => void) {
194
+ this.eventEmitter.on(ON_UNEXPECTED_SHUTDOWN_EVENT, handler);
195
+ }
196
+
185
197
  /**
186
198
  * API method for driver developers to log timings for important events
187
199
  */
@@ -327,18 +339,6 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
327
339
  }
328
340
  }
329
341
 
330
- /**
331
- * If this driver has requested proxying of bidi connections to an upstream bidi endpoint, this
332
- * method should be overridden to return the URL of that websocket, to indicate that bidi
333
- * proxying is enabled. Otherwise, a null return will indicate that bidi proxying should not be
334
- * active and bidi commands will be handled by this driver.
335
- *
336
- * @returns {string | null}
337
- */
338
- get bidiProxyUrl(): string | null {
339
- return null;
340
- }
341
-
342
342
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
343
343
  proxyActive(sessionId: string): boolean {
344
344
  return false;
@@ -20,6 +20,7 @@ import {
20
20
  import B from 'bluebird';
21
21
  import _ from 'lodash';
22
22
  import {fixCaps, isW3cCaps} from '../helpers/capabilities';
23
+ import {getLevenshteinSuggestion} from '../helpers/levenshtein-match';
23
24
  import {calcSignature} from '../helpers/session';
24
25
  import {DELETE_SESSION_COMMAND, determineProtocol, errors} from '../protocol';
25
26
  import {processCapabilities, validateCaps} from './capabilities';
@@ -396,11 +397,17 @@ export class BaseDriver<
396
397
  }
397
398
 
398
399
  logExtraCaps(caps: Capabilities<C>) {
399
- const extraCaps = _.difference(_.keys(caps), _.keys(this._desiredCapConstraints));
400
+ const knownCaps = _.keys(this._desiredCapConstraints);
401
+ const extraCaps = _.difference(_.keys(caps), knownCaps);
400
402
  if (extraCaps.length) {
401
403
  this.log.warn(`The following provided capabilities were not recognized by this driver:`);
402
404
  for (const cap of extraCaps) {
403
- this.log.warn(` ${cap}`);
405
+ const suggestion = getLevenshteinSuggestion(cap, knownCaps);
406
+ this.log.warn(
407
+ suggestion
408
+ ? ` ${cap} (did you mean '${suggestion}'?)`
409
+ : ` ${cap}`,
410
+ );
404
411
  }
405
412
  }
406
413
  }
@@ -18,9 +18,9 @@ export class ExtensionCore {
18
18
  bidiEventSubs: Record<string, string[]>;
19
19
  bidiCommands: BidiModuleMap = BIDI_COMMANDS as BidiModuleMap;
20
20
  _logPrefix?: string;
21
- protected _log: AppiumLogger;
22
21
  // used to handle driver events
23
22
  readonly eventEmitter: NodeJS.EventEmitter;
23
+ protected _log: AppiumLogger;
24
24
 
25
25
 
26
26
  constructor(logPrefix?: string) {
@@ -36,7 +36,7 @@ const APPLICATIONS_CACHE = new LRUCache<string, CachedAppInfoEntry>({
36
36
  `expired after ${CACHED_APPS_MAX_AGE_MS}ms`
37
37
  );
38
38
  if (fullPath) {
39
- fs.rimraf(fullPath);
39
+ void fs.rimraf(fullPath);
40
40
  }
41
41
  },
42
42
  noDisposeOnSet: true,
@@ -67,6 +67,25 @@ process.on('exit', () => {
67
67
  }
68
68
  });
69
69
 
70
+ interface RemoteAppProps {
71
+ lastModified: Date | null;
72
+ immutable: boolean;
73
+ maxAge: number | null;
74
+ etag: string | null;
75
+ }
76
+
77
+ interface RemoteAppData {
78
+ status: number;
79
+ stream: Readable;
80
+ headers: AxiosResponseHeaders | RawAxiosRequestHeaders;
81
+ }
82
+
83
+ /** Cache value we store (extends CachedAppInfo with optional packageHash) */
84
+ interface CachedAppInfoEntry extends Omit<CachedAppInfo, 'packageHash'> {
85
+ packageHash?: string | null;
86
+ fullPath?: string;
87
+ }
88
+
70
89
  /**
71
90
  * Performs initial application package configuration so the app is ready for driver use.
72
91
  * Resolves local paths, downloads remote apps (http/https) with optional caching, and
@@ -361,19 +380,7 @@ export function generateDriverLogPrefix(obj: object | null, _sessionId?: string
361
380
  return `${obj.constructor.name}@${node.getObjectId(obj).substring(0, 4)}`;
362
381
  }
363
382
 
364
- // #region Private types and helpers
365
- interface RemoteAppProps {
366
- lastModified: Date | null;
367
- immutable: boolean;
368
- maxAge: number | null;
369
- etag: string | null;
370
- }
371
-
372
- interface RemoteAppData {
373
- status: number;
374
- stream: Readable;
375
- headers: AxiosResponseHeaders | RawAxiosRequestHeaders;
376
- }
383
+ // #region Private helpers
377
384
 
378
385
  function parseAppLink(appLink: string): URL | {protocol?: string; pathname?: string; href?: string; search?: string} {
379
386
  try {
@@ -562,13 +569,6 @@ function toNaturalNumber(defaultValue: number, envVarName?: string): number {
562
569
  return num > 0 ? num : defaultValue;
563
570
  }
564
571
 
565
- /** Cache value we store (extends CachedAppInfo with optional packageHash) */
566
- interface CachedAppInfoEntry extends Omit<CachedAppInfo, 'packageHash'> {
567
- packageHash?: string | null;
568
- fullPath?: string;
569
- }
570
- // #endregion
571
-
572
572
  export default {
573
573
  configureApp,
574
574
  isPackageOrBundle,
@@ -0,0 +1,74 @@
1
+ import type {StringRecord} from '@appium/types';
2
+ import {distance} from 'fastest-levenshtein';
3
+ import _ from 'lodash';
4
+
5
+ /**
6
+ * Inclusive maximum Levenshtein edit distance for offering a "did you mean" hint.
7
+ * Matches with distance greater than this value are treated as unrelated.
8
+ */
9
+ export const LEVENSHTEIN_SUGGESTION_MAX_EDIT_DISTANCE = 2;
10
+
11
+ export interface LevenshteinRankResult {
12
+ /** Candidates sorted by ascending edit distance from `target`, then alphabetically within ties. */
13
+ sorted: string[];
14
+ /** Closest name only if its edit distance is at most `maxEditDistance` (inclusive). */
15
+ suggestion: string | undefined;
16
+ }
17
+
18
+ /**
19
+ * Sorts candidates by Levenshtein distance from `target` and optionally picks a suggestion
20
+ * when the closest match is within `maxEditDistance` edits (single pass over candidates).
21
+ */
22
+ export function rankLevenshteinCandidates(
23
+ target: string,
24
+ candidates: readonly string[],
25
+ maxEditDistance: number = LEVENSHTEIN_SUGGESTION_MAX_EDIT_DISTANCE,
26
+ ): LevenshteinRankResult {
27
+ if (!candidates.length) {
28
+ return {sorted: [], suggestion: undefined};
29
+ }
30
+
31
+ const matchesMap: StringRecord<string[]> = candidates
32
+ .map((name) => [distance(target, name), name] as const)
33
+ .reduce((acc, [dist, name]) => {
34
+ const key = String(dist);
35
+ if (key in acc) {
36
+ acc[key].push(name);
37
+ } else {
38
+ acc[key] = [name];
39
+ }
40
+ return acc;
41
+ }, {});
42
+ const sortedDistanceKeys = _.keys(matchesMap).sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
43
+ const sorted = _.flatten(
44
+ sortedDistanceKeys.map((k) => (matchesMap[k] ?? []).sort()),
45
+ );
46
+
47
+ const best = sorted[0];
48
+ const firstDistanceKey = sortedDistanceKeys[0];
49
+ const minDist = firstDistanceKey !== undefined ? parseInt(firstDistanceKey, 10) : NaN;
50
+ const suggestion = maxEditDistance >= 0 && best !== undefined && !Number.isNaN(minDist) && minDist <= maxEditDistance
51
+ ? best
52
+ : undefined;
53
+ return {sorted, suggestion};
54
+ }
55
+
56
+ /**
57
+ * Sorts strings by ascending Levenshtein distance from `target`.
58
+ * Strings with the same distance are sorted alphabetically.
59
+ */
60
+ export function sortByLevenshteinDistance(target: string, candidates: readonly string[]): string[] {
61
+ return rankLevenshteinCandidates(target, candidates, Number.POSITIVE_INFINITY).sorted;
62
+ }
63
+
64
+ /**
65
+ * Returns the closest string in `candidates` by Levenshtein distance only if that
66
+ * distance is at most `maxEditDistance` (inclusive).
67
+ */
68
+ export function getLevenshteinSuggestion(
69
+ target: string,
70
+ candidates: readonly string[],
71
+ maxEditDistance: number = LEVENSHTEIN_SUGGESTION_MAX_EDIT_DISTANCE,
72
+ ): string | undefined {
73
+ return rankLevenshteinCandidates(target, candidates, maxEditDistance).suggestion;
74
+ }
@@ -69,14 +69,14 @@ export class ProtocolConverter {
69
69
  return this._log ?? DEFAULT_LOG;
70
70
  }
71
71
 
72
- set downstreamProtocol(value: string | null | undefined) {
73
- this._downstreamProtocol = value;
74
- }
75
-
76
72
  get downstreamProtocol(): string | null | undefined {
77
73
  return this._downstreamProtocol;
78
74
  }
79
75
 
76
+ set downstreamProtocol(value: string | null | undefined) {
77
+ this._downstreamProtocol = value;
78
+ }
79
+
80
80
  /**
81
81
  * Handle "crossing" endpoints for the case when upstream and downstream
82
82
  * drivers operate different protocols.