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

12

48

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?

Houser answered 7/12, 2011 at 22:32 Comment(0)
H
38

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/
Houser answered 10/12, 2011 at 4:17 Comment(5)
And with plain Minitest: assert page.body =~ /ITEM1.*ITEM2.*ITEM3/Uniflorous
You could also check for the end of a tag: expect(page) =~ >ITEM1<.*>ITEM2<.*>ITEM3<Flabellum
Thank you, very elegant solution. I ended up using expect(response.body).to match(/ITEM1.*ITEM2.*ITEM3/m) with RSpec 4 and multi-line matchMidshipman
Can someone explain what ITEM represents in these examples? Are those class names?Hydroscope
@Barryman9000 It was the actual text in each of the <li>Houser
V
29

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.

Viole answered 8/11, 2012 at 16:0 Comment(1)
nth-last-child selects the nth child from the bottom. quirksmode.org/css/nthlastchild.htmlViole
P
28

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
Phage answered 8/2, 2013 at 9:56 Comment(5)
Using Webrat I get "undefined local variable or method `page'"Metametabel
@Metametabel sorry, i can't help there. i've only used capybara so far.Phage
I've made it work with webrat. Already forked the project on github, will release my code when I have some time to change the docs and publish the gemMetametabel
I'm using this, and it works. I want to check order of items within a particular div, what should do?Lordly
The article is really good for further information.Darnel
Y
6

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.

Yardmaster answered 10/8, 2016 at 14:57 Comment(1)
most pragmatic 💪Trickster
F
5

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.

Foresee answered 31/7, 2014 at 19:38 Comment(1)
Thanks - that article really helped me (we're using Minitest, not RSpec)Dietetic
T
2

This page has got a very clever way of testing the order of string elements on a page - be they in a list or not:

https://makandracards.com/makandra/789-match-strings-in-a-given-order-with-cucumber-and-capybara

It is in the form of a cucumber step, but you should be able to extract what you need for an Rspec step. It is similar to Johns 'kludgy' solution - but a bit fancier from what I can make out. All of their other cunning capybara testing steps can be found here:

https://github.com/makandra/spreewald

Tilsit answered 28/12, 2012 at 18:37 Comment(0)
F
1

Use the orderly gem, written by the author of the article mentioned previously.

It's as simple as:

expect(this).to appear_before(that)
Firedamp answered 26/5, 2016 at 4:31 Comment(0)
D
0

Using capybara-ui you could use the #widgets method to get all of the elements in top-down order.

# First define the widget,
# or reusable dom element reference.
# In this case in a role
class UserRole < Capybara::UI::Role
  widget :list_item, '.list-item'
end

# Then test the expected order using #widgets
role = UserRole.new
expected_order = ['buy milk', 'get gas', 'call dad']
actual_order = role.widgets(:list_item).map(&:text)

expect(actual_order).to eq(expected_order)
Deuteranope answered 23/1, 2016 at 0:59 Comment(0)
S
0

In above solutions, it was very difficult to test order if having multiple elements on page. So, I have found another way.

I have posted the solution here - Test order(sequence) of content using capybara

Mapping the arrays having css specific elements and then match that array with expected array(keep elements in sequence)

Snicker answered 25/1, 2018 at 2:32 Comment(1)
In the OP's case I think this is a flaky test waiting to happen. Mapping the specific elements if at least one already exists on the page prior to the javascript that adds a new one will immediately return only those elements that are currently on the page, which may not include the new one if the javascript takes too long. Better to be explicit and use positional pseudo-class selectors as other answers have suggested.Photothermic
B
0

Since page.body.should is deprecated, we can use regexp matching for a multiline comparing like a Ben said in a comment over there:

expect(page.body).to match /#{item1.name}.*#{item2.name}/m
Britt answered 21/10, 2020 at 11:25 Comment(0)
W
0

Here's a custom matcher that I currently use to test the order of page contents:

RSpec::Matchers.define :appear_before do |later_content|
  match do |earlier_content|
    earlier_content_index = page.body.index(earlier_content)
    later_content_index = page.body.index(later_content)

    @failure_message = "Expected \"#{earlier_content}\" to appear before \"#{later_content}\""

    def raise_missing_content_error(content)
      @failure_message += " but \"#{content}\" was not found on the page."
      raise RSpec::Expectations::ExpectationNotMetError
    end

    raise_missing_content_error(earlier_content) if earlier_content_index.nil?
    raise_missing_content_error(later_content) if later_content_index.nil?

    earlier_content_index < later_content_index
  end

  failure_message { @failure_message }
end

You can then use it like this:

expect('My new post').to appear_before('My old post')
Woad answered 24/4, 2022 at 16:30 Comment(0)
R
-1

Capybara should support the + selector (I believe it uses Nokogiri, which certainly supports this). Are you perhaps using an old version? Or are you using a Capybara driver that doesn't support JavaScript, so that the extra element isn't getting rendered at all?

Also, don't use RSpec for testing your HTML. Cucumber is far better at this. For interacting with JavaScript, you'll want to use Selenium or Capybara-Webkit. Or if you have a lot of JavaScript, consider testing it with Jasmine.

Romanaromanas answered 7/12, 2011 at 23:15 Comment(3)
I don't know why you got a downvote, but you're perfectly correct about Capybara (well, Nokogiri) supporting the + selector. My suspicion is the new li was not being added adjacent to ITEM #1, and he should have used the ~ selector instead.Thaumaturgy
How is cucumber better at testing html?Kehoe
@DrewV Cucumber is better at doing assertions about user-facing content in user-facing terms. RSpec is at the wrong level of abstraction for testing UI.Romanaromanas

© 2022 - 2024 — McMap. All rights reserved.