have_selectorin RSpec 2.x
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 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
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.should have_selector('...') end
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
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.
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
have_selector will accept this, thereby you might think you can just get away a string replacement
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.
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.should have_selector('...') elements.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
have_tag would accept most attribute selectors without quotes, but not so the Webrat.
Note the single quotes around
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
:conditions arrays could do. For example if you wanted to regular expression match, e.g. a form URL, you could do this.
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.
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_xpathseems 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 qualifier is one such example.
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.
If you’re trying to be bold enough to want to match classes other than
div tags with the class selector, e.g.
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:
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.