Skip to content

Commit

Permalink
Fix an error propagation bug when a command fails from bad inputs.
Browse files Browse the repository at this point in the history
This fixes a bug where commands that had rejected promises as inputs
could not catch errors from those inputs. One example is trying to
catch a NoSuchElementError through a chained call to click:

driver.findElement(By.id('not-there')).click().thenCatch(function(e) {
if (e.code === NO_SUCH_ELEMENT) {
    // Handle element not found
}
});
  • Loading branch information
jleyba committed Sep 5, 2014
1 parent e0dbda0 commit 654bf24
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 17 deletions.
24 changes: 8 additions & 16 deletions javascript/webdriver/promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -423,24 +423,15 @@ webdriver.promise.Deferred = function(opt_canceller, opt_flow) {
}

if (!handled && state == webdriver.promise.Deferred.State_.REJECTED) {
pendingRejectionKey = propagateError(value);
flow.pendingRejections_ += 1;
pendingRejectionKey = flow.timer.setTimeout(function() {
pendingRejectionKey = null;
flow.pendingRejections_ -= 1;
flow.abortFrame_(value);
}, 0);
}
}

/**
* Propagates an unhandled rejection to the parent ControlFlow in a
* future turn of the JavaScript event loop.
* @param {*} error The error value to report.
* @return {number} The key for the registered timeout.
*/
function propagateError(error) {
flow.pendingRejections_ += 1;
return flow.timer.setTimeout(function() {
flow.pendingRejections_ -= 1;
flow.abortFrame_(error);
}, 0);
}

