feat(base-driver): support appium:options capability to hold all appium caps
MODIFY
packages/base-driver/lib/basedriver/capabilities.js
packages/base-driver/lib/basedriver/capabilities.js
@@ -4,6 +4,10 @@ import { util } from '@appium/support';
import log from './logger';
import { errors } from '../protocol/errors';
+const APPIUM_VENDOR_PREFIX = 'appium:';
+const APPIUM_OPTS_CAP = 'options';
+const PREFIXED_APPIUM_OPTS_CAP = `${APPIUM_VENDOR_PREFIX}${APPIUM_OPTS_CAP}`;
+
// Takes primary caps object and merges it into a secondary caps object.
// (see https://www.w3.org/TR/webdriver/#dfn-merging-capabilities)
function mergeCaps (primary = {}, secondary = {}) {
@@ -221,8 +225,48 @@ function processCapabilities (caps, constraints = {}, shouldValidateCaps = true)
return matchedCaps;
}
+/**
+ * Return a copy of a capabilities object which has taken everything within the 'options'
+ * capability and promoted it to the top level. Note that this function is assumed to be run after
+ * all vendor prefixes have already been stripped from the top level. So we are dealing with e.g.
+ * 'options' and not 'appium:options' at this point. Any prefixes _inside_ the 'options' capability
+ * will themselves be stripped. This is designed as an internal function, not one to operate on
+ * user-constructed capabilities.
+ *
+ * @param {object} originalCaps - the capabilities to analyze and promote from 'options'
+ * @return {object} - the capabilities with 'options' promoted if necessary
+ */
+function promoteAppiumOptions (originalCaps) {
+ const appiumOptions = originalCaps[APPIUM_OPTS_CAP];
+ if (!appiumOptions) {
+ return originalCaps;
+ }
+
+ let caps = _.cloneDeep(originalCaps);
+ if (!_.isPlainObject(appiumOptions)) {
+ throw new errors.SessionNotCreatedError(`The ${APPIUM_OPTS_CAP} capability must be an object`);
+ }
+
+ // first get rid of any prefixes inside appium:options
+ stripAppiumPrefixes(appiumOptions);
+
+ // warn if we are going to overwrite any keys on the base caps object
+ const overwrittenKeys = _.intersection(Object.keys(caps), Object.keys(appiumOptions));
+ if (overwrittenKeys.length > 0) {
+ log.warn(`Found capabilities inside ${PREFIXED_APPIUM_OPTS_CAP} that will overwrite ` +
+ `capabilities at the top level: ${JSON.stringify(overwrittenKeys)}`);
+ }
+
+ // now just apply them to the main caps object
+ caps = {...caps, ...appiumOptions};
+
+ // and remove all traces of the options cap
+ delete caps[APPIUM_OPTS_CAP];
+ return caps;
+}
+
export {
- parseCaps, processCapabilities, validateCaps, mergeCaps,
- findNonPrefixedCaps, isStandardCap
+ parseCaps, processCapabilities, validateCaps, mergeCaps, APPIUM_VENDOR_PREFIX, APPIUM_OPTS_CAP,
+ findNonPrefixedCaps, isStandardCap, stripAppiumPrefixes, promoteAppiumOptions, PREFIXED_APPIUM_OPTS_CAP,
};

