[ANN] assert2-0.4.6 provides assert_xhtml, an alternative to assert_select


#1

Rubyists:

Consider the following monstrosity, coded using assert_select:

assert_select “div#logo_box img[src=/0000/0001/logo.gif][alt=My
Company]”

Now, behold it rewritten to use assert_xhtml:

assert_xhtml do
div.logo_box! do
img :src => /logo.gif$/, :alt => ‘My Company’
end
end

That sample contains more Ruby; it’s not just one big string.

Still not convinced? Oh, I forgot the around the ! Try this:

assert_xhtml do
div.logo_box! do
a :href => ‘/’ do
img :src => /logo.gif$/, :alt => ‘My Company’
end
end
end

And we had an issue with the small logo sneaking into the logo_box once.
Let’s
exclude it:

assert_xhtml do
div.logo_box do
a :href => ‘/’ do
img :src => /logo.gif$/, :alt => ‘My Company’
end
without!{ img :src => /mini_logo.gif/ }
end
end

Imagine adding all that to assert_select - it would get much harder to
read, and
more complex. assert_xhtml uses Nokogiri::HTML::Builder notation, so
anything it
can build, you can query.

Version 0.4.6 adds all the following features. To begin, enter:

gem install nokogiri assert2

== require ‘assert2/xhtml’ ==

All assert{ 2.0 } dependencies are optional. If you have Nokogiri
(>=1.2.2), you can test Rails views like this:

  user = users(:Moses)
  get :edit_user, :id =>  user.id

  assert_xhtml do

    form :action =>  '/users' do
      fieldset do
        legend 'Personal Information'
        label 'First name'
        input :type =>  'text',
              :name =>  'user[first_name]'
              :value =>  user.first_name
      end
    end

  end

That’s a Rails functional test on a form. The assertion expects the form
to target the given action, and contain a fieldset, a legend, a label,
and
a populated text input field. The assertion forgives any other details,
such as intervening structural tags, excess spaces, or extra attributes;
and complains if any required detail is missing, out of order, or
ill-formed.

The DSL inside that block is Nokogiri::HTML::Builder notation. Generally
speaking, anything Nokogiri can build, you can specify.

=== arguments ===

Call assert_xhtml(my_xml){} to interrogate your XML. When called without
an argument, the method reads @response.body.

=== without! ===

Every assert* has a matching deny* method. assert_xhtml recognizes the
special element without! as a request to fail if the given elements
do indeed appear in your output:

 get :info, :record_id => record.id
 assert_xhtml do
   div :class => :content do
     without!{ div :class => :download }
   end
 end

That assertion will fail if the outer

tag does not
exist, or if any inner
does exist.

The without! element respects your document layout. This assertion
passes…

 assert_xhtml SAMPLE_LIST do
   ul{ li{ ul{ li 'Sales report'
       without!{ li 'All Sales report criteria' } } } }
 end

