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.
- data/.document +5 -0
- data/.gitignore +22 -0
- data/LICENSE +20 -0
- data/README.rdoc +18 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/butternut.gemspec +79 -0
- data/lib/butternut/cucumber.css +112 -0
- data/lib/butternut/cucumber.sass +152 -0
- data/lib/butternut/formatter.rb +465 -0
- data/lib/butternut/helpers.rb +51 -0
- data/lib/butternut/scenario_extensions.rb +7 -0
- data/lib/butternut.rb +32 -0
- data/spec/butternut/formatter_spec.rb +288 -0
- data/spec/butternut/helpers_spec.rb +128 -0
- data/spec/butternut_spec.rb +53 -0
- data/spec/fixtures/foo.css +3 -0
- data/spec/fixtures/foo.html +20 -0
- data/spec/fixtures/foo.js +3 -0
- data/spec/fixtures/picard.jpg +0 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +78 -0
- data/tmp/features/.gitignore +2 -0
- data/tmp/main/.gitignore +2 -0
- metadata +121 -0
@@ -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", '
')
|
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(/<span class="(.*?)">/, '<span class="\1">').gsub(/<\/span>/, '</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
|
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
|
+
|