Module: AssertXPath

Description

See: assertxpath.rubyforge.org/

                        ___________________________
    #                __/   >tok tok tok<           \
    #< <%<          <__  assert_xpath reads XHTML  |
    #   (\\=           |  and queries its details! |
    #    |              ---------------------------
    #===={===
    #

Public Instance methods


assert_any_xpath (xpath, matcher = nil, diagnostic = nil, &block)

Search nodes for a matching XPath whose AssertXPath::Element#inner_text matches a Regular Expression. Depends on assert_xml

  • xpath - a query string describing a path among XML nodes. See: XPath Tutorial Roundup
  • matcher - optional Regular Expression to test node contents
  • diagnostic - optional string to add to failure message
  • block|node| - optional block called once per match. If this block returns a value other than false or nil, assert_any_xpath stops looping and returns the current node

Example:

    def test_assert_any_xpath
      assert_xml '<yo><i>we</i><i zone="phone">find_me</i></yo>'
      assert_any_xpath '/yo/i', /find_me/
      assert_any_xpath :i, /find_me/
      assert_any_xpath :i, /we/
      assert_equal 'wefind_me', @xdoc.inner_text
      once = 0
      
      assert_any_xpath :i, /find_me/ do |i|
        assert_equal '<i zone=\'phone\'>find_me</i>', indent_xml.strip
        assert_equal 'phone', i[:zone]  #  ERGO  i.zone should raise if not found
        once += 1
        false  #  stops the loop
      end

      assert_equal 1, once
      once = 0
      
      node = assert_any_xpath :i do
               assert_equal '<i>we</i>', indent_xml.strip
               once += 1
               true
             end
      
      assert_equal 1, once
      assert_equal 'we', node.text
      twice = 0

      assert_any_xpath :i, /e/ do |node|
        assert_match /we|find_me/, node.text
        twice += 1
        false  #  false means keep looping
      end

      assert_equal 2, twice
      twice = 0

      assert_any_xpath :i do |node|
        assert_match /we|find_me/, node.text
        twice += 1
        false  #  false means keep looping
      end

      assert_equal 2, twice

      assert_raise_message AFE, /should find.*frootloop/ do
        assert_any_xpath :frootloop
      end
      
      assert_raise_message AFE, /can find.*i.*but can't find.*don't find me/ do
        assert_any_xpath :i, /don't find me/
      end
    end
     # File lib/assert_xpath.rb, line 506
506:   def assert_any_xpath(xpath, matcher = nil, diagnostic = nil, &block)
507:     matcher ||= //
508:     block   ||= lambda{ true }
509:     found_any = false
510:     found_match = false
511:     xpath = symbol_to_xpath(xpath)
512: 
513:     stash_xdoc do  #  ERGO  check that stash_xdoc does _not_ mess with @hdoc
514:       #assert_xpath xpath, diagnostic
515: 
516:       if @hdoc
517:         @xdoc.search(xpath) do |@xdoc|
518:           found_any = true
519:           
520:           if @xdoc.inner_text =~ matcher
521:             found_match = true
522:             _bequeath_attributes(@xdoc)
523:             return @xdoc  if block.call(@xdoc)
524:             #  note we only exit block if block.nil? or call returns false
525:           end
526:         end
527:       else  # ERGO      merge!
528:         @xdoc.each_element(xpath) do |@xdoc|
529:           found_any = true
530:           
531:           if @xdoc.inner_text =~ matcher
532:             found_match = true
533:             _bequeath_attributes(@xdoc)
534:             return @xdoc  if block.call(@xdoc)
535:             #  note we only exit block if block.nil? or call returns false
536:           end
537:         end
538:       end
539:     end
540: 
541:     found_any or
542:       flunk_xpath(diagnostic, "should find xpath <#{_esc xpath}>")
543:       
544:     found_match or
545:       flunk_xpath(
546:           diagnostic, 
547:           "can find xpath <#{_esc xpath}> but can't find pattern <?>",
548:           matcher
549:           )
550:   end

assert_hpricot (*args, &block)

This parses one XML string using Hpricot, so subsequent calls to assert_xpath will use Hpricot expressions. This method does not depend on invoke_hpricot, and subsequent test cases will run in their suite‘s mode.

