Skip to content

Commit

Permalink
Add tags support (close #7)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbpros committed Feb 3, 2012
1 parent ad62dfc commit c5dcf07
Show file tree
Hide file tree
Showing 32 changed files with 1,020 additions and 53 deletions.
2 changes: 1 addition & 1 deletion features/cucumber-tck
Submodule cucumber-tck updated from aa4f85 to 5d3b3d
47 changes: 43 additions & 4 deletions features/step_definitions/cucumber_js_mappings.rb
Expand Up @@ -18,6 +18,15 @@ def run_feature
run_simple "#{cucumber_bin} #{FEATURE_FILE}", false
end

def run_feature_with_tag_groups tag_groups
write_main_step_definitions_file
command = "#{cucumber_bin} #{FEATURE_FILE}"
tag_groups.each do |tag_group|
command += " --tags #{tag_group.join(',')}"
end
run_simple command, false
end

def cucumber_bin
File.expand_path(File.dirname(__FILE__) + '/../../bin/cucumber.js')
end
Expand Down Expand Up @@ -131,17 +140,31 @@ def write_scenario
scenario_with_steps("A scenario", "Given a step")
end

def provide_cycle_logging_facilities
return if @cycle_logging_facilities_ready
def write_passing_scenario_with_tags(tags)
tags = [tags] unless tags.respond_to? :any?
@next_step_count ||= 0
step_name = nth_step_name @next_step_count += 1
provide_cycle_logging_facilities
append_step_definition(step_name, "this.logCycleEvent('#{step_name}');\ncallback();")
append_to_feature <<-EOF
@cycle_logging_facilities_ready = true
append_support_code <<-EOF
#{tags.join(' ')}
Scenario: scenario tagged with #{tags.join(', ')}
Given #{step_name}
EOF
end

def provide_cycle_logging_facilities
unless @cycle_logging_facilities_ready
append_support_code <<-EOF
this.World.prototype.logCycleEvent = function logCycleEvent(name) {
fd = fs.openSync('#{CYCLE_LOG_FILE}', 'a');
fs.writeSync(fd, " -> " + name, null);
fs.closeSync(fd);
};
EOF
@cycle_logging_facilities_ready = true
end
end

def assert_passing_scenario
Expand Down Expand Up @@ -186,6 +209,11 @@ def assert_cycle_sequence *args
check_file_content(CucumberJsMappings::CYCLE_LOG_FILE, expected_string, true)
end

def assert_complete_cycle_sequence *args
expected_string = args.join " -> "
check_exact_file_content(CucumberJsMappings::CYCLE_LOG_FILE, expected_string)
end

def assert_data_table_equals_json(json)
prep_for_fs_check do
log_file_contents = IO.read(DATA_TABLE_LOG_FILE)
Expand All @@ -211,6 +239,13 @@ def assert_suggested_step_definition_snippet(stepdef_keyword, stepdef_pattern, p
assert_partial_output(expected_snippet, all_output)
end

def assert_executed_scenarios *scenario_offsets
sequence = scenario_offsets.inject('') do |sequence, scenario_offset|
"#{sequence} -> #{nth_step_name(scenario_offset)}"
end
assert_complete_cycle_sequence sequence
end

def failed_output
"failed"
end
Expand Down Expand Up @@ -258,5 +293,9 @@ def get_file_contents(file_path)
f.read
end
end

def nth_step_name n
"step #{n}"
end
end
World(CucumberJsMappings)
79 changes: 73 additions & 6 deletions features/step_definitions/cucumber_steps.js
Expand Up @@ -77,32 +77,79 @@ var cucumberSteps = function() {
callback();
});

Given(/^a scenario tagged with "([^"]*)"$/, function(tag, callback) {
this.addPassingScenarioWithTags([tag]);
callback();
});

Given(/^a scenario tagged with "([^"]*)" and "([^"]*)"$/, function(tag1, tag2, callback) {
this.addPassingScenarioWithTags([tag1, tag2]);
callback();
});

this.Given(/^a scenario tagged with "([^"]*)", "([^"]*)" and "([^"]*)"$/, function(tag1, tag2, tag3, callback) {
this.addPassingScenarioWithTags([tag1, tag2, tag3]);
callback();
});

When(/^Cucumber executes the scenario$/, function(callback) {
this.runFeature(callback);
this.runFeature({}, callback);
});

When(/^Cucumber executes a scenario$/, function(callback) {
this.runAScenario(callback);
});

When(/^Cucumber runs the feature$/, function(callback) {
this.runFeature(callback);
this.runFeature({}, callback);
});

When(/^Cucumber runs the scenario with steps for a calculator$/, function(callback) {
RpnCalculator = require('../support/rpn_calculator');
var supportCode = function() { require('./calculator_steps').initialize.call(this, RpnCalculator) };
this.runFeatureWithSupportCodeSource(supportCode, callback);
this.runFeatureWithSupportCodeSource(supportCode, {}, callback);
});

