[rspec-users] response.body.should be_xml_with -- how to do nested xpath matches

Phlip phlip2005 at gmail.com
Sun Mar 8 13:00:45 EDT 2009


RSpecsters:

I like nested XPath expressions, because the outer XPath assertion can clip the 
diagnostic of the inner XPath assertion. You don't get the whole page spewed 
into your face when you fault!

This specification, for example, tests Yury Kotlyarov's user login page, from:

   http://github.com/yura/howto-rspec-custom-matchers/tree/master

   it 'should have xpathic tags' do
     render '/users/new'

     response.body.should be_xml_with{
       xpath :form, :action => '/users' do
         xpath :fieldset do
           xpath :'legend[ contains(., "Personal Information") ]' and
           xpath :'label[ contains(., "First name") ]' and
           xpath :input, :type => 'text', :name => 'user[first_name]'
         end
       end
     }
   end

That tests this (otherwise innocuous) new.html.erb:

<form action="/users">
   <fieldset>
     <legend>Personal Information</legend>
     <ol>
       <li id="control_user_first_name">
         <label for="user_first_name">First name</label>
         <input type="text" name="user[first_name]" id="user_first_name" />
       </li>
     </ol>
   </fieldset>
</form>

If that code had any major complex <%= erb %> activity, the tests^W 
specifications would keep it honest.

Let's change the specification, to simulate a bug, and try it:

     xpath :input, :type => 'text', :name => 'user[first_nome]' # was _name

That provides this mind-blast of errata:

'/users/new should have xpathic tags' FAILED
xpath: "descendant-or-self::input[@type = $type and @name = $name]"
arguments: {"name"=>"user[first_nome]", "type"=>"text"}

xml context:

<fieldset>
   <legend>
     Personal Information
   </legend>
   <ol>
     <li id='control_user_first_name'>
       <label for='user_first_name'>
         First name
       </label>
       <input name='user[first_name]' type='text' id='user_first_name'/>
     </li>
   </ol>
</fieldset>

assert{ ( ( xpath(:"legend[ contains(., \"Personal Information\") ]") ) and
      ( ( ( xpath(:"label[ contains(., \"First name\") ]") ) and
     ( xpath(:input, { :type => "text", :name => "user[first_nome]" }) ) ) ) ) }
   --> nil - should pass 


xpath(:"legend[ contains(., \"Personal Information\") ]")
   --> <legend> ... </>

xpath(:"label[ contains(., \"First name\") ]")
   --> <label for='user_first_name'> ... </>

xpath(:input, { :type => "text", :name => "user[first_nome]" })
   --> nil

./spec/views/users/new.html.erb_spec.rb:63:
script/spec:5:

Finished in 0.116823 seconds

2 examples, 1 failure

Note that the error message restricted itself to the XHTML inside the <form> 
tag. This is a major benefit when diagnosing a huge page that failed. (But also 
note that your HTML, like your code, should come in small reusable snippets, 
such as partials, and that these should get tested directly!)

Soon I will upgrade this system to use Nokogiri instead of (>cough<) REXML.

Now, while I go and put the "simple" matcher that does this onto Twitter, 
YouTube, Mingle, Facebook, Mindfuck, Reddit, Tumblog, LinkedIn, and Gist, you 
all can just read it below my sig.

-- 
   Phlip
   http://www.zeroplayer.com/

require File.dirname(__FILE__) + "/../../spec_helper"
require 'assert2/xpath'
require 'spec/matchers/wrap_expectation'

Spec::Runner.configure do |c|
   c.include Test::Unit::Assertions
end  #  TODO blog this

describe "/users/new" do

   it "should have user form" do
     render '/users/new'
     response.should have_form('/users') do
       with_field_set 'Personal Information' do
         with_text_field 'First name', 'user', 'first_name'
       end
     end
   end

   class BeXmlWith

     def initialize(scope, &block)
       @scope, @block = scope, block
     end

     def matches?(stwing, &block)
       waz_xdoc = @xdoc

       @scope.wrap_expectation self do
         @scope.assert_xhtml stwing
         return (block || @block || proc{}).call
       end
     ensure
       @xdoc = waz_xdoc
     end

     attr_accessor :failure_message

     def negative_failure_message
       "yack yack yack"
     end
   end

   def be_xml_with(&block)
     BeXmlWith.new(self, &block)
   end

   def be_xml_with_(&block)
     waz_xdoc = @xdoc
     simple_matcher 'yo' do |given, matcher|
       wrap_expectation matcher do
         assert_xhtml given  #  this works
         block.call  #  crashes with a nil.first error!
       end
     end
   ensure
     @xdoc = waz_xdoc
   end

   it 'should have xpathic tags' do
     render '/users/new'

     response.body.should be_xml_with{
       xpath :form, :action => '/users' do
         xpath :fieldset do
           xpath :'legend[ contains(., "Personal Information") ]' and
           xpath :'label[ contains(., "First name") ]' and
           xpath :input, :type => 'text', :name => 'user[first_name]'
         end
       end
     }
   end

end



More information about the rspec-users mailing list