Cucumber: Wait for ajax:success - ruby-on-rails

I have the following typical cucumber steps in a Rails 3.1 project:
...
When I follow "Remove from cart"
Then I should see "Test Product removed from cart"
The difficulty is that "Remove from cart" button is an ajax :remote call, which returns "Test Product removed from cart" to the #cart_notice element via:
$('#cart_notice').append("<%= #product.name %> removed from cart");
The function works fine in the browser, but doesn't find the "Test Product removed from cart" text in cucumber. I'm guessing this is because Cucumber is searching for the text before the AJAX returns it?
So, in short...how do I ensure cucumber waits for the ajax to return a result before searching for the desired content?

To add to what dexter said, you may want to write a step that executes JS in the browser which waits for ajax requests to finish. With jQuery, I use this step:
When /^I wait for the ajax request to finish$/ do
start_time = Time.now
page.evaluate_script('jQuery.isReady&&jQuery.active==0').class.should_not eql(String) until page.evaluate_script('jQuery.isReady&&jQuery.active==0') or (start_time + 5.seconds) < Time.now do
sleep 1
end
end
You can then include the step as needed, or after every javascript step:
AfterStep('#javascript') do
begin
When 'I wait for the ajax request to finish'
rescue
end
end
I was having issues with the automatic synchronization, and this cleared it up.

I guess you are using cucumber with capybara. In that case, capybara comes with a resynchronize feature. "Capybara can block and wait for Ajax requests to finish after you’ve interacted with the page." - from capybara documentation
You can enable it in features/support/env.rb
Capybara.register_driver :selenium do |app|
Capybara::Driver::Selenium.new(app, :browser => browser.to_sym, :resynchronize => true)
end
But, I have seen this causing timeout issues. So, if that isn't working for you, I would recommend introducing a manual wait step before asserting the results of the ajax request.
...
When I follow "Remove from cart"
And I wait for 5 seconds
Then I should see "Test Product removed from cart"
You can define the wait step in step_definitions/web_steps.rb as
When /^I wait for (\d+) seconds?$/ do |secs|
sleep secs.to_i
end

I suppose wait_until should do the job. It will command to capybara to check something until its true for some time.

