diff --git a/features/step_definitions/cucumber_js_mappings.rb b/features/step_definitions/cucumber_js_mappings.rb index 31ebdaee9..9d2e4f634 100644 --- a/features/step_definitions/cucumber_js_mappings.rb +++ b/features/step_definitions/cucumber_js_mappings.rb @@ -5,6 +5,8 @@ module CucumberJsMappings WORLD_VARIABLE_LOG_FILE = "world_variable.log" WORLD_FUNCTION_LOG_FILE = "world_function.log" DATA_TABLE_LOG_FILE = "data_table.log" + CYCLE_LOG_FILE = "cycle.log" + attr_accessor :support_code def features_dir @@ -195,10 +197,25 @@ def append_support_code(code) def write_main_step_definitions_file append_to_file(STEP_DEFINITIONS_FILE, "var fs = require('fs');\nvar stepDefinitions = function() {\n"); + append_to_file(STEP_DEFINITIONS_FILE, world_code) append_to_file(STEP_DEFINITIONS_FILE, support_code); append_to_file(STEP_DEFINITIONS_FILE, "};\nmodule.exports = stepDefinitions;") end + def world_code + <<-EOF + this.World = function() { + this.callCount = 0; + }; + + this.World.prototype.logCycleEvent = function logCycleEvent(name) { + fd = fs.openSync('#{CYCLE_LOG_FILE}', 'a'); + fs.writeSync(fd, " -> " + name, null); + fs.closeSync(fd); + }; +EOF + end + def write_coffee_script_definition_file append_to_file COFFEE_SCRIPT_DEFINITIONS_FILE, <<-EOF fs = require('fs') diff --git a/features/step_definitions/cucumber_steps.js b/features/step_definitions/cucumber_steps.js index 30466c5d9..cb7cc12f3 100644 --- a/features/step_definitions/cucumber_steps.js +++ b/features/step_definitions/cucumber_steps.js @@ -18,6 +18,15 @@ var cucumberSteps = function() { callback(); }); + Given(/^a passing (before|after) hook$/, function(hookType, callback) { + var defineHook = (hookType == 'before' ? 'Before' : 'After'); + this.stepDefinitions += defineHook + "(function(callback) {\ + world.touchStep('" + hookType + "-hook');\ + callback();\ +});\n"; + callback(); + }); + Given(/^the step "([^"]*)" has a failing mapping$/, function(stepName, callback) { this.stepDefinitions += "Given(/^" + stepName + "$/, function(callback) {\ world.touchStep(\"" + stepName + "\");\ diff --git a/features/step_definitions/cucumber_steps.rb b/features/step_definitions/cucumber_steps.rb index 4081eeadd..4b20ee084 100644 --- a/features/step_definitions/cucumber_steps.rb +++ b/features/step_definitions/cucumber_steps.rb @@ -5,6 +5,17 @@ write_coffee_script_definition_file end +# TODO: encapsulate and move to cucumber-features +Given /^a passing (before|after) hook$/ do |hook_type| + define_hook = hook_type.capitalize + append_support_code <<-EOF +this.#{define_hook}(function(callback) { + this.logCycleEvent('#{hook_type}'); + callback(); +}); +EOF +end + When /^Cucumber executes a scenario using that mapping$/ do write_feature <<-EOF Feature: @@ -14,6 +25,20 @@ run_feature end +# TODO: encapsulate and move to cucumber-features +When /^Cucumber executes a scenario$/ do + append_step_definition("a step", "this.logCycleEvent('step');\ncallback();") + scenario_with_steps "A scenario", "Given Cucumber executes a step definition" + run_feature +end + +# TODO: encapsulate and move to cucumber-features +Then /^the (after|before) hook is fired (?:after|before) the scenario$/ do |hook_type| + expected_string = (hook_type == 'before' ? 'before -> step' : 'step -> after') + check_file_content(CucumberJsMappings::CYCLE_LOG_FILE, expected_string, true) +end + +# TODO: encapsulate and move to cucumber-features Then /^the mapping is run$/ do assert_passed "a mapping" end diff --git a/features/step_definitions/cucumber_world.js b/features/step_definitions/cucumber_world.js index ac2e962ad..0e7bda956 100644 --- a/features/step_definitions/cucumber_world.js +++ b/features/step_definitions/cucumber_world.js @@ -12,6 +12,7 @@ var proto = CucumberWorld.prototype; proto.runFeature = function runFeature(callback) { var supportCode; var supportCodeSource = "supportCode = function() {\n var Given = When = Then = this.defineStep;\n" + + " var Before = this.Before, After = this.After;\n" + this.stepDefinitions + "};\n"; var world = this; eval(supportCodeSource); diff --git a/lib/cucumber/runtime/ast_tree_walker.js b/lib/cucumber/runtime/ast_tree_walker.js index e4a402184..09b6c719a 100644 --- a/lib/cucumber/runtime/ast_tree_walker.js +++ b/lib/cucumber/runtime/ast_tree_walker.js @@ -46,7 +46,17 @@ var AstTreeWalker = function(features, supportCodeLibrary, listeners) { var event = AstTreeWalker.Event(AstTreeWalker.SCENARIO_EVENT_NAME, payload); self.broadcastEventAroundUserFunction( event, - function(callback) { scenario.acceptVisitor(self, callback); }, + function(callback) { + supportCodeLibrary.triggerBeforeHooks(world, function() { + scenario.acceptVisitor(self, function() { + supportCodeLibrary.triggerAfterHooks(world, function() { + if (callback) { + callback(); + } + }); + }); + }); + }, callback ); }, diff --git a/lib/cucumber/support_code.js b/lib/cucumber/support_code.js index 47a89e6ec..d1322709f 100644 --- a/lib/cucumber/support_code.js +++ b/lib/cucumber/support_code.js @@ -1,4 +1,5 @@ var SupportCode = {}; +SupportCode.Hook = require('./support_code/hook'); SupportCode.Library = require('./support_code/library'); SupportCode.StepDefinition = require('./support_code/step_definition'); SupportCode.StepDefinitionSnippetBuilder = require('./support_code/step_definition_snippet_builder'); diff --git a/lib/cucumber/support_code/hook.js b/lib/cucumber/support_code/hook.js new file mode 100644 index 000000000..5f3589ef1 --- /dev/null +++ b/lib/cucumber/support_code/hook.js @@ -0,0 +1,11 @@ +var Hook = function(type, code) { + var Cucumber = require('../../cucumber'); + + var self = { + invoke: function(world, callback) { + code.apply(world, [callback]); + } + }; + return self; +}; +module.exports = Hook; diff --git a/lib/cucumber/support_code/library.js b/lib/cucumber/support_code/library.js index 3d64272b9..3de43844d 100644 --- a/lib/cucumber/support_code/library.js +++ b/lib/cucumber/support_code/library.js @@ -1,7 +1,9 @@ var Library = function(supportCodeDefinition) { var Cucumber = require('../../cucumber'); - var stepDefinitions = Cucumber.Type.Collection(); + var beforeHooks = Cucumber.Type.Collection(); + var afterHooks = Cucumber.Type.Collection(); + var stepDefinitions = Cucumber.Type.Collection(); var worldConstructor = Cucumber.SupportCode.WorldConstructor(); var self = { @@ -21,6 +23,28 @@ var Library = function(supportCodeDefinition) { return (stepDefinition != undefined); }, + defineBeforeHook: function defineBeforeHook(code) { + var beforeHook = Cucumber.SupportCode.Hook('before', code); + beforeHooks.add(beforeHook); + }, + + triggerBeforeHooks: function(world, callback) { + beforeHooks.forEach(function(beforeHook, callback) { + beforeHook.invoke(world, callback); + }, callback); + }, + + defineAfterHook: function defineAfterHook(code) { + var afterHook = Cucumber.SupportCode.Hook('after', code); + afterHooks.unshift(afterHook); + }, + + triggerAfterHooks: function(world, callback) { + afterHooks.forEach(function(afterHook, callback) { + afterHook.invoke(world, callback); + }, callback); + }, + defineStep: function defineStep(name, code) { var stepDefinition = Cucumber.SupportCode.StepDefinition(name, code); stepDefinitions.add(stepDefinition); @@ -32,6 +56,8 @@ var Library = function(supportCodeDefinition) { }; var supportCodeHelper = { + Before : self.defineBeforeHook, + After : self.defineAfterHook, Given : self.defineStep, When : self.defineStep, Then : self.defineStep, diff --git a/lib/cucumber/type/collection.js b/lib/cucumber/type/collection.js index 30e2f4bce..4a45844ef 100644 --- a/lib/cucumber/type/collection.js +++ b/lib/cucumber/type/collection.js @@ -2,6 +2,7 @@ var Collection = function() { var items = new Array(); var self = { add: function add(item) { items.push(item); }, + unshift: function unshift(item) { items.unshift(item); }, getLast: function getLast() { return items[items.length-1]; }, syncForEach: function syncForEach(userFunction) { items.forEach(userFunction); }, forEach: function forEach(userFunction, callback) { diff --git a/spec/cucumber/runtime/ast_tree_walker_spec.js b/spec/cucumber/runtime/ast_tree_walker_spec.js index dd9252101..c0c275b00 100644 --- a/spec/cucumber/runtime/ast_tree_walker_spec.js +++ b/spec/cucumber/runtime/ast_tree_walker_spec.js @@ -97,10 +97,10 @@ describe("Cucumber.Runtime.AstTreeWalker", function() { var feature, callback, event, payload; beforeEach(function() { - feature = createSpyWithStubs("Feature AST element", {acceptVisitor: null}); - callback = createSpy("Callback"); - event = createSpy("Event"); - payload = {feature: feature}; + feature = createSpyWithStubs("Feature AST element", {acceptVisitor: null}); + callback = createSpy("Callback"); + event = createSpy("Event"); + payload = {feature: feature}; spyOn(Cucumber.Runtime.AstTreeWalker, 'Event').andReturn(event); spyOn(treeWalker, 'broadcastEventAroundUserFunction'); }); @@ -165,13 +165,16 @@ describe("Cucumber.Runtime.AstTreeWalker", function() { var world; beforeEach(function() { - scenario = createSpyWithStubs("Scenario AST element", {acceptVisitor: null}); - callback = createSpy("Callback"); - event = createSpy("Event"); - payload = {scenario: scenario}; - world = createSpy("world instance"); + scenario = createSpyWithStubs("Scenario AST element", {acceptVisitor: null}); + callback = createSpy("Callback"); + event = createSpy("Event"); + payload = {scenario: scenario}; + world = createSpy("world instance"); + triggerHookFake = function(world, callback) { callback(); }; spyOn(Cucumber.Runtime.AstTreeWalker, 'Event').andReturn(event); spyOnStub(supportCodeLibrary, 'instantiateNewWorld').andReturn(world); + spyOnStub(supportCodeLibrary, 'triggerBeforeHooks').andCallFake(triggerHookFake); + spyOnStub(supportCodeLibrary, 'triggerAfterHooks').andCallFake(triggerHookFake); spyOn(treeWalker, 'broadcastEventAroundUserFunction'); spyOn(treeWalker, 'witnessNewScenario'); spyOn(treeWalker, 'setWorld'); @@ -208,6 +211,27 @@ describe("Cucumber.Runtime.AstTreeWalker", function() { toHaveBeenCalledWithValueAsNthParameter(callback, 3); }); + it("invokes before hooks", function() { + treeWalker.visitScenario(scenario, callback); + treeWalker.broadcastEventAroundUserFunction.mostRecentCall.args[1](); + + expect(supportCodeLibrary.triggerBeforeHooks). + toHaveBeenCalledWithValueAsNthParameter(world, 1); + expect(supportCodeLibrary.triggerBeforeHooks). + toHaveBeenCalledWithAFunctionAsNthParameter(2); + }); + + it("invokes after hooks", function() { + treeWalker.visitScenario(scenario, callback); + treeWalker.broadcastEventAroundUserFunction.mostRecentCall.args[1](); + scenario.acceptVisitor.mostRecentCall.args[1](); + + expect(supportCodeLibrary.triggerAfterHooks). + toHaveBeenCalledWithValueAsNthParameter(world, 1); + expect(supportCodeLibrary.triggerAfterHooks). + toHaveBeenCalledWithAFunctionAsNthParameter(2); + }); + describe("user function", function() { var userFunction, userFunctionCallback; @@ -217,7 +241,13 @@ describe("Cucumber.Runtime.AstTreeWalker", function() { userFunction = treeWalker.broadcastEventAroundUserFunction.mostRecentCall.args[1]; }); - it("visits the scenario, passing it the received callback", function() { + it("visits the scenario, passing it the tree walker", function() { + userFunction(userFunctionCallback); + expect(scenario.acceptVisitor). + toHaveBeenCalledWithValueAsNthParameter(treeWalker, 1); + }); + + xit("visits the scenario, passing it the received callback", function() { userFunction(userFunctionCallback); expect(scenario.acceptVisitor).toHaveBeenCalledWith(treeWalker, userFunctionCallback); }); diff --git a/spec/cucumber/support_code/library_spec.js b/spec/cucumber/support_code/library_spec.js index cbc420d61..d0c03b28f 100644 --- a/spec/cucumber/support_code/library_spec.js +++ b/spec/cucumber/support_code/library_spec.js @@ -3,13 +3,17 @@ require('../../support/spec_helper'); describe("Cucumber.SupportCode.Library", function() { var Cucumber = requireLib('cucumber'); var library, rawSupportCode; + var beforeHookCollection; + var afterHookCollection; var stepDefinitionCollection; var worldConstructor; var spiesDuringSupportCodeDefinitionExecution = {}; var worldConstructorCalled; beforeEach(function() { - rawSupportCode = createSpy("Raw support code"); + rawSupportCode = createSpy("Raw support code"); + afterHookCollection = Cucumber.Type.Collection(); + beforeHookCollection = Cucumber.Type.Collection(); stepDefinitionCollection = [ createSpyWithStubs("First step definition", {matchesStepName:false}), createSpyWithStubs("Second step definition", {matchesStepName:false}), @@ -18,7 +22,15 @@ describe("Cucumber.SupportCode.Library", function() { worldConstructorCalled = false; worldConstructor = function() { worldConstructorCalled = true; }; spyOnStub(stepDefinitionCollection, 'syncForEach').andCallFake(function(cb) { stepDefinitionCollection.forEach(cb); }); - spyOn(Cucumber.Type, 'Collection').andReturn(stepDefinitionCollection); + spyOn(Cucumber.Type, 'Collection').andCallFake(function() { + if (this.Collection.callCount == 1) { + return beforeHookCollection; + } else if (this.Collection.callCount == 2) { + return afterHookCollection; + } else { + return stepDefinitionCollection; + } + }); spyOn(Cucumber.SupportCode, 'WorldConstructor').andReturn(worldConstructor); library = Cucumber.SupportCode.Library(rawSupportCode); }); @@ -47,6 +59,16 @@ describe("Cucumber.SupportCode.Library", function() { supportCodeHelper = rawSupportCode.mostRecentCall.object; }); + it("exposes a method to define Before methods", function() { + expect(supportCodeHelper.Before).toBeAFunction(); + expect(supportCodeHelper.Before).toBe(library.defineBeforeHook); + }); + + it("exposes a method to define After methods", function() { + expect(supportCodeHelper.After).toBeAFunction(); + expect(supportCodeHelper.After).toBe(library.defineAfterHook); + }); + it("exposes a method to define Given steps", function() { expect(supportCodeHelper.Given).toBeAFunction(); expect(supportCodeHelper.Given).toBe(library.defineStep); @@ -132,6 +154,106 @@ describe("Cucumber.SupportCode.Library", function() { }); }); + describe("defineBeforeHook", function() { + var beforeHook, code; + + beforeEach(function() { + code = createSpy("before code"); + beforeHook = createSpy("before hook"); + spyOn(Cucumber.SupportCode, "Hook").andReturn(beforeHook); + spyOnStub(beforeHookCollection, "add"); + }); + + it("creates a before hook with the code", function() { + library.defineBeforeHook(code); + expect(Cucumber.SupportCode.Hook).toHaveBeenCalledWith('before', code); + }); + + it("adds the before hook to the before hooks collection", function() { + library.defineBeforeHook(code); + expect(beforeHookCollection.add).toHaveBeenCalledWith(beforeHook); + }); + }); + + describe("triggerBeforeHooks", function() { + var beforeHook, callback, code, invokeSpy, world; + + beforeEach(function() { + code = createSpy("before code"); + world = library.instantiateNewWorld(); + callback = createSpy("callback"); + beforeHook = createSpy("before hook"); + invokeSpy = spyOnStub(beforeHook, "invoke"); + spyOn(Cucumber.SupportCode, "Hook").andReturn(beforeHook); + library.defineBeforeHook(code); + }); + + it("triggers each before hook", function() { + library.triggerBeforeHooks(world, function() { + expect(beforeHook, "invoke"). + toHaveBeenCalledWithValueAsNthParameter(world, 1); + expect(beforeHook, "invoke"). + toHaveBeenCalledWithAFunctionAsNthParameter(2); + }); + }); + + it("calls the callback when finished", function() { + invokeSpy.andCallFake(function(world, callback) { callback(); }); + library.triggerBeforeHooks(world, callback); + expect(callback).toHaveBeenCalled(); + }); + }); + + describe("defineAfterHook", function() { + var code, afterHook; + + 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('after', code); + }); + + it("unshifts the after hook to the after hooks collection", function() { + library.defineAfterHook(code); + expect(afterHookCollection.unshift).toHaveBeenCalledWith(afterHook); + }); + }); + + describe("triggerAfterHooks", function() { + var afterHook, callback, code, invokeSpy, world; + + beforeEach(function() { + code = createSpy("after code"); + world = library.instantiateNewWorld(); + callback = createSpy("callback"); + afterHook = createSpy("after hook"); + invokeSpy = spyOnStub(afterHook, "invoke"); + spyOn(Cucumber.SupportCode, "Hook").andReturn(afterHook); + library.defineAfterHook(code); + }); + + it("triggers each after hook", function() { + library.triggerAfterHooks(world, function() { + expect(afterHook, "invoke"). + toHaveBeenCalledWithValueAsNthParameter(world, 1); + expect(afterHook, "invoke"). + toHaveBeenCalledWithAFunctionAsNthParameter(2); + }); + }); + + it("calls the callback when finished", function() { + invokeSpy.andCallFake(function(world, callback) { callback(); }); + library.triggerAfterHooks(world, callback); + expect(callback).toHaveBeenCalled(); + }); + }); + describe("defineStep()", function() { var name, code, stepDefinition; diff --git a/spec/cucumber/type/collection_spec.js b/spec/cucumber/type/collection_spec.js index 318c1f85e..f11addc3a 100644 --- a/spec/cucumber/type/collection_spec.js +++ b/spec/cucumber/type/collection_spec.js @@ -7,6 +7,7 @@ describe("Cucumber.Type.Collection", function() { beforeEach(function() { itemArray = [1, 2, 3]; spyOn(itemArray, 'push'); + spyOn(itemArray, 'unshift'); spyOn(global, 'Array').andReturn(itemArray); collection = Cucumber.Type.Collection(); }); @@ -25,6 +26,14 @@ describe("Cucumber.Type.Collection", function() { }); }); + describe("unshift()", function() { + it("unshifts the item onto the start of the item array", function() { + var item = createSpy("Collection item"); + collection.unshift(item); + expect(itemArray.unshift).toHaveBeenCalledWith(item); + }); + }); + describe("getLast()", function() { it("returns the latest added item from the array", function() { var lastItem = createSpy("last item");