report_builder 0.1.4 → 0.1.5

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.
@@ -1,50 +1,8 @@
1
- require 'json'
2
- require 'builder'
3
- require 'base64'
1
+ require 'report_builder/builder'
4
2
 
5
- # Add except method to Hash
6
- class Hash
7
- def except(*keys)
8
- dup.except!(*keys)
9
- end
10
-
11
- def except!(*keys)
12
- keys.each { |key| delete(key) }
13
- self
14
- end
15
- end
16
-
17
- # Main report builder class
18
- class ReportBuilder
19
-
20
- # report_builder:
21
- #
22
- # ReportBuilder.configure do |config|
23
- # config.json_path = 'cucumber_sample/logs'
24
- # config.report_path = 'my_test_report'
25
- # config.report_types = [:json, :html]
26
- # config.report_tabs = [:overview, :features, :scenarios, :errors]
27
- # config.report_title = 'My Test Results'
28
- # config.compress_images = false
29
- # config.additional_info = {browser: 'Chrome', environment: 'Stage 5'}
30
- # end
31
- #
32
- # ReportBuilder.build_report
33
- #
34
-
35
- # colors corresponding to status
36
- COLOR = {
37
- passed: '#90ed7d',
38
- working: '#90ed7d',
39
- failed: '#f45b5b',
40
- broken: '#f45b5b',
41
- undefined: '#e4d354',
42
- incomplete: '#e7a35c',
43
- pending: '#f7a35c',
44
- skipped: '#7cb5ec',
45
- output: '#007fff'
46
- }
47
3
 
4
+ module ReportBuilder
5
+
48
6
  #
49
7
  # Ex: ReportBuilder.configure do |config|
50
8
  # config.json_path = 'cucumber_sample/logs'
@@ -57,17 +15,9 @@ class ReportBuilder
57
15
  # end
58
16
  #
59
17
  def self.configure
60
- default_options = OpenStruct.new(
61
- json_path: nil, # [String] / [Array] Input json file, array of json files/path or json files path, (Default current directory)
62
- report_path: 'test_report', # [String] Output file path with name
63
- report_types: [:html], # [Array] Output file types to build, [:json, :html] or ['html', 'json']
64
- report_tabs: [:overview, :features], # [Array] Tabs to build, [:overview, :features, :scenarios, :errors] or ['overview', 'features', 'scenarios', 'errors']
65
- report_title: 'Test Results', # [String] Report and html title
66
- compress_images: false, # [Boolean] Set true to reducing the size of HTML report, Note: If true, takes more time to build report
67
- additional_info: {} # [Hash] Additional info for report summary
68
- )
69
- yield default_options if block_given?
70
- @options = default_options.marshal_dump
18
+ defaults = builder.default_options
19
+ yield defaults if block_given?
20
+ builder.options = defaults.marshal_dump
71
21
  end
72
22
 
73
23
  #
@@ -86,617 +36,11 @@ class ReportBuilder
86
36
  # ReportBuilder.build_report options
87
37
  #
88
38
  def self.build_report(options = nil)