Example:

    def test_assert_hpricot
      assert_hpricot '<yo><i>nope</i><i class="foo">b xml</i></yo>'
      assert_kind_of Hpricot::Doc, @hdoc
      called_once = false
      @hdoc.search('//i'){  called_once = true  }
      assert called_once
      assert_equal @xdoc, @hdoc
    end

See also: assert_hpricot

     # File lib/assert_xpath.rb, line 420
420:   def assert_hpricot(*args, &block)
421:     xml = args.shift
422:     xml ||= @xdoc || @response.body
423:   #  ERGO  document that callseq!
424:     require 'hpricot'
425:     @xdoc = @hdoc = Hpricot(xml.to_s)  #  ERGO  take that to_s out of all callers
426:     return assert_xpath(*args, &block)  if args.length > 0
427:     return @xdoc
428:   end

assert_rexml (*args, &block)

Processes a string of text, or the hidden @response.body, using REXML, and sets the hidden @xdoc node. Does not depend on, or change, the values of invoke_rexml or invoke_hpricot

Example:

    def test_assert_rexml
      x = assert_rexml('<x id="42" />')
      assert_kind_of ::REXML::Element, x
      assert_equal '42', x[:id]
      x = assert_rexml('<x id="43" />')
      assert_equal '43', x[:id]
      assert_raise_message(AFE, /missing attribute.*v/){  x[:v]  }
    end
     # File lib/assert_xpath.rb, line 386
386:   def assert_rexml(*args, &block)
387:     contents = (args.shift || @response.body).to_s
388: #  ERGO  benchmark these things
389: 
390:     contents.gsub!('\\\'', '&apos;')
391:     contents.gsub!('//<![CDATA[<![CDATA[', '')
392:     contents.gsub!('//<![CDATA[', '')
393:     contents.gsub!('//]]>', '')
394:     contents.gsub!('//]>', '')
395: 
396:     begin
397:       @xdoc = REXML::Document.new(contents)
398:     rescue REXML::ParseException => e
399:       raise e  unless e.message =~ /attempted adding second root element to document/
400:       @xdoc = REXML::Document.new("<xhtml>#{ contents }</xhtml>")
401:     end
402:     
403:     _bequeath_attributes(@xdoc)
404:     assert_xpath(*args, &block)  if args != []
405:     return (assert_xpath('/*') rescue nil)  if @xdoc
406:   end

assert_tag_id (tag, id, diagnostic = nil, &block)

Wraps the common idiom assert_xpath(‘descendant-or-self::./my_tag[ @id = "my_id" ]’). Depends on assert_xml

  • tag - an XML node name, such as div or input. If this is a :symbol, we prefix ".//"
  • id - string or symbol uniquely identifying the node. This must not contain punctuation
  • diagnostic - optional string to add to failure message
  • block|node| - optional block containing assertions, based on assert_xpath, which operate on this node as the XPath ’.’ current node.

Returns the obtained REXML::Element node

Examples:

  assert_tag_id '/span/div', "audience_#{ring.id}" do
    assert_xpath 'table/tr/td[1]' do |td|
      #...
      assert_tag_id :form, :for_sale
    end
  end
    def test_assert_tag_id_and_tidy
      assert_tidy '<html><form id="for_sale">' +
                  '  <input type="text" id="item" value="boots">' +
                  '</html>', :quiet # about the two missing end tags!

      assert_tag_id :form, :for_sale do
        input = assert_tag_id('input', :item)
        assert_equal 'text', input[:type]
        assert_equal 'boots', input[:value]
      end
    end
    def test_assert_tag_id
      assert_xml '<html><form id="for_sale">
                    <input type="text" id="item" value="boots" />
                  </form></html>'

      assert_tag_id :form, :for_sale do
        input = assert_tag_id('input', :item)
        assert_equal 'text' , input[:type]
        assert_equal 'boots', input[:value]
      end
    end
     # File lib/assert_xpath.rb, line 610
610:   def assert_tag_id(tag, id, diagnostic = nil, &block)
611:   #  CONSIDER  upgrade assert_tag_id to use each_element_with_attribute
612:     assert_xpath build_xpath(tag, id), diagnostic, &block
613:   end

assert_tidy (messy = @response.body, verbosity = :noisy)

