ugly_face 0.1

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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/ChangeLog +2 -0
  4. data/Gemfile +13 -0
  5. data/Guardfile +19 -0
  6. data/LICENSE +21 -0
  7. data/README.md +25 -0
  8. data/Rakefile +19 -0
  9. data/autotrader.png +0 -0
  10. data/cucumber.yml +4 -0
  11. data/features/feature_pages.feature +110 -0
  12. data/features/pretty_face_report.feature +81 -0
  13. data/features/step_definitions/report_steps.rb +36 -0
  14. data/features/support/_feature_header.erb +2 -0
  15. data/features/support/_suite_header.erb +2 -0
  16. data/features/support/env.rb +20 -0
  17. data/features/support/error_display.rb +16 -0
  18. data/features/support/hooks.rb +11 -0
  19. data/features/support/logo.png +0 -0
  20. data/fixtures/advanced.feature +57 -0
  21. data/fixtures/background.feature +10 -0
  22. data/fixtures/basic.feature +20 -0
  23. data/fixtures/failing_background.feature +7 -0
  24. data/fixtures/more/more.feature +8 -0
  25. data/fixtures/onemore/deep/more.feature +8 -0
  26. data/fixtures/onemore/more.feature +8 -0
  27. data/fixtures/step_definitions/advanced_steps.rb +34 -0
  28. data/fixtures/step_definitions/basic_steps.rb +25 -0
  29. data/fixtures/support/env.rb +3 -0
  30. data/lib/.DS_Store +0 -0
  31. data/lib/ugly_face/.DS_Store +0 -0
  32. data/lib/ugly_face/formatter/html.rb +289 -0
  33. data/lib/ugly_face/formatter/report.rb +285 -0
  34. data/lib/ugly_face/formatter/view_helper.rb +61 -0
  35. data/lib/ugly_face/templates/_main_header.erb +1 -0
  36. data/lib/ugly_face/templates/_page_header.erb +2 -0
  37. data/lib/ugly_face/templates/_step.erb +39 -0
  38. data/lib/ugly_face/templates/debug.png +0 -0
  39. data/lib/ugly_face/templates/failed.png +0 -0
  40. data/lib/ugly_face/templates/feature.erb +143 -0
  41. data/lib/ugly_face/templates/logo.png +0 -0
  42. data/lib/ugly_face/templates/main.erb +162 -0
  43. data/lib/ugly_face/templates/passed.png +0 -0
  44. data/lib/ugly_face/templates/pending.png +0 -0
  45. data/lib/ugly_face/templates/screenshot.png +0 -0
  46. data/lib/ugly_face/templates/skipped.png +0 -0
  47. data/lib/ugly_face/templates/style.css +346 -0
  48. data/lib/ugly_face/templates/table_failed.png +0 -0
  49. data/lib/ugly_face/templates/table_passed.png +0 -0
  50. data/lib/ugly_face/templates/table_pending.png +0 -0
  51. data/lib/ugly_face/templates/table_skipped.png +0 -0
  52. data/lib/ugly_face/templates/table_undefined.png +0 -0
  53. data/lib/ugly_face/templates/undefined.png +0 -0
  54. data/lib/ugly_face/version.rb +3 -0
  55. data/lib/ugly_face.rb +45 -0
  56. data/spec/lib/customization_spec.rb +23 -0
  57. data/spec/lib/html_formatter_spec.rb +140 -0
  58. data/spec/spec_helper.rb +5 -0
  59. data/ugly_face.gemspec +29 -0
  60. metadata +199 -0