…even though the target document contains an

  • All Sales report
    criteria
  • :
     <ul style='font-size: 18'>
       <li>model
         <ul>
           <li>Billings report</li>
           <li>Sales report</li>
           <li>Billings criteria</li>
           <li>Common system</li>
         </ul>
       </li>
       <li>controller
         <ul>
           <li>All Sales report criteria</li>
           <li>All Billings reports</li>
         </ul>
       </li>
     </ul>
    

    The two

  • elements appear in different
      lists, so the assertion
      does not associate them.

      The committee does not yet know what without!{ without!{} } does, so
      please
      do not rely on its current behavior, whatever that is!

      === escapes ===

      Certain elements, such as and , have the same names as
      internal
      methods. If you experience a bizarre error message, such as “wrong
      argument
      type Hash (expected Array)”, add a ! to the end of the element, like
      this:

     assert_xhtml do
       h2 'Sites'
    
       select! :id => 'sites',
               :name => 'sites[]',
               :multiple => :multiple,
               :size => SaleController::LIST_SIZE
     end
    

    === text ===

    An element such as h3{ ‘text’ } will match

    text

    , with leading
    or
    trailing blanks, but it won’t match

    text

    . This
    rule
    prevents runaway matches between high- and low-level elements.

    The example for the next section illustrates how to mix text and
    attribute
    specifications on the same element.

    A text specification may be a /regexp/.

    === :xpath!=> ===

    assert_xhtml works by throwing away structural information. If you need
    more control over your structure, use an :xpath! attribute to apply raw
    XPath specifications to your target elements.

    This assertion detect the rather pedestrian fact that your
    element remains inside your block - and it did not
    escape and rampage off to somewhere else:

     assert_xhtml do
       title :xpath! => 'parent::head/parent::html' do
         text 'Chamber of Commerce - Info - Hope Orphanage'
       end
     end
    

    Note the XPath evaluates as a predicate of the target , so its
    parent
    axis lists the familiar elements in reverse.

    That code also shows the ‘text’ directive, inserting text contents
    directly
    into the enclosing element. A future version of Nokogiri will allow the
    element’s first argument to specify its text.

    An :xpath! of a number evaluates to the 1-based index of an item in its
    parent. This assertion forces list items to appear in the correct order:

     assert_xhtml do
       ul :style => 'font-size: 18' do
         li 'model' do
           li(:xpath! => 1){ text 'Sales report'  }
           li(:xpath! => 2){ text 'Billings report' }
           li(:xpath! => 3){ text 'Billings criteria' }
         end
       end
     end
    

    === :verbose! => true ===

    Sometimes when an assertion fails, you can’t tell why. To see each
    context the assertion considers, add :verbose! => true to the lowest
    element you know works, and run the tests:

     assert_xhtml SAMPLE_FORM do
       fieldset do
         li :verbose! => true do
           label 'First name', :for => :user_first_name
         end
       end
     end
    

    The verbose option works as “spew”, not as a diagnostic, and it reports
    each considered element’s contents.

    Because XPath evaluates the , in our example, before the

  • ,
    you
    might need to comment the out to see a successful spew on the
  • .

    === scope ===

    assert_xhtml{} yields its block to Nokogiri::HTML::Builder, which turns
    every method call into an HTML element. This freedom comes at a price -
    you can’t easily call your own methods!

    Use this scope trick to pass your outer scope into the specification:

      get :edit_user, :id => users(:Moses).id
      scope = self
    
      assert_xhtml do
        form :action => '/users' do
          input :value => scope.users(:Moses).first_name
        end
      end
    

    Notice we could improve that test by declaring a variable,
    user = users(:Moses), in the outer scope, and simply passing
    the user variable itself into the specification.

    === :class=> ===

    The :class attribute is magic. This assertion passes…

     assert_xhtml SAMPLE_LIST do
       ul :class => :kalika do
         li 'Billings report'
       end
     end
    

    …despite the actual HTML contains

      . This
      feature
      simulates the CSS Selector notation that matches classes by their
      cascading
      effects.

      === class & ID shortcuts ===

      Nokogiri expands div.rad.thing! to

      . That
      means you don’t need to write div :class => ‘rad’, :id => ‘thing’ (or
      ul :class => :kalika). You can then put other arguments after the
      shortcut,
      and the
      in our example receives them, too.

      === diagnostic message ===

      When this assertion fails, it attempts to print out…

      • your reference elements, rendered as HTML
      • each “near-miss” region of your sample HTML

      The next version will feature much better diagnostics. Until they work,
      if these
      diagnostics are not sufficient, put :verbose! on the lowest element you
      think
      works, and comment out its contents…

      === RSpec ===

      The matching “specification”, in RSpec language, is be_html_with{}.
      Its syntax and behavior are the same:

      it ‘should have a cute form’ do
      render ‘/users/new’

       response.body.should be_html_with{
         form :action => '/users' do
           fieldset do
             legend 'Personal Information'
             label 'First nome'
             input :type => 'text', :name => 'user[first_name]'
           end
         end
       }
      

      end

      Good hunting!


      Phlip