Skip to content

Commit 2c3b5e2

Browse files
tristandunnjbpros
authored andcommitted
Add Before/After hooks (#32, close #31)
1 parent e956a14 commit 2c3b5e2

File tree

13 files changed

+327
-34
lines changed

13 files changed

+327
-34
lines changed

features/cucumber-tck

Submodule cucumber-tck updated from 1288b20 to aa4f859

features/step_definitions/cucumber_js_mappings.rb

+39-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ module CucumberJsMappings
55
WORLD_VARIABLE_LOG_FILE = "world_variable.log"
66
WORLD_FUNCTION_LOG_FILE = "world_function.log"
77
DATA_TABLE_LOG_FILE = "data_table.log"
8+
CYCLE_LOG_FILE = "cycle.log"
9+
810
attr_accessor :support_code
911

1012
def features_dir
@@ -108,6 +110,36 @@ def write_world_function
108110
EOF
109111
end
110112

113+
def write_passing_hook hook_type
114+
provide_cycle_logging_facilities
115+
define_hook = hook_type.capitalize
116+
append_support_code <<-EOF
117+
this.#{define_hook}(function(callback) {
118+
this.logCycleEvent('#{hook_type}');
119+
callback();
120+
});
121+
EOF
122+
end
123+
124+
def write_scenario
125+
provide_cycle_logging_facilities
126+
append_step_definition("a step", "this.logCycleEvent('step');\ncallback();")
127+
scenario_with_steps("A scenario", "Given a step")
128+
end
129+
130+
def provide_cycle_logging_facilities
131+
return if @cycle_logging_facilities_ready
132+
133+
@cycle_logging_facilities_ready = true
134+
append_support_code <<-EOF
135+
this.World.prototype.logCycleEvent = function logCycleEvent(name) {
136+
fd = fs.openSync('#{CYCLE_LOG_FILE}', 'a');
137+
fs.writeSync(fd, " -> " + name, null);
138+
fs.closeSync(fd);
139+
};
140+
EOF
141+
end
142+
111143
def assert_passing_scenario
112144
assert_partial_output("1 scenario (1 passed)", all_output)
113145
assert_success true
@@ -145,6 +177,11 @@ def assert_world_function_called
145177
check_file_presence [WORLD_FUNCTION_LOG_FILE], true
146178
end
147179

180+
def assert_cycle_sequence *args
181+
expected_string = args.join " -> "
182+
check_file_content(CucumberJsMappings::CYCLE_LOG_FILE, expected_string, true)
183+
end
184+
148185
def assert_data_table_equals_json(json)
149186
prep_for_fs_check do
150187
log_file_contents = IO.read(DATA_TABLE_LOG_FILE)
@@ -154,15 +191,15 @@ def assert_data_table_equals_json(json)
154191
end
155192
end
156193

157-
def assert_suggested_step_definition_snippet(stepdef_type, stepdef_name, parameter_count = 0, doc_string = false, data_table = false)
194+
def assert_suggested_step_definition_snippet(stepdef_keyword, stepdef_pattern, parameter_count = 0, doc_string = false, data_table = false)
158195
parameter_count ||= 0
159196
params = Array.new(parameter_count) { |i| "arg#{i+1}" }
160197
params << "string" if doc_string
161198
params << "table" if data_table
162199
params << "callback"
163200
params = params.join ", "
164201
expected_snippet = <<-EOF
165-
this.#{stepdef_type}(/#{stepdef_name}/, function(#{params}) {
202+
this.#{stepdef_keyword}(/#{stepdef_pattern}/, function(#{params}) {
166203
// express the regexp above with the code you wish you had
167204
callback.pending();
168205
});

features/step_definitions/cucumber_steps.js

+22-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ var cucumberSteps = function() {
1818
callback();
1919
});
2020

