chutney 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +14 -0
  3. data/.gitignore +14 -0
  4. data/.rubocop.yml +55 -0
  5. data/Dockerfile +9 -0
  6. data/Gemfile +3 -0
  7. data/Guardfile +3 -0
  8. data/LICENSE +22 -0
  9. data/README.md +84 -0
  10. data/Rakefile +51 -0
  11. data/chutney.gemspec +54 -0
  12. data/config/default.yml +58 -0
  13. data/exe/chutney +35 -0
  14. data/lib/chutney/.DS_Store +0 -0
  15. data/lib/chutney/configuration.rb +32 -0
  16. data/lib/chutney/issue.rb +35 -0
  17. data/lib/chutney/linter/avoid_outline_for_single_example.rb +19 -0
  18. data/lib/chutney/linter/avoid_period.rb +19 -0
  19. data/lib/chutney/linter/avoid_scripting.rb +24 -0
  20. data/lib/chutney/linter/background_does_more_than_setup.rb +20 -0
  21. data/lib/chutney/linter/background_requires_multiple_scenarios.rb +18 -0
  22. data/lib/chutney/linter/bad_scenario_name.rb +21 -0
  23. data/lib/chutney/linter/be_declarative.rb +49 -0
  24. data/lib/chutney/linter/file_name_differs_feature_name.rb +27 -0
  25. data/lib/chutney/linter/invalid_file_name.rb +16 -0
  26. data/lib/chutney/linter/invalid_step_flow.rb +41 -0
  27. data/lib/chutney/linter/missing_example_name.rb +23 -0
  28. data/lib/chutney/linter/missing_feature_description.rb +17 -0
  29. data/lib/chutney/linter/missing_feature_name.rb +18 -0
  30. data/lib/chutney/linter/missing_scenario_name.rb +18 -0
  31. data/lib/chutney/linter/missing_test_action.rb +16 -0
  32. data/lib/chutney/linter/missing_verification.rb +16 -0
  33. data/lib/chutney/linter/required_tags_starts_with.rb +16 -0
  34. data/lib/chutney/linter/same_tag_for_all_scenarios.rb +73 -0
  35. data/lib/chutney/linter/tag_collector.rb +10 -0
  36. data/lib/chutney/linter/tag_constraint.rb +35 -0
  37. data/lib/chutney/linter/tag_used_multiple_times.rb +23 -0
  38. data/lib/chutney/linter/too_clumsy.rb +17 -0
  39. data/lib/chutney/linter/too_long_step.rb +17 -0
  40. data/lib/chutney/linter/too_many_different_tags.rb +45 -0
  41. data/lib/chutney/linter/too_many_steps.rb +15 -0
  42. data/lib/chutney/linter/too_many_tags.rb +19 -0
  43. data/lib/chutney/linter/unique_scenario_names.rb +22 -0
  44. data/lib/chutney/linter/unknown_variable.rb +47 -0
  45. data/lib/chutney/linter/unused_variable.rb +47 -0
  46. data/lib/chutney/linter/use_background.rb +82 -0
  47. data/lib/chutney/linter/use_outline.rb +53 -0
  48. data/lib/chutney/linter.rb +164 -0
  49. data/lib/chutney/version.rb +3 -0
  50. data/lib/chutney.rb +131 -0
  51. data/spec/chutney_spec.rb +68 -0
  52. data/spec/configuration_spec.rb +58 -0
  53. data/spec/required_tags_starts_with_spec.rb +74 -0
  54. data/spec/shared_contexts/file_exists.rb +12 -0
  55. data/spec/shared_contexts/gherkin_linter.rb +14 -0
  56. metadata +201 -0
