diff --git a/lib/mjsonwp/errors.js b/lib/mjsonwp/errors.js index 710ec265d..980017fd9 100644 --- a/lib/mjsonwp/errors.js +++ b/lib/mjsonwp/errors.js @@ -1,17 +1,20 @@ import ES6Error from 'es6-error'; import _ from 'lodash'; -import { util } from 'appium-support'; +import { util, logger } from 'appium-support'; import HTTPStatusCodes from 'http-status-codes'; +const log = logger.getLogger('MJSONWP'); + // base error class for all of our errors class MJSONWPError extends ES6Error { - constructor (msg, jsonwpCode, w3cStatus = 400) { + constructor (msg, jsonwpCode, w3cStatus, error) { super(msg); this.jsonwpCode = jsonwpCode; + this.error = error || 'An unknown error has occurred'; if (this.jsonwpCode === null) { this.jsonwpCode = 13; } - this.w3cStatus = w3cStatus; + this.w3cStatus = w3cStatus || HTTPStatusCodes.BAD_REQUEST; } } @@ -23,11 +26,15 @@ class NoSuchDriverError extends MJSONWPError { static w3cStatus () { return HTTPStatusCodes.NOT_FOUND; } + static error () { + return 'The given session id is not in the list of active sessions'; + } constructor (err) { - super(err || 'A session is either terminated or not started', NoSuchDriverError.code(), NoSuchDriverError.w3cStatus()); + super(err || 'A session is either terminated or not started', NoSuchDriverError.code(), NoSuchDriverError.w3cStatus(), NoSuchDriverError.error()); } } + class NoSuchElementError extends MJSONWPError { static code () { return 7; @@ -35,9 +42,12 @@ class NoSuchElementError extends MJSONWPError { static w3cStatus () { return HTTPStatusCodes.NOT_FOUND; } + static error () { + return 'An element could not be located on the page using the given search parameters'; + } constructor (err) { super(err || 'An element could not be located on the page using the given ' + - 'search parameters.', NoSuchElementError.code(), NoSuchElementError.w3cStatus()); + 'search parameters.', NoSuchElementError.code(), NoSuchElementError.w3cStatus(), NoSuchElementError.error()); } } @@ -45,9 +55,12 @@ class NoSuchFrameError extends MJSONWPError { static code () { return 8; } + static error () { + return 'A command to switch to a frame could not be satisfied because the frame could not be found'; + } constructor (err) { super(err || 'A request to switch to a frame could not be satisfied because ' + - 'the frame could not be found.', NoSuchFrameError.code()); + 'the frame could not be found.', NoSuchFrameError.code(), null, NoSuchFrameError.error()); } } @@ -58,10 +71,13 @@ class UnknownCommandError extends MJSONWPError { static w3cStatus () { return HTTPStatusCodes.NOT_FOUND; } + static error () { + return 'A command could not be executed because the remote end is not aware of it'; + } constructor (err) { super(err || 'The requested resource could not be found, or a request was ' + 'received using an HTTP method that is not supported by the mapped ' + - 'resource.', UnknownCommandError.code(), UnknownCommandError.w3cStatus()); + 'resource.', UnknownCommandError.code(), UnknownCommandError.w3cStatus(), UnknownCommandError.error()); } } @@ -69,9 +85,12 @@ class StaleElementReferenceError extends MJSONWPError { static code () { return 10; } + static error () { + return 'A command failed because the referenced element is no longer attached to the DOM'; + } constructor (err) { super(err || 'An element command failed because the referenced element is no ' + - 'longer attached to the DOM.', StaleElementReferenceError.code()); + 'longer attached to the DOM.', StaleElementReferenceError.code(), null, StaleElementReferenceError.error()); } } @@ -89,10 +108,13 @@ class InvalidElementStateError extends MJSONWPError { static code () { return 12; } + static error () { + return 'A command could not be completed because the element is in an invalid state'; + } constructor (err) { super(err || 'An element command could not be completed because the element is ' + 'in an invalid state (e.g. attempting to click a disabled element).', - InvalidElementStateError.code()); + InvalidElementStateError.code(), null, InvalidElementStateError.error()); } } @@ -103,6 +125,9 @@ class UnknownError extends MJSONWPError { static w3cStatus () { return HTTPStatusCodes.INTERNAL_SERVER_ERROR; } + static error () { + return 'An unknown error occurred in the remote end while processing the command'; + } constructor (originalError) { let origMessage = originalError; if (originalError instanceof Error) { @@ -114,7 +139,7 @@ class UnknownError extends MJSONWPError { message = `${message} Original error: ${origMessage}`; } - super(message, UnknownError.code(), UnknownError.w3cStatus()); + super(message, UnknownError.code(), UnknownError.w3cStatus(), UnknownError.error()); } } @@ -122,8 +147,11 @@ class UnknownMethodError extends MJSONWPError { static w3cStatus () { return HTTPStatusCodes.METHOD_NOT_ALLOWED; } + static error () { + return 'The requested command matched a known URL but did not match an method for that URL'; + } constructor (err) { - super(err || 'Unknown method.', null, UnknownMethodError.w3cStatus()); + super(err || 'Unknown method.', null, UnknownMethodError.w3cStatus(), UnknownMethodError.error()); } } @@ -131,8 +159,12 @@ class UnsupportedOperationError extends MJSONWPError { static w3cStatus () { return HTTPStatusCodes.INTERNAL_SERVER_ERROR; } + static error () { + return 'A command that should have executed properly cannot be supported for some reason'; + } constructor (err) { - super(err || 'A server-side error occurred. Command cannot be supported.', null, UnsupportedOperationError.w3cStatus()); + super(err || 'A server-side error occurred. Command cannot be supported.', + null, UnsupportedOperationError.w3cStatus(), UnsupportedOperationError.error()); } } @@ -140,9 +172,39 @@ class ElementIsNotSelectableError extends MJSONWPError { static code () { return 15; } + static error () { + return 'An attempt was made to select an element that cannot be selected'; + } constructor (err) { super(err || 'An attempt was made to select an element that cannot be selected.', - ElementIsNotSelectableError.code()); + ElementIsNotSelectableError.code(), null, ElementIsNotSelectableError.error()); + } +} + +class ElementClickInterceptedError extends MJSONWPError { + static error () { + return 'The Element Click command could not be completed because the element receiving the events is obscuring the element that was requested clicked'; + } + constructor (err) { + super(err || ElementClickInterceptedError.error(), ElementIsNotSelectableError.code(), null, ElementClickInterceptedError.error()); + } +} + +class ElementNotInteractableError extends MJSONWPError { + static error () { + return 'A command could not be completed because the element is not pointer- or keyboard interactable'; + } + constructor (err) { + super(err || ElementNotInteractableError.error(), ElementIsNotSelectableError.code(), null, ElementNotInteractableError.error()); + } +} + +class InsecureCertificateError extends MJSONWPError { + static error () { + return 'An action caused the user agent to hit a certificate warning, which is usually the result of an expired or invalid TLS certificate.'; + } + constructor (err) { + super(err || InsecureCertificateError.error(), ElementIsNotSelectableError.code(), null, InsecureCertificateError.error()); } } @@ -151,11 +213,14 @@ class JavaScriptError extends MJSONWPError { return 17; } static w3cStatus () { - return 500; + return HTTPStatusCodes.INTERNAL_SERVER_ERROR; + } + static error () { + return 'An error occurred while executing JavaScript supplied by the user'; } constructor (err) { super(err || 'An error occurred while executing user supplied JavaScript.', - JavaScriptError.code(), JavaScriptError.w3cStatus()); + JavaScriptError.code(), JavaScriptError.w3cStatus(), JavaScriptError.error()); } } @@ -176,9 +241,12 @@ class TimeoutError extends MJSONWPError { static w3cStatus () { return HTTPStatusCodes.REQUEST_TIMEOUT; } + static error () { + return 'An operation did not complete before its timeout expired'; + } constructor (err) { super(err || 'An operation did not complete before its timeout expired.', - TimeoutError.code(), TimeoutError.w3cStatus()); + TimeoutError.code(), TimeoutError.w3cStatus(), TimeoutError.error()); } } @@ -186,15 +254,18 @@ class NoSuchWindowError extends MJSONWPError { static code () { return 23; } + static error () { + return 'A command to switch to a window could not be satisfied because the window could not be found'; + } constructor (err) { super(err || 'A request to switch to a different window could not be satisfied ' + - 'because the window could not be found.', NoSuchWindowError.code()); + 'because the window could not be found.', NoSuchWindowError.code(), null, NoSuchWindowError.error()); } } class InvalidArgumentError extends MJSONWPError { constructor (err) { - super(err || 'The arguments passed to the command are either invalid or malformed'); + super(err || 'The arguments passed to the command are either invalid or malformed', null, null, BadParametersError.error()); } } @@ -202,9 +273,21 @@ class InvalidCookieDomainError extends MJSONWPError { static code () { return 24; } + static error () { + return 'An illegal attempt was made to set a cookie under a different domain than the current page'; + } constructor (err) { super(err || 'An illegal attempt was made to set a cookie under a different ' + - 'domain than the current page.', InvalidCookieDomainError.code()); + 'domain than the current page.', InvalidCookieDomainError.code(), null, InvalidCookieDomainError.error()); + } +} + +class InvalidCoordinatesError extends MJSONWPError { + static error () { + return 'The coordinates provided to an interactions operation are invalid'; + } + constructor (err) { + super(err || InvalidCoordinatesError.error(), ElementIsNotSelectableError.code(), null, InvalidCoordinatesError.error()); } } @@ -212,8 +295,11 @@ class NoSuchCookieError extends MJSONWPError { static w3cStatus () { return HTTPStatusCodes.NOT_FOUND; } + static error () { + return 'No cookie matching the given path name was found amongst the associated cookies of the current browsing context’s active document'; + } constructor (err) { - super(err || 'Could not find cookie', null, NoSuchCookieError.w3cStatus()); + super(err || 'Could not find cookie', null, NoSuchCookieError.w3cStatus(), NoSuchCookieError.error()); } } @@ -224,9 +310,12 @@ class UnableToSetCookieError extends MJSONWPError { static w3cStatus () { return HTTPStatusCodes.INTERNAL_SERVER_ERROR; } + static error () { + return 'A command to set a cookie’s value could not be satisfied'; + } constructor (err) { super(err || 'A request to set a cookie\'s value could not be satisfied.', - UnableToSetCookieError.code(), UnableToSetCookieError.w3cStatus()); + UnableToSetCookieError.code(), UnableToSetCookieError.w3cStatus(), UnableToSetCookieError.error()); } } @@ -237,9 +326,12 @@ class UnexpectedAlertOpenError extends MJSONWPError { static w3cStatus () { return HTTPStatusCodes.INTERNAL_SERVER_ERROR; } + static error () { + return 'A modal dialog was open, blocking this operation'; + } constructor (err) { super(err || 'A modal dialog was open, blocking this operation', - UnexpectedAlertOpenError.code(), UnexpectedAlertOpenError.w3cStatus()); + UnexpectedAlertOpenError.code(), UnexpectedAlertOpenError.w3cStatus(), UnexpectedAlertOpenError.error()); } } @@ -260,9 +352,12 @@ class ScriptTimeoutError extends MJSONWPError { static w3cStatus () { return HTTPStatusCodes.REQUEST_TIMEOUT; } + static error () { + return 'A script did not complete before its timeout expired'; + } constructor (err) { super(err || 'A script did not complete before its timeout expired.', - ScriptTimeoutError.code(), ScriptTimeoutError.w3cStatus()); + ScriptTimeoutError.code(), ScriptTimeoutError.w3cStatus(), ScriptTimeoutError.error()); } } @@ -299,9 +394,12 @@ class InvalidSelectorError extends MJSONWPError { static code () { return 32; } + static error () { + return 'Argument was an invalid selector'; + } constructor (err) { super(err || 'Argument was an invalid selector (e.g. XPath/CSS).', - InvalidSelectorError.code()); + InvalidSelectorError.code(), null, InvalidSelectorError.error()); } } @@ -312,13 +410,16 @@ class SessionNotCreatedError extends MJSONWPError { static w3cStatus () { return HTTPStatusCodes.INTERNAL_SERVER_ERROR; } + static error () { + return 'A new session could not be created'; + } constructor (details) { let message = 'A new session could not be created.'; if (details) { message += ` Details: ${details}`; } - super(message, SessionNotCreatedError.code(), SessionNotCreatedError.w3cStatus()); + super(message, SessionNotCreatedError.code(), SessionNotCreatedError.w3cStatus(), SessionNotCreatedError.error()); } } @@ -327,11 +428,23 @@ class MoveTargetOutOfBoundsError extends MJSONWPError { return 34; } static w3cStatus () { - return 500; + return HTTPStatusCodes.INTERNAL_SERVER_ERROR; + } + static error () { + return 'The target for mouse interaction is not in the browser’s viewport and cannot be brought into that viewport'; } constructor (err) { super(err || 'Target provided for a move action is out of bounds.', - MoveTargetOutOfBoundsError.code(), MoveTargetOutOfBoundsError.w3cStatus()); + MoveTargetOutOfBoundsError.code(), MoveTargetOutOfBoundsError.w3cStatus(), MoveTargetOutOfBoundsError.error()); + } +} + +class NoSuchAlertError extends MJSONWPError { + static error () { + return 'An attempt was made to operate on a modal dialog when one was not open'; + } + constructor (err) { + super(err || NoSuchAlertError.error(), null, null, NoSuchAlertError.error()); } } @@ -354,6 +467,7 @@ class InvalidContextError extends MJSONWPError { } } +// Treat this as an alias for UnknownCommandError class NotYetImplementedError extends MJSONWPError { static code () { return 13; @@ -362,7 +476,8 @@ class NotYetImplementedError extends MJSONWPError { return HTTPStatusCodes.NOT_FOUND; // W3C equivalent is called 'Unknown Command' (A command could not be executed because the remote end is not aware of it) } constructor (err) { - super(err || 'Method has not yet been implemented', NotYetImplementedError.code(), NotYetImplementedError.w3cStatus()); + super(err || 'Method has not yet been implemented', + NotYetImplementedError.code(), NotYetImplementedError.w3cStatus(), UnknownCommandError.error()); } } @@ -382,14 +497,20 @@ class UnableToCaptureScreen extends MJSONWPError { static w3cStatus () { return HTTPStatusCodes.INTERNAL_SERVER_ERROR; } + static error () { + return 'A screen capture was made impossible'; + } constructor (err) { - super(err || 'Unable to capture screen', null, UnableToCaptureScreen.w3cStatus()); + super(err || 'Unable to capture screen', null, UnableToCaptureScreen.w3cStatus(), UnableToCaptureScreen.error()); } } - +// Equivalent to W3C InvalidArgumentError class BadParametersError extends ES6Error { + static error () { + return 'The arguments passed to a command are either invalid or malformed'; + } constructor (requiredParams, actualParams, errMessage) { let message; if (!errMessage) { @@ -427,6 +548,7 @@ class ProxyRequestError extends ES6Error { //returns actual error cause for request failure based on `jsonwp.status` return errorFromCode(this.jsonwp.status, this.jsonwp.value); } + // TODO: Handle W3C proxying return new UnknownError(this.message); } } @@ -434,6 +556,7 @@ class ProxyRequestError extends ES6Error { const errors = {NotYetImplementedError, NotImplementedError, BadParametersError, + InvalidArgumentError, NoSuchDriverError, NoSuchElementError, UnknownCommandError, @@ -441,14 +564,17 @@ const errors = {NotYetImplementedError, ElementNotVisibleError, InvalidElementStateError, UnknownError, - InvalidArgumentError, ElementIsNotSelectableError, + ElementClickInterceptedError, + ElementNotInteractableError, + InsecureCertificateError, JavaScriptError, XPathLookupError, TimeoutError, NoSuchWindowError, NoSuchCookieError, InvalidCookieDomainError, + InvalidCoordinatesError, UnableToSetCookieError, UnexpectedAlertOpenError, NoAlertOpenError, @@ -459,6 +585,7 @@ const errors = {NotYetImplementedError, InvalidSelectorError, SessionNotCreatedError, MoveTargetOutOfBoundsError, + NoSuchAlertError, NoSuchContextError, InvalidContextError, NoSuchFrameError, @@ -500,4 +627,58 @@ function errorFromCode (code, message) { return new UnknownError(message); } -export { MJSONWPError, errors, isErrorType, errorFromCode }; +function getResponseForW3CError (err) { + let httpStatus; + let error; + if (!err.w3cStatus) { + log.error(`Encountered internal error running command: ${err.stack}`); + err = new errors.UnknownError(err.message); + } + + if (isErrorType(err, errors.BadParametersError)) { + // respond with a 400 if we have bad parameters + log.debug(`Bad parameters: ${err}`); + httpStatus = HTTPStatusCodes.BAD_REQUEST; + error = 'The arguments passed to a command are either invalid or malformed'; + } else { + error = err.error; + } + httpStatus = err.w3cStatus; + let httpResBody = { + value: { + error, + message: err.message, + stacktrace: err.stack, + } + }; + return [httpStatus, httpResBody]; +} + +function getResponseForJsonwpError (err) { + let httpStatus = HTTPStatusCodes.INTERNAL_SERVER_ERROR; + let httpResBody = { + status: err.jsonwpCode, + value: { + message: err.message + } + }; + + if (isErrorType(err, errors.BadParametersError)) { + // respond with a 400 if we have bad parameters + log.debug(`Bad parameters: ${err}`); + httpStatus = HTTPStatusCodes.BAD_REQUEST; + httpResBody = err.message; + } else if (isErrorType(err, errors.NotYetImplementedError) || + isErrorType(err, errors.NotImplementedError)) { + // respond with a 501 if the method is not implemented + httpStatus = HTTPStatusCodes.NOT_IMPLEMENTED; + } else if (isErrorType(err, errors.NoSuchDriverError)) { + // respond with a 404 if there is no driver for the session + httpStatus = HTTPStatusCodes.NOT_FOUND; + } + + + return [httpStatus, httpResBody]; +} + +export { MJSONWPError, errors, isErrorType, errorFromCode, getResponseForW3CError, getResponseForJsonwpError }; diff --git a/lib/mjsonwp/mjsonwp.js b/lib/mjsonwp/mjsonwp.js index 2e7302211..0aabb7180 100644 --- a/lib/mjsonwp/mjsonwp.js +++ b/lib/mjsonwp/mjsonwp.js @@ -1,7 +1,7 @@ import _ from 'lodash'; import { logger, util } from 'appium-support'; import { validators } from './validators'; -import { errors, isErrorType, MJSONWPError, errorFromCode } from './errors'; +import { errors, isErrorType, MJSONWPError, errorFromCode, getResponseForW3CError, getResponseForJsonwpError } from './errors'; import { METHOD_MAP, NO_SESSION_ID_COMMANDS } from './routes'; import { renameKey } from '../basedriver/helpers'; import B from 'bluebird'; @@ -166,46 +166,6 @@ function makeArgs (requestParams, jsonObj, payloadParams, protocol) { return args; } -function getResponseForJsonwpError (err) { - let httpStatus = 500; - let httpResBody = { - status: err.jsonwpCode, - value: { - message: err.message - } - }; - - if (isErrorType(err, errors.BadParametersError)) { - // respond with a 400 if we have bad parameters - log.debug(`Bad parameters: ${err}`); - httpStatus = 400; - httpResBody = err.message; - } else if (isErrorType(err, errors.NotYetImplementedError) || - isErrorType(err, errors.NotImplementedError)) { - // respond with a 501 if the method is not implemented - httpStatus = 501; - } else if (isErrorType(err, errors.NoSuchDriverError)) { - // respond with a 404 if there is no driver for the session - httpStatus = 404; - } - - - return [httpStatus, httpResBody]; -} - -function getResponseForW3CError (err) { - let httpStatus; - if (!err.w3cStatus) { - log.error(`Encountered internal error running command: ${err.stack}`); - err = new errors.UnknownError(err.message); - } - httpStatus = err.w3cStatus; - let httpResBody = err.message; - return [httpStatus, httpResBody]; -} - - - function routeConfiguringFunction (driver) { if (!driver.sessionExists) { throw new Error('Drivers used with MJSONWP must implement `sessionExists`'); diff --git a/test/mjsonwp/errors-specs.js b/test/mjsonwp/errors-specs.js index fa8855455..0103bc09e 100644 --- a/test/mjsonwp/errors-specs.js +++ b/test/mjsonwp/errors-specs.js @@ -1,4 +1,5 @@ import { errors, errorFromCode } from '../..'; +import { getResponseForW3CError } from '../../lib/mjsonwp/errors'; import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import _ from 'lodash'; @@ -177,3 +178,38 @@ describe('w3c Status Codes', function () { (new errors.NoSuchFrameError()).should.have.property('w3cStatus', 400); }); }); +describe('.getResponseForW3CError', function () { + it('should return an error, message and stacktrace for just a generic exception', function () { + try { + throw new Error('Some random error'); + } catch (e) { + const [httpStatus, httpResponseBody] = getResponseForW3CError(e); + httpStatus.should.equal(500); + const {error, message, stacktrace} = httpResponseBody.value; + message.should.match(/Some random error/); + error.should.equal('An unknown error occurred in the remote end while processing the command'); + stacktrace.should.match(/at getResponseForW3CError/); + stacktrace.should.match(/Some random error/); + stacktrace.should.match(/errors-specs.js/); + } + }); + it('should return an error, message and stacktrace for a NoSuchElementError', function () { + const noSuchElementError = new errors.NoSuchElementError('specific error message'); + const [httpStatus, httpResponseBody] = getResponseForW3CError(noSuchElementError); + httpStatus.should.equal(404); + const {error, message, stacktrace} = httpResponseBody.value; + error.should.equal('An element could not be located on the page using the given search parameters'); + message.should.match(/specific error message/); + stacktrace.should.match(/errors-specs.js/); + }); + it('should handle BadParametersError', function () { + const badParamsError = new errors.BadParametersError('__FOO__', '__BAR__', '__HELLO_WORLD__'); + const [httpStatus, httpResponseBody] = getResponseForW3CError(badParamsError); + httpStatus.should.equal(400); + const {error, message, stacktrace} = httpResponseBody.value; + error.should.equal('The arguments passed to a command are either invalid or malformed'); + message.should.match(/__BAR__/); + message.should.match(/__HELLO_WORLD__/); + stacktrace.should.match(/errors-specs.js/); + }); +}); \ No newline at end of file diff --git a/test/mjsonwp/mjsonwp-e2e-specs.js b/test/mjsonwp/mjsonwp-e2e-specs.js index 1791afa68..393192b93 100644 --- a/test/mjsonwp/mjsonwp-e2e-specs.js +++ b/test/mjsonwp/mjsonwp-e2e-specs.js @@ -426,34 +426,51 @@ describe('MJSONWP', async function () { }); it(`should throw 400 Bad Parameters exception if the parameters are bad`, async function () { - const {message, statusCode} = await request.post(`${sessionUrl}/actions`, { + const {statusCode, error} = await request.post(`${sessionUrl}/actions`, { json: { bad: 'params', } }).should.eventually.be.rejected; - message.should.match(/Parameters were incorrect/); statusCode.should.equal(400); + + const {error:w3cError, message, stacktrace} = error.value; + message.should.match(/Parameters were incorrect/); + stacktrace.should.match(/mjsonwp.js/); + w3cError.should.be.a.string; + w3cError.should.equal(errors.BadParametersError.error()); }); it(`should throw 404 Not Found exception if the command hasn't been implemented yet`, async function () { - const {message, statusCode} = await request.post(`${sessionUrl}/actions`, { + const {statusCode, error} = await request.post(`${sessionUrl}/actions`, { json: { actions: [], } }).should.eventually.be.rejected; - message.should.match(/Method has not yet been implemented/); statusCode.should.equal(404); + + const {error:w3cError, message, stacktrace} = error.value; + message.should.match(/Method has not yet been implemented/); + stacktrace.should.match(/mjsonwp.js/); + w3cError.should.be.a.string; + w3cError.should.equal(errors.UnknownCommandError.error()); + message.should.match(/Method has not yet been implemented/); }); it(`should throw 500 Unknown Error if the command throws an unexpected exception`, async function () { driver.performActions = () => { throw new Error(`Didn't work`); }; - const {message, statusCode} = await request.post(`${sessionUrl}/actions`, { + const {statusCode, error} = await request.post(`${sessionUrl}/actions`, { json: { actions: [], } }).should.eventually.be.rejected; - message.should.match(/500[\w\W]*Didn't work/); statusCode.should.equal(500); + + const {error:w3cError, message, stacktrace} = error.value; + stacktrace.should.match(/mjsonwp.js/); + w3cError.should.be.a.string; + w3cError.should.equal(errors.UnknownError.error()); + message.should.match(/Didn't work/); + delete driver.performActions; }); @@ -496,7 +513,7 @@ describe('MJSONWP', async function () { it(`should fail with a 408 error if it throws a TimeoutError exception`, async () => { sinon.stub(driver, 'setUrl', () => { throw new errors.TimeoutError; }); - let {statusCode} = await request({ + let {statusCode, error} = await request({ url: `${sessionUrl}/url`, method: 'POST', json: { @@ -504,6 +521,13 @@ describe('MJSONWP', async function () { } }).should.eventually.be.rejected; statusCode.should.equal(408); + + const {error:w3cError, message, stacktrace} = error.value; + stacktrace.should.match(/mjsonwp.js/); + w3cError.should.be.a.string; + w3cError.should.equal(errors.TimeoutError.error()); + message.should.match(/An operation did not complete before its timeout expired/); + sinon.restore(driver, 'setUrl'); });