diff --git a/features/docs/formatters/json_formatter.feature b/features/docs/formatters/json_formatter.feature index 2d3b303809..b6f48d0aee 100644 --- a/features/docs/formatters/json_formatter.feature +++ b/features/docs/formatters/json_formatter.feature @@ -270,7 +270,7 @@ Feature: JSON output formatter """ - @spawn @wip-jruby + @spawn Scenario: DocString Given a file named "features/doc_string.feature" with: """ @@ -285,7 +285,7 @@ Feature: JSON output formatter And a file named "features/step_definitions/steps.rb" with: """ Then /I should fail with/ do |s| - raise s + raise RuntimeError, s end """ When I run `cucumber --format json features/doc_string.feature` @@ -333,7 +333,7 @@ Feature: JSON output formatter ] """ - @spawn + @spawn Scenario: embedding screenshot When I run `cucumber -b --format json features/embed.feature` Then it should pass with JSON: @@ -381,6 +381,7 @@ Feature: JSON output formatter """ + @spawn Scenario: scenario outline When I run `cucumber --format json features/outline.feature` Then it should fail with JSON: @@ -395,75 +396,69 @@ Feature: JSON output formatter "description": "", "elements": [ { - "id": "an-outline-feature;outline", + "id": "an-outline-feature;outline;examples1;2", "keyword": "Scenario Outline", "name": "outline", - "line": 3, "description": "", - "type": "scenario_outline", + "line": 8, + "type": "scenario", "steps": [ { "keyword": "Given ", - "name": "this step ", - "line": 4, + "name": "this step passes", + "line": 8, "match": { - "location": "features/outline.feature:4" + "location": "features/step_definitions/steps.rb:1" + }, + "result": { + "status": "passed", + "duration": 1 } } - ], - "examples": [ + ] + }, + { + "id": "an-outline-feature;outline;examples1;3", + "keyword": "Scenario Outline", + "name": "outline", + "description": "", + "line": 9, + "type": "scenario", + "steps": [ { - "keyword": "Examples", - "name": "examples1", - "line": 6, - "description": "", - "id": "an-outline-feature;outline;examples1", - "rows": [ - { - "cells": [ - "status" - ], - "line": 7, - "id": "an-outline-feature;outline;examples1;1" - }, - { - "cells": [ - "passes" - ], - "line": 8, - "id": "an-outline-feature;outline;examples1;2" - }, - { - "cells": [ - "fails" - ], - "line": 9, - "id": "an-outline-feature;outline;examples1;3" - } - ] - }, + "keyword": "Given ", + "name": "this step fails", + "line": 9, + "match": { + "location": "features/step_definitions/steps.rb:4" + }, + "result": { + "status": "failed", + "error_message": " (RuntimeError)\n./features/step_definitions/steps.rb:4:in `/^this step fails$/'\nfeatures/outline.feature:9:in `Given this step fails'\nfeatures/outline.feature:4:in `Given this step '", + "duration": 1 + } + } + ] + }, + { + "id": "an-outline-feature;outline;examples2;2", + "keyword": "Scenario Outline", + "name": "outline", + "description": "", + "line": 13, + "type": "scenario", + "steps": [ { - "keyword": "Examples", - "name": "examples2", - "line": 11, - "description": "", - "id": "an-outline-feature;outline;examples2", - "rows": [ - { - "cells": [ - "status" - ], - "line": 12, - "id": "an-outline-feature;outline;examples2;1" - }, - { - "cells": [ - "passes" - ], - "line": 13, - "id": "an-outline-feature;outline;examples2;2" - } - ] + "keyword": "Given ", + "name": "this step passes", + "line": 13, + "match": { + "location": "features/step_definitions/steps.rb:1" + }, + "result": { + "status": "passed", + "duration": 1 + } } ] } @@ -735,7 +730,7 @@ Feature: JSON output formatter puts "Before hook 2" embed "src", "mime_type", "label" end - + AfterStep do puts "AfterStep hook 1" embed "src", "mime_type", "label" diff --git a/features/lib/support/normalise_output.rb b/features/lib/support/normalise_output.rb index da2dc3df24..615444264f 100644 --- a/features/lib/support/normalise_output.rb +++ b/features/lib/support/normalise_output.rb @@ -19,15 +19,32 @@ def normalise_json(json) elements = feature.fetch('elements') { [] } elements.each do |scenario| scenario['steps'].each do |step| - if step['result'] - expect(step['result']['duration']).to be >= 0 - step['result']['duration'] = 1 + ['steps', 'before', 'after'].each do |type| + if scenario[type] + scenario[type].each do |step_or_hook| + normalise_json_step_or_hook(step_or_hook) + if step_or_hook['after'] + step_or_hook['after'].each do |hook| + normalise_json_step_or_hook(hook) + end + end + end + end end end end end end + + def normalise_json_step_or_hook(step_or_hook) + if step_or_hook['result'] + if step_or_hook['result']['duration'] + expect(step_or_hook['result']['duration']).to be >= 0 + step_or_hook['result']['duration'] = 1 + end + end + end + end World(NormaliseArubaOutput) - diff --git a/lib/cucumber/formatter/json.rb b/lib/cucumber/formatter/json.rb index a2e720ec4d..ea0cd6cbd6 100644 --- a/lib/cucumber/formatter/json.rb +++ b/lib/cucumber/formatter/json.rb @@ -1,19 +1,280 @@ -require 'cucumber/formatter/gherkin_formatter_adapter' +require 'multi_json' +require 'base64' require 'cucumber/formatter/io' -require 'gherkin/formatter/argument' -require 'gherkin/formatter/json_formatter' module Cucumber module Formatter # The formatter used for --format json - class Json < GherkinFormatterAdapter + class Json include Io - def initialize(runtime, io, options) - @io = ensure_io(io, "json") - super(Gherkin::Formatter::JSONFormatter.new(@io), false, options) + def initialize(runtime, io, _options) + @runtime = runtime + @io = ensure_io(io, 'json') + @feature_hashes = [] + end + + def before_test_case(test_case) + builder = Builder.new(test_case) + unless same_feature_as_previous_test_case?(test_case.feature) + @feature_hash = builder.feature_hash + @feature_hashes << @feature_hash + end + @test_case_hash = builder.test_case_hash + if builder.background? + feature_elements << builder.background_hash + @element_hash = builder.background_hash + else + feature_elements << @test_case_hash + @element_hash = @test_case_hash + end + end + + def before_test_step(test_step) + return if prepare_world_hook?(test_step) + if hook?(test_step) + @step_or_hook_hash = {} + hooks_of_type(test_step) << @step_or_hook_hash + return + end + if first_step_after_background?(test_step) + feature_elements << @test_case_hash + @element_hash = @test_case_hash + end + @step_or_hook_hash = create_step_hash(test_step.source.last) + steps << @step_or_hook_hash + @step_hash = @step_or_hook_hash + end + + def after_test_step(test_step, result) + return if prepare_world_hook?(test_step) + add_match_and_result(test_step.source.last, result) + end + + def done + @io.write(MultiJson.dump(@feature_hashes, pretty: true)) + end + + def puts(message) + test_step_output << message + end + + def embed(src, mime_type, _label) + if File.file?(src) + content = File.open(src, 'rb') { |f| f.read } + data = encode64(content) + else + if mime_type =~ /;base64$/ + mime_type = mime_type[0..-8] + data = src + else + data = encode64(src) + end + end + test_step_embeddings << { mime_type: mime_type, data: data } + end + + private + + def same_feature_as_previous_test_case?(feature) + current_feature[:uri] == feature.file && current_feature[:line] == feature.location.line + end + + def first_step_after_background?(test_step) + test_step.source[1].name != @element_hash[:name] + end + + def prepare_world_hook?(test_step) + test_step.source.last.location.file.end_with?('cucumber/filters/prepare_world.rb') + end + + def hook?(test_step) + hook_source?(test_step.source.last) + end + + def hook_source?(step_source) + ['Before hook', 'After hook', 'AfterStep hook'].include? step_source.name + end + + def current_feature + @feature_hash ||= {} + end + + def feature_elements + @feature_hash[:elements] ||= [] + end + + def steps + @element_hash[:steps] ||= [] + end + + def hooks_of_type(test_step) + name = test_step.source.last.name + if name == 'Before hook' + return before_hooks + elsif name == 'After hook' + return after_hooks + elsif name == 'AfterStep hook' + return after_step_hooks + else + fail 'Unkown hook type ' + name + end + end + + def before_hooks + @element_hash[:before] ||= [] + end + + def after_hooks + @element_hash[:after] ||= [] + end + + def after_step_hooks + @step_hash[:after] ||= [] + end + + def test_step_output + @step_or_hook_hash[:output] ||= [] + end + + def test_step_embeddings + @step_or_hook_hash[:embeddings] ||= [] + end + + def create_step_hash(step_source) + step_hash = { + keyword: step_source.keyword, + name: step_source.name, + line: step_source.location.line + } + step_hash[:doc_string] = create_doc_string_hash(step_source.multiline_arg) if step_source.multiline_arg.doc_string? + step_hash + end + + def create_doc_string_hash(doc_string) + { + value: doc_string.content, + content_type: doc_string.content_type, + line: doc_string.location.line + } + end + + def add_match_and_result(step_source, result) + @step_or_hook_hash[:match] = create_match_hash(step_source, result) + @step_or_hook_hash[:result] = create_result_hash(result) + end + + def create_match_hash(step_source, result) + if result.undefined? || hook_source?(step_source) + location = step_source.location + else + location = @runtime.step_match(step_source.name).file_colon_line + end + { location: location } + end + + def create_result_hash(result) + result_hash = { + status: result.to_sym + } + result_hash[:error_message] = create_error_message(result) if result.failed? || result.pending? + result.duration.tap { |duration| result_hash[:duration] = duration.nanoseconds } + result_hash + end + + def create_error_message(result) + message_element = result.failed? ? result.exception : result + message = "#{message_element.message} (#{message_element.class})" + ([message] + message_element.backtrace).join("\n") + end + + def encode64(data) + # strip newlines from the encoded data + Base64.encode64(data).gsub(/\n/, '') + end + + class Builder + attr_reader :feature_hash, :background_hash, :test_case_hash + + def initialize(test_case) + @background_hash = nil + test_case.describe_source_to(self) + test_case.feature.background.describe_to(self) + end + + def background? + @background_hash != nil + end + + def feature(feature) + @feature_hash = { + uri: feature.file, + id: create_id(feature), + keyword: feature.keyword, + name: feature.name, + description: feature.description, + line: feature.location.line + } + @feature_hash[:tags] = create_tags_array(feature.tags) unless feature.tags.empty? + @test_case_hash[:id].insert(0, @feature_hash[:id] + ';') + end + + def background(background) + @background_hash = { + keyword: background.keyword, + name: background.name, + description: background.description, + line: background.location.line, + type: 'background' + } + end + + def scenario(scenario) + @test_case_hash = { + id: create_id(scenario), + keyword: scenario.keyword, + name: scenario.name, + description: scenario.description, + line: scenario.location.line, + type: 'scenario' + } + @test_case_hash[:tags] = create_tags_array(scenario.tags) unless scenario.tags.empty? + end + + def scenario_outline(scenario) + @test_case_hash = { + id: create_id(scenario) + ';' + @example_id, + keyword: scenario.keyword, + name: scenario.name, + description: scenario.description, + line: @row.location.line, + type: 'scenario' + } + @test_case_hash[:tags] = create_tags_array(scenario.tags) unless scenario.tags.empty? + end + + def examples_table(examples_table) + # the json file have traditionally used the header row as row 1, + # wheras cucumber-ruby-core used the first example row as row 1. + @example_id = create_id(examples_table) + ";#{@row.number + 1}" + end + + def examples_table_row(row) + @row = row + end + + private + + def create_id(element) + element.name.downcase.gsub(/ /, '-') + end + + def create_tags_array(tags) + tags_array = [] + tags.each { |tag| tags_array << { name: tag.name, line: tag.location.line } } + tags_array + end end end end end - diff --git a/lib/cucumber/formatter/legacy_api/runtime_facade.rb b/lib/cucumber/formatter/legacy_api/runtime_facade.rb index 9a88e69891..079d0533c2 100644 --- a/lib/cucumber/formatter/legacy_api/runtime_facade.rb +++ b/lib/cucumber/formatter/legacy_api/runtime_facade.rb @@ -23,6 +23,10 @@ def scenarios(status = nil) def steps(status = nil) results.steps(status) end + + def step_match(step_name, name_to_report=nil) + support_code.step_match(step_name, name_to_report) + end end end diff --git a/spec/cucumber/formatter/json_spec.rb b/spec/cucumber/formatter/json_spec.rb new file mode 100644 index 0000000000..00f81a612c --- /dev/null +++ b/spec/cucumber/formatter/json_spec.rb @@ -0,0 +1,640 @@ +require 'spec_helper' +require 'cucumber/formatter/spec_helper' +require 'cucumber/formatter/json' +require 'cucumber/cli/options' +require 'multi_json' + +module Cucumber + module Formatter + describe Json do + extend SpecHelperDsl + include SpecHelper + + context "Given a single feature" do + before(:each) do + @out = StringIO.new + @formatter = Json.new(runtime, @out, {}) + run_defined_feature + end + + describe "with a scenario with no steps" do + define_feature <<-FEATURE + Feature: Banana party + + Scenario: Monkey eats bananas + FEATURE + + it "outputs the json data" do + expect(load_normalised_json(@out)).to eq MultiJson.load(%{ + [{"id": "banana-party", + "uri": "spec.feature", + "keyword": "Feature", + "name": "Banana party", + "line": 1, + "description": "", + "elements": + [{"id": "banana-party;monkey-eats-bananas", + "keyword": "Scenario", + "name": "Monkey eats bananas", + "line": 3, + "description": "", + "type": "scenario"}]}]}) + end + end + + describe "with a scenario with an undefined step" do + define_feature <<-FEATURE + Feature: Banana party + + Scenario: Monkey eats bananas + Given there are bananas + FEATURE + + it "outputs the json data" do + expect(load_normalised_json(@out)).to eq MultiJson.load(%{ + [{"id": "banana-party", + "uri": "spec.feature", + "keyword": "Feature", + "name": "Banana party", + "line": 1, + "description": "", + "elements": + [{"id": "banana-party;monkey-eats-bananas", + "keyword": "Scenario", + "name": "Monkey eats bananas", + "line": 3, + "description": "", + "type": "scenario", + "steps": + [{"keyword": "Given ", + "name": "there are bananas", + "line": 4, + "match": {"location": "spec.feature:4"}, + "result": {"status": "undefined"}}]}]}]}) + end + end + + describe "with a scenario with a passed step" do + define_feature <<-FEATURE + Feature: Banana party + + Scenario: Monkey eats bananas + Given there are bananas + FEATURE + + define_steps do + Given(/^there are bananas$/) {} + end + + it "outputs the json data" do + expect(load_normalised_json(@out)).to eq MultiJson.load(%{ + [{"id": "banana-party", + "uri": "spec.feature", + "keyword": "Feature", + "name": "Banana party", + "line": 1, + "description": "", + "elements": + [{"id": "banana-party;monkey-eats-bananas", + "keyword": "Scenario", + "name": "Monkey eats bananas", + "line": 3, + "description": "", + "type": "scenario", + "steps": + [{"keyword": "Given ", + "name": "there are bananas", + "line": 4, + "match": {"location": "spec/cucumber/formatter/json_spec.rb:86"}, + "result": {"status": "passed", + "duration": 1}}]}]}]}) + end + end + + describe "with a scenario with a failed step" do + define_feature <<-FEATURE + Feature: Banana party + + Scenario: Monkey eats bananas + Given there are bananas + FEATURE + + define_steps do + Given(/^there are bananas$/) { raise "no bananas" } + end + + it "outputs the json data" do + expect(load_normalised_json(@out)).to eq MultiJson.load(%{ + [{"id": "banana-party", + "uri": "spec.feature", + "keyword": "Feature", + "name": "Banana party", + "line": 1, + "description": "", + "elements": + [{"id": "banana-party;monkey-eats-bananas", + "keyword": "Scenario", + "name": "Monkey eats bananas", + "line": 3, + "description": "", + "type": "scenario", + "steps": + [{"keyword": "Given ", + "name": "there are bananas", + "line": 4, + "match": {"location": "spec/cucumber/formatter/json_spec.rb:123"}, + "result": {"status": "failed", + "error_message": "no bananas (RuntimeError)\\n./spec/cucumber/formatter/json_spec.rb:123:in `/^there are bananas$/'\\nspec.feature:4:in `Given there are bananas'", + "duration": 1}}]}]}]}) + end + end + + describe "with a scenario with a pending step" do + define_feature <<-FEATURE + Feature: Banana party + + Scenario: Monkey eats bananas + Given there are bananas + FEATURE + + define_steps do + Given(/^there are bananas$/) { pending } + end + + it "outputs the json data" do + expect(load_normalised_json(@out)).to eq MultiJson.load(%{ + [{"id": "banana-party", + "uri": "spec.feature", + "keyword": "Feature", + "name": "Banana party", + "line": 1, + "description": "", + "elements": + [{"id": "banana-party;monkey-eats-bananas", + "keyword": "Scenario", + "name": "Monkey eats bananas", + "line": 3, + "description": "", + "type": "scenario", + "steps": + [{"keyword": "Given ", + "name": "there are bananas", + "line": 4, + "match": {"location": "spec/cucumber/formatter/json_spec.rb:161"}, + "result": {"status": "pending", + "error_message": "TODO (Cucumber::Pending)\\n./spec/cucumber/formatter/json_spec.rb:161:in `/^there are bananas$/'\\nspec.feature:4:in `Given there are bananas'", + "duration": 1}}]}]}]}) + end + end + + describe "with a scenario outline with one example" do + define_feature <<-FEATURE + Feature: Banana party + + Scenario Outline: Monkey eats bananas + Given there are + + Examples: Fruit Table + | fruit | + | bananas | + FEATURE + + define_steps do + Given(/^there are bananas$/) {} + end + + it "outputs the json data" do + expect(load_normalised_json(@out)).to eq MultiJson.load(%{ + [{"id": "banana-party", + "uri": "spec.feature", + "keyword": "Feature", + "name": "Banana party", + "line": 1, + "description": "", + "elements": + [{"id": "banana-party;monkey-eats-bananas;fruit-table;2", + "keyword": "Scenario Outline", + "name": "Monkey eats bananas", + "line": 8, + "description": "", + "type": "scenario", + "steps": + [{"keyword": "Given ", + "name": "there are bananas", + "line": 8, + "match": {"location": "spec/cucumber/formatter/json_spec.rb:203"}, + "result": {"status": "passed", + "duration": 1}}]}]}]}) + end + end + + describe "with a tags in the feature file" do + define_feature <<-FEATURE + @f + Feature: Banana party + + @s + Scenario: Monkey eats bananas + Given there are bananas + + @so + Scenario Outline: Monkey eats bananas + Given there are + + @ex + Examples: Fruit Table + | fruit | + | bananas | + FEATURE + + define_steps do + Given(/^there are bananas$/) {} + end + + it "the tags are included in the json data" do + expect(load_normalised_json(@out)).to eq MultiJson.load(%{ + [{"id": "banana-party", + "uri": "spec.feature", + "keyword": "Feature", + "name": "Banana party", + "line": 2, + "description": "", + "tags": [{"name": "@f", + "line": 1}], + "elements": + [{"id": "banana-party;monkey-eats-bananas", + "keyword": "Scenario", + "name": "Monkey eats bananas", + "line": 5, + "description": "", + "tags": [{"name": "@s", + "line": 4}], + "type": "scenario", + "steps": + [{"keyword": "Given ", + "name": "there are bananas", + "line": 6, + "match": {"location": "spec/cucumber/formatter/json_spec.rb:251"}, + "result": {"status": "passed", + "duration": 1}}]}, + {"id": "banana-party;monkey-eats-bananas;fruit-table;2", + "keyword": "Scenario Outline", + "name": "Monkey eats bananas", + "line": 15, + "description": "", + "tags": [{"name": "@so", + "line": 8}], + "type": "scenario", + "steps": + [{"keyword": "Given ", + "name": "there are bananas", + "line": 15, + "match": {"location": "spec/cucumber/formatter/json_spec.rb:251"}, + "result": {"status": "passed", + "duration": 1}}]}]}]}) + end + end + + describe "with a scenario with a step with a doc string" do + define_feature <<-FEATURE + Feature: Banana party + + Scenario: Monkey eats bananas + Given there are bananas + """ + the doc string + """ + FEATURE + + define_steps do + Given(/^there are bananas$/) { |s| s } + end + + it "includes the doc string in the json data" do + expect(load_normalised_json(@out)).to eq MultiJson.load(%{ + [{"id": "banana-party", + "uri": "spec.feature", + "keyword": "Feature", + "name": "Banana party", + "line": 1, + "description": "", + "elements": + [{"id": "banana-party;monkey-eats-bananas", + "keyword": "Scenario", + "name": "Monkey eats bananas", + "line": 3, + "description": "", + "type": "scenario", + "steps": + [{"keyword": "Given ", + "name": "there are bananas", + "line": 4, + "doc_string": {"value": "the doc string", + "content_type": "", + "line": 5}, + "match": {"location": "spec/cucumber/formatter/json_spec.rb:310"}, + "result": {"status": "passed", + "duration": 1}}]}]}]}) + end + end + + describe "with a scenario with a step that use puts" do + define_feature <<-FEATURE + Feature: Banana party + + Scenario: Monkey eats bananas + Given there are bananas + FEATURE + + define_steps do + Given(/^there are bananas$/) { puts "from step" } + end + + it "includes the output from the step in the json data" do + expect(load_normalised_json(@out)).to eq MultiJson.load(%{ + [{"id": "banana-party", + "uri": "spec.feature", + "keyword": "Feature", + "name": "Banana party", + "line": 1, + "description": "", + "elements": + [{"id": "banana-party;monkey-eats-bananas", + "keyword": "Scenario", + "name": "Monkey eats bananas", + "line": 3, + "description": "", + "type": "scenario", + "steps": + [{"keyword": "Given ", + "name": "there are bananas", + "line": 4, + "output": ["from step"], + "match": {"location": "spec/cucumber/formatter/json_spec.rb:350"}, + "result": {"status": "passed", + "duration": 1}}]}]}]}) + end + end + + describe "with a background" do + define_feature <<-FEATURE + Feature: Banana party + + Background: There are bananas + Given there are bananas + + Scenario: Monkey eats bananas + Then the monkey eats bananas + + Scenario: Monkey eats more bananas + Then the monkey eats more bananas + FEATURE + + it "includes the background in the json data each time it is executed" do + expect(load_normalised_json(@out)).to eq MultiJson.load(%{ + [{"id": "banana-party", + "uri": "spec.feature", + "keyword": "Feature", + "name": "Banana party", + "line": 1, + "description": "", + "elements": + [{"keyword": "Background", + "name": "There are bananas", + "line": 3, + "description": "", + "type": "background", + "steps": + [{"keyword": "Given ", + "name": "there are bananas", + "line": 4, + "match": {"location": "spec.feature:4"}, + "result": {"status": "undefined"}}]}, + {"id": "banana-party;monkey-eats-bananas", + "keyword": "Scenario", + "name": "Monkey eats bananas", + "line": 6, + "description": "", + "type": "scenario", + "steps": + [{"keyword": "Then ", + "name": "the monkey eats bananas", + "line": 7, + "match": {"location": "spec.feature:7"}, + "result": {"status": "undefined"}}]}, + {"keyword": "Background", + "name": "There are bananas", + "line": 3, + "description": "", + "type": "background", + "steps": + [{"keyword": "Given ", + "name": "there are bananas", + "line": 4, + "match": {"location": "spec.feature:4"}, + "result": {"status": "undefined"}}]}, + {"id": "banana-party;monkey-eats-more-bananas", + "keyword": "Scenario", + "name": "Monkey eats more bananas", + "line": 9, + "description": "", + "type": "scenario", + "steps": + [{"keyword": "Then ", + "name": "the monkey eats more bananas", + "line": 10, + "match": {"location": "spec.feature:10"}, + "result": {"status": "undefined"}}]}]}]}) + end + end + + describe "with a scenario with a step that embeds data directly" do + define_feature <<-FEATURE + Feature: Banana party + + Scenario: Monkey eats bananas + Given there are bananas + FEATURE + + define_steps do + Given(/^there are bananas$/) { data = "YWJj" + embed data, "mime-type;base64" } + end + + it "includes the data from the step in the json data" do + expect(load_normalised_json(@out)).to eq MultiJson.load(%{ + [{"id": "banana-party", + "uri": "spec.feature", + "keyword": "Feature", + "name": "Banana party", + "line": 1, + "description": "", + "elements": + [{"id": "banana-party;monkey-eats-bananas", + "keyword": "Scenario", + "name": "Monkey eats bananas", + "line": 3, + "description": "", + "type": "scenario", + "steps": + [{"keyword": "Given ", + "name": "there are bananas", + "line": 4, + "embeddings": [{"mime_type": "mime-type", + "data": "YWJj"}], + "match": {"location": "spec/cucumber/formatter/json_spec.rb:460"}, + "result": {"status": "passed", + "duration": 1}}]}]}]}) + end + end + + describe "with a scenario with a step that embeds a file" do + define_feature <<-FEATURE + Feature: Banana party + + Scenario: Monkey eats bananas + Given there are bananas + FEATURE + + define_steps do + Given(/^there are bananas$/) { + RSpec::Mocks.allow_message(File, :file?) { true } + f1 = RSpec::Mocks::Double.new + RSpec::Mocks.allow_message(File, :open) { |&block| block.call(f1) } + RSpec::Mocks.allow_message(f1, :read) { "foo" } + embed('out/snapshot.jpeg', 'image/png') + } + end + + it "includes the file content in the json data" do + expect(load_normalised_json(@out)).to eq MultiJson.load(%{ + [{"id": "banana-party", + "uri": "spec.feature", + "keyword": "Feature", + "name": "Banana party", + "line": 1, + "description": "", + "elements": + [{"id": "banana-party;monkey-eats-bananas", + "keyword": "Scenario", + "name": "Monkey eats bananas", + "line": 3, + "description": "", + "type": "scenario", + "steps": + [{"keyword": "Given ", + "name": "there are bananas", + "line": 4, + "embeddings": [{"mime_type": "image/png", + "data": "Zm9v"}], + "match": {"location": "spec/cucumber/formatter/json_spec.rb:500"}, + "result": {"status": "passed", + "duration": 1}}]}]}]}) + end + end + + describe "with a scenario with hooks" do + define_feature <<-FEATURE + Feature: Banana party + + Scenario: Monkey eats bananas + Given there are bananas + FEATURE + + define_steps do + Before() {} + Before() {} + After() {} + After() {} + AfterStep() {} + AfterStep() {} + Around() { |scenario, block| block.call } + Given(/^there are bananas$/) {} + end + + it "includes all hooks except the around hook in the json data" do + expect(load_normalised_json(@out)).to eq MultiJson.load(%{ + [{"id": "banana-party", + "uri": "spec.feature", + "keyword": "Feature", + "name": "Banana party", + "line": 1, + "description": "", + "elements": + [{"id": "banana-party;monkey-eats-bananas", + "keyword": "Scenario", + "name": "Monkey eats bananas", + "line": 3, + "description": "", + "type": "scenario", + "before": + [{"match": {"location": "spec/cucumber/formatter/json_spec.rb:545"}, + "result": {"status": "passed", + "duration": 1}}, + {"match": {"location": "spec/cucumber/formatter/json_spec.rb:546"}, + "result": {"status": "passed", + "duration": 1}}], + "steps": + [{"keyword": "Given ", + "name": "there are bananas", + "line": 4, + "match": {"location": "spec/cucumber/formatter/json_spec.rb:552"}, + "result": {"status": "passed", + "duration": 1}, + "after": + [{"match": {"location": "spec/cucumber/formatter/json_spec.rb:549"}, + "result": {"status": "passed", + "duration": 1}}, + {"match": {"location": "spec/cucumber/formatter/json_spec.rb:550"}, + "result": {"status": "passed", + "duration": 1}}]}], + "after": + [{"match": {"location": "spec/cucumber/formatter/json_spec.rb:548"}, + "result": {"status": "passed", + "duration": 1}}, + {"match": {"location": "spec/cucumber/formatter/json_spec.rb:547"}, + "result": {"status": "passed", + "duration": 1}}]}]}]}) + end + end + + end + + def load_normalised_json(out) + normalise_json(MultiJson.load(out.string)) + end + + def normalise_json(json) + #make sure duration was captured (should be >= 0) + #then set it to what is "expected" since duration is dynamic + json.each do |feature| + elements = feature.fetch('elements') { [] } + elements.each do |scenario| + ['steps', 'before', 'after'].each do |type| + if scenario[type] + scenario[type].each do |step_or_hook| + normalise_json_step_or_hook(step_or_hook) + if step_or_hook['after'] + step_or_hook['after'].each do |hook| + normalise_json_step_or_hook(hook) + end + end + end + end + end + end + end + end + + def normalise_json_step_or_hook(step_or_hook) + if step_or_hook['result'] + if step_or_hook['result']['duration'] + expect(step_or_hook['result']['duration']).to be >= 0 + step_or_hook['result']['duration'] = 1 + end + end + end + + end + end +end