Old question, but the Spreewald gem should help
https://github.com/makandra/spreewald
You can use the patiently method from the Spreewald gem like so:
Then /^I should see "([^\"]*)" in the HTML$/ do |text|
patiently do
page.body.should include(text)
end
end
The step will maintain a loop for a period of time until the desired text appears in the test dom, else the step will fail.
(taken from https://makandracards.com/makandra/12139-waiting-for-page-loads-and-ajax-requests-to-finish-with-capybara)

Related

Capybara testing with RSpec in Ruby

on my index page I have this div:
<div class="banner">
<h1 class="glow-header">Galaxy Far, Far Away? Quick Trip to Mars?<br>
Pianeta has you covered.</h1>
<div>
In my testfile this works:
RSpec.describe 'home features' do
it 'displays the name of the app and links to the index-all planets page' do
visit root_path
expect(page).to have_content('Space is full of surprises.')
click_link('Go Beyond')
expect(current_path).to eq('/planets')
expect(page).to have_content('Galaxy Far, Far Away?')
end
end
But I would like it to be working with the h1 included.
I did this:
expect(page).to have_content('<h1 class="glow-header">Galaxy Far, Far Away? Quick Trip to Mars?<br>
Pianeta has you covered.</h1>')
end
But the test failed. What did I do wrong ?
The #has_content?/#has_text? method only checks the text content of the page. It does not look at the HTML tags.
If you want to check for content within a specific HTML element there is a #within method that takes a block and will scope the Capybara lookups within it to be within the matched element. The element referenced by #within must exist or Capybara will raise an exception.
page.within('h1.glow-header') do
expect(page).to have_content('Galaxy Far, Far Away?')
end
If you don't want to deal with scoping using within for a single expectation you could do
expect(page).to have_css('h1.glow-header', text: 'Galaxy Far, Far Away?')
If you've already got a reference to the header you could also do something like
header = find('h1.glow-header')
...
expect(header).to have_text('Galaxy Far, Far Away?')
Additionally you should not be doing expect(current_path).to eq('/planets'). Using RSpecs eq matcher with Capybara will lead to flaky tests as soon as you move to using an asynchronous (JS supporting) driver, because it prevents Capybaras auto waiting/retrying behaviors. Instead you should use the Capybara provided matcher
expect(page).to have_current_path('/planets')

Capybara not finding content on page when it should?

I'm using Capybara, and I'm selecting an item from a dropdown using this command:
select "Some Option", from: "client_id", visible: :all
This is the view code I'm using to generate the select:
<%= form_with(url: '#', method: 'GET') do |f| %>
<%= f.select :client_id,
options_for_select(
Client.accessible_by(current_ability)
.map{|c| [c.name, c.id]}, selected_client_id
),
{include_blank: defined?(include_blank) ? include_blank : false},
{
class: "form-control selectpicker border",
id: "client_id",
onchange: "this.form.submit()",
data: {'live-search': true, style: "btn-white"}
}
%>
<% end %>
Which renders fine and works as expected when I myself am interacting with it on the page.
I believe the problem stems from this: onchange: "this.form.submit()"
When Capybara chooses an option from the select, it's supposed to submit the form, and then the page is supposed to reload with a query string /?client_id=X, but that is not what's happening and I don't know why.
When I look at the screenshot provided from the error, it appears as though Capybara hasn't even selected anything from the dropdown, it just says "Nothing selected."
Here's the Capybara test:
def check_grand_total(expected_value)
visit current_path
click_link "Balance"
# it seems as though Capybara is not waiting for this to finish... but I don't know, just my theory
# this is supposed to trigger: this.form.submit() on the dropdown
select "Some Client", from: "client_id", visible: :all
actual_value = page.first('.grand-total').text
assert_equal(expected_value, actual_value)
end
The error:
E
Error:
OrdersTest#test_deposit_orders:
Capybara::ExpectationNotMet: expected to find css ".grand-total" at least 1 time but there were no matches
test/system/helpers/balance_test_helper.rb:10:in `check_grand_total'
test/system/orders_test.rb:113:in `block in <class:OrdersTest>'
I'm thinking maybe Capybara isn't actually triggering the change event for some reason.
The class names you're specifying, and the fact that you're specifying (visible: :all) imply you may be using some type of JS widget to replace the standard html select element. Is that true? Overriding visibility checking in Capybara actions doesn't make any sense since you shouldn't be interacting with non-visible elements, and depending on exactly which action and driver will either error, or just not do anything. If you are using a replacement widget then you need to interact with the elements created by the widget rather than using select
If you're not using a JS widget then it could just be a case of a badly written test. There are a couple of things right away - first is that you are visiting current_path, second is that you're asserting against text rather than using one of the Capybara provided assertions. The thing to remember is that browser actions occur asynchronously, so when you tell Capybara to choose an option from a select element there is no guarantee the actions triggered by doing that have completed when the select call returns. Because of that you should be using the capybara provided assertions/expectations which include waiting/retrying behavior in order to sync the browser with the tests.
def check_grand_total(expected_value)
# This visit looks dangerous because it's not guaranteed to have changed
# the page when the `visit` returns, which means it may try and click
# the link on the previous page. You should really be checking for something
# that wouldn't be on the previous page -- It's also strange that there is a current path since tests should be independent
visit current_path
click_link "Balance" # see above comment
select "Some Client", from: "client_id"
# This will wait up to Capybara.default_max_wait_time seconds for
# the grand total element to contain the expected text
assert_selector '.grand-total', exact_text: expected_value
# If using RSpec it would be
# expect(page).to have_selector '.grand-total', exact_text: expected_value
end
So I think the issue is that the onchange even does not actually fire This "issue" suggests causing focus loss via
fill_in("foo", with: "hello world").send_keys(:tab)
so it may be possible to adapt that to
select("Some Client", from: "client_id", visible: :all).send_keys(:tab)
That being said it also appears that a method exists to force an event on an Element Capybara::Node::Element#trigger so I would start with trying:
select("Some Client", from: "client_id", visible: :all).trigger(:onchange)
Disclaimer: I am not very familiar with capybara so this answer is based on source review and googling. According to Thomas Walpole, who clearly has extensive experience, " trigger isn't supported by most drivers because it doesn't make a lot of sense to use during testing due to it not replicating what a user would actually do."

Link doesn't hide after some time capybara

I use capybara to test this code which located in my Comment model (5 minutes are conventional):
def editable?
self.created_at < (Time.now - 5.minute)
end
The view of link:
- unless comment.editable?
= link_to 'Edit', edit_category_theme_comment_path(#category, #theme, comment)
So after 5 minutes link to edit must hide from the page (but we need to refresh the page). Here is my code in RSpec for creating comments and testing the link-hide functionality:
def create_comment(options={})
options[:content] ||= 'I am a comment'
visit category_theme_path(category, theme)
within '.comment-form' do
fill_in 'Content', with: options[:content]
click_button 'Submit'
end
end
context 'Comment has content' do
before(:each) { create_comment }
it 'hides the edit symbol due 5 minutes after comment was created' do
using_wait_time 400 do
visit category_theme_path(category, theme)
save_and_open_page
expect(page).to have_no_css('.comment-edit')
end
end
end
But I got: Failure/Error: expect(page).to have_no_css('.comment-edit')expected #has_no_css?(".comment-edit") to return true, got false
I also try to use page.reload!, expect(page).to have_no_css('.comment-edit', wait: 400) and other related staff, but capybara don't want to wait. Maybe I use using_wait_time for a wrong place, if that - how I can test this?
There are a number of things wrong with your approach to testing this. The reason your attempt is failing is because using_wait_time just sets the amount of time Capybaras matchers will wait for their expectations to become true, it doesn't actually make the program wait. Therefore expect(page).to have_no_css('.comment-edit') will wait up to the time your using_wait_time specifies, rechecking the page every 50 milliseconds or so for the content, but it never reloads the page and doesn't wait before the loading the page. For your approach to work you would need to sleep before visiting the page
it 'hides the edit symbol due 5 minutes after comment was created' do
sleep 5.minutes
visit category_theme_path(category, theme)
save_and_open_page
expect(page).to have_no_css('.comment-edit')
end
however this is a terrible idea since your tests will become ridiculously slow.
Rather than that approach you could be generating your test comment already "old" (as pascal betz suggests), using something like FactoryGirl and specifying a created_at that is more than 5 minutes ago.
FactoryGirl.create(:comment, created_at: 5.minutes.ago)
or if you want to continue creating your comment through the interface then include something like the Timecop gem and you can do
Timecop.travel(5.minutes.from_now) do
visit category_theme_path(category, theme)
save_and_open_page
expect(page).to have_no_css('.comment-edit')
end
which will move the clock 5 minutes forward before visiting the page, and then reset it back to normal once the block if finished.
Additionally, your editable? method is comparing in the wrong direction and should be
def editable?
self.created_at > (Time.now - 5.minute)
end
Then the view should be
- if comment.editable?
= link_to 'Edit', edit_category_theme_comment_path(#category, #theme, comment)
Work with seed data that is 5 minutes old and visit that page. Like this you do not need to wait five minutes.
Also your code
- unless comment.editable?
= link_to 'Edit', edit_category_theme_comment_path(#category, #theme, comment)
should probably read
- if comment.editable?
= link_to 'Edit', edit_category_theme_comment_path(#category, #theme, comment)

How do I click_link with Capybara that has a method: :post?

In my application, I have this link:
link_to 'Start', some_path, method: :post
and, in a feature test, I need to click this link and then assert that the page does not contain this link (instead, it should contain a 'Stop' link)
I tried to:
click_link 'Start'
but it does not work - either the link Start is still on the page or I am getting a page with a 'getting redirected' text (I use save_and_open_page)
How do I solve this issue?
EDIT: I am using the poltergeist driver.
To avoid js/browser overhead you can also do a fake click with pure ruby.
For example click_ujs_link "Send" with this code.
def click_ujs_link(name)
link = find(:link, name, {})
visit_with_method link['data-method'], link['href']
end
def visit_with_method(method, url)
page.driver.send method, url
visit page.driver.response.location
end
To answer my own question, the solution is trivial: just set js: true on the scenarios that contain the link, like for example this:
scenario 'some description', js: true do
The explanation: for links with methods other than GET, Rails.js dynamically generates a form, something like this
...
so you need to tell Capybara that it should run the scenario using a javascript engine (poltergeist, in our case), and you do so via the js: true option.

How to click the form commit button in Capybara

With Rails, rspec and capybara, I'm trying to test your typical ERB generated form:
<form action="/pages/1" id="edit_page_1" method="post">
<input id="page_title" name="page[title]" type="text">
<input name="commit" type="submit" value="Update Page">
</form>
I run two kinds of feature specs, those that are the same no matter what the language, and those that are I18N specific (for internationalization testing).
The problem is there is no clear way to click that submit button with capybara, unless I'm missing the obvious. I would expect simply click('commit') to do the trick.
Using click_button('Update Page') works but is obviously language specific and can't be used with both the New and Edit templates even though they render the same form template.
Adding an id to the submit button works, but I strongly dislike changing the code exclusively because the test requires it.
Using a css or xml matcher both looks ugly (a user would never know/care about accessing an element that way) and it is overkill.
In the end a macro was the answer I needed as apparently there is nothing native in capybara.
# spec/support/form_helpers.rb
module FormHelpers
def submit_form
find('input[name="commit"]').click
end
end
This gets included in spec_helper.rb
RSpec.configure do |config|
config.include FormHelpers, :type => :feature
...etc...
I used :type => :feature so it gets included only in the integration tests.
In the integration tests you can use it like this:
scenario 'pages can be created' do
visit new_page_path
fill_in 'page_title', with: 'A Tale of Two Cities'
submit_form # Clicks the commit button regardless of id or text
expect(page).to have_content 'The page was created'
...etc..
end
Of course submit_form can also be used inside within blocks and with :js => true.
I usually do:
within 'form#edit_page_1' do
find('input[name="page[title]"]').set "Some Value"
find('input[name="commit"]').click
end
Its tied to the html but to its semantic attributes, so I feel like its fine.
Actually I never use the magical finders.
Btw I dont understand your comment: (a user would never know/care about accessing an element that way).
Integration specs are for you, it mimics a user for sure but its just a matter of giving proper instructions.
Try to use:
find('input[name="commit"]').click
It helps

Resources