21+
Given(/^a passing (before|after) hook$/, function(hookType, callback) {
22+
var defineHook = (hookType == 'before' ? 'Before' : 'After');
23+
this.stepDefinitions += defineHook + "(function(callback) {\
24+
world.logCycleEvent('" + hookType + "');\
25+
callback();\
26+
});\n";
27+
callback();
28+
});
29+
2130
Given(/^the step "([^"]*)" has a failing mapping$/, function(stepName, callback) {
2231
this.stepDefinitions += "Given(/^" + stepName + "$/, function(callback) {\
2332
world.touchStep(\"" + stepName + "\");\
@@ -58,7 +67,7 @@ var cucumberSteps = function() {
5867
Given(/^the following data table in a step:$/, function(dataTable, callback) {
5968
this.featureSource += "Feature:\n";
6069
this.featureSource += " Scenario:\n";
61-
this.featureSource += " When a step with data table:\n"
70+
this.featureSource += " When a step with data table:\n";
6271
this.featureSource += dataTable.replace(/^/gm, ' ');
6372
callback();
6473
});
@@ -67,6 +76,10 @@ var cucumberSteps = function() {
6776
this.runFeature(callback);
6877
});
6978

79+
When(/^Cucumber executes a scenario$/, function(callback) {
80+
this.runAScenario(callback);
81+
});
82+
7083
When(/^Cucumber runs the feature$/, function(callback) {
7184
this.runFeature(callback);
7285
});
@@ -146,5 +159,13 @@ var cucumberSteps = function() {
146159
this.assertEqual(expectedDataTable, World.mostRecentInstance.dataTableLog);
147160
callback();
148161
});
162+
163+
Then(/^the (before|after) hook is fired (?:before|after) the scenario$/, function(hookType, callback) {
164+
if (hookType == 'before')
165+
this.assertCycleSequence(hookType, 'step');
166+
else
167+
this.assertCycleSequence('step', hookType);
168+
callback();
169+
});
149170
};
150171
module.exports = cucumberSteps;

features/step_definitions/cucumber_world.js

+23
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ var World = function(callback) {
33
this.featureSource = "";
44
this.stepDefinitions = "";
55
this.runOutput = "";
6+
this.cycleEvents = "";
67
this.runSucceeded = false;
78
World.mostRecentInstance = this;
89
callback(this);
@@ -13,6 +14,7 @@ var proto = World.prototype;
1314
proto.runFeature = function runFeature(callback) {
1415
var supportCode;
1516
var supportCodeSource = "supportCode = function() {\n var Given = When = Then = this.defineStep;\n" +
17+
" var Before = this.Before, After = this.After;\n" +
1618
this.stepDefinitions + "};\n";
1719
var world = this;
1820
eval(supportCodeSource);
@@ -33,6 +35,21 @@ proto.runFeatureWithSupportCodeSource = function runFeatureWithSupportCodeSource
3335
});
3436
}
3537

38+
proto.runAScenario = function runAScenario(callback) {
39+
this.featureSource += "Feature:\n";
40+
this.featureSource += " Scenario:\n";
41+
this.featureSource += " Given a step\n";
42+
this.stepDefinitions += "Given(/^a step$/, function(callback) {\
43+
world.logCycleEvent('step');\
44+
callback();\
45+
});";
46+
this.runFeature(callback);
47+
}
48+
49+
proto.logCycleEvent = function logCycleEvent(event) {
50+
this.cycleEvents += " -> " + event;
51+
}
52+
3653
proto.touchStep = function touchStep(string) {
3754
this.touchedSteps.push(string);
3855
}
@@ -117,4 +134,10 @@ proto.assertEqual = function assertRawDataTable(expected, actual) {
117134
throw(new Error("Expected:\n\"" + actualJSON + "\"\nto match:\n\"" + expectedJSON + "\""));
118135
}
119136

137+
proto.assertCycleSequence = function assertCycleSequence(first, second) {
138+
var partialSequence = first + ' -> ' + second;
139+
if (this.cycleEvents.indexOf(partialSequence) < 0)
140+
throw(new Error("Expected cycle sequence \"" + this.cycleEvents + "\" to contain \"" + partialSequence + "\""));
141+
}
142+
120143
exports.World = World;

lib/cucumber/runtime/ast_tree_walker.js

+18-3
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,14 @@ var AstTreeWalker = function(features, supportCodeLibrary, listeners) {
4141
supportCodeLibrary.instantiateNewWorld(function(world) {
4242
self.setWorld(world);
4343
self.witnessNewScenario();
44-
var payload = { scenario: scenario };
45-
var event = AstTreeWalker.Event(AstTreeWalker.SCENARIO_EVENT_NAME, payload);
44+
var payload = { scenario: scenario };
45+
var event = AstTreeWalker.Event(AstTreeWalker.SCENARIO_EVENT_NAME, payload);
46+
var hookedUpScenarioVisit = self.hookUpFunction(
47+
function(callback) { scenario.acceptVisitor(self, callback); }
48+
);
4649
self.broadcastEventAroundUserFunction(
4750
event,
48-
function(callback) { scenario.acceptVisitor(self, callback); },
51+
hookedUpScenarioVisit,
4952
callback
5053
);
5154
});
@@ -105,6 +108,18 @@ var AstTreeWalker = function(features, supportCodeLibrary, listeners) {
105108
);
106109
},
107110