When(/^the data table is passed to a step mapping that converts it to key\/value pairs$/, function(callback) {
this.stepDefinitions += "When(/^a step with data table:$/, function(dataTable, callback) {\
world.dataTableLog = dataTable.hashes();\
callback();\
world.dataTableLog = dataTable.hashes();\
callback();\
});\n";
this.runFeature(callback);
this.runFeature({}, callback);
});

When(/^Cucumber executes scenarios tagged with "([^"]*)"$/, function(tag, callback) {
this.runFeature({tags: [[tag]]}, callback);
});

When(/^Cucumber executes scenarios not tagged with "([^"]*)"$/, function(tag, callback) {
this.runFeature({tags: [['~'+tag]]}, callback);
});

When(/^Cucumber executes scenarios tagged with "([^"]*)" or "([^"]*)"$/, function(tag1, tag2, callback) {
this.runFeature({tags: [[tag1, tag2]]}, callback);
});

When(/^Cucumber executes scenarios tagged with both "([^"]*)" and "([^"]*)"$/, function(tag1, tag2, callback) {
this.runFeature({tags: [[tag1], [tag2]]}, callback);
});

When(/^Cucumber executes scenarios not tagged with "([^"]*)" nor "([^"]*)"$/, function(tag1, tag2, callback) {
this.runFeature({tags: [['~'+tag1], ['~'+tag2]]}, callback);
});

When(/^Cucumber executes scenarios not tagged with both "([^"]*)" and "([^"]*)"$/, function(tag1, tag2, callback) {
this.runFeature({tags: [['~'+tag1, '~'+tag2]]}, callback);
});

When(/^Cucumber executes scenarios tagged with "([^"]*)" or without "([^"]*)"$/, function(tag1, tag2, callback) {
this.runFeature({tags: [[tag1, '~'+tag2]]}, callback);
});

When(/^Cucumber executes scenarios tagged with "([^"]*)" but not with both "([^"]*)" and "([^"]*)"$/, function(tag1, tag2, tag3, callback) {
this.runFeature({tags: [[tag1], ['~'+tag2], ['~'+tag3]]}, callback);
});

Then(/^the scenario passes$/, function(callback) {
this.assertPassedScenario();
callback();
Expand Down Expand Up @@ -177,5 +224,25 @@ var cucumberSteps = function() {
this.assertFailureMessage("World constructor called back without World instance");
callback();
});

Then(/^only the first scenario is executed$/, function(callback) {
this.assertExecutedNumberedScenarios(1);
callback();
});

Then(/^only the first two scenarios are executed$/, function(callback) {
this.assertExecutedNumberedScenarios(1, 2);
callback();
});

Then(/^only the third scenario is executed$/, function(callback) {
this.assertExecutedNumberedScenarios(3);
callback();
});

Then(/^only the second, third and fourth scenarios are executed$/, function(callback) {
this.assertExecutedNumberedScenarios(2, 3, 4);
callback();
});
};
module.exports = cucumberSteps;
92 changes: 81 additions & 11 deletions features/step_definitions/cucumber_world.js
Expand Up @@ -4,28 +4,33 @@ var World = function(callback) {
this.stepDefinitions = "";
this.runOutput = "";
this.cycleEvents = "";
this.stepCount = 0;
this.runSucceeded = false;
World.mostRecentInstance = this;
callback(this);
};

var proto = World.prototype;

proto.runFeature = function runFeature(callback) {
proto.runFeature = function runFeature(options, 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);
this.runFeatureWithSupportCodeSource(supportCode, callback);
this.runFeatureWithSupportCodeSource(supportCode, options, callback);
}