Thin wrapper on the Tidy command line program (the one released 2005 September)

  • messy - optional string containing messy HTML. Defaults to @response.body.
  • verbosity - optional noise level. Defaults to :noisy - any other value will repress all of Tidy‘s screams of horror regarding the quality of your HTML.

The resulting XHTML loads into assert_xml. Use this to retrofit assert_xpath tests to less-than-pristine HTML.

assert_tidy obeys invoke_rexml and invoke_hpricot, to select its HTML parser

Examples:

 get :adjust, :id => transaction.id  # <-- fetches ill-formed HTML
 assert_tidy @response.body, :quiet  # <-- upgrades it to well-formed
 assert_tag_id '//table', :payment_history do  # <-- sees good XML
   #...
 end
    def test_assert_tag_id_and_tidy
      assert_tidy '<html><form id="for_sale">' +
                  '  <input type="text" id="item" value="boots">' +
                  '</html>', :quiet # about the two missing end tags!

      assert_tag_id :form, :for_sale do
        input = assert_tag_id('input', :item)
        assert_equal 'text', input[:type]
        assert_equal 'boots', input[:value]
      end
    end
     # File lib/assert_xpath.rb, line 685
685:   def assert_tidy(messy = @response.body, verbosity = :noisy)
686:     scratch_html = RAILS_ROOT + '/tmp/scratch.html'
687:     #  CONSIDER  a railsoid tmp file system?
688:     #  CONSIDER  yield to something to respond to errors?
689:     File.open(scratch_html, 'w'){|f|  f.write(messy)  }
690:     gripes = `tidy -eq #{scratch_html} 2>&1`
691:     gripes.split("\n")
692:     
693:     exclude, inclued = gripes.partition do |g|
694:       g =~ / - Info\: /                                  or
695:       g =~ /Warning\: missing \<\!DOCTYPE\> declaration/ or
696:       g =~ /proprietary attribute/                       or
697:       g =~ /lacks "(summary|alt)" attribute/
698:     end
699:     
700:     puts inclued if verbosity == :noisy
701:     # inclued.map{|i| puts Regexp.escape(i) }
702:     assert_xml `tidy -wrap 1001 -asxhtml #{scratch_html} 2>/dev/null`
703:       #  CONSIDER  that should report serious HTML deformities
704:   end

assert_xml(xml = @response.body [, assert_xpath arguments]) → @xdoc, or assert_xpath's return value

Prepare XML for assert_xpath et al

  • xml - optional string containing XML. Without it, we read @response.body
  • xpath, diagnostic, block - optional arguments passed to assert_xpath

Sets and returns the new secret @xdoc REXML::Element root

Assertions based on assert_xpath will call this automatically if the secret @xdoc is nil. This implies we may freely call assert_xpath after any method that populates @response.body — if @xdoc is nil. When in doubt, call assert_xml explicitly

assert_xml also translates the contents of assert_select nodes. Use this to bridge assertions from one system to another. For example:

Returns the first node in the XML

Examples:

  assert_select 'div#home_page' do |home_page|
    assert_xml home_page  #  <-- calls home_page.to_s
    assert_xpath ".//img[ @src = '#{newb.image_uri(self)}' ]"
    deny_tag_id :form, :edit_user
  end
    def test_assert_long_sick_expression
      assert_xml '<a><b>c</b><b>d<e g="h">f</e><e g="i">j</e></b></a>'
      a = assert_xpath('a')
      assert_match /^c/, (a / :b).text #  ERGO  more accurate?
      assert_match /^c/, (a / 'b').text
      assert_match /^d/, (a / 'b[2]').text
      assert_match /^f/, (a / 'b/e').text
      assert_equal 'h', (a / 'b/e')[:g]    unless @use_hpricot
      assert_equal 'h', (a / 'b/e').first[:g]  if @use_hpricot
    end

See: AssertXPathSuite#test_assert_xml_drill

     # File lib/assert_xpath.rb, line 373
373:   def assert_xml(*args, &block)
374:     return assert_hpricot(*args, &block)  if @use_hpricot
375:     return assert_rexml(*args, &block)
376:   end

assert_xpath (xpath, diagnostic = nil, &block)

Return the first XML node matching a query string. Depends on assert_xml to populate our secret internal REXML::Element, @xdoc

  • xpath - a query string describing a path among XML nodes. See: XPath Tutorial Roundup
  • diagnostic - optional string to add to failure message
  • block|node| - optional block containing assertions, based on assert_xpath, which operate on this node as the XPath ’.’ current node

