appium 2.0.0-beta.2 → 2.0.0-beta.20

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 (70) hide show
  1. package/README.md +9 -9
  2. package/build/lib/appium-config.schema.json +0 -0
  3. package/build/lib/appium.js +157 -53
  4. package/build/lib/cli/argparse-actions.js +104 -0
  5. package/build/lib/cli/args.js +115 -279
  6. package/build/lib/cli/driver-command.js +11 -1
  7. package/build/lib/cli/extension-command.js +60 -8
  8. package/build/lib/cli/extension.js +30 -7
  9. package/build/lib/cli/npm.js +17 -14
  10. package/build/lib/cli/parser.js +152 -89
  11. package/build/lib/cli/plugin-command.js +11 -1
  12. package/build/lib/cli/utils.js +29 -3
  13. package/build/lib/config-file.js +141 -0
  14. package/build/lib/config.js +76 -61
  15. package/build/lib/driver-config.js +42 -19
  16. package/build/lib/drivers.js +8 -4
  17. package/build/lib/ext-config-io.js +165 -0
  18. package/build/lib/extension-config.js +130 -61
  19. package/build/lib/grid-register.js +22 -24
  20. package/build/lib/logger.js +3 -3
  21. package/build/lib/logsink.js +11 -13
  22. package/build/lib/main.js +197 -77
  23. package/build/lib/plugin-config.js +20 -10
  24. package/build/lib/plugins.js +4 -2
  25. package/build/lib/schema/appium-config-schema.js +252 -0
  26. package/build/lib/schema/arg-spec.js +120 -0
  27. package/build/lib/schema/cli-args.js +173 -0
  28. package/build/lib/schema/cli-transformers.js +76 -0
  29. package/build/lib/schema/index.js +36 -0
  30. package/build/lib/schema/keywords.js +62 -0
  31. package/build/lib/schema/schema.js +357 -0
  32. package/build/lib/utils.js +44 -99
  33. package/lib/appium-config.schema.json +277 -0
  34. package/lib/appium.js +201 -65
  35. package/lib/cli/argparse-actions.js +77 -0
  36. package/lib/cli/args.js +174 -375
  37. package/lib/cli/driver-command.js +4 -0
  38. package/lib/cli/extension-command.js +70 -5
  39. package/lib/cli/extension.js +25 -5
  40. package/lib/cli/npm.js +18 -12
  41. package/lib/cli/parser.js +254 -79
  42. package/lib/cli/plugin-command.js +4 -0
  43. package/lib/cli/utils.js +21 -1
  44. package/lib/config-file.js +227 -0
  45. package/lib/config.js +109 -62
  46. package/lib/driver-config.js +66 -11
  47. package/lib/drivers.js +4 -1
  48. package/lib/ext-config-io.js +287 -0
  49. package/lib/extension-config.js +225 -67
  50. package/lib/grid-register.js +27 -24
  51. package/lib/logger.js +1 -1
  52. package/lib/logsink.js +10 -7
  53. package/lib/main.js +211 -77
  54. package/lib/plugin-config.js +34 -5
  55. package/lib/plugins.js +1 -0
  56. package/lib/schema/appium-config-schema.js +286 -0
  57. package/lib/schema/arg-spec.js +218 -0
  58. package/lib/schema/cli-args.js +273 -0
  59. package/lib/schema/cli-transformers.js +123 -0
  60. package/lib/schema/index.js +2 -0
  61. package/lib/schema/keywords.js +119 -0
  62. package/lib/schema/schema.js +577 -0
  63. package/lib/utils.js +42 -88
  64. package/package.json +55 -80
  65. package/postinstall.js +71 -0
  66. package/types/appium-config.d.ts +197 -0
  67. package/types/types.d.ts +201 -0
  68. package/CHANGELOG.md +0 -3515
  69. package/build/lib/cli/parser-helpers.js +0 -82
  70. package/lib/cli/parser-helpers.js +0 -79
