Skip to content

Commit

Permalink
Add around hooks (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbpros committed Feb 22, 2012
1 parent e02b0b4 commit 2bc9537
Show file tree
Hide file tree
Showing 11 changed files with 508 additions and 143 deletions.
2 changes: 1 addition & 1 deletion features/cucumber-tck
Submodule cucumber-tck updated from 7b5b64 to 80eaf4
14 changes: 13 additions & 1 deletion features/step_definitions/cucumber_js_mappings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,24 @@ def write_world_function
def write_passing_hook hook_type
provide_cycle_logging_facilities
define_hook = hook_type.capitalize
append_support_code <<-EOF
if hook_type == "around"
append_support_code <<-EOF
this.#{define_hook}(function(runScenario) {
this.logCycleEvent('#{hook_type}-pre');
runScenario(function(callback) {
this.logCycleEvent('#{hook_type}-post');
callback();
});
});
EOF
else
append_support_code <<-EOF
this.#{define_hook}(function(callback) {
this.logCycleEvent('#{hook_type}');
callback();
});
EOF
end
end

def write_scenario options = {}
Expand Down
25 changes: 23 additions & 2 deletions features/step_definitions/cucumber_steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ var cucumberSteps = function() {
callback();
});

Given(/^a passing around hook$/, function(callback) {
this.stepDefinitions += "this.Around(function(runScenario) {\
world.logCycleEvent('around-pre');\
runScenario(function(callback) {\
world.logCycleEvent('around-post');\
callback();\
});\
});\n";
callback();
});

Given(/^the step "([^"]*)" has a failing mapping$/, function(stepName, callback) {
this.stepDefinitions += "Given(/^" + stepName + "$/, function(callback) {\
world.touchStep(\"" + stepName + "\");\
Expand Down Expand Up @@ -240,9 +251,19 @@ callback();\

Then(/^the (before|after) hook is fired (?:before|after) the scenario$/, function(hookType, callback) {
if (hookType == 'before')
this.assertCycleSequence(hookType, 'step');
this.assertCycleSequence(hookType, 'step 1');
else
this.assertCycleSequence('step', hookType);
this.assertCycleSequence('step 1', hookType);
callback();
});

Then(/^the around hook fires around the scenario$/, function(callback) {
this.assertCycleSequence('around-pre', 'step 1', 'around-post');
callback();
});

Then(/^the around hook is fired around the other hooks$/, function(callback) {
this.assertCycleSequence('around-pre', 'before', 'step 1', 'after', 'around-post');
callback();
});

