Skip to content

Commit

Permalink
Add support for string-based step definition patterns (closes #48)
Browse files Browse the repository at this point in the history
Thanks to Ted de Koning (@tdekoning) for the original pull request.
  • Loading branch information
jbpros committed Apr 18, 2012
1 parent 91d68d9 commit 8aa8d9d
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 17 deletions.
34 changes: 26 additions & 8 deletions README.md
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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();
});
Expand All @@ -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.
Expand Down
1 change: 0 additions & 1 deletion features/coffeescript_support.feature
Expand Up @@ -8,4 +8,3 @@ Feature: CoffeeScript support
When Cucumber executes a scenario using that mapping
Then the feature passes
And the mapping is run

16 changes: 16 additions & 0 deletions 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
23 changes: 23 additions & 0 deletions features/step_definitions/cucumber_js_mappings.rb
Expand Up @@ -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
Expand Down Expand Up @@ -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|
Expand Down
29 changes: 28 additions & 1 deletion features/step_definitions/cucumber_steps.js
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
});

Expand Down Expand Up @@ -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();
Expand Down
22 changes: 22 additions & 0 deletions features/step_definitions/cucumber_steps.rb
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
38 changes: 38 additions & 0 deletions features/step_definitions/cucumber_world.js
Expand Up @@ -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) {\
Expand All @@ -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'] || [];
Expand Down Expand Up @@ -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."));
Expand Down
39 changes: 35 additions & 4 deletions 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) {
Expand All @@ -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);
};
Expand Down Expand Up @@ -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;
44 changes: 41 additions & 3 deletions spec/cucumber/support_code/step_definition_spec.js
Expand Up @@ -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);
});
});
});

Expand Down

0 comments on commit 8aa8d9d

Please sign in to comment.