proto.runFeatureWithSupportCodeSource = function runFeatureWithSupportCodeSource(supportCode, callback) {
proto.runFeatureWithSupportCodeSource = function runFeatureWithSupportCodeSource(supportCode, options, callback) {
var world = this;
var Cucumber = require('../../lib/cucumber');
var cucumber = Cucumber(this.featureSource, supportCode);
options = options || {};
var tags = options['tags'] || [];

var cucumber = Cucumber(this.featureSource, supportCode, {tags: tags});
var formatter = Cucumber.Listener.ProgressFormatter({logToConsole: false});

cucumber.attachListener(formatter);
try {
cucumber.start(function(succeeded) {
Expand All @@ -42,14 +47,12 @@ proto.runFeatureWithSupportCodeSource = function runFeatureWithSupportCodeSource
}

proto.runAScenario = function runAScenario(callback) {
this.featureSource += "Feature:\n";
this.featureSource += " Scenario:\n";
this.featureSource += " Given a step\n";
this.stepDefinitions += "Given(/^a step$/, function(callback) {\
this.addScenario("", "Given a step");
this.stepDefinitions += "Given(/^a step$/, function(callback) {\
world.logCycleEvent('step');\
callback();\
});";
this.runFeature(callback);
this.runFeature({}, callback);
}

proto.logCycleEvent = function logCycleEvent(event) {
Expand All @@ -64,6 +67,40 @@ proto.isStepTouched = function isStepTouched(pattern) {
return (this.touchedSteps.indexOf(pattern) >= 0);
}

proto.addScenario = function addScenario(name, contents, options) {
options = options || {};
var tags = options['tags'] || [];
var tagString = (tags.length > 0 ? tags.join(" ") + "\n" : "");
var scenarioName = tagString + "Scenario: " + name;
this.createEmptyFeature();
this.featureSource += this.indentCode(scenarioName, 1);
this.featureSource += this.indentCode(contents, 2);
};

proto.addPassingScenarioWithTags = function addPassingScenarioWithTags(tags) {
var stepName = this.makeNumberedStepName();
var scenarioName = "A scenario tagged with " + tags.join(', ');
var step = "Given " + stepName + "\n";
this.addScenario(scenarioName, step, {tags: tags});
this.stepDefinitions += "Given(/^" + stepName + "$/, function(callback) {\
world.logCycleEvent('" + stepName + "');\
callback();\
});\n";
};

proto.createEmptyFeature = function createEmptyFeature() {
if (!this.emptyFeatureReady) {
this.featureSource += "Feature: A feature\n\n";
this.emptyFeatureReady = true;
}
};

proto.makeNumberedStepName = function makeNumberedStepName(index) {
var index = index || (++this.stepCount);
var stepName = "step " + index;
return stepName;
}

proto.assertPassedFeature = function assertPassedFeature() {
this.assertNoPartialOutput("failed", this.runOutput);
this.assertSuccess();
Expand Down Expand Up @@ -140,10 +177,43 @@ proto.assertEqual = function assertRawDataTable(expected, actual) {
throw(new Error("Expected:\n\"" + actualJSON + "\"\nto match:\n\"" + expectedJSON + "\""));
}

proto.assertCycleSequence = function assertCycleSequence(first, second) {
var partialSequence = first + ' -> ' + second;
proto.assertExecutedNumberedScenarios = function assertExecutedNumberedScenarios() {
var self = this;
var scenarioIndexes = Array.prototype.slice.apply(arguments);
var stepNames = [];
scenarioIndexes.forEach(function(scenarioIndex) {
var stepName = self.makeNumberedStepName(scenarioIndex);
stepNames.push(stepName);
});
this.assertCompleteCycleSequence.apply(this, stepNames);
}

proto.assertCycleSequence = function assertCycleSequence() {
var events = Array.prototype.slice.apply(arguments);
var partialSequence = ' -> ' + events.join(' -> ');
if (this.cycleEvents.indexOf(partialSequence) < 0)
throw(new Error("Expected cycle sequence \"" + this.cycleEvents + "\" to contain \"" + partialSequence + "\""));
}

proto.assertCompleteCycleSequence = function assertCompleteCycleSequence() {
var events = Array.prototype.slice.apply(arguments);
var sequence = ' -> ' + events.join(' -> ');

if (this.cycleEvents != sequence)
throw(new Error("Expected cycle sequence \"" + this.cycleEvents + "\" to be \"" + sequence + "\""));

}

proto.indentCode = function indentCode(code, levels) {
var indented = '';
var lines = code.split("\n");
levels = levels || 1;

lines.forEach(function(line) {
var indent = (line == "" ? "" : Array(levels + 1).join(" "));
indented += indent + line + "\n";
});
return indented;
};

exports.World = World;
4 changes: 2 additions & 2 deletions lib/cucumber.js
@@ -1,5 +1,5 @@
var Cucumber = function(featureSource, supportCodeInitializer) {
var configuration = Cucumber.VolatileConfiguration(featureSource, supportCodeInitializer);
var Cucumber = function(featureSource, supportCodeInitializer, options) {
var configuration = Cucumber.VolatileConfiguration(featureSource, supportCodeInitializer, options);
var runtime = Cucumber.Runtime(configuration);
return runtime;
};
Expand Down
2 changes: 2 additions & 0 deletions lib/cucumber/ast.js
Expand Up @@ -5,6 +5,8 @@ Ast.DataTable = require('./ast/data_table');
Ast.DocString = require('./ast/doc_string');
Ast.Feature = require('./ast/feature');
Ast.Features = require('./ast/features');
Ast.Filter = require('./ast/filter');
Ast.Scenario = require('./ast/scenario');
Ast.Step = require('./ast/step');
Ast.Tag = require('./ast/tag');
module.exports = Ast;

0 comments on commit c5dcf07

Please sign in to comment.