diff --git a/features/cucumber-tck b/features/cucumber-tck index 7b5b64277..80eaf4d64 160000 --- a/features/cucumber-tck +++ b/features/cucumber-tck @@ -1 +1 @@ -Subproject commit 7b5b642774561ddb25ff9c88269245a51917cd27 +Subproject commit 80eaf4d64e0545c504353c47e214219c65b81b9c diff --git a/features/step_definitions/cucumber_js_mappings.rb b/features/step_definitions/cucumber_js_mappings.rb index e4a7f117e..13ed6ffe6 100644 --- a/features/step_definitions/cucumber_js_mappings.rb +++ b/features/step_definitions/cucumber_js_mappings.rb @@ -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 = {} diff --git a/features/step_definitions/cucumber_steps.js b/features/step_definitions/cucumber_steps.js index 6ac1f4c6a..811f93f56 100644 --- a/features/step_definitions/cucumber_steps.js +++ b/features/step_definitions/cucumber_steps.js @@ -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 + "\");\ @@ -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(); }); diff --git a/features/step_definitions/cucumber_world.js b/features/step_definitions/cucumber_world.js index 75f46267b..69b8386ce 100644 --- a/features/step_definitions/cucumber_world.js +++ b/features/step_definitions/cucumber_world.js @@ -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); diff --git a/lib/cucumber/runtime/ast_tree_walker.js b/lib/cucumber/runtime/ast_tree_walker.js index bde35f8aa..d150c7910 100644 --- a/lib/cucumber/runtime/ast_tree_walker.js +++ b/lib/cucumber/runtime/ast_tree_walker.js @@ -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, @@ -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); }, diff --git a/lib/cucumber/support_code/library.js b/lib/cucumber/support_code/library.js index b0f6d7dec..deb8cdf71 100644 --- a/lib/cucumber/support_code/library.js +++ b/lib/cucumber/support_code/library.js @@ -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 = { @@ -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) { @@ -64,6 +58,7 @@ var Library = function(supportCodeDefinition) { }; var supportCodeHelper = { + Around : self.defineAroundHook, Before : self.defineBeforeHook, After : self.defineAfterHook, Given : self.defineStep, @@ -77,4 +72,5 @@ var Library = function(supportCodeDefinition) { return self; }; +Library.Hooker = require('./library/hooker'); module.exports = Library; diff --git a/lib/cucumber/support_code/library/hooker.js b/lib/cucumber/support_code/library/hooker.js new file mode 100644 index 000000000..de873affe --- /dev/null +++ b/lib/cucumber/support_code/library/hooker.js @@ -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; diff --git a/lib/cucumber/type/collection.js b/lib/cucumber/type/collection.js index 4a45844ef..96cc9120f 100644 --- a/lib/cucumber/type/collection.js +++ b/lib/cucumber/type/collection.js @@ -21,7 +21,8 @@ var Collection = function() { }); }; iterate(); - } + }, + length: function length() { return items.length; } }; return self; }; diff --git a/spec/cucumber/runtime/ast_tree_walker_spec.js b/spec/cucumber/runtime/ast_tree_walker_spec.js index 8874ea584..4764525e3 100644 --- a/spec/cucumber/runtime/ast_tree_walker_spec.js +++ b/spec/cucumber/runtime/ast_tree_walker_spec.js @@ -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'); }); @@ -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() { @@ -211,20 +211,22 @@ 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); }); @@ -232,7 +234,7 @@ describe("Cucumber.Runtime.AstTreeWalker", function() { it("broadcasts the visit of the scenario", function() { worldInstantiationCompletionCallback(world); - expect(treeWalker.broadcastEventAroundUserFunction).toHaveBeenCalledWith(event, scenarioVisitWithHooks, callback); + expect(treeWalker.broadcastEventAroundUserFunction).toHaveBeenCalledWith(event, hookedUpScenarioVisit, callback); }); }); }); diff --git a/spec/cucumber/support_code/library/hooker_spec.js b/spec/cucumber/support_code/library/hooker_spec.js new file mode 100644 index 000000000..b630d86b7 --- /dev/null +++ b/spec/cucumber/support_code/library/hooker_spec.js @@ -0,0 +1,309 @@ +require('../../../support/spec_helper'); + +describe("Cucumber.SupportCode.Library.Hooker", function() { + var Cucumber = requireLib('cucumber'); + var hooker, aroundHooks, beforeHooks, afterHooks; + + beforeEach(function() { + aroundHooks = createSpy("around hook collection"); + beforeHooks = createSpy("before hook collection"); + afterHooks = createSpy("after hook collection"); + spyOn(Cucumber.Type, 'Collection').andReturnSeveral([aroundHooks, beforeHooks, afterHooks]); + hooker = Cucumber.SupportCode.Library.Hooker(); + }); + + describe("constructor", function() { + it("creates collections of around, before and after hooks", function() { + expect(Cucumber.Type.Collection).toHaveBeenCalled(); + expect(Cucumber.Type.Collection.callCount).toBe(3); + }); + }); + + describe("addAroundHookCode", function() { + var aroundHook, code; + + beforeEach(function() { + code = createSpy("around code"); + aroundHook = createSpy("around hook"); + spyOn(Cucumber.SupportCode, "Hook").andReturn(aroundHook); + spyOnStub(aroundHooks, "add"); + }); + + it("creates an around hook with the code", function() { + hooker.addAroundHookCode(code); + expect(Cucumber.SupportCode.Hook).toHaveBeenCalledWith(code); + }); + + it("unshifts the around hook to the around hook collection", function() { + hooker.addAroundHookCode(code); + expect(aroundHooks.add).toHaveBeenCalledWith(aroundHook); + }); + }); + + describe("addBeforeHookCode", function() { + var beforeHook, code; + + beforeEach(function() { + code = createSpy("before code"); + beforeHook = createSpy("before hook"); + spyOn(Cucumber.SupportCode, "Hook").andReturn(beforeHook); + spyOnStub(beforeHooks, "add"); + }); + + it("creates a before hook with the code", function() { + hooker.addBeforeHookCode(code); + expect(Cucumber.SupportCode.Hook).toHaveBeenCalledWith(code); + }); + + it("adds the before hook to the before hook collection", function() { + hooker.addBeforeHookCode(code); + expect(beforeHooks.add).toHaveBeenCalledWith(beforeHook); + }); + }); + + describe("addAfterHookCode", function() { + var afterHook, code; + + beforeEach(function() { + code = createSpy("after code"); + afterHook = createSpy("after hook"); + spyOn(Cucumber.SupportCode, "Hook").andReturn(afterHook); + spyOnStub(afterHooks, "unshift"); + }); + + it("creates a after hook with the code", function() { + hooker.addAfterHookCode(code); + expect(Cucumber.SupportCode.Hook).toHaveBeenCalledWith(code); + }); + + it("prepends the after hook to the after hook collection", function() { + hooker.addAfterHookCode(code); + expect(afterHooks.unshift).toHaveBeenCalledWith(afterHook); + }); + }); + + describe("hookUpFunctionWithWorld()", function() { + var userFunction, world; + + beforeEach(function() { + userFunction = createSpy("user function"); + world = createSpy("world instance"); + }); + + it("returns a function", function() { + expect(hooker.hookUpFunctionWithWorld(userFunction, world)).toBeAFunction(); + }); + + describe("returned function", function() { + var returnedFunction, callback, postScenarioAroundHookCallbacks; + + beforeEach(function() { + returnedFunction = hooker.hookUpFunctionWithWorld(userFunction, world); + callback = createSpy("callback"); + postScenarioAroundHookCallbacks = createSpy("post-scenario around hook callbacks"); + spyOnStub(aroundHooks, 'forEach'); + Cucumber.Type.Collection.reset(); + Cucumber.Type.Collection.andReturn(postScenarioAroundHookCallbacks); + }); + + it("instantiates a collection for the post-scenario around hook callbacks", function() { + returnedFunction(callback); + expect(Cucumber.Type.Collection).toHaveBeenCalled(); + }); + + it("iterates over the around hooks", function() { + returnedFunction(callback); + expect(aroundHooks.forEach).toHaveBeenCalledWithAFunctionAsNthParameter(1); + expect(aroundHooks.forEach).toHaveBeenCalledWithAFunctionAsNthParameter(2); + }); + + describe("for each around hook", function() { + var aroundHookIteration, aroundHook, iterationCallback; + + beforeEach(function() { + aroundHook = createSpyWithStubs("around hook", {invoke: null}); + iterationCallback = createSpy("iteration callback"); + returnedFunction(callback); + aroundHookIteration = aroundHooks.forEach.mostRecentCall.args[0]; + }); + + it("invokes the around hook with the world instance", function() { + aroundHookIteration(aroundHook, iterationCallback); + expect(aroundHook.invoke).toHaveBeenCalled(); + expect(aroundHook.invoke).toHaveBeenCalledWithValueAsNthParameter(world, 1); + expect(aroundHook.invoke).toHaveBeenCalledWithAFunctionAsNthParameter(2); + }); + + describe("on around hook invocation completion", function() { + var invocationCompletionCallback, postScenarioAroundHookCallback; + + beforeEach(function() { + aroundHookIteration(aroundHook, iterationCallback); + invocationCompletionCallback = aroundHook.invoke.mostRecentCall.args[1]; + postScenarioAroundHookCallback = createSpy("post-scenario around hook callback"); + spyOnStub(postScenarioAroundHookCallbacks, 'unshift'); + }); + + it("prepends the returned post-scenario hook callback to the post-scenario hook callback collection", function() { + invocationCompletionCallback(postScenarioAroundHookCallback); + expect(postScenarioAroundHookCallbacks.unshift).toHaveBeenCalledWith(postScenarioAroundHookCallback); + }); + + it("calls back", function() { + invocationCompletionCallback(postScenarioAroundHookCallback); + expect(iterationCallback).toHaveBeenCalled(); + }); + }); + }); + + describe("on around hook look completion", function() { + var aroundHooksLoopCallback; + + beforeEach(function() { + returnedFunction(callback); + aroundHooksLoopCallback = aroundHooks.forEach.mostRecentCall.args[1]; + spyOn(hooker, 'triggerBeforeHooks'); + }); + + it("triggers the before hooks", function() { + aroundHooksLoopCallback(); + expect(hooker.triggerBeforeHooks).toHaveBeenCalled(); + expect(hooker.triggerBeforeHooks).toHaveBeenCalledWithValueAsNthParameter(world, 1); + expect(hooker.triggerBeforeHooks).toHaveBeenCalledWithAFunctionAsNthParameter(2); + }); + + describe("on before hooks completion", function() { + var beforeHooksCompletionCallback; + + beforeEach(function() { + aroundHooksLoopCallback(); + beforeHooksCompletionCallback = hooker.triggerBeforeHooks.mostRecentCall.args[1]; + }); + + it("calls the user function", function() { + beforeHooksCompletionCallback(); + expect(userFunction).toHaveBeenCalled(); + expect(userFunction).toHaveBeenCalledWithAFunctionAsNthParameter(1); + }); + + describe("on user function completion", function() { + var userFunctionCallback; + + beforeEach(function() { + beforeHooksCompletionCallback(); + userFunctionCallback = userFunction.mostRecentCall.args[0]; + spyOn(hooker, 'triggerAfterHooks'); + }); + + it("triggers the after hooks", function() { + userFunctionCallback(); + expect(hooker.triggerAfterHooks).toHaveBeenCalled(); + expect(hooker.triggerAfterHooks).toHaveBeenCalledWithValueAsNthParameter(world, 1); + expect(hooker.triggerAfterHooks).toHaveBeenCalledWithAFunctionAsNthParameter(2); + }); + + describe("on after hooks completion", function() { + var afterHooksCompletionCallback; + + beforeEach(function() { + userFunctionCallback(); + afterHooksCompletionCallback = hooker.triggerAfterHooks.mostRecentCall.args[1]; + spyOnStub(postScenarioAroundHookCallbacks, 'forEach'); + }); + + it("iterates over the post-scenario around hook callbacks", function() { + afterHooksCompletionCallback(); + expect(postScenarioAroundHookCallbacks.forEach).toHaveBeenCalled(); + expect(postScenarioAroundHookCallbacks.forEach).toHaveBeenCalledWithAFunctionAsNthParameter(1); + expect(postScenarioAroundHookCallbacks.forEach).toHaveBeenCalledWithValueAsNthParameter(callback, 2); + }); + + describe("for each post-scenario around hook", function() { + var postScenarioAroundHookIteration, postScenarioAroundHookCallback, postScenarioAroundHookIterationCallback; + + beforeEach(function() { + afterHooksCompletionCallback(); + postScenarioAroundHookIteration = postScenarioAroundHookCallbacks.forEach.mostRecentCall.args[0]; + postScenarioAroundHookCallback = createSpy("post-scenario around hook callback"); + postScenarioAroundHookIterationCallback = createSpy("post-scenario around hook iteration callback"); + }); + + it("calls the post-scenario around hook callback", function() { + postScenarioAroundHookIteration(postScenarioAroundHookCallback, postScenarioAroundHookIterationCallback); + expect(postScenarioAroundHookCallback).toHaveBeenCalledWith(postScenarioAroundHookIterationCallback); + expect(postScenarioAroundHookCallback.mostRecentCall.object).toBe(world); + }); + }); + }); + }); + }); + }); + }); + }); + + describe("triggerBeforeHooks", function() { + var world, callback; + + beforeEach(function() { + world = createSpy("world"); + callback = createSpy("callback"); + spyOnStub(beforeHooks, 'forEach'); + }); + + it("iterates over the before hooks", function() { + hooker.triggerBeforeHooks(world, callback); + expect(beforeHooks.forEach).toHaveBeenCalled(); + expect(beforeHooks.forEach).toHaveBeenCalledWithAFunctionAsNthParameter(1); + expect(beforeHooks.forEach).toHaveBeenCalledWithValueAsNthParameter(callback, 2); + }); + + describe("for each before hook", function() { + var beforeHook, forEachBeforeHookFunction, forEachBeforeHookFunctionCallback; + + beforeEach(function() { + hooker.triggerBeforeHooks(world, callback); + forEachBeforeHookFunction = beforeHooks.forEach.mostRecentCall.args[0]; + forEachBeforeHookFunctionCallback = createSpy("for each before hook iteration callback"); + beforeHook = createSpyWithStubs("before hook", {invoke: null}); + }); + + it("invokes the hook", function() { + forEachBeforeHookFunction(beforeHook, forEachBeforeHookFunctionCallback); + expect(beforeHook.invoke).toHaveBeenCalledWith(world, forEachBeforeHookFunctionCallback); + }); + }); + }); + + describe("triggerAfterHooks", function() { + var world, callback; + + beforeEach(function() { + world = createSpy("world"); + callback = createSpy("callback"); + spyOnStub(afterHooks, 'forEach'); + }); + + it("iterates over the after hooks", function() { + hooker.triggerAfterHooks(world, callback); + expect(afterHooks.forEach).toHaveBeenCalled(); + expect(afterHooks.forEach).toHaveBeenCalledWithAFunctionAsNthParameter(1); + expect(afterHooks.forEach).toHaveBeenCalledWithValueAsNthParameter(callback, 2); + }); + + describe("for each after hook", function() { + var afterHook, forEachAfterHookFunction, forEachAfterHookFunctionCallback; + + beforeEach(function() { + hooker.triggerAfterHooks(world, callback); + forEachAfterHookFunction = afterHooks.forEach.mostRecentCall.args[0]; + forEachAfterHookFunctionCallback = createSpy("for each after hook iteration callback"); + afterHook = createSpyWithStubs("after hook", {invoke: null}); + }); + + it("invokes the hook", function() { + forEachAfterHookFunction(afterHook, forEachAfterHookFunctionCallback); + expect(afterHook.invoke).toHaveBeenCalledWith(world, forEachAfterHookFunctionCallback); + }); + }); + }); +}); diff --git a/spec/cucumber/support_code/library_spec.js b/spec/cucumber/support_code/library_spec.js index 7ba5634fa..797879658 100644 --- a/spec/cucumber/support_code/library_spec.js +++ b/spec/cucumber/support_code/library_spec.js @@ -2,33 +2,33 @@ require('../../support/spec_helper'); describe("Cucumber.SupportCode.Library", function() { var Cucumber = requireLib('cucumber'); - var library, rawSupportCode; - var beforeHookCollection; - var afterHookCollection; + var library, rawSupportCode, hooker; var stepDefinitionCollection; var worldConstructor; - var spiesDuringSupportCodeDefinitionExecution = {}; beforeEach(function() { - rawSupportCode = createSpy("Raw support code"); - beforeHookCollection = createSpy("before hook collection"); - afterHookCollection = createSpy("after hook collection"); + rawSupportCode = createSpy("Raw support code"); stepDefinitionCollection = [ createSpyWithStubs("First step definition", {matchesStepName:false}), createSpyWithStubs("Second step definition", {matchesStepName:false}), createSpyWithStubs("Third step definition", {matchesStepName:false}) ]; + hooker = createSpyWithStubs("hooker"); worldConstructor = createSpy("world constructor"); spyOnStub(stepDefinitionCollection, 'syncForEach').andCallFake(function(cb) { stepDefinitionCollection.forEach(cb); }); - spyOn(Cucumber.Type, 'Collection').andReturnSeveral([beforeHookCollection, afterHookCollection, stepDefinitionCollection]); + spyOn(Cucumber.Type, 'Collection').andReturn(stepDefinitionCollection); + spyOn(Cucumber.SupportCode.Library, 'Hooker').andReturn(hooker); spyOn(Cucumber.SupportCode, 'WorldConstructor').andReturn(worldConstructor); library = Cucumber.SupportCode.Library(rawSupportCode); }); describe("constructor", function() { - it("creates collecitons of before hooks, after hooks and step definitions", function() { + it("creates a collection of step definitions", function() { expect(Cucumber.Type.Collection).toHaveBeenCalled(); - expect(Cucumber.Type.Collection.callCount).toBe(3); + }); + + it("instantiates a hooker", function() { + expect(Cucumber.SupportCode.Library.Hooker).toHaveBeenCalled(); }); it("executes the raw support code", function() { @@ -50,6 +50,11 @@ describe("Cucumber.SupportCode.Library", function() { supportCodeHelper = rawSupportCode.mostRecentCall.object; }); + it("exposes a method to define Around hooks", function() { + expect(supportCodeHelper.Around).toBeAFunction(); + expect(supportCodeHelper.Around).toBe(library.defineAroundHook); + }); + it("exposes a method to define Before hooks", function() { expect(supportCodeHelper.Before).toBeAFunction(); expect(supportCodeHelper.Before).toBe(library.defineBeforeHook); @@ -144,111 +149,65 @@ describe("Cucumber.SupportCode.Library", function() { }); }); - describe("defineBeforeHook", function() { - var beforeHook, code; + describe("hookUpFunctionWithWorld", function() { + var userFunction, world, hookedUpFunction; beforeEach(function() { - code = createSpy("before code"); - beforeHook = createSpy("before hook"); - spyOn(Cucumber.SupportCode, "Hook").andReturn(beforeHook); - spyOnStub(beforeHookCollection, "add"); + userFunction = createSpy("user function"); + hookedUpFunction = createSpy("hooked up function"); + world = createSpy("world instance"); + spyOnStub(hooker, 'hookUpFunctionWithWorld').andReturn(hookedUpFunction); }); - it("creates a before hook with the code", function() { - library.defineBeforeHook(code); - expect(Cucumber.SupportCode.Hook).toHaveBeenCalledWith(code); + it("hooks up the function with the world instance", function() { + library.hookUpFunctionWithWorld(userFunction, world); + expect(hooker.hookUpFunctionWithWorld).toHaveBeenCalledWith(userFunction, world); }); - it("adds the before hook to the before hooks collection", function() { - library.defineBeforeHook(code); - expect(beforeHookCollection.add).toHaveBeenCalledWith(beforeHook); + it("returns the hooked up function", function() { + expect(library.hookUpFunctionWithWorld(userFunction, world)).toBe(hookedUpFunction); }); }); - describe("triggerBeforeHooks", function() { - var world, callback; + describe("defineAroundHook", function() { + var code; beforeEach(function() { - world = createSpy("world"); - callback = createSpy("callback"); - spyOnStub(beforeHookCollection, 'forEach'); + code = createSpy("hook code"); + spyOnStub(hooker, 'addAroundHookCode'); }); - it("iterates over the before hooks", function() { - library.triggerBeforeHooks(world, callback); - expect(beforeHookCollection.forEach).toHaveBeenCalled(); - expect(beforeHookCollection.forEach).toHaveBeenCalledWithAFunctionAsNthParameter(1); - expect(beforeHookCollection.forEach).toHaveBeenCalledWithValueAsNthParameter(callback, 2); - }); - - describe("for each before hook", function() { - var beforeHook, forEachBeforeHookFunction, forEachBeforeHookFunctionCallback; - - beforeEach(function() { - library.triggerBeforeHooks(world, callback); - forEachBeforeHookFunction = beforeHookCollection.forEach.mostRecentCall.args[0]; - forEachBeforeHookFunctionCallback = createSpy("for each before hook iteration callback"); - beforeHook = createSpyWithStubs("before hook", {invoke: null}); - }); - - it("invokes the hook", function() { - forEachBeforeHookFunction(beforeHook, forEachBeforeHookFunctionCallback); - expect(beforeHook.invoke).toHaveBeenCalledWith(world, forEachBeforeHookFunctionCallback); - }); + it("instructs the hooker to use the code as an around hook", function() { + library.defineAroundHook(code); + expect(hooker.addAroundHookCode).toHaveBeenCalledWith(code); }); }); - describe("defineAfterHook", function() { - var afterHook, code; + describe("defineBeforeHook", function() { + var code; beforeEach(function() { - code = createSpy("after code"); - afterHook = createSpy("after hook"); - spyOn(Cucumber.SupportCode, "Hook").andReturn(afterHook); - spyOnStub(afterHookCollection, "unshift"); - }); - - it("creates a after hook with the code", function() { - library.defineAfterHook(code); - expect(Cucumber.SupportCode.Hook).toHaveBeenCalledWith(code); + code = createSpy("hook code"); + spyOnStub(hooker, 'addBeforeHookCode'); }); - it("adds the after hook to the after hooks collection", function() { - library.defineAfterHook(code); - expect(afterHookCollection.unshift).toHaveBeenCalledWith(afterHook); + it("instructs the hooker to use the code as an before hook", function() { + library.defineBeforeHook(code); + expect(hooker.addBeforeHookCode).toHaveBeenCalledWith(code); }); }); - describe("triggerAfterHooks", function() { - var world, callback; + describe("defineAfterHook", function() { + var code; beforeEach(function() { - world = createSpy("world"); - callback = createSpy("callback"); - spyOnStub(afterHookCollection, 'forEach'); + code = createSpy("hook code"); + spyOnStub(hooker, 'addAfterHookCode'); }); - it("iterates over the after hooks", function() { - library.triggerAfterHooks(world, callback); - expect(afterHookCollection.forEach).toHaveBeenCalled(); - expect(afterHookCollection.forEach).toHaveBeenCalledWithAFunctionAsNthParameter(1); - expect(afterHookCollection.forEach).toHaveBeenCalledWithValueAsNthParameter(callback, 2); - }); - - describe("for each after hook", function() { - var afterHook, forEachAfterHookFunction, forEachAfterHookFunctionCallback; - - beforeEach(function() { - library.triggerAfterHooks(world, callback); - forEachAfterHookFunction = afterHookCollection.forEach.mostRecentCall.args[0]; - forEachAfterHookFunctionCallback = createSpy("for each after hook iteration callback"); - afterHook = createSpyWithStubs("after hook", {invoke: null}); - }); - - it("invokes the hook", function() { - forEachAfterHookFunction(afterHook, forEachAfterHookFunctionCallback); - expect(afterHook.invoke).toHaveBeenCalledWith(world, forEachAfterHookFunctionCallback); - }); + it("instructs the hooker to use the code as an after hook", function() { + library.defineAfterHook(code); + expect(hooker.addAfterHookCode).toHaveBeenCalledWith(code); }); });