Handle /timeouts W3C + MJSONWP proxying
MODIFY
lib/basedriver/commands/timeout.js
lib/basedriver/commands/timeout.js
@@ -12,7 +12,7 @@ let commands = {}, helpers = {}, extensions = {};
const MIN_TIMEOUT = 0;
// If we define `commands.timeouts` instead of `commands.timeoutsW3C`, the command `timeouts` will be called
-// from other dirver's timeouts. See https://github.com/appium/appium-base-driver/pull/164
+// from other driver's timeouts. See https://github.com/appium/appium-base-driver/pull/164
// Arguments will be: [{"protocol":"W3C","implicit":30000}, "1dcfe021-8fc8-49bd-8dac-e986d3091b97", ...]
// eslint-disable-next-line no-unused-vars
commands.timeouts = async function (timeoutsObj) {

MODIFY
lib/jsonwp-proxy/proxy.js
lib/jsonwp-proxy/proxy.js
@@ -3,6 +3,7 @@ import { logger, util } from 'appium-support';
import request from 'request-promise';
import { getSummaryByCode } from '../jsonwp-status/status';
import { errors, isErrorType, errorFromMJSONWPStatusCode, errorFromW3CJsonCode } from '../protocol/errors';
+import BaseDriver from '../basedriver/driver';
const log = logger.getLogger('JSONWP Proxy');
@@ -10,6 +11,8 @@ const log = logger.getLogger('JSONWP Proxy');
const LOG_OBJ_LENGTH = 1024; // MAX LENGTH Logged to file / console
const DEFAULT_REQUEST_TIMEOUT = 240000;
+const {MJSONWP, W3C} = BaseDriver.DRIVER_PROTOCOL;
+
class JWProxy {
constructor (opts = {}) {
Object.assign(this, {
@@ -155,11 +158,88 @@ class JWProxy {
return [res, resBody];
}
+ /**
+ * W3C /timeouts can take as many as 3 timeout types at once, MJSONWP /timeouts only takes one
+ * at a time. So if we're using W3C and proxying to MJSONWP and there's more than one timeout type
+ * provided in the request, we need to do 3 proxies and combine the result
+ *
+ * @param {Object} body Request body
+ * @return {Array} Array of W3C + MJSONWP compatible timeout objects
+ */
+ getTimeoutRequestObjects (body) {
+ const {protocol} = body;
+ const timeoutObjects = [];
+ const baseObj = {
+ script: body.script,
+ pageLoad: body.pageLoad,
+ implicit: body.implicit,
+ };
+ if (protocol === W3C) {
+ for (let [type, ms] of _.toPairs(baseObj)) {
+ if (_.isNumber(ms)) {
+ timeoutObjects.push({
+ ...baseObj,
+ type,
+ ms,
+ });
+ }
+ }
+ return timeoutObjects;
+ }
+
+ return [{
+ ...baseObj,
+ [body.type]: body.ms,
+ type: body.type,
+ ms: body.ms,
+ }];
+ }
+
+ /**
+ * Proxy an array of timeout objects and merge the result
+ * @param {String} url Endpoint url
+ * @param {String} method Endpoint method
+ * @param {Object} body Request body
+ */
+ async proxySetTimeouts (url, method, body) {
+ let response, resBody;
+ for (let timeoutObj of this.getTimeoutRequestObjects(body)) {
+ [response, resBody] = await this.proxy(url, method, timeoutObj);
+
+ let protocol = this.getProtocolFromResBody(resBody);
+
+ // If we got a non-MJSONWP response, return the result, nothing left to do
+ if (protocol !== MJSONWP) {
+ return [response, resBody];
+ }
+
+ // If we got an error, return the error right away
+ if (response.statusCode >= 400) {
+ return [response, resBody];
+ }
+
+ // ...Otherwise, continue to the next timeouts call
+ }
+ return [response, resBody];
+ }
+
+ getProtocolFromResBody (resBody) {
+ if (_.isPlainObject(resBody) && util.hasValue(resBody.status)) {
+ return MJSONWP;
+ } else if (util.hasValue(resBody.value)) {
+ return W3C;
+ }
+ }
+
async command (url, method, body = null) {
let response;
let resBody;
try {
+ if (url.endsWith('timeouts') && method.toLowerCase() === 'post') {
+ [response, resBody] = await this.proxySetTimeouts(url, method, body);
+ } else {
[response, resBody] = await this.proxy(url, method, body);
+ }
} catch (err) {
if (isErrorType(err, errors.ProxyRequestError)) {
throw err.getActualError();
@@ -167,7 +247,8 @@ class JWProxy {
throw new errors.UnknownError(err.message);
}
resBody = util.safeJsonParse(resBody);
- if (_.isPlainObject(resBody) && util.hasValue(resBody.status)) {
+ let protocol = this.getProtocolFromResBody(resBody);
+ if (protocol === MJSONWP) {
// Got response in MJSONWP format
if (response.statusCode === 200 && resBody.status === 0) {
return resBody.value;
@@ -180,7 +261,7 @@ class JWProxy {
}
throw errorFromMJSONWPStatusCode(status, _.isEmpty(message) ? getSummaryByCode(status) : message);
}
- } else if (_.has(resBody, 'value')) {
+ } else if (protocol === W3C) {
// Got response in W3C format
if (response.statusCode < 300) {
return resBody.value;

MODIFY
test/jsonwp-proxy/proxy-specs.js
test/jsonwp-proxy/proxy-specs.js
@@ -111,6 +111,46 @@ describe('proxy', function () {
body.should.eql({status: 11, value: {message: 'Invisible element'}});
});
});
+ describe('getTimeoutRequestObjects', function () {
+ let j;
+ before(function () {
+ j = mockProxy();
+ });
+ it('should take W3C inputs and produce MJSONWP + W3C compatible objects', async function () {
+ let timeoutObjects = await j.getTimeoutRequestObjects({protocol: 'W3C', script: 100});
+ timeoutObjects.length.should.equal(1);
+ timeoutObjects[0].should.eql({script: 100, pageLoad: undefined, implicit: undefined, type: 'script', ms: 100});
+ });
+ it('should take multiple W3C timeouts and produce multiple MJSONWP + W3C compatible objects', async function () {
+ let [scriptTimeout, pageLoadTimeout, implicitTimeout] = await j.getTimeoutRequestObjects({protocol: 'W3C', script: 100, pageLoad: 200, implicit: 300});
+ const expectedObjBase = {script: 100, pageLoad: 200, implicit: 300};
+ scriptTimeout.should.eql({
+ ...expectedObjBase,
+ type: 'script',
+ ms: 100,
+ });
+ pageLoadTimeout.should.eql({
+ ...expectedObjBase,
+ type: 'pageLoad',
+ ms: 200,
+ });
+ implicitTimeout.should.eql({
+ ...expectedObjBase,
+ type: 'implicit',
+ ms: 300,
+ });
+ });
+ it('should create W3C timeouts from MJSONWP inputs', async function () {
+ let [timeoutObject] = await j.getTimeoutRequestObjects({protocol: 'MJSONWP', type: 'script', ms: 200});
+ timeoutObject.should.eql({
+ script: 200,
+ pageLoad: undefined,
+ implicit: undefined,
+ type: 'script',
+ ms: 200,
+ });
+ });
+ });
describe('command proxy', function () {
it('should successfully proxy command', async function () {
let j = mockProxy();

MODIFY
test/protocol/protocol-e2e-specs.js
test/protocol/protocol-e2e-specs.js
@@ -727,6 +727,106 @@ describe('Protocol', async function () {
w3cError.should.equal('unknown error');
stacktrace.should.match(/arbitrary stacktrace/);
});
+
+ // Tests for special timeout cases: Refer to https://github.com/appium/appium/issues/11016)
+ describe('/timeouts W3C and MJSONWP compatibility', function () {
+ it('should use a merged /timeouts object that is W3C and MJSONWP compatible', async function () {
+ addHandler(app, 'post', '/wd/hub/session/:sessionId/timeouts', (req, res) => {
+ const {type, ms, script} = req.body;
+ type.should.equal('script');
+ ms.should.equal(100);
+ script.should.equal(100);
+ res.json({
+ sessionId: req.params.sessionId,
+ value: true,
+ status: 0,
+ });
+ });
+ driver.timeouts = async (timeoutsObj) => await jwproxy.command('/timeouts', 'POST', timeoutsObj);
+ const {value} = await request.post(`${sessionUrl}/timeouts`, {
+ json: {
+ script: 100,
+ },
+ });
+ value.should.be.true;
+ });
+
+ it('should call MJSONWP /timeouts multiple times if multiple W3C timeout types are passed', async function () {
+ let timeoutsCalled = {};
+ let calls = 0;
+ addHandler(app, 'post', '/wd/hub/session/:sessionId/timeouts', (req, res) => {
+ const {type, ms, script, pageLoad, implicit} = req.body;
+ timeoutsCalled[type] = true;
+ calls++;
+
+ switch (type) {
+ case 'script':
+ ms.should.equal(100);
+ break;
+ case 'pageLoad':
+ ms.should.equal(200);
+ break;
+ case 'implicit':
+ ms.should.equal(300);
+ break;
+ default:
+ throw new Error(`Invalid type provided: ${type}`);
+ }
+
+ script.should.equal(100);
+ pageLoad.should.equal(200);
+ implicit.should.equal(300);
+ res.json({
+ sessionId: req.params.sessionId,
+ value: true,
+ status: 0,
+ });
+ });
+ driver.timeouts = async (timeoutsObj) => await jwproxy.command('/timeouts', 'POST', timeoutsObj);
+ const {value} = await request.post(`${sessionUrl}/timeouts`, {
+ json: {
+ script: 100,
+ pageLoad: 200,
+ implicit: 300,
+ },
+ });
+ timeoutsCalled.script.should.be.true;
+ timeoutsCalled.pageLoad.should.be.true;
+ timeoutsCalled.implicit.should.be.true;
+ calls.should.equal(3);
+ value.should.be.true;
+ });
+
+ it('should reject if one of the W3C timeouts being passed in fails on MJSONWP', async function () {
+ addHandler(app, 'post', '/wd/hub/session/:sessionId/timeouts', (req, res) => {
+ const {type} = req.body;
+
+ if (type === 'pageLoad') {
+ res.status(500).json({
+ sessionId,
+ status: 6,
+ value: 'A problem occurred on pageLoad timeout',
+ });
+ } else {
+ res.json({
+ sessionId: req.params.sessionId,
+ value: true,
+ status: 0,
+ });
+ }
+ });
+ driver.timeouts = async (timeoutsObj) => await jwproxy.command('/timeouts', 'POST', timeoutsObj);
+ const {statusCode, message} = await request.post(`${sessionUrl}/timeouts`, {
+ json: {
+ script: 100,
+ pageLoad: 200,
+ implicit: 300,
+ },
+ }).should.eventually.be.rejected;
+ statusCode.should.equal(HTTPStatusCodes.NOT_FOUND);
+ message.should.match(/A problem occurred/);
+ });
+ });
});
});
});

Handle /timeouts W3C + MJSONWP proxying (#231)