Skip to content

Commit bf57ff1

Browse files
committed
Add support for tagged hooks (close #32)
1 parent f383a0b commit bf57ff1

18 files changed

+445
-161
lines changed

features/cucumber-tck

Submodule cucumber-tck updated from 8559dee to 0f42d85

features/hooks.feature

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Feature: Environment Hooks
2+
3+
The following scenario is a regression test for special "around" hooks which
4+
deserve a bit more of attention.
5+
6+
Scenario: Tagged around hook with untagged scenario
7+
Given an around hook tagged with "@foo"
8+
When Cucumber executes a scenario with no tags
9+
Then the hook is not fired

features/step_definitions/cucumber_js_mappings.rb

+30-12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module CucumberJsMappings
66
WORLD_FUNCTION_LOG_FILE = "world_function.log"
77
DATA_TABLE_LOG_FILE = "data_table.log"
88
CYCLE_LOG_FILE = "cycle.log"
9+
CYCLE_SEQUENCE_SEPARATOR = " -> "
910

1011
attr_accessor :support_code
1112

@@ -135,23 +136,34 @@ def write_world_function
135136
EOF
136137
end
137138

138-
def write_passing_hook hook_type
139+
def write_passing_hook options = {}
140+
log_string = options[:log_cycle_event_as]
141+
if options[:type]
142+
hook_type = options[:type]
143+
log_string ||= hook_type
144+
else
145+
hook_type = "before"
146+
log_string ||= "hook"
147+
end
148+
tags = options[:tags] || []
139149
provide_cycle_logging_facilities
140150
define_hook = hook_type.capitalize
151+
params = tags.any? ? "'#{tags.join("', '")}', " : ""
152+
141153
if hook_type == "around"
142154
append_support_code <<-EOF
143-
this.#{define_hook}(function(runScenario) {
144-
this.logCycleEvent('#{hook_type}-pre');
155+
this.#{define_hook}(#{params}function(runScenario) {
156+
this.logCycleEvent('#{log_string}-pre');
145157
runScenario(function(callback) {
146-
this.logCycleEvent('#{hook_type}-post');
158+
this.logCycleEvent('#{log_string}-post');
147159
callback();
148160
});
149161
});
150162
EOF
151163
else
152164
append_support_code <<-EOF
153-
this.#{define_hook}(function(callback) {
154-
this.logCycleEvent('#{hook_type}');
165+
this.#{define_hook}(#{params}function(callback) {
166+
this.logCycleEvent('#{log_string}');
155167
callback();
156168
});
157169
EOF
@@ -178,7 +190,7 @@ def provide_cycle_logging_facilities
178190
append_support_code <<-EOF
179191
this.World.prototype.logCycleEvent = function logCycleEvent(name) {
180192
fd = fs.openSync('#{CYCLE_LOG_FILE}', 'a');
181-
fs.writeSync(fd, " -> " + name, null);
193+
fs.writeSync(fd, "#{CYCLE_SEQUENCE_SEPARATOR}" + name, null);
182194
fs.closeSync(fd);
183195
};
184196
EOF
@@ -224,12 +236,18 @@ def assert_world_function_called
224236
end
225237

226238
def assert_cycle_sequence *args
227-
expected_string = args.join " -> "
239+
expected_string = args.join CYCLE_SEQUENCE_SEPARATOR
228240
check_file_content(CucumberJsMappings::CYCLE_LOG_FILE, expected_string, true)
229241
end
230242

243+
def assert_cycle_sequence_excluding *args
244+
args.each do |unexpected_string|
245+
check_file_content(CucumberJsMappings::CYCLE_LOG_FILE, unexpected_string, false)
246+
end
247+
end
248+
231249
def assert_complete_cycle_sequence *args
232-
expected_string = args.join " -> "
250+
expected_string = "#{CYCLE_SEQUENCE_SEPARATOR}#{args.join(CYCLE_SEQUENCE_SEPARATOR)}"
233251
check_exact_file_content(CucumberJsMappings::CYCLE_LOG_FILE, expected_string)
234252
end
235253

@@ -259,10 +277,10 @@ def assert_suggested_step_definition_snippet(stepdef_keyword, stepdef_pattern, p
259277
end
260278

261279
def assert_executed_scenarios *scenario_offsets
262-
sequence = scenario_offsets.inject('') do |sequence, scenario_offset|
263-
"#{sequence} -> #{nth_step_name(scenario_offset)}"
280+
sequence = scenario_offsets.inject([]) do |sequence, scenario_offset|
281+
sequence << nth_step_name(scenario_offset)
264282
end
265-
assert_complete_cycle_sequence sequence
283+
assert_complete_cycle_sequence *sequence
266284
end
267285

268286
def failed_output

features/step_definitions/cucumber_steps.js

+44-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ var cucumberSteps = function() {
2828
});
2929

