assert_xpath 0.3.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,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/