Skip to content
Browse files

Handle asynchronous exceptions (close #51)

  • Loading branch information...
1 parent 4848550 commit 9091129df4866cf705fb4ea7fb488f81e7cb96c2 @jbpros jbpros committed
View
15 features/asynchronous_failing_steps.feature
@@ -1,6 +1,6 @@
Feature: Asynchronous failing steps
- Scenario: see failing asynchronous scenarios
+ Scenario: see asynchronously failing scenarios
Given the following feature:
"""
Feature: a feature
@@ -11,3 +11,16 @@ Feature: Asynchronous failing steps
And the step "I divide 10 by 0" has a mapping asynchronously failing with the message "Divide by 0, uh?"
When Cucumber runs the feature
Then the scenario called "a failing scenario" is reported as failing
+
+ @untestable-on-self
+ Scenario: see asynchronously failing scenarios with exception
+ Given the following feature:
+ """
+ Feature: a feature
+ Scenario: a failing scenario
+ When I divide 10 by 0
+ Then the result is 9
+ """
+ And the step "I divide 10 by 0" has a mapping asynchronously failing through an exception with the message "Divide by 0, uh?"
+ When Cucumber runs the feature
+ Then the scenario called "a failing scenario" is reported as failing
View
4 features/step_definitions/cucumber_js_mappings.rb
@@ -61,6 +61,10 @@ def write_asynchronously_failing_mapping_with_message(step_name, message)
append_step_definition(step_name, "setTimeout(function() { callback.fail('#{message}');}, 10);")
end
+ def write_asynchronously_failing_mapping_through_exception_with_message(step_name, message)
+ append_step_definition(step_name, "setTimeout(function() { throw new Error('#{message}');}, 10);")
+ end
+
def write_mapping_incrementing_world_variable_by_value(step_name, increment_value)
append_step_definition(step_name, "this.variable += #{increment_value}; callback();")
end
View
4 features/step_definitions/cucumber_steps.js
@@ -86,8 +86,8 @@ var cucumberSteps = function() {
world.touchStep(\"" + stepName + "\");\
setTimeout(function() {callback.fail(new Error('" + message + "'));}, 10);\
});\n";
- callback();
-});
+ callback();
+ });
Given(/^the step "([^"]*)" has a pending mapping$/, function(stepName, callback) {
this.stepDefinitions += "Given(/^" + stepName + "$/, function(callback) {\
View
4 features/step_definitions/cucumber_steps.rb
@@ -21,6 +21,10 @@
write_asynchronously_failing_mapping_with_message(step_name, message)
end
+Given /^the step "([^"]*)" has a mapping asynchronously failing through an exception with the message "([^"]*)"$/ do |step_name, message|
+ write_asynchronously_failing_mapping_through_exception_with_message(step_name, message)
+end
+
Given /^a custom World constructor calling back with an explicit object$/ do
write_custom_world_constructor_calling_back_with_explicit_object
end
View
25 lib/cucumber/support_code/step_definition.js
@@ -21,29 +21,37 @@ var StepDefinition = function(pattern, code) {
},
invoke: function invoke(step, world, callback) {
+ var cleanUp = function cleanUp() {
+ Cucumber.Util.Exception.unregisterUncaughtExceptionHandler(handleException);
+ };
+
var codeCallback = function() {
var successfulStepResult = Cucumber.Runtime.SuccessfulStepResult({step: step});
+ cleanUp();
callback(successfulStepResult);
};
codeCallback.pending = function pending(reason) {
var pendingStepResult = Cucumber.Runtime.PendingStepResult({step: step, pendingReason: reason});
+ cleanUp();
callback(pendingStepResult);
};
codeCallback.fail = function fail(failureReason) {
var failureException = failureReason || new Error(StepDefinition.UNKNOWN_STEP_FAILURE_MESSAGE);
var failedStepResult = Cucumber.Runtime.FailedStepResult({step: step, failureException: failureException});
+ cleanUp();
callback(failedStepResult);
};
- var parameters = self.buildInvocationParameters(step, codeCallback);
+ var parameters = self.buildInvocationParameters(step, codeCallback);
+ var handleException = self.buildExceptionHandlerToCodeCallback(codeCallback);
+ Cucumber.Util.Exception.registerUncaughtExceptionHandler(handleException);
+
try {
code.apply(world, parameters);
} catch (exception) {
- if (exception)
- Cucumber.Debug.warn(exception.stack || exception, 'exception inside feature', 3);
- codeCallback.fail(exception);
+ handleException(exception);
}
},
@@ -58,6 +66,15 @@ var StepDefinition = function(pattern, code) {
}
parameters.push(callback);
return parameters;
+ },
+
+ buildExceptionHandlerToCodeCallback: function buildExceptionHandlerToCodeCallback(codeCallback) {
+ var exceptionHandler = function handleScenarioException(exception) {
+ if (exception)
+ Cucumber.Debug.warn(exception.stack || exception, 'exception inside feature', 3);
+ codeCallback.fail(exception);
+ };
+ return exceptionHandler;
}
};
return self;
View
1 lib/cucumber/util.js
@@ -1,5 +1,6 @@
var Util = {};
Util.Arguments = require('./util/arguments');
+Util.Exception = require('./util/exception');
Util.RegExp = require('./util/reg_exp');
Util.String = require('./util/string');
module.exports = Util;
View
17 lib/cucumber/util/exception.js
@@ -0,0 +1,17 @@
+var Exception = {
+ registerUncaughtExceptionHandler: function registerUncaughtExceptionHandler(exceptionHandler) {
+ if (process.on)
+ process.on('uncaughtException', exceptionHandler);
+ else
+ window.onerror = exceptionHandler;
+ },
+
+ unregisterUncaughtExceptionHandler: function unregisterUncaughtExceptionHandler(exceptionHandler) {
+ if (process.removeListener)
+ process.removeListener('uncaughtException', exceptionHandler);
+ else
+ window.onerror = void(0);
+ }
+};
+
+module.exports = Exception;
View
82 spec/cucumber/support_code/step_definition_spec.js
@@ -81,14 +81,17 @@ describe("Cucumber.SupportCode.StepDefinition", function() {
describe("invoke()", function() {
var step, world, callback;
- var parameters;
+ var parameters, exceptionHandler;
beforeEach(function() {
- step = createSpy("step");
- world = createSpy("world");
- callback = createSpy("callback");
- parameters = createSpy("code execution parameters");
+ step = createSpy("step");
+ world = createSpy("world");
+ callback = createSpy("callback");
+ parameters = createSpy("code execution parameters");
+ exceptionHandler = createSpy("exception handler");
+ spyOn(Cucumber.Util.Exception, 'registerUncaughtExceptionHandler');
spyOn(stepDefinition, 'buildInvocationParameters').andReturn(parameters);
+ spyOn(stepDefinition, 'buildExceptionHandlerToCodeCallback').andReturn(exceptionHandler);
spyOn(stepDefinitionCode, 'apply');
});
@@ -99,6 +102,20 @@ describe("Cucumber.SupportCode.StepDefinition", function() {
expect(stepDefinition.buildInvocationParameters).toHaveBeenCalledWithAFunctionAsNthParameter(2);
});
+ it("builds an exception handler for the code callback", function() {
+ stepDefinition.invoke(step, world, callback);
+ expect(stepDefinition.buildExceptionHandlerToCodeCallback).toHaveBeenCalledWithAFunctionAsNthParameter(1);
+
+ var codeExecutionCallbackPassedToParameterBuilder = stepDefinition.buildInvocationParameters.mostRecentCall.args[1];
+ var codeExecutionCallbackPassedToExceptionHandlerBuilder = stepDefinition.buildExceptionHandlerToCodeCallback.mostRecentCall.args[0];
+ expect(codeExecutionCallbackPassedToExceptionHandlerBuilder).toBe(codeExecutionCallbackPassedToParameterBuilder);
+ });
+
+ it("registers the exception handler for uncaught exceptions", function () {
+ stepDefinition.invoke(step, world, callback);
+ expect(Cucumber.Util.Exception.registerUncaughtExceptionHandler).toHaveBeenCalledWith(exceptionHandler);
+ });
+
it("calls the step definition code with the parameters and World as 'this'", function() {
stepDefinition.invoke(step, world, callback);
expect(stepDefinitionCode.apply).toHaveBeenCalledWith(world, parameters);
@@ -113,6 +130,7 @@ describe("Cucumber.SupportCode.StepDefinition", function() {
codeExecutionCallback = stepDefinition.buildInvocationParameters.mostRecentCall.args[1];
successfulStepResult = createSpy("successful step result");
spyOn(Cucumber.Runtime, 'SuccessfulStepResult').andReturn(successfulStepResult);
+ spyOn(Cucumber.Util.Exception, 'unregisterUncaughtExceptionHandler');
});
it("creates a successful step result", function() {
@@ -120,6 +138,11 @@ describe("Cucumber.SupportCode.StepDefinition", function() {
expect(Cucumber.Runtime.SuccessfulStepResult).toHaveBeenCalledWith({step: step});
});
+ it("unregisters the exception handler", function() {
+ codeExecutionCallback();
+ expect(Cucumber.Util.Exception.unregisterUncaughtExceptionHandler).toHaveBeenCalledWith(exceptionHandler);
+ });
+
it("calls back", function() {
codeExecutionCallback();
expect(callback).toHaveBeenCalledWith(successfulStepResult);
@@ -147,6 +170,11 @@ describe("Cucumber.SupportCode.StepDefinition", function() {
expect(Cucumber.Runtime.PendingStepResult).toHaveBeenCalledWith({step: step, pendingReason: pendingReason});
});
+ it("unregisters the exception handler", function() {
+ codeExecutionCallback.pending(pendingReason);
+ expect(Cucumber.Util.Exception.unregisterUncaughtExceptionHandler).toHaveBeenCalledWith(exceptionHandler);
+ });
+
it("calls back", function() {
codeExecutionCallback.pending(pendingReason);
expect(callback).toHaveBeenCalledWith(pendingStepResult);
@@ -176,6 +204,11 @@ describe("Cucumber.SupportCode.StepDefinition", function() {
});
});
+ it("unregisters the exception handler", function() {
+ codeExecutionCallback.fail(failureReason);
+ expect(Cucumber.Util.Exception.unregisterUncaughtExceptionHandler).toHaveBeenCalledWith(exceptionHandler);
+ });
+
it("calls back", function() {
codeExecutionCallback.fail(failureReason);
expect(callback).toHaveBeenCalledWith(failedStepResult);
@@ -184,23 +217,16 @@ describe("Cucumber.SupportCode.StepDefinition", function() {
});
describe("when the step definition code throws an exception", function() {
- var failedStepResult, failureException;
+ var failureException;
beforeEach(function() {
failureException = createSpy("I am a failing step definition");
- failedStepResult = createSpy("failed step result");
stepDefinitionCode.apply.andThrow(failureException);
- spyOn(Cucumber.Runtime, 'FailedStepResult').andReturn(failedStepResult);
});
- it("creates a new failed step result", function() {
+ it("handles the exception with the exception handler", function() {
stepDefinition.invoke(step, world, callback);
- expect(Cucumber.Runtime.FailedStepResult).toHaveBeenCalledWith({step: step, failureException: failureException});
- });
-
- it("calls back with the step result", function() {
- stepDefinition.invoke(step, world, callback);
- expect(callback).toHaveBeenCalledWith(failedStepResult);
+ expect(exceptionHandler).toHaveBeenCalledWith(failureException);
});
});
});
@@ -286,4 +312,30 @@ describe("Cucumber.SupportCode.StepDefinition", function() {
expect(stepDefinition.buildInvocationParameters(step, callback)).toBe(matches);
});
});
+
+ describe("buildExceptionHandlerToCodeCallback()", function () {
+ var codeCallback, exceptionHandler;
+
+ beforeEach(function() {
+ codeCallback = createSpyWithStubs("code callback", {fail: null});
+ exceptionHandler = stepDefinition.buildExceptionHandlerToCodeCallback(codeCallback);
+ });
+
+ it("returns an exception handler", function() {
+ expect(exceptionHandler).toBeAFunction();
+ });
+
+ describe("returned exception handler", function () {
+ var exception;
+
+ beforeEach(function () {
+ exception = createSpy("exception");
+ });
+
+ it("calls back as a failure with the exception", function () {
+ exceptionHandler(exception);
+ expect(codeCallback.fail).toHaveBeenCalledWith(exception);
+ });
+ });
+ });
});
View
79 spec/cucumber/util/exception_spec.js
@@ -0,0 +1,79 @@
+require('../../support/spec_helper');
+
+describe("Cucumber.Util.Arguments", function() {
+ var UNCAUGHT_EXCEPTION_EVENT = 'uncaughtException';
+
+ var Cucumber = requireLib('cucumber');
+
+ describe(".registerUncaughtExceptionHandler()", function () {
+ var exceptionHandler;
+
+ describe("in a Node.js environment", function() {
+ beforeEach(function () {
+ exceptionHandler = createSpy("exception handler");
+ spyOn(process, 'on');
+ });
+
+ it("registers the exception handler to the process's 'uncaughtException' event", function () {
+ Cucumber.Util.Exception.registerUncaughtExceptionHandler(exceptionHandler);
+ expect(process.on).toHaveBeenCalledWith(UNCAUGHT_EXCEPTION_EVENT, exceptionHandler);
+ });
+ });
+
+ describe("in a browser environment", function() {
+ var previousOn;
+
+ beforeEach(function () {
+ previousOn = process.on;
+ process.on = void(0);
+ exceptionHandler = createSpy("exception handler");
+ global.window = createSpy("window");
+ });
+
+ afterEach(function () {
+ process.on = previousOn;
+ });
+
+ it("registers the exception handler to the windows's 'onerror' event handler", function () {
+ Cucumber.Util.Exception.registerUncaughtExceptionHandler(exceptionHandler);
+ expect(window.onerror).toBe(exceptionHandler);
+ });
+ });
+ });
+
+ describe(".unregisterUncaughtExceptionHandler()", function () {
+ var exceptionHandler;
+
+ describe("in a Node.js environment", function() {
+ beforeEach(function () {
+ exceptionHandler = createSpy("exception handler");
+ spyOn(process, 'removeListener');
+ });
+
+ it("registers the exception handler to the process's 'uncaughtException' event", function () {
+ Cucumber.Util.Exception.unregisterUncaughtExceptionHandler(exceptionHandler);
+ expect(process.removeListener).toHaveBeenCalledWith(UNCAUGHT_EXCEPTION_EVENT, exceptionHandler);
+ });
+ });
+
+ describe("in a browser environment", function() {
+ var previousRemoveListener;
+
+ beforeEach(function () {
+ previousRemoveListener = process.removeListener;
+ process.removeListener = void(0);
+ exceptionHandler = createSpy("exception handler");
+ global.window = createSpyWithStubs("window", {onerror: exceptionHandler});
+ });
+
+ afterEach(function () {
+ process.removeListener = previousRemoveListener;
+ });
+
+ it("registers the exception handler to the windows's 'onerror' event handler", function () {
+ Cucumber.Util.Exception.unregisterUncaughtExceptionHandler(exceptionHandler);
+ expect(window.onerror).toBeUndefined();
+ });
+ });
+ });
+});

0 comments on commit 9091129

Please sign in to comment.
Something went wrong with that request. Please try again.