3030
Given(/^a passing around hook$/, function(callback) {
31-
this.stepDefinitions += "this.Around(function(runScenario) {\
31+
this.stepDefinitions += "Around(function(runScenario) {\
3232
world.logCycleEvent('around-pre');\
3333
runScenario(function(callback) {\
3434
world.logCycleEvent('around-post');\
@@ -38,6 +38,33 @@ var cucumberSteps = function() {
3838
callback();
3939
});
4040

41+
Given(/^an untagged hook$/, function(callback) {
42+
this.stepDefinitions += "Before(function(callback) {\
43+
world.logCycleEvent('hook');\
44+
callback();\
45+
});\n";
46+
callback();
47+
});
48+
49+
Given(/^a hook tagged with "([^"]*)"$/, function(tag, callback) {
50+
this.stepDefinitions += "Before('" + tag +"', function(callback) {\
51+
world.logCycleEvent('hook');\
52+
callback();\
53+
});\n";
54+
callback();
55+
});
56+
57+
Given(/^an around hook tagged with "([^"]*)"$/, function(tag, callback) {
58+
this.stepDefinitions += "Around('" + tag + "', function(runScenario) {\
59+
world.logCycleEvent('hook-pre');\
60+
runScenario(function(callback) {\
61+
world.logCycleEvent('hook-post');\
62+
callback();\
63+
});\
64+
});\n";
65+
callback();
66+
});
67+
4168
Given(/^the step "([^"]*)" has a failing mapping$/, function(stepName, callback) {
4269
this.stepDefinitions += "Given(/^" + stepName + "$/, function(callback) {\
4370
world.touchStep(\"" + stepName + "\");\
@@ -133,10 +160,15 @@ setTimeout(callback.pending, 10);\
133160
this.runFeature({}, callback);
134161
});
135162

136-
When(/^Cucumber executes a scenario$/, function(callback) {
163+
When(/^Cucumber executes a scenario(?: with no tags)?$/, function(callback) {
137164
this.runAScenario(callback);
138165
});
139166

167+
When(/^Cucumber executes a scenario tagged with "([^"]*)"$/, function(tag, callback) {
168+
this.addPassingScenarioWithTags(tag);
169+
this.runFeature({}, callback);
170+
});
171+
140172
When(/^Cucumber runs the feature$/, function(callback) {
141173
this.runFeature({}, callback);
142174
});
@@ -267,6 +299,16 @@ callback();\
267299
callback();
268300
});
269301

302+
Then(/^the hook is fired$/, function(callback) {
303+
this.assertCycleSequence('hook');
304+
callback();
305+
});
306+
307+
Then(/^the hook is not fired$/, function(callback) {
308+
this.assertCycleSequenceExcluding('hook');
309+
callback();
310+
});
311+
270312
Then(/^an error about the missing World instance is raised$/, function(callback) {
271313
this.assertFailureMessage("World constructor called back without World instance");
272314
callback();

features/step_definitions/cucumber_steps.rb

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
write_asynchronously_failing_mapping_with_message(step_name, message)
1818
end
1919

20+
Given /^an around hook tagged with "([^"]*)"$/ do |tag|
21+
write_passing_hook :type => "around", :tags => [tag], :log_cycle_event_as => "hook"
22+
end
23+
2024
When /^Cucumber executes a scenario using that mapping$/ do
2125
write_feature <<-EOF
2226
Feature:

