report_builder 0.1.4 → 0.1.5

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