Capybara: How To Assert A Given Number of Elements Exist - ruby-on-rails

I've upgraded my whole stack from a Rails 3.0 based project to 3.1. I have specs passing but my features are now being a bit picky. The issue I'm currently having is this step:
Then /^I should see (\d+) menu item(?:s)? within "([^"]*)"$/ do |count, selector|
page.find(:css, selector, :count => count.to_i)
end
And in the feature itself, I might put:
Then I should see 5 menu items within "tr#menu_item_row"
The message I get is:
Then I should see 5 menu items within "tr#menu_item_row" # features/step_definitions/admin_menu_steps.rb:1
Ambiguous match, found 5 elements matching css "tr#menu_item_row" (Capybara::Ambiguous)
./features/step_definitions/admin_menu_steps.rb:2:in `/^I should see (\d+) menu item(?:s)? within "([^"]*)"$/'
features/admin_menu.feature:30:in `Then I should see 5 menu items within "tr#menu_item_row"'
As far as I can tell, the 5 elements match the 5 that were actually found. Did I write this code wrong or has something major changed? Thanks!

If you want to check 5 elements you shouldn't use #find as by default since Capybara 2.0 this method always throws an exception if it finds more or less than one element. This was an intentional and (I believe) a good change.
To assert that 5 elements are present an appropriate method is a rspec matcher:
expect(page).to have_css(selector, count: count.to_i)
I don't recommend to set match to prefer_exact as recommended by #fontno as in most of situations you want Capybara to throw an exception if find finds more than one element.

Yes, this is a change between versions 1.x and 2.x. You can look at all the changes in the capybara upgrade guide and this blog post.
The find method now raises an ambiguous match error if more than one element is found. If you only have a few examples you could do something like this
Then /^I should see (\d+) menu item(?:s)? within "([^"]*)"$/ do |count, selector|
page.find(:css, selector, :count => count.to_i, match: prefer_exact)
end
or if you have many examples like this you could change the capybara configuration for backwards compatability, something like this
Capybara.configure do |config|
config.match = :prefer_exact
config.ignore_hidden_elements = false
end
You may have to modify this to get it working but this is the general idea. See the links I mention above, its all in there. Hope this sets you in the right direction

Related

RSpec Capybara Element at X no longer present in the DOM

I have a RSpec test with this:
within all('tr')[1] do
expect(page).to have_content 'Title'
expect(page).to have_content 'Sub Title'
end
And it's failing at expect(page).to have_content 'Title' with the following error message:
Element at 54 no longer present in the DOM
I have not been able to find the exact meaning of what this error message means and this test is flakey, sometimes it passes, sometimes not.
Unlike other capybara finders, all doesn't really wait for elements to appear in the DOM. So if your table is not fully loaded, it simply goes straight to the expectation within that block and could potentially fail. That could easily explain why sometimes it fails and other times it succeeds.
I suggest using another expectation before this block and ensure that the table is fully loaded first. I don't know what your DOM looks like, but you can try something like:
expect(page).to have_css('tr td', :count => 15)
So at this point, you've waited for 15 rows to show up in the DOM prior to moving onto your next steps. Hope that helps.
As the other answer details, #all by default doesn't wait for elements to appear so it's possible you're not actually getting the elements you think you are. Rather than the solution in the other answer, it is possible to make #all wait for elements to appear by specifying one of the :count, :minimum, :maximum, or :between options.
within all('tr', minimum: 10)[1] do
...
end
for instance. This is all assuming that the action before the #within in your test isn't triggering an ajax action that is replacing existing rows in a table with other rows. If row replacement is happening then you may be running into one of the biggest downsides of using #all -- when using #all the returned elements cannot automatically be re-queried if they leave the page and are replaced, since their entire query can't be stored with them (no index into the results). In that case you're better off changing the code to
within find(:xpath, './/tr[2]') do
...
end
This way the element you're searching within can be reloaded automatically if needed

Rspec with Capybara have_css matcher is not working

