diff --git a/packages/appium/lib/appium.js b/packages/appium/lib/appium.js index 4acb54ca9..c6fa3c274 100644 --- a/packages/appium/lib/appium.js +++ b/packages/appium/lib/appium.js @@ -53,6 +53,7 @@ class AppiumDriver extends BaseDriver { // It is not recommended to access this property directly from the outside this.pendingDrivers = {}; + /** @type {PluginExtensionClass[]} */ this.pluginClasses = []; // list of which plugins are active this.sessionPlugins = {}; // map of sessions to actual plugin instances per session this.sessionlessPlugins = []; // some commands are sessionless, so we need a set of plugins for them @@ -61,9 +62,12 @@ class AppiumDriver extends BaseDriver { updateBuildInfo(); } - /** @type {DriverConfig|undefined} */ + /** @type {import('./driver-config').default|undefined} */ driverConfig; + /** @type {import('express').Express|undefined} */ + server; + /** * Cancel commands queueing for the umbrella Appium driver */ @@ -670,3 +674,23 @@ export class NoDriverProxyCommandError extends Error { } export { AppiumDriver }; + + +/** + * @typedef {Object} StaticExtMembers + * @property {(app: import('express').Express, httpServer: import('http').Server) => import('type-fest').Promisable} [updateServer] + * @property {import('@appium/base-driver').MethodMap} [newMethodMap] + */ + +/** + * @typedef {Object} StaticPluginMembers + * @property {string} pluginName + */ + +/** + * @typedef {import('type-fest').Class & StaticPluginMembers & StaticExtMembers} PluginExtensionClass + */ + +/** + * @typedef {import('type-fest').Class & StaticExtMembers} DriverExtensionClass + */ diff --git a/packages/appium/lib/cli/args.js b/packages/appium/lib/cli/args.js index ac1970712..1304a215c 100644 --- a/packages/appium/lib/cli/args.js +++ b/packages/appium/lib/cli/args.js @@ -1,7 +1,6 @@ // @ts-check // @ts-ignore -import { DEFAULT_BASE_PATH } from '@appium/base-driver'; import _ from 'lodash'; import DriverConfig from '../driver-config'; import { APPIUM_HOME, DRIVER_TYPE, INSTALL_TYPES, PLUGIN_TYPE } from '../extension-config'; @@ -175,13 +174,7 @@ function makeRunArgs (type) { */ function getServerArgs () { return new Map([ - ...toParserArgs({ - overrides: { - basePath: { - default: DEFAULT_BASE_PATH - }, - } - }), + ...toParserArgs(), ...serverArgsDisallowedInConfig, ]); } @@ -195,21 +188,31 @@ const serverArgsDisallowedInConfig = new Map([ ['--shell'], { required: false, - default: null, help: 'Enter REPL mode', - action: 'store_true', + action: 'store_const', + const: true, dest: 'shell', }, ], + [ + ['--show-build-info'], + { + dest: 'showBuildInfo', + action: 'store_const', + const: true, + required: false, + help: 'Show info about the Appium build and exit', + }, + ], [ ['--show-config'], { - default: false, dest: 'showConfig', - action: 'store_true', + action: 'store_const', + const: true, required: false, - help: 'Show info about the appium server configuration and exit', - }, + help: 'Show the current Appium configuration and exit', + } ], [ ['--config'], diff --git a/packages/appium/lib/cli/parser.js b/packages/appium/lib/cli/parser.js index 354ae88e9..8fb5c5fe5 100644 --- a/packages/appium/lib/cli/parser.js +++ b/packages/appium/lib/cli/parser.js @@ -146,7 +146,7 @@ class ArgParser { return _.reduce( args, (unpacked, value, key) => { - if (hasArgSpec(key)) { + if (!_.isUndefined(value) && hasArgSpec(key)) { const {dest} = /** @type {import('../schema/arg-spec').ArgSpec} */(getArgSpec(key)); _.set(unpacked, dest, value); } else { diff --git a/packages/appium/lib/config.js b/packages/appium/lib/config.js index 16c11a3ec..82b5753f2 100644 --- a/packages/appium/lib/config.js +++ b/packages/appium/lib/config.js @@ -1,3 +1,6 @@ +// @ts-check + +/* eslint-disable no-console */ import _ from 'lodash'; import { mkdirp, system, fs } from '@appium/support'; import axios from 'axios'; @@ -22,7 +25,7 @@ const BUILD_INFO = { }; function getNodeVersion () { - return semver.coerce(process.version); + return /** @type {import('semver').SemVer} */(semver.coerce(process.version)); } async function updateBuildInfo (useGithubApiFallback = false) { @@ -42,7 +45,7 @@ async function updateBuildInfo (useGithubApiFallback = false) { * * This is needed because Appium cannot assume `package.json` and `.git` are in the same * directory. Monorepos, see? - * @returns {string|void} Path to dir or `undefined` if not found + * @returns {Promise} Path to dir or `undefined` if not found */ async function findGitRoot () { return await findUp(GIT_META_ROOT, {cwd: rootDir, type: 'directory'}); @@ -80,6 +83,11 @@ async function getGitRev (useGithubApiFallback = false) { return null; } +/** + * @param {string} commitSha + * @param {boolean} [useGithubApiFallback] + * @returns {Promise} + */ async function getGitTimestamp (commitSha, useGithubApiFallback = false) { const gitRoot = await findGitRoot(); if (gitRoot) { @@ -144,30 +152,30 @@ function warnNodeDeprecations () { // } } -async function showConfig () { +async function showBuildInfo () { await updateBuildInfo(true); console.log(JSON.stringify(getBuildInfo())); // eslint-disable-line no-console } /** * Returns k/v pairs of server arguments which are _not_ the defaults. - * @param {import('../types/types').ParsedArgs} args - * @returns {Partial} + * @param {ParsedArgs} args + * @returns {Partial} */ function getNonDefaultServerArgs (args) { // hopefully these function names are descriptive enough - const typesDiffer = (dest) => typeof args[dest] !== typeof defaultsFromSchema[dest]; + const typesDiffer = /** @param {string} dest */(dest) => typeof args[dest] !== typeof defaultsFromSchema[dest]; - const defaultValueIsArray = (dest) => _.isArray(defaultsFromSchema[dest]); + const defaultValueIsArray = /** @param {string} dest */(dest) => _.isArray(defaultsFromSchema[dest]); - const argsValueIsArray = (dest) => _.isArray(args[dest]); + const argsValueIsArray = /** @param {string} dest */(dest) => _.isArray(args[dest]); - const arraysDiffer = (dest) => _.size(_.difference(args[dest], defaultsFromSchema[dest])) > 0; + const arraysDiffer = /** @param {string} dest */(dest) => _.gt(_.size(_.difference(args[dest], defaultsFromSchema[dest])), 0); - const valuesDiffer = (dest) => args[dest] !== defaultsFromSchema[dest]; + const valuesDiffer = /** @param {string} dest */(dest) => args[dest] !== defaultsFromSchema[dest]; - const defaultIsDefined = (dest) => !_.isUndefined(defaultsFromSchema[dest]); + const defaultIsDefined = /** @param {string} dest */(dest) => !_.isUndefined(defaultsFromSchema[dest]); // note that `_.overEvery` is like an "AND", and `_.overSome` is like an "OR" @@ -209,6 +217,46 @@ function getNonDefaultServerArgs (args) { return _.pickBy(args, (__, key) => isNotDefault(key)); } +/** + * Compacts an object for {@link showConfig}: + * 1. Removes `subcommand` key/value + * 2. Removes `undefined` values + * 3. Removes empty objects (but not `false` values) + * Does not operate recursively. + */ +const compactConfig = _.partial( + _.omitBy, + _, + (value, key) => key === 'subcommand' || _.isUndefined(value) || (_.isObject(value) && _.isEmpty(value)) +); + +/** + * Shows a breakdown of the current config after CLI params, config file loaded & defaults applied. + * + * The actual shape of `preConfigParsedArgs` and `defaults` does not matter for the purposes of this function, + * but it's intended to be called with values of type {@link ParsedArgs} and `DefaultValues`, respectively. + * + * @param {object} preConfigParsedArgs - Parsed CLI args (or param to `init()`) before config & defaults applied + * @param {import('./config-file').ReadConfigFileResult} configResult - Result of attempting to load a config file + * @param {object} defaults - Configuration defaults from schemas + */ +function showConfig (preConfigParsedArgs, configResult, defaults) { + console.log('Appium Configuration\n'); + if (configResult.config) { + console.log(`via config file at ${configResult.filepath}:\n`); + console.dir(compactConfig(configResult.config)); + } else { + console.log(`(no configuration file loaded)\n`); + } + console.log('via CLI or function call:\n'); + console.dir(compactConfig(preConfigParsedArgs)); + console.log('\nvia defaults:\n'); + console.dir(compactConfig(defaults)); +} + +/** + * @param {string} tmpDir + */ async function validateTmpDir (tmpDir) { try { await mkdirp(tmpDir); @@ -219,7 +267,11 @@ async function validateTmpDir (tmpDir) { } export { - getBuildInfo, checkNodeOk, showConfig, + getBuildInfo, checkNodeOk, showBuildInfo, warnNodeDeprecations, validateTmpDir, getNonDefaultServerArgs, - getGitRev, APPIUM_VER, updateBuildInfo + getGitRev, APPIUM_VER, updateBuildInfo, showConfig }; + +/** + * @typedef {import('../types/types').ParsedArgs} ParsedArgs + */ diff --git a/packages/appium/lib/extension-config.js b/packages/appium/lib/extension-config.js index abc4ddad3..3f141bddb 100644 --- a/packages/appium/lib/extension-config.js +++ b/packages/appium/lib/extension-config.js @@ -270,7 +270,7 @@ export default class ExtensionConfig { /** * Loads extension and returns its main class * @param {string} extName - * @returns {(...args: any[]) => object } + * @returns {import('type-fest').Class} */ require (extName) { const {mainClass} = this.installedExtensions[extName]; diff --git a/packages/appium/lib/main.js b/packages/appium/lib/main.js index ff4027742..fc03cff73 100755 --- a/packages/appium/lib/main.js +++ b/packages/appium/lib/main.js @@ -3,6 +3,7 @@ // transpile:main // @ts-check +import logger from './logger'; // logger needs to remain first of imports // @ts-ignore import { routeConfiguringFunction as makeRouter, server as baseServer } from '@appium/base-driver'; import { logger as logFactory, util } from '@appium/support'; @@ -12,13 +13,12 @@ import { AppiumDriver } from './appium'; import { driverConfig, pluginConfig, USE_ALL_PLUGINS } from './cli/args'; import { runExtensionCommand } from './cli/extension'; import { default as getParser, SERVER_SUBCOMMAND } from './cli/parser'; -import { APPIUM_VER, checkNodeOk, getGitRev, getNonDefaultServerArgs, showConfig, validateTmpDir, warnNodeDeprecations } from './config'; +import { APPIUM_VER, checkNodeOk, getGitRev, getNonDefaultServerArgs, showBuildInfo, validateTmpDir, warnNodeDeprecations, showConfig } from './config'; import { readConfigFile } from './config-file'; import { DRIVER_TYPE, PLUGIN_TYPE } from './extension-config'; import registerNode from './grid-register'; -import logger from './logger'; // logger needs to remain first of imports import { init as logsinkInit } from './logsink'; -import { getDefaultsForSchema, validate } from './schema/schema'; +import { getDefaultsForSchema } from './schema/schema'; import { inspect } from './utils'; /** @@ -32,14 +32,12 @@ async function preflightChecks (args, throwInsteadOfExit = false) { if (args.longStacktrace) { require('longjohn').async_trace_limit = -1; } - if (args.showConfig) { - await showConfig(); + if (args.showBuildInfo) { + await showBuildInfo(); process.exit(0); } warnNodeDeprecations(); - validate(args); - if (args.tmpDir) { await validateTmpDir(args.tmpDir); } @@ -112,7 +110,7 @@ function logServerPort (address, port) { * * @param {Object} args - argparser parsed dict * @param {import('./plugin-config').default} pluginConfig - a plugin extension config - * @returns {({pluginName: string} & ((...args: any[]) => unknown))[]} + * @returns {PluginExtensionClass[]} */ function getActivePlugins (args, pluginConfig) { return _.compact(Object.keys(pluginConfig.installedExtensions).filter((pluginName) => @@ -121,7 +119,7 @@ function getActivePlugins (args, pluginConfig) { ).map((pluginName) => { try { logger.info(`Attempting to load plugin ${pluginName}...`); - const PluginClass = /** @type {{pluginName: string} & ((...args: any[]) => unknown)} */(pluginConfig.require(pluginName)); + const PluginClass = /** @type {PluginExtensionClass} */(pluginConfig.require(pluginName)); PluginClass.pluginName = pluginName; // store the plugin name on the class so it can be used later return PluginClass; @@ -156,10 +154,22 @@ function getActiveDrivers (args, driverConfig) { })); } +/** + * Gets a list of `updateServer` functions from all extensions + * @param {DriverExtensionClass[]} driverClasses + * @param {PluginExtensionClass[]} pluginClasses + * @returns {StaticExtMembers['updateServer'][]} + */ function getServerUpdaters (driverClasses, pluginClasses) { - return [...driverClasses, ...pluginClasses].map((klass) => klass.updateServer).filter(Boolean); + return _.compact(_.map([...driverClasses, ...pluginClasses], 'updateServer')); } +/** + * Makes a big `MethodMap` from all the little `MethodMap`s in the extensions + * @param {DriverExtensionClass[]} driverClasses + * @param {PluginExtensionClass[]} pluginClasses + * @returns {import('@appium/base-driver').MethodMap} + */ function getExtraMethodMap (driverClasses, pluginClasses) { return [...driverClasses, ...pluginClasses].reduce( (map, klass) => ({...map, ...klass.newMethodMap}), @@ -175,20 +185,27 @@ function getExtraMethodMap (driverClasses, pluginClasses) { * If `args` contains a non-empty `subcommand` which is not `server`, this function * will resolve with an empty object. * - * @todo: Use generics/conditional types to specify return values. + * @param {ParsedArgs} [args] - Parsed args + * @returns {Promise>} * @example * import {init, getSchema} from 'appium'; * const options = {}; // config object * await init(options); * const schema = getSchema(); // entire config schema including plugins and drivers - * @param {ParsedArgs} [args] - Parsed args - * @returns {Promise>} */ async function init (args) { const parser = await getParser(); let throwInsteadOfExit = false; /** @type {ParsedArgs} */ + let preConfigParsedArgs; + /** @type {typeof preConfigParsedArgs & import('type-fest').AsyncReturnType['config']} */ let parsedArgs; + /** + * This is a definition (instead of declaration) because TS can't figure out + * the value will be defined when it's used. + * @type {ReturnType} + */ + let defaults = {}; if (args) { // if we have a containing package instead of running as a CLI process, // that package might not appreciate us calling 'process.exit' willy- @@ -198,29 +215,39 @@ async function init (args) { // but remove it since it's not a real server arg per se delete args.throwInsteadOfExit; } - parsedArgs = {...args, subcommand: args.subcommand ?? SERVER_SUBCOMMAND}; + preConfigParsedArgs = {...args, subcommand: args.subcommand ?? SERVER_SUBCOMMAND}; } else { // otherwise parse from CLI - parsedArgs = parser.parseArgs(); + preConfigParsedArgs = parser.parseArgs(); } - const configResult = await readConfigFile(parsedArgs.configFile); + const configResult = await readConfigFile(preConfigParsedArgs.configFile); if (!_.isEmpty(configResult.errors)) { throw new Error(`Errors in config file ${configResult.filepath}:\n ${configResult.reason ?? configResult.errors}`); } + // merge config and apply defaults. // the order of precendece is: // 1. command line args // 2. config file // 3. defaults from config file. - if (parsedArgs.subcommand === SERVER_SUBCOMMAND) { + if (preConfigParsedArgs.subcommand === SERVER_SUBCOMMAND) { + defaults = getDefaultsForSchema(false); + + if (preConfigParsedArgs.showConfig) { + showConfig(preConfigParsedArgs, configResult, defaults); + return {}; + } + parsedArgs = _.defaultsDeep( - parsedArgs, + preConfigParsedArgs, configResult.config?.server, - getDefaultsForSchema(false) + defaults ); + } else { + parsedArgs = preConfigParsedArgs; } await logsinkInit(parsedArgs); @@ -310,7 +337,6 @@ async function main (args) { 'to visit sites which could maliciously try to start Appium ' + 'sessions on your machine'); } - // @ts-ignore appiumDriver.server = server; try { // configure as node on grid, if necessary @@ -363,3 +389,15 @@ export { readConfigFile } from './config-file'; /** * @typedef {import('../types/types').ParsedArgs} ParsedArgs */ + +/** + * @typedef {import('./appium').PluginExtensionClass} PluginExtensionClass + */ + +/** + * @typedef {import('./appium').DriverExtensionClass} DriverExtensionClass + */ + +/** + * @typedef {import('./appium').StaticExtMembers} StaticExtMembers + */ diff --git a/packages/appium/lib/schema/cli-args.js b/packages/appium/lib/schema/cli-args.js index ad2bf317f..777367db9 100644 --- a/packages/appium/lib/schema/cli-args.js +++ b/packages/appium/lib/schema/cli-args.js @@ -98,11 +98,9 @@ function makeDescription (schema) { * as understood by `argparse`. * @param {AppiumJSONSchema} subSchema - JSON schema for the option * @param {ArgSpec} argSpec - Argument spec tuple - * @param {SubSchemaToArgDefOptions} [opts] - Options * @returns {[string[], import('argparse').ArgumentOptions]} Tuple of flag and options */ -function subSchemaToArgDef (subSchema, argSpec, opts = {}) { - const {overrides = {}} = opts; +function subSchemaToArgDef (subSchema, argSpec) { let { type, appiumCliAliases, @@ -110,7 +108,7 @@ function subSchemaToArgDef (subSchema, argSpec, opts = {}) { enum: enumValues, } = subSchema; - const {name, arg, dest} = argSpec; + const {name, arg} = argSpec; const aliases = [ aliasToFlag(argSpec), @@ -142,7 +140,8 @@ function subSchemaToArgDef (subSchema, argSpec, opts = {}) { switch (type) { // booleans do not have a type per `ArgumentOptions`, just an "action" case TYPENAMES.BOOLEAN: { - argOpts.action = 'store_true'; + argOpts.action = 'store_const'; + argOpts.const = true; break; } @@ -227,16 +226,6 @@ function subSchemaToArgDef (subSchema, argSpec, opts = {}) { } } - // overrides override anything we computed here. usually this involves "custom types", - // which are really just transform functions. - argOpts = _.merge( - argOpts, - /** should the override keys correspond to the prop name or the prop dest? - * the prop dest is computed by {@link aliasToDest}. - */ - overrides[dest] ?? {}, - ); - return [aliases, argOpts]; } @@ -244,32 +233,19 @@ function subSchemaToArgDef (subSchema, argSpec, opts = {}) { * Converts the finalized, flattened schema representation into * ArgumentDefinitions for handoff to `argparse`. * - * @param {ToParserArgsOptions} opts - Options * @throws If schema has not been added to ajv (via `finalizeSchema()`) * @returns {import('../cli/args').ArgumentDefinitions} A map of arryas of * aliases to `argparse` arguments; empty if no schema found */ -export function toParserArgs (opts = {}) { +export function toParserArgs () { const flattened = flattenSchema().filter(({schema}) => !schema.appiumCliIgnored); return new Map( _.map(flattened, ({schema, argSpec}) => - subSchemaToArgDef(schema, argSpec, opts), + subSchemaToArgDef(schema, argSpec), ), ); } -/** - * Options for {@link toParserArgs} - * @typedef {SubSchemaToArgDefOptions} ToParserArgsOptions - */ - -/** - * Options for {@link subSchemaToArgDef}. - * @typedef {Object} SubSchemaToArgDefOptions - * @property {string} [prefix] - The prefix to use for the flag, if any - * @property {{[key: string]: import('argparse').ArgumentOptions}} [overrides] - An object of key/value pairs to override the default values - */ - /** * @template T * @typedef {import('ajv/dist/types').FormatValidator} FormatValidator diff --git a/packages/appium/test/cli/cli-helpers.js b/packages/appium/test/cli/cli-helpers.js index 17c952474..aff32f755 100644 --- a/packages/appium/test/cli/cli-helpers.js +++ b/packages/appium/test/cli/cli-helpers.js @@ -143,6 +143,14 @@ export const installLocalExtension = _.curry( * @typedef {Error & AppiumRunErrorProps & import('../../lib/cli/npm').TeenProcessExecErrorProps} AppiumRunError */ +/** + * @typedef {import('../../lib/cli/npm').TeenProcessExecResult} TeenProcessExecResult + */ + +/** + * @typedef {import('../../lib/cli/npm').TeenProcessExecError} TeenProcessExecError + */ + /** * Wraps the error returned by {@link exec}. * @typedef {Object} AppiumRunErrorProps diff --git a/packages/appium/test/config-specs.js b/packages/appium/test/config-specs.js index db771a962..55307b11b 100644 --- a/packages/appium/test/config-specs.js +++ b/packages/appium/test/config-specs.js @@ -1,22 +1,61 @@ +// @ts-check + // transpile:mocha -import _ from 'lodash'; import sinon from 'sinon'; import getParser from '../lib/cli/parser'; -import { checkNodeOk, getBuildInfo, getNonDefaultServerArgs, showConfig, validateTmpDir, warnNodeDeprecations } from '../lib/config'; +import { checkNodeOk, getBuildInfo, getNonDefaultServerArgs, showBuildInfo, showConfig, validateTmpDir, warnNodeDeprecations } from '../lib/config'; import logger from '../lib/logger'; import { getDefaultsForSchema, resetSchema, registerSchema, finalizeSchema } from '../lib/schema/schema'; describe('Config', function () { + /** @type {import('sinon').SinonSandbox} */ + let sandbox; + + beforeEach(function () { + sandbox = sinon.createSandbox(); + }); + + afterEach(function () { + sandbox.restore(); + }); + describe('Appium config', function () { - describe('showConfig', function () { - before(function () { - sinon.spy(console, 'log'); - }); - it('should log the config to console', async function () { + /** @type {import('sinon').SinonSpy<[message?: any, ...extra: any[]],void>} */ + let log; + /** @type {import('sinon').SinonSpy<[message?: any, ...extra: any[]],void>} */ + let dir; + beforeEach(function () { + log = sandbox.spy(console, 'log'); + dir = sandbox.spy(console, 'dir'); + }); + + describe('showBuildInfo()', function () { + it('should log build info to console', async function () { const config = getBuildInfo(); - await showConfig(); - console.log.calledOnce.should.be.true; // eslint-disable-line no-console - console.log.getCall(0).args[0].should.contain(JSON.stringify(config)); // eslint-disable-line no-console + await showBuildInfo(); + log.should.have.been.calledOnce; + log.firstCall.args.should.contain(JSON.stringify(config)); + }); + }); + + describe('showConfig', function () { + describe('when a config file is present', function () { + it('should dump the current Appium config', function () { + showConfig({foo: 'bar'}, {config: {server: {address: 'quux'}}}, {spam: 'food'}); + log.should.have.been.calledWith('Appium Configuration\n'); + }); + + it('should skip empty objects', function () { + showConfig({foo: 'bar', cows: {}, pigs: [], sheep: 0, ducks: false}, {config: {server: {address: 'quux'}}}, {spam: 'food'}); + dir.should.have.been.calledWith({foo: 'bar', sheep: 0, ducks: false}); + }); + }); + + describe('when a config file is not present', function () { + it('should dump the current Appium config sans config file contents', function () { + showConfig({foo: 'bar'}, {}, {spam: 'food'}); + log.should.have.been.calledWith('(no configuration file loaded)\n'); + }); }); }); }); @@ -27,11 +66,7 @@ describe('Config', function () { // need to be able to write to process.version // but also to have access to process methods // so copy them over to a writable object - let tempProcess = {}; - for (let [prop, value] of _.toPairs(process)) { - tempProcess[prop] = value; - } - process = tempProcess; // eslint-disable-line no-global-assign + process = {...process}; // eslint-disable-line no-global-assign }); after(function () { process = _process; // eslint-disable-line no-global-assign @@ -45,6 +80,7 @@ describe('Config', function () { ]; for (const version of unsupportedVersions) { it(`should fail if node is ${version}`, function () { + // @ts-expect-error process.version = version; checkNodeOk.should.throw(); }); @@ -53,14 +89,17 @@ describe('Config', function () { describe('supported nodes', function () { it('should succeed if node is 12+', function () { + // @ts-expect-error process.version = 'v12.0.1'; checkNodeOk.should.not.throw(); }); it('should succeed if node is 13+', function () { + // @ts-expect-error process.version = 'v13.6.0'; checkNodeOk.should.not.throw(); }); it('should succeed if node is 14+', function () { + // @ts-expect-error process.version = 'v14.0.0'; checkNodeOk.should.not.throw(); }); @@ -70,17 +109,19 @@ describe('Config', function () { describe('warnNodeDeprecations', function () { let spy; before(function () { - spy = sinon.spy(logger, 'warn'); + spy = sandbox.spy(logger, 'warn'); }); beforeEach(function () { spy.resetHistory(); }); it('should not log a warning if node is 8+', function () { + // @ts-expect-error process.version = 'v8.0.0'; warnNodeDeprecations(); logger.warn.callCount.should.equal(0); }); it('should not log a warning if node is 9+', function () { + // @ts-expect-error process.version = 'v9.0.0'; warnNodeDeprecations(); logger.warn.callCount.should.equal(0); @@ -142,6 +183,7 @@ describe('Config', function () { validateTmpDir('/private/if_you_run_with_sudo_this_wont_fail').should.be.rejectedWith(/could not ensure/); }); it('should fail to use an undefined tmp dir', function () { + // @ts-expect-error validateTmpDir().should.be.rejectedWith(/could not ensure/); }); it('should be able to use a tmp dir with correct permissions', function () { @@ -165,7 +207,7 @@ describe('Config', function () { }); it('should not fail if process.argv[1] is undefined', async function () { - process.argv[1] = undefined; + delete process.argv[1]; let args = await getParser(); args.prog.should.be.equal('appium'); }); diff --git a/packages/appium/test/parser-specs.js b/packages/appium/test/parser-specs.js index 18c05c30e..67de11e76 100644 --- a/packages/appium/test/parser-specs.js +++ b/packages/appium/test/parser-specs.js @@ -94,7 +94,7 @@ describe('parser', function () { }); it('should parse --allow-insecure correctly', function () { - p.parseArgs([]).should.have.property('allowInsecure', undefined); + p.parseArgs([]).should.not.have.property('allowInsecure'); p.parseArgs(['--allow-insecure', '']).allowInsecure.should.eql([]); p.parseArgs(['--allow-insecure', 'foo']).allowInsecure.should.eql(['foo']); p.parseArgs(['--allow-insecure', 'foo,bar']).allowInsecure.should.eql(['foo', 'bar']); @@ -102,7 +102,7 @@ describe('parser', function () { }); it('should parse --deny-insecure correctly', function () { - p.parseArgs([]).should.have.property('denyInsecure', undefined); + p.parseArgs([]).should.not.have.property('denyInsecure'); p.parseArgs(['--deny-insecure', '']).denyInsecure.should.eql([]); p.parseArgs(['--deny-insecure', 'foo']).denyInsecure.should.eql(['foo']); p.parseArgs(['--deny-insecure', 'foo,bar']).denyInsecure.should.eql(['foo', 'bar']); @@ -161,7 +161,7 @@ describe('parser', function () { it('should not yet apply defaults', function () { const args = p.parseArgs([]); - args.driver.fake.should.eql({sillyWebServerHost: undefined, sillyWebServerPort: undefined}); + args.should.not.have.property('driver'); }); it('should nicely handle extensions w/ dashes in them', async function () { diff --git a/packages/appium/test/schema/cli-args-specs.js b/packages/appium/test/schema/cli-args-specs.js index 4f21dc1cb..9c8e71dee 100644 --- a/packages/appium/test/schema/cli-args-specs.js +++ b/packages/appium/test/schema/cli-args-specs.js @@ -20,7 +20,7 @@ describe('cli-args', function () { registerSchema(extType, extName, schema); } finalizeSchema(); - return _.fromPairs([...toParserArgs(opts)]); + return _.fromPairs([...toParserArgs()]); } beforeEach(resetSchema); @@ -40,10 +40,10 @@ describe('cli-args', function () { result = getArgs({schema, extName, extType}); }); - it('should return options containing `action` prop of `store_true` and no `type`', function () { + it('should return options containing `action` prop of `store_const` and no `type`', function () { expect(result['--plugin-blob-foo']).to.have.property( 'action', - 'store_true', + 'store_const', ); }); @@ -343,32 +343,6 @@ describe('cli-args', function () { }); }); }); - - describe('overrides', function () { - // this might be better some other way, IDK. I suspect this will be rarely used. - // since extensions can't actually call `toParserArgs()`, they can't set `overrides` anyway. - it('should set them via "dest" key', function () { - const schema = { - properties: { - foo: { - type: 'string', - }, - }, - type: 'object', - }; - - const result = getArgs({ - schema, - extName, - extType, - overrides: {'plugin.blob.foo': {enum: ['slug', 'snail']}}, - }); - expect(result['--plugin-blob-foo']).to.have.deep.property('enum', [ - 'slug', - 'snail', - ]); - }); - }); }); }); }); diff --git a/packages/appium/types/types.d.ts b/packages/appium/types/types.d.ts index 100ad44ab..db56359d5 100644 --- a/packages/appium/types/types.d.ts +++ b/packages/appium/types/types.d.ts @@ -160,6 +160,11 @@ interface MoreArgs { /** * If true, show the build info and exit */ + showBuildInfo: boolean; + + /** + * If true, show config and exit + */ showConfig: boolean; /**