MODIFY
packages/base-driver/lib/basedriver/commands/session.js
packages/base-driver/lib/basedriver/commands/session.js
@@ -3,7 +3,7 @@ import _ from 'lodash';
import log from '../logger';
import { errors } from '../../protocol';
import { util } from '@appium/support';
-import { processCapabilities } from '../capabilities';
+import { processCapabilities, promoteAppiumOptions, APPIUM_OPTS_CAP, PREFIXED_APPIUM_OPTS_CAP } from '../capabilities';
let commands = {};
@@ -31,10 +31,15 @@ commands.createSession = async function createSession (jsonwpDesiredCapabilities
this.originalCaps = _.cloneDeep(w3cCapabilities);
log.debug(`Creating session with W3C capabilities: ${JSON.stringify(w3cCapabilities, null, 2)}`);
+
let caps;
try {
- const _capabilities = processCapabilities(w3cCapabilities, this.desiredCapConstraints, this.shouldValidateCaps);
- caps = fixCaps(_capabilities, this.desiredCapConstraints);
+ caps = processCapabilities(w3cCapabilities, this.desiredCapConstraints, this.shouldValidateCaps);
+ if (caps[APPIUM_OPTS_CAP]) {
+ log.debug(`Found ${PREFIXED_APPIUM_OPTS_CAP} capability present; will promote items inside to caps`);
+ caps = promoteAppiumOptions(caps);
+ }
+ caps = fixCaps(caps, this.desiredCapConstraints);
} catch (e) {
throw new errors.SessionNotCreatedError(e.message);
}
@@ -146,3 +151,4 @@ function fixCaps (originalCaps, desiredCapConstraints = {}) {
}
export default commands;
+export { promoteAppiumOptions };

MODIFY
packages/base-driver/test/basedriver/capabilities-specs.js
packages/base-driver/test/basedriver/capabilities-specs.js
-import { parseCaps, validateCaps, mergeCaps, processCapabilities, findNonPrefixedCaps } from '../../lib/basedriver/capabilities';
+import { parseCaps, validateCaps, mergeCaps, processCapabilities, findNonPrefixedCaps,
+ promoteAppiumOptions, APPIUM_OPTS_CAP, stripAppiumPrefixes } from '../../lib/basedriver/capabilities';
import _ from 'lodash';
import { desiredCapabilityConstraints } from '../../lib/basedriver/desired-caps';
@@ -434,4 +435,71 @@ describe('caps', function () {
findNonPrefixedCaps({alwaysMatch, firstMatch}).should.eql(['nonStandardOne', 'nonStandardTwo', 'nonStandardThree', 'nonStandardFour', 'nonStandardFive']);
});
});
+
+ describe('#promoteAppiumOptions', function () {
+ const appiumCaps = {
+ 'appium:platformVersion': '14.4',
+ 'appium:deviceName': 'iPhone 11',
+ 'appium:app': '/foo/bar.app.zip',
+ 'appium:automationName': 'XCUITest',
+ };
+ const nonAppiumCaps = {
+ platformName: 'iOS',
+ };
+ const appiumUnprefixedCaps = _.clone(appiumCaps);
+ stripAppiumPrefixes(appiumUnprefixedCaps);
+ const simpleCaps = {
+ ...nonAppiumCaps,
+ ...appiumUnprefixedCaps,
+ };
+ it('should do nothing to caps that dont include the options', function () {
+ promoteAppiumOptions(simpleCaps).should.eql(simpleCaps);
+ });
+ it('should promote options', function () {
+ const caps = {
+ ...nonAppiumCaps,
+ [APPIUM_OPTS_CAP]: {
+ ...appiumUnprefixedCaps,
+ }
+ };
+ promoteAppiumOptions(caps).should.eql(simpleCaps);
+ });
+ it('should get rid of prefixes if caps inside options are prefixed', function () {
+ const caps = {
+ ...nonAppiumCaps,
+ [APPIUM_OPTS_CAP]: {
+ ...appiumCaps,
+ }
+ };
+ promoteAppiumOptions(caps).should.eql(simpleCaps);
+ });
+ it('should preserve non-appium vendor prefixes', function () {
+ const googCaps = {'goog:chromeOptions': {foo: 'bar'}};
+ const caps = {
+ ...nonAppiumCaps,
+ [APPIUM_OPTS_CAP]: {
+ ...appiumCaps,
+ ...googCaps,
+ },
+ };
+ promoteAppiumOptions(caps).should.eql({
+ ...simpleCaps,
+ ...googCaps,
+ });
+ });
+ it('should overwrite caps found on the top level', function () {
+ const caps = {
+ ...nonAppiumCaps,
+ foo: 'bar',
+ [APPIUM_OPTS_CAP]: {
+ ...appiumCaps,
+ foo: 'baz',
+ }
+ };
+ promoteAppiumOptions(caps).should.eql({
+ ...simpleCaps,
+ foo: 'baz',
+ });
+ });
+ });
});

MODIFY
packages/base-driver/test/basedriver/driver-e2e-tests.js
packages/base-driver/test/basedriver/driver-e2e-tests.js
import _ from 'lodash';
-import { server, routeConfiguringFunction, DeviceSettings, errors } from '../..';
+import { BaseDriver, server, routeConfiguringFunction, DeviceSettings, errors } from '../..';
import {
MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY
} from '../../lib/constants';
import axios from 'axios';
import B from 'bluebird';
import getPort from 'get-port';
+import { PREFIXED_APPIUM_OPTS_CAP } from '../../lib/basedriver/capabilities';
function baseDriverE2ETests (DriverClass, defaultCaps = {}) {
let address = defaultCaps['appium:address'] ?? '127.0.0.1';
@@ -481,6 +482,26 @@ function baseDriverE2ETests (DriverClass, defaultCaps = {}) {
value.message.should.match(/.+50.+timeout.+/);
});
});
+
+ if (DriverClass === BaseDriver) {
+ // only run this test on basedriver, not other drivers which also use these tests, since we
+ // don't want them to try and start sessions with these random capabilities that are
+ // necessary to test the appium options logic
+ describe('special appium:options capability', function () {
+ it('should be able to start a session with caps held in appium:options', async function () {
+ const ret = await startSession({
+ platformName: 'iOS',
+ [PREFIXED_APPIUM_OPTS_CAP]: {
+ platformVersion: '11.4',
+ 'appium:deviceName': 'iPhone 11',
+ }
+ });
+ d.opts.platformVersion.should.eql('11.4');
+ d.opts.deviceName.should.eql('iPhone 11');
+ await endSession(ret.sessionId);
+ });
+ });
+ }
});
}

feat(base-driver): support appium:options capability to hold all appium caps