In Cucumber, with Rspec and Capybara I have a test to check that a button has a class. Here it is
expect(#some_button).to have_css(".in-cart")
it fails, but
#some_button['class']
returns
'btn product in-cart'
so the button definitely has the 'in-cart' class.
As a temporary measure I've changed my test to be;-
expect(#some_button['class']).to match /in-cart/
Which is obviously insane. But why should 'have_css', or 'has_css?' return false for a DOM element that clearly has the expected class?
Also page.all('.in-cart') includes the button, so Capybara can definitely find it.
Incidentally I also tried 'button.in-cart', 'in-cart',expect (etc).to have_selector, expect(etc.has_selector?('.in-cart')).to be_truthy and all combinations.
have_css matcher is expected to be applied to the parent container instead of the actual element
# your view
<div id="container">
<div class="in_cart product"></div>
</div>
# your step definition
parent = page.find('div#container')
expect(parent).to have_css(".in-cart")
# => returns true, as there is nested div.in_cart
expect('div#container div').to have_css(".in-cart")
# => returns false, as there is no such selector inside of the latter div
As of matching attributes of the exact object, you've got to stick with simple quering by key
element = page.find('div#container div')
element['class'].should include('in-cart')
expect(element['class']).to match /in-cart/
Same logic applies to all of the RSpecMatchers.
In newer versions of Capybara/Rspec, by default, expect(page) will query the entire page looking for a match; however, sometimes we may not want that and instead will prefer to target a specific class/region of the page. For those cases, narrow down the context of page using within:
within('.container') do
expect(page).to have_css(".in-cart")
end
Assuming, the parent has a class container Capybara will only search within this element.
expect(page).to have_css('button.in-cart')

click element if it exists in capybara

I wish to click a popup message that appears on my test app if it is present. I am new to capybara and cant seem to find a way to do this. I have previous experience with watir and if I were doing it with watir it would be something like:
if browser.link(:text, "name").exists? do
browser.link(:text, "name").click
end
How can I do the same in capybara? Note this link will not always appear hence why I wish to have the if statement.
A straight of the head code is to just invoke a has_link? matcher and then click_link action:
if page.has_link?('name')
page.click_link('name')
end
But it will be not the fastest solution as Capybara will make two queries to driver to get element: first one in has_link? and the second one in click_link.
A better variant may be to make only one query to get an element:
# This code doesn't check that an element exists only at one place and just chooses the first one
link = first('name')
link.click if link
or
# This code checks that element exists only at one place
links = all('name')
unless links.empty?
links.count.should == 1
link = links.first
link.click
end
Personally I would go with has_link?/click_link implementation as the second variant does't check that element exists only at one place and the third one is too long.
In case I used has_css? query :
if page.has_css?("button #popUpButton")
click_button(#popUpButton")
end
You can use first, with option minimum: 0
item = first ".dropdown-item", minimum: 0
item.click if item&.visible?
Have you tried doing something like:
if page.find('.id') do
click_link('Some Link') # or page.find('.id').click
else
page.should_not have_selector('.id') # or something like that
end

How can I alleviate timing/AJAX woes using Capybara/Capybara Webkit/RSpec click_button and page.select?

For the sake of simplicity, I've left out most of my test and only included the offending code. It is:
click_button('Search')
page.select 'Preferred', :from => 'ticket_service_type'
When I run this, I receive the following:
Failure/Error: page.select 'Preferred', :from => 'ticket_service_type'
Capybara::ElementNotFound:
cannot select option, no select box with id, name, or label 'ticket_service_type' found`
The AJAX request this button click event triggers doesn't have anything to do with the select tag, so reversing the order in the test causes the test to pass. I know that Capybara's default wait time is 2 seconds and so I changed it to 10 with:
Capybara.default_wait_time = 10
This does not cause the test to pass. How can I get these two methods to play nice with one another and work in the order in which a user would operate the web page?
(Had I posted the code from my spec, I bet this would have been solved quickly.)
From The Cucumber Book (emphasis mine):
Luckily, Capybara knows how to deal with this situation in a simple way. If we add an explicit call to find, passing a CSS selector for a DOM element on the page that doesn’t yet exist, Capybara will wait a little (50ms) and try again until the element appears. If it doesn’t appear after a while (two seconds by default, though this is configurable), it will raise an exception, causing the step definition to fail.
So have your AJAX write something to the DOM then find() it in your step definition. It's not ideal. In my case I'm introducing a (hidden) DOM element just to facilitate testing but I haven't found another way.
Be sure to add :js => true to integration tests which depend upon JavaScript.

Cucumber, How to check if parent has a certain class?

I am using cucumber with rails, and I'm checking if the parent of an element has a certain class. I found this code, but it doesn't work. I'm using Capybara by the way. Thanks in advance!
Then the element "([^"]*)" with parent "([^"]*)" should have class "([^"]*)" do |element_id,parent,css_class|
response.should have_selector "#{parent} .#{css_class}" do |matches|
matches.should have_selector element_id
end
end
Technically, this isn't really the way you should be testing with cucumber. Cucumber is for testing behaviour, not checking the DOM - though of course it can be used in that way.
If you're really wanting to write something like this, you could make that much simpler:
Then /^the element "([^"]*)" with parent "([^"]*)" should have class "([^"]*)"$/ do |element_id,parent,css_class|
page.should have_css("#{parent}.#{css_class} #{element_id}")
end
This sucks on many levels, though. Not only are you checking the DOM directly from the step, which makes for very unreadable features, but you're mixing passing in element names, class names and element IDs - each with a slightly different style... In this step, element can be any type of selector whereas parent and css_class are much more fixed - they must always 'fit' into the selector string or it won't find anything.
I haven't really explained that very well, but in a nutshell, you should consider what you're actually trying to test and think whether it can be renamed to something more useful and re-usable. Would you ever be able to re-use that step without looking at it's implementation to figure out what goes where?
Also, with more expressive naming, the test instantly becomes more useful later down the line. For example, the step Then I should see the current list is active is much more readable and expressive than Then the element "li.active" with parent "ul" should have class "active-list". Be specific in the step definition implementation, not your features!
Have a read of this blog post - http://elabs.se/blog/15-you-re-cuking-it-wrong - it should give you a good idea of how to write better steps.

Resources