Returns the obtained REXML::Element node

Examples:

  render :partial => 'my_partial'

  assert_xpath '/table' do |table|
    assert_xpath './/p[ @class = "brown_text" ]/a' do |a|
      assert_equal user.login,   a.text  # <-- native <code>REXML::Element#text</code> method
      assert_match /\/my_name$/, a[:href]  # <-- attribute generated by +assert_xpath+
    end
    assert_equal "ring_#{ring.id}", table.id!  # <-- attribute generated by +assert_xpath+, escaped with !
  end
    def test_assert_xpath
      assert_xml '<bob><marley walkin="like he talk it" /></bob>'
      assert_xpath :bob
      assert_equal 'like he talk it', assert_xpath(:marley)[:walkin]
      
      assert_xpath :marley do
        deny_xpath :bob, 'we should not see <bob>, because he\'s above us'
        assert_xpath '/bob', 'REXML can see <bob> because / re-roots the search'  unless @use_hpricot
      end

      assert_xpath '/bob/marley'
      assert_xpath 'bob/marley'
      bob = assert_xpath(:bob)
      that = self
      block_was_called = false

      bob.marley do
        that.assert_equal 'like he talk it', @walkin # <-- an attribute
        block_was_called = true
        true
      end

      assert block_was_called, 'child-methods should call their blocks'
    end

See: AssertXPathSuite#test_indent_xml, XPath Checker

     # File lib/assert_xpath.rb, line 461
461:   def assert_xpath(xpath, diagnostic = nil, &block)
462: #     return assert_any_xpath(xpath, diagnostic) {
463: #              block.call(@xdoc) if block
464: #              true
465: #            }
466:     stash_xdoc do
467:       xpath = symbol_to_xpath(xpath)
468:       node  = @xdoc.search(xpath).first
469:       @xdoc = node || flunk_xpath(diagnostic, "should find xpath <#{_esc xpath}>")
470:       @xdoc = _bequeath_attributes(@xdoc)
471:       block.call(@xdoc) if block  #  ERGO  tribute here?
472:       return @xdoc
473:     end
474:   end

deny_any_xpath (xpath, matcher, diagnostic = nil)

Negates assert_any_xpath. Depends on assert_xml

  • xpath - a query string describing a path among XML nodes. This must succeed - use deny_xpath for simple queries that must fail
  • matcher - optional Regular Expression to test node contents. If xpath locates multiple nodes, this pattern must fail to match each node to pass the assertion.
  • diagnostic - optional string to add to failure message

Contrived example:

  assert_xml '<heathrow><terminal>5</terminal><lean>methods</lean></heathrow>'

  assert_raise_message Test::Unit::AssertionFailedError,
                          /all xpath.*\.\/\/lean.*not have.*methods/ do
    deny_any_xpath :lean, /methods/
  end

  deny_any_xpath :lean, /denver/

See: AssertXPathSuite#test_deny_any_xpath, assert_raise (on Ruby) - Don't Just Say "No"

     # File lib/assert_xpath.rb, line 573
573:   def deny_any_xpath(xpath, matcher, diagnostic = nil)
574:     @xdoc or assert_xml
575:     xpath = symbol_to_xpath(xpath)
576: 
577:     assert_any_xpath xpath, nil, diagnostic do |node|
578:       if node.inner_text =~ matcher
579:         flunk_xpath(
580:             diagnostic, 
581:             "all xpath <#{_esc xpath}> nodes should not have pattern <?>",
582:             matcher
583:             )
584:       end
585:     end
586:   end

deny_tag_id (tag, id, diagnostic = nil)

Negates assert_tag_id. Depends on assert_xml

Example - see: assert_xml

See: assert_tag_id

     # File lib/assert_xpath.rb, line 621
621:   def deny_tag_id(tag, id, diagnostic = nil)
622:     deny_xpath build_xpath(tag, id), diagnostic
623:   end

deny_xpath (xpath, diagnostic = nil)

Negates assert_xpath. Depends on assert_xml