features/step_definitions/cucumber_world.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ var proto = World.prototype;
1515
proto.runFeature = function runFeature(options, callback) {
1616
var supportCode;
1717
var supportCodeSource = "supportCode = function() {\n var Given = When = Then = this.defineStep;\n" +
18-
" var Before = this.Before, After = this.After;\n" +
18+
" var Around = this.Around, Before = this.Before, After = this.After;\n" +
1919
this.stepDefinitions + "};\n";
2020
var world = this;
2121
eval(supportCodeSource);
@@ -215,6 +215,15 @@ proto.assertCompleteCycleSequence = function assertCompleteCycleSequence() {
215215

216216
}
217217

218+
proto.assertCycleSequenceExcluding = function assertCycleSequenceExcluding() {
219+
var self = this;
220+
var events = Array.prototype.slice.apply(arguments);
221+
events.forEach(function(event) {
222+
if (self.cycleEvents.indexOf(event) >= 0)
223+
throw(new Error("Expected cycle sequence \"" + self.cycleEvents + "\" not to contain \"" + event + "\""));
224+
});
225+
}
226+
218227
proto.indentCode = function indentCode(code, levels) {
219228
var indented = '';
220229
var lines = code.split("\n");

lib/cucumber/cli/argument_parser.js

+1-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
var _ = require('underscore');
2-
31
var ArgumentParser = function(argv) {
42
var Cucumber = require('../../cucumber');
53

@@ -55,11 +53,7 @@ var ArgumentParser = function(argv) {
5553

5654
getTagGroups: function getTagGroups() {
5755
var tagOptionValues = self.getOptionOrDefault(ArgumentParser.TAGS_OPTION_NAME, []);
58-
var tagGroups = _.map(tagOptionValues, function(tagOptionValue) {
59-
var tagGroupParser = Cucumber.TagGroupParser(tagOptionValue);
60-
var tagGroup = tagGroupParser.parse();
61-
return tagGroup;
62-
});
56+
var tagGroups = Cucumber.TagGroupParser.getTagGroupsFromStrings(tagOptionValues);
6357
return tagGroups;
6458
},
6559

lib/cucumber/runtime/ast_tree_walker.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ var AstTreeWalker = function(features, supportCodeLibrary, listeners) {
4545
self.witnessNewScenario();
4646
var payload = { scenario: scenario };
4747
var event = AstTreeWalker.Event(AstTreeWalker.SCENARIO_EVENT_NAME, payload);
48-
var hookedUpScenarioVisit = supportCodeLibrary.hookUpFunctionWithWorld(
48+
var hookedUpScenarioVisit = supportCodeLibrary.hookUpFunction(
4949
function(callback) { scenario.acceptVisitor(self, callback); },
50+
scenario,
5051
world
5152
);
5253
self.broadcastEventAroundUserFunction(

lib/cucumber/support_code/hook.js

+25-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,31 @@
1-
var Hook = function(code) {
1+
var _ = require('underscore');
2+
3+
var Hook = function(code, options) {
24
var Cucumber = require('../../cucumber');
35

6+
var tags = options['tags'] || [];
7+
48
var self = {
5-
invoke: function(world, callback) {
6-
code.call(world, callback);
9+
invokeBesideScenario: function invokeBesideScenario(scenario, world, callback) {
10+
if (self.appliesToScenario(scenario))
11+
code.call(world, callback);
12+
else
13+
callback(function(endPostScenarioAroundHook) { endPostScenarioAroundHook(); });
14+
},
15+
16+
appliesToScenario: function appliesToScenario(scenario) {
17+
var astFilter = self.getAstFilter();
18+
return astFilter.isScenarioEnrolled(scenario);
19+
},
20+
21+
getAstFilter: function getAstFilter() {
22+
var tagGroups = Cucumber.TagGroupParser.getTagGroupsFromStrings(tags);
23+
var rules = _.map(tagGroups, function(tagGroup) {
24+
var rule = Cucumber.Ast.Filter.AnyOfTagsRule(tagGroup);
25+
return rule;
26+
});
27+
var astFilter = Cucumber.Ast.Filter(rules);
28+
return astFilter;
729
}
830
};
931
return self;

lib/cucumber/support_code/library.js

+14-8
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,27 @@ var Library = function(supportCodeDefinition) {
2323
return (stepDefinition != undefined);
2424
},
2525

26-
hookUpFunctionWithWorld: function hookUpFunctionWithWorld(userFunction, world) {
27-
var hookedUpFunction = hooker.hookUpFunctionWithWorld(userFunction, world);
26+
hookUpFunction: function hookUpFunction(userFunction, scenario, world) {
27+
var hookedUpFunction = hooker.hookUpFunction(userFunction, scenario, world);
2828
return hookedUpFunction;
2929
},
3030

31-
defineAroundHook: function defineAroundHook(code) {
32-
hooker.addAroundHookCode(code);
31+
defineAroundHook: function defineAroundHook() {
32+
var tagGroupStrings = Cucumber.Util.Arguments(arguments);
33+
var code = tagGroupStrings.pop();
34+
hooker.addAroundHookCode(code, {tags: tagGroupStrings});
3335
},
3436

35-
defineBeforeHook: function defineBeforeHook(code) {
36-
hooker.addBeforeHookCode(code);
37+
defineBeforeHook: function defineBeforeHook() {
38+
var tagGroupStrings = Cucumber.Util.Arguments(arguments);
39+
var code = tagGroupStrings.pop();
40+
hooker.addBeforeHookCode(code, {tags: tagGroupStrings});
3741
},
3842

39-
defineAfterHook: function defineAfterHook(code) {
40-
hooker.addAfterHookCode(code);
43+
defineAfterHook: function defineAfterHook() {
44+
var tagGroupStrings = Cucumber.Util.Arguments(arguments);
45+
var code = tagGroupStrings.pop();
46+
hooker.addAfterHookCode(code, {tags: tagGroupStrings});
4147
},
4248

4349
defineStep: function defineStep(name, code) {

lib/cucumber/support_code/library/hooker.js

+14-14
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,43 @@ var Hooker = function() {
66
var afterHooks = Cucumber.Type.Collection();
77

88
var self = {
9-
addAroundHookCode: function addAroundHookCode(code) {
10-
var aroundHook = Cucumber.SupportCode.Hook(code);
9+
addAroundHookCode: function addAroundHookCode(code, options) {
10+
var aroundHook = Cucumber.SupportCode.Hook(code, options);
1111
aroundHooks.add(aroundHook);
1212
},
1313

14-
addBeforeHookCode: function addBeforeHookCode(code) {
15-
var beforeHook = Cucumber.SupportCode.Hook(code);
14+
addBeforeHookCode: function addBeforeHookCode(code, options) {
15+
var beforeHook = Cucumber.SupportCode.Hook(code, options);
1616
beforeHooks.add(beforeHook);
1717
},
1818

19-
addAfterHookCode: function addAfterHookCode(code) {
20-
var afterHook = Cucumber.SupportCode.Hook(code);
19+
addAfterHookCode: function addAfterHookCode(code, options) {
20+
var afterHook = Cucumber.SupportCode.Hook(code, options);
2121
afterHooks.unshift(afterHook);
2222
},
2323

24-
hookUpFunctionWithWorld: function hookUpFunctionWithWorld(userFunction, world) {
24+
hookUpFunction: function hookUpFunction(userFunction, scenario, world) {
2525
var hookedUpFunction = function(callback) {
2626
var postScenarioAroundHookCallbacks = Cucumber.Type.Collection();
2727
aroundHooks.forEach(callPreScenarioAroundHook, callBeforeHooks);
2828

2929
function callPreScenarioAroundHook(aroundHook, preScenarioAroundHookCallback) {
30-
aroundHook.invoke(world, function(postScenarioAroundHookCallback) {
30+
aroundHook.invokeBesideScenario(scenario, world, function(postScenarioAroundHookCallback) {
3131
postScenarioAroundHookCallbacks.unshift(postScenarioAroundHookCallback);
3232
preScenarioAroundHookCallback();
3333
});
3434
}
3535

3636
function callBeforeHooks() {
37-
self.triggerBeforeHooks(world, callUserFunction);
37+
self.triggerBeforeHooks(scenario, world, callUserFunction);
3838
}
3939

4040
function callUserFunction() {
4141
userFunction(callAfterHooks);
4242
}
4343

4444
function callAfterHooks() {
45-
self.triggerAfterHooks(world, callPostScenarioAroundHooks);
45+
self.triggerAfterHooks(scenario, world, callPostScenarioAroundHooks);
4646
}
4747

4848
function callPostScenarioAroundHooks() {
@@ -59,15 +59,15 @@ var Hooker = function() {
5959
return hookedUpFunction;
6060
},
6161

62-
triggerBeforeHooks: function triggerBeforeHooks(world, callback) {
62+
triggerBeforeHooks: function triggerBeforeHooks(scenario, world, callback) {
6363
beforeHooks.forEach(function(beforeHook, callback) {
64-
beforeHook.invoke(world, callback);
64+
beforeHook.invokeBesideScenario(scenario, world, callback);
6565
}, callback);
6666
},
6767

68-
triggerAfterHooks: function triggerAfterHooks(world, callback) {
68+
triggerAfterHooks: function triggerAfterHooks(scenario, world, callback) {
6969
afterHooks.forEach(function(afterHook, callback) {
70-
afterHook.invoke(world, callback);
70+
afterHook.invokeBesideScenario(scenario, world, callback);
7171
}, callback);
7272
}
7373
};

0 commit comments

Comments
 (0)