Expand Down
2 changes: 1 addition & 1 deletion features/step_definitions/cucumber_world.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ proto.runFeatureWithSupportCodeSource = function runFeatureWithSupportCodeSource
proto.runAScenario = function runAScenario(callback) {
this.addScenario("", "Given a step");
this.stepDefinitions += "Given(/^a step$/, function(callback) {\
world.logCycleEvent('step');\
world.logCycleEvent('step 1');\
callback();\
});";
this.runFeature({}, callback);
Expand Down
21 changes: 5 additions & 16 deletions lib/cucumber/runtime/ast_tree_walker.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ var AstTreeWalker = function(features, supportCodeLibrary, listeners) {
supportCodeLibrary.instantiateNewWorld(function(world) {
self.setWorld(world);
self.witnessNewScenario();
var payload = { scenario: scenario };
var event = AstTreeWalker.Event(AstTreeWalker.SCENARIO_EVENT_NAME, payload);
var hookedUpScenarioVisit = self.hookUpFunction(
function(callback) { scenario.acceptVisitor(self, callback); }
var payload = { scenario: scenario };
var event = AstTreeWalker.Event(AstTreeWalker.SCENARIO_EVENT_NAME, payload);
var hookedUpScenarioVisit = supportCodeLibrary.hookUpFunctionWithWorld(
function(callback) { scenario.acceptVisitor(self, callback); },
world
);
self.broadcastEventAroundUserFunction(
event,
Expand Down Expand Up @@ -111,18 +112,6 @@ var AstTreeWalker = function(features, supportCodeLibrary, listeners) {
);
},

hookUpFunction: function hookUpFunction(hookedUpFunction) {
return function(callback) {
supportCodeLibrary.triggerBeforeHooks(self.getWorld(), function() {
hookedUpFunction(function() {
supportCodeLibrary.triggerAfterHooks(self.getWorld(), function() {
callback();
});
});
});
}
},

lookupStepDefinitionByName: function lookupStepDefinitionByName(stepName) {
return supportCodeLibrary.lookupStepDefinitionByName(stepName);
},
Expand Down
28 changes: 12 additions & 16 deletions lib/cucumber/support_code/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ var Library = function(supportCodeDefinition) {
var MISSING_WORLD_INSTANCE_ERROR = "World constructor called back without World instance.";
var Cucumber = require('../../cucumber');

var beforeHooks = Cucumber.Type.Collection();
var afterHooks = Cucumber.Type.Collection();
var stepDefinitions = Cucumber.Type.Collection();
var hooker = Cucumber.SupportCode.Library.Hooker();
var worldConstructor = Cucumber.SupportCode.WorldConstructor();

var self = {
Expand All @@ -24,26 +23,21 @@ var Library = function(supportCodeDefinition) {
return (stepDefinition != undefined);
},

defineBeforeHook: function defineBeforeHook(code) {
var beforeHook = Cucumber.SupportCode.Hook(code);
beforeHooks.add(beforeHook);
hookUpFunctionWithWorld: function hookUpFunctionWithWorld(userFunction, world) {
var hookedUpFunction = hooker.hookUpFunctionWithWorld(userFunction, world);
return hookedUpFunction;
},

triggerBeforeHooks: function(world, callback) {
beforeHooks.forEach(function(beforeHook, callback) {
beforeHook.invoke(world, callback);
}, callback);
defineAroundHook: function defineAroundHook(code) {
hooker.addAroundHookCode(code);
},

defineAfterHook: function defineAfterHook(code) {
var afterHook = Cucumber.SupportCode.Hook(code);
afterHooks.unshift(afterHook);
defineBeforeHook: function defineBeforeHook(code) {
hooker.addBeforeHookCode(code);
},

triggerAfterHooks: function(world, callback) {
afterHooks.forEach(function(afterHook, callback) {
afterHook.invoke(world, callback);
}, callback);
defineAfterHook: function defineAfterHook(code) {
hooker.addAfterHookCode(code);
},

defineStep: function defineStep(name, code) {
Expand All @@ -64,6 +58,7 @@ var Library = function(supportCodeDefinition) {
};

var supportCodeHelper = {
Around : self.defineAroundHook,
Before : self.defineBeforeHook,
After : self.defineAfterHook,
Given : self.defineStep,
Expand All @@ -77,4 +72,5 @@ var Library = function(supportCodeDefinition) {

return self;
};
Library.Hooker = require('./library/hooker');
module.exports = Library;
76 changes: 76 additions & 0 deletions lib/cucumber/support_code/library/hooker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
var Hooker = function() {
var Cucumber = require('../../../cucumber');

var aroundHooks = Cucumber.Type.Collection();
var beforeHooks = Cucumber.Type.Collection();
var afterHooks = Cucumber.Type.Collection();

var self = {
addAroundHookCode: function addAroundHookCode(code) {
var aroundHook = Cucumber.SupportCode.Hook(code);
aroundHooks.add(aroundHook);
},

addBeforeHookCode: function addBeforeHookCode(code) {
var beforeHook = Cucumber.SupportCode.Hook(code);
beforeHooks.add(beforeHook);
},

addAfterHookCode: function addAfterHookCode(code) {
var afterHook = Cucumber.SupportCode.Hook(code);
afterHooks.unshift(afterHook);
},

hookUpFunctionWithWorld: function hookUpFunctionWithWorld(userFunction, world) {
var hookedUpFunction = function(callback) {
var postScenarioAroundHookCallbacks = Cucumber.Type.Collection();
aroundHooks.forEach(callPreScenarioAroundHook, callBeforeHooks);

function callPreScenarioAroundHook(aroundHook, preScenarioAroundHookCallback) {
aroundHook.invoke(world, function(postScenarioAroundHookCallback) {
postScenarioAroundHookCallbacks.unshift(postScenarioAroundHookCallback);
preScenarioAroundHookCallback();
});
}

function callBeforeHooks() {
self.triggerBeforeHooks(world, callUserFunction);
}

function callUserFunction() {
userFunction(callAfterHooks);
}

function callAfterHooks() {
self.triggerAfterHooks(world, callPostScenarioAroundHooks);
}

function callPostScenarioAroundHooks() {
postScenarioAroundHookCallbacks.forEach(
callPostScenarioAroundHook,
callback
);
}

function callPostScenarioAroundHook(postScenarioAroundHookCallback, callback) {
postScenarioAroundHookCallback.call(world, callback);
}
};
return hookedUpFunction;
},

triggerBeforeHooks: function triggerBeforeHooks(world, callback) {
beforeHooks.forEach(function(beforeHook, callback) {
beforeHook.invoke(world, callback);
}, callback);
},

triggerAfterHooks: function triggerAfterHooks(world, callback) {
afterHooks.forEach(function(afterHook, callback) {
afterHook.invoke(world, callback);
}, callback);
}
};
return self;
};
module.exports = Hooker;
3 changes: 2 additions & 1 deletion lib/cucumber/type/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ var Collection = function() {
});
};
iterate();
}
},
length: function length() { return items.length; }
};
return self;
};
Expand Down
36 changes: 19 additions & 17 deletions spec/cucumber/runtime/ast_tree_walker_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,9 @@ describe("Cucumber.Runtime.AstTreeWalker", function() {

describe("visitScenario()", function() {
var scenario, callback;
var world;

beforeEach(function() {
scenario = createSpyWithStubs("Scenario AST element", {acceptVisitor: null});
scenario = createSpyWithStubs("scenario");
callback = createSpy("Callback");
spyOnStub(supportCodeLibrary, 'instantiateNewWorld');
});
Expand All @@ -177,21 +176,22 @@ describe("Cucumber.Runtime.AstTreeWalker", function() {
});

describe("on world instantiation completion", function() {
var worldInstantiationCompletionCallback, world, event, payload, hookedUpFunction;
var worldInstantiationCompletionCallback;
var world, event, payload;
var hookedUpScenarioVisit;

beforeEach(function() {
world = createSpy("world instance");
treeWalker.visitScenario(scenario, callback);
worldInstantiationCompletionCallback = supportCodeLibrary.instantiateNewWorld.mostRecentCall.args[0];

event = createSpy("Event");
payload = {scenario: scenario};
scenarioVisitWithHooks = createSpy("scenario visit with hooks");
spyOn(Cucumber.Runtime.AstTreeWalker, 'Event').andReturn(event);
spyOn(treeWalker, 'broadcastEventAroundUserFunction');
world = createSpy("world instance");
event = createSpy("scenario visit event");
hookedUpScenarioVisit = createSpy("hooked up scenario visit");
payload = {scenario: scenario};
spyOn(treeWalker, 'setWorld');
spyOn(treeWalker, 'witnessNewScenario');
spyOn(treeWalker, 'hookUpFunction').andReturn(scenarioVisitWithHooks);
spyOn(Cucumber.Runtime.AstTreeWalker, 'Event').andReturn(event);
spyOnStub(supportCodeLibrary, 'hookUpFunctionWithWorld').andReturn(hookedUpScenarioVisit);
spyOn(treeWalker, 'broadcastEventAroundUserFunction');
});

it("sets the new World instance", function() {
Expand All @@ -211,28 +211,30 @@ describe("Cucumber.Runtime.AstTreeWalker", function() {

it("hooks up a function", function() {
worldInstantiationCompletionCallback(world);
expect(treeWalker.hookUpFunction).toHaveBeenCalled();
expect(treeWalker.hookUpFunction).toHaveBeenCalledWithAFunctionAsNthParameter(1);
expect(supportCodeLibrary.hookUpFunctionWithWorld).toHaveBeenCalled();
expect(supportCodeLibrary.hookUpFunctionWithWorld).toHaveBeenCalledWithAFunctionAsNthParameter(1);
expect(supportCodeLibrary.hookUpFunctionWithWorld).toHaveBeenCalledWithValueAsNthParameter(world, 2);
});

describe("hooked up function", function() {
var hookedUpFunction, hookedUpFunctionCallback;

beforeEach(function() {
hookedUpFunctionCallback = createSpy("hooked up function callback");
worldInstantiationCompletionCallback(world);
hookedUpFunction = treeWalker.hookUpFunction.mostRecentCall.args[0];
hookedUpFunction = supportCodeLibrary.hookUpFunctionWithWorld.mostRecentCall.args[0];
hookedUpFunctionCallback = createSpy("hooked up function callback");
spyOnStub(scenario, 'acceptVisitor');
});

it("tells the scenario to accept the tree walker itself as a visitor", function() {
it("instructs the scenario to accept the tree walker as a visitor", function() {
hookedUpFunction(hookedUpFunctionCallback);
expect(scenario.acceptVisitor).toHaveBeenCalledWith(treeWalker, hookedUpFunctionCallback);
});
});

it("broadcasts the visit of the scenario", function() {
worldInstantiationCompletionCallback(world);
expect(treeWalker.broadcastEventAroundUserFunction).toHaveBeenCalledWith(event, scenarioVisitWithHooks, callback);
expect(treeWalker.broadcastEventAroundUserFunction).toHaveBeenCalledWith(event, hookedUpScenarioVisit, callback);
});
});
});
Expand Down
Loading

0 comments on commit 2bc9537

Please sign in to comment.