Examples:

  assert_tag_id :td, :object_list do
    assert_xpath "table[ position() = 1 and @id = 'object_#{object1.id}' ]"
    deny_xpath   "table[ position() = 2 and @id = 'object_#{object2.id}' ]"
  end  #  find object1 is still displayed, but object2 is not in position 2
    def test_deny_xpath
      assert_xml '<kiwi><eland/></kiwi>'
      deny_xpath 'koodoo'

      assert_raise_message AFE, /should not find.*eland/ do
        deny_xpath './/eland'
      end

      assert_raise_message AFE, /should not find.*eland/ do
        deny_xpath :eland
      end
      
      assert_raise_message AFE, /should not find.*eland/ do
        deny_xpath '//eland'
      end
      
      #  Note:  deny_xpath 'eland' will fail
      #         with Hpricot and pass with REXML
      
      if @use_hpricot
        assert_raise_message AFE, /should not find.*eland/ do
          deny_xpath 'eland'
        end
      else
        deny_xpath 'eland'  #  REXML requires //
      end
    end
     # File lib/assert_xpath.rb, line 486
486:   def deny_xpath(xpath, diagnostic = nil)
487:     @xdoc or assert_xml
488:     xpath = symbol_to_xpath(xpath)
489: 
490:     @xdoc.search(xpath).first and
491:       flunk_xpath(diagnostic, "should not find: <#{_esc xpath}>")
492:   end

drill (&block)

ERGO document me

     # File lib/assert_xpath.rb, line 124
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

indent_xml (doc = @xdoc || assert_xml)

Pretty-print a REXML::Element or Hpricot::Elem

  • doc - optional element. Defaults to the current assert_xml document

returns: string with indented XML

Use this while developing a test case, to see what the current @xdoc node contains (as populated by assert_xml and manipulated by assert_xpath et al)

For example:

   assert_javascript 'if(x == 42) answer_great_question();'

   assert_js_if /x.*42/ do
     puts indent_xml  #  <-- temporary statement to see what to assert next!
   end

See: AssertXPathSuite#test_indent_xml

     # File lib/assert_xpath.rb, line 642
642:   def indent_xml(doc = @xdoc || assert_xml)
643:     if doc.kind_of?(Hpricot::Elem) or doc.kind_of?(Hpricot::Doc)
644:       zdoc = doc
645:       doc = REXML::Document.new(doc.to_s.strip) rescue nil
646:       unless doc  #  Hpricot didn't well-formify the HTML!
647:         return zdoc.to_s  #  note: not indented, but good enough for error messages
648:       end
649:     end
650: 
651: # require 'rexml/formatters/default'
652: # bar = REXML::Formatters::Pretty.new
653: # out = String.new
654: # bar.write(doc, out)
655: # return out
656: 
657: return doc.to_s  #  FIXME  reconcile with 1.8.6.111!
658: 
659:     x = StringIO.new
660:     doc.write(x, 2)
661:     return x.string  #  CONSIDER  does REXML have a simpler way?
662:   end

invoke_hpricot ()

This activates a secret variable, @use_hpricot, so subsequent assert_xml calls will use Hpricot. Use assert_hpricot to run one test case in Hpricot mode, and use invoke_hpricot, from your setup() method, to run entire suites in this mode. These test cases explore some differences between the two assertion systems:

    def test_assert_long_xpath
      assert_xml '<anna><marie><candy><lights>' +
                 '  <since><imp>' +
                 '    <pulp lay="things" />' +
                 '  </imp></since>' +
                 '</lights></candy></marie></anna>'

      assert_xpath '/anna/marie/candy/lights/since/imp/pulp[ @lay = "things" ]'
      anna = assert_xpath('/anna')
      pulp = anna/:marie/:candy/:lights/:since/:imp/:pulp
      assert_equal pulp.first[:lay], 'things'  if @use_hpricot
      assert_equal pulp[:lay],       'things'  unless @use_hpricot
      assert anna.marie.candy.lights.since.imp.pulp{ @lay == 'things' }
    end
     # File lib/assert_xpath.rb, line 330
330:   def invoke_hpricot
331:     @hdoc = @xdoc = nil
332:     @use_hpricot = true
333:   end

invoke_rexml ()

This passivates a secret variable, @use_hpricot, so subsequent assert_xml calls will use REXML. See invoke_hpricot to learn the various differences between the two systems

     # File lib/assert_xpath.rb, line 339
339:   def invoke_rexml
340:     @hdoc = @xdoc = nil
341:     @use_hpricot = false
342:   end