89
-
90
- configure unless @options
91
- @options.merge! options if options.is_a? Hash
92
-
93
- raise 'Error: Invalid report_types Use: [:json, :html]' unless @options[:report_types].is_a? Array
94
- raise 'Error: Invalid report_tabs Use: [:overview, :features, :scenarios, :errors]' unless @options[:report_tabs].is_a? Array
95
-
96
- @options[:report_types].map!(&:to_s).map!(&:upcase)
97
- @options[:report_tabs].map!(&:to_s).map!(&:downcase)
98
-
99
- input = files @options[:json_path]
100
- all_features = features input rescue (raise 'ReportBuilderParsingError')
101
-
102
- File.open(@options[:report_path] + '.json', 'w') do |file|
103
- file.write JSON.pretty_generate all_features
104
- puts "JSON test report generated: '#{@options[:report_path]}.json'"
105
- end if @options[:report_types].include? 'JSON'
106
-
107
- all_scenarios = scenarios all_features
108
- all_steps = steps all_scenarios
109
- all_tags = tags all_scenarios
110
- total_time = total_time all_features
111
- feature_data = data all_features
112
- scenario_data = data all_scenarios
113
- step_data = data all_steps
114
-
115
- File.open(@options[:report_path] + '.html', 'w:UTF-8') do |file|
116
- @builder = Builder::XmlMarkup.new(target: file, indent: 0)
117
- @builder.declare!(:DOCTYPE, :html)
118
- @builder << '<html>'
119
-
120
- @builder.head do
121
- @builder.meta(charset: 'UTF-8')
122
- @builder.title @options[:report_title]
123
-
124
- @builder.style(type: 'text/css') do
125
- @builder << File.read(File.dirname(__FILE__) + '/../vendor/assets/stylesheets/jquery-ui.min.css')
126
- COLOR.each do |color|
127
- @builder << ".#{color[0].to_s}{background:#{color[1]};color:#434348;padding:2px}"
128
- end
129
- @builder << '.summary{margin-bottom:4px;border: 1px solid #c5c5c5;border-radius:4px;background:#f1f1f1;color:#434348;padding:4px;overflow:hidden;vertical-align:bottom;}'
130
- @builder << '.summary .results{text-align:right;float:right;}'
131
- @builder << '.summary .info{text-align:left;float:left;}'
132
- @builder << '.data_table{border-collapse: collapse;} .data_table td{padding: 5px; border: 1px solid #ddd;}'
133
- @builder << '.ui-tooltip{background: black; color: white; font-size: 12px; padding: 2px 4px; border-radius: 20px; box-shadow: 0 0 7px black;}'
134
- end
135
-
136
- @builder.script(type: 'text/javascript') do
137
- %w(jquery-min jquery-ui.min highcharts highcharts-3d).each do |js|
138
- @builder << File.read(File.dirname(__FILE__) + '/../vendor/assets/javascripts/' + js + '.js')
139
- end
140
- @builder << '$(function(){$("#results").tabs();});'
141
- @builder << "$(function(){$('#features').accordion({collapsible: true, heightStyle: 'content', active: false, icons: false});});"
142
- (0..all_features.size).each do |n|
143
- @builder << "$(function(){$('#feature#{n}').accordion({collapsible: true, heightStyle: 'content', active: false, icons: false});});"
144
- end
145
- @builder << "$(function(){$('#status').accordion({collapsible: true, heightStyle: 'content', active: false, icons: false});});"
146
- scenario_data.each do |data|
147
- @builder << "$(function(){$('##{data[:name]}').accordion({collapsible: true, heightStyle: 'content', active: false, icons: false});});"
148
- end
149
- @builder << '$(function() {$(document).tooltip({track: true});});'
150
- end
151
- end
152
-
153
- @builder << '<body>'
154
-
155
- @builder.div(class: 'summary') do
156
- @builder.span(class: 'info') do
157
- info = @options[:additional_info].empty?
158
- @builder << '<br/>&nbsp;&nbsp;&nbsp;' if info
159
- @builder.span(style: "font-size:#{info ? 36 : 18 }px;font-weight: bold;") do
160
- @builder << @options[:report_title]
161
- end
162
- @options[:additional_info].each do |l|
163
- @builder << '<br/>' + l[0].to_s.capitalize + ' : ' + l[1].to_s
164
- end
165
- end if @options[:additional_info].is_a? Hash
166
- @builder.span(class: 'results') do
167
- s = all_features.size
168
- @builder << s.to_s + " feature#{'s' if s > 1} ("
169
- feature_data.each do |data|
170
- @builder << ' ' + data[:count].to_s + ' ' + data[:name]
171
- end
172
- s = all_scenarios.size
173
- @builder << ')<br/>' + s.to_s + " scenario#{'s' if s > 1} ("
174
- scenario_data.each do |data|
175
- @builder << ' ' + data[:count].to_s + ' ' + data[:name]
176
- end
177
- s = all_steps.size
178
- @builder << ')<br/>' + s.to_s + " step#{'s' if s > 1} ("
179
- step_data.each do |data|
180
- @builder << ' ' + data[:count].to_s + ' ' + data[:name]
181
- end
182
- @builder << ')<br/>&#128336; ' + duration(total_time).to_s
183
- end
184
- end
185
-
186
- @builder.div(id: 'results') do
187
- build_menu @options[:report_tabs]
188
-
189
- @builder.div(id: 'overviewTab') do
190
- @builder << "<div id='featurePieChart' style=\"float:left;width:33%\"></div>"
191
- @builder << "<div id='scenarioPieChart' style=\"display:inline-block;width:33%\"></div>"
192
- @builder << "<div id='stepPieChart' style=\"float:right;width:33%\"></div>"
193
- end if @options[:report_tabs].include? 'overview'
194
-
195
- @builder.div(id: 'featuresTab') do
196
- build_tags_drop_down(all_tags)
197
- @builder.div(id: 'features') do
198
- all_features.each_with_index do |feature, n|
199
- @builder.h3(style: "background:#{COLOR[feature['status'].to_sym]}") do
200
- @builder.span(class: feature['status']) do
201
- @builder << "<strong>#{feature['keyword']}</strong> #{feature['name']} (#{duration(feature['duration'])})"
202
- end
203
- end
204
- @builder.div do
205
- @builder.div(id: "feature#{n}") do
206
- feature['elements'].each{|scenario| build_scenario scenario}
207
- end
208
- end
209
- end
210
- end
211
- @builder << "<div id='featureTabPieChart'></div>"
212
- end if @options[:report_tabs].include? 'features'
213
-
214
- @builder.div(id: 'scenariosTab') do
215
- build_tags_drop_down(all_tags)
216
- @builder.div(id: 'status') do
217
- all_scenarios.group_by{|scenario| scenario['status']}.each do |data|
218
- @builder.h3(style: "background:#{COLOR[data[0].to_sym]}") do
219
- @builder.span(class: data[0]) do
220
- @builder << "<strong>#{data[0].capitalize} scenarios (Count: <span id='count'>#{data[1].size}</span>)</strong>"
221
- end
222
- end
223
- @builder.div do
224
- @builder.div(id: data[0]) do
225
- data[1].sort_by{|scenario| scenario['name']}.each{|scenario| build_scenario scenario}
226
- end
227
- end
228
- end
229
- end
230
- @builder << "<div id='scenarioTabPieChart'></div>"
231
- end if @options[:report_tabs].include? 'scenarios'
232
-
233
- @builder.div(id: 'errorsTab') do
234
- @builder.ol do
235
- all_scenarios.each{|scenario| build_error_list scenario}
236
- end
237
- end if @options[:report_tabs].include? 'errors'
238
- end
239
-
240
- @builder.script(type: 'text/javascript') do
241
- @builder << pie_chart_js('featurePieChart', 'Features', feature_data) if @options[:report_tabs].include? 'overview'
242
- @builder << donut_js('featureTabPieChart', 'Features', feature_data) if @options[:report_tabs].include? 'features'
243
- @builder << pie_chart_js('scenarioPieChart', 'Scenarios', scenario_data) if @options[:report_tabs].include? 'overview'
244
- @builder << donut_js('scenarioTabPieChart', 'Scenarios', scenario_data) if @options[:report_tabs].include? 'scenarios'
245
- @builder << pie_chart_js('stepPieChart', 'Steps', step_data) if @options[:report_tabs].include? 'overview'
246
- unless all_tags.empty?
247
- @builder << '$("#featuresTab .select-tags").change(function(){
248
- $("#featuresTab .scenario-all").hide().next().hide().parent().hide().parent().hide().prev().hide();
249
- $("#featuresTab ." + $(this).val()).show().parent().show().parent().prev().show();});' if @options[:report_tabs].include? 'features'
250
- @builder << '$("#scenariosTab .select-tags").change(function(){var val = $(this).val();$("#scenariosTab .scenario-all").hide().next().hide();
251
- $("#scenariosTab ." + val).show();$("#scenariosTab #count").each(function(){status = $(this).parent().parent().prop("className");
252
- count = $("#scenariosTab #" + status + " ." + val).length;countElement = $("#scenariosTab ." + status + " #count");
253
- countElement.parent().parent().parent().show();if(count==0){countElement.parent().parent().parent().hide().next().hide();}
254
- countElement.html(count);});});' if @options[:report_tabs].include? 'scenarios'
255
- end
256
- end
257
-
258
- @builder << '</body>'
259
- @builder << '</html>'
260
-
261
- puts "HTML test report generated: '#{@options[:report_path]}.html'"
262
- end if @options[:report_types].include? 'HTML'
263
-
264
- File.open(@options[:report_path] + '.retry', 'w:UTF-8') do |file|
265
- all_features.each do |feature|
266
- if feature['status'] == 'broken'
267
- feature['elements'].each { |scenario| file.puts "#{feature['uri']}:#{scenario['line']}" if scenario['status'] == 'failed' }
268
- end
269
- end
270
- end if @options[:report_types].include? 'RETRY'
271
-
272
- [total_time, feature_data, scenario_data, step_data]
273
- end
274
-
275
- def self.build_menu(tabs)
276
- @builder.ul do
277
- tabs.each do |tab|
278
- @builder.li do
279
- @builder.a(href: "##{tab}Tab") do
280
- @builder << tab.capitalize
281
- end
282
- end
283
- end
284
- end
285
- end
286
-
287
- def self.build_scenario(scenario)
288
- tags = (scenario['tags'] ? scenario['tags'].map{|tag| tag['name']}.join(' ') : '')
289
- @builder.h3(style: "background:#{COLOR[scenario['status'].to_sym]}", title: tags, class: 'scenario-all ' + tags.gsub('@','tag-')) do
290
- @builder.span(class: scenario['status']) do
291
- @builder << "<strong>#{scenario['keyword']}</strong> #{scenario['name']} (#{duration(scenario['duration'])})"
292
- end
293
- end
294
- @builder.div do
295
- scenario['before'].each do |before|
296
- build_hook_error before
297
- end
298
- scenario['steps'].each do |step|
299
- build_step step, scenario['keyword']
300
- end
301
- scenario['after'].each do |after|
302
- build_output after['output']
303
- build_hook_error after
304
- build_embedding after['embeddings']
305
- end
306
- end
307
- end
308
-
309
- def self.build_step(step, scenario_keyword)
310
- @builder.div(class: step['status']) do
311
- @builder << "<strong>#{step['keyword']}</strong> #{step['name']} (#{duration(step['duration'])})"
312
- end
313
- build_data_table step['rows']
314
- build_output step['output']
315
- build_step_error step
316
- build_embedding step['embeddings']
317
- step['after'].each do |after|
318
- build_output after['output']
319
- build_step_hook_error after, scenario_keyword
320
- build_embedding after['embeddings']
321
- end if step['after']
322
- end
323
-
324
- def self.build_data_table(rows)
325
- @builder.table(class: 'data_table', style: 'margin: 10px') do
326
- rows.each do |row|
327
- @builder.tr do
328
- row['cells'].each do |cell|
329
- @builder << "<td> #{cell} </td>"
330
- end
331
- end
332
- end
333
- end if rows.is_a? Array
334
- end
335
-
336
- def self.build_output(outputs)
337
- outputs.each do |output|
338
- @builder << "<span style='color:#{COLOR[:output]}'>#{output.to_s.gsub("\n",'</br>').gsub("\t",'&nbsp;&nbsp;').gsub(' ','&nbsp;')}</span><br/>"
339
- end if outputs.is_a?(Array)
340
- end
341
-
342
- def self.build_tags_drop_down(tags)
343
- @builder.div(style: 'text-align:center;padding:5px;') do
344
- @builder << '<strong>Tag: </strong>'
345
- @builder.select(class: 'select-tags') do
346
- @builder.option(value: 'scenario-all') do
347
- @builder << 'All'
348
- end
349
- tags.sort.each do |tag|
350
- @builder.option(value: tag.gsub('@','tag-')) do
351
- @builder << tag
352
- end
353
- end
354
- end
355
- end if tags.is_a?(Array)
356
- end
357
-
358
- def self.build_step_error(step)
359
- if step['status'] == 'failed' && step['result']['error_message']
360
- @builder << "<strong style=color:#{COLOR[:failed]}>Error: </strong>"
361
- error = step['result']['error_message'].split("\n")
362
- @builder.span(style: "color:#{COLOR[:failed]}") do
363
- error[0..-3].each do |line|
364
- @builder << line + '<br/>'
365
- end
366
- end
367
- @builder << "<strong>SD: </strong>#{error[-2]} <br/>"
368
- @builder << "<strong>FF: </strong>#{error[-1]}<br/>"
369
- end
370
- end
371
-
372
- def self.build_hook_error(hook)
373
- if hook['status'] == 'failed'
374
- @builder << "<strong style=color:#{COLOR[:failed]}>Error: </strong>"
375
- error = hook['result']['error_message'].split("\n")
376
- @builder.span(style: "color:#{COLOR[:failed]}") do
377
- error[0..-2].each do |line|
378
- @builder << line + '<br/>'
379
- end
380
- end
381
- @builder << "<strong>Hook: </strong>#{error[-1]}<br/>"
382
- end
383
- end
384
-
385
- def self.build_step_hook_error(hook, scenario_keyword)
386
- if hook['result']['error_message']
387
- @builder << "<strong style=color:#{COLOR[:failed]}>Error: </strong>"
388
- error = hook['result']['error_message'].split("\n")
389
- @builder.span(style: "color:#{COLOR[:failed]}") do
390
- (scenario_keyword == 'Scenario Outline' ? error[0..-8] : error[0..-5]).each do |line|
391
- @builder << line + '<br/>'
392
- end
393
- end
394
- @builder << "<strong>Hook: </strong>#{scenario_keyword == 'Scenario Outline' ? error[-7] : error[-4]} <br/>"
395
- @builder << "<strong>FF: </strong>#{error[-2]}<br/>"
396
- end
397
- end
398
-
399
- def self.build_embedding(embeddings)
400
- @embedding_count ||= 0
401
- embeddings.each do |embedding|
402
- src = Base64.decode64(embedding['data'])
403
- id = "embedding_#{@embedding_count}"
404
- if embedding['mime_type'] =~ /^image\/(png|gif|jpg|jpeg)/
405
- begin
406
- @builder.span(class: 'image') do
407
- @builder.a(href: '', style: 'text-decoration: none;', onclick: "img=document.getElementById('#{id}');img.style.display = (img.style.display == 'none' ? 'block' : 'none');return false") do
408
- @builder.span(style: "color: #{COLOR[:output]}; font-weight: bold; border-bottom: 1px solid #{COLOR[:output]};") do
409
- @builder << "Screenshot ##{@embedding_count}"
410
- end
411
- end
412
- @builder << '<br/>'
413
- @options[:compress_images] ? build_unique_image(embedding, id) : build_image(embedding,id)
414
- end
415
- rescue => e
416
- puts 'Image embedding failed!'
417
- puts [e.class, e.message, e.backtrace[0..10].join("\n")].join("\n")
418
- end
419
- elsif embedding['mime_type'] =~ /^text\/plain/
420
- begin
421
- if src.include?('|||')
422
- title, link = src.split('|||')
423
- @builder.span(class: 'link') do
424
- @builder.a(id: id, style: 'text-decoration: none;', href: link, title: title) do
425
- @builder.span(style: "color: #{COLOR[:output]}; font-weight: bold; border-bottom: 1px solid #{COLOR[:output]};") do
426
- @builder << title
427
- end
428
- end
429
- @builder << '<br/>'
430
- end
431
- else
432
- @builder.span(class: 'info') do
433
- @builder << src
434
- @builder << '<br/>'
435
- end
436
- end
437
- rescue => e
438
- puts('Link embedding skipped!')
439
- puts [e.class, e.message, e.backtrace[0..10].join("\n")].join("\n")
440
- end
441
- end
442
- @embedding_count += 1
443
- end if embeddings.is_a?(Array)
444
- end
445
-
446
- def self.build_unique_image(image, id)
447
- @images ||= []
448
- index = @images.find_index image
449
- if index
450
- klass = "image_#{index}"
451
- else
452
- @images << image
453
- klass = "image_#{@images.size - 1}"
454
- @builder.style(type: 'text/css') do
455
- begin
456
- src = Base64.decode64(image['data'])
457
- src = 'data:' + image['mime_type'] + ';base64,' + src unless src =~ /^data:image\/(png|gif|jpg|jpeg);base64,/
458
- @builder << "img.#{klass} {content: url(#{src});}"
459
- rescue
460
- src = image['data']
461
- src = 'data:' + image['mime_type'] + ';base64,' + src unless src =~ /^data:image\/(png|gif|jpg|jpeg);base64,/
462
- @builder << "img.#{klass} {content: url(#{src});}"
463
- end
464
- end
465
- end
466
- @builder << %{<img id='#{id}' class='#{klass}' style='display: none; border: 1px solid #{COLOR[:output]};' />}
467
- end
468
-
469
- def self.build_image(image, id)
470
- begin
471
- src = Base64.decode64(image['data'])
472
- src = 'data:' + image['mime_type'] + ';base64,' + src unless src =~ /^data:image\/(png|gif|jpg|jpeg);base64,/
473
- @builder << %{<img id='#{id}' style='display: none; border: 1px solid #{COLOR[:output]};' src='#{src}'/>}
474
- rescue
475
- src = image['data']
476
- src = 'data:' + image['mime_type'] + ';base64,' + src unless src =~ /^data:image\/(png|gif|jpg|jpeg);base64,/
477
- @builder << %{<img id='#{id}' style='display: none; border: 1px solid #{COLOR[:output]};' src='#{src}'/>}
478
- end
479
- end
480
-
481
- def self.build_error_list(scenario)
482
- scenario['before'].each do |before|
483
- next unless before['status'] == 'failed'
484
- @builder.li do
485
- error = before['result']['error_message'].split("\n")
486
- @builder.span(style: "color:#{COLOR[:failed]}") do
487
- error[0..-2].each do |line|
488
- @builder << line + '<br/>'
489
- end
490
- end
491
- @builder << "<strong>Hook: </strong>#{error[-1]} <br/>"
492
- @builder << "<strong>Scenario: </strong>#{scenario['name']} <br/><hr/>"
493
- end
494
- end
495
- scenario['steps'].each do |step|
496
- step['after'].each do |after|
497
- next unless after['status'] == 'failed'
498
- @builder.li do
499
- error = after['result']['error_message'].split("\n")
500
- @builder.span(style: "color:#{COLOR[:failed]}") do
501
- (scenario['keyword'] == 'Scenario Outline' ? error[0..-8] : error[0..-5]).each do |line|
502
- @builder << line + '<br/>'
503
- end
504
- end
505
- @builder << "<strong>Hook: </strong>#{scenario['keyword'] == 'Scenario Outline' ? error[-7] : error[-4]} <br/>"
506
- @builder << "<strong>FF: </strong>#{error[-2]} <br/><hr/>"
507
- end
508
- end if step['after']
509
- next unless step['status'] == 'failed' && step['result']['error_message']
510
- @builder.li do
511
- error = step['result']['error_message'].split("\n")
512
- @builder.span(style: "color:#{COLOR[:failed]}") do
513
- error[0..-3].each do |line|
514
- @builder << line + '<br/>'
515
- end
516
- end
517
- @builder << "<strong>SD: </strong>#{error[-2]} <br/>"
518
- @builder << "<strong>FF: </strong>#{error[-1]} <br/><hr/>"
519
- end
520
- end
521
- scenario['after'].each do |after|
522
- next unless after['status'] == 'failed'
523
- @builder.li do
524
- error = after['result']['error_message'].split("\n")
525
- @builder.span(style: "color:#{COLOR[:failed]}") do
526
- error[0..-2].each do |line|
527
- @builder << line + '<br/>'
528
- end
529
- end
530
- @builder << "<strong>Hook: </strong>#{error[-1]} <br/>"
531
- @builder << "<strong>Scenario: </strong>#{scenario['name']} <br/><hr/>"
532
- end
533
- end
534
- end
535
-
536
- def self.features(files)
537
- files.each_with_object([]) { |file, features|
538
- data = File.read(file)
539
- next if data.empty?
540
- features << JSON.parse(data)
541
- }.flatten.group_by { |feature|
542
- feature['uri']+feature['id']+feature['line'].to_s
543
- }.values.each_with_object([]) { |group, features|
544
- features << group.first.except('elements').merge('elements' => group.map{|feature| feature['elements']}.flatten)
545
- }.sort_by!{|feature| feature['name']}.each{|feature|
546
- if feature['elements'][0]['type'] == 'background'
547
- (0..feature['elements'].size-1).step(2) do |i|
548
- feature['elements'][i]['steps'] ||= []
549
- feature['elements'][i]['steps'].each{|step| step['name']+=(' ('+feature['elements'][i]['keyword']+')')}
550
- feature['elements'][i+1]['steps'] = feature['elements'][i]['steps'] + feature['elements'][i+1]['steps']
551
- feature['elements'][i+1]['before'] = feature['elements'][i]['before'] if feature['elements'][i]['before']
552
- end
553
- feature['elements'].reject!{|element| element['type'] == 'background'}
554
- end
555
- feature['elements'].each { |scenario|
556
- scenario['before'] ||= []
557
- scenario['before'].each { |before|
558
- before['result']['duration'] ||= 0
559
- before.merge! 'status' => before['result']['status'], 'duration' => before['result']['duration']
560
- }
561
- scenario['steps'] ||= []
562
- scenario['steps'].each { |step|
563
- step['result']['duration'] ||= 0
564
- duration = step['result']['duration']
565
- status = step['result']['status']
566
- step['after'].each { |after|
567
- after['result']['duration'] ||= 0
568
- duration += after['result']['duration']
569
- status = 'failed' if after['result']['status'] == 'failed'
570
- after.merge! 'status' => after['result']['status'], 'duration' => after['result']['duration']
571
- } if step['after']
572
- step.merge! 'status' => status, 'duration' => duration
573
- }
574
- scenario['after'] ||= []
575
- scenario['after'].each { |after|
576
- after['result']['duration'] ||= 0
577
- after.merge! 'status' => after['result']['status'], 'duration' => after['result']['duration']
578
- }
579
- scenario.merge! 'status' => scenario_status(scenario), 'duration' => total_time(scenario['before']) + total_time(scenario['steps']) + total_time(scenario['after'])
580
- }
581
- feature.merge! 'status' => feature_status(feature), 'duration' => total_time(feature['elements'])
582
- }
583
- end
584
-
585
- def self.feature_status(feature)
586
- feature_status = 'working'
587
- feature['elements'].each do |scenario|
588
- status = scenario['status']
589
- return 'broken' if status == 'failed'
590
- feature_status = 'incomplete' if %w(undefined pending).include?(status)
591
- end
592
- feature_status
593
- end
594
-
595
- def self.scenarios(features)
596
- features.map do |feature|
597
- feature['elements']
598
- end.flatten
599
- end
600
-
601
- def self.scenario_status(scenario)
602
- (scenario['before'] + scenario['steps'] + scenario['after']).each do |step|
603
- status = step['status']
604
- return status unless status == 'passed'
605
- end
606
- 'passed'
607
- end
608
-
609
- def self.steps(scenarios)
610
- scenarios.map do |scenario|
611
- scenario['steps']
612
- end.flatten
613
- end
614
-
615
- def self.tags(scenarios)
616
- scenarios.map do |scenario|
617
- scenario['tags'] ? scenario['tags'].map{|t| t['name']} : []
618
- end.flatten.uniq
619
- end
620
-
621
- def self.files(path)
622
- files = if path.is_a? String
623
- (path =~ /\.json$/) ? [path] : Dir.glob("#{path}/*.json")
624
- elsif path.nil?
625
- Dir.glob('*.json')
626
- elsif path.is_a? Array
627
- path.map do |file|
628
- (file =~ /\.json$/) ? file : Dir.glob("#{file}/*.json")
629
- end.flatten
630
- else
631
- raise 'InvalidInput'
632
- end
633
- raise 'InvalidOrNoInputFile' if files.empty?
634
- files.uniq
635
- end
636
-
637
- def self.data(all_data)
638
- all_data.group_by{|db| db['status']}.map do |data|
639
- {name: data[0],
640
- count: data[1].size,
641
- color: COLOR[data[0].to_sym]}
642
- end
643
- end
644
-
645
- def self.total_time(data)
646
- total_time = 0
647
- data.each{|item| total_time += item['duration']}
648
- total_time
649
- end
650
-
651
- def self.duration(seconds)
652
- seconds = seconds.to_f/1000000000
653
- m, s = seconds.divmod(60)
654
- "#{m}m #{'%.3f' % s}s"
655
- end
656
-
657
- def self.pie_chart_js(id, title, data)
658
- data = data.each_with_object('') do |h, s|
659
- s << "{name: '#{h[:name].capitalize}'"
660
- s << ",y: #{h[:count]}"
661
- s << ',sliced: true' if h[:sliced]
662
- s << ',selected: true' if h[:selected]
663
- s << ",color: '#{h[:color]}'" if h[:color]
664
- s << '},'
665
- end.chop
666
- "$(function (){$('##{id}').highcharts({credits: {enabled: false}, chart: {type: 'pie',
667
- options3d: {enabled: true, alpha: 45, beta: 0}}, title: {text: '#{title}'},
668
- tooltip: {pointFormat: 'Count: <b>{point.y}</b><br/>Percentage: <b>{point.percentage:.1f}%</b>'},
669
- plotOptions: {pie: {allowPointSelect: true, cursor: 'pointer', depth: 35, dataLabels: {enabled: true,
670
- format: '{point.name}'}}}, series: [{type: 'pie', name: 'Results', data: [#{data}]}]});});"
39
+ builder.build_report(options)
671
40
  end
672
41
 
673
- def self.donut_js(id, title, data)
674
- data = data.each_with_object('') do |h, s|
675
- s << "{name: '#{h[:name].capitalize}'"
676
- s << ",y: #{h[:count]}"
677
- s << ',sliced: true' if h[:sliced]
678
- s << ',selected: true' if h[:selected]
679
- s << ",color: '#{h[:color]}'" if h[:color]
680
- s << '},'
681
- end.chop
682
- "$(function (){$('##{id}').highcharts({credits: {enabled: false},
683
- chart: {plotBackgroundColor: null, plotBorderWidth: 0, plotShadow: false, width: $(document).width()-80},
684
- title: {text: '#{title}', align: 'center', verticalAlign: 'middle', y: 40},
685
- tooltip: {pointFormat: 'Count: <b>{point.y}</b><br/>Percentage: <b>{point.percentage:.1f}%</b>'},
686
- plotOptions: {pie: {dataLabels: {enabled: true, distance: -50,
687
- style: {fontWeight: 'bold', color: 'white', textShadow: '0px 1px 2px black'}},
688
- startAngle: -90, endAngle: 90, center: ['50%', '75%']}},
689
- series: [{type: 'pie', innerSize: '50%', name: 'Results', data: [#{data}]}]});});"
42
+ def self.builder
43
+ @builder ||= Builder.new
690
44
  end
691
45
 
692
- private_class_method :donut_js, :pie_chart_js, :files,
693
- :features, :feature_status,
694
- :scenarios, :scenario_status, :steps,
695
- :data, :duration, :total_time,
696
- :build_scenario, :build_step,
697
- :build_menu, :build_output, :build_embedding,
698
- :build_error_list, :build_step_error,
699
- :build_hook_error, :build_step_hook_error,
700
- :build_unique_image, :build_image,
701
- :build_data_table, :tags, :build_tags_drop_down
702
46
  end