Is it possible to test the order of elements via RSpec/Capybara?

I'm using RSpec/Capybara as my test suite. I have some javascript that dynamically appends <li> to the end of a <ul>. I want to write a request spec to ensure that this is happening.

I tried using the has_css Capybara method and advanced CSS selectors to test for the ordering of the <li> elements, but Capybara doesn't support the + CSS selector.

Example:

page.should have_css('li:contains("ITEM #1")')
pseuo_add_new_li
page.should have_css('li:contains("ITEM #1")+li:contains("ITEM #2")')

Does anyone know of another way to test for ordering?


Solution 1:

I resolved this issue by testing for a regex match against the body content of the page. A bit kludgy, but it works.

page.body.should =~ /ITEM1.*ITEM2.*ITEM3/

Solution 2:

I found a more canonical way of testing this behaviour with CSS. You could user :first-child, :last-child and :nth-child(n) selectors in whichever assert you like.

In your example I'd try these assertions:

page.should have_tag("ul:last-child", :text => "ITEM #1")
pseuo_add_new_li
page.should have_tag("ul:nth-last-child(2)", :text => "ITEM #1")
page.should have_tag("ul:last-child", :text => "ITEM #2")

I hope this helps someone. Read more about this.

Solution 3:

this article lists several ways to test sort order in RSpec, the best of which seems to be this matcher:

RSpec::Matchers.define :appear_before do |later_content|
  match do |earlier_content|
    page.body.index(earlier_content) < page.body.index(later_content)
  end
end

Solution 4:

I have had the same issue recently and found this neat & ideal solution: http://launchware.com/articles/acceptance-testing-asserting-sort-order

It's even packaged as a tiny gem.

Solution 5:

You can use the all finder method to select multiple elements and then use collect to pull out the text into an array:

assert_equal page.all('#navigation ul li').collect(&:text), ['Item 1', 'Item 2', 'Item 3']

If your list isn't visible on the page such as a popup navigation menu, you need to pass visible: false into the all method.