butternut 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,465 @@
1
+ require 'cucumber/formatter/ordered_xml_markup'
2
+ require 'cucumber/formatter/duration'
3
+ require 'tmpdir'
4
+ require 'fileutils'
5
+ require 'nokogiri'
6
+ require 'uri'
7
+ require 'open-uri'
8
+
9
+ module Butternut
10
+ class Formatter
11
+ include ERB::Util # for the #h method
12
+ include Cucumber::Formatter::Duration
13
+
14
+ # FIXME: this is obviously not nominal, but I have no way of
15
+ # accepting additional options from Cucumber at present
16
+ FEATURES_DIR_PARTS = %w{.. features}
17
+ FEATURES_HTML_PREFIX = "/features"
18
+
19
+ def initialize(step_mother, io, options)
20
+ @io = io
21
+ @options = options
22
+ @buffer = {}
23
+ @current_builder = create_builder(@io)
24
+
25
+ if @options && @options[:formats]
26
+ format = @options[:formats].detect { |(name, _)| underscore(name) == "butternut/formatter" }
27
+ if format && format[1].is_a?(String)
28
+ base_dir = File.dirname(File.expand_path(format[1]))
29
+ features_dir = File.expand_path(File.join(base_dir, *FEATURES_DIR_PARTS))
30
+
31
+ if File.exist?(features_dir)
32
+ today = Date.today.to_s
33
+
34
+ @source_output_dir = File.join(features_dir, today)
35
+ @source_html_path = FEATURES_HTML_PREFIX + "/" + today
36
+ if !File.exist?(@source_output_dir)
37
+ FileUtils.mkdir(@source_output_dir)
38
+ end
39
+ else
40
+ $stderr.puts "Directory not found: #{features_dir}"
41
+ end
42
+ end
43
+ end
44
+ @source_output_dir ||= Dir.tmpdir
45
+ @source_html_path ||= "file://" + Dir.tmpdir
46
+ end
47
+
48
+ def before_features(features)
49
+ start_buffering :features
50
+ end
51
+
52
+ def after_features(features)
53
+ stop_buffering :features
54
+ # <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
55
+ builder.declare!(
56
+ :DOCTYPE,
57
+ :html,
58
+ :PUBLIC,
59
+ '-//W3C//DTD XHTML 1.0 Strict//EN',
60
+ 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'
61
+ )
62
+ builder.html(:xmlns => 'http://www.w3.org/1999/xhtml') do
63
+ builder.head do
64
+ builder.meta(:content => 'text/html;charset=utf-8')
65
+ builder.title 'Cucumber'
66
+ inline_css
67
+ end
68
+ builder.body do
69
+ builder.div(:class => 'cucumber') do
70
+ builder << buffer(:features)
71
+ builder.div(format_duration(features.duration), :class => 'duration')
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def before_feature(feature)
78
+ start_buffering :feature
79
+ @exceptions = []
80
+ end
81
+
82
+ def after_feature(feature)
83
+ stop_buffering :feature
84
+ builder.div(:class => 'feature') do
85
+ builder << buffer(:feature)
86
+ end
87
+ end
88
+
89
+ def before_comment(comment)
90
+ start_buffering :comment
91
+ end
92
+
93
+ def after_comment(comment)
94
+ stop_buffering :comment
95
+ builder.pre(:class => 'comment') do
96
+ builder << buffer(:comment)
97
+ end
98
+ end
99
+
100
+ def comment_line(comment_line)
101
+ builder.text!(comment_line)
102
+ builder.br
103
+ end
104
+
105
+ def after_tags(tags)
106
+ @tag_spacer = nil
107
+ end
108
+
109
+ def tag_name(tag_name)
110
+ builder.text!(@tag_spacer) if @tag_spacer
111
+ @tag_spacer = ' '
112
+ builder.span(tag_name, :class => 'tag')
113
+ end
114
+
115
+ def feature_name(name)
116
+ lines = name.split(/\r?\n/)
117
+ return if lines.empty?
118
+ builder.h2 do |h2|
119
+ builder.span(lines[0], :class => 'val')
120
+ end
121
+ builder.p(:class => 'narrative') do
122
+ lines[1..-1].each do |line|
123
+ builder.text!(line.strip)
124
+ builder.br
125
+ end
126
+ end
127
+ end
128
+
129
+ def before_background(background)
130
+ @in_background = true
131
+ start_buffering :background
132
+ end
133
+
134
+ def after_background(background)
135
+ stop_buffering :background
136
+ @in_background = nil
137
+ builder.div(:class => 'background') do
138
+ builder << buffer(:background)
139
+ end
140
+ end
141
+
142
+ def background_name(keyword, name, file_colon_line, source_indent)
143
+ @listing_background = true
144
+ builder.h3 do |h3|
145
+ builder.span(keyword, :class => 'keyword')
146
+ builder.text!(' ')
147
+ builder.span(name, :class => 'val')
148
+ end
149
+ end
150
+
151
+ def before_feature_element(feature_element)
152
+ start_buffering :feature_element
153
+ @feature_element = feature_element
154
+ end
155
+
156
+ def after_feature_element(feature_element)
157
+ stop_buffering :feature_element
158
+ css_class = {
159
+ Cucumber::Ast::Scenario => 'scenario',
160
+ Cucumber::Ast::ScenarioOutline => 'scenario outline'
161
+ }[feature_element.class]
162
+
163
+ builder.div(:class => css_class) do
164
+ builder << buffer(:feature_element)
165
+ end
166
+ @open_step_list = true
167
+ @feature_element = nil
168
+ end
169
+
170
+ def scenario_name(keyword, name, file_colon_line, source_indent)
171
+ @listing_background = false
172
+ builder.h3 do
173
+ builder.span(keyword, :class => 'keyword')
174
+ builder.text!(' ')
175
+ builder.span(name, :class => 'val')
176
+ end
177
+ end
178
+
179
+ def before_outline_table(outline_table)
180
+ @outline_row = 0
181
+ start_buffering :outline_table
182
+ end
183
+
184
+ def after_outline_table(outline_table)
185
+ stop_buffering :outline_table
186
+ builder.table do
187
+ builder << buffer(:outline_table)
188
+ end
189
+ @outline_row = nil
190
+ end
191
+
192
+ def before_examples(examples)
193
+ start_buffering :examples
194
+ end
195
+
196
+ def after_examples(examples)
197
+ stop_buffering :examples
198
+ builder.div(:class => 'examples') do
199
+ builder << buffer(:examples)
200
+ end
201
+ end
202
+
203
+ def examples_name(keyword, name)
204
+ builder.h4 do
205
+ builder.span(keyword, :class => 'keyword')
206
+ builder.text!(' ')
207
+ builder.span(name, :class => 'val')
208
+ end
209
+ end
210
+
211
+ def before_steps(steps)
212
+ start_buffering :steps
213
+ end
214
+
215
+ def after_steps(steps)
216
+ stop_buffering :steps
217
+ builder.ol do
218
+ builder << buffer(:steps)
219
+ end
220
+ end
221
+
222
+ def before_step(step)
223
+ @step_id = step.dom_id
224
+ end
225
+
226
+ def before_step_result(keyword, step_match, multiline_arg, status, exception, source_indent, background)
227
+ start_buffering :step_result
228
+ @hide_this_step = false
229
+ if exception
230
+ if @exceptions.include?(exception)
231
+ @hide_this_step = true
232
+ return
233
+ end
234
+ @exceptions << exception
235
+ end
236
+ if status != :failed && @in_background ^ background
237
+ @hide_this_step = true
238
+ return
239
+ end
240
+ @status = status
241
+ end
242
+
243
+ def after_step_result(keyword, step_match, multiline_arg, status, exception, source_indent, background)
244
+ stop_buffering :step_result
245
+ return if @hide_this_step
246
+ builder.li(:id => @step_id, :class => "step #{status}") do
247
+ builder.div do
248
+ builder << buffer(:step_result)
249
+ end
250
+ builder.div(:class => "page") do
251
+ if @feature_element.respond_to?(:last_page_source)
252
+ page_source = @feature_element.last_page_source
253
+ if page_source
254
+ page_url = @feature_element.last_page_url
255
+ page_source = transform_page_source(page_source, page_url)
256
+ path = source_file_name
257
+ File.open(path, "w") { |f| f.print(page_source) }
258
+
259
+ builder.a({:target => "_blank", :href => "#{@source_html_path}/#{File.basename(path)}"}) do
260
+ builder.span(:class => "icon-show") { builder << "" }
261
+ end
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end
267
+
268
+ def step_name(keyword, step_match, status, source_indent, background)
269
+ @step_matches ||= []
270
+ background_in_scenario = background && !@listing_background
271
+ @skip_step = @step_matches.index(step_match) || background_in_scenario
272
+ @step_matches << step_match
273
+
274
+ unless @skip_step
275
+ build_step(keyword, step_match, status)
276
+ end
277
+ end
278
+
279
+ def exception(exception, status)
280
+ return if @hide_this_step
281
+ builder.pre(format_exception(exception), :class => status)
282
+ end
283
+
284
+ def before_multiline_arg(multiline_arg)
285
+ start_buffering :multiline_arg
286
+ end
287
+
288
+ def after_multiline_arg(multiline_arg)
289
+ stop_buffering :multiline_arg
290
+ return if @hide_this_step || @skip_step
291
+ if Cucumber::Ast::Table === multiline_arg
292
+ builder.table do
293
+ builder << buffer(:multiline_arg)
294
+ end
295
+ else
296
+ builder << buffer(:multiline_arg)
297
+ end
298
+ end
299
+
300
+ def py_string(string)
301
+ return if @hide_this_step
302
+ builder.pre(:class => 'val') do |pre|
303
+ builder << string.gsub("\n", '&#x000A;')
304
+ end
305
+ end
306
+
307
+ def before_table_row(table_row)
308
+ @row_id = table_row.dom_id
309
+ @col_index = 0
310
+ start_buffering :table_row
311
+ end
312
+
313
+ def after_table_row(table_row)
314
+ stop_buffering :table_row
315
+ return if @hide_this_step
316
+ builder.table(:id => @row_id) do
317
+ builder << buffer(:table_row)
318
+ end
319
+ if table_row.exception
320
+ builder.tr do
321
+ builder.td(:colspan => @col_index.to_s, :class => 'failed') do
322
+ builder.pre do |pre|
323
+ pre << format_exception(table_row.exception)
324
+ end
325
+ end
326
+ end
327
+ end
328
+ @outline_row += 1 if @outline_row
329
+ end
330
+
331
+ def table_cell_value(value, status)
332
+ return if @hide_this_step
333
+
334
+ cell_type = @outline_row == 0 ? :th : :td
335
+ attributes = {:id => "#{@row_id}_#{@col_index}", :class => 'val'}
336
+ attributes[:class] += " #{status}" if status
337
+ build_cell(cell_type, value, attributes)
338
+ @col_index += 1
339
+ end
340
+
341
+ def announce(announcement)
342
+ builder.pre(announcement, :class => 'announcement')
343
+ end
344
+
345
+ private
346
+
347
+ def build_step(keyword, step_match, status)
348
+ step_name = step_match.format_args(lambda{|param| %{<span class="param">#{param}</span>}})
349
+ builder.div do |div|
350
+ builder.span(keyword, :class => 'keyword')
351
+ builder.text!(' ')
352
+ builder.span(:class => 'step val') do |name|
353
+ name << h(step_name).gsub(/&lt;span class=&quot;(.*?)&quot;&gt;/, '<span class="\1">').gsub(/&lt;\/span&gt;/, '</span>')
354
+ end
355
+ end
356
+ end
357
+
358
+ def build_cell(cell_type, value, attributes)
359
+ builder.__send__(cell_type, value, attributes)
360
+ end
361
+
362
+ def inline_css
363
+ builder.style(:type => 'text/css') do
364
+ builder.text!(File.read(File.dirname(__FILE__) + '/cucumber.css'))
365
+ end
366
+ end
367
+
368
+ def format_exception(exception)
369
+ (["#{exception.message} (#{exception.class})"] + exception.backtrace).join("\n")
370
+ end
371
+
372
+ def builder
373
+ @current_builder
374
+ end
375
+
376
+ def buffer(label)
377
+ result = @buffer[label]
378
+ @buffer[label] = ''
379
+ result
380
+ end
381
+
382
+ def start_buffering(label)
383
+ @buffer[label] ||= ''
384
+ @parent_builder ||= {}
385
+ @parent_builder[label] = @current_builder
386
+ @current_builder = create_builder(@buffer[label])
387
+ end
388
+
389
+ def stop_buffering(label)
390
+ @current_builder = @parent_builder[label]
391
+ end
392
+
393
+ def create_builder(io)
394
+ Cucumber::Formatter::OrderedXmlMarkup.new(:target => io, :indent => 0)
395
+ end
396
+
397
+ def source_file_name
398
+ t = Time.now.strftime("%Y%m%d")
399
+ path = nil
400
+ while path.nil?
401
+ path = File.join(@source_output_dir, "butternut#{t}-#{$$}-#{rand(0x100000000).to_s(36)}.html")
402
+ path = nil if File.exist?(path)
403
+ end
404
+ path
405
+ end
406
+
407
+ # Snagged from active_support
408
+ def underscore(camel_cased_word)
409
+ camel_cased_word.to_s.gsub(/::/, '/').
410
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
411
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
412
+ tr("-", "_").
413
+ downcase
414
+ end
415
+
416
+ def transform_page_source(page_source, page_url)
417
+ page_uri = URI.parse(page_url)
418
+ page_uri.query = nil
419
+ page_uri.path = File.dirname(page_uri.path)
420
+ page_url = page_uri.to_s
421
+
422
+ collected_files = []
423
+
424
+ doc = Nokogiri.HTML(page_source)
425
+ { 'img' => 'src',
426
+ 'link[rel=stylesheet]' => 'href'
427
+ }.each_pair do |selector, attr|
428
+ doc.css(selector).each do |elt|
429
+ elt_url = elt[attr]
430
+ next if elt_url.nil?
431
+
432
+ elt_url.gsub!('\\"', "")
433
+ next if elt_url.empty?
434
+ next if collected_files.index(elt_url)
435
+
436
+ basename = File.basename(elt_url)
437
+ local_file = File.join(@source_output_dir, basename)
438
+ remote_file = case elt_url
439
+ when %r{^\w+://} then elt_url
440
+ else
441
+ elt_url.sub!(/^\//, "")
442
+ page_url + "/" + elt_url
443
+ end
444
+ File.open(local_file, "w") { |f| f.write open(remote_file).read }
445
+ collected_files << elt_url
446
+
447
+ elt[attr] = basename
448
+ end
449
+ end
450
+
451
+ # disable links
452
+ doc.css('a').each do |link|
453
+ link['href'] = "#"
454
+ end
455
+
456
+ # turn off scripts
457
+ doc.css('script').each { |s| s.unlink }
458
+
459
+ # disable form elements
460
+ doc.css('input, select, textarea').each { |x| x['disabled'] = 'disabled' }
461
+
462
+ doc.to_s
463
+ end
464
+ end
465
+ end
@@ -0,0 +1,51 @@
1
+ module Butternut
2
+ module Helpers
3
+ def browser
4
+ @browser ||= Celerity::Browser.new
5
+ end
6
+
7
+ def visit(url)
8
+ browser.goto(url)
9
+ @page_changed = true
10
+ end
11
+
12
+ def current_url
13
+ browser.page.getWebResponse.getRequestUrl.toString
14
+ end
15
+
16
+ def current_page_source
17
+ browser.page ? browser.page.as_xml : nil
18
+ end
19
+
20
+ # Fill in a text field with a value
21
+ def fill_in(label_or_name, options = {})
22
+ elt = find_element_by_label_or_name(:text_field, label_or_name)
23
+ if elt.exist?
24
+ elt.value = options[:with]
25
+ @page_changed = true
26
+ end
27
+ end
28
+
29
+ def select(option_text, options = {})
30
+ elt = find_element_by_label_or_name(:select_list, options[:from])
31
+ if elt.exist?
32
+ elt.select(option_text)
33
+ @page_changed = true
34
+ end
35
+ end
36
+
37
+ def click_button(button_value)
38
+ browser.button(button_value).click
39
+ @page_changed = true
40
+ end
41
+
42
+ def page_changed?
43
+ @page_changed
44
+ end
45
+
46
+ def find_element_by_label_or_name(type, label_or_name)
47
+ elt = browser.send(type, :label, label_or_name)
48
+ elt.exist? ? elt : browser.send(type, :name, label_or_name)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,7 @@
1
+ module Butternut
2
+ module ScenarioExtensions
3
+ attr_accessor :last_page_source, :last_page_url
4
+ end
5
+ end
6
+
7
+ Cucumber::Ast::Scenario.send(:include, Butternut::ScenarioExtensions)
data/lib/butternut.rb ADDED
@@ -0,0 +1,32 @@
1
+ require 'rubygems'
2
+ require 'cucumber'
3
+ require 'celerity'
4
+
5
+ module Butternut
6
+ def self.setup_hooks(obj)
7
+ obj.instance_exec do
8
+ AfterStep do |object|
9
+ begin
10
+ if object.is_a?(Cucumber::Ast::Scenario)
11
+ if page_changed?
12
+ object.last_page_source = current_page_source
13
+ object.last_page_url = current_url
14
+ else
15
+ object.last_page_source = nil
16
+ object.last_page_url = nil
17
+ end
18
+ @page_changed = false
19
+ end
20
+ rescue Exception => e
21
+ p e
22
+ pp caller
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ require File.dirname(__FILE__) + "/butternut/scenario_extensions"
30
+ require File.dirname(__FILE__) + "/butternut/helpers"
31
+ require File.dirname(__FILE__) + "/butternut/formatter"
32
+