assert_xpath 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1004 @@
1
+ require 'rexml/document'
2
+ require 'stringio'
3
+
4
+ RAILS_ENV = ENV.fetch('RAILS_ENV', 'test') unless defined?(RAILS_ENV)
5
+ AFE = Test::Unit::AssertionFailedError unless defined?(AFE)
6
+
7
+ =begin
8
+ #:stopdoc:
9
+ # "We had so much fun robbing the bank we forgot to take the money"
10
+ #
11
+ # ERGO comic book: programmers hunting bugs portrayed as big game hunters in jungle
12
+
13
+ # ERGO use length not size
14
+
15
+ # ERGO complain to ZenTest forum that
16
+ # assert_in_epsilon should not blot out
17
+ # its internal message if you add an external one
18
+ - test_zentest_assertions should use assert_raise_message
19
+ - we should use assert_include more
20
+ - use util_capture more!
21
+ - use Test::Rails::ControllerTestCase & HelperTestCase
22
+ - use all the assertions in ViewTest (and compete!)
23
+ - tell ZT their view assertions should nest
24
+ - take target name out of render calls if the test case name is correct!
25
+ - use path_parameters to override
26
+ - use the LayoutsController dummy object trick!
27
+ - use named routes more!
28
+
29
+ # ERGO learn FormEncodedPairParser
30
+ # ERGO RDoc a blog entry
31
+ # ERGO write deny_match, and make it work correctly!!
32
+ # ERGO link from Hpricot references to real Hpricot verbiage
33
+ # ERGO <tt> -> <code> in RDoc!!
34
+ # ERGO assert_xpath is nothing but a call to assert_any_xpath
35
+ # ERGO two %transclude directives in a row should work
36
+ # ERGO document, redundantly, that @xdoc always has bequeathed_attributes
37
+ # ERGO complain to RDoc maintainers that {}[] targets the current frame!
38
+ # monkey-patch thereof
39
+ # ERGO link out to REXML::Element
40
+ # ERGO how to RDoc format the sample for indent_xml etc?
41
+ # ERGO how to syntax hilite the sample for indent_xml etc?
42
+ # ERGO cross links in sample code
43
+ # ERGO back RDoc with complete source
44
+ # ERGO links from RDoc file reports in their contents
45
+ # ERGO is search{} defined in terms of assert_any_xpath??
46
+ # ERGO get Hpricot::search going for self-or-descendent, and take out all Hpricot::Doc cruft!
47
+ # ERGO does Hpricot#Doc[] or #Elem[] conflict with anything?
48
+ # ERGO merge the common bequeathed attributes into one module
49
+ # indent_xml should find a system built-into Hpricot?
50
+ #:startdoc:
51
+ =end
52
+
53
+
54
+ =begin rdoc
55
+ See: http://assertxpath.rubyforge.org/
56
+
57
+ %html <pre>
58
+ ___________________________
59
+ # __/ >tok tok tok< \
60
+ #< <%< <__ assert_xpath reads XHTML |
61
+ # (\\= | and queries its details! |
62
+ # | ---------------------------
63
+ #===={===
64
+ #
65
+ %html </pre>
66
+
67
+ =end
68
+
69
+
70
+ module AssertXPath
71
+
72
+ module CommonXPathExtensions #:nodoc:
73
+
74
+ def symbolic?(index)
75
+ return index.to_s if (index.kind_of? String or index.kind_of? Symbol)
76
+ end
77
+
78
+ def[](index)
79
+ if symbol = symbolic?(index)
80
+ return attributes[symbol] if attributes.has_key? symbol
81
+ raise_magic_member_not_found(symbol, caller)
82
+ end
83
+
84
+ return super # ERGO test this works?
85
+ end
86
+
87
+ def raise_magic_member_not_found(symbol, whats_caller_ERGO)
88
+ raise AFE, # ERGO merge with other raiser(s)
89
+ "missing attribute: `#{symbol}` in " +
90
+ "<#{ name } #{ attributes.keys.join(' ') }>",
91
+ whats_caller_ERGO
92
+ end
93
+
94
+ def identifiable?(str) # ERGO self. ?
95
+ return str =~ /^ [[:alpha:]] [[:alnum:]_]* $/ix
96
+ end # ERGO simplify??
97
+
98
+ # ERGO mock the YarWiki and run its tests locally!!!
99
+
100
+ def tribute(block)
101
+ stash = {} # put back the ones we changed!
102
+
103
+ if block
104
+ varz = instance_variables
105
+
106
+ attributes.each do |key, value|
107
+ if identifiable?(key) # deal if the key ain't a valid variable
108
+ key = "@#{ key }"
109
+ stash[key] = instance_variable_get(key) if varz.include?(key)
110
+ #p stash[key]
111
+ instance_variable_set key, value
112
+ end
113
+ end
114
+
115
+ return instance_eval(&block)
116
+ end
117
+ ensure # put them back!
118
+ stash.each{|key, value| instance_variable_set(key, value) }
119
+ end # this utterly sick convenience helps Ruby {@id} look like XPathic [@id]
120
+
121
+ end
122
+
123
+ # ERGO document me
124
+ def drill(&block)
125
+ if block
126
+ # ERGO harmonize with bang! version
127
+ # ERGO deal if the key ain't a valid variable
128
+
129
+ unless tribute(block) # ERGO pass in self (node)?
130
+ sib = self
131
+ nil while (sib = sib.next_sibling) and sib.node_type != :element
132
+ p sib # ERGO do tests ever get here?
133
+ q = sib and _bequeath_attributes(sib).drill(&block)
134
+ return sib if q
135
+ raise Test::Unit::AssertionFailedError.new("can't find beyond <#{xpath}>")
136
+ end
137
+ end
138
+
139
+ return self
140
+ # ERGO if block returns false/nil, find siblings until it passes.
141
+ # throw a test failure if it don't.
142
+ # ERGO axis concept
143
+ end
144
+
145
+ end
146
+
147
+ # ERGO node.descendant{ @type == 'text' and @id == 'foo' }.value
148
+ # ERGO node..a_descendant - overload ..
149
+
150
+ def _bequeath_attributes(node) #:nodoc:
151
+ return node if node.kind_of?(::Hpricot::Elem) or node.kind_of?(::Hpricot::Doc)
152
+ # ERGO shouldn't this be in a stinkin' module??
153
+ # ERGO SIMPLER!!
154
+ # ERGO document me
155
+ def node.drill(&block)
156
+ if block
157
+ # ERGO harmonize with bang! version
158
+ # ERGO deal if the key ain't a valid variable
159
+
160
+ unless tribute(block) # ERGO pass in self (node)?
161
+ sib = self
162
+ nil while (sib = sib.next_sibling) and sib.node_type != :element
163
+ q = sib and _bequeath_attributes(sib).drill(&block)
164
+ return sib if q
165
+ raise Test::Unit::AssertionFailedError.new("can't find beyond <#{ xpath }>")
166
+ end
167
+ end
168
+
169
+ return self
170
+ # ERGO if block returns false/nil, find siblings until it passes.
171
+ # throw a test failure if it don't.
172
+ # ERGO axis concept
173
+ end
174
+
175
+ return node # ERGO use this return value
176
+ end # ERGO is _ a good RPP for a "pretend private"?
177
+
178
+
179
+ module AssertXPath
180
+
181
+ Element = ::REXML::Element unless defined?(Element) #:nodoc:
182
+
183
+ class XmlHelper #:nodoc:
184
+ def libxml? ; false end # this is not a 'downcast' (bad in OO)
185
+ def rexml? ; false end # becase diverse libraries are a "boundary"
186
+ def hpricot? ; false end # situation. We can't control their contents!
187
+ end
188
+
189
+ class HpricotHelper < XmlHelper #:nodoc:
190
+ def hpricot? ; true end
191
+ def symbol_to_xpath(tag) tag.to_s end
192
+ def assert_xml(suite, *args, &block)
193
+ return suite.assert_hpricot(*args, &block)
194
+ end
195
+ end
196
+
197
+ class LibxmlHelper < XmlHelper #:nodoc:
198
+ def libxml? ; true end
199
+ def symbol_to_xpath(tag) "descendant-or-self::#{tag}" end
200
+ def assert_xml(suite, *args, &block)
201
+ return suite.assert_libxml(*args, &block)
202
+ end
203
+ end
204
+
205
+ class RexmlHelper < XmlHelper #:nodoc:
206
+ def rexml? ; true end
207
+ def symbol_to_xpath(tag) ".//#{tag}" end
208
+ def assert_xml(suite, *args, &block)
209
+ return suite.assert_rexml(*args, &block)
210
+ end
211
+ end
212
+
213
+ # Subsequent +assert_xml+ calls will use
214
+ # Hpricot[http://code.whytheluckystiff.net/hpricot/].
215
+ # (Alternately,
216
+ # +assert_hpricot+ will run one assertion in Hpricot mode.)
217
+ # Put +invoke_hpricot+ into +setup+() method, to
218
+ # run entire suites in this mode. These test cases
219
+ # explore some differences between the two assertion systems:
220
+ # %transclude AssertXPathSuite#test_assert_long_xpath
221
+ #
222
+ def invoke_hpricot
223
+ @xdoc = nil
224
+ @helper = HpricotHelper.new
225
+ end
226
+
227
+ # Subsequent +assert_xml+ calls will use
228
+ # LibXML[http://libxml.rubyforge.org/].
229
+ # (Alternately,
230
+ # +assert_libxml+ will run one assertion in Hpricot mode.)
231
+ # Put +invoke_libxml+ into +setup+() method, to
232
+ # run entire suites in this mode.
233
+ #
234
+ def invoke_libxml(favorite_flavor = :html)
235
+ @_favorite_flavor = favorite_flavor
236
+ @xdoc = nil
237
+ @helper = LibxmlHelper.new
238
+ end
239
+
240
+ def _doc_type # ERGO document all these!
241
+ { :html => '<!DOCTYPE HTML PUBLIC ' +
242
+ '"-//W3C//DTD HTML 4.01 Transitional//EN" '+
243
+ '"http://www.w3.org/TR/html4/loose.dtd">',
244
+ :xhtml => '<!DOCTYPE html PUBLIC ' +
245
+ '"-//W3C//DTD XHTML 1.0 Transitional//EN" ' +
246
+ '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >',
247
+ :xml => ''
248
+ }.freeze
249
+ end
250
+ private :_doc_type
251
+
252
+ # FIXME what happens to assert_js_replace_html bearing entities??
253
+
254
+ # Subsequent +assert_xml+ calls will use REXML. See
255
+ # +invoke_hpricot+ to learn the various differences between the
256
+ # two systems
257
+ def invoke_rexml
258
+ @xdoc = nil
259
+ @helper = RexmlHelper.new
260
+ end
261
+
262
+ # %html <a name='assert_xml'></a>
263
+ #
264
+ # Prepare XML for assert_xpath <em>et al</em>
265
+ # * +xml+ - optional string containing XML. Without it, we read <code>@response.body</code>
266
+ # * <code>xpath, diagnostic, block</code> - optional arguments passed to +assert_xpath+
267
+ # Sets and returns the new secret <code>@xdoc</code> REXML::Element root
268
+ # call-seq:
269
+ # assert_xml(xml = @response.body <em>[, assert_xpath arguments]</em>) -> @xdoc, or assert_xpath's return value
270
+ #
271
+ # Assertions based on +assert_xpath+ will call this automatically if
272
+ # the secret <code>@xdoc</code> is +nil+. This implies we may freely call
273
+ # +assert_xpath+ after any method that populates <code>@response.body</code>
274
+ # -- if <code>@xdoc</code> is +nil+. When in doubt, call +assert_xml+ explicitly
275
+ #
276
+ # +assert_xml+ also translates the contents of +assert_select+ nodes. Use this to
277
+ # bridge assertions from one system to another. For example:
278
+ #
279
+ # Returns the first node in the XML
280
+ #
281
+ # Examples:
282
+ # assert_select 'div#home_page' do |home_page|
283
+ # assert_xml home_page # <-- calls home_page.to_s
284
+ # assert_xpath ".//img[ @src = '#{newb.image_uri(self)}' ]"
285
+ # deny_tag_id :form, :edit_user
286
+ # end
287
+ #
288
+ # %transclude AssertXPathSuite#test_assert_long_sick_expression
289
+ # See: AssertXPathSuite#test_assert_xml_drill
290
+ #
291
+ def assert_xml(*args, &block)
292
+ using :libxml? # prop-ulates @helper
293
+ return @helper.assert_xml(self, *args, &block)
294
+ end # ERGO take out the rescue nil!, and pass the diagnostic thru
295
+
296
+ # Processes a string of text, or the hidden <code>@response.body</code>,
297
+ # using REXML, and sets the hidden <code>@xdoc</code> node. Does
298
+ # not depend on, or change, the values of +invoke_hpricot+, +invoke_libxml+,
299
+ # or +invoke_rexml+
300
+ #
301
+ # Example:
302
+ # %transclude AssertXPathSuite#test_assert_rexml
303
+ #
304
+ def assert_rexml(*args, &block)
305
+ contents = (args.shift || @response.body).to_s
306
+ # ERGO benchmark these things
307
+
308
+ contents.gsub!('\\\'', '&apos;')
309
+ contents.gsub!('//<![CDATA[<![CDATA[', '')
310
+ contents.gsub!('//<![CDATA[', '')
311
+ contents.gsub!('//]]>', '')
312
+ contents.gsub!('//]>', '')
313
+
314
+ begin
315
+ @xdoc = REXML::Document.new(contents)
316
+ rescue REXML::ParseException => e
317
+ raise e unless e.message =~ /attempted adding second root element to document/
318
+ @xdoc = REXML::Document.new("<xhtml>#{ contents }</xhtml>")
319
+ end
320
+
321
+ _bequeath_attributes(@xdoc)
322
+ assert_xpath(*args, &block) if args != []
323
+ return (assert_xpath('/*') rescue nil) if @xdoc
324
+ end
325
+
326
+ def assert_libxml(*args, &block)
327
+ xml = args.shift || @xdoc || @response.body
328
+ require 'xml/libxml'
329
+ xp = XML::Parser.new()
330
+ xhtml = xml.to_s
331
+
332
+ if xhtml !~ /^\<\!DOCTYPE\b/ and xhtml !~ /\<\?xml\b/
333
+ xhtml = _doc_type[@_favorite_flavor || :html] + "\n" + xhtml
334
+ end # ERGO document we pass HTML level into invoker
335
+
336
+ # # FIXME blog that libxml will fully validate your ass...
337
+
338
+ xp.string = xhtml
339
+ # FIXME blog we don't work with libxml-ruby 3.8.4
340
+ # XML::Parser.default_load_external_dtd = false
341
+ XML::Parser.default_pedantic_parser = false # FIXME optionalize that
342
+
343
+ #what? xp
344
+ doc = xp.parse
345
+ #what? doc
346
+ #puts doc.debug_dump
347
+ @xdoc = doc.root
348
+ # @xdoc.namespace ||= XML::NS.new('')
349
+
350
+ #pp (@xdoc.root.public_methods - public_methods).sort
351
+ return assert_xpath(*args, &block) if args.length > 0
352
+ return @xdoc
353
+ end
354
+
355
+ def using(kode)
356
+ @helper ||= RexmlHelper.new # ERGO escallate this!
357
+ return @helper.send(kode)
358
+ end
359
+
360
+ # FIXME test that the helper system withstands this effect:
361
+ # ""
362
+
363
+ # %html <a name='assert_hpricot'></a>
364
+ #
365
+ # This parses one XML string using Hpricot, so subsequent
366
+ # calls to +assert_xpath+ will use Hpricot expressions.
367
+ # This method does not depend on +invoke_hpricot+, and
368
+ # subsequent test cases will run in their suite's mode.
369
+ #
370
+ # Example:
371
+ # %transclude AssertXPathSuite#test_assert_hpricot
372
+ #
373
+ # See also: assert_hpricot[http://www.oreillynet.com/onlamp/blog/2007/08/assert_hpricot_1.html]
374
+ #
375
+ def assert_hpricot(*args, &block)
376
+ xml = args.shift || @xdoc || @response.body ## ERGO why @xdoc??
377
+ # ERGO document that callseq!
378
+ require 'hpricot'
379
+ @xdoc = Hpricot(xml.to_s) # ERGO take that to_s out of all callers
380
+ return assert_xpath(*args, &block) if args.length > 0
381
+ return @xdoc
382
+ end # ERGO reasonable error message if ill-formed
383
+
384
+ # ENCORE Bus to Julian! (-;
385
+ # ERGO why %html <a name='assert_xpath' /> screws up?
386
+
387
+ # %html <a name='assert_xpath'></a>
388
+ #
389
+ # Return the first XML node matching a query string. Depends on +assert_xml+
390
+ # to populate our secret internal REXML::Element, <code>@xdoc</code>
391
+ # * +xpath+ - a query string describing a path among XML nodes.
392
+ # See: {XPath Tutorial Roundup}[http://krow.livejournal.com/523993.html]
393
+ # * +diagnostic+ - optional string to add to failure message
394
+ # * <code>block|node|</code> - optional block containing assertions, based on +assert_xpath+,
395
+ # which operate on this node as the XPath '.' current +node+
396
+ # Returns the obtained REXML::Element +node+
397
+ #
398
+ # Examples:
399
+ #
400
+ # render :partial => 'my_partial'
401
+ #
402
+ # assert_xpath '/table' do |table|
403
+ # assert_xpath './/p[ @class = "brown_text" ]/a' do |a|
404
+ # assert_equal user.login, a.text # <-- native <code>REXML::Element#text</code> method
405
+ # assert_match /\/my_name$/, a[:href] # <-- attribute generated by +assert_xpath+
406
+ # end
407
+ # assert_equal "ring_#{ring.id}", table.id! # <-- attribute generated by +assert_xpath+, escaped with !
408
+ # end
409
+ #
410
+ # %transclude AssertXPathSuite#test_assert_xpath
411
+ #
412
+ # See: AssertXPathSuite#test_indent_xml,
413
+ # {XPath Checker}[https://addons.mozilla.org/en-US/firefox/addon/1095]
414
+ #
415
+ def assert_xpath(xpath, diagnostic = nil, &block)
416
+ # return assert_any_xpath(xpath, diagnostic) {
417
+ # block.call(@xdoc) if block
418
+ # true
419
+ # }
420
+ stash_xdoc do
421
+ xpath = symbol_to_xpath(xpath)
422
+ node = @xdoc.search(xpath).first
423
+ @xdoc = node || flunk_xpath(diagnostic, "should find xpath <#{_esc xpath}>")
424
+ @xdoc = _bequeath_attributes(@xdoc)
425
+ block.call(@xdoc) if block # ERGO tribute here?
426
+ return @xdoc
427
+ end
428
+ end
429
+
430
+ # FIXME assert_js_argument(2) gotta return nill for nada
431
+
432
+ # Negates +assert_xpath+. Depends on +assert_xml+
433
+ #
434
+ # Examples:
435
+ # assert_tag_id :td, :object_list do
436
+ # assert_xpath "table[ position() = 1 and @id = 'object_#{object1.id}' ]"
437
+ # deny_xpath "table[ position() = 2 and @id = 'object_#{object2.id}' ]"
438
+ # end # find object1 is still displayed, but object2 is not in position 2
439
+ #
440
+ # %transclude AssertXPathSuite#test_deny_xpath
441
+ #
442
+ def deny_xpath(xpath, diagnostic = nil)
443
+ @xdoc or assert_xml
444
+ xpath = symbol_to_xpath(xpath)
445
+
446
+ @xdoc.search(xpath).first and
447
+ flunk_xpath(diagnostic, "should not find: <#{_esc xpath}>")
448
+ end
449
+
450
+ # Search nodes for a matching XPath whose <code>AssertXPath::Element#inner_text</code> matches a Regular Expression. Depends on +assert_xml+
451
+ # * +xpath+ - a query string describing a path among XML nodes.
452
+ # See: {XPath Tutorial Roundup}[http://krow.livejournal.com/523993.html]
453
+ # * +matcher+ - optional Regular Expression to test node contents
454
+ # * +diagnostic+ - optional string to add to failure message
455
+ # * <code>block|node|</code> - optional block called once per match.
456
+ # If this block returns a value other than +false+ or +nil+,
457
+ # assert_any_xpath stops looping and returns the current +node+
458
+ #
459
+ # Example:
460
+ # %transclude AssertXPathSuite#test_assert_any_xpath
461
+ #
462
+ def assert_any_xpath(xpath, matcher = nil, diagnostic = nil, &block)
463
+ matcher ||= //
464
+ block ||= lambda{ true }
465
+ found_any = false
466
+ found_match = false
467
+ xpath = symbol_to_xpath(xpath)
468
+
469
+ stash_xdoc do
470
+ #assert_xpath xpath, diagnostic
471
+
472
+ if !using(:rexml?)
473
+ @xdoc.search(xpath) do |@xdoc|
474
+ found_any = true
475
+
476
+ if @xdoc.inner_text =~ matcher
477
+ found_match = true
478
+ _bequeath_attributes(@xdoc)
479
+ return @xdoc if block.call(@xdoc)
480
+ # note we only exit block if block.nil? or call returns false
481
+ end
482
+ end
483
+ else # ERGO merge!
484
+ @xdoc.each_element(xpath) do |@xdoc|
485
+ found_any = true
486
+
487
+ if @xdoc.inner_text =~ matcher
488
+ found_match = true
489
+ _bequeath_attributes(@xdoc)
490
+ return @xdoc if block.call(@xdoc)
491
+ # note we only exit block if block.nil? or call returns false
492
+ end
493
+ end
494
+ end
495
+ end
496
+
497
+ found_any or
498
+ flunk_xpath(diagnostic, "should find xpath <#{_esc xpath}>")
499
+
500
+ found_match or
501
+ flunk_xpath(
502
+ diagnostic,
503
+ "can find xpath <#{_esc xpath}> but can't find pattern <?>",
504
+ matcher
505
+ )
506
+ end
507
+
508
+ # Negates +assert_any_xpath+. Depends on +assert_xml+
509
+ #
510
+ # * +xpath+ - a query string describing a path among XML nodes. This
511
+ # must succeed - use +deny_xpath+ for simple queries that must fail
512
+ # * +matcher+ - optional Regular Expression to test node contents. If +xpath+ locates multiple nodes,
513
+ # this pattern must fail to match each node to pass the assertion.
514
+ # * +diagnostic+ - optional string to add to failure message
515
+ #
516
+ # Contrived example:
517
+ # assert_xml '<heathrow><terminal>5</terminal><lean>methods</lean></heathrow>'
518
+ #
519
+ # assert_raise_message Test::Unit::AssertionFailedError,
520
+ # /all xpath.*\.\/\/lean.*not have.*methods/ do
521
+ # deny_any_xpath :lean, /methods/
522
+ # end
523
+ #
524
+ # deny_any_xpath :lean, /denver/
525
+ #
526
+ # See: AssertXPathSuite#test_deny_any_xpath,
527
+ # {assert_raise (on Ruby) - Don't Just Say "No"}[http://www.oreillynet.com/onlamp/blog/2007/07/assert_raise_on_ruby_dont_just.html]
528
+ #
529
+ def deny_any_xpath(xpath, matcher, diagnostic = nil)
530
+ @xdoc or assert_xml
531
+ xpath = symbol_to_xpath(xpath)
532
+
533
+ assert_any_xpath xpath, nil, diagnostic do |node|
534
+ if node.inner_text =~ matcher
535
+ flunk_xpath(
536
+ diagnostic,
537
+ "all xpath <#{_esc xpath}> nodes should not have pattern <?>",
538
+ matcher
539
+ )
540
+ end
541
+ end
542
+ end
543
+
544
+ # Wraps the common idiom <code>assert_xpath('descendant-or-self::./<em>my_tag</em>[ @id = "<em>my_id</em>" ]')</code>. Depends on +assert_xml+
545
+ # * +tag+ - an XML node name, such as +div+ or +input+.
546
+ # If this is a <code>:symbol</code>, we prefix "<code>.//</code>"
547
+ # * +id+ - string or symbol uniquely identifying the node. This must not contain punctuation
548
+ # * +diagnostic+ - optional string to add to failure message
549
+ # * <code>block|node|</code> - optional block containing assertions, based on
550
+ # +assert_xpath+, which operate on this node as the XPath '.' current node.
551
+ # Returns the obtained REXML::Element +node+
552
+ #
553
+ # Examples:
554
+ #
555
+ # assert_tag_id '/span/div', "audience_#{ring.id}" do
556
+ # assert_xpath 'table/tr/td[1]' do |td|
557
+ # #...
558
+ # assert_tag_id :form, :for_sale
559
+ # end
560
+ # end
561
+ #
562
+ # %transclude AssertXPathSuite#test_assert_tag_id_and_tidy
563
+ #
564
+ # %transclude AssertXPathSuite#test_assert_tag_id
565
+ #
566
+ def assert_tag_id(tag, id, diagnostic = nil, &block)
567
+ # CONSIDER upgrade assert_tag_id to use each_element_with_attribute
568
+ assert_xpath build_xpath(tag, id), diagnostic, &block
569
+ end # NOTE: ids may not contain ', so we are delimiter-safe
570
+
571
+ # Negates +assert_tag_id+. Depends on +assert_xml+
572
+ #
573
+ # Example - see: +assert_xml+
574
+ #
575
+ # See: +assert_tag_id+
576
+ #
577
+ def deny_tag_id(tag, id, diagnostic = nil)
578
+ deny_xpath build_xpath(tag, id), diagnostic
579
+ end
580
+
581
+ # Pretty-print a REXML::Element or Hpricot::Elem
582
+ # * +doc+ - optional element. Defaults to the current +assert_xml+ document
583
+ # returns: string with indented XML
584
+ #
585
+ # Use this while developing a test case, to see what
586
+ # the current <code>@xdoc</code> node contains (as populated by +assert_xml+ and
587
+ # manipulated by +assert_xpath+ <em>et al</em>)
588
+ #
589
+ # For example:
590
+ # assert_javascript 'if(x == 42) answer_great_question();'
591
+ #
592
+ # assert_js_if /x.*42/ do
593
+ # puts indent_xml # <-- temporary statement to see what to assert next!
594
+ # end
595
+ #
596
+ # See: AssertXPathSuite#test_indent_xml
597
+ #
598
+ def indent_xml(doc = @xdoc || assert_xml)
599
+ if doc.kind_of?(Hpricot::Elem) or doc.kind_of?(Hpricot::Doc)
600
+ zdoc = doc
601
+ doc = REXML::Document.new(doc.to_s.strip) rescue nil
602
+ unless doc # Hpricot didn't well-formify the HTML!
603
+ return zdoc.to_s # note: not indented, but good enough for error messages
604
+ end
605
+ end
606
+
607
+ # require 'rexml/formatters/default'
608
+ # bar = REXML::Formatters::Pretty.new
609
+ # out = String.new
610
+ # bar.write(doc, out)
611
+ # return out
612
+
613
+ return doc.to_s # ERGO reconcile with 1.8.6.111!
614
+
615
+ x = StringIO.new
616
+ doc.write(x, 2)
617
+ return x.string # CONSIDER does REXML have a simpler way?
618
+ end
619
+
620
+ # %html <a name='assert_tidy'></a>
621
+ # Thin wrapper on the Tidy command line program (the one released 2005 September)
622
+ # * +messy+ - optional string containing messy HTML. Defaults to <code>@response.body</code>.
623
+ # * +verbosity+ - optional noise level. Defaults to <code>:noisy</code>, which
624
+ # reports most errors. :verbose reports all information, and other value
625
+ # will repress all of Tidy's screams of horror regarding the quality of your HTML.
626
+ # The resulting XHTML loads into +assert_xml+. Use this to retrofit +assert_xpath+ tests
627
+ # to less-than-pristine HTML.
628
+ #
629
+ # +assert_tidy+ obeys +invoke_rexml+ and +invoke_hpricot+, to
630
+ # select its HTML parser
631
+ #
632
+ # Examples:
633
+ #
634
+ # get :adjust, :id => transaction.id # <-- fetches ill-formed HTML
635
+ # assert_tidy @response.body, :quiet # <-- upgrades it to well-formed
636
+ # assert_tag_id '//table', :payment_history do # <-- sees good XML
637
+ # #...
638
+ # end
639
+ #
640
+ # %transclude AssertXPathSuite#test_assert_tag_id_and_tidy
641
+ #
642
+ def assert_tidy(messy = @response.body, verbosity = :noisy)
643
+ scratch_html = RAILS_ROOT + '/tmp/scratch.html'
644
+ # CONSIDER a railsoid tmp file system?
645
+ # CONSIDER yield to something to respond to errors?
646
+ File.open(scratch_html, 'w'){|f| f.write(messy) }
647
+ gripes = `tidy -eq #{scratch_html} 2>&1`
648
+ gripes.split("\n")
649
+
650
+ # TODO kvetch about client_input_channel_req: channel 0 rtype keepalive@openssh.com reply 1
651
+
652
+ puts gripes if verbosity == :verbose
653
+
654
+ puts gripes.reject{|g|
655
+ g =~ / - Info\: / or
656
+ g =~ /Warning\: missing \<\!DOCTYPE\> declaration/ or
657
+ g =~ /proprietary attribute/ or
658
+ g =~ /lacks "(summary|alt)" attribute/
659
+ } if verbosity == :noisy
660
+
661
+ assert_xml `tidy -wrap 1001 -asxhtml #{ scratch_html } 2>/dev/null`
662
+ # CONSIDER that should report serious HTML deformities
663
+ end # CONSIDER how to tidy <% escaped %> eRB code??
664
+
665
+ # ERGO new module for these...
666
+ # See: http://www.oreillynet.com/onlamp/blog/2007/07/assert_latest_and_greatest.html
667
+
668
+ def assert_flunked(gripe, &block) #:nodoc:
669
+ assert_raise_message Test::Unit::AssertionFailedError, gripe, &block
670
+ end # ERGO move to assert{ 2.0 }, reflect, and leave there!
671
+
672
+ def deny_flunked(gripe, &block) #:nodoc:
673
+ deny_raise_message Test::Unit::AssertionFailedError, gripe, &block
674
+ end # ERGO move to assert{ 2.0 }, reflect, and leave there!
675
+
676
+ def assert_latest(model, message = nil, &block)
677
+ get_latest(model, &block) or flunk_latest(model, message, true, block)
678
+ end
679
+
680
+ def deny_latest(model, message = nil, &block)
681
+ get_latest(model, &block) and flunk_latest(model, message, false, block)
682
+ end
683
+
684
+ def get_latest(model, &block)
685
+ max_id = model.maximum(:id) || 0
686
+ block.call
687
+ all = model.find( :all,
688
+ :conditions => "id > #{max_id}",
689
+ :order => "id asc" )
690
+ return *all # <-- returns nil for [],
691
+ # one object for [x],
692
+ # or an array with more than one item
693
+ end
694
+
695
+ # FIXME write a plugin for cruisecontrol.rb
696
+ # that links metrics to Gruff per project
697
+ # (and link from assert2.rf.org to rf.org/projects/assert2
698
+
699
+ def flunk_latest(model, message, polarity, block)
700
+ rationale = "should#{ ' not' unless polarity
701
+ } create new #{ model.name.pluralize
702
+ } in block:\n\t\t#{
703
+ reflect_source(&block).gsub("\n", "\n\t\t")
704
+ }\n"
705
+ # RubyNodeReflector::RubyReflector.new(block, false).result }"
706
+ # note we don't evaluate...
707
+ flunk build_message(message, rationale)
708
+ end
709
+
710
+ private
711
+
712
+ # ERGO switch to descendant-or-self
713
+ # ERGO then update documentation of those who use this
714
+ def symbol_to_xpath(tag)
715
+ return tag unless tag.class == Symbol
716
+ using :libxml? # prop-ulates @helper
717
+ return @helper.symbol_to_xpath(tag)
718
+ end
719
+
720
+ def build_xpath(tag, id)
721
+ return symbol_to_xpath(tag) + "[ @id = '#{id}' ]"
722
+ end
723
+
724
+ def stash_xdoc
725
+ former_xdoc = @xdoc || assert_xml
726
+ yield ensure @xdoc = former_xdoc
727
+ end
728
+
729
+ def flunk_xpath(diagnostic, template, *args) #:nodoc:
730
+ xml = _esc(indent_xml).relevance || '(@xdoc is blank!)'
731
+ flunk build_message(diagnostic, "#{template} in\n#{xml}", *args)
732
+ end
733
+
734
+ end
735
+
736
+
737
+ def _esc(x) #:nodoc:
738
+ return x.gsub('?', '\?')
739
+ end
740
+
741
+
742
+ #####################################################
743
+
744
+ # FIXME got_libxml?
745
+
746
+ # ERGO hpricot gets its own module (REXML-free!)
747
+
748
+ def got_hpricot? # ERGO help tests pass without it
749
+ require 'hpricot'
750
+ return true
751
+ rescue MissingSourceFile
752
+ return false
753
+ end
754
+
755
+ #####################################################
756
+
757
+ # parking some tiny conveniences here,
758
+ # where even production code can get to them...
759
+ module Relevate
760
+ def relevant?
761
+ return ! blank?
762
+ end
763
+
764
+ def relevance
765
+ return to_s if relevant?
766
+ end
767
+ end
768
+
769
+ # ERGO dry these up
770
+ class String
771
+ def blank?
772
+ return strip.size == 0
773
+ end
774
+ end
775
+
776
+ class NilClass
777
+ def blank?; true; end
778
+ end
779
+
780
+ # ERGO include our test modules like this too
781
+ # ERGO seek relevant? calls that could use relevance
782
+
783
+ NilClass.send :include, Relevate
784
+ String .send :include, Relevate
785
+
786
+ #:enddoc:
787
+
788
+ # props: http://www.intertwingly.net/blog/2007/11/02/MonkeyPatch-for-Ruby-1-8-6
789
+ doc = REXML::Document.new '<doc xmlns="ns"><item name="foo"/></doc>'
790
+ if not doc.root.elements["item[@name='foo']"]
791
+ class REXML::Element
792
+ def attribute( name, namespace=nil )
793
+ prefix = nil
794
+ prefix = namespaces.index(namespace) if namespace
795
+ prefix = nil if prefix == 'xmlns'
796
+ attributes.get_attribute( "#{prefix ? prefix + ':' : ''}#{name}" )
797
+ end
798
+ end
799
+ end
800
+
801
+
802
+ # These monkey patches push Hpricot behavior closer to our customized REXML behavior
803
+ module Hpricot #:nodoc:
804
+ class Doc #:nodoc:
805
+ include AssertXPath::CommonXPathExtensions
806
+
807
+ def [](index) #:nodoc:
808
+ return root[index] if symbolic? index
809
+ super
810
+ end
811
+
812
+ def text
813
+ return root.text
814
+ end
815
+
816
+ def method_missing(*args, &block) #:nodoc:
817
+ # if got = search(symbol).first get first descendant working here!
818
+ # ERGO call root here
819
+ symbol = args.first.to_s.sub(/\!$/, '')
820
+
821
+ root.children.grep(Hpricot::Elem).each do |kid|
822
+ if kid.name == symbol
823
+ return kid.drill(&block)
824
+ # ERGO assert on return value
825
+ # ERGO pass kid in for if you want it
826
+ end
827
+ end
828
+ # ERGO raise here?
829
+ end
830
+ end
831
+
832
+ class Elem #:nodoc:
833
+ include AssertXPath::CommonXPathExtensions
834
+
835
+ def [](index) #:nodoc:
836
+ # ERGO do this conflict with anything?
837
+ if symbol = symbolic?(index)
838
+ return attributes[symbol] if attributes.has_key? symbol
839
+ raise_magic_member_not_found(symbol, caller)
840
+ end
841
+
842
+ super
843
+ end
844
+
845
+ def text # simulate REXML style - fetch child with text
846
+ return (text? ? to_s : '') + children.select(&:text?).map(&:to_s).compact.join
847
+ end
848
+
849
+ def node_type
850
+ return :element # ERGO make me less useless
851
+ end
852
+
853
+ def drill(&block)
854
+ if block
855
+ # ERGO harmonize with bang! version
856
+ # ERGO deal if the key ain't a valid variable
857
+ # ERGO get method_missing to stop returning []
858
+ unless tribute(block) # ERGO pass in self (node)?
859
+ sib = self
860
+ nil while (sib = sib.next_sibling) and sib.node_type != :element
861
+ q = sib and _bequeath_attributes(sib).drill(&block)
862
+ return sib if q
863
+ raise Test::Unit::AssertionFailedError.new("can't find beyond <#{_esc xpath}>")
864
+ end
865
+ end
866
+
867
+ return self
868
+ # ERGO if block returns false/nil, find siblings until it passes.
869
+ # throw a test failure if it don't.
870
+ # ERGO axis concept
871
+ end
872
+
873
+ def method_missing(*args, &block) #:nodoc:
874
+ symbol = args.shift.to_s.sub(/\!$/, '')
875
+
876
+ children.grep(Hpricot::Elem).each do |kid|
877
+ if kid.name == symbol
878
+ kid.tribute(block)
879
+ # ERGO assert on return value
880
+ # ERGO pass kid in for if you want it
881
+ return kid
882
+ end
883
+ end
884
+
885
+ raise_magic_member_not_found(symbol, caller) # ERGO repurpose!
886
+ end
887
+ end
888
+
889
+ end
890
+
891
+
892
+ module XML
893
+ class Node #:nodoc:
894
+ include AssertXPath::CommonXPathExtensions
895
+
896
+ def search(xpath, &block)
897
+ if block
898
+ find(xpath, "x:http://www.w3.org/1999/xhtml").each(&block)
899
+ #find(xpath, &block)
900
+ end
901
+ return [find_first(xpath, "x:http://www.w3.org/1999/xhtml")]
902
+ end
903
+
904
+ def text
905
+ #p text?
906
+ find_first('text()').to_s
907
+ #text? ? content : ''
908
+ end
909
+
910
+ def inner_text(interstitial = '')
911
+ array = []
912
+ self.find( 'descendant-or-self::text()' ).each{|x| array << x }
913
+ return array.join(interstitial)
914
+ end # ERGO match??
915
+
916
+ def attributes
917
+ hash = {}
918
+ each_attr{|attr| hash[attr.name] = attr.value }
919
+ return hash # ERGO uh, was there a leaner way??
920
+ end
921
+
922
+ def [](index) #:nodoc:
923
+ return attributes[index.to_s] || super
924
+ end
925
+
926
+ def get_path(xpath)
927
+ node = find_first(xpath.to_s)
928
+ return _bequeath_attributes(node) if node
929
+ end # ERGO test that attributes are bequeathed!
930
+
931
+ alias :/ get_path
932
+
933
+ def method_missing(*args, &block) #:nodoc:
934
+ # ERGO use the define_method trick here
935
+ symbol = args.shift.to_s
936
+ symbol.sub!(/\!$/, '')
937
+
938
+ kid = if symbol == '/'
939
+ find_first('/')
940
+ else
941
+ find_first("./#{symbol}")
942
+ end
943
+ return _bequeath_attributes(kid).drill(&block) if kid
944
+ raise_magic_member_not_found(symbol, caller)
945
+ end
946
+ end
947
+
948
+ end
949
+
950
+
951
+ class REXML::Element
952
+ include AssertXPath::CommonXPathExtensions
953
+
954
+ # Semi-private method to match Hpricotic abilities
955
+ def search(xpath)
956
+ return self.each_element( xpath ){}
957
+ end
958
+
959
+ def method_missing(*args, &block) #:nodoc:
960
+ symbol = args.shift
961
+
962
+ each_element("./#{symbol}") do |kid|
963
+ return _bequeath_attributes(kid).drill(&block)
964
+ end # ERGO element/:child - def/
965
+
966
+ raise_magic_member_not_found(symbol, caller) # ERGO repurpose!
967
+ end # ERGO convert attribute chain to xpath
968
+
969
+ # Returns all text contents from a node and its descendants
970
+ #
971
+ # Example:
972
+ #
973
+ # assert_match 'can\'t be blank', assert_tag_id(:div, :errorExplanation).inner_text.strip
974
+ #
975
+ # %transclude AssertXPathSuite#test_inner_text
976
+ #
977
+ def inner_text(interstitial = '')
978
+ return self.each_element( './/text()' ){}.join(interstitial)
979
+ end # ERGO match??
980
+
981
+ def get_path(xpath)
982
+ node = each_element(xpath.to_s){}.first
983
+ return _bequeath_attributes(node) if node
984
+ end # ERGO test that attributes are bequeathed!
985
+
986
+ alias :/ get_path
987
+
988
+ # ERGO use set_backtrace to seat the backtracer to your code
989
+ # ERGO move _bequeath stuff in here!
990
+ # ERGO phase out the missing_method stuff that adds props
991
+
992
+ end
993
+
994
+
995
+ # FIXME hpricot, libxml, rexml always in alpha order
996
+
997
+ # http://www.oreillynet.com/ruby/blog/2008/01/assert_efficient_sql.html
998
+ # http://www.oreillynet.com/onlamp/blog/2007/09/big_requirements_up_front.html
999
+ # http://www.oreillynet.com/onlamp/blog/2007/08/assert_hpricot_1.html
1000
+ # http://www.oreillynet.com/onlamp/blog/2007/08/xpath_checker_and_assert_xpath.html
1001
+ # http://phlip.eblogs.com/2007/07/28/javascriptpureperl-for-ruby-enthusiasts/
1002
+ # http://www.oreillynet.com/onlamp/blog/2007/07/assert_latest_and_greatest.html
1003
+ # http://www.oreillynet.com/onlamp/blog/2007/07/assert_raise_on_ruby_dont_just.html
1004
+ # http://phlip.eblogs.com/2007/01/02/growl-driven-development/