From c5dcf07a4c674becee147f6aeaa96e4a0f3bf1ee Mon Sep 17 00:00:00 2001 From: Julien Biezemans Date: Fri, 3 Feb 2012 22:28:06 +0100 Subject: [PATCH] Add tags support (close #7) --- features/cucumber-tck | 2 +- .../step_definitions/cucumber_js_mappings.rb | 47 +++++- features/step_definitions/cucumber_steps.js | 79 ++++++++- features/step_definitions/cucumber_world.js | 92 +++++++++-- lib/cucumber.js | 4 +- lib/cucumber/ast.js | 2 + lib/cucumber/ast/assembler.js | 30 +++- lib/cucumber/ast/feature.js | 11 +- lib/cucumber/ast/filter.js | 16 ++ lib/cucumber/ast/filter/any_of_tags_rule.js | 17 ++ .../ast/filter/element_matching_tag_spec.js | 35 ++++ lib/cucumber/ast/scenario.js | 11 +- lib/cucumber/ast/tag.js | 11 ++ lib/cucumber/cli/argument_parser.js | 15 ++ lib/cucumber/cli/configuration.js | 16 ++ lib/cucumber/parser.js | 12 +- lib/cucumber/runtime.js | 3 +- lib/cucumber/volatile_configuration.js | 20 ++- spec/cucumber/ast/assembler_spec.js | 115 ++++++++++++- spec/cucumber/ast/feature_spec.js | 8 + .../ast/filter/any_of_tags_rule_spec.js | 62 +++++++ .../filter/element_matching_tag_spec_spec.js | 154 ++++++++++++++++++ spec/cucumber/ast/filter_spec.js | 55 +++++++ spec/cucumber/ast/scenario_spec.js | 8 + spec/cucumber/ast/tag_spec.js | 19 +++ spec/cucumber/cli/argument_parser_spec.js | 54 ++++++ spec/cucumber/cli/configuration_spec.js | 52 ++++++ spec/cucumber/parser_spec.js | 35 +++- spec/cucumber/runtime_spec.js | 13 +- spec/cucumber/volatile_configuration_spec.js | 60 +++++++ spec/cucumber_spec.js | 11 +- .../support/configurations_shared_examples.js | 4 + 32 files changed, 1020 insertions(+), 53 deletions(-) create mode 100644 lib/cucumber/ast/filter.js create mode 100644 lib/cucumber/ast/filter/any_of_tags_rule.js create mode 100644 lib/cucumber/ast/filter/element_matching_tag_spec.js create mode 100644 lib/cucumber/ast/tag.js create mode 100644 spec/cucumber/ast/filter/any_of_tags_rule_spec.js create mode 100644 spec/cucumber/ast/filter/element_matching_tag_spec_spec.js create mode 100644 spec/cucumber/ast/filter_spec.js create mode 100644 spec/cucumber/ast/tag_spec.js diff --git a/features/cucumber-tck b/features/cucumber-tck index aa4f859f8..5d3b3d5f5 160000 --- a/features/cucumber-tck +++ b/features/cucumber-tck @@ -1 +1 @@ -Subproject commit aa4f859f80a9f4eeb37525269a04bef17e99d1b2 +Subproject commit 5d3b3d5f54d98cacc31ca687f7eb41fa5bef9fe9 diff --git a/features/step_definitions/cucumber_js_mappings.rb b/features/step_definitions/cucumber_js_mappings.rb index acc69c0c5..b8d5c6b4e 100644 --- a/features/step_definitions/cucumber_js_mappings.rb +++ b/features/step_definitions/cucumber_js_mappings.rb @@ -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 @@ -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 @@ -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) @@ -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 @@ -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) diff --git a/features/step_definitions/cucumber_steps.js b/features/step_definitions/cucumber_steps.js index 1c7c0c3bb..a77a750f5 100644 --- a/features/step_definitions/cucumber_steps.js +++ b/features/step_definitions/cucumber_steps.js @@ -77,8 +77,23 @@ 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) { @@ -86,23 +101,55 @@ var cucumberSteps = function() { }); 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(); @@ -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; diff --git a/features/step_definitions/cucumber_world.js b/features/step_definitions/cucumber_world.js index eea3aad70..a7e885ef7 100644 --- a/features/step_definitions/cucumber_world.js +++ b/features/step_definitions/cucumber_world.js @@ -4,6 +4,7 @@ var World = function(callback) { this.stepDefinitions = ""; this.runOutput = ""; this.cycleEvents = ""; + this.stepCount = 0; this.runSucceeded = false; World.mostRecentInstance = this; callback(this); @@ -11,21 +12,25 @@ var World = function(callback) { 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) { @@ -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) { @@ -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(); @@ -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; diff --git a/lib/cucumber.js b/lib/cucumber.js index 61c57ebad..7abcbdb3f 100644 --- a/lib/cucumber.js +++ b/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; }; diff --git a/lib/cucumber/ast.js b/lib/cucumber/ast.js index a4f724117..e9677e382 100644 --- a/lib/cucumber/ast.js +++ b/lib/cucumber/ast.js @@ -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; diff --git a/lib/cucumber/ast/assembler.js b/lib/cucumber/ast/assembler.js index 558d6abdf..e1dd628ba 100644 --- a/lib/cucumber/ast/assembler.js +++ b/lib/cucumber/ast/assembler.js @@ -1,5 +1,6 @@ -var Assembler = function(features) { +var Assembler = function(features, filter) { var currentFeature, currentScenarioOrBackground, currentStep; + var stashedTags = []; var self = { setCurrentFeature: function setCurrentFeature(feature) { @@ -28,6 +29,21 @@ var Assembler = function(features) { return currentStep; }, + stashTag: function stashTag(tag) { + stashedTags.push(tag); + }, + + revealTags: function revealTags() { + var revealedTags = stashedTags; + stashedTags = []; + return revealedTags; + }, + + applyStashedTagsToElement: function applyStashedTagsToElement(element) { + var revealedTags = self.revealTags(); + element.setTags(revealedTags); + }, + insertBackground: function insertBackground(background) { self.setCurrentScenarioOrBackground(background); var currentFeature = self.getCurrentFeature(); @@ -45,20 +61,28 @@ var Assembler = function(features) { }, insertFeature: function insertFeature(feature) { + self.applyStashedTagsToElement(feature); self.setCurrentFeature(feature); features.addFeature(feature); }, insertScenario: function insertScenario(scenario) { + self.applyStashedTagsToElement(scenario); self.setCurrentScenarioOrBackground(scenario); - var currentFeature = self.getCurrentFeature(); - currentFeature.addScenario(scenario); + if (filter.isScenarioEnrolled(scenario)) { + var currentFeature = self.getCurrentFeature(); + currentFeature.addScenario(scenario); + } }, insertStep: function insertStep(step) { self.setCurrentStep(step); var currentScenarioOrBackground = self.getCurrentScenarioOrBackground(); currentScenarioOrBackground.addStep(step); + }, + + insertTag: function insertTag(tag) { + self.stashTag(tag); } }; return self; diff --git a/lib/cucumber/ast/feature.js b/lib/cucumber/ast/feature.js index 81131912e..2c73a29f4 100644 --- a/lib/cucumber/ast/feature.js +++ b/lib/cucumber/ast/feature.js @@ -1,8 +1,9 @@ var Feature = function(keyword, name, description, line) { var Cucumber = require('../../cucumber'); - var scenarios = Cucumber.Type.Collection(); var background; + var scenarios = Cucumber.Type.Collection(); + var tags = []; var self = { getKeyword: function getKeyword() { @@ -43,6 +44,14 @@ var Feature = function(keyword, name, description, line) { return scenarios.getLast(); }, + setTags: function setTags(newTags) { + tags = newTags; + }, + + getTags: function getTags() { + return tags; + }, + acceptVisitor: function acceptVisitor(visitor, callback) { self.instructVisitorToVisitBackground(visitor, function() { self.instructVisitorToVisitScenarios(visitor, callback); diff --git a/lib/cucumber/ast/filter.js b/lib/cucumber/ast/filter.js new file mode 100644 index 000000000..eb132e5bf --- /dev/null +++ b/lib/cucumber/ast/filter.js @@ -0,0 +1,16 @@ +var _ = require('underscore'); + +var Filter = function(rules) { + var self = { + isScenarioEnrolled: function isScenarioEnrolled(scenario) { + var enrolled = _.all(rules, function(rule) { + return rule.isSatisfiedByElement(scenario); + }); + return enrolled; + } + }; + return self; +}; +Filter.AnyOfTagsRule = require('./filter/any_of_tags_rule'); +Filter.ElementMatchingTagSpec = require('./filter/element_matching_tag_spec'); +module.exports = Filter; \ No newline at end of file diff --git a/lib/cucumber/ast/filter/any_of_tags_rule.js b/lib/cucumber/ast/filter/any_of_tags_rule.js new file mode 100644 index 000000000..269968170 --- /dev/null +++ b/lib/cucumber/ast/filter/any_of_tags_rule.js @@ -0,0 +1,17 @@ +var _ = require('underscore'); + +var AnyOfTagsRule = function(tags) { + var Cucumber = require('../../../cucumber'); + + var self = { + isSatisfiedByElement: function isSatisfiedByElement(element) { + var satisfied = _.any(tags, function(tag) { + var spec = Cucumber.Ast.Filter.ElementMatchingTagSpec(tag); + return spec.isMatching(element); + }); + return satisfied; + } + }; + return self; +}; +module.exports = AnyOfTagsRule; diff --git a/lib/cucumber/ast/filter/element_matching_tag_spec.js b/lib/cucumber/ast/filter/element_matching_tag_spec.js new file mode 100644 index 000000000..48d9ef762 --- /dev/null +++ b/lib/cucumber/ast/filter/element_matching_tag_spec.js @@ -0,0 +1,35 @@ +var _ = require('underscore'); + +var ElementMatchingTagSpec = function(tagName) { + var self = { + isMatching: function isMatching(element) { + var elementTags = element.getTags(); + var matching; + if (self.isExpectingTag()) + matching = _.any(elementTags, self.isTagSatisfying); + else + matching = _.all(elementTags, self.isTagSatisfying); + return matching; + }, + + isTagSatisfying: function isTagSatisfying(tag) { + var checkedTagName = tag.getName(); + var satisfying; + if (self.isExpectingTag()) + satisfying = checkedTagName == tagName; + else { + var negatedCheckedTagName = ElementMatchingTagSpec.NEGATION_CHARACTER + checkedTagName; + satisfying = negatedCheckedTagName != tagName; + } + return satisfying; + }, + + isExpectingTag: function isExpectingTag() { + var expectingTag = tagName[0] != ElementMatchingTagSpec.NEGATION_CHARACTER; + return expectingTag; + } + }; + return self; +}; +ElementMatchingTagSpec.NEGATION_CHARACTER = '~'; +module.exports = ElementMatchingTagSpec; diff --git a/lib/cucumber/ast/scenario.js b/lib/cucumber/ast/scenario.js index a5a5166e9..ed488459c 100644 --- a/lib/cucumber/ast/scenario.js +++ b/lib/cucumber/ast/scenario.js @@ -1,8 +1,9 @@ var Scenario = function(keyword, name, description, line) { var Cucumber = require('../../cucumber'); - var steps = Cucumber.Type.Collection(); var background; + var steps = Cucumber.Type.Collection(); + var tags = []; var self = { setBackground: function setBackground(newBackground) { @@ -39,6 +40,14 @@ var Scenario = function(keyword, name, description, line) { return steps.getLast(); }, + setTags: function setTags(newTags) { + tags = newTags; + }, + + getTags: function getTags() { + return tags; + }, + acceptVisitor: function acceptVisitor(visitor, callback) { self.instructVisitorToVisitBackgroundSteps(visitor, function() { self.instructVisitorToVisitScenarioSteps(visitor, callback); diff --git a/lib/cucumber/ast/tag.js b/lib/cucumber/ast/tag.js new file mode 100644 index 000000000..9ac1bc1a9 --- /dev/null +++ b/lib/cucumber/ast/tag.js @@ -0,0 +1,11 @@ +var Tag = function(name, line) { + var Cucumber = require('../../cucumber'); + + var self = { + getName: function getName() { + return name; + } + }; + return self; +}; +module.exports = Tag; \ No newline at end of file diff --git a/lib/cucumber/cli/argument_parser.js b/lib/cucumber/cli/argument_parser.js index e1a8d04ce..714d781c7 100644 --- a/lib/cucumber/cli/argument_parser.js +++ b/lib/cucumber/cli/argument_parser.js @@ -1,3 +1,5 @@ +var _ = require('underscore'); + var ArgumentParser = function(argv) { var nopt = require('nopt'); var path = require('path'); @@ -49,9 +51,19 @@ var ArgumentParser = function(argv) { return unexpandedSupportCodeFilePaths; }, + getTagGroups: function getTagGroups() { + var tagOptionValues = self.getOptionOrDefault(ArgumentParser.TAGS_OPTION_NAME, []); + var tagGroups = _.map(tagOptionValues, function(tagOptionValue) { + var tagGroup = tagOptionValue.split(ArgumentParser.TAG_OPTION_SEPARATOR); + return tagGroup; + }); + return tagGroups; + }, + getKnownOptionDefinitions: function getKnownOptionDefinitions() { var definitions = {}; definitions[ArgumentParser.REQUIRE_OPTION_NAME] = [path, Array]; + definitions[ArgumentParser.TAGS_OPTION_NAME] = [String, Array]; definitions[ArgumentParser.HELP_FLAG_NAME] = Boolean; definitions[ArgumentParser.VERSION_FLAG_NAME] = Boolean; return definitions; @@ -96,6 +108,9 @@ ArgumentParser.FEATURE_FILENAME_REGEXP = /\/[^\/]+\.feature$/i; ArgumentParser.LONG_OPTION_PREFIX = "--"; ArgumentParser.REQUIRE_OPTION_NAME = "require"; ArgumentParser.REQUIRE_OPTION_SHORT_NAME = "r"; +ArgumentParser.TAGS_OPTION_NAME = "tags"; +ArgumentParser.TAGS_OPTION_SHORT_NAME = "t"; +ArgumentParser.TAG_OPTION_SEPARATOR = ","; ArgumentParser.HELP_FLAG_NAME = "help"; ArgumentParser.HELP_FLAG_SHORT_NAME = "h"; ArgumentParser.DEFAULT_HELP_FLAG_VALUE = false; diff --git a/lib/cucumber/cli/configuration.js b/lib/cucumber/cli/configuration.js index 66e5aaf83..177ded2d6 100644 --- a/lib/cucumber/cli/configuration.js +++ b/lib/cucumber/cli/configuration.js @@ -12,6 +12,12 @@ var Configuration = function(argv) { return featureSources; }, + getAstFilter: function getAstFilter() { + var tagRules = self.getTagAstFilterRules(); + var astFilter = Cucumber.Ast.Filter(tagRules); + return astFilter; + }, + getSupportCodeLibrary: function getSupportCodeLibrary() { var supportCodeFilePaths = argumentParser.getSupportCodeFilePaths(); var supportCodeLoader = Cucumber.Cli.SupportCodeLoader(supportCodeFilePaths); @@ -19,6 +25,16 @@ var Configuration = function(argv) { return supportCodeLibrary; }, + getTagAstFilterRules: function getTagAstFilterRules() { + var tagGroups = argumentParser.getTagGroups(); + var rules = []; + tagGroups.forEach(function(tags) { + var rule = Cucumber.Ast.Filter.AnyOfTagsRule(tags); + rules.push(rule); + }); + return rules; + }, + isHelpRequested: function isHelpRequested() { var isHelpRequested = argumentParser.isHelpRequested(); return isHelpRequested; diff --git a/lib/cucumber/parser.js b/lib/cucumber/parser.js index c229807cc..52ecd9b36 100644 --- a/lib/cucumber/parser.js +++ b/lib/cucumber/parser.js @@ -1,9 +1,9 @@ -var Parser = function(featureSources) { +var Parser = function(featureSources, astFilter) { var Gherkin = require('gherkin'); var Cucumber = require('../cucumber'); var features = Cucumber.Ast.Features(); - var astAssembler = Cucumber.Ast.Assembler(features); + var astAssembler = Cucumber.Ast.Assembler(features, astFilter); var self = { parse: function parse() { @@ -25,10 +25,16 @@ var Parser = function(featureSources) { feature: self.handleFeature, row: self.handleDataTableRow, scenario: self.handleScenario, - step: self.handleStep + step: self.handleStep, + tag: self.handleTag }; }, + handleTag: function handleTag(tag, line) { + var tag = Cucumber.Ast.Tag(tag, line); + astAssembler.insertTag(tag); + }, + handleBackground: function handleBackground(keyword, name, description, line) { var background = Cucumber.Ast.Background(keyword, name, description, line); astAssembler.insertBackground(background); diff --git a/lib/cucumber/runtime.js b/lib/cucumber/runtime.js index ffdfad822..00b10448a 100644 --- a/lib/cucumber/runtime.js +++ b/lib/cucumber/runtime.js @@ -19,7 +19,8 @@ var Runtime = function(configuration) { getFeatures: function getFeatures() { var featureSources = configuration.getFeatureSources(); - var parser = Cucumber.Parser(featureSources); + var astFilter = configuration.getAstFilter(); + var parser = Cucumber.Parser(featureSources, astFilter); var features = parser.parse(); return features; }, diff --git a/lib/cucumber/volatile_configuration.js b/lib/cucumber/volatile_configuration.js index c0541e77b..97c9b5b60 100644 --- a/lib/cucumber/volatile_configuration.js +++ b/lib/cucumber/volatile_configuration.js @@ -1,16 +1,34 @@ -var VolatileConfiguration = function VolatileConfiguration(featureSource, supportCodeInitializer) { +var VolatileConfiguration = function VolatileConfiguration(featureSource, supportCodeInitializer, options) { var Cucumber = require('../cucumber'); var supportCodeLibrary = Cucumber.SupportCode.Library(supportCodeInitializer); + options = options || {}; + var tagGroups = options['tags'] || []; + var self = { getFeatureSources: function getFeatureSources() { var featureNameSourcePair = [VolatileConfiguration.FEATURE_SOURCE_NAME, featureSource]; return [featureNameSourcePair]; }, + getAstFilter: function getAstFilter() { + var tagRules = self.getTagAstFilterRules(); + var astFilter = Cucumber.Ast.Filter(tagRules); + return astFilter; + }, + getSupportCodeLibrary: function getSupportCodeLibrary() { return supportCodeLibrary; + }, + + getTagAstFilterRules: function getTagAstFilterRules() { + var rules = []; + tagGroups.forEach(function(tags) { + var rule = Cucumber.Ast.Filter.AnyOfTagsRule(tags); + rules.push(rule); + }); + return rules; } }; return self; diff --git a/spec/cucumber/ast/assembler_spec.js b/spec/cucumber/ast/assembler_spec.js index 821cb5094..d0edfdad2 100644 --- a/spec/cucumber/ast/assembler_spec.js +++ b/spec/cucumber/ast/assembler_spec.js @@ -2,11 +2,12 @@ require('../../support/spec_helper'); describe("Cucumber.Ast.Assembler", function() { var Cucumber = requireLib('cucumber'); - var assembler, features; + var assembler, features, filter; beforeEach(function() { features = createSpy("features"); - assembler = Cucumber.Ast.Assembler(features); + filter = createSpy("filter"); + assembler = Cucumber.Ast.Assembler(features, filter); }); describe("setCurrentFeature()", function() { @@ -76,6 +77,48 @@ describe("Cucumber.Ast.Assembler", function() { }); }); + describe("revealTags() [stashTag()]", function() { + var firstTag, secondTag; + + beforeEach(function() { + firstTag = createSpy("first tag"); + secondTag = createSpy("second tag"); + assembler.stashTag(firstTag); + assembler.stashTag(secondTag); + }); + + it("returns the stashed tags", function() { + expect(assembler.revealTags()).toEqual([firstTag, secondTag]); + }); + + it("removes the tags from the stash", function() { + var thirdTag = createSpy("third tag"); + assembler.revealTags(); + assembler.stashTag(thirdTag); + expect(assembler.revealTags()).toEqual([thirdTag]); + }); + }); + + describe("applyStashedTagsToElement()", function() { + var element, revealedTags; + + beforeEach(function() { + element = createSpyWithStubs("any AST element accepting tags", {setTags: null}); + revealedTags = createSpy("revealed tags"); + spyOn(assembler, 'revealTags').andReturn(revealedTags); + }); + + it("reveals the tags", function() { + assembler.applyStashedTagsToElement(element); + expect(assembler.revealTags).toHaveBeenCalled(); + }); + + it("sets the tags to the element", function() { + assembler.applyStashedTagsToElement(element); + expect(element.setTags).toHaveBeenCalledWith(revealedTags); + }); + }); + describe("insertBackground()", function() { var background, currentFeature; @@ -108,9 +151,15 @@ describe("Cucumber.Ast.Assembler", function() { beforeEach(function() { feature = createSpy("feature"); spyOnStub(features, 'addFeature'); + spyOn(assembler, 'applyStashedTagsToElement'); spyOn(assembler, 'setCurrentFeature'); }); + it("applies the stashed tags to the feature", function() { + assembler.insertFeature(feature); + expect(assembler.applyStashedTagsToElement).toHaveBeenCalledWith(feature); + }); + it("sets the feature as the current feature", function() { assembler.insertFeature(feature); expect(assembler.setCurrentFeature).toHaveBeenCalledWith(feature); @@ -166,25 +215,61 @@ describe("Cucumber.Ast.Assembler", function() { var scenario, currentFeature; beforeEach(function() { - scenario = createSpy("scenario"); + scenario = createSpy("scenario"); currentFeature = createSpyWithStubs("current feature", {addScenario: null}); + spyOn(assembler, 'applyStashedTagsToElement'); + spyOnStub(filter, 'isScenarioEnrolled'); spyOn(assembler, 'getCurrentFeature').andReturn(currentFeature); spyOn(assembler, 'setCurrentScenarioOrBackground'); }); + it("applies the stashed tags to the scenario", function() { + assembler.insertScenario(scenario); + expect(assembler.applyStashedTagsToElement).toHaveBeenCalledWith(scenario); + }); + it("sets the scenario as the current scenario", function() { assembler.insertScenario(scenario); expect(assembler.setCurrentScenarioOrBackground).toHaveBeenCalledWith(scenario); }); - it("gets the current feature", function() { + it("asks the filter if the scenario is enrolled", function() { assembler.insertScenario(scenario); - expect(assembler.getCurrentFeature).toHaveBeenCalled(); + expect(filter.isScenarioEnrolled).toHaveBeenCalledWith(scenario); }); - it("adds the scenario to the current feature", function() { - assembler.insertScenario(scenario); - expect(currentFeature.addScenario).toHaveBeenCalledWith(scenario); + describe("when the scenario is enrolled", function() { + + beforeEach(function() { + filter.isScenarioEnrolled.andReturn(true); + }); + + it("gets the current feature", function() { + assembler.insertScenario(scenario); + expect(assembler.getCurrentFeature).toHaveBeenCalled(); + }); + + it("adds the scenario to the current feature", function() { + assembler.insertScenario(scenario); + expect(currentFeature.addScenario).toHaveBeenCalledWith(scenario); + }); + }); + + describe("when the scenario is not enrolled", function() { + + beforeEach(function() { + filter.isScenarioEnrolled.andReturn(false); + }); + + it("does not get the current feature", function() { + assembler.insertScenario(scenario); + expect(assembler.getCurrentFeature).not.toHaveBeenCalled(); + }); + + it("does not add the scenario to the current feature", function() { + assembler.insertScenario(scenario); + expect(currentFeature.addScenario).not.toHaveBeenCalledWith(scenario); + }); }); }); @@ -213,4 +298,18 @@ describe("Cucumber.Ast.Assembler", function() { expect(currentScenarioOrBackground.addStep).toHaveBeenCalledWith(step); }); }); + + describe("insertTag()", function() { + var tag; + + beforeEach(function() { + tag = createSpy("tag"); + spyOn(assembler, 'stashTag'); + }); + + it("stashes the tag", function() { + assembler.insertTag(tag); + expect(assembler.stashTag).toHaveBeenCalledWith(tag); + }); + }); }); diff --git a/spec/cucumber/ast/feature_spec.js b/spec/cucumber/ast/feature_spec.js index 2df425adc..87aa91ea7 100644 --- a/spec/cucumber/ast/feature_spec.js +++ b/spec/cucumber/ast/feature_spec.js @@ -118,6 +118,14 @@ describe("Cucumber.Ast.Feature", function() { }); }); + describe("getTags() [setTags()]", function() { + it("returns the tags", function() { + var tags = createSpy("tags"); + feature.setTags(tags); + expect(feature.getTags()).toBe(tags); + }); + }); + describe("acceptVisitor", function() { var visitor, callback; diff --git a/spec/cucumber/ast/filter/any_of_tags_rule_spec.js b/spec/cucumber/ast/filter/any_of_tags_rule_spec.js new file mode 100644 index 000000000..c9bd45b3c --- /dev/null +++ b/spec/cucumber/ast/filter/any_of_tags_rule_spec.js @@ -0,0 +1,62 @@ +require('../../../support/spec_helper'); + +describe("Cucumber.Ast.Filter.AnyOfTagsRule", function() { + var Cucumber = requireLib('cucumber'); + + var rule, tags; + + beforeEach(function() { + tags = createSpy("tags"); + rule = Cucumber.Ast.Filter.AnyOfTagsRule(tags); + }); + + describe("isSatisfiedByElement()", function() { + var _ = require('underscore'); + + var element, satisfyingElement; + + beforeEach(function() { + element = createSpy("element"); + satisfyingElement = createSpy("wether the element is satisfying"); + spyOn(_, 'any').andReturn(satisfyingElement); + }); + + it("looks for a tag matching some condition", function() { + rule.isSatisfiedByElement(element); + expect(_.any).toHaveBeenCalled(); + expect(_.any).toHaveBeenCalledWithValueAsNthParameter(tags, 1); + expect(_.any).toHaveBeenCalledWithAFunctionAsNthParameter(2); + }); + + describe("every tag condition", function() { + var spec, everyTagConditionFunc, tag, matchingSpec; + + beforeEach(function() { + matchingSpec = createSpy("wether the spec is satisfied or not"); + tag = createSpy("tag"); + spec = createSpyWithStubs("element matching tag spec", {isMatching: matchingSpec}); + rule.isSatisfiedByElement(element); + everyTagConditionFunc = _.any.mostRecentCall.args[1]; + spyOn(Cucumber.Ast.Filter, 'ElementMatchingTagSpec').andReturn(spec); + }); + + it("instantiates an element matching tag spec", function() { + everyTagConditionFunc(tag); + expect(Cucumber.Ast.Filter.ElementMatchingTagSpec).toHaveBeenCalledWith(tag); + }); + + it("checks wether the element is matching the spec", function() { + everyTagConditionFunc(tag); + expect(spec.isMatching).toHaveBeenCalledWith(element); + }); + + it("returns match result", function() { + expect(everyTagConditionFunc(tag)).toBe(matchingSpec); + }); + }); + + it("returns wether it found a matching tag or not", function() { + expect(rule.isSatisfiedByElement(element)).toBe(satisfyingElement); + }); + }); +}); diff --git a/spec/cucumber/ast/filter/element_matching_tag_spec_spec.js b/spec/cucumber/ast/filter/element_matching_tag_spec_spec.js new file mode 100644 index 000000000..da250970d --- /dev/null +++ b/spec/cucumber/ast/filter/element_matching_tag_spec_spec.js @@ -0,0 +1,154 @@ +require('../../../support/spec_helper'); + +describe("Cucumber.Ast.Filter.ElementMatchingTagSpec", function() { + var Cucumber = requireLib('cucumber'); + + var spec, tagName; + + beforeEach(function() { + tagName = "tag"; + spec = Cucumber.Ast.Filter.ElementMatchingTagSpec(tagName); + }); + + describe("isMatching()", function() { + var _ = require('underscore'); + + var element, elementTags, matchingElement; + + beforeEach(function() { + elementTags = createSpy("element tags"); + element = createSpyWithStubs("element", {getTags: elementTags}); + matchingElement = createSpy("wether the element is matching or not"); + spyOn(spec, 'isExpectingTag'); + }); + + it("gets the element tags", function() { + spec.isMatching(element); + expect(element.getTags).toHaveBeenCalled(); + }); + + it("checks wether the spec tag is expected or not", function() { + spec.isMatching(element); + expect(spec.isExpectingTag).toHaveBeenCalled(); + }); + + describe("when the spec tag is expected on the element", function() { + beforeEach(function() { + spec.isExpectingTag.andReturn(true); + spyOn(_, 'any').andReturn(matchingElement); + }); + + it("checks wether any of the element tags match the spec tag", function() { + spec.isMatching(element); + expect(_.any).toHaveBeenCalledWith(elementTags, spec.isTagSatisfying); + }); + + it("returns wether the element matched or not", function() { + expect(spec.isMatching(element)).toBe(matchingElement); + }); + }); + + describe("when the spec tag is not expected on the element", function() { + beforeEach(function() { + spec.isExpectingTag.andReturn(false); + spyOn(_, 'all').andReturn(matchingElement); + }); + + it("checks wether any of the element tags match the spec tag", function() { + spec.isMatching(element); + expect(_.all).toHaveBeenCalledWith(elementTags, spec.isTagSatisfying); + }); + + it("returns wether the element matched or not", function() { + expect(spec.isMatching(element)).toBe(matchingElement); + }); + }); + }); + + describe("isTagSatisfying()", function() { + var checkedTag; + + beforeEach(function() { + checkedTag = createSpyWithStubs("element tag", {getName: null}); + spyOn(spec, 'isExpectingTag'); + }); + + it("gets the name of the tag", function() { + spec.isTagSatisfying(checkedTag); + expect(checkedTag.getName).toHaveBeenCalled(); + }); + + it("checks wether the spec tag is expected or not on the element", function() { + spec.isTagSatisfying(checkedTag); + expect(spec.isExpectingTag).toHaveBeenCalled(); + }); + + describe("when the spec expects the tag to be present on the element", function() { + beforeEach(function() { + spec.isExpectingTag.andReturn(true); + }); + + describe("when the tag names are identical", function() { + beforeEach(function() { + checkedTag.getName.andReturn(tagName); + }); + + it("is truthy", function() { + expect(spec.isTagSatisfying(checkedTag)).toBeTruthy(); + }); + }); + + describe("when the tag names are different", function() { + beforeEach(function() { + checkedTag.getName.andReturn("@obscure_tag"); + }); + + it("is falsy", function() { + expect(spec.isTagSatisfying(checkedTag)).toBeFalsy(); + }); + }); + }); + + describe("when the spec expects the tag to be absent on the element", function() { + beforeEach(function() { + tagName = "tag"; + spec = Cucumber.Ast.Filter.ElementMatchingTagSpec("~" + tagName); + spyOn(spec, 'isExpectingTag').andReturn(false); + }); + + describe("when the tag names are identical", function() { + beforeEach(function() { + checkedTag.getName.andReturn(tagName); + }); + + it("is truthy", function() { + expect(spec.isTagSatisfying(checkedTag)).toBeFalsy(); + }); + }); + + describe("when the tag names are different", function() { + beforeEach(function() { + checkedTag.getName.andReturn("@obscure_tag"); + }); + + it("is falsy", function() { + expect(spec.isTagSatisfying(checkedTag)).toBeTruthy(); + }); + }); + }); + }); + + describe("isExpectingTag()", function() { + it("is truthy when the tag does not start with a tilde (~)", function() { + tagName = "tag"; + spec = Cucumber.Ast.Filter.ElementMatchingTagSpec(tagName); + expect(spec.isExpectingTag()).toBeTruthy(); + }); + + it("is falsy when the tag starts with a tilde (~)", function() { + tagName = "~tag"; + spec = Cucumber.Ast.Filter.ElementMatchingTagSpec(tagName); + expect(spec.isExpectingTag()).toBeFalsy(); + }); + }); +}); diff --git a/spec/cucumber/ast/filter_spec.js b/spec/cucumber/ast/filter_spec.js new file mode 100644 index 000000000..9c1c121ad --- /dev/null +++ b/spec/cucumber/ast/filter_spec.js @@ -0,0 +1,55 @@ +require('../../support/spec_helper'); + +describe("Cucumber.Ast.Filter", function() { + var Cucumber = requireLib('cucumber'); + + var filter, rules; + + beforeEach(function() { + rules = createSpy("rules"); + filter = Cucumber.Ast.Filter(rules); + }); + + describe("isScenarioEnrolled()", function() { + var _ = require('underscore'); + + var scenario, scenarioEnrolled; + + beforeEach(function() { + scenario = createSpy("scenario"); + scenarioEnrolled = createSpy("wether the scenario is enrolled or not"); + spyOn(_, 'all').andReturn(scenarioEnrolled); + }); + + it("checks all the rules for a condition", function() { + filter.isScenarioEnrolled(scenario); + expect(_.all).toHaveBeenCalled(); + expect(_.all).toHaveBeenCalledWithValueAsNthParameter(rules, 1); + expect(_.all).toHaveBeenCalledWithAFunctionAsNthParameter(2); + }); + + describe("every rule condition", function() { + var ruleConditionFunc, rule, ruleSatisfied; + + beforeEach(function() { + ruleSatisfied = createSpy("wether the rule was satisfied or not"); + rule = createSpyWithStubs("rule", {isSatisfiedByElement: ruleSatisfied}); + filter.isScenarioEnrolled(scenario); + ruleConditionFunc = _.all.mostRecentCall.args[1]; + }); + + it("checks wether the rule is satisfied by the scenario", function() { + ruleConditionFunc(rule); + expect(rule.isSatisfiedByElement).toHaveBeenCalledWith(scenario); + }); + + it("returns wether the rule wa satisfied or not", function() { + expect(ruleConditionFunc(rule)).toBe(ruleSatisfied); + }); + }); + + it("returns wether the scenario was enrolled or not", function() { + expect(filter.isScenarioEnrolled(scenario)).toBe(scenarioEnrolled); + }) + }); +}); \ No newline at end of file diff --git a/spec/cucumber/ast/scenario_spec.js b/spec/cucumber/ast/scenario_spec.js index 3aa16603f..b85729f49 100644 --- a/spec/cucumber/ast/scenario_spec.js +++ b/spec/cucumber/ast/scenario_spec.js @@ -92,6 +92,14 @@ describe("Cucumber.Ast.Scenario", function() { }); }); + describe("getTags() [setTags()]", function() { + it("returns the tags", function() { + var tags = createSpy("tags"); + scenario.setTags(tags); + expect(scenario.getTags()).toBe(tags); + }); + }); + describe("acceptVisitor", function() { var visitor, callback; diff --git a/spec/cucumber/ast/tag_spec.js b/spec/cucumber/ast/tag_spec.js new file mode 100644 index 000000000..eacb00317 --- /dev/null +++ b/spec/cucumber/ast/tag_spec.js @@ -0,0 +1,19 @@ +require('../../support/spec_helper'); + +describe("Cucumber.Ast.Tag", function() { + var Cucumber = requireLib('cucumber'); + + var tag, name, line; + + beforeEach(function() { + name = createSpy("tag name"); + line = createSpy("tag line"); + tag = Cucumber.Ast.Tag(name, line); + }); + + describe("getName()", function() { + it("returns the name of the tag", function() { + expect(tag.getName()).toBe(name); + }); + }); +}); \ No newline at end of file diff --git a/spec/cucumber/cli/argument_parser_spec.js b/spec/cucumber/cli/argument_parser_spec.js index 576ef4792..134dccc97 100644 --- a/spec/cucumber/cli/argument_parser_spec.js +++ b/spec/cucumber/cli/argument_parser_spec.js @@ -59,6 +59,11 @@ describe("Cucumber.Cli.ArgumentParser", function() { expect(knownOptionDefinitions[Cucumber.Cli.ArgumentParser.REQUIRE_OPTION_NAME]).toEqual([path, Array]); }); + it("defines a --tags option to include and exclude tags", function() { + var knownOptionDefinitions = argumentParser.getKnownOptionDefinitions(); + expect(knownOptionDefinitions[Cucumber.Cli.ArgumentParser.TAGS_OPTION_NAME]).toEqual([String, Array]); + }); + it("defines a --help flag", function() { var knownOptionDefinitions = argumentParser.getKnownOptionDefinitions(); expect(knownOptionDefinitions[Cucumber.Cli.ArgumentParser.HELP_FLAG_NAME]).toEqual(Boolean); @@ -239,6 +244,55 @@ describe("Cucumber.Cli.ArgumentParser", function() { }); }); + describe("getTagGroups()", function() { + var _ = require('underscore'); + + var tagOptionValues, tagGroups; + + beforeEach(function() { + tagOptionValues = createSpy("tag option values"); + tagGroups = createSpy("tag groups"); + spyOn(argumentParser, 'getOptionOrDefault').andReturn(tagOptionValues); + spyOn(_, 'map').andReturn(tagGroups); + }); + + it("gets the tag option values", function() { + argumentParser.getTagGroups(); + expect(argumentParser.getOptionOrDefault).toHaveBeenCalledWith(Cucumber.Cli.ArgumentParser.TAGS_OPTION_NAME, []); + }); + + it("maps the tag groups", function() { + argumentParser.getTagGroups(); + expect(_.map).toHaveBeenCalled(); + expect(_.map).toHaveBeenCalledWithValueAsNthParameter(tagOptionValues, 1); + expect(_.map).toHaveBeenCalledWithAFunctionAsNthParameter(2); + }); + + describe("tag group mapper function", function() { + var tagGroupMapperFunc, tagOptionValue, tagGroup; + + beforeEach(function() { + tagGroup = createSpy("tag group"); + tagOptionValue = createSpyWithStubs("tag option value", {split: tagGroup}); + argumentParser.getTagGroups(); + tagGroupMapperFunc = _.map.mostRecentCall.args[1]; + }); + + it("splits the tag option value based on commas", function() { + tagGroupMapperFunc(tagOptionValue); + expect(tagOptionValue.split).toHaveBeenCalledWith(','); + }); + + it("returns the splitted tag group", function() { + expect(tagGroupMapperFunc(tagOptionValue)).toBe(tagGroup); + }); + }); + + it("returns the tag option values", function() { + expect(argumentParser.getTagGroups()).toBe(tagGroups); + }); + }); + describe("isHelpRequested()", function() { var isHelpRequested; diff --git a/spec/cucumber/cli/configuration_spec.js b/spec/cucumber/cli/configuration_spec.js index 6da7c4f37..21d0e2a1a 100644 --- a/spec/cucumber/cli/configuration_spec.js +++ b/spec/cucumber/cli/configuration_spec.js @@ -60,6 +60,31 @@ describe("Cucumber.Cli.Configuration", function() { }); }); + describe("getAstFilter()", function() { + var astFilter, tagFilterRules; + + beforeEach(function() { + astFilter = createSpyWithStubs("AST filter"); + tagFilterRules = createSpy("tag specs"); + spyOn(Cucumber.Ast, 'Filter').andReturn(astFilter); + spyOn(configuration, 'getTagAstFilterRules').andReturn(tagFilterRules); + }); + + it("gets the tag filter rules", function() { + configuration.getAstFilter(); + expect(configuration.getTagAstFilterRules).toHaveBeenCalled(); + }); + + it("instantiates an AST filter", function() { + configuration.getAstFilter(); + expect(Cucumber.Ast.Filter).toHaveBeenCalledWith(tagFilterRules); + }); + + it("returns the AST filter", function() { + expect(configuration.getAstFilter()).toBe(astFilter); + }); + }); + describe("getSupportCodeLibrary()", function() { var supportCodeFilePaths, supportCodeLoader, supportCodeLibrary; @@ -92,6 +117,33 @@ describe("Cucumber.Cli.Configuration", function() { }); }); + describe("getTagAstFilterRules()", function() { + var tagGroups, rules; + + beforeEach(function() { + tagGroups = [createSpy("tag group 1"), createSpy("tag group 2"), createSpy("tag group 3")]; + rules = [createSpy("any of tags rule 1"), createSpy("any of tags rule 2"), createSpy("any of tags rule 3")]; + spyOnStub(argumentParser, 'getTagGroups').andReturn(tagGroups); + spyOn(Cucumber.Ast.Filter, 'AnyOfTagsRule').andReturnSeveral(rules); + }); + + it("gets the tag groups from the argument parser", function() { + configuration.getTagAstFilterRules(); + expect(argumentParser.getTagGroups).toHaveBeenCalled(); + }); + + it("creates an 'any of tags' filter rule per each group", function() { + configuration.getTagAstFilterRules(); + tagGroups.forEach(function(tagGroup) { + expect(Cucumber.Ast.Filter.AnyOfTagsRule).toHaveBeenCalledWith(tagGroup); + }); + }); + + it("returns all the rules", function() { + expect(configuration.getTagAstFilterRules()).toEqual(rules); + }); + }); + describe("isHelpRequired()", function() { beforeEach(function() { spyOnStub(argumentParser, 'isHelpRequested'); diff --git a/spec/cucumber/parser_spec.js b/spec/cucumber/parser_spec.js index d1a53b5c5..3c51fede2 100644 --- a/spec/cucumber/parser_spec.js +++ b/spec/cucumber/parser_spec.js @@ -3,10 +3,11 @@ require('../support/spec_helper'); describe("Cucumber.Parser", function() { var Cucumber = requireLib('cucumber'); var parser, featureSources; - var features, astAssembler; + var features, astFilter, astAssembler; beforeEach(function() { features = createSpy("Root 'features' AST element"); + astFilter = createSpy("AST filter"); featureSources = [ ["(feature:1)", createSpy('first feature source')], ["(feature:2)", createSpy('second feature source')] @@ -14,7 +15,7 @@ describe("Cucumber.Parser", function() { astAssembler = createSpy("AST assembler"); spyOn(Cucumber.Ast, 'Features').andReturn(features); spyOn(Cucumber.Ast, 'Assembler').andReturn(astAssembler); - parser = Cucumber.Parser(featureSources); + parser = Cucumber.Parser(featureSources, astFilter); }); describe("constructor", function() { @@ -23,7 +24,7 @@ describe("Cucumber.Parser", function() { }); it("instantiates an AST assembler", function() { - expect(Cucumber.Ast.Assembler).toHaveBeenCalledWith(features); + expect(Cucumber.Ast.Assembler).toHaveBeenCalledWith(features, astFilter); }); }); @@ -116,6 +117,12 @@ describe("Cucumber.Parser", function() { eventHandlers = parser.getEventHandlers(); expect(eventHandlers['row']).toBe(parser.handleDataTableRow); }); + + it("provides a 'tag' handler", function() { + spyOn(parser, 'handleTag'); + eventHandlers = parser.getEventHandlers(); + expect(eventHandlers['tag']).toBe(parser.handleTag); + }); }); describe("handleBackground()", function() { @@ -275,4 +282,26 @@ describe("Cucumber.Parser", function() { expect(astAssembler.insertStep).toHaveBeenCalledWith(step); }); }); + + describe("handleTag()", function() { + var name, line; + + beforeEach(function() { + name = createSpy("tag name"); + line = createSpy("line number"); + tag = createSpy("tag AST element"); + spyOn(Cucumber.Ast, 'Tag').andReturn(tag); + spyOnStub(astAssembler, 'insertTag'); + }); + + it("creates a new tag AST element", function() { + parser.handleTag(name, line); + expect(Cucumber.Ast.Tag).toHaveBeenCalledWith(name, line); + }); + + it("tells the AST assembler to insert the tag into the tree", function() { + parser.handleTag(name, line); + expect(astAssembler.insertTag).toHaveBeenCalledWith(tag); + }); + }); }); diff --git a/spec/cucumber/runtime_spec.js b/spec/cucumber/runtime_spec.js index f8fde1708..5d81feac8 100644 --- a/spec/cucumber/runtime_spec.js +++ b/spec/cucumber/runtime_spec.js @@ -10,7 +10,7 @@ describe("Cucumber.Runtime", function() { listeners = createSpyWithStubs("listener collection", {add: null}); configuration = createSpy("configuration"); spyOn(Cucumber.Type, 'Collection').andReturn(listeners); - runtime = Cucumber.Runtime(configuration); + runtime = Cucumber.Runtime(configuration); }); describe("constructor", function() { @@ -74,13 +74,15 @@ describe("Cucumber.Runtime", function() { }); describe("getFeatures()", function() { - var featureSources, parser, features; + var featureSources, astFilter, parser, features; beforeEach(function() { featureSources = createSpy("feature sources"); + astFilter = createSpy("AST filter"); features = createSpy("features (AST)"); parser = createSpyWithStubs("parser", {parse: features}); spyOnStub(configuration, 'getFeatureSources').andReturn(featureSources); + spyOnStub(configuration, 'getAstFilter').andReturn(astFilter); spyOn(Cucumber, 'Parser').andReturn(parser); }); @@ -89,9 +91,14 @@ describe("Cucumber.Runtime", function() { expect(configuration.getFeatureSources).toHaveBeenCalled(); }); + it("gets the AST filter from the configuration", function() { + runtime.getFeatures(); + expect(configuration.getAstFilter).toHaveBeenCalled(); + }); + it("creates a new Cucumber parser for the feature sources", function() { runtime.getFeatures(); - expect(Cucumber.Parser).toHaveBeenCalledWith(featureSources); + expect(Cucumber.Parser).toHaveBeenCalledWith(featureSources, astFilter); }); it("tells the parser to parse the features", function() { diff --git a/spec/cucumber/volatile_configuration_spec.js b/spec/cucumber/volatile_configuration_spec.js index 61b3f5f55..1a88e8c7d 100644 --- a/spec/cucumber/volatile_configuration_spec.js +++ b/spec/cucumber/volatile_configuration_spec.js @@ -33,9 +33,69 @@ describe("Cucumber.VolatileConfiguration", function() { }) }); + describe("getAstFilter()", function() { + var astFilter, tagFilterRules; + + beforeEach(function() { + astFilter = createSpyWithStubs("AST filter"); + tagFilterRules = createSpy("tag specs"); + spyOn(Cucumber.Ast, 'Filter').andReturn(astFilter); + spyOn(configuration, 'getTagAstFilterRules').andReturn(tagFilterRules); + }); + + it("gets the tag filter rules", function() { + configuration.getAstFilter(); + expect(configuration.getTagAstFilterRules).toHaveBeenCalled(); + }); + + it("instantiates an AST filter", function() { + configuration.getAstFilter(); + expect(Cucumber.Ast.Filter).toHaveBeenCalledWith(tagFilterRules); + }); + + it("returns the AST filter", function() { + expect(configuration.getAstFilter()).toBe(astFilter); + }); + }); + describe("getSupportCodeLibrary()", function() { it("returns the support code library", function() { expect(configuration.getSupportCodeLibrary()).toBe(supportCodeLibrary); }); }); + + describe("getTagAstFilterRules()", function() { + describe("when there are no tags specified", function() { + beforeEach(function() { + configuration = Cucumber.VolatileConfiguration(featureSource, supportCodeInitializer); + }); + + it("returns an empty set of rules", function() { + expect(configuration.getTagAstFilterRules()).toEqual([]); + }); + }); + + describe("when there are some tags", function() { + var options, tagGroups, rules; + + beforeEach(function() { + tagGroups = [createSpy("tag group 1"), createSpy("tag group 2"), createSpy("tag group 3")]; + rules = [createSpy("any of tags rule 1"), createSpy("any of tags rule 2"), createSpy("any of tags rule 3")]; + spyOn(Cucumber.Ast.Filter, 'AnyOfTagsRule').andReturnSeveral(rules); + options = {tags: tagGroups}; + configuration = Cucumber.VolatileConfiguration(featureSource, supportCodeInitializer, options); + }); + + it("creates an 'any of tags' filter rule per each group", function() { + configuration.getTagAstFilterRules(); + tagGroups.forEach(function(tagGroup) { + expect(Cucumber.Ast.Filter.AnyOfTagsRule).toHaveBeenCalledWith(tagGroup); + }); + }); + + it("returns all the rules", function() { + expect(configuration.getTagAstFilterRules()).toEqual(rules); + }); + }); + }); }); diff --git a/spec/cucumber_spec.js b/spec/cucumber_spec.js index 41e45bf3d..a9adaa132 100644 --- a/spec/cucumber_spec.js +++ b/spec/cucumber_spec.js @@ -3,11 +3,12 @@ require('./support/spec_helper'); describe("Cucumber", function() { var Cucumber = requireLib('cucumber'); - var featureSource, supportCodeInitializer, configuration; + var featureSource, supportCodeInitializer, options, configuration; beforeEach(function() { featureSource = createSpy("feature source"); supportCodeInitializer = createSpy("support code initialize"); + options = createSpy("other options"); configuration = createSpy("volatile configuration"); runtime = createSpy("Cucumber runtime"); spyOn(Cucumber, 'VolatileConfiguration').andReturn(configuration); @@ -15,16 +16,16 @@ describe("Cucumber", function() { }); it("creates a volatile configuration with the feature source and support code definition", function() { - Cucumber(featureSource, supportCodeInitializer); - expect(Cucumber.VolatileConfiguration).toHaveBeenCalledWith(featureSource, supportCodeInitializer); + Cucumber(featureSource, supportCodeInitializer, options); + expect(Cucumber.VolatileConfiguration).toHaveBeenCalledWith(featureSource, supportCodeInitializer, options); }); it("creates a Cucumber runtime with the configuration", function() { - Cucumber(featureSource, supportCodeInitializer); + Cucumber(featureSource, supportCodeInitializer, options); expect(Cucumber.Runtime).toHaveBeenCalledWith(configuration); }); it("returns the Cucumber runtime", function() { - expect(Cucumber(featureSource, supportCodeInitializer)).toBe(runtime); + expect(Cucumber(featureSource, supportCodeInitializer, options)).toBe(runtime); }); }); diff --git a/spec/support/configurations_shared_examples.js b/spec/support/configurations_shared_examples.js index 95b6a590d..cbe755948 100644 --- a/spec/support/configurations_shared_examples.js +++ b/spec/support/configurations_shared_examples.js @@ -12,4 +12,8 @@ itBehavesLikeAllCucumberConfigurations = function itBehavesLikeAllCucumberConfig it("supplies the support code library", function() { expect(configuration.getSupportCodeLibrary).toBeAFunction(); }); + + it("supplies the AST filter", function() { + expect(configuration.getAstFilter).toBeAFunction(); + }); }