Validates this page at W3C

assert{ xpath }

xpath{} tests XML and XHTML

xpath{} works closely with assert{ 2.0 } to provide elaborate, detailed, formatted reports when your XHTML code has gone astray.

Installation

xpath{} bundles with assert{ 2.0 } and assert{ 2.1 }. Install one of them first, then

 require 'assert2/xpath'

That also imports assert{ 2.0 }.

Strategy

First, generate your XHTML, then pass it into assert_xhtml() . Use _assert_xml() if all you have is a fragment of XHTML, or XML from some other schema.

Use a call like title = xpath('/html/head/title') alone, without assert{}, to extract XML nodes non-judgementally. It returns a nil when it fails. The returned node will provide text and attribute helper methods.

To learn XPath, read "XPath Checker and assert_xpath", and attach an XPath tool like XPath Checker or XPather to your Firefox web browser.

Then wrap your xpath() calls in assert{} to verify their details. For example, if your production code generates a <form> with a useful edit field in it, you can capture the field like this:

  assert{ xpath('//input[ @type = "text" and @name = "user" and 

                              @value = "Roosevelt Franklin" ]') }

You can program xpath() using XPath notation, or convenient Ruby option-hash notation. The equivalent query is:

  assert{ xpath(:input, :type => :text, :name => :user", 

                              :value => 'Roosevelt Franklin') }

When these assertions fail, xpath() works closely with assert{} to present a detailed, formatted, readable report of the XML node that failed, and its relevant context within a document.

xpath{} can evaluate a block; if this contains more xpath() calls, they nest inside the outer xpath{}'s context. This lets you skip over irrelevant details in a document, and only test the relevant regions.

If a nested xpath{} fails, the diagnostic only reflects the inner context. This is you don't have to read an entire document to find the part that failed.

assert_xhtml( xhtml )

Use this method to push well-formed XHTML into the assertion system. Subsequent xpath() calls will interrogate this XHTML, using XPath notation:

  def test_assert_xhtml
    
    assert_xhtml '<html><body><div id="forty_two">42</div></body></html>'
    
    assert{ xpath('//div[ @id = "forty_two" ]').text == '42' }
  end

_assert_xml()

Some versions of assert_xhtml() fuss when passed an XHTML fragment, or XML under some other schema. Use _assert_xml() to bypass those conveniences:

  def test__assert_xml
    
   _assert_xml '<Mean><Woman>Blues</Woman></Mean>'
   
    assert{ xpath('/Mean/Woman').text == 'Blues' }
  end

xpath( 'path' )

The function's first argument can be raw XPath in a string, or a symbol. The first symbol gets decorated with the 'descendant-or-self::' XPath axis, and a second symbol, or an option hash, get converted into a predicate, like [ @id = "forty_two" ].

All the following queries reach out to the same node. Prefer the last notation, to cut thru a large XHTML web page down to the element containing the contents that you need to test:

  def test_assert_xpath
    assert_xhtml '<html><body><div id="forty_two">42</div></body></html>'
    
    assert{ xpath('descendant-or-self::div[ @id = "forty_two" ]').text == '42' }
    assert{ xpath('//div[ @id = "forty_two" ]').text == '42' }
    assert{ xpath(:'div[ @id = "forty_two" ]').text == '42' }
    assert{ xpath(:div, :id => :forty_two).text == '42' }
    assert{ xpath(:div, :forty_two).text == '42' }
  end

xpath( DSL )

You can write simple XPath queries using Ruby's familiar hash notation. Query a node's string contents with ?.:

  def test_xpath_dsl
    assert_xhtml 'hit
                  <a href="http://antwrp.gsfc.nasa.gov/apod/"
                    >apod</a>
                  daily!'
    assert do

      xpath :a, 
            :href => 'http://antwrp.gsfc.nasa.gov/apod/',
            ?. => 'apod'  #  the ?. resolves to XPath: a[ . = "apod" ]

    end
  end

xpath( ID shortcut )

When xpath()'s second argument is a :symbol, it expands to [ @id = "symbol" ]. Subsequent arguments use Hash notation:

  def test_xpath_ID_shortcut
    assert_xhtml '<html><body>
                    <div id="forty_two" class="answer_to_the_great_question">
                      42
                    </div>
                  </body></html>'

    div_1 = xpath(:div, :forty_two, :class => :answer_to_the_great_question)
    div_2 = xpath(:div, :id => :forty_two, :class => :answer_to_the_great_question)

    assert{ div_1.text =~ /42/ and div_1 == div_2 }
  end

xpath().text

xpath() returns the first matching REXML::Node object (or nil if it found none). The object has a useful method, .text, which returns the nearest text contents:

  def test_xpath_text
   _assert_xml '<Mean><Woman>Blues</Woman></Mean>'
    assert do

      xpath('/Mean/Woman').text == 'Blues'

    end
  end

xpath() Attribute Accessors

Raw REXML::Nodes require a cluttered syntax to access node attributes. Because xpath() supports expedient queries, it adds a Hash-like accessor to returned nodes:

  def test_indent_xml
   _assert_xml '<a href="http://www.youtube.com/watch?v=lWqr3mFAJ0Y"
                    >YouTube - UB40 - Sardonicus</a>'
    xpath '/a' do |a|

      a.attributes['href'] =~ /youtube/ and #  raw REXML::Node
      a['href']            =~ /youtube/ and #  permitted by xpath()
      a[:href ]            =~ /youtube/     #  convenient!

    end
  end

Nested xpath{}

xpath{} takes a block, and passes this to assert{}. Any further xpath() calls, inside this block, evaluate within the XML context set by the outer block.

This is useful because if you have a huge web page (such as the one you are reading) you need assertions that operate on one small region alone - the place where a feature must appear. You can typically give all your <div> regions useful ids, then use xpath(:div, :my_id) to restrict further xpath{} calls:

  def test_nested_xpaths
    assert_xhtml (DocPath + 'assert2_xpath.html').read
    assert 'this tests the panel you are now reading' do

      xpath :a, :name => :Nested_xpath do  #  finds the panel's anchor
        xpath '../following-sibling::div[1]' do   #  finds that a tag's adjacent sibling
          xpath :'pre/span', ?. => 'test_nested_xpaths' do |span|
            span.text =~ /nested/  #  the block passes the target node thru the |goalposts|
          end
        end
      end

    end
  end

xpath{ block } Calls assert{ block }

When xpath{} has a block, it passes its detected |node| into the block. If the block returns nil or false, it will flunk() the block, using assert{}'s inner mechanics:

  def test_xpath_passes_its_block_to_assert_2
   _assert_xml '<tag>contents</tag>'
    assert_flunk /text.* --> "contents"/ do

      xpath '/tag' do |tag|
        tag.text == 'wrong contents!'
      end

    end
  end

Nested xpath{} Faults

When an inner xpath{} fails, the diagnostic's "xml context:" field contains only the inner XML. This prevents excessive spew when testing entire web pages:

  def test_nested_xpath_faults
    assert_xhtml (DocPath + 'assert2_xpath.html').read
    diagnostic = assert_flunk /BAD.*CONTENTS/ do
      xpath :a, :name => :Nested_xpath_Faults do

         # the ../ finds this entire panel, the div[1] finds its content area,
         # and the pre finds the code sample you are reading
        xpath '../following-sibling::div[1]/pre' do
          
            # this will fail because that text ain't found in a span
            # (the concat() makes it into two spans!)
          xpath :'span[ . = concat("BAD", " CONTENTS") ]'
        end

      end
    end
#    puts diagnostic
      # the diagnostic won't cantain the string "excessive spew", from
      # the top of the panel, because the second xpath{} call excluded it
    deny{ diagnostic =~ /excessive spew/ } #  FIXME  uh, this is the wrong context!!
  end

xpath( ?. ) Matches Recursive Text

When an XML node contains child nodes with text, the XPath predicate [ . = "..." ] matches all their text, concatenated together. xpath()'s DSL converts ?. into that notation:

  def test_nested_xpath_text
   _assert_xml '<boats><a>frig</a><b>ates</b></boats>'

    assert{ xpath :boats, ?. => 'frigates' }
  end

xpath( ?. ) Notation Is not the Same as xpath().text

xpath() returns the first node, in document order, which matches its XPath arguments. So ?. will force xpath() to keep searching for a hit.

xpath().text will find the first matching node, then offer its .text for comparison. These assertions explicate the difference:

  def test_xpath_text_is_not_the_same_as_question_dot_notation
    _assert_xml '<Mean>
                  <Woman>Blues</Woman>
                  <Woman>Dub</Woman>
                </Mean>'
    assert do

      xpath(:Woman).text == 'Blues' and
      xpath(:Woman, ?. => :Dub).text == 'Dub'
                   # use a symbol ^ to match a string here, as a convenience

    end
  end

Use indent_xml to Help Build your XPath

To see what region xpath{} has selected in a big document, temporarily add puts indent_xml and to xpath's block, then run your tests:

  def test_indent_xml
    assert_xhtml (DocPath + 'assert2_xpath.html').read
    xpath :span, ?. => :test_indent_xml do
      xpath '..' do
        
        # puts indent_xml and  #  Decomment this to see where you are in the document now
        
        indent_xml.match("<pre>\n") and
        ! indent_xml.match('<html')
      end
    end
  end