@@ -0,0 +1,289 @@
1
+ require 'action_view'
2
+ require 'fileutils'
3
+ require 'cucumber/formatter/io'
4
+ require 'cucumber/formatter/duration'
5
+ require 'cucumber/formatter/console'
6
+ require 'cucumber/ast/scenario'
7
+ require 'cucumber/ast/table'
8
+ require 'cucumber/ast/outline_table'
9
+ require File.join(File.dirname(__FILE__), 'view_helper')
10
+ require File.join(File.dirname(__FILE__), 'report')
11
+
12
+ # Starting with ActionPack 4.1.1, the module Mime doesn't get initialized before it's needed by UglyFace and so
13
+ # it would blow up with errors about uninitialized constants. We need to explicitly load it to prevent this problem.
14
+ require 'action_dispatch/http/mime_type'
15
+
16
+ module UglyFace
17
+ module Formatter
18
+
19
+ class Html
20
+ include Cucumber::Formatter::Io
21
+ include Cucumber::Formatter::Duration
22
+ include Cucumber::Formatter::Console
23
+ include ViewHelper
24
+
25
+ attr_reader :report, :logo
26
+
27
+ def initialize(step_mother, path_or_io, options)
28
+ @path = path_or_io
29
+ set_path_and_file(path_or_io)
30
+ @path_to_erb = File.join(File.dirname(__FILE__), '..', 'templates')
31
+ @step_mother = step_mother
32
+ @options = options
33
+ # The expand option is set to true by RubyMine and cannot be turned off using the IDE. This option causes
34
+ # a test run while using this gem to terminate.
35
+ @options[:expand] = false unless @options.nil?
36
+ @report = Report.new
37
+ @img_id = 0
38
+ @logo = 'logo.png'
39
+ @delayed_messages = []
40
+ end
41
+
42
+ def set_path_and_file(path_or_io)
43
+ return if path_or_io.nil?
44
+ dir = File.dirname(path_or_io)
45
+ FileUtils.mkdir_p dir unless File.directory? dir
46
+ @io = ensure_io(path_or_io, 'html')
47
+ end
48
+
49
+ def embed(src, mime_type, label)
50
+ case(mime_type)
51
+ when /^image\/(png|gif|jpg|jpeg)/
52
+ embed_image(src, label)
53
+ end
54
+ end
55
+
56
+ def embed_image(src, label)
57
+ @report.current_scenario.image << src.split(separator).last
58
+ @report.current_scenario.image_label << label
59
+ @report.current_scenario.image_id << "img_#{@img_id}"
60
+ @img_id += 1
61
+ filename = "#{File.dirname(@path)}#{separator}images"
62
+ FileUtils.cp src, filename
63
+ end
64
+
65
+ def before_features(features)
66
+ make_output_directories
67
+ @tests_started = Time.now
68
+ end
69
+
70
+ def features_summary_file
71
+ parts = @io.path.split(separator)
72
+ parts[parts.length - 1]
73
+ end
74
+
75
+ def before_feature(feature)
76
+ @report.add_feature ReportFeature.new(feature, features_summary_file)
77
+ end
78
+
79
+ def after_feature(feature)
80
+ @report.current_feature.close(feature)
81
+ end
82
+
83
+ def before_background(background)
84
+ @report.begin_background
85
+ end
86
+
87
+ def after_background(background)
88
+ @report.end_background
89
+ @report.current_feature.background << ReportStep.new(background)
90
+ end
91
+
92
+ def before_feature_element(feature_element)
93
+ unless scenario_outline? feature_element
94
+ @report.add_scenario ReportScenario.new(feature_element)
95
+ end
96
+ end
97
+
98
+ def after_feature_element(feature_element)
99
+ unless scenario_outline?(feature_element)
100
+ process_scenario(feature_element)
101
+ end
102
+ end
103
+
104
+ def before_table_row(example_row)
105
+ @report.add_scenario ReportScenario.new(example_row) unless info_row?(example_row)
106
+ end
107
+
108
+ def after_table_row(example_row)
109
+ unless info_row?(example_row)
110
+ @report.current_scenario.populate(example_row)
111
+ build_scenario_outline_steps(example_row)
112
+ end
113
+ populate_cells(example_row) if example_row.instance_of? Cucumber::Ast::Table::Cells
114
+ end
115
+
116
+ def before_step(step)
117
+ @step_timer = Time.now
118
+ end
119
+
120
+ def after_step(step)
121
+ step = process_step(step) unless step_belongs_to_outline? step
122
+ if @cells
123
+ step.table = @cells
124
+ @cells = nil
125
+ end
126
+ end
127
+
128
+ def after_features(features)
129
+ @features = features
130
+ @duration = format_duration(Time.now - @tests_started)
131
+ copy_images
132
+ copy_stylesheets
133
+ generate_report
134
+ end
135
+
136
+ def features
137
+ @report.features
138
+ end
139
+
140
+ def custom_suite_header?
141
+ return false unless customization_directory
142
+
143
+ Dir.foreach(customization_directory) do |file|
144
+ return true if file == '_suite_header.erb'
145
+ end
146
+ false
147
+ end
148
+
149
+ def custom_feature_header?
150
+ return false unless customization_directory
151
+
152
+ Dir.foreach(customization_directory) do |file|
153
+ return true if file == '_feature_header.erb'
154
+ end
155
+ false
156
+ end
157
+
158
+ private
159
+
160
+ def generate_report
161
+ paths = [@path_to_erb, customization_directory.to_s]
162
+ renderer = ActionView::Base.new(paths)
163
+ filename = File.join(@path_to_erb, 'main')
164
+ @io.puts renderer.render(:file => filename, :locals => {:report => self, :logo => @logo})
165
+ features.each do |feature|
166
+ write_feature_file(feature)
167
+ end
168
+ end
169
+
170
+ def write_feature_file(feature)
171
+ paths = [@path_to_erb, customization_directory.to_s]
172
+ renderer = ActionView::Base.new(paths)
173
+ filename = File.join(@path_to_erb, 'feature')
174
+ output_file = "#{File.dirname(@path)}#{separator}#{feature.file}"
175
+ to_cut = output_file.split(separator).last
176
+ directory = output_file.sub("#{separator}#{to_cut}", '')
177
+ FileUtils.mkdir_p directory unless File.directory? directory
178
+ file = File.new(output_file, Cucumber.file_mode('w'))
179
+ file.puts renderer.render(:file => filename, :locals => {:feature => feature, :logo => @logo, :customize => custom_feature_header?})
180
+ file.flush
181
+ file.close
182
+ end
183
+
184
+ def make_output_directories
185
+ make_directory 'images'
186
+ make_directory 'stylesheets'
187
+ end
188
+
189
+ def make_directory(dir)
190
+ path = "#{File.dirname(@path)}#{separator}#{dir}"
191
+ FileUtils.mkdir_p path unless File.directory? path
192
+ end
193
+
194
+ def copy_directory(dir, file_names, file_extension)
195
+ path = "#{File.dirname(@path)}#{separator}#{dir}"
196
+ file_names.each do |file|
197
+ copy_file File.join(File.dirname(__FILE__), '..', 'templates', "#{file}.#{file_extension}"), path
198
+ end
199
+ end
200
+
201
+ def copy_file(source, destination)
202
+ FileUtils.cp source, destination
203
+ end
204
+
205
+ def copy_images
206
+ copy_directory 'images', %w(debug screenshot failed passed pending undefined skipped table_failed table_passed table_pending table_undefined table_skipped), "png"
207
+ logo = logo_file
208
+ copy_file logo, "#{File.join(File.dirname(@path), 'images')}" if logo
209
+ copy_directory 'images', ['custom_logo'], 'png' unless logo
210
+ end
211
+
212
+ def copy_stylesheets
213
+ copy_directory 'stylesheets', ['style'], 'css'
214
+ end
215
+
216
+ def logo_file
217
+ dir = customization_directory
218
+ Dir.foreach(dir) do |file|
219
+ if file =~ /^logo\.(png|gif|jpg|jpeg)$/
220
+ @logo = file
221
+ return File.join(dir, file)
222
+ end
223
+ end if dir
224
+ end
225
+
226
+ def customization_directory
227
+ dir = File.join(File.expand_path('features'), 'support', 'ugly_face')
228
+ return dir if File.exists? dir
229
+ end
230
+
231
+ def process_scenario(scenario)
232
+ @report.current_scenario.populate(scenario)
233
+ end
234
+
235
+ def process_step(step, status=nil)
236
+ duration = Time.now - @step_timer
237
+ report_step = ReportStep.new(step)
238
+ report_step.duration = duration
239
+ report_step.status = status unless status.nil?
240
+ if step.background?
241
+ @report.current_feature.background << report_step if @report.processing_background_steps?
242
+ else
243
+ @report.add_step report_step
244
+ end
245
+ report_step
246
+ end
247
+
248
+ def scenario_outline?(feature_element)
249
+ feature_element.is_a? Cucumber::Ast::ScenarioOutline
250
+ end
251
+
252
+ def info_row?(example_row)
253
+ return example_row.scenario_outline.nil? if example_row.respond_to? :scenario_outline
254
+ return true if example_row.instance_of? Cucumber::Ast::Table::Cells
255
+ false
256
+ end
257
+
258
+ def step_belongs_to_outline?(step)
259
+ scenario = step.instance_variable_get "@feature_element"
260
+ not scenario.nil?
261
+ end
262
+
263
+ def build_scenario_outline_steps(example_row)
264
+ si = example_row.instance_variable_get :@step_invocations
265
+ si.each do |row|
266
+ process_step(row, row.status)
267
+ end
268
+ end
269
+
270
+ def step_error(exception)
271
+ return nil if exception.nil?
272
+ exception.backtrace[-1] =~ /^#{step.file_colon_line}/ ? exception : nil
273
+ end
274
+
275
+ def populate_cells(example_row)
276
+ @cells ||= []
277
+ values = []
278
+ example_row.to_a.each do |cell|
279
+ values << cell.value
280
+ end
281
+ @cells << values
282
+ end
283
+
284
+ def separator
285
+ File::ALT_SEPARATOR || File::SEPARATOR
286
+ end
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,285 @@
1
+ module UglyFace
2
+ module Formatter
3
+
4
+ module Formatting
5
+ def summary_percent(number, total)
6
+ percent = (number.to_f / total) * 100
7
+ "#{number} <span class=\"percentage\">(#{'%.1f' % percent}%)</span>"
8
+ end
9
+
10
+ def formatted_duration(duration)
11
+ m, s = duration.divmod(60)
12
+ "#{m}m#{'%.3f' % s}s"
13
+ rescue
14
+ "N m Ns"
15
+ end
16
+
17
+ def image_tag_for(status, source=nil)
18
+ dir = "#{directory_prefix_for(source)}images"
19
+ "<img src=\"#{dir}/#{status}.png\" alt=\"#{status}\" title=\"#{status}\">"
20
+ end
21
+
22
+ def table_image_for(status, source=nil)
23
+ dir = "#{directory_prefix_for(source)}images"
24
+ "<img src=\"#{dir}/table_#{status}.png\" alt=\"#{status}\" title=\"#{status}\">"
25
+
26
+ end
27
+
28
+ def directory_prefix_for(source=nil)
29
+ dir = ''
30
+ back_dir = source.count(separator) if source
31
+ back_dir.times do
32
+ dir += "..#{separator}"
33
+ end
34
+ dir
35
+ end
36
+
37
+ def separator
38
+ File::ALT_SEPARATOR || File::SEPARATOR
39
+ end
40
+ end
41
+
42
+
43
+ class Report
44
+ attr_reader :features
45
+
46
+ def initialize
47
+ @features = []
48
+ end
49
+
50
+ def current_feature
51
+ @features.last
52
+ end
53
+
54
+ def current_scenario
55
+ current_feature.scenarios.last
56
+ end
57
+
58
+ def add_feature(feature)
59
+ @features << feature
60
+ end
61
+
62
+ def add_scenario(scenario)
63
+ current_feature.scenarios << scenario
64
+ end
65
+
66
+ def begin_background
67
+ @processing_background = true
68
+ end
69
+
70
+ def end_background
71
+ @processing_background = false
72
+ end
73
+
74
+ def processing_background_steps?
75
+ @processing_background
76
+ end
77
+
78
+ def add_step(step)
79
+ current_scenario.steps << step
80
+ end
81
+ end
82
+
83
+ class ReportFeature
84
+ include Formatting
85
+ attr_accessor :scenarios, :background, :description
86
+ attr_reader :title, :file, :start_time, :duration, :parent_filename
87
+
88
+ def initialize(feature, parent_filename)
89
+ @scenarios = []
90
+ @background = []
91
+ @start_time = Time.now
92
+ @description = feature.description
93
+ @parent_filename = parent_filename
94
+ end
95
+
96
+ def close(feature)
97
+ @title = feature.title
98
+ @duration = Time.now - start_time
99
+ a_file = feature.file.sub(/\.feature/, '.html')
100
+ to_cut = a_file.split(separator).first
101
+ @file = a_file.sub("#{to_cut}#{separator}", '')
102
+ end
103
+
104
+ def steps
105
+ steps = []
106
+ scenarios.each { |scenario| steps += scenario.steps }
107
+ steps
108
+ end
109
+
110
+ def background_title
111
+ title = @background.find { |step| step.keyword.nil? }
112
+ end
113
+
114
+ def background_steps
115
+ @background.find_all { |step| step.keyword }
116
+ end
117
+
118
+ def scenarios_for(status)
119
+ scenarios.find_all { |scenario| scenario.status == status }
120
+ end
121
+
122
+ def scenario_summary_for(status)
123
+ scenarios_with_status = scenarios_for(status)
124
+ summary_percent(scenarios_with_status.length, scenarios.length)
125
+ end
126
+
127
+ def step_summary_for(status)
128
+ steps_with_status = steps.find_all { |step| step.status == status }
129
+ summary_percent(steps_with_status.length, steps.length)
130
+ end
131
+
132
+ def scenario_average_duration
133
+ durations = scenarios.collect { |scenario| scenario.duration }
134
+ formatted_duration(durations.reduce(:+).to_f / durations.size)
135
+ end
136
+
137
+ def step_average_duration
138
+ steps = scenarios.collect { |scenario| scenario.steps }
139
+ durations = steps.flatten.collect { |step| step.duration }
140
+ formatted_duration(durations.reduce(:+).to_f / durations.size)
141
+ end
142
+
143
+ def get_binding
144
+ binding
145
+ end
146
+
147
+ def description?
148
+ !description.nil? && !description.empty?
149
+ end
150
+
151
+ def has_background?
152
+ background.length > 0
153
+ end
154
+
155
+ def file
156
+ @file.split("features#{separator}").last
157
+ end
158
+
159
+ def parent_filename
160
+ @parent_filename.split(separator).last
161
+ end
162
+ end
163
+
164
+ class ReportScenario
165
+ attr_accessor :name, :file_colon_line, :status, :steps, :duration, :image, :image_label, :image_id
166
+
167
+ def initialize(scenario)
168
+ @steps = []
169
+ @image = []
170
+ @image_label = []
171
+ @image_id = []
172
+ @start = Time.now
173
+ end
174
+
175
+ def populate(scenario)
176
+ @duration = Time.now - @start
177
+ if scenario.instance_of? Cucumber::Ast::Scenario
178
+ @name = scenario.name
179
+ @file_colon_line = scenario.file_colon_line
180
+ elsif scenario.instance_of? Cucumber::Ast::OutlineTable::ExampleRow
181
+ @name = scenario.scenario_outline.name
182
+ @file_colon_line = scenario.backtrace_line
183
+ end
184
+ @status = scenario.status
185
+ end
186
+
187
+ def has_image?
188
+ not image.nil?
189
+ end
190
+ end
191
+
192
+ class ReportStep
193
+ attr_accessor :name, :keyword, :file_colon_line, :status, :duration, :table, :multiline_arg, :error
194
+
195
+ def initialize(step)
196
+ @name = step.name
197
+ @file_colon_line = step.file_colon_line
198
+ unless step.instance_of? Cucumber::Ast::Background
199
+ if step.respond_to? :actual_keyword
200
+ @keyword = step.actual_keyword
201
+ else
202
+ @keyword = step.keyword
203
+ end
204
+ @status = step.status
205
+ @multiline_arg = step.multiline_arg
206
+ @error = step.exception
207
+ end
208
+ end
209
+
210
+ def failed_with_error?
211
+ status == :failed && !error.nil?
212
+ end
213
+
214
+ def has_table?
215
+ not table.nil?
216
+ end
217
+
218
+ def has_multiline_arg?
219
+ !multiline_arg.nil? && !has_table?
220
+ end
221
+
222
+ def file_with_error(file_colon_line)
223
+ @snippet_extractor ||= SnippetExtractor.new
224
+ file, line = @snippet_extractor.file_name_and_line(file_colon_line)
225
+ file
226
+ end
227
+
228
+ #from cucumber ===================
229
+ def extra_failure_content(file_colon_line)
230
+ @snippet_extractor ||= SnippetExtractor.new
231
+ @snippet_extractor.snippet(file_colon_line)
232
+ end
233
+
234
+ class SnippetExtractor
235
+ require 'syntax/convertors/html';
236
+ @@converter = Syntax::Convertors::HTML.for_syntax "ruby"
237
+
238
+ def file_name_and_line(error_line)
239
+ if error_line =~ /(.*):(\d+)/
240
+ [$1, $2.to_i]
241
+ end
242
+ end
243
+
244
+ def snippet(error)
245
+ raw_code, line, file = snippet_for(error[0])
246
+ highlighted = @@converter.convert(raw_code, false)
247
+
248
+ "<pre class=\"ruby\"><strong>#{file + "\n"}</strong><code>#{post_process(highlighted, line)}</code></pre>"
249
+ end
250
+
251
+ def snippet_for(error_line)
252
+ file, line = file_name_and_line(error_line)
253
+ if file
254
+ [lines_around(file, line), line, file]
255
+ else
256
+ ["# Couldn't get snippet for #{error_line}", 1, 'File Unknown']
257
+ end
258
+ end
259
+
260
+ def lines_around(file, line)
261
+ if File.file?(file)
262
+ # lines = File.open(file).read.split("\n")
263
+ lines = File.readlines(file)
264
+ min = [0, line-3].max
265
+ max = [line+1, lines.length-1].min
266
+ # lines[min..max].join("\n")
267
+ lines[min..max].join
268
+ else
269
+ "# Couldn't get snippet for #{file}"
270
+ end
271
+ end
272
+
273
+ def post_process(highlighted, offending_line)
274
+ new_lines = []
275
+ highlighted.split("\n").each_with_index do |line, i|
276
+ new_line = "<span class=\"linenum\">#{offending_line+i-2}</span>#{line}"
277
+ new_line = "<span class=\"offending\">#{new_line}</span>" if i == 2
278
+ new_lines << new_line
279
+ end
280
+ new_lines.join("\n")
281
+ end
282
+ end
283
+ end
284
+ end
285
+ end
@@ -0,0 +1,61 @@
1
+ require 'cucumber/ast/scenario_outline'
2
+
3
+ module UglyFace
4
+ module Formatter
5
+ module ViewHelper
6
+
7
+ def start_time
8
+ @tests_started.strftime("%a %B %-d, %Y at %H:%M:%S")
9
+ end
10
+
11
+ def step_count
12
+ @step_mother.steps.length
13
+ end
14
+
15
+ def scenario_count
16
+ @step_mother.scenarios.length
17
+ end
18
+
19
+ def total_duration
20
+ @duration
21
+ end
22
+
23
+ def step_average_duration(features)
24
+ scenarios = features.collect { |feature| feature.scenarios }
25
+ steps = scenarios.flatten.collect { |scenario| scenario.steps }
26
+ durations = steps.flatten.collect { |step| step.duration }
27
+ format_duration get_average_from_float_array durations
28
+ end
29
+
30
+ def scenario_average_duration(features)
31
+ scenarios = features.collect { |feature| feature.scenarios }
32
+ durations = scenarios.flatten.collect { |scenario| scenario.duration }
33
+ format_duration get_average_from_float_array durations
34
+ end
35
+
36
+ def scenarios_summary_for(status)
37
+ summary_percent(@step_mother.scenarios(status).length, scenario_count)
38
+ end
39
+
40
+ def steps_summary_for(status)
41
+ summary_percent(@step_mother.steps(status).length, step_count)
42
+ end
43
+
44
+ def failed_scenario?(scenario)
45
+ scenario.status == :failed
46
+ end
47
+
48
+
49
+ private
50
+
51
+ def get_average_from_float_array(arr)
52
+ arr.reduce(:+).to_f / arr.size
53
+ end
54
+
55
+ def summary_percent(number, total)
56
+ percent = (number.to_f / total) * 100
57
+ "#{number} <span class=\"percentage\">(#{'%.1f' % percent}%)</span>"
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1 @@
1
+ <h2 class="results">Run Results</h2>
@@ -0,0 +1,2 @@
1
+ <h2 class="results">Feature Results: <%= feature.title %></h2>
2
+ <br />
@@ -0,0 +1,39 @@
1
+ <%= "#{step.keyword} #{step.name}" %>
2
+ <% if step.has_table? %>
3
+ <br />
4
+ <table border="1" class="param_table">
5
+ <% step.table.each do |row| %>
6
+ <tr>
7
+ <% row.each_with_index do |column, index| %>
8
+ <% if index == 0 %>
9
+ <th><%= column %></th>
10
+ <% else %>
11
+ <td><%= column %></td>
12
+ <% end %>
13
+ <% end %>
14
+ </tr>
15
+ <% end %>
16
+ </table>
17
+ <% end %>
18
+ <% if step.has_multiline_arg? %>
19
+ <br />
20
+ <table border="1" class="multiline_arg">
21
+ <tr>
22
+ <td><pre><%= step.multiline_arg %></pre></td>
23
+ </tr>
24
+ </table>
25
+ <% end %>
26
+ <% if step.failed_with_error? %>
27
+ <table>
28
+ <tr class="error">
29
+ <td class="message">
30
+ <strong><pre><%= "#{step.error.message} (#{step.error.class})" %></pre></strong>
31
+ </td>
32
+ </tr>
33
+ <tr>
34
+ <td>
35
+ <%= raw(step.extra_failure_content(step.error.backtrace)) %>
36
+ </td>
37
+ </tr>
38
+ </table>
39
+ <% end %>
Binary file
Binary file