feat!(appium): support driver-specific args via --driver-args
MODIFY
docs/en/advanced-concepts/migrating-to-appium-2.0.md
docs/en/advanced-concepts/migrating-to-appium-2.0.md
@@ -32,6 +32,28 @@ npm install -g appium --drivers=xcuitest,uiautomator2
This will install Appium and the two drivers for you in one go.
+#### :bangbang: Driver-specific command line options
+
+With Appium 1.x, command-line options specific to particular drivers were all hosted on the main Appium server. So, for example, `--chromedriver-executable` was a CLI parameter you could use with Appium to set the location of a specific Chromedriver version for use with, say, the UiAutomator2 driver.
+
+With Appium 2.x, all driver- and platform-specific CLI params have been moved to the drivers themselves. To access them, we use a single CLI param called `--driver-args`, whose value is a JSON string or a path to a JSON file, which when parsed is an object of the following form:
+
+```json
+{
+ "<driver-name>": {
+ "<arg-name>": <arg-value>,
+ ...
+ },
+ ...
+}
+```
+
+In other words, you can construct an object with parameters for one or more drivers by their name. The parameters which are available to use this way will be documented by the driver itself. So to see what has become of `--chromedriver-executable`, you can head to the UiAutomator2 driver documentation. In this case, we'd start Appium like this:
+
+```
+appium --driver-args='{"uiautomator2": {"chromedriverExecutable": "/path/to/chromedriver/binary"}}'
+```
+
#### :bangbang: Driver updates
In the past, to get updates to your iOS or Android drivers, you'd simply wait for those updates to be rolled into a new release of Appium, and then update your Appium version. With Appium 2.x, the Appium server and the Appium drivers are versioned and released separately. This means that drivers can be on their own release cadence and that you can get driver updates as they happen, rather than waiting for a new Appium server release. The way to check for driver updates is with the CLI:

