Posted on:
January 08, 2011
by Wolfram Arnold

From have_tag to have_selector in RSpec 2.x

In RSpec 2, view testing is not backwards compatible with RSpec 1.x. Notably the have_tag selector matcher is replaced with Webrat’s have_selector. Here’s what you need to know to make this transition smoother.

If you’ve moved a project to RSpec 2.x, you have probably run into a number of problems. Some make sense others less so. Particularly with some of the API-breaking changes I can’t help but wonder if they were made more for purist reasons than for pragmatic intention.

One of these is the disappearance of have_tag.

have_tag was removed, and you have to include Webrat or Capybara to import the have_selector syntax. Dave Chelimsky rationalized this that the implementation of have_selector in Webrat is better anyway. Perhaps so. I also grant him that he doesn’t want to maintain have_tag, which was a wrapper around the old but powerful assert_select library. But why not make it available as a plugin, like some of the pieces that were chucked out of Rails core?

My particular gripe with this is that the removal of have_tag and substitution with Webrat’s have_selector is not a drop-in replacement. You have to work for it.

Firstly, Webrat uses Nokogiri under the hood which is certainly a sound choice. Unfortunately Nokogiri’s processing of CSS pseudo class selectors, such as first-of-type and nth-of-type is broken in gem versions 1.4.4 and 1.5.0.beta3, which I tried.

have_tag relies on a different parser implementation that does not have these bugs.

I posted this gist which with details how to reproduce the Nokogiri issue.

Workarounds are not too difficult, by using a nested assertion block. So instead of…

rendered.should have_selector('form .doc_form:first-of-type') do
  have_selector('...')
end

…you can write…

rendered.should have_selector('form .doc_form') do |doc_forms|
  doc_form[0].should have_selector('...')
end

Beware of old 3-argument syntax

That workaround exposed another issue.

If you ever used with_tag in its 3-argument form, i.e.:

  response.should have_tag(".doc_form") do |tags|
     with_tag tags.first, "input[...]"
  end

then this code will silently pass. The 3-argument form of have_tag and with_tag accepts a node object as first argument and checks the 2nd argument against the first. In have_selector, however, the first argument is considered the expected value. Admittedly, have_tag was inconsistent in this regard, accepting the actual value once through calling context (2-argument from) or as first parameter (3-argument form). Hence perhaps the API-breaking change in RSpec 2.x.

Silent error with nested tag assertions

This discovery led me to another problem. In RSpec 1.x, nesting was possible without declaring a block variable:

  response.should have_tag('.doc_form') do  # no block variable
    with_tag('...')   # assertion operates inside surrounding tag selection
  end

Webrat’s have_selector will accept this, thereby you might think you can just get away a string replacement have_tag,with_tag –> have_selector. However this opens a subtle problem in that the nested assertions inside the block do not operate within the surrounding tag selection as you might think.

The fix

To fix this, you must use a block variable and repeat should for each have_selector assertion. That will pick up the correct tag selection from the surrounding context, even without explicit block variable. So, the solution is:

  rendered.should have_selector('.doc_form') do |f|
    f.should have_selector('...')   # Note the should at the beginning
  end

One nice thing about this is, if the selected elements are an array, then you can access them from the block variable:

  rendered.should have_selector('.doc_form') do |elements|
    elements[0].should have_selector('...')
    elements[1].should have_selector('...')
    # ...
  end

It would be nice if this worked without a block variable and the implicit context contained the elements returned by the surrounding selector. This, however, silently fails. If you just repeat should have_selector without reference to a block variable, Webrat (or RSpec 2.x?) silently pulls a new context out of the hat which contains the RSpec error message. Bug or feature? You decide.

  rendered.should have_selector('.doc_form') do
    should have_selector('...')  # THIS DOES NOT ASSERT AGAINST THE TAGS SELECTED BY THE SURROUNDING CONTEXT!!!
  end

Other issues?

have_tag would accept most attribute selectors without quotes, but not so the Webrat.

  have_tag("form[action=/people]")
  have_tag("input[name='people[document_id]'][value=12]")

becomes:

  have_selector("form[action='/people']")
  have_selector("input[name='people[document_id]'][value='12']")

Note the single quotes around /people and 12. For some reason in attribute selectors the value must be quoted if it’s a number or a slash, or presumably anything other than a normal word character.

Another things that’s gone is that have_text had the ability to interpolate with a ? operator, similar to what ActiveRecord :conditions arrays could do. For example if you wanted to regular expression match, e.g. a form URL, you could do this.

  have_tag("form[action=?]", %r{posts/.*/comments})

This functionality this concise has disappeared. CSS3 has a limited attribute matching option, with the *= operator, but that’s not as powerful as full regular expressions. And then you could do a match on the Nokogiri Elements returned in the block variable, but that takes more code, too.

HaveTag matcher?

What’s more Webrat adds its own HaveTag matcher class, but it’s in false disguise. It is a wrapper around have_selector, not actually a compatible implementation to RSpec’s previous have_tag matcher.

have_xpath seems more robust

I just discovered by browsing the sources that Webrat’s have_selector is just a wrapper around its HaveXpath matcher class, and the xpath stuff seems to work a little better. However, you have to work with XPath and the current trend and community expertise appears tilted strongly in favor of CSS selectors.

Count?

The :count qualifier is one such example.

Even though have_select accepts a :count argument, it is broken in the CSS implementation, but seems to work in the XPath implementation, see below. Example:

     rendered.should have_xpath('//div[@class="document"]', :count => 2)
     rendered.should have_selector('div.document', :count => 2)

These two lines do not behave the same. The first, the XPath selector, reports the correct count. The CSS selector claims the element was not found, even though it’s run on the exact same output as the XPath selector.

Note that even with the XPath matcher, the :count option has some rough edges: If the count is different from the expectation, then the error messages is not very helpful and perhaps even misleading:

expected following text to match xpath //div[@class="document"]:

It makes no mention of the actual vs. the expected count. Even if the expected element is on the page once, it will have the above error message.

Class selectors other than for div tags

If you’re trying to be bold enough to want to match classes other than div tags with the class selector, e.g.

   have_selector('span.error')

you’ll be disappointed again. The class matching rule apparently only works with div tags. I don’t know if this is a Webrat or a Nokogiri problem, but it’s disappointing. Again, using XPath works correctly:

   have_xpath('//span[@class="error"]')

Capybara anyone?

It’s seemed to me over the last couple of months that Webrat has been falling a bit behind and Capybara is the tool of choice as integration test framework. Capybara also has a have_selector matcher, however integration with rspec isn’t quite as far along yet. While using Webrat’s have_selector requires solely the inclusion of the Webrat gem in the Gemfile, this is not sufficient for Capybara. Capybara defines the has_selector? matcher on its Node object and I’m not sure if it’s easy or hard to make this available in RSpec 2.x view tests.

blog comments powered by Disqus