@@ -0,0 +1,227 @@
1
+ // @ts-check
2
+
3
+ import betterAjvErrors from '@sidvind/better-ajv-errors';
4
+ import { lilconfig } from 'lilconfig';
5
+ import _ from 'lodash';
6
+ import yaml from 'yaml';
7
+ import { getSchema, validate } from './schema/schema';
8
+
9
+ /**
10
+ * lilconfig loader to handle `.yaml` files
11
+ * @type {import('lilconfig').LoaderSync}
12
+ */
13
+ function yamlLoader (filepath, content) {
14
+ return yaml.parse(content);
15
+ }
16
+
17
+ /**
18
+ * A cache of the raw config file (a JSON string) at a filepath.
19
+ * This is used for better error reporting.
20
+ * Note that config files needn't be JSON, but it helps if they are.
21
+ * @type {Map<string,RawJson>}
22
+ */
23
+ const rawConfig = new Map();
24
+
25
+ /**
26
+ * Custom JSON loader that caches the raw config file (for use with `better-ajv-errors`).
27
+ * If it weren't for this cache, this would be unnecessary.
28
+ * @type {import('lilconfig').LoaderSync}
29
+ */
30
+ function jsonLoader (filepath, content) {
31
+ rawConfig.set(filepath, content);
32
+ return JSON.parse(content);
33
+ }
34
+
35
+ /**
36
+ * Loads a config file from an explicit path
37
+ * @param {LilconfigAsyncSearcher} lc - lilconfig instance
38
+ * @param {string} filepath - Path to config file
39
+ * @returns {Promise<import('lilconfig').LilconfigResult>}
40
+ */
41
+ async function loadConfigFile (lc, filepath) {
42
+ try {
43
+ // removing "await" will cause any rejection to _not_ be caught in this block!
44
+ return await lc.load(filepath);
45
+ } catch (/** @type {unknown} */err) {
46
+ if (/** @type {NodeJS.ErrnoException} */(err).code === 'ENOENT') {
47
+ /** @type {NodeJS.ErrnoException} */(err).message = `Config file not found at user-provided path: ${filepath}`;
48
+ throw err;
49
+ } else if (err instanceof SyntaxError) {
50
+ // generally invalid JSON
51
+ err.message = `Config file at user-provided path ${filepath} is invalid:\n${err.message}`;
52
+ throw err;
53
+ }
54
+ throw err;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Searches for a config file
60
+ * @param {LilconfigAsyncSearcher} lc - lilconfig instance
61
+ * @returns {Promise<import('lilconfig').LilconfigResult>}
62
+ */
63
+ async function searchConfigFile (lc) {
64
+ return await lc.search();
65
+ }
66
+
67
+ /**
68
+ * Given an array of errors and the result of loading a config file, generate a
69
+ * helpful string for the user.
70
+ *
71
+ * - If `opts` contains a `json` property, this should be the original JSON
72
+ * _string_ of the config file. This is only applicable if the config file
73
+ * was in JSON format. If present, it will associate line numbers with errors.
74
+ * - If `errors` happens to be empty, this will throw.
75
+ * @param {import('ajv').ErrorObject[]} errors - Non-empty array of errors. Required.
76
+ * @param {ReadConfigFileResult['config']|any} [config] -
77
+ * Configuration & metadata
78
+ * @param {FormatConfigErrorsOptions} [opts]
79
+ * @throws {TypeError} If `errors` is empty
80
+ * @returns {string}
81
+ */
82
+ export function formatErrors (errors = [], config = {}, opts = {}) {
83
+ if (errors && !errors.length) {
84
+ throw new TypeError('Array of errors must be non-empty');
85
+ }
86
+ return betterAjvErrors(getSchema(opts.schemaId), config, errors, {
87
+ json: opts.json,
88
+ format: 'cli',
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Given an optional path, read a config file. Validates the config file.
94
+ *
95
+ * Call {@link validate} if you already have a config object.
96
+ * @param {string} [filepath] - Path to config file, if we have one
97
+ * @param {ReadConfigFileOptions} [opts] - Options
98
+ * @public
99
+ * @returns {Promise<ReadConfigFileResult>} Contains config and filepath, if found, and any errors
100
+ */
101
+ export async function readConfigFile (filepath, opts = {}) {
102
+ const lc = lilconfig('appium', {
103
+ loaders: {
104
+ '.yaml': yamlLoader,
105
+ '.yml': yamlLoader,
106
+ '.json': jsonLoader,
107
+ noExt: jsonLoader,
108
+ },
109
+ packageProp: 'appiumConfig'
110
+ });
111
+
112
+ const result = filepath
113
+ ? await loadConfigFile(lc, filepath)
114
+ : await searchConfigFile(lc);
115
+
116
+ if (result && !result.isEmpty && result.filepath) {
117
+ const {normalize = true, pretty = true} = opts;
118
+ try {
119
+ /** @type {ReadConfigFileResult} */
120
+ let configResult;
121
+ const errors = validate(result.config);
122
+ if (_.isEmpty(errors)) {
123
+ configResult = {...result, errors};
124
+ } else {
125
+ const reason = formatErrors(errors, result.config, {
126
+ json: rawConfig.get(result.filepath),
127
+ pretty,
128
+ });
129
+ configResult = reason
130
+ ? {...result, errors, reason}
131
+ : {...result, errors};
132
+ }
133
+
134
+ if (normalize) {
135
+ // normalize (to camel case) all top-level property names of the config file
136
+ configResult.config = normalizeConfig(
137
+ /** @type {AppiumConfig} */ (configResult.config),
138
+ );
139
+ }
140
+
141
+ return configResult;
142
+ } finally {
143
+ // clean up the raw config file cache, which is only kept to better report errors.
144
+ rawConfig.delete(result.filepath);
145
+ }
146
+ }
147
+ return result ?? {};
148
+ }
149
+
150
+ /**
151
+ * Convert schema property names to either a) the value of the `appiumCliDest` property, if any; or b) camel-case
152
+ * @param {AppiumConfig} config - Configuration object
153
+ * @returns {NormalizedAppiumConfig} New object with camel-cased keys (or `dest` keys).
154
+ */
155
+ function normalizeConfig (config) {
156
+ const schema = getSchema();
157
+ /**
158
+ * @param {AppiumConfig} config
159
+ * @param {string} [section] - Keypath (lodash `_.get()` style) to section of config. If omitted, assume root Appium config schema
160
+ * @returns Normalized section of config
161
+ */
162
+ const normalize = (config, section) => {
163
+ const obj = _.isUndefined(section) ? config : _.get(config, section, config);
164
+
165
+ const mappedObj = _.mapKeys(obj, (__, prop) =>
166
+ schema.properties[prop]?.appiumCliDest ?? _.camelCase(prop),
167
+ );
168
+
169
+ return _.mapValues(mappedObj, (value, property) => {
170
+ const nextSection = section ? `${section}.${property}` : property;
171
+ return isSchemaTypeObject(value) ? normalize(config, nextSection) : value;
172
+ });
173
+ };
174
+
175
+ /**
176
+ * Returns `true` if the schema prop references an object, or if it's an object itself
177
+ * @param {import('ajv').SchemaObject|object} schema - Referencing schema object
178
+ */
179
+ const isSchemaTypeObject = (schema) => Boolean(schema.properties);
180
+
181
+ return normalize(config);
182
+ }
183
+
184
+ /**
185
+ * Result of calling {@link readConfigFile}.
186
+ * @typedef {Object} ReadConfigFileResult
187
+ * @property {import('ajv').ErrorObject[]} [errors] - Validation errors
188
+ * @property {string} [filepath] - The path to the config file, if found
189
+ * @property {boolean} [isEmpty] - If `true`, the config file exists but is empty
190
+ * @property {AppiumConfig} [config] - The parsed configuration
191
+ * @property {string|betterAjvErrors.IOutputError[]} [reason] - Human-readable error messages and suggestions. If the `pretty` option is `true`, this will be a nice string to print.
192
+ */
193
+
194
+ /**
195
+ * Options for {@link readConfigFile}.
196
+ * @typedef {Object} ReadConfigFileOptions
197
+ * @property {boolean} [pretty=true] If `false`, do not use color and fancy formatting in the `reason` property of the {@link ReadConfigFileResult}. The value of `reason` is then suitable for machine-reading.
198
+ * @property {boolean} [normalize=true] If `false`, do not normalize key names to camel case.
199
+ */
200
+
201
+ /**
202
+ * This is an `AsyncSearcher` which is inexplicably _not_ exported by the `lilconfig` type definition.
203
+ * @typedef {ReturnType<import('lilconfig')["lilconfig"]>} LilconfigAsyncSearcher
204
+ */
205
+
206
+ /**
207
+ * The contents of an Appium config file. Generated from schema
208
+ * @typedef {import('../types/types').AppiumConfig} AppiumConfig
209
+ */
210
+
211
+ /**
212
+ * The contents of an Appium config file with camelcased property names (and using `appiumCliDest` value if present). Generated from {@link AppiumConfig}
213
+ * @typedef {import('../types/types').NormalizedAppiumConfig} NormalizedAppiumConfig
214
+ */
215
+
216
+ /**
217
+ * The string should be a raw JSON string.
218
+ * @typedef {string} RawJson
219
+ */
220
+
221
+ /**
222
+ * Options for {@link formatErrors}.
223
+ * @typedef {Object} FormatConfigErrorsOptions
224
+ * @property {import('./config-file').RawJson} [json] - Raw JSON config (as string)
225
+ * @property {boolean} [pretty=true] - Whether to format errors as a CLI-friendly string
226
+ * @property {string} [schemaId] - Specific ID of a prop; otherwise entire schema
227
+ */
package/lib/config.js CHANGED
@@ -1,14 +1,18 @@
1
1
  import _ from 'lodash';
2
- import path from 'path';
3
- import { mkdirp, fs, system } from 'appium-support';
2
+ import { mkdirp, system, fs } from '@appium/support';
4
3
  import axios from 'axios';
5
4
  import { exec } from 'teen_process';
6
5
  import { rootDir } from './utils';
7
6
  import logger from './logger';
8
7
  import semver from 'semver';
8
+ import {
9
+ StoreDeprecatedDefaultCapabilityAction, DEFAULT_CAPS_ARG,
10
+ } from './cli/argparse-actions';
11
+ import findUp from 'find-up';
12
+ import { getDefaultsFromSchema } from './schema/schema';
9
13
 
14
+ const npmPackage = fs.readPackageJsonFrom(__dirname);
10
15
 
11
- const npmPackage = require(path.resolve(rootDir, 'package.json'));
12
16
  const APPIUM_VER = npmPackage.version;
13
17
  const MIN_NODE_VERSION = npmPackage.engines.node;
14
18
 
@@ -24,6 +28,11 @@ function getNodeVersion () {
24
28
  return semver.coerce(process.version);
25
29
  }
26
30
 
31
+ function isSubClass (candidateClass, superClass) {
32
+ return _.isFunction(superClass) && _.isFunction(candidateClass)
33
+ && (candidateClass.prototype instanceof superClass || candidateClass === superClass);
34
+ }
35
+
27
36
  async function updateBuildInfo (useGithubApiFallback = false) {
28
37
  const sha = await getGitRev(useGithubApiFallback);
29
38
  if (!sha) {
@@ -36,11 +45,23 @@ async function updateBuildInfo (useGithubApiFallback = false) {
36
45
  }
37
46
  }
38
47
 
48
+ /**
49
+ * Finds the Git metadata dir (see `GIT_META_ROOT`)
50
+ *
51
+ * This is needed because Appium cannot assume `package.json` and `.git` are in the same
52
+ * directory. Monorepos, see?
53
+ * @returns {string|void} Path to dir or `undefined` if not found
54
+ */
55
+ async function findGitRoot () {
56
+ return await findUp(GIT_META_ROOT, {cwd: rootDir, type: 'directory'});
57
+ }
58
+
39
59
  async function getGitRev (useGithubApiFallback = false) {
40
- if (await fs.exists(path.resolve(rootDir, GIT_META_ROOT))) {
60
+ const gitRoot = await findGitRoot();
61
+ if (gitRoot) {
41
62
  try {
42
63
  const {stdout} = await exec(GIT_BINARY, ['rev-parse', 'HEAD'], {
43
- cwd: rootDir
64
+ cwd: gitRoot
44
65
  });
45
66
  return stdout.trim();
46
67
  } catch (ign) {}
@@ -68,10 +89,11 @@ async function getGitRev (useGithubApiFallback = false) {
68
89
  }
69
90
 
70
91
  async function getGitTimestamp (commitSha, useGithubApiFallback = false) {
71
- if (await fs.exists(path.resolve(rootDir, GIT_META_ROOT))) {
92
+ const gitRoot = await findGitRoot();
93
+ if (gitRoot) {
72
94
  try {
73
95
  const {stdout} = await exec(GIT_BINARY, ['show', '-s', '--format=%ci', commitSha], {
74
- cwd: rootDir
96
+ cwd: gitRoot
75
97
  });
76
98
  return stdout.trim();
77
99
  } catch (ign) {}
@@ -135,65 +157,89 @@ async function showConfig () {
135
157
  console.log(JSON.stringify(getBuildInfo())); // eslint-disable-line no-console
136
158
  }
137
159
 
138
- function getNonDefaultArgs (parser, args) {
139
- let nonDefaults = {};
140
- for (let rawArg of parser.rawArgs) {
141
- let arg = rawArg[1].dest;
142
- if (args[arg] && args[arg] !== rawArg[1].defaultValue) {
143
- nonDefaults[arg] = args[arg];
144
- }
160
+ function getNonDefaultServerArgs (parser, args) {
161
+ // hopefully these function names are descriptive enough
162
+
163
+ function typesDiffer (dest) {
164
+ return typeof args[dest] !== typeof defaultsFromSchema[dest];
165
+ }
166
+
167
+ function defaultValueIsArray (dest) {
168
+ return _.isArray(defaultsFromSchema[dest]);
169
+ }
170
+
171
+ function argsValueIsArray (dest) {
172
+ return _.isArray(args[dest]);
173
+ }
174
+
175
+ function arraysDiffer (dest) {
176
+ return _.difference(args[dest], defaultsFromSchema[dest]).length > 0;
145
177
  }
146
- return nonDefaults;
147
- }
148
178
 
149
- function checkValidPort (port, portName) {
150
- if (port > 0 && port < 65536) return true; // eslint-disable-line curly
151
- logger.error(`Port '${portName}' must be greater than 0 and less than 65536. Currently ${port}`);
152
- return false;
179
+ function valuesUnequal (dest) {
180
+ return args[dest] !== defaultsFromSchema[dest];
181
+ }
182
+
183
+ function defaultIsDefined (dest) {
184
+ return !_.isUndefined(defaultsFromSchema[dest]);
185
+ }
186
+
187
+ // note that `_.overEvery` is like an "AND", and `_.overSome` is like an "OR"
188
+
189
+ const argValueNotArrayOrArraysDiffer = _.overSome([
190
+ _.negate(argsValueIsArray),
191
+ arraysDiffer
192
+ ]);
193
+
194
+ const defaultValueNotArrayAndValuesUnequal = _.overEvery([
195
+ _.negate(defaultValueIsArray), valuesUnequal
196
+ ]);
197
+
198
+ /**
199
+ * This used to be a hideous conditional, but it's broken up into a hideous function instead.
200
+ * hopefully this makes things a little more understandable.
201
+ * - checks if the default value is defined
202
+ * - if so, and the default is not an array:
203
+ * - ensures the types are the same
204
+ * - ensures the values are equal
205
+ * - if so, and the default is an array:
206
+ * - ensures the args value is an array
207
+ * - ensures the args values do not differ from the default values
208
+ * @param {string} dest - argument name (`dest` value)
209
+ * @returns {boolean}
210
+ */
211
+ const isNotDefault = _.overEvery([
212
+ defaultIsDefined,
213
+ _.overSome([
214
+ typesDiffer,
215
+ _.overEvery([
216
+ defaultValueIsArray,
217
+ argValueNotArrayOrArraysDiffer
218
+ ]),
219
+ defaultValueNotArrayAndValuesUnequal
220
+ ])
221
+ ]);
222
+
223
+ const defaultsFromSchema = getDefaultsFromSchema();
224
+
225
+ return _.pickBy(args, (__, key) => isNotDefault(key));
153
226
  }
154
227
 
155
- function validateServerArgs (parser, args) {
156
- // arguments that cannot both be set
157
- let exclusives = [
158
- ['noReset', 'fullReset'],
159
- ['ipa', 'safari'],
160
- ['app', 'safari'],
161
- ['forceIphone', 'forceIpad'],
162
- ['deviceName', 'defaultDevice']
163
- ];
164
-
165
- for (let exSet of exclusives) {
166
- let numFoundInArgs = 0;
167
- for (let opt of exSet) {
168
- if (_.has(args, opt) && args[opt]) {
169
- numFoundInArgs++;
170
- }
171
- }
172
- if (numFoundInArgs > 1) {
173
- throw new Error(`You can't pass in more than one argument from the ` +
174
- `set ${JSON.stringify(exSet)}, since they are ` +
175
- `mutually exclusive`);
228
+ function getDeprecatedArgs (parser, args) {
229
+ // go through the server command line arguments and figure
230
+ // out which of the ones used are deprecated
231
+ return parser.rawArgs.reduce((acc, [[name], {dest, default: defaultValue, action}]) => {
232
+ if (!args[dest] || args[dest] === defaultValue) {
233
+ return acc;
176
234
  }
177
- }
178
235
 
179
- const validations = {
180
- port: checkValidPort,
181
- callbackPort: checkValidPort,
182
- bootstrapPort: checkValidPort,
183
- chromedriverPort: checkValidPort,
184
- robotPort: checkValidPort,
185
- backendRetries: (r) => r >= 0
186
- };
187
-
188
- const nonDefaultArgs = getNonDefaultArgs(parser, args);
189
-
190
- for (let [arg, validator] of _.toPairs(validations)) {
191
- if (_.has(nonDefaultArgs, arg)) {
192
- if (!validator(args[arg], arg)) {
193
- throw new Error(`Invalid argument for param ${arg}: ${args[arg]}`);
194
- }
236
+ if (action?.deprecated_for) {
237
+ acc[name] = action.deprecated_for;
238
+ } else if (isSubClass(action, StoreDeprecatedDefaultCapabilityAction)) {
239
+ acc[name] = DEFAULT_CAPS_ARG;
195
240
  }
196
- }
241
+ return acc;
242
+ }, {});
197
243
  }
198
244
 
199
245
  async function validateTmpDir (tmpDir) {
@@ -206,7 +252,8 @@ async function validateTmpDir (tmpDir) {
206
252
  }
207
253
 
208
254
  export {
209
- getBuildInfo, validateServerArgs, checkNodeOk, showConfig,
210
- warnNodeDeprecations, validateTmpDir, getNonDefaultArgs,
211
- getGitRev, checkValidPort, APPIUM_VER, updateBuildInfo,
255
+ getBuildInfo, checkNodeOk, showConfig,
256
+ warnNodeDeprecations, validateTmpDir, getNonDefaultServerArgs,
257
+ getGitRev, APPIUM_VER, updateBuildInfo,
258
+ getDeprecatedArgs
212
259
  };
@@ -1,25 +1,74 @@
1
+ // @ts-check
2
+
1
3
  import _ from 'lodash';
2
- import ExtensionConfig, { DRIVER_TYPE } from './extension-config';
4
+ import ExtensionConfig from './extension-config';
5
+ import { DRIVER_TYPE } from './ext-config-io';
3
6
 
4
7
  export default class DriverConfig extends ExtensionConfig {
5
- constructor (appiumHome, logFn = null) {
8
+ /**
9
+ * A mapping of `APPIUM_HOME` values to {@link DriverConfig} instances.
10
+ * Each `APPIUM_HOME` should only have one associated `DriverConfig` instance.
11
+ * @type {Record<string,DriverConfig>}
12
+ * @private
13
+ */
14
+ static _instances = {};
15
+
16
+ /**
17
+ * Call {@link DriverConfig.getInstance} instead.
18
+ * @private
19
+ * @param {string} appiumHome
20
+ * @param {(...args: any[]) => void} [logFn]
21
+ */
22
+ constructor (appiumHome, logFn) {
6
23
  super(appiumHome, DRIVER_TYPE, logFn);
24
+ /** @type {Set<string>} */
25
+ this.knownAutomationNames = new Set();
26
+ }
27
+
28
+ async read () {
29
+ this.knownAutomationNames.clear();
30
+ return await super.read();
7
31
  }
8
32
 
9
- getConfigProblems (driver) {
33
+ /**
34
+ * Creates or gets an instance of {@link DriverConfig} based value of `appiumHome`
35
+ * @param {string} appiumHome - `APPIUM_HOME` path
36
+ * @param {(...args: any[]) => void} [logFn] - Optional logging function
37
+ * @returns {DriverConfig}
38
+ */
39
+ static getInstance (appiumHome, logFn) {
40
+ const instance = DriverConfig._instances[appiumHome] ?? new DriverConfig(appiumHome, logFn);
41
+ DriverConfig._instances[appiumHome] = instance;
42
+ return instance;
43
+ }
44
+
45
+ /**
46
+ *
47
+ * @param {object} extData
48
+ * @param {string} extName
49
+ * @returns {import('./extension-config').Problem[]}
50
+ */
51
+ // eslint-disable-next-line no-unused-vars
52
+ getConfigProblems (extData, extName) {
10
53
  const problems = [];
11
- const automationNames = [];
12
- const {platformNames, automationName} = driver;
54
+ const {platformNames, automationName} = extData;
13
55
 
14
56
  if (!_.isArray(platformNames)) {
15
57
  problems.push({
16
- err: 'Missing or incorrect supported platformName list.',
58
+ err: 'Missing or incorrect supported platformNames list.',
17
59
  val: platformNames
18
60
  });
19
61
  } else {
20
- for (const pName of platformNames) {
21
- if (!_.isString(pName)) {
22
- problems.push({err: 'Incorrectly formatted platformName.', val: pName});
62
+ if (_.isEmpty(platformNames)) {
63
+ problems.push({
64
+ err: 'Empty platformNames list.',
65
+ val: platformNames
66
+ });
67
+ } else {
68
+ for (const pName of platformNames) {
69
+ if (!_.isString(pName)) {
70
+ problems.push({err: 'Incorrectly formatted platformName.', val: pName});
71
+ }
23
72
  }
24
73
  }
25
74
  }
@@ -28,17 +77,23 @@ export default class DriverConfig extends ExtensionConfig {
28
77
  problems.push({err: 'Missing or incorrect automationName', val: automationName});
29
78
  }
30
79
 
31
- if (_.includes(automationNames, automationName)) {
80
+ if (this.knownAutomationNames.has(automationName)) {
32
81
  problems.push({
33
82
  err: 'Multiple drivers claim support for the same automationName',
34
83
  val: automationName
35
84
  });
36
85
  }
37
- automationNames.push(automationName);
86
+
87
+ // should we retain the name at the end of this function, once we've checked there are no problems?
88
+ this.knownAutomationNames.add(automationName);
38
89
 
39
90
  return problems;
40
91
  }
41
92
 
93
+ /**
94
+ * @param {string} driverName
95
+ * @param {object} extData
96
+ */
42
97
  extensionDesc (driverName, {version, automationName}) {
43
98
  return `${driverName}@${version} (automationName '${automationName}')`;
44
99
  }
package/lib/drivers.js CHANGED
@@ -12,9 +12,12 @@ const KNOWN_DRIVERS = {
12
12
  youiengine: 'appium-youiengine-driver',
13
13
  windows: 'appium-windows-driver',
14
14
  mac: 'appium-mac-driver',
15
+ mac2: 'appium-mac2-driver',
15
16
  espresso: 'appium-espresso-driver',
16
17
  tizen: 'appium-tizen-driver',
17
18
  flutter: 'appium-flutter-driver',
19
+ safari: 'appium-safari-driver',
20
+ gecko: 'appium-geckodriver',
18
21
  };
19
22
 
20
23
  function getDriverBySupport (drivers, matchAutomationName, matchPlatformName) {
@@ -64,7 +67,7 @@ function findMatchingDriver (config, {automationName, platformName}) {
64
67
  if (!driver) {
65
68
  throw new Error(`MainClass ${mainClass} did not result in a driver object`);
66
69
  }
67
- return {driver, version};
70
+ return {driver, version, driverName};
68
71
  } catch (err) {
69
72
  const msg = `Could not find a driver for automationName ` +
70
73
  `'${automationName}' and platformName ${platformName}'. ` +