MODIFY
docs/en/writing-running-appium/server-args.md
docs/en/writing-running-appium/server-args.md
# Appium server arguments
-Since Appium 1.5, many server arguments have been deprecated in favor of the [--default-capabilities flag](/docs/en/writing-running-appium/default-capabilities-arg.md).
+Since Appium 1.5, many server arguments have been removed in favor of the [--default-capabilities param](/docs/en/writing-running-appium/default-capabilities-arg.md).
-Usage: `node . [flags]`
+Since Appium 2.0, all driver-specific params have been made accessible via `--driver-args` instead of as their own separate params. See below for details on `--driver-args`.
-## Server flags
-All flags are optional, but some are required in conjunction with certain others.
+Usage: `appium [params]` (or `appium server [params]` to be explicit)
+## Server params
+All params are optional, but some are required in conjunction with certain others.
<expand_table>
-|Flag|Default|Description|Example|
+|Param|Default|Description|Example|
|----|-------|-----------|-------|
-|`--shell`|null|Enter REPL mode||
-|`--allow-cors`|false|Turn on CORS compatibility mode, which will allow connections to the Appium server from within websites hosted on any domain. Be careful when enabling this feature, since there is a potential security risk if you visit a website that uses a cross-domain request to initiate or introspect sessions on your running Appium server.||
|`-a`, `--address`|0.0.0.0|IP Address to listen on|`--address 0.0.0.0`|
|`-p`, `--port`|4723|port to listen on|`--port 4723`|
|`-pa`, `--base-path`|null|Initial path segment where the Appium/WebDriver API will be hosted. Every endpoint will be behind this segment.|`--base-path /wd/hub`|
+|`--driver-args`|`{}`|JSON object string (or path to JSON file) consiting of CLI arguments to send to specific drivers. Should be of the form `{"[driverName]": {"[driverArg1Name]": [driverArg1Value], ...}}`|`--driver-args '{"xcuitest": {"webkitDebugProxyPort": 1234}}'|
+|`--allow-cors`|false|Turn on CORS compatibility mode, which will allow connections to the Appium server from within websites hosted on any domain. Be careful when enabling this feature, since there is a potential security risk if you visit a website that uses a cross-domain request to initiate or introspect sessions on your running Appium server.||
|`-ca`, `--callback-address`|null|callback IP Address (default: same as --address)|`--callback-address 127.0.0.1`|
|`-cp`, `--callback-port`|null|callback port (default: same as port)|`--callback-port 4723`|
|`--session-override`|false|Enables session override (clobbering)||
@@ -28,14 +29,12 @@ All flags are optional, but some are required in conjunction with certain others
|`--log-no-colors`|false|Do not use colors in console output||
|`-G`, `--webhook`|null|Also send log output to this HTTP listener|`--webhook localhost:9876`|
|`--nodeconfig`|null|Configuration JSON file to register appium with selenium grid|`--nodeconfig /abs/path/to/nodeconfig.json`|
-
|`--show-config`|false|Show info about the appium server configuration and exit||
|`--no-perms-check`|false|Bypass Appium's checks to ensure we can read/write necessary files||
|`--strict-caps`|false|Cause sessions to fail if desired caps are sent in that Appium does not recognize as valid for the selected device||
|`--tmp`|null|Absolute path to directory Appium can use to manage temporary files, like built-in iOS apps it needs to move around. On *nix/Mac defaults to /tmp, on Windows defaults to C:\Windows\Temp||
|`--trace-dir`|null|Absolute path to directory Appium use to save ios instruments traces, defaults to <tmp dir>/appium-instruments||
|`--debug-log-spacing`|false|Add exaggerated spacing in logs to help with visual inspection||
-
|`-dc`, `--default-capabilities`|{}|Set the default desired capabilities, which will be set on each session unless overridden by received capabilities.|`--default-capabilities [ '{"app": "myapp.app", "deviceName": "iPhone Simulator"}' | /path/to/caps.json ]`|
|`--relaxed-security`|false|Disable additional security checks, so it is possible to use some advanced features, provided by drivers supporting this option. Only enable it if all the clients are in the trusted network and it is not the case if a client could potentially break out of the session sandbox. Can override enabling of specific features with --deny-insecure. See also the [security doc](/docs/en/writing-running-appium/security.md)||
|`--allow-insecure`|[]|Allow a list of features which are considered insecure and must be turned on explicitly by system administrators. Feature names are documented by the relevant server/driver. Should be a comma-separated list, or a path to a filename containing one feature name per line. Features listed in --deny-insecure will override anything listed here. Does not make sense to use in conjunction with --relaxed-security. See also the [security doc](/docs/en/writing-running-appium/security.md)|`--allow-insecure=foo,bar`|

MODIFY
packages/appium/lib/appium.js
packages/appium/lib/appium.js
@@ -5,7 +5,7 @@ import { findMatchingDriver } from './drivers';
import { BaseDriver, errors, isSessionCommand } from '@appium/base-driver';
import B from 'bluebird';
import AsyncLock from 'async-lock';
-import { parseExtensionArgs, parseCapsForInnerDriver, pullSettings } from './utils';
+import { getExtensionArgs, parseCapsForInnerDriver, pullSettings, validateExtensionArgs } from './utils';
import { util } from '@appium/support';
const desiredCapabilityConstraints = {
@@ -165,9 +165,9 @@ class AppiumDriver extends BaseDriver {
}
let runningDriversData, otherPendingDriversData;
- const parsedInnerDriverArgs = parseExtensionArgs(this.args.driverArgs, driverName);
- const d = new InnerDriver(this.args, true, parsedInnerDriverArgs);
+ const driverInstance = new InnerDriver(this.args, true);
+
// We want to assign security values directly on the driver. The driver
// should not read security values from `this.opts` because those values
// could have been set by a malicious user via capabilities, whereas we
@@ -176,23 +176,39 @@ class AppiumDriver extends BaseDriver {
log.info(`Applying relaxed security to '${InnerDriver.name}' as per ` +
`server command line argument. All insecure features will be ` +
`enabled unless explicitly disabled by --deny-insecure`);
- d.relaxedSecurityEnabled = true;
+ driverInstance.relaxedSecurityEnabled = true;
}
if (!_.isEmpty(this.args.denyInsecure)) {
log.info('Explicitly preventing use of insecure features:');
this.args.denyInsecure.map((a) => log.info(` ${a}`));
- d.denyInsecure = this.args.denyInsecure;
+ driverInstance.denyInsecure = this.args.denyInsecure;
}
if (!_.isEmpty(this.args.allowInsecure)) {
log.info('Explicitly enabling use of insecure features:');
this.args.allowInsecure.map((a) => log.info(` ${a}`));
- d.allowInsecure = this.args.allowInsecure;
+ driverInstance.allowInsecure = this.args.allowInsecure;
+ }
+
+ // Likewise, any driver-specific CLI args that were passed in should be assigned directly to
+ // the driver so that they cannot be mimicked by a malicious user sending in capabilities
+ const driverCliArgs = getExtensionArgs(this.args.driverArgs, driverName);
+ if (!_.isEmpty(driverCliArgs)) {
+ if (!_.has(InnerDriver, 'argsConstraints')) {
+ throw new Error(`You sent in CLI args for the ${driverName} driver, but the driver ` +
+ `does not define any`);
}
+ // parse/verify CLI args and merge them to this.args if OK or throw an exception
+ validateExtensionArgs(driverCliArgs, InnerDriver.argsConstraints);
+ driverInstance.cliArgs = driverCliArgs;
+ delete this.args.driverArgs;
+ log.debug(`Set CLI arguments on ${driverName} driver instance: ${JSON.stringify(driverCliArgs)}`);
+ }
+
// This assignment is required for correct web sockets functionality inside the driver
- d.server = this.server;
+ driverInstance.server = this.server;
try {
runningDriversData = await this.curSessionDataForDriver(InnerDriver);
} catch (e) {
@@ -201,43 +217,43 @@ class AppiumDriver extends BaseDriver {
await pendingDriversGuard.acquire(AppiumDriver.name, () => {
this.pendingDrivers[InnerDriver.name] = this.pendingDrivers[InnerDriver.name] || [];
otherPendingDriversData = this.pendingDrivers[InnerDriver.name].map((drv) => drv.driverData);
- this.pendingDrivers[InnerDriver.name].push(d);
+ this.pendingDrivers[InnerDriver.name].push(driverInstance);
});
try {
- [innerSessionId, dCaps] = await d.createSession(
+ [innerSessionId, dCaps] = await driverInstance.createSession(
processedJsonwpCapabilities,
reqCaps,
processedW3CCapabilities,
[...runningDriversData, ...otherPendingDriversData]
);
- protocol = d.protocol;
+ protocol = driverInstance.protocol;
await sessionsListGuard.acquire(AppiumDriver.name, () => {
- this.sessions[innerSessionId] = d;
+ this.sessions[innerSessionId] = driverInstance;
});
} finally {
await pendingDriversGuard.acquire(AppiumDriver.name, () => {
- _.pull(this.pendingDrivers[InnerDriver.name], d);
+ _.pull(this.pendingDrivers[InnerDriver.name], driverInstance);
});
}
- this.attachUnexpectedShutdownHandler(d, innerSessionId);
+ this.attachUnexpectedShutdownHandler(driverInstance, innerSessionId);
log.info(`New ${InnerDriver.name} session created successfully, session ` +
`${innerSessionId} added to master session list`);
// set the New Command Timeout for the inner driver
- d.startNewCommandTimeout();
+ driverInstance.startNewCommandTimeout();
// apply initial values to Appium settings (if provided)
- if (d.isW3CProtocol() && !_.isEmpty(w3cSettings)) {
+ if (driverInstance.isW3CProtocol() && !_.isEmpty(w3cSettings)) {
log.info(`Applying the initial values to Appium settings parsed from W3C caps: ` +
JSON.stringify(w3cSettings));
- await d.updateSettings(w3cSettings);
- } else if (d.isMjsonwpProtocol() && !_.isEmpty(jwpSettings)) {
+ await driverInstance.updateSettings(w3cSettings);
+ } else if (driverInstance.isMjsonwpProtocol() && !_.isEmpty(jwpSettings)) {
log.info(`Applying the initial values to Appium settings parsed from MJSONWP caps: ` +
JSON.stringify(jwpSettings));
- await d.updateSettings(jwpSettings);
+ await driverInstance.updateSettings(jwpSettings);
}
} catch (error) {
return {
@@ -408,7 +424,7 @@ class AppiumDriver extends BaseDriver {
return this.pluginClasses.map((PluginClass) => {
const name = `${PluginClass.pluginName} (${sessionId})`;
const opts = {
- pluginArgs: parseExtensionArgs(this.args.pluginArgs, PluginClass.pluginName),
+ pluginArgs: getExtensionArgs(this.args.pluginArgs, PluginClass.pluginName),
};
return new PluginClass(name, opts);
});

MODIFY
packages/appium/lib/utils.js
packages/appium/lib/utils.js
import _ from 'lodash';
import logger from './logger';
-import { processCapabilities, PROTOCOLS } from '@appium/base-driver';
-import { parseJsonStringOrFile } from './cli/parser-helpers';
+import { processCapabilities, PROTOCOLS, validateCaps } from '@appium/base-driver';
import { fs } from '@appium/support';
const W3C_APPIUM_PREFIX = 'appium';
@@ -34,18 +33,42 @@ function inspectObject (args) {
}
}
-function parseExtensionArgs (extensionArgs, extensionName) {
- if (!_.isString(extensionArgs)) {
+function getExtensionArgs (extensionArgs, extensionName) {
+ if (!_.has(extensionArgs, extensionName)) {
return {};
}
- const parsedExtensionArgs = parseJsonStringOrFile(extensionArgs);
- const extensionSpecificArgs = parsedExtensionArgs[extensionName];
- if (!_.isPlainObject(extensionSpecificArgs)) {
+ if (!_.isPlainObject(extensionArgs[extensionName])) {
throw new Error(`Driver or plugin arguments must be plain objects`);
}
- return extensionSpecificArgs;
+ return extensionArgs[extensionName];
}
+
+function ensureNoUnknownArgs (extensionArgs, argsConstraints) {
+ const knownArgNames = Object.keys(argsConstraints);
+ for (const argName of Object.keys(extensionArgs)) {
+ if (!knownArgNames.includes(argName)) {
+ throw new Error(`"${argName}" is not a recognized arg. Are you sure it's in the list ` +
+ `of supported args? ${JSON.stringify(knownArgNames)}`);
+ }
+ }
+}
+
+/**
+ * Takes in a set of driver/plugin args passed in by user, and arg constraints
+ * and throws an error if any arg is unknown or of the incorrect type
+ *
+ * @param {object} extensionArgs - Driver or Plugin specific args
+ * @param {object} argsConstraints - Constraints for arguments
+*/
+function validateExtensionArgs (extensionArgs, argsConstraints) {
+ if (!_.isEmpty(extensionArgs) && !_.isEmpty(argsConstraints)) {
+ ensureNoUnknownArgs(extensionArgs, argsConstraints);
+ validateCaps(extensionArgs, argsConstraints);
+ }
+}
+
+
/**
* Takes the caps that were provided in the request and translates them
* into caps that can be used by the inner drivers.
@@ -233,5 +256,6 @@ const rootDir = fs.findRoot(__dirname);
export {
inspectObject, parseCapsForInnerDriver, insertAppiumPrefixes, rootDir,
- getPackageVersion, pullSettings, removeAppiumPrefixes, parseExtensionArgs,
+ getPackageVersion, pullSettings, removeAppiumPrefixes, getExtensionArgs,
+ validateExtensionArgs
};

MODIFY
packages/appium/test/driver-e2e-specs.js
packages/appium/test/driver-e2e-specs.js
@@ -17,8 +17,10 @@ import sinon from 'sinon';
let TEST_SERVER;
let TEST_PORT;
-const FAKE_ARGS = {'sillyWebServerPort': 1234, 'host': 'hey'};
-const FAKE_DRIVER_ARGS = JSON.stringify({'fake': FAKE_ARGS});
+const sillyWebServerPort = 1234;
+const sillyWebServerHost = 'hey';
+const FAKE_ARGS = {sillyWebServerPort, sillyWebServerHost};
+const FAKE_DRIVER_ARGS = {fake: FAKE_ARGS};
const shouldStartServer = process.env.USE_RUNNING_SERVER !== '0';
const caps = W3C_PREFIXED_CAPS;
const wdOpts = {
@@ -91,7 +93,8 @@ describe('FakeDriver - via HTTP', function () {
const {sessionId} = driver;
try {
const {data} = await axios.get(`${baseUrl}/${sessionId}/fakedriverargs`);
- data.value.should.eql({});
+ should.not.exist(data.value.sillyWebServerPort);
+ should.not.exist(data.value.sillyWebServerHost);
} finally {
await driver.deleteSession();
}
@@ -99,15 +102,22 @@ describe('FakeDriver - via HTTP', function () {
});
describe('cli args handling for passed in args', function () {
- it('should recieve user cli args from a driver if arguments were passed in', async function () {
+ before(async function () {
await serverClose();
const args = {driverArgs: FAKE_DRIVER_ARGS};
await serverStart(args);
+ });
+ after(async function () {
+ await serverClose();
+ await serverStart();
+ });
+ it('should receive user cli args from a driver if arguments were passed in', async function () {
let driver = await wdio({...wdOpts, capabilities: caps});
const {sessionId} = driver;
try {
const {data} = await axios.get(`${baseUrl}/${sessionId}/fakedriverargs`);
- data.value.should.eql(FAKE_ARGS);
+ data.value.sillyWebServerPort.should.eql(sillyWebServerPort);
+ data.value.sillyWebServerHost.should.eql(sillyWebServerHost);
} finally {
await driver.deleteSession();
}

MODIFY
packages/appium/test/driver-specs.js
packages/appium/test/driver-specs.js
// transpile:mocha
import { AppiumDriver } from '../lib/appium';
+import { BaseDriver } from '@appium/base-driver';
import { FakeDriver } from '@appium/fake-driver';
import { BASE_CAPS, W3C_CAPS, W3C_PREFIXED_CAPS } from './helpers';
import _ from 'lodash';
@@ -13,16 +14,24 @@ const SESSION_ID = 1;
describe('AppiumDriver', function () {
describe('AppiumDriver', function () {
- function getDriverAndFakeDriver () {
- const appium = new AppiumDriver({});
- const fakeDriver = new FakeDriver();
+ function getDriverAndFakeDriver (appiumArgs = {}, DriverClass = FakeDriver) {
+ const appium = new AppiumDriver(appiumArgs);
+ const fakeDriver = new DriverClass();
const mockFakeDriver = sinon.mock(fakeDriver);
+ mockFakeDriver._fakeDriver = fakeDriver;
+ const mockedDriverReturnerClass = function Driver () {
+ return fakeDriver;
+ };
+ // need to specially assign args constraints since that is a static class method and won't
+ // make it through to appium otherwise with our mocked driver returner
+ if (!_.isUndefined(DriverClass.argsConstraints)) {
+ mockedDriverReturnerClass.argsConstraints = DriverClass.argsConstraints;
+ }
appium._findMatchingDriver = function () {
return {
- driver: function Driver () {
- return fakeDriver;
- },
+ driver: mockedDriverReturnerClass,
version: '1.2.3',
+ driverName: 'fake',
};
};
return [appium, mockFakeDriver];
@@ -149,6 +158,48 @@ describe('AppiumDriver', function () {
await appium.createSession(jsonwpCaps, undefined, w3cCaps);
mockFakeDriver.verify();
});
+ it('should error if you include driver args for a driver that doesnt define any', async function () {
+ class NoArgsDriver {}
+ const args = {driverArgs: {fake: {webkitDebugProxyPort: 1234}}};
+ [appium, mockFakeDriver] = getDriverAndFakeDriver(args, NoArgsDriver);
+ const {error} = await appium.createSession(undefined, undefined, W3C_CAPS);
+ error.should.match(/does not define any/);
+ });
+ it('should error if you include driver args a driver doesnt support', async function () {
+ class DiffArgsDriver extends BaseDriver {
+ static get argsConstraints () {
+ return {
+ randomArg: {
+ isNumber: true
+ }
+ };
+ }
+ }
+ const args = {driverArgs: {fake: {diffArg: 1234}}};
+ [appium, mockFakeDriver] = getDriverAndFakeDriver(args, DiffArgsDriver);
+ const {error} = await appium.createSession(undefined, undefined, W3C_CAPS);
+ error.should.match(/not a recognized arg/);
+ });
+ it('should validate args and put them on driver.cliArgs', async function () {
+ class ArgsDriver extends BaseDriver {
+ static get argsConstraints () {
+ return {
+ randomArg: {
+ isNumber: true
+ }
+ };
+ }
+ }
+ const args = {driverArgs: {fake: {randomArg: 1234}}};
+ [appium, mockFakeDriver] = getDriverAndFakeDriver(args, ArgsDriver);
+ const {value} = await appium.createSession(undefined, undefined, W3C_CAPS);
+ try {
+ mockFakeDriver._fakeDriver.cliArgs.should.eql({randomArg: 1234});
+ should.not.exist(appium.args.driverArgs);
+ } finally {
+ await appium.deleteSession(value[0]);
+ }
+ });
});
describe('deleteSession', function () {
let appium;

MODIFY
packages/appium/test/utils-specs.js
packages/appium/test/utils-specs.js
import {
parseCapsForInnerDriver, insertAppiumPrefixes, pullSettings,
- removeAppiumPrefixes, parseExtensionArgs,
+ removeAppiumPrefixes, getExtensionArgs, validateExtensionArgs
} from '../lib/utils';
import { BASE_CAPS, W3C_CAPS } from './helpers';
import _ from 'lodash';
-const FAKE_ARGS = `{"sillyWebServerPort":1234,"host":"hey"}`;
const FAKE_DRIVER_NAME = `fake`;
-const FAKE_DRIVER_ARGS = `{"${FAKE_DRIVER_NAME}": ${FAKE_ARGS}}`;
describe('utils', function () {
describe('parseCapsForInnerDriver()', function () {
@@ -213,41 +211,47 @@ describe('utils', function () {
});
});
- describe('parseExtensionArgs()', function () {
- it('should take a valid json string with a driver name and return a valid json object for that driver', function () {
- parseExtensionArgs(
- FAKE_DRIVER_ARGS,
- FAKE_DRIVER_NAME,
- ).should.eql(JSON.parse(FAKE_DRIVER_ARGS)[FAKE_DRIVER_NAME]);
- });
- it('should take a valid json string with an invalid driver name and throw an error', function () {
- const fakeDriverArgs = `{"xcuitest": ${FAKE_ARGS}}`;
- (() => parseExtensionArgs(fakeDriverArgs, FAKE_DRIVER_NAME)).should.throw();
- });
- it('should take an empty json object and return an empty object', function () {
- const fakeDriverArgs = {};
-
- parseExtensionArgs(
- fakeDriverArgs,
- FAKE_DRIVER_NAME,
- ).should.eql({});
+ describe('getExtensionArgs()', function () {
+ it('should return an empty object if extension not in args', function () {
+ getExtensionArgs({foo: {bar: 1234}}, FAKE_DRIVER_NAME).should.eql({});
+ });
+ it('should return the args for an extension', function () {
+ getExtensionArgs({foo: {bar: 1234}}, 'foo').should.eql({bar: 1234});
+ });
+ it('should throw an error if arg is not a plain object', function () {
+ const fakeDriverArgs = {fake: 1234};
+ (() => getExtensionArgs(fakeDriverArgs, FAKE_DRIVER_NAME,)).should.throw();
});
- it('should take an invalid json string and throw an error', function () {
- const fakeDriverArgs = `blah`;
-
- (() => parseExtensionArgs(fakeDriverArgs, FAKE_DRIVER_NAME)).should.throw();
});
- it('should take a valid json file with a driver name and return a valid json object for that driver', function () {
- const fakeDriverArgsPath = require.resolve('./fixtures/driverArgs.json');
- parseExtensionArgs(
- fakeDriverArgsPath,
- FAKE_DRIVER_NAME,
- ).should.eql(JSON.parse(FAKE_DRIVER_ARGS)[FAKE_DRIVER_NAME]);
+ describe('validateExtensionArgs', function () {
+ const webkitDebugProxyPort = 22222;
+ const wdaLocalPort = 8000;
+ const driverArgs = {wdaLocalPort, webkitDebugProxyPort};
+ const ARGS_CONSTRAINTS = {
+ webkitDebugProxyPort: {
+ isNumber: true
+ },
+ wdaLocalPort: {
+ isNumber: true
+ },
+ };
+
+ it('should not throw if args are valid', function () {
+ validateExtensionArgs(driverArgs, ARGS_CONSTRAINTS);
});
- it('should throw an error if arg is not a plain object', function () {
- const fakeDriverArgs = '{"fake": 1234}';
- (() => parseExtensionArgs(fakeDriverArgs, FAKE_DRIVER_NAME,)).should.throw();
+ it('should not throw if unrequired args are not included', function () {
+ validateExtensionArgs({webkitDebugProxyPort}, ARGS_CONSTRAINTS);
+ });
+ it('should throw if an arg is of the wrong type', function () {
+ should.throw(() =>
+ validateExtensionArgs({webkitDebugProxyPort, wdaLocalPort: 'abcd'}, ARGS_CONSTRAINTS)
+ );
+ });
+ it('should throw if an unrecognized arg is included', function () {
+ should.throw(() =>
+ validateExtensionArgs({webkitDebugProxyPort, foo: '1234'}, ARGS_CONSTRAINTS)
+ );
});
});
});

MODIFY
packages/base-driver/lib/basedriver/driver.js
packages/base-driver/lib/basedriver/driver.js
@@ -38,7 +38,28 @@ class BaseDriver extends Protocol {
*/
static baseVersion = BASEDRIVER_VER;
- constructor (opts = {}, shouldValidateCaps = true, driverArgs = {}) {
+ /**
+ * Override this getter in child driver classes
+ * if you'd like to expose any command line arguments.
+ * These arguments take precedence over any opts.
+ *
+ * @returns {Object} The map which represents constraints over
+ * supported command line arguments. The map should look similar
+ * to the desired capabilities constraints one, for example:
+ * {
+ * webkitDebugProxyPort: {
+ * isNumber: true
+ * },
+ * wdaLocalPort: {
+ * isNumber: true
+ * },
+ * }
+ */
+ static get argsConstraints () {
+ return {};
+ }
+
+ constructor (opts = {}, shouldValidateCaps = true) {
super();
// setup state
@@ -48,9 +69,6 @@ class BaseDriver extends Protocol {
this.originalCaps = null; // To give the original capabilities to reset
this.helpers = helpers;
- // allow for cli args to be used by drivers
- this.driverArgs = driverArgs;
-
// basePath is used for several purposes, for example in setting up
// proxying to other drivers, since we need to know what the base path
// of any incoming request might look like. We set it to the default

MODIFY
packages/fake-driver/lib/driver.js
packages/fake-driver/lib/driver.js
@@ -5,9 +5,19 @@ import { FakeApp } from './fake-app';
import commands from './commands';
class FakeDriver extends BaseDriver {
+ static get argsConstraints () {
+ return {
+ sillyWebServerPort: {
+ isNumber: true
+ },
+ sillyWebServerHost: {
+ isString: true
+ }
+ };
+ }
- constructor (opts = {}, shouldValidateCaps = true, driverArgs = {}) {
- super(opts, shouldValidateCaps, driverArgs);
+ constructor (opts = {}, shouldValidateCaps = true) {
+ super(opts, shouldValidateCaps);
this.appModel = null;
this.curContext = 'NATIVE_APP';
this.elMap = {};
@@ -15,8 +25,7 @@ class FakeDriver extends BaseDriver {
this.maxElId = 0;
this.caps = {};
this.fakeThing = null;
- // TODO: remove this line once we're relying completely on basedriver from appium 2.0
- this.driverArgs = driverArgs;
+ this.cliArgs = {};
this.desiredCapConstraints = {
'app': {
@@ -69,7 +78,7 @@ class FakeDriver extends BaseDriver {
async getFakeDriverArgs () {
await B.delay(1);
- return this.driverArgs;
+ return this.cliArgs;
}
static newMethodMap = {

feat!(appium): support driver-specific args via --driver-args

Co-authored-by: Ayan Khan <ayan@headspin.io>