111+
hookUpFunction: function hookUpFunction(hookedUpFunction) {
112+
return function(callback) {
113+
supportCodeLibrary.triggerBeforeHooks(self.getWorld(), function() {
114+
hookedUpFunction(function() {
115+
supportCodeLibrary.triggerAfterHooks(self.getWorld(), function() {
116+
callback();
117+
});
118+
});
119+
});
120+
}
121+
},
122+
108123
lookupStepDefinitionByName: function lookupStepDefinitionByName(stepName) {
109124
return supportCodeLibrary.lookupStepDefinitionByName(stepName);
110125
},

lib/cucumber/support_code.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
var SupportCode = {};
2+
SupportCode.Hook = require('./support_code/hook');
23
SupportCode.Library = require('./support_code/library');
34
SupportCode.StepDefinition = require('./support_code/step_definition');
45
SupportCode.StepDefinitionSnippetBuilder = require('./support_code/step_definition_snippet_builder');

lib/cucumber/support_code/hook.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
var Hook = function(code) {
2+
var Cucumber = require('../../cucumber');
3+
4+
var self = {
5+
invoke: function(world, callback) {
6+
code.call(world, callback);
7+
}
8+
};
9+
return self;
10+
};
11+
module.exports = Hook;

lib/cucumber/support_code/library.js

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
var Library = function(supportCodeDefinition) {
22
var Cucumber = require('../../cucumber');
33

4-
var stepDefinitions = Cucumber.Type.Collection();
4+
var beforeHooks = Cucumber.Type.Collection();
5+
var afterHooks = Cucumber.Type.Collection();
6+
var stepDefinitions = Cucumber.Type.Collection();
57
var worldConstructor = Cucumber.SupportCode.WorldConstructor();
68

79
var self = {
@@ -21,6 +23,28 @@ var Library = function(supportCodeDefinition) {
2123
return (stepDefinition != undefined);
2224
},
2325

26+
defineBeforeHook: function defineBeforeHook(code) {
27+
var beforeHook = Cucumber.SupportCode.Hook(code);
28+
beforeHooks.add(beforeHook);
29+
},
30+
31+
triggerBeforeHooks: function(world, callback) {
32+
beforeHooks.forEach(function(beforeHook, callback) {
33+
beforeHook.invoke(world, callback);
34+
}, callback);
35+
},
36+
37+
defineAfterHook: function defineAfterHook(code) {
38+
var afterHook = Cucumber.SupportCode.Hook(code);
39+
afterHooks.unshift(afterHook);
40+
},
41+
42+
triggerAfterHooks: function(world, callback) {
43+
afterHooks.forEach(function(afterHook, callback) {
44+
afterHook.invoke(world, callback);
45+
}, callback);
46+
},
47+
2448
defineStep: function defineStep(name, code) {
2549
var stepDefinition = Cucumber.SupportCode.StepDefinition(name, code);
2650
stepDefinitions.add(stepDefinition);
@@ -36,6 +60,8 @@ var Library = function(supportCodeDefinition) {
3660
};
3761

3862
var supportCodeHelper = {
63+
Before : self.defineBeforeHook,
64+
After : self.defineAfterHook,
3965
Given : self.defineStep,
4066
When : self.defineStep,
4167
Then : self.defineStep,

lib/cucumber/type/collection.js

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ var Collection = function() {
22
var items = new Array();
33
var self = {
44
add: function add(item) { items.push(item); },
5+
unshift: function unshift(item) { items.unshift(item); },
56
getLast: function getLast() { return items[items.length-1]; },
67
syncForEach: function syncForEach(userFunction) { items.forEach(userFunction); },
78
forEach: function forEach(userFunction, callback) {

spec/cucumber/runtime/ast_tree_walker_spec.js

+24-22
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,10 @@ describe("Cucumber.Runtime.AstTreeWalker", function() {
9797
var feature, callback, event, payload;
9898

9999
beforeEach(function() {
100-
feature = createSpyWithStubs("Feature AST element", {acceptVisitor: null});
101-
callback = createSpy("Callback");
102-
event = createSpy("Event");
103-
payload = {feature: feature};
100+
feature = createSpyWithStubs("Feature AST element", {acceptVisitor: null});
101+
callback = createSpy("Callback");
102+
event = createSpy("Event");
103+
payload = {feature: feature};
104104
spyOn(Cucumber.Runtime.AstTreeWalker, 'Event').andReturn(event);
105105
spyOn(treeWalker, 'broadcastEventAroundUserFunction');
106106
});
@@ -177,19 +177,21 @@ describe("Cucumber.Runtime.AstTreeWalker", function() {
177177
});
178178

179179
describe("on world instantiation completion", function() {
180-
var worldInstantiationCompletionCallback, world, event, payload;
180+
var worldInstantiationCompletionCallback, world, event, payload, hookedUpFunction;
181181

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

187-
event = createSpy("Event");
188-
payload = {scenario: scenario};
187+
event = createSpy("Event");
188+
payload = {scenario: scenario};
189+
scenarioVisitWithHooks = createSpy("scenario visit with hooks");
189190
spyOn(Cucumber.Runtime.AstTreeWalker, 'Event').andReturn(event);
190191
spyOn(treeWalker, 'broadcastEventAroundUserFunction');
191192
spyOn(treeWalker, 'setWorld');
192193
spyOn(treeWalker, 'witnessNewScenario');
194+
spyOn(treeWalker, 'hookUpFunction').andReturn(scenarioVisitWithHooks);
193195
});
194196

195197
it("sets the new World instance", function() {
@@ -207,31 +209,31 @@ describe("Cucumber.Runtime.AstTreeWalker", function() {
207209
expect(Cucumber.Runtime.AstTreeWalker.Event).toHaveBeenCalledWith(Cucumber.Runtime.AstTreeWalker.SCENARIO_EVENT_NAME, payload);
208210
});
209211

210-
it("broadcasts the visit of the scenario", function() {
212+
it("hooks up a function", function() {
211213
worldInstantiationCompletionCallback(world);
212-
expect(treeWalker.broadcastEventAroundUserFunction).toHaveBeenCalled();
213-
expect(treeWalker.broadcastEventAroundUserFunction).
214-
toHaveBeenCalledWithValueAsNthParameter(event, 1);
215-
expect(treeWalker.broadcastEventAroundUserFunction).
216-
toHaveBeenCalledWithAFunctionAsNthParameter(2);
217-
expect(treeWalker.broadcastEventAroundUserFunction).
218-
toHaveBeenCalledWithValueAsNthParameter(callback, 3);
214+
expect(treeWalker.hookUpFunction).toHaveBeenCalled();
215+
expect(treeWalker.hookUpFunction).toHaveBeenCalledWithAFunctionAsNthParameter(1);
219216
});
220217

221-
describe("user function", function() {
222-
var userFunction, userFunctionCallback;
218+
describe("hooked up function", function() {
219+
var hookedUpFunction, hookedUpFunctionCallback;
223220

224221
beforeEach(function() {
225-
userFunctionCallback = createSpy("User function callback");
222+
hookedUpFunctionCallback = createSpy("hooked up function callback");
226223
worldInstantiationCompletionCallback(world);
227-
userFunction = treeWalker.broadcastEventAroundUserFunction.mostRecentCall.args[1];
224+
hookedUpFunction = treeWalker.hookUpFunction.mostRecentCall.args[0];
228225
});
229226

230-
it("visits the scenario, passing it the received callback", function() {
231-
userFunction(userFunctionCallback);
232-
expect(scenario.acceptVisitor).toHaveBeenCalledWith(treeWalker, userFunctionCallback);
227+
it("tells the scenario to accept the tree walker itself as a visitor", function() {
228+
hookedUpFunction(hookedUpFunctionCallback);
229+
expect(scenario.acceptVisitor).toHaveBeenCalledWith(treeWalker, hookedUpFunctionCallback);
233230
});
234231
});
232+
233+
it("broadcasts the visit of the scenario", function() {
234+
worldInstantiationCompletionCallback(world);
235+
expect(treeWalker.broadcastEventAroundUserFunction).toHaveBeenCalledWith(event, scenarioVisitWithHooks, callback);
236+
});
235237
});
236238
});
237239

0 commit comments

Comments
 (0)