Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master

Fetching latest commit…

Cannot retrieve the latest commit at this time

Failed to load latest commit information.
doc
lib
test
.gemified
README
Rakefile

README

The assert{ 2.0 } project asks a simple question:

  How the leanest possible assertions 
  can yield the maximum diagnostics?

Its latest assertion, assert_xhtml, answers this question for HTML.

To begin, grab it with:

  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 <div class='content'> tag does not
exist, or if any inner <div class='download'> 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 <li>All Sales report
criteria</li>:

    <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 <li> elements appear in different <ul> 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 <select> and <id>, 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 <h3> text </h3>, with leading and
trailing blanks, but it won't match <h3><span>text</span></h3>. This rule
prevents runaway matches between high- and low-level elements.

A text specification may be a /regexp/, and an element may contain both text 
and attributes:

  h2 /Sites/, :style => /right/

The next section illustrates the text() directive.

===:xpath!=>===

assert_xhtml works by throwing away structural information. If your document
structure is more unruly and wild, it might need more constraints. Use an :xpath!
attribute to apply raw XPath specifications to your target elements.

This assertion detect the rather pedestrian fact that your <title>
element remains inside your <html><head> 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

(That code also shows the 'text' directive, inserting text contents directly
into the enclosing element. Sometimes the directive is the most convenient
way to specify the content.)

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 <label>, in our example, before the <li>, you
might need to comment the <label> out to see a successful spew on the <li>.

===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 <ul class='kalika goddess'>. This feature
simulates the CSS Selector notation that matches classes by their cascading 
effects.

===class & ID shortcuts===

Nokogiri expands div.rad.thing! to <div class='rad' id='thing'/>. 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 <div> in our example receives them, too.

===diagnostic message===

When this assertion fails, it prints out...

 - your reference elements, rendered as HTML
 - the best "near-miss" region of your sample HTML

Note that the diagnostic displays without! and xpath! tags literally.

If your assertion fails, and the diagnostic does not display a relevant
near-miss, the best recourse is to wrap your target element with a known
container element. Don't do this:

  assert_xhtml do
    li 'my one lonely list item'
  end

Do this:

  assert_xhtml do
    ul.my_unordered_list! do
      li 'my one lonely list item'
    end
  end

That is guaranteed to match the first <ul id='my_unordered_list'> in your
document.

===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!
Something went wrong with that request. Please try again.