@@ -0,0 +1,27 @@
1
+ require 'chutney/linter'
2
+
3
+ module Chutney
4
+ # service class to lint for file name differs feature name
5
+ class FileNameDiffersFeatureName < Linter
6
+ def lint
7
+ features do |file, feature|
8
+ next unless feature.include? :name
9
+
10
+ expected_feature_name = title_case file
11
+ next if ignore_whitespaces(feature[:name]).casecmp(ignore_whitespaces(expected_feature_name)) == 0
12
+
13
+ references = [reference(file, feature)]
14
+ add_error(references, "Feature name should be '#{expected_feature_name}'")
15
+ end
16
+ end
17
+
18
+ def title_case(value)
19
+ value = File.basename(value, '.feature')
20
+ value.split('_').collect(&:capitalize).join(' ')
21
+ end
22
+
23
+ def ignore_whitespaces(value)
24
+ value.delete('-').delete('_').delete(' ')
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ require 'chutney/linter'
2
+
3
+ module Chutney
4
+ # service class to lint for invalid file names
5
+ class InvalidFileName < Linter
6
+ def lint
7
+ files do |file|
8
+ base = File.basename file
9
+ next unless base != base.downcase || base =~ /[ -]/
10
+
11
+ references = [reference(file)]
12
+ add_error(references, 'Feature files should be snake_cased')
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,41 @@
1
+ require 'chutney/linter'
2
+
3
+ module Chutney
4
+ # service class to lint for invalid step flow
5
+ class InvalidStepFlow < Linter
6
+ def lint
7
+ filled_scenarios do |file, feature, scenario|
8
+ steps = scenario[:steps].select { |step| step[:keyword] != 'And ' && step[:keyword] != 'But ' }
9
+ next if steps.empty?
10
+
11
+ last_step_is_an_action(file, feature, scenario, steps)
12
+ given_after_non_given(file, feature, scenario, steps)
13
+ verification_before_action(file, feature, scenario, steps)
14
+ end
15
+ end
16
+
17
+ def last_step_is_an_action(file, feature, scenario, steps)
18
+ references = [reference(file, feature, scenario, steps.last)]
19
+ add_error(references, 'Last step is an action') if steps.last[:keyword] == 'When '
20
+ end
21
+
22
+ def given_after_non_given(file, feature, scenario, steps)
23
+ last_step = steps.first
24
+ steps.each do |step|
25
+ references = [reference(file, feature, scenario, step)]
26
+ description = 'Given after Action or Verification'
27
+ add_error(references, description) if step[:keyword] == 'Given ' && last_step[:keyword] != 'Given '
28
+ last_step = step
29
+ end
30
+ end
31
+
32
+ def verification_before_action(file, feature, scenario, steps)
33
+ steps.each do |step|
34
+ break if step[:keyword] == 'When '
35
+
36
+ references = [reference(file, feature, scenario, step)]
37
+ add_error(references, 'Missing Action step') if step[:keyword] == 'Then '
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,23 @@
1
+ require 'chutney/linter'
2
+
3
+ module Chutney
4
+ # service class to lint for missing example names
5
+ class MissingExampleName < Linter
6
+ MESSAGE = 'You have an unnamed or ambiguously named example'.freeze
7
+
8
+ def lint
9
+ scenarios do |file, feature, scenario|
10
+ next unless scenario.key? :examples
11
+ next unless scenario[:examples].length > 1
12
+
13
+ scenario[:examples].each do |example|
14
+ name = example.key?(:name) ? example[:name].strip : ''
15
+ next unless name.empty?
16
+
17
+ references = [reference(file, feature, scenario)]
18
+ add_error(references, MESSAGE)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ require 'chutney/linter'
2
+
3
+ module Chutney
4
+ # service class to lint for missing feature descriptions
5
+ class MissingFeatureDescription < Linter
6
+ MESSAGE = 'Features should have a description so that its purpose is clear'.freeze
7
+ def lint
8
+ features do |file, feature|
9
+ name = feature.key?(:description) ? feature[:description].strip : ''
10
+ next unless name.empty?
11
+
12
+ references = [reference(file, feature)]
13
+ add_error(references, MESSAGE)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ require 'chutney/linter'
2
+
3
+ module Chutney
4
+ # service class to lint for missing feature names
5
+ class MissingFeatureName < Linter
6
+ MESSAGE = 'All features should have a name'.freeze
7
+
8
+ def lint
9
+ features do |file, feature|
10
+ name = feature.key?(:name) ? feature[:name].strip : ''
11
+ next unless name.empty?
12
+
13
+ references = [reference(file, feature)]
14
+ add_error(references, MESSAGE)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ require 'chutney/linter'
2
+
3
+ module Chutney
4
+ # service class to lint for missing scenario names
5
+ class MissingScenarioName < Linter
6
+ MESSAGE = 'All scenarios should have a name'.freeze
7
+
8
+ def lint
9
+ scenarios do |file, feature, scenario|
10
+ name = scenario.key?(:name) ? scenario[:name].strip : ''
11
+ references = [reference(file, feature, scenario)]
12
+ next unless name.empty?
13
+
14
+ add_error(references, MESSAGE)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ require 'chutney/linter'
2
+
3
+ module Chutney
4
+ # service class to lint for missing test actions
5
+ class MissingTestAction < Linter
6
+ def lint
7
+ filled_scenarios do |file, feature, scenario|
8
+ when_steps = scenario[:steps].select { |step| step[:keyword] == 'When ' }
9
+ next unless when_steps.empty?
10
+
11
+ references = [reference(file, feature, scenario)]
12
+ add_error(references, 'No \'When\'-Step')
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ require 'chutney/linter'
2
+
3
+ module Chutney
4
+ # service class to lint for missing verifications
5
+ class MissingVerification < Linter
6
+ def lint
7
+ filled_scenarios do |file, feature, scenario|
8
+ then_steps = scenario[:steps].select { |step| step[:keyword] == 'Then ' }
9
+ next unless then_steps.empty?
10
+
11
+ references = [reference(file, feature, scenario)]
12
+ add_error(references, 'No \'Then\' step')
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ require 'chutney/linter/tag_constraint'
2
+ require 'chutney/linter'
3
+
4
+ module Chutney
5
+ # service class to lint for tags used multiple times
6
+ class RequiredTagsStartsWith < Linter
7
+ include TagConstraint
8
+
9
+ def match_pattern?(target)
10
+ target.each do |t|
11
+ return true if t.delete!('@').start_with?(*@pattern)
12
+ end
13
+ false
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,73 @@
1
+ require 'chutney/linter'
2
+
3
+ module Chutney
4
+ # service class to lint for using same tag on all scenarios
5
+ class SameTagForAllScenarios < Linter
6
+ def lint
7
+ features do |file, feature|
8
+ next unless feature.include? :children
9
+
10
+ lint_scenarios file, feature
11
+ lint_examples file, feature
12
+ end
13
+ end
14
+
15
+ def lint_scenarios(file, feature)
16
+ tags = gather_same_tags feature
17
+ return if tags.nil?
18
+ return if tags.empty?
19
+ return unless feature[:children].length > 1
20
+
21
+ references = [reference(file, feature)]
22
+ tags.each do |tag|
23
+ next if tag == '@skip'
24
+
25
+ add_error(references, "Tag '#{tag}' should be used at Feature level")
26
+ end
27
+ end
28
+
29
+ def lint_examples(file, feature)
30
+ feature[:children].each do |scenario|
31
+ tags = gather_same_tags_for_outline scenario
32
+ next if tags.nil? || tags.empty?
33
+ next unless scenario[:examples].length > 1
34
+
35
+ references = [reference(file, feature, scenario)]
36
+ tags.each do |tag|
37
+ next if tag == '@skip'
38
+
39
+ add_error(references, "Tag '#{tag}' should be used at Scenario Outline level")
40
+ end
41
+ end
42
+ end
43
+
44
+ def gather_same_tags(feature)
45
+ result = nil
46
+ feature[:children].each do |scenario|
47
+ next if scenario[:type] == :Background
48
+ return nil unless scenario.include? :tags
49
+
50
+ tags = scenario[:tags].map { |tag| tag[:name] }
51
+ result = tags if result.nil?
52
+
53
+ result &= tags
54
+ end
55
+ result
56
+ end
57
+
58
+ def gather_same_tags_for_outline(scenario)
59
+ result = nil
60
+ return result unless scenario.include? :examples
61
+
62
+ scenario[:examples].each do |example|
63
+ return nil unless example.include? :tags
64
+
65
+ tags = example[:tags].map { |tag| tag[:name] }
66
+ result = tags if result.nil?
67
+
68
+ result &= tags
69
+ end
70
+ result
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,10 @@
1
+ module Chutney
2
+ # Mixin to lint for tags based on their relationship to eachother
3
+ module TagCollector
4
+ def gather_tags(element)
5
+ return [] unless element.include? :tags
6
+
7
+ element[:tags].map { |tag| tag[:name][1..-1] }
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,35 @@
1
+ module Chutney
2
+ # Mixin to lint for tags that have certain string contraints
3
+ module TagConstraint
4
+ def lint
5
+ scenarios do |file, feature, scenario|
6
+ next if match_pattern? tags(feature)
7
+ next if match_pattern? tags(scenario)
8
+
9
+ references = [reference(file, feature, scenario)]
10
+ add_error(references, 'Required Tag not found')
11
+ end
12
+ end
13
+
14
+ def tags(element)
15
+ return [] unless element.include? :tags
16
+
17
+ element[:tags].map { |a| a[:name] }
18
+ end
19
+
20
+ def matcher(pattern)
21
+ @pattern = pattern
22
+ validate_input
23
+ end
24
+
25
+ def match_pattern?(_tags)
26
+ raise NoMethodError, 'This is an abstraction that must be implemented by the includer'
27
+ end
28
+
29
+ def validate_input
30
+ raise 'No Tags provided in the YAML' if @pattern.nil?
31
+
32
+ warn 'Required Tags matcher has no value' if @pattern.empty?
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,23 @@
1
+ require 'chutney/linter'
2
+
3
+ module Chutney
4
+ # service class to lint for tags used multiple times
5
+ class TagUsedMultipleTimes < Linter
6
+ def lint
7
+ scenarios do |file, feature, scenario|
8
+ references = [reference(file, feature, scenario)]
9
+ total_tags = tags(feature) + tags(scenario)
10
+ double_used_tags = total_tags.find_all { |a| total_tags.count(a) > 1 }.uniq!
11
+ next if double_used_tags.nil?
12
+
13
+ add_error(references, "Tag #{double_used_tags.join(' and ')} used multiple times")
14
+ end
15
+ end
16
+
17
+ def tags(element)
18
+ return [] unless element.include? :tags
19
+
20
+ element[:tags].map { |a| a[:name] }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ require 'chutney/linter'
2
+
3
+ module Chutney
4
+ # service class to lint for too clumsy scenarios
5
+ class TooClumsy < Linter
6
+ MESSAGE = 'This scenario is too long at %d characters'.freeze
7
+ def lint
8
+ filled_scenarios do |file, feature, scenario|
9
+ characters = scenario[:steps].map { |step| step[:text].length }.inject(0, :+)
10
+ next if characters < 400
11
+
12
+ references = [reference(file, feature, scenario)]
13
+ add_error(references, MESSAGE % characters)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ require 'chutney/linter'
2
+
3
+ module Chutney
4
+ # service class to lint for too long steps
5
+ class TooLongStep < Linter
6
+ MESSAGE = 'This step is too long at %d characters'.freeze
7
+
8
+ def lint
9
+ steps do |file, feature, scenario, step|
10
+ next if step[:text].length < 80
11
+
12
+ references = [reference(file, feature, scenario, step)]
13
+ add_error(references, MESSAGE % step[:text].length)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,45 @@
1
+ require 'chutney/linter'
2
+ require 'chutney/linter/tag_collector'
3
+
4
+ module Chutney
5
+ # service class to lint for too many different tags
6
+ class TooManyDifferentTags < Linter
7
+ include TagCollector
8
+
9
+ def lint
10
+ overall_tags = []
11
+ overall_references = []
12
+ features do |file, feature|
13
+ tags = tags_for_feature(feature)
14
+ overall_tags += tags
15
+ references = [reference(file, feature)]
16
+ overall_references += references unless tags.empty?
17
+
18
+ warn_single_feature(references, tags)
19
+ end
20
+ warn_across_all_features(overall_references, overall_tags)
21
+ end
22
+
23
+ def warn_single_feature(references, tags)
24
+ tags.uniq!
25
+ references.uniq!
26
+ return false unless tags.length >= 3
27
+
28
+ add_error(references, "Used #{tags.length} Tags within single Feature")
29
+ end
30
+
31
+ def warn_across_all_features(references, tags)
32
+ tags.uniq!
33
+ references.uniq!
34
+ return false unless tags.length >= 10
35
+
36
+ add_error(references, "Used #{tags.length} Tags across all Features")
37
+ end
38
+
39
+ def tags_for_feature(feature)
40
+ return [] unless feature.include? :children
41
+
42
+ gather_tags(feature) + feature[:children].map { |scenario| gather_tags(scenario) }.flatten
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,15 @@
1
+ require 'chutney/linter'
2
+
3
+ module Chutney
4
+ # service class to lint for too many steps
5
+ class TooManySteps < Linter
6
+ def lint
7
+ filled_scenarios do |file, feature, scenario|
8
+ next if scenario[:steps].length < 10
9
+
10
+ references = [reference(file, feature, scenario)]
11
+ add_error(references, "Scenario is too long at #{scenario[:steps].length} steps")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ require 'chutney/linter'
2
+ require 'chutney/linter/tag_collector'
3
+
4
+ module Chutney
5
+ # service class to lint for too many tags
6
+ class TooManyTags < Linter
7
+ include TagCollector
8
+
9
+ def lint
10
+ scenarios do |file, feature, scenario|
11
+ tags = gather_tags(feature) + gather_tags(scenario)
12
+ next unless tags.length >= 3
13
+
14
+ references = [reference(file, feature, scenario)]
15
+ add_error(references, "Used #{tags.length} Tags")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ require 'chutney/linter'
2
+
3
+ module Chutney
4
+ # service class to lint for unique scenario names
5
+ class UniqueScenarioNames < Linter
6
+ def lint
7
+ references_by_name = Hash.new []
8
+ scenarios do |file, feature, scenario|
9
+ next unless scenario.key? :name
10
+
11
+ scenario_name = "#{feature[:name]}.#{scenario[:name]}"
12
+ references_by_name[scenario_name] = references_by_name[scenario_name] + [reference(file, feature, scenario)]
13
+ end
14
+
15
+ references_by_name.each do |name, references|
16
+ next if references.length <= 1
17
+
18
+ add_error(references, "Scenario name should be unique, '#{name}' used #{references.length} times")
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,47 @@
1
+ require 'chutney/linter'
2
+
3
+ module Chutney
4
+ # service class to lint for unknown variables
5
+ class UnknownVariable < Linter
6
+ def lint
7
+ filled_scenarios do |file, feature, scenario|
8
+ known_vars = Set.new known_variables scenario
9
+ scenario[:steps].each do |step|
10
+ step_vars(step).each do |used_var|
11
+ next if known_vars.include? used_var
12
+
13
+ references = [reference(file, feature, scenario)]
14
+ add_error(references, "Variable '<#{used_var}>' is unknown")
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ def step_vars(step)
21
+ vars = gather_vars step[:text]
22
+ return vars unless step.include? :argument
23
+
24
+ vars + gather_vars_from_argument(step[:argument])
25
+ end
26
+
27
+ def gather_vars_from_argument(argument)
28
+ return gather_vars argument[:content] if argument[:type] == :DocString
29
+
30
+ (argument[:rows] || []).map do |row|
31
+ row[:cells].map { |value| gather_vars value[:value] }.flatten
32
+ end.flatten
33
+ end
34
+
35
+ def gather_vars(string)
36
+ string.scan(/<.+?>/).map { |val| val[1..-2] }
37
+ end
38
+
39
+ def known_variables(scenario)
40
+ (scenario[:examples] || []).map do |example|
41
+ next unless example.key? :tableHeader
42
+
43
+ example[:tableHeader][:cells].map { |cell| cell[:value].strip }
44
+ end.flatten
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,47 @@
1
+ require 'chutney/linter'
2
+
3
+ module Chutney
4
+ # service class to lint for unused variables
5
+ class UnusedVariable < Linter
6
+ def lint
7
+ scenarios do |file, feature, scenario|
8
+ next unless scenario.key? :examples
9
+
10
+ scenario[:examples].each do |example|
11
+ next unless example.key? :tableHeader
12
+
13
+ example[:tableHeader][:cells].map { |cell| cell[:value] }.each do |variable|
14
+ references = [reference(file, feature, scenario)]
15
+ add_error(references, "Variable '<#{variable}>' is unused") unless used?(variable, scenario)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ def used?(variable, scenario)
22
+ variable = "<#{variable}>"
23
+ return false unless scenario.key? :steps
24
+
25
+ scenario[:steps].each do |step|
26
+ return true if step[:text].include? variable
27
+ next unless step.include? :argument
28
+ return true if used_in_docstring?(variable, step)
29
+ return true if used_in_table?(variable, step)
30
+ end
31
+ false
32
+ end
33
+
34
+ def used_in_docstring?(variable, step)
35
+ step[:argument][:type] == :DocString && step[:argument][:content].include?(variable)
36
+ end
37
+
38
+ def used_in_table?(variable, step)
39
+ return false unless step[:argument][:type] == :DataTable
40
+
41
+ step[:argument][:rows].each do |row|
42
+ row[:cells].each { |value| return true if value[:value].include?(variable) }
43
+ end
44
+ false
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,82 @@
1
+ require 'chutney/linter'
2
+
3
+ module Chutney
4
+ # service class to lint for using background
5
+ class UseBackground < Linter
6
+ def lint
7
+ features do |file, feature|
8
+ next if scenarios_with_steps(feature) <= 1
9
+
10
+ givens = gather_givens feature
11
+ next if givens.nil?
12
+ next if givens.length <= 1
13
+ next if givens.uniq.length > 1
14
+
15
+ references = [reference(file, feature)]
16
+ add_error(references, "Step '#{givens.uniq.first}' should be part of background")
17
+ end
18
+ end
19
+
20
+ def scenarios_with_steps(feature)
21
+ scenarios = 0
22
+ return 0 unless feature.key? :children
23
+
24
+ feature[:children].each do |scenario|
25
+ next unless scenario.include? :steps
26
+ next if scenario[:steps].empty?
27
+
28
+ scenarios += 1
29
+ end
30
+ scenarios
31
+ end
32
+
33
+ def gather_givens(feature)
34
+ return unless feature.include? :children
35
+
36
+ has_non_given_step = false
37
+ feature[:children].each do |scenario|
38
+ next unless scenario.include? :steps
39
+ next if scenario[:steps].empty?
40
+
41
+ has_non_given_step = true unless scenario[:steps].first[:keyword] == 'Given '
42
+ end
43
+ return if has_non_given_step
44
+
45
+ result = []
46
+ expanded_steps(feature) { |given| result.push given }
47
+ result
48
+ end
49
+
50
+ def expanded_steps(feature)
51
+ feature[:children].each do |scenario|
52
+ next unless scenario[:type] != :Background
53
+ next unless scenario.include? :steps
54
+ next if scenario[:steps].empty?
55
+
56
+ prototypes = [render_step(scenario[:steps].first)]
57
+ prototypes = expand_examples(scenario[:examples], prototypes) if scenario.key? :examples
58
+ prototypes.each { |prototype| yield prototype }
59
+ end
60
+ end
61
+
62
+ def expand_examples(examples, prototypes)
63
+ examples.each do |example|
64
+ prototypes = prototypes.map { |prototype| expand_outlines(prototype, example) }.flatten
65
+ end
66
+ prototypes
67
+ end
68
+
69
+ def expand_outlines(sentence, example)
70
+ result = []
71
+ headers = example[:tableHeader][:cells].map { |cell| cell[:value] }
72
+ example[:tableBody].each do |row| # .slice(1, example[:tableBody].length).each do |row|
73
+ modified_sentence = sentence.dup
74
+ headers.zip(row[:cells].map { |cell| cell[:value] }).map do |key, value|
75
+ modified_sentence.gsub!("<#{key}>", value)
76
+ end
77
+ result.push modified_sentence
78
+ end
79
+ result
80
+ end
81
+ end
82
+ end