Try to automatically fix W3C capabilities by using existing JSONWP caps
MODIFY
lib/utils.js
lib/utils.js
@@ -3,6 +3,8 @@ import logger from './logger';
import { processCapabilities, BaseDriver } from 'appium-base-driver';
import findRoot from 'find-root';
+const W3C_APPIUM_PREFIX = 'appium';
+
function inspectObject (args) {
function getValueArray (obj, indent = ' ') {
if (!_.isObject(obj)) {
@@ -34,17 +36,31 @@ function inspectObject (args) {
/**
* Takes the caps that were provided in the request and translates them
* into caps that can be used by the inner drivers.
- * @param {Object} jsonwpCaps
+ *
+ * @param {Object} jsonwpCapabilities
* @param {Object} w3cCapabilities
* @param {Object} constraints
* @param {Object} defaultCapabilities
*/
-function parseCapsForInnerDriver (jsonwpCaps, w3cCapabilities, constraints={}, defaultCapabilities={}) {
+function parseCapsForInnerDriver (jsonwpCapabilities, w3cCapabilities, constraints={}, defaultCapabilities={}) {
// Check if the caller sent JSONWP caps, W3C caps, or both
- const hasW3CCaps = _.isPlainObject(w3cCapabilities);
- const hasJSONWPCaps = _.isPlainObject(jsonwpCaps);
+ const hasW3CCaps = _.isPlainObject(w3cCapabilities)
+ && _.has(w3cCapabilities, 'firstMatch') && _.has(w3cCapabilities, 'alwaysMatch');
+ const hasJSONWPCaps = _.isPlainObject(jsonwpCapabilities);
+ let protocol = null;
+ let desiredCaps = {};
+ let processedW3CCapabilities = null;
+ let processedJsonwpCapabilities = null;
- let protocol;
+ if (!hasJSONWPCaps && !hasW3CCaps) {
+ return {
+ desiredCaps,
+ processedJsonwpCapabilities,
+ processedW3CCapabilities,
+ protocol,
+ error: new Error('Either JSONWP or W3C capabilities should be provided'),
+ };
+ }
const {W3C, MJSONWP} = BaseDriver.DRIVER_PROTOCOL;
@@ -60,41 +76,64 @@ function parseCapsForInnerDriver (jsonwpCaps, w3cCapabilities, constraints={}, d
}
if (hasJSONWPCaps) {
- jsonwpCaps = {
+ jsonwpCapabilities = {
...defaultCapabilities,
- ...jsonwpCaps,
+ ...jsonwpCapabilities,
};
}
// Get MJSONWP caps
- let desiredCaps = {};
- let processedJsonwpCapabilities = null;
if (hasJSONWPCaps) {
protocol = MJSONWP;
- desiredCaps = jsonwpCaps;
- processedJsonwpCapabilities = {...desiredCaps};
+ desiredCaps = jsonwpCapabilities;
+ processedJsonwpCapabilities = removeW3CPrefixes({...desiredCaps});
}
// Get W3C caps
- let processedW3CCapabilities = null;
- let error;
if (hasW3CCaps) {
protocol = W3C;
// Call the process capabilities algorithm to find matching caps on the W3C
// (see: https://github.com/jlipps/simple-wd-spec#processing-capabilities)
+ let isFixingNeededForW3cCaps = false;
try {
desiredCaps = processCapabilities(w3cCapabilities, constraints, true);
- } catch (err) {
- if (jsonwpCaps) {
- logger.warn(`Could not parse W3C capabilities: ${err.message}. Falling back to JSONWP protocol.`);
+ } catch (error) {
+ if (!hasJSONWPCaps) {
return {
- desiredCaps: removeW3CPrefixes(desiredCaps),
- processedJsonwpCapabilities: removeW3CPrefixes(processedJsonwpCapabilities),
+ desiredCaps,
+ processedJsonwpCapabilities,
+ processedW3CCapabilities,
+ protocol,
+ error,
+ };
+ }
+ logger.info(`Could not parse W3C capabilities: ${error.message}`);
+ isFixingNeededForW3cCaps = true;
+ }
+
+ if (hasJSONWPCaps && !isFixingNeededForW3cCaps) {
+ const differingKeys = _.difference(_.keys(processedJsonwpCapabilities), _.keys(removeW3CPrefixes(desiredCaps)));
+ if (!_.isEmpty(differingKeys)) {
+ logger.info(`The following capabilities were provided in the JSONWP desired capabilities that are missing ` +
+ `in W3C capabilities: ${JSON.stringify(differingKeys)}`);
+ isFixingNeededForW3cCaps = true;
+ }
+ }
+
+ if (isFixingNeededForW3cCaps && hasJSONWPCaps) {
+ logger.info('Trying to fix W3C capabilities by merging them with JSONWP caps');
+ w3cCapabilities = fixW3cCapabilities(w3cCapabilities, jsonwpCapabilities);
+ try {
+ desiredCaps = processCapabilities(w3cCapabilities, constraints, true);
+ } catch (error) {
+ logger.warn(`Could not parse fixed W3C capabilities: ${error.message}. Falling back to JSONWP protocol`);
+ return {
+ desiredCaps: processedJsonwpCapabilities,
+ processedJsonwpCapabilities,
processedW3CCapabilities: null,
protocol: MJSONWP,
};
}
- error = err;
}
// Create a new w3c capabilities payload that contains only the matching caps in `alwaysMatch`
@@ -102,23 +141,51 @@ function parseCapsForInnerDriver (jsonwpCaps, w3cCapabilities, constraints={}, d
alwaysMatch: {...insertAppiumPrefixes(desiredCaps)},
firstMatch: [{}],
};
+ }
- // If we found extraneuous keys in JSONWP caps, fall back to JSONWP
- if (hasJSONWPCaps) {
- let differingKeys = _.difference(_.keys(jsonwpCaps), _.keys(desiredCaps));
- if (!_.isEmpty(differingKeys)) {
- logger.warn(`The following capabilities were provided in the JSONWP desired capabilities that are missing ` +
- `in W3C capabilities: ${JSON.stringify(differingKeys)}. Falling back to JSONWP protocol.`);
- protocol = MJSONWP;
- desiredCaps = jsonwpCaps;
- return {desiredCaps, processedJsonwpCapabilities, processedW3CCapabilities: null, protocol};
+ return {desiredCaps, processedJsonwpCapabilities, processedW3CCapabilities, protocol};
}
+
+/**
+ * This helper method tries to fix corrupted W3C capabilities by
+ * merging them to existing JSONWP capabilities.
+ *
+ * @param {Object} w3cCaps W3C capabilities
+ * @param {Object} jsonwpCaps JSONWP capabilities
+ * @returns {Object} Fixed W3C capabilities
+ */
+function fixW3cCapabilities (w3cCaps, jsonwpCaps) {
+ const result = {
+ firstMatch: w3cCaps.firstMatch || [],
+ alwaysMatch: w3cCaps.alwaysMatch || {},
+ };
+ const keysToInsert = _.keys(jsonwpCaps);
+ const removeMatchingKeys = (match) => {
+ _.pull(keysToInsert, match);
+ const colonIndex = match.indexOf(':');
+ if (colonIndex >= 0 && match.length > colonIndex) {
+ _.pull(keysToInsert, match.substring(colonIndex + 1));
+ }
+ if (keysToInsert.includes(`${W3C_APPIUM_PREFIX}:${match}`)) {
+ _.pull(keysToInsert, `${W3C_APPIUM_PREFIX}:${match}`);
+ }
+ };
+
+ for (const firstMatchEntry of result.firstMatch) {
+ for (const pair of _.toPairs(firstMatchEntry)) {
+ removeMatchingKeys(pair[0]);
}
}
- return {desiredCaps, processedJsonwpCapabilities, processedW3CCapabilities, protocol, error};
+ for (const pair of _.toPairs(result.alwaysMatch)) {
+ removeMatchingKeys(pair[0]);
}
+ for (const key of keysToInsert) {
+ result.alwaysMatch[key] = jsonwpCaps[key];
+ }
+ return result;
+}
/**
* Takes a capabilities objects and prefixes capabilities with `appium:`
@@ -144,7 +211,7 @@ function insertAppiumPrefixes (caps) {
if (STANDARD_CAPS.includes(name) || name.includes(':')) {
prefixedCaps[name] = value;
} else {
- prefixedCaps[`appium:${name}`] = value;
+ prefixedCaps[`${W3C_APPIUM_PREFIX}:${name}`] = value;
}
}
return prefixedCaps;

MODIFY
test/driver-e2e-specs.js
test/driver-e2e-specs.js
@@ -160,7 +160,7 @@ describe('FakeDriver - via HTTP', function () {
await request.delete({ url: `${baseUrl}/${value.sessionId}` });
});
- it('should accept a combo of W3C and JSONWP but use JSONWP if desiredCapabilities contains extraneous keys', async function () {
+ it('should accept a combo of W3C and JSONWP and if JSONWP has extraneous keys, they should be merged into W3C capabilities', async function () {
const combinedCaps = {
"desiredCapabilities": {
...caps,
@@ -176,17 +176,18 @@ describe('FakeDriver - via HTTP', function () {
};
const {sessionId, status, value} = await request.post({url: baseUrl, json: combinedCaps});
- status.should.exist;
- sessionId.should.exist;
- should.not.exist(value.sessionId);
- value.should.deep.equal({
+ should.not.exist(sessionId);
+ should.not.exist(status);
+ value.sessionId.should.exist;
+ value.capabilities.should.deep.equal({
...caps,
automationName: 'Fake',
anotherParam: 'Hello',
+ w3cParam: 'w3cParam',
});
// End session
- await request.delete({ url: `${baseUrl}/${sessionId}` });
+ await request.delete({ url: `${baseUrl}/${value.sessionId}` });
});
it('should reject bad W3C capabilities with a BadParametersError (400)', async function () {

MODIFY
test/driver-specs.js
test/driver-specs.js
@@ -147,8 +147,17 @@ describe('AppiumDriver', function () {
someOtherParam: 'someOtherParam',
};
+ let expectedW3cCaps = {
+ ...w3cCaps,
+ alwaysMatch: {
+ ...w3cCaps.alwaysMatch,
+ 'appium:automationName': 'Fake',
+ 'appium:someOtherParam': 'someOtherParam',
+ },
+ };
+
mockFakeDriver.expects("createSession")
- .once().withArgs(jsonwpCaps, undefined, null)
+ .once().withArgs(jsonwpCaps, undefined, expectedW3cCaps)
.returns([SESSION_ID, jsonwpCaps]);
await appium.createSession(jsonwpCaps, undefined, w3cCaps);

MODIFY
test/utils-specs.js
test/utils-specs.js
@@ -69,7 +69,7 @@ describe('utils', function () {
firstMatch: [{}],
});
});
- it('should fall back to MJSONWP caps if MJSONWP contains extraneous caps that aren not in W3C', function () {
+ it('should merge extraneous MJSONWP caps into W3C', function () {
let jsonwpCaps = {
...BASE_CAPS,
automationName: 'Fake',
@@ -78,12 +78,27 @@ describe('utils', function () {
alwaysMatch: {platformName: 'Fake', propertyName: 'PROP_NAME'},
});
- should.not.exist(processedW3CCapabilities);
- desiredCaps.should.eql(jsonwpCaps);
+ // We expect a combo of jsonwp caps and w3c provided caps with `appium:` prefix for non-standard caps
+ const expectedCaps = {};
+
+ for (let [key, value] of _.toPairs(jsonwpCaps)) {
+ if (key !== 'platformName') {
+ expectedCaps[`appium:${key}`] = value;
+ } else {
+ expectedCaps[key] = value;
+ }
+ }
+ expectedCaps['appium:propertyName'] = 'PROP_NAME';
+
+ processedW3CCapabilities.alwaysMatch.should.eql(expectedCaps);
+ desiredCaps.should.eql({
+ ...jsonwpCaps,
+ propertyName: 'PROP_NAME',
+ });
processedJsonwpCapabilities.should.eql(jsonwpCaps);
- protocol.should.equal('MJSONWP');
+ protocol.should.equal('W3C');
});
- it('should fall back to MJSONWP caps if W3C capabilities are invalid', function () {
+ it('should fix W3C caps by using MJSONWP if invalid W3C caps were provided', function () {
let w3cCapabilities = {
alwaysMatch: {platformName: 'Fake', propertyName: 'PROP_NAME'},
};
@@ -93,11 +108,10 @@ describe('utils', function () {
}
};
const {desiredCaps, processedJsonwpCapabilities, processedW3CCapabilities, protocol} = parseCapsForInnerDriver({...BASE_CAPS}, w3cCapabilities, constraints);
-
- should.not.exist(processedW3CCapabilities);
- desiredCaps.should.eql(BASE_CAPS);
+ processedW3CCapabilities.should.exist;
+ desiredCaps.should.eql({...BASE_CAPS, propertyName: 'PROP_NAME'});
processedJsonwpCapabilities.should.eql(BASE_CAPS);
- protocol.should.equal('MJSONWP');
+ protocol.should.equal('W3C');
});
});

Try to automatically fix W3C capabilities by using existing JSONWP caps (#10962)