closer 0.0.2 → 0.0.3

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.
@@ -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