closer 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,693 @@
1
+ require 'erb'
2
+ require 'cucumber/formatter/ordered_xml_markup'
3
+ require 'cucumber/formatter/duration'
4
+ require 'cucumber/formatter/io'
5
+ require_relative 'closer_html'
6
+
7
+ module Closer
8
+ module Formatter
9
+ class Html
10
+ include ERB::Util # for the #h method
11
+ include ::Cucumber::Formatter::Duration
12
+ include ::Cucumber::Formatter::Io
13
+ include CloserHtml
14
+
15
+ def initialize(runtime, path_or_io, options)
16
+ @io = ensure_io(path_or_io, "html")
17
+ @runtime = runtime
18
+ @options = options
19
+ @buffer = {}
20
+ @builder = create_builder(@io)
21
+ @feature_number = 0
22
+ @scenario_number = 0
23
+ @step_number = 0
24
+ @header_red = nil
25
+ @delayed_messages = []
26
+ @img_id = 0
27
+ @inside_outline = false
28
+ end
29
+
30
+ def embed(src, mime_type, label)
31
+ case(mime_type)
32
+ when /^image\/(png|gif|jpg|jpeg)/
33
+ embed_image(src, label)
34
+ end
35
+ end
36
+
37
+ def embed_image(src, label)
38
+ id = "img_#{@img_id}"
39
+ @img_id += 1
40
+ @builder.span(:class => 'embed') do |pre|
41
+ pre << %{<a href="" onclick="img=document.getElementById('#{id}'); img.style.display = (img.style.display == 'none' ? 'block' : 'none');return false">#{label}</a><br>&nbsp;
42
+ <img id="#{id}" style="display: none" src="#{src}"/>}
43
+ end
44
+ end
45
+
46
+
47
+ def before_features(features)
48
+ @step_count = features.step_count
49
+
50
+ @builder.declare!(:DOCTYPE, :html)
51
+
52
+ @builder << '<html>'
53
+ @builder.head do
54
+ @builder.meta('http-equiv' => 'Content-Type', :content => 'text/html;charset=utf-8')
55
+ @builder.title 'Cucumber Features'
56
+ inline_css
57
+ inline_js
58
+ end
59
+ @builder << '<body>'
60
+ @builder << "<!-- Step count #{@step_count}-->"
61
+ @builder << '<div class="cucumber">'
62
+ @builder.div(:id => 'cucumber-header') do
63
+ @builder.div(:id => 'label') do
64
+ @builder.h1('Cucumber Features')
65
+ end
66
+ @builder.div(:id => 'summary') do
67
+ @builder.p('',:id => 'totals')
68
+ @builder.p('',:id => 'duration')
69
+ @builder.div(:id => 'expand-collapse') do
70
+ @builder.p('すべて開く', :id => 'expander')
71
+ @builder.p('すべて閉じる', :id => 'collapser')
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def after_features(features)
78
+ print_stats(features)
79
+ @builder << '</div>'
80
+ @builder << '</body>'
81
+ @builder << '</html>'
82
+ end
83
+
84
+ def before_feature(feature)
85
+ dir = feature_dir(feature)
86
+ unless dir.empty?
87
+ if @feature_dir != dir
88
+ @builder << '<div class="feature_dir"><span class="val" onclick="toggle_feature_dir(this);">'
89
+ @builder << dir
90
+ @builder << '</span></div>'
91
+ end
92
+
93
+ @feature_dir = dir
94
+ end
95
+
96
+ @feature = feature
97
+ @exceptions = []
98
+ @builder << "<div id=\"#{feature_id}\" class=\"feature\">"
99
+ end
100
+
101
+ def after_feature(feature)
102
+ @builder << '</div>'
103
+ end
104
+
105
+ def before_comment(comment)
106
+ @builder << '<pre class="comment">' unless magic_comment?(comment)
107
+ end
108
+
109
+ def after_comment(comment)
110
+ @builder << '</pre>' unless magic_comment?(comment)
111
+ end
112
+
113
+ def comment_line(comment_line)
114
+ unless magic_comment?(comment_line)
115
+ @builder.text!(comment_line)
116
+ @builder.br
117
+ end
118
+ end
119
+
120
+ def after_tags(tags)
121
+ @tag_spacer = nil
122
+ end
123
+
124
+ def tag_name(tag_name)
125
+ @builder.text!(@tag_spacer) if @tag_spacer
126
+ @tag_spacer = ' '
127
+ @builder.span(tag_name, :class => 'tag')
128
+ end
129
+
130
+ def feature_name(keyword, name)
131
+ title = feature_dir(@feature, true) + @feature.file.split('/').last.gsub(/\.feature/, '')
132
+ lines = name.split(/\r?\n/)
133
+ return if lines.empty?
134
+ @builder.h2 do |h2|
135
+ @builder.span(:class => 'val') do
136
+ @builder << title
137
+ end
138
+ end
139
+
140
+ if lines.size > 1
141
+ @builder.div(:class => 'narrative') do
142
+ @builder << lines[1..-1].join("\n")
143
+ end
144
+ end
145
+ end
146
+
147
+ def before_background(background)
148
+ @in_background = true
149
+ @builder << '<div class="background">'
150
+ end
151
+
152
+ def after_background(background)
153
+ @in_background = nil
154
+ @builder << '</div>'
155
+ end
156
+
157
+ def background_name(keyword, name, file_colon_line, source_indent)
158
+ @listing_background = true
159
+ @builder.h3 do |h3|
160
+ @builder.span(keyword, :class => 'keyword')
161
+ @builder.text!(' ')
162
+ @builder.span(name, :class => 'val')
163
+ end
164
+ end
165
+
166
+ def before_feature_element(feature_element)
167
+ @scenario_number+=1
168
+ @scenario_red = false
169
+ css_class = {
170
+ ::Cucumber::Ast::Scenario => 'scenario',
171
+ ::Cucumber::Ast::ScenarioOutline => 'scenario outline'
172
+ }[feature_element.class]
173
+ @builder << "<div class='#{css_class}'>"
174
+ end
175
+
176
+ def after_feature_element(feature_element)
177
+ @builder << '</div>'
178
+ @open_step_list = true
179
+ end
180
+
181
+ def scenario_name(keyword, name, file_colon_line, source_indent)
182
+ @builder.span(:class => 'scenario_file', :style => 'display: none;') do
183
+ @builder << file_colon_line
184
+ end
185
+ @listing_background = false
186
+
187
+ lines = name.split("\n")
188
+ @builder.h3 do
189
+ @builder.span(lines[0], :class => 'val')
190
+ end
191
+
192
+ if lines.size > 1
193
+ @builder.div(:class => 'narrative', :style => 'display: none;') do
194
+ @builder << lines[1..-1].join("\n")
195
+ end
196
+ end
197
+ end
198
+
199
+ def before_outline_table(outline_table)
200
+ @inside_outline = true
201
+ @outline_row = 0
202
+ @builder << '<table>'
203
+ end
204
+
205
+ def after_outline_table(outline_table)
206
+ @builder << '</table>'
207
+ @outline_row = nil
208
+ @inside_outline = false
209
+ end
210
+
211
+ def before_examples(examples)
212
+ @builder << '<div class="examples">'
213
+ end
214
+
215
+ def after_examples(examples)
216
+ @builder << '</div>'
217
+ end
218
+
219
+ def examples_name(keyword, name)
220
+ @builder.h4 do
221
+ @builder.span(keyword, :class => 'keyword')
222
+ @builder.text!(' ')
223
+ @builder.span(name, :class => 'val')
224
+ end
225
+ end
226
+
227
+ def before_steps(steps)
228
+ @builder << '<ol style="display: none;">'
229
+ end
230
+
231
+ def after_steps(steps)
232
+ @builder << '</ol>'
233
+ end
234
+
235
+ def before_step(step)
236
+ @step_id = step.dom_id
237
+ @step_number += 1
238
+ @step = step
239
+ end
240
+
241
+ def after_step(step)
242
+ move_progress
243
+ end
244
+
245
+ def before_step_result(keyword, step_match, multiline_arg, status, exception, source_indent, background, file_colon_line)
246
+ @step_match = step_match
247
+ @hide_this_step = false
248
+ if exception
249
+ if @exceptions.include?(exception)
250
+ @hide_this_step = true
251
+ return
252
+ end
253
+ @exceptions << exception
254
+ end
255
+ if status != :failed && @in_background ^ background
256
+ @hide_this_step = true
257
+ return
258
+ end
259
+ @status = status
260
+ return if @hide_this_step
261
+ set_scenario_color(status)
262
+
263
+ if ! @delayed_messages.empty? and status == :passed
264
+ @builder << "<li class='step #{status} expand'>"
265
+ else
266
+ @builder << "<li class='step #{status}'>"
267
+ end
268
+ end
269
+
270
+ def after_step_result(keyword, step_match, multiline_arg, status, exception, source_indent, background, file_colon_line)
271
+ return if @hide_this_step
272
+ # print snippet for undefined steps
273
+ if status == :undefined
274
+ keyword = @step.actual_keyword if @step.respond_to?(:actual_keyword)
275
+ step_multiline_class = @step.multiline_arg ? @step.multiline_arg.class : nil
276
+ @builder.pre do |pre|
277
+ pre << @runtime.snippet_text(keyword,step_match.instance_variable_get("@name") || '',step_multiline_class)
278
+ end
279
+ end
280
+ @builder << '</li>'
281
+
282
+ unless status == :undefined
283
+ step_file = step_match.file_colon_line
284
+ step_contents = "<div class=\"step_contents\"><pre>"
285
+ step_file.gsub(/^([^:]*\.rb):(\d*)/) do
286
+ line_index = $2.to_i - 1
287
+
288
+ file = $1.force_encoding('UTF-8')
289
+ File.readlines(File.expand_path(file))[line_index..-1].each do |line|
290
+ step_contents << line
291
+ break if line.chop == 'end' or line.chop.start_with?('end ')
292
+ end
293
+ end
294
+ step_contents << "</pre></div>"
295
+ @builder << step_contents
296
+ end
297
+
298
+ print_messages
299
+ end
300
+
301
+ def step_name(keyword, step_match, status, source_indent, background, file_colon_line)
302
+ background_in_scenario = background && !@listing_background
303
+ @skip_step = background_in_scenario
304
+
305
+ unless @skip_step
306
+ build_step(keyword, step_match, status)
307
+ end
308
+ end
309
+
310
+ def exception(exception, status)
311
+ return if @hide_this_step
312
+ build_exception_detail(exception)
313
+ end
314
+
315
+ def extra_failure_content(file_colon_line)
316
+ @snippet_extractor ||= SnippetExtractor.new
317
+ "<pre class=\"ruby\"><code>#{@snippet_extractor.snippet(file_colon_line)}</code></pre>"
318
+ end
319
+
320
+ def before_multiline_arg(multiline_arg)
321
+ return if @hide_this_step || @skip_step
322
+ if ::Cucumber::Ast::Table === multiline_arg
323
+ @builder << '<table>'
324
+ end
325
+ end
326
+
327
+ def after_multiline_arg(multiline_arg)
328
+ return if @hide_this_step || @skip_step
329
+ if ::Cucumber::Ast::Table === multiline_arg
330
+ @builder << '</table>'
331
+ end
332
+ end
333
+
334
+ def doc_string(string)
335
+ return if @hide_this_step
336
+ @builder.pre(:class => 'val') do |pre|
337
+ @builder << h(string).gsub("\n", '&#x000A;')
338
+ end
339
+ end
340
+
341
+
342
+ def before_table_row(table_row)
343
+ @row_id = table_row.dom_id
344
+ @col_index = 0
345
+ return if @hide_this_step
346
+ @builder << "<tr class='step' id='#{@row_id}'>"
347
+ end
348
+
349
+ def after_table_row(table_row)
350
+ return if @hide_this_step
351
+ print_table_row_messages
352
+ @builder << '</tr>'
353
+ if table_row.exception
354
+ @builder.tr do
355
+ @builder.td(:colspan => @col_index.to_s, :class => 'failed') do
356
+ @builder.pre do |pre|
357
+ pre << h(format_exception(table_row.exception))
358
+ end
359
+ end
360
+ end
361
+ if table_row.exception.is_a? ::Cucumber::Pending
362
+ set_scenario_color_pending
363
+ else
364
+ set_scenario_color_failed
365
+ end
366
+ end
367
+ if @outline_row
368
+ @outline_row += 1
369
+ end
370
+ @step_number += 1
371
+ move_progress
372
+ end
373
+
374
+ def table_cell_value(value, status)
375
+ return if @hide_this_step
376
+
377
+ @cell_type = @outline_row == 0 ? :th : :td
378
+ attributes = {:id => "#{@row_id}_#{@col_index}", :class => 'step'}
379
+ attributes[:class] += " #{status}" if status
380
+ build_cell(@cell_type, value, attributes)
381
+ set_scenario_color(status) if @inside_outline
382
+ @col_index += 1
383
+ end
384
+
385
+ def puts(message)
386
+ @delayed_messages << message
387
+ #@builder.pre(message, :class => 'message')
388
+ end
389
+
390
+ def print_messages
391
+ return if @delayed_messages.empty?
392
+
393
+ #@builder.ol do
394
+ @delayed_messages.each do |ann|
395
+ @builder.li(:class => 'message', :style => 'display: none;') do
396
+ @builder << ann
397
+ end
398
+ end
399
+ #end
400
+ empty_messages
401
+ end
402
+
403
+ def print_table_row_messages
404
+ end
405
+
406
+ def empty_messages
407
+ @delayed_messages = []
408
+ end
409
+
410
+ protected
411
+
412
+ def build_exception_detail(exception)
413
+ backtrace = Array.new
414
+ @builder.div(:class => 'message') do
415
+ message = exception.message
416
+ if defined?(RAILS_ROOT) && message.include?('Exception caught')
417
+ matches = message.match(/Showing <i>(.+)<\/i>(?:.+) #(\d+)/)
418
+ backtrace += ["#{RAILS_ROOT}/#{matches[1]}:#{matches[2]}"] if matches
419
+ matches = message.match(/<code>([^(\/)]+)<\//m)
420
+ message = matches ? matches[1] : ""
421
+ end
422
+
423
+ unless exception.instance_of?(RuntimeError)
424
+ message = "#{message} (#{exception.class})"
425
+ end
426
+
427
+ @builder.pre do
428
+ @builder.text!(message)
429
+ end
430
+ end
431
+ @builder.div(:class => 'backtrace') do
432
+ @builder.pre do
433
+ backtrace = exception.backtrace
434
+ backtrace.delete_if { |x| x =~ /\/gems\/(cucumber|rspec)/ }
435
+ @builder << backtrace_line(backtrace.join("\n"))
436
+ end
437
+ end
438
+ extra = extra_failure_content(backtrace)
439
+ @builder << extra unless extra == ""
440
+ end
441
+
442
+ def set_scenario_color(status)
443
+ if status.nil? or status == :undefined or status == :pending
444
+ set_scenario_color_pending
445
+ end
446
+ if status == :failed
447
+ set_scenario_color_failed
448
+ end
449
+ end
450
+
451
+ def set_scenario_color_failed
452
+ id = current_time_string
453
+ style = 'display: none; margin: 0; padding: 0;'
454
+ @builder << "<div id=\"#{id}\" style=\"#{style}\"></div>"
455
+
456
+ @builder.script do
457
+ @builder.text!("makeRed('cucumber-header');") unless @header_red
458
+ @header_red = true
459
+ @builder.text!("$(function() { $('##{id}').closest('.scenario').addClass('faild'); });") unless @scenario_red
460
+ @scenario_red = true
461
+ end
462
+ end
463
+
464
+ def set_scenario_color_pending
465
+ id = current_time_string
466
+ style = 'display: none; margin: 0; padding: 0;'
467
+ @builder << "<div id=\"#{id}\" style=\"#{style}\"></div>"
468
+
469
+ @builder.script do
470
+ @builder.text!("makeYellow('cucumber-header');") unless @header_red
471
+ @builder.text!("$(function() { $('##{id}').closest('.scenario').addClass('pending'); });") unless @scenario_red
472
+ end
473
+ end
474
+
475
+ def build_step(keyword, step_match, status)
476
+ if @in_background
477
+ display_keyword = keyword.strip + ' '
478
+ else
479
+ if keyword.strip == '*'
480
+ display_keyword = ''
481
+ else
482
+ display_keyword = keyword.strip + ' '
483
+ end
484
+ end
485
+
486
+ step_name = step_match.format_args(lambda{|param| %{<span class="param">#{param}</span>}})
487
+ @builder.div(:class => 'step_name') do |div|
488
+ @builder.span(display_keyword, :class => 'keyword')
489
+ @builder.span(:class => 'step val') do |name|
490
+ name << h(step_name).gsub(/&lt;span class=&quot;(.*?)&quot;&gt;/, '<span class="\1">').gsub(/&lt;\/span&gt;/, '</span>')
491
+ end
492
+ end
493
+
494
+ step_file = step_match.file_colon_line.force_encoding('UTF-8')
495
+ step_file.gsub(/^([^:]*\.rb):(\d*)/) do
496
+ step_file = "<span style=\"cursor: pointer;\" onclick=\"toggle_step_file(this); return false;\">#{step_file}</span>"
497
+ end
498
+
499
+ @builder.div(:class => 'step_file') do |div|
500
+ @builder.span do
501
+ @builder << step_file
502
+ end
503
+ end
504
+ end
505
+
506
+ def build_cell(cell_type, value, attributes)
507
+ @builder.__send__(cell_type, attributes) do
508
+ @builder.div do
509
+ @builder.span(value,:class => 'step param')
510
+ end
511
+ end
512
+ end
513
+
514
+ def inline_css
515
+ @builder.style(:type => 'text/css') do
516
+ @builder << File.read(File.dirname(__FILE__) + '/cucumber.css')
517
+ @builder << File.read(File.dirname(__FILE__) + '/closer.css')
518
+ end
519
+ end
520
+
521
+ def inline_js
522
+ @builder.script(:type => 'text/javascript') do
523
+ @builder << inline_jquery
524
+ @builder << inline_js_content
525
+ @builder << inline_closer
526
+ end
527
+ end
528
+
529
+ def inline_jquery
530
+ File.read(File.dirname(__FILE__) + '/jquery-min.js')
531
+ end
532
+
533
+ def inline_closer
534
+ ret = ''
535
+ ret << File.read(File.join(File.dirname(__FILE__), 'closer.js'))
536
+ ret << File.read(File.join(File.dirname(__FILE__), 'screenshot.js'))
537
+
538
+ if should_expand
539
+ ret << %w{
540
+ $(document).ready(function() {
541
+ $('#expander').click();
542
+ });
543
+ }.join(' ')
544
+ end
545
+
546
+ ret
547
+ end
548
+
549
+ def inline_js_content
550
+ <<-EOF
551
+
552
+ SCENARIOS = "div.scenario h3";
553
+
554
+ $(document).ready(function() {
555
+ $(SCENARIOS).css('cursor', 'pointer');
556
+ $(SCENARIOS).click(function() {
557
+ $(this).siblings().toggle(250);
558
+ });
559
+
560
+ $("#collapser").css('cursor', 'pointer');
561
+ $("#collapser").click(function() {
562
+ $(SCENARIOS).siblings().hide();
563
+ $('li.message').hide();
564
+ });
565
+
566
+ $("#expander").css('cursor', 'pointer');
567
+ $("#expander").click(function() {
568
+ $(SCENARIOS).siblings().show();
569
+ $('li.message').show();
570
+ });
571
+ })
572
+
573
+ function moveProgressBar(percentDone) {
574
+ $("cucumber-header").css('width', percentDone +"%");
575
+ }
576
+ function makeRed(element_id) {
577
+ $('#'+element_id).css('background', '#C40D0D');
578
+ $('#'+element_id).css('color', '#FFFFFF');
579
+ }
580
+ function makeYellow(element_id) {
581
+ $('#'+element_id).css('background', '#FAF834');
582
+ $('#'+element_id).css('color', '#000000');
583
+ }
584
+
585
+ EOF
586
+ end
587
+
588
+ def move_progress
589
+ @builder << " <script type=\"text/javascript\">moveProgressBar('#{percent_done}');</script>"
590
+ end
591
+
592
+ def percent_done
593
+ result = 100.0
594
+ if @step_count != 0
595
+ result = ((@step_number).to_f / @step_count.to_f * 1000).to_i / 10.0
596
+ end
597
+ result
598
+ end
599
+
600
+ def format_exception(exception)
601
+ (["#{exception.message}"] + exception.backtrace).join("\n")
602
+ end
603
+
604
+ def backtrace_line(line)
605
+ line.gsub(/\A([^:]*\.(?:rb|feature|haml)):(\d*).*\z/) do
606
+ if ENV['TM_PROJECT_DIRECTORY']
607
+ "<a href=\"txmt://open?url=file://#{File.expand_path($1)}&line=#{$2}\">#{$1}:#{$2}</a> "
608
+ else
609
+ line
610
+ end
611
+ end
612
+ end
613
+
614
+ def print_stats(features)
615
+ @builder << "<script type=\"text/javascript\">document.getElementById('duration').innerHTML = \"Finished in <strong>#{format_duration(features.duration)} seconds</strong>\";</script>"
616
+ @builder << "<script type=\"text/javascript\">document.getElementById('totals').innerHTML = \"#{print_stat_string(features)}\";</script>"
617
+ end
618
+
619
+ def print_stat_string(features)
620
+ string = String.new
621
+ string << dump_count(@runtime.scenarios.length, "scenario")
622
+ scenario_count = print_status_counts{|status| @runtime.scenarios(status)}
623
+ string << scenario_count if scenario_count
624
+ string << "<br />"
625
+ string << dump_count(@runtime.steps.length, "step")
626
+ step_count = print_status_counts{|status| @runtime.steps(status)}
627
+ string << step_count if step_count
628
+ end
629
+
630
+ def print_status_counts
631
+ counts = [:failed, :skipped, :undefined, :pending, :passed].map do |status|
632
+ elements = yield status
633
+ elements.any? ? "#{elements.length} #{status.to_s}" : nil
634
+ end.compact
635
+ return " (#{counts.join(', ')})" if counts.any?
636
+ end
637
+
638
+ def dump_count(count, what, state=nil)
639
+ [count, state, "#{what}#{count == 1 ? '' : 's'}"].compact.join(" ")
640
+ end
641
+
642
+ def create_builder(io)
643
+ ::Cucumber::Formatter::OrderedXmlMarkup.new(:target => io, :indent => 0)
644
+ end
645
+
646
+ class SnippetExtractor #:nodoc:
647
+ class NullConverter; def convert(code, pre); code; end; end #:nodoc:
648
+ begin; require 'syntax/convertors/html'; @@converter = Syntax::Convertors::HTML.for_syntax "ruby"; rescue LoadError => e; @@converter = NullConverter.new; end
649
+
650
+ def snippet(error)
651
+ raw_code, line = snippet_for(error[0])
652
+ highlighted = @@converter.convert(raw_code, false)
653
+ highlighted << "\n<span class=\"comment\"># gem install syntax to get syntax highlighting</span>" if @@converter.is_a?(NullConverter)
654
+ post_process(highlighted, line)
655
+ end
656
+
657
+ def snippet_for(error_line)
658
+ if error_line =~ /(.*):(\d+)/
659
+ file = $1
660
+ line = $2.to_i
661
+ [lines_around(file, line), line]
662
+ else
663
+ ["# Couldn't get snippet for #{error_line}", 1]
664
+ end
665
+ end
666
+
667
+ def lines_around(file, line)
668
+ if File.file?(file)
669
+ lines = File.open(file).read.split("\n")
670
+ min = [0, line-3].max
671
+ max = [line+1, lines.length-1].min
672
+ selected_lines = []
673
+ selected_lines.join("\n")
674
+ lines[min..max].join("\n")
675
+ else
676
+ "# Couldn't get snippet for #{file}"
677
+ end
678
+ end
679
+
680
+ def post_process(highlighted, offending_line)
681
+ new_lines = []
682
+ highlighted.split("\n").each_with_index do |line, i|
683
+ new_line = "<span class=\"linenum\">#{offending_line+i-2}</span>#{line}"
684
+ new_line = "<span class=\"offending\">#{new_line}</span>" if i == 2
685
+ new_lines << new_line
686
+ end
687
+ new_lines.join("\n")
688
+ end
689
+
690
+ end
691
+ end
692
+ end
693
+ end