Skip to content
Browse files

Add support for string-based step definition patterns (closes #48)

Thanks to Ted de Koning (@tdekoning) for the original pull request.
  • Loading branch information...
1 parent 91d68d9 commit 8aa8d9d6201074af08a2e1d8b32c1d7178c79f8a @jbpros jbpros committed Apr 18, 2012
View
34 README.md
@@ -105,9 +105,9 @@ Feature: Example feature
So that I can concentrate on building awesome applications
Scenario: Reading documentation
- Given I am on the cucumber.js github page
+ Given I am on the Cucumber.js Github repository
When I go to the README file
- Then I should see "Usage"
+ Then I should see "Usage" as the page title
```
### Support Files
@@ -172,7 +172,7 @@ Step definitions are run when steps match their name. `this` is an instance of `
var myStepDefinitionsWrapper = function () {
this.World = require("../support/world.js").World; // overwrite default World constructor
- this.Given(/REGEXP/, function(callback) {
+ this.Given(/^I am on the Cucumber.js Github repository$/, function(callback) {
// Express the regexp above with the code you wish you had.
// `this` is set to a new this.World instance.
// i.e. you may use this.browser to execute the step:
@@ -183,18 +183,19 @@ var myStepDefinitionsWrapper = function () {
// be executed by Cucumber.
});
- this.When(/REGEXP/, function(callback) {
+ this.When(/^I go to the README file$/, function(callback) {
// Express the regexp above with the code you wish you had. Call callback() at the end
// of the step, or callback.pending() if the step is not yet implemented:
callback.pending();
});
- this.Then(/REGEXP/, function(callback) {
- // You can make steps fail by calling the `fail()` function on the callback:
+ this.Then(/^I should see "(.*)" as the page title$/, function(title, callback) {
+ // matching groups are passed as parameters to the step definition
- if (!this.isOnPageWithTitle("Cucumber.js demo"))
- callback.fail(new Error("Expected to be on 'Cucumber.js demo' page"));
+ if (!this.isOnPageWithTitle(title))
+ // You can make steps fail by calling the `fail()` function on the callback:
+ callback.fail(new Error("Expected to be on page with title " + title));
else
callback();
});
@@ -203,6 +204,23 @@ var myStepDefinitionsWrapper = function () {
module.exports = myStepDefinitionsWrapper;
```
+It is also possible to use simple strings instead of regexps as step definition patterns:
+
+```javascript
+this.Then('I should see "$title" as the page title', function(title, callback) {
+ // the above string is converted to the following Regexp by Cucumber:
+ // /^I should see "([^"]*)" as the page title$/
+
+ if (!this.isOnPageWithTitle(title))
+ // You can make steps fail by calling the `fail()` function on the callback:
+ callback.fail(new Error("Expected to be on page with title " + title));
+ else
+ callback();
+});
+```
+
+`'I have $count "$string"'` would translate to `/^I have (.*) "([^"]*)")$/`.
+
#### Hooks
Hooks can be used to prepare and clean the environment before and after each scenario is executed.
View
1 features/coffeescript_support.feature
@@ -8,4 +8,3 @@ Feature: CoffeeScript support
When Cucumber executes a scenario using that mapping
Then the feature passes
And the mapping is run
-
View
16 features/step_definition_string_pattern.feature
@@ -0,0 +1,16 @@
+Feature: step definitions with string pattern
+ Some people don't like Regexps as step definition patterns.
+ Cucumber also supports string-based patterns.
+
+ Scenario: step definition with string-based pattern
+ Given a mapping with a string-based pattern
+ When Cucumber executes a scenario using that mapping
+ Then the feature passes
+ And the mapping is run
+
+ Scenario: step definition with string-based pattern and parameters
+ Given a mapping with a string-based pattern and parameters
+ When Cucumber executes a scenario that passes arguments to that mapping
+ Then the feature passes
+ And the mapping is run
+ And the mapping receives the arguments
View
23 features/step_definitions/cucumber_js_mappings.rb
@@ -292,6 +292,11 @@ def assert_executed_scenarios *scenario_offsets
assert_complete_cycle_sequence *sequence
end
+ def assert_passed_with_arguments(pattern, arguments)
+ raise "#{pattern} did not pass" unless pattern_exists?(pattern)
+ check_exact_file_content step_file(pattern), arguments.join("\n")
+ end
+
def failed_output
"failed"
end
@@ -333,6 +338,24 @@ def write_coffee_script_definition_file
EOF
end
+ def write_string_based_pattern_mapping
+ append_support_code <<-EOF
+this.defineStep("a mapping", function(callback) {
+ fs.writeFileSync("#{step_file("a mapping")}", "");
+ callback();
+});
+EOF
+ end
+
+ def write_string_based_pattern_mapping_with_parameters
+ append_support_code <<-EOF
+this.defineStep('a mapping with $word_param "$multi_word_param"', function(p1, p2, callback) {
+ fs.writeFileSync("#{step_file("a mapping")}", p1 + "\\n" + p2);
+ callback();
+});
+EOF
+ end
+
def get_file_contents(file_path)
file_realpath = File.expand_path(file_path, File.dirname(__FILE__))
File.open(file_realpath, 'rb') do |f|
View
29 features/step_definitions/cucumber_steps.js
@@ -105,6 +105,16 @@ setTimeout(callback.pending, 10);\
callback();
});
+ Given(/^a mapping with a string-based pattern$/, function(callback) {
+ this.addStringBasedPatternMapping();
+ callback();
+ });
+
+ Given(/^a mapping with a string-based pattern and parameters$/, function(callback) {
+ this.addStringBasedPatternMappingWithParameters();
+ callback();
+ });
+
Given(/^the following feature:$/, function(feature, callback) {
this.featureSource = feature;
callback();
@@ -166,8 +176,15 @@ setTimeout(callback.pending, 10);\
this.runAScenario(callback);
});
+ this.When(/^Cucumber executes a scenario using that mapping$/, function(callback) {
+ this.runAScenarioCallingMapping(callback);
+ });
+
+ this.When(/^Cucumber executes a scenario that passes arguments to that mapping$/, function(callback) {
+ this.runAScenarioCallingMappingWithParameters(callback);
+ });
+
When(/^Cucumber executes a scenario that calls a function on the explicit World object$/, function(callback) {
- // express the regexp above with the code you wish you had
this.runAScenarioCallingWorldFunction(callback);
});
@@ -266,6 +283,16 @@ callback();\
callback();
});
+ Then(/^the mapping is run$/, function(callback) {
+ this.assertPassedMapping();
+ callback();
+ });
+
+ Then(/^the mapping receives the arguments$/, function(callback) {
+ this.assertPassedMappingWithArguments();
+ callback();
+ });
+
Then(/^the feature passes$/, function(callback) {
this.assertPassedFeature();
callback();
View
22 features/step_definitions/cucumber_steps.rb
@@ -5,6 +5,14 @@
write_coffee_script_definition_file
end
+Given /^a mapping with a string-based pattern$/ do
+ write_string_based_pattern_mapping
+end
+
+Given /^a mapping with a string-based pattern and parameters$/ do
+ write_string_based_pattern_mapping_with_parameters
+end
+
Given /^the step "([^"]*)" has an asynchronous pending mapping$/ do |step_name|
write_asynchronous_pending_mapping(step_name)
end
@@ -30,6 +38,16 @@
run_feature
end
+When /^Cucumber executes a scenario that passes arguments to that mapping$/ do
+ @mapping_arguments = [5, "cucumbers in perfect state"]
+ write_feature <<-EOF
+Feature:
+ Scenario:
+ Given a mapping with #{@mapping_arguments[0]} "#{@mapping_arguments[1]}"
+EOF
+ run_feature
+end
+
When /^Cucumber executes a scenario that calls a function on the explicit World object$/ do
write_mapping_calling_world_function("I call the explicit world object function")
write_feature <<-EOF
@@ -44,6 +62,10 @@
assert_passed "a mapping"
end
+Then /^the mapping receives the arguments$/ do
+ assert_passed_with_arguments "a mapping", @mapping_arguments
+end
+
Then /^the explicit World object function should have been called$/ do
assert_explicit_world_object_function_called
end
View
38 features/step_definitions/cucumber_world.js
@@ -55,6 +55,17 @@ proto.runAScenario = function runAScenario(callback) {
this.runFeature({}, callback);
};
+proto.runAScenarioCallingMapping = function runAScenarioCallingMapping(callback) {
+ this.addScenario("", "Given a mapping");
+ this.runFeature({}, callback);
+};
+
+proto.runAScenarioCallingMappingWithParameters = function runAScenarioCallingMappingWithParameters(callback) {
+ this.expectedMappingArguments = [5, "fresh cucumbers"];
+ this.addScenario("", 'Given a mapping with ' + this.expectedMappingArguments[0] + ' "' + this.expectedMappingArguments[1] + '"');
+ this.runFeature({}, callback);
+};
+
proto.runAScenarioCallingWorldFunction = function runAScenarioCallingWorldFunction(callback) {
this.addScenario("", "Given a step");
this.stepDefinitions += "Given(/^a step$/, function(callback) {\
@@ -77,6 +88,21 @@ proto.isStepTouched = function isStepTouched(pattern) {
return (this.touchedSteps.indexOf(pattern) >= 0);
};
+proto.addStringBasedPatternMapping = function addStringBasedPatternMapping() {
+ this.stepDefinitions += "Given('a mapping', function(callback) {\
+ world.logCycleEvent('a mapping');\
+ callback();\
+});";
+};
+
+proto.addStringBasedPatternMappingWithParameters = function addStringBasedPatternMappingWithParameters() {
+ this.stepDefinitions += "Given('a mapping with $word_param \"$multi_word_param\"', function(p1, p2, callback) {\
+ world.logCycleEvent('a mapping');\
+ world.actualMappingArguments = [p1, p2];\
+ callback();\
+});";
+};
+
proto.addScenario = function addScenario(name, contents, options) {
options = options || {};
var tags = options['tags'] || [];
@@ -166,6 +192,18 @@ proto.assertSkippedStep = function assertSkippedStep(stepName) {
throw(new Error("Expected step \"" + stepName + "\" to have been skipped."));
};
+proto.assertPassedMapping = function assertPassedMapping() {
+ this.assertCycleSequence("a mapping");
+};
+
+proto.assertPassedMappingWithArguments = function assertPassedMappingWithArguments() {
+ this.assertPassedMapping();
+ if (this.actualMappingArguments.length != this.expectedMappingArguments.length ||
+ this.actualMappingArguments[0] != this.expectedMappingArguments[0] ||
+ this.actualMappingArguments[1] != this.expectedMappingArguments[1])
+ throw(new Error("Expected arguments to be passed to mapping."));
+};
+
proto.assertSuccess = function assertSuccess() {
if (!this.runSucceeded)
throw(new Error("Expected Cucumber to succeed but it failed."));
View
39 lib/cucumber/support_code/step_definition.js
@@ -1,11 +1,35 @@
-var UNKNOWN_STEP_FAILURE_MESSAGE = "Step failure";
-
var StepDefinition = function(pattern, code) {
var Cucumber = require('../../cucumber');
+ // var constructor = function() {
+ // // Converts a string to a proper regular expression
+ // var parseRegexpString = function( regexpString ) {
+
+ // // Replace the $VARIABLES with the correct regex.
+ // regexpString = regexpString.replace(/\$[a-zA-Z0-9]+/g, '([^"]*)');
+ // return new RegExp( regexpString );
+ // }
+
+ // if( typeof(regexp)=='string' ) {
+ // // regexp is a string, convert it to a regexp.
+ // regexp = parseRegexpString(regexp);
+ // }
+ // }
+
+ // constructor();
+
var self = {
getPatternRegexp: function getPatternRegexp() {
- return pattern;
+ var regexp;
+ if (pattern.replace) {
+ var regexpString = pattern
+ .replace(StepDefinition.QUOTED_DOLLAR_PARAMETER_REGEXP, StepDefinition.QUOTED_DOLLAR_PARAMETER_SUBSTITUTION)
+ .replace(StepDefinition.DOLLAR_PARAMETER_REGEXP, StepDefinition.DOLLAR_PARAMETER_SUBSTITUTION);
+ regexp = RegExp(regexpString);
+ }
+ else
+ regexp = pattern;
+ return regexp;
},
matchesStepName: function matchesStepName(stepName) {
@@ -25,7 +49,7 @@ var StepDefinition = function(pattern, code) {
};
codeCallback.fail = function fail(failureReason) {
- var failureException = failureReason || new Error(UNKNOWN_STEP_FAILURE_MESSAGE);
+ var failureException = failureReason || new Error(StepDefinition.UNKNOWN_STEP_FAILURE_MESSAGE);
var failedStepResult = Cucumber.Runtime.FailedStepResult({step: step, failureException: failureException});
callback(failedStepResult);
};
@@ -55,4 +79,11 @@ var StepDefinition = function(pattern, code) {
};
return self;
};
+
+StepDefinition.DOLLAR_PARAMETER_REGEXP = /\$[a-zA-Z_-]+/;
+StepDefinition.DOLLAR_PARAMETER_SUBSTITUTION = '(.*)';
+StepDefinition.QUOTED_DOLLAR_PARAMETER_REGEXP = /"\$[a-zA-Z_-]+"/;
+StepDefinition.QUOTED_DOLLAR_PARAMETER_SUBSTITUTION = '"([^"]*)"';
+StepDefinition.UNKNOWN_STEP_FAILURE_MESSAGE = "Step failure";
+
module.exports = StepDefinition;
View
44 spec/cucumber/support_code/step_definition_spec.js
@@ -5,14 +5,52 @@ describe("Cucumber.SupportCode.StepDefinition", function() {
var stepDefinition, pattern, stepDefinitionCode;
beforeEach(function() {
- pattern = createSpyWithStubs("pattern", {test:null});
+ pattern = createSpyWithStubs("pattern", {test: null});
stepDefinitionCode = createSpy("step definition code");
stepDefinition = Cucumber.SupportCode.StepDefinition(pattern, stepDefinitionCode);
+ spyOn(global, 'RegExp');
});
describe("getPatternRegexp()", function() {
- it("returns the pattern itself", function() {
- expect(stepDefinition.getPatternRegexp()).toBe(pattern);
+ describe("when the pattern is a RegExp", function() {
+ it("does not instantiate a RegExp", function() {
+ expect(global.RegExp).not.toHaveBeenCalled();
+ });
+
+ it("returns the pattern itself", function() {
+ expect(stepDefinition.getPatternRegexp()).toBe(pattern);
+ });
+ });
+
+ describe("when the pattern is a String", function() {
+ var regexp, quotedDollarParameterSubstitutedPattern, regexpString;
+
+ beforeEach(function() {
+ regexp = createSpy("regexp");
+ regexpString = createSpy("regexp string");
+ quotedDollarParameterSubstitutedPattern = createSpyWithStubs("quoted dollar param substituted pattern", {replace: regexpString});
+ spyOnStub(pattern, 'replace').andReturn(quotedDollarParameterSubstitutedPattern);
+ global.RegExp.andReturn(regexp);
+ });
+
+ it("replaces quoted dollar-prefixed parameters with the regexp equivalent", function() {
+ stepDefinition.getPatternRegexp();
+ expect(pattern.replace).toHaveBeenCalledWith(Cucumber.SupportCode.StepDefinition.QUOTED_DOLLAR_PARAMETER_REGEXP, Cucumber.SupportCode.StepDefinition.QUOTED_DOLLAR_PARAMETER_SUBSTITUTION);
+ });
+
+ it("replaces other dollar-prefixed parameter with the regexp equivalent", function() {
+ stepDefinition.getPatternRegexp();
+ expect(quotedDollarParameterSubstitutedPattern.replace).toHaveBeenCalledWith(Cucumber.SupportCode.StepDefinition.DOLLAR_PARAMETER_REGEXP, Cucumber.SupportCode.StepDefinition.DOLLAR_PARAMETER_SUBSTITUTION);
+ });
+
+ it("instantiates a new RegExp", function() {
+ stepDefinition.getPatternRegexp();
+ expect(global.RegExp).toHaveBeenCalledWith(regexpString);
+ });
+
+ it("returns the new RegExp", function() {
+ expect(stepDefinition.getPatternRegexp()).toBe(regexp);
+ });
});
});

0 comments on commit 8aa8d9d

Please sign in to comment.
Something went wrong with that request. Please try again.