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