/**
* Notifies a single listener of this Deferred's change in state.
* @param {!webdriver.promise.Deferred.Listener_} listener The listener to
Expand Down Expand Up @@ -485,9 +476,10 @@ webdriver.promise.Deferred = function(opt_canceller, opt_flow) {
// The moment a listener is registered, we consider this deferred to be
// handled; the callback must handle any rejection errors.
handled = true;
if (pendingRejectionKey) {
if (pendingRejectionKey !== null) {
flow.pendingRejections_ -= 1;
flow.timer.clearTimeout(pendingRejectionKey);
pendingRejectionKey = null;
}

var deferred = new webdriver.promise.Deferred(cancel, flow);
Expand Down
166 changes: 166 additions & 0 deletions javascript/webdriver/test/webdriver_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1575,6 +1575,36 @@ function testExecuteScript_scriptReturnsAnError() {
}


function testExecuteScript_failsIfArgumentIsARejectedPromise() {
var testHelper = TestHelper.expectingSuccess().replayAll();

var callback = callbackHelper(assertIsStubError);

var arg = webdriver.promise.rejected(STUB_ERROR);
arg.thenCatch(goog.nullFunction); // Suppress default handler.

var driver = testHelper.createDriver();
driver.executeScript(goog.nullFunction, arg).thenCatch(callback);
testHelper.execute();
callback.assertCalled();
}


function testExecuteAsyncScript_failsIfArgumentIsARejectedPromise() {
var testHelper = TestHelper.expectingSuccess().replayAll();

var callback = callbackHelper(assertIsStubError);

var arg = webdriver.promise.rejected(STUB_ERROR);
arg.thenCatch(goog.nullFunction); // Suppress default handler.

var driver = testHelper.createDriver();
driver.executeAsyncScript(goog.nullFunction, arg).thenCatch(callback);
testHelper.execute();
callback.assertCalled();
}


function testFindElement_elementNotFound() {
var testHelper = TestHelper.
expectingFailure(expectedError(ECode.NO_SUCH_ELEMENT, 'Unable to find element')).
Expand Down Expand Up @@ -2058,6 +2088,23 @@ function testElementEquals_sendsRpcIfElementsHaveDifferentIds() {
callback.assertCalled();
}


function testElementEquals_failsIfAnInputElementCouldNotBeFound() {
var testHelper = TestHelper.expectingSuccess().replayAll();

var callback = callbackHelper(assertIsStubError);
var id = webdriver.promise.rejected(STUB_ERROR);
id.thenCatch(goog.nullFunction); // Suppress default handler.

var driver = testHelper.createDriver();
var a = new webdriver.WebElement(driver, {'ELEMENT': 'foo'});
var b = new webdriver.WebElementPromise(driver, id);

webdriver.WebElement.equals(a, b).thenCatch(callback);
testHelper.execute();
callback.assertCalled();
}

function testWaiting_waitSucceeds() {
var testHelper = TestHelper.expectingSuccess().
expect(CName.FIND_ELEMENTS, {'using':'id', 'value':'foo'}).
Expand Down Expand Up @@ -2245,3 +2292,122 @@ function testFetchingLogs() {
testHelper.execute();
pair.assertCallback();
}


function testCommandsFailIfInitialSessionCreationFailed() {
var testHelper = TestHelper.expectingSuccess().replayAll();
var navigateResult = callbackPair(null, assertIsStubError);
var quitResult = callbackPair(null, assertIsStubError);

var session = webdriver.promise.rejected(STUB_ERROR);

var driver = testHelper.createDriver(session);
driver.get('some-url').then(navigateResult.callback, navigateResult.errback);
driver.quit().then(quitResult.callback, quitResult.errback);

testHelper.execute();
navigateResult.assertErrback();
quitResult.assertErrback();
}


function testWebElementCommandsFailIfInitialDriverCreationFailed() {
var testHelper = TestHelper.expectingSuccess().replayAll();

var session = webdriver.promise.rejected(STUB_ERROR);
var callback = callbackHelper(assertIsStubError);

var driver = testHelper.createDriver(session);
driver.findElement(By.id('foo')).click().thenCatch(callback);
testHelper.execute();
callback.assertCalled();
}


function testWebElementCommansFailIfElementCouldNotBeFound() {
var testHelper = TestHelper.
expectingSuccess().
expect(CName.FIND_ELEMENT, {'using':'id', 'value':'foo'}).
andReturnError(ECode.NO_SUCH_ELEMENT,
{'message':'Unable to find element'}).
replayAll();

var callback = callbackHelper(
expectedError(ECode.NO_SUCH_ELEMENT, 'Unable to find element'));

var driver = testHelper.createDriver();
driver.findElement(By.id('foo')).click().thenCatch(callback);
testHelper.execute();
callback.assertCalled();
}


function testCannotFindChildElementsIfParentCouldNotBeFound() {
var testHelper = TestHelper.
expectingSuccess().
expect(CName.FIND_ELEMENT, {'using':'id', 'value':'foo'}).
andReturnError(ECode.NO_SUCH_ELEMENT,
{'message':'Unable to find element'}).
replayAll();

var callback = callbackHelper(
expectedError(ECode.NO_SUCH_ELEMENT, 'Unable to find element'));

var driver = testHelper.createDriver();
driver.findElement(By.id('foo'))
.findElement(By.id('bar'))
.findElement(By.id('baz'))
.thenCatch(callback);
testHelper.execute();
callback.assertCalled();
}


function testActionSequenceFailsIfInitialDriverCreationFailed() {
var testHelper = TestHelper.expectingSuccess().replayAll();

var session = webdriver.promise.rejected(STUB_ERROR);

// Suppress the default error handler so we can verify it propagates
// to the perform() call below.
session.thenCatch(goog.nullFunction);

var callback = callbackHelper(assertIsStubError);

var driver = testHelper.createDriver(session);
driver.actions().
mouseDown().
mouseUp().
perform().
thenCatch(callback);
testHelper.execute();
callback.assertCalled();
}


function testAlertCommandsFailIfAlertNotPresent() {
var testHelper = TestHelper
.expectingSuccess()
.expect(CName.GET_ALERT_TEXT)
.andReturnError(ECode.NO_SUCH_ALERT, {'message': 'no alert'})
.replayAll();

var driver = testHelper.createDriver();
var alert = driver.switchTo().alert();

var expectError = expectedError(ECode.NO_SUCH_ALERT, 'no alert');
var callbacks = [];
for (var key in webdriver.Alert.prototype) {
if (webdriver.Alert.prototype.hasOwnProperty(key)) {
var helper = callbackHelper(expectError);
callbacks.push(key, helper);
alert[key].call(alert).thenCatch(helper);
}
}

testHelper.execute();
for (var i = 0; i < callbacks.length - 1; i += 2) {
callbacks[i + 1].assertCalled(
'Error did not propagate for ' + callbacks[i]);
}
}
57 changes: 56 additions & 1 deletion javascript/webdriver/webdriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,14 +281,39 @@ webdriver.WebDriver.prototype.schedule = function(command, description) {
checkHasNotQuit();
command.setParameter('sessionId', this.session_);

// If any of the command parameters are rejected promises, those
// rejections may be reported as unhandled before the control flow
// attempts to execute the command. To ensure parameters errors
// propagate through the command itself, we resolve all of the
// command parameters now, but suppress any errors until the ControlFlow
// actually executes the command. This addresses scenarios like catching
// an element not found error in:
//
// driver.findElement(By.id('foo')).click().thenCatch(function(e) {
// if (e.code === bot.ErrorCode.NO_SUCH_ELEMENT) {
// // Do something.
// }
// });
var prepCommand = webdriver.WebDriver.toWireValue_(command.getParameters());
prepCommand.thenCatch(goog.nullFunction);

var flow = this.flow_;
var executor = this.executor_;
return flow.execute(function() {
// A call to WebDriver.quit() may have been scheduled in the same event
// loop as this |command|, which would prevent us from detecting that the
// driver has quit above. Therefore, we need to make another quick check.
// We still check above so we can fail as early as possible.
checkHasNotQuit();
return webdriver.WebDriver.executeCommand_(self.executor_, command);

// Retrieve resolved command parameters; any previously suppressed errors
// will now propagate up through the control flow as part of the command
// execution.
return prepCommand.then(function(parameters) {
command.setParameters(parameters);
return webdriver.promise.checkedNodeCall(
goog.bind(executor.execute, executor, command));
});
}, description).then(function(response) {
try {
bot.response.checkResponse(response);
Expand Down Expand Up @@ -2186,6 +2211,36 @@ webdriver.AlertPromise = function(driver, alert) {
return alert.getText();
});
};

/**
* Defers action until the alert has been located.
* @override
*/
this.accept = function() {
return alert.then(function(alert) {
return alert.accept();
});
};

/**
* Defers action until the alert has been located.
* @override
*/
this.dismiss = function() {
return alert.then(function(alert) {
return alert.dismiss();
});
};

/**
* Defers action until the alert has been located.
* @override
*/
this.sendKeys = function(text) {
return alert.then(function(alert) {
return alert.sendKeys(text);
});
};
};
goog.inherits(webdriver.AlertPromise, webdriver.Alert);

Expand Down

0 comments on commit 654bf24

Please sign in to comment.