ugly_face 0.1

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