chutney 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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