butternut 0.0.0

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,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
+