Waiting for redirect in Capybara Rails - ruby-on-rails

I am doing some integration tests on my website that is builtin Spree. I am using RSpec + Capybara in order to do these tests.
I found that in the newer versions of Capybara, wait_until was removed from the source code because it is not necessary anymore to wait for an AJAX Request this way. Somehow, now Capybara is smart enough to wait for the AJAX Requests.
What I am doing is quite simple:
visit product_path(product)
within '.vip-buy-content' do
first('.buy-button').click
end
expect(page.current_path).to eq(cart_path)
When the button is clicked, an AJAX request is done. When the AJAX request is done, a redirect happens.
However, the expectation fails because it seems that Capybara will process this faster than the AJAX request and the redirect.
If add a "sleep 10" above the expect, the spec will pass because it has enough time to process this.
I would like a nicer way to wait for the redirect. Any ideas?.

I don't know if this will solve this problem, but I usually add a wait_for_ajax.rb file in my supports folder, and create a module such as this.
module WaitForAjax
def wait_for_ajax
Timeout.timeout(Capybara.default_wait_time) do
loop until finished_all_ajax_requests?
end
end
def finished_all_ajax_requests?
page.evaluate_script('jQuery.active').zero?
end
end
RSpec.configure do |config|
config.include WaitForAjax, type: :feature
end
And then add
visit product_path(product)
within '.vip-buy-content' do
first('.buy-button').click
end
wait_for_ajax
expect(page.current_path).to eq(cart_path)
My assumption you are using :js=>true on your describe blocks. Hope this helps!

Related

Capybara doesn't wait to complete the submit action on click_button "Save"

I have the following rspec fragment:
describe "Save should create a ClassificationScheme" do
subject { lambda { click_button "Save"; sleep 1 } }
it { should change(ClassificationScheme, :count).by(1)
end
Without the "sleep 1" capybara doesn't wait for the action fired by the save button and the spec fails. With the sleep 1 is OK, but is there any better solution?
Note, that this test is running in Firefox using selenium webdriver.
My versions:
rails 4.1.12
rspec 2.99.0
capybara 2.4.4
selenium-webdriver 3.2.1
firefox 51.0.1
When you click something using Capybara there is no guarantee any actions triggered by that click have completed when the method returns. This is because Capybara knows nothing about what the browser is doing other than that it clicked an element on the screen. Instead of sleeping you need to check for something that visually changes on the page to indicate the action triggered by clicking the button has completed. That may be a message stating the save happened successfully or an element disappearing, etc. Something along the lines of
describe "Save should create a ClassificationScheme" do
subject { lambda { click_button "Save"; page.should have_text('Classification Saved' } }
it { should change(ClassificationScheme, :count).by(1)
end
Note: you should also update Capybara - 2.4.4 was released in October of 2014, there have been a lot of improvements since then.
You didn't include code for your submit action, but if there's anything asynchronous going on, like an Ajax request, the submit action itself will finish quickly, while the async task is still processing the request. If that's the case, you can use a helper like this:
# spec/support/wait_for_ajax.rb
module WaitForAjax
def wait_for_ajax
Timeout.timeout(Capybara.default_max_wait_time) do
loop until finished_all_ajax_requests?
end
end
def finished_all_ajax_requests?
page.evaluate_script('jQuery.active').zero?
end
end
RSpec.configure do |config|
config.include WaitForAjax, type: :feature
end
Code courtesy Thoughtbot.
Note that this only includes the helper in feature specs; so either tag your specs with type: :feature, or change the config.include line above to include it in whatever spec type you're using.
To use it:
describe "Save should create a ClassificationScheme" do
subject { lambda { click_button "Save"; wait_for_ajax } }
it { should change(ClassificationScheme, :count).by(1)
end

Capybara expect BEFORE ajax request completed

Here is short flow of my form:
You click Submit button
Submit button gets disabled
AJAX request is sent to API.
AJAX response is received.
Button is clickable again.
scenario 'user can not resubmit form before API response' do
expect(find_button('Submit')[:disabled]).to be_falsey
click_button 'Submit'
expect(find_button('Submit')[:disabled]).not_to be_falsey
## Here ajax response is received
expect(find_button('Submit')[:disabled]).to be_falsey
end
This test actually passes but I feel I have no control over it and I'm not concerned if it really test what I intended.
Is there any way to have more control over AJAX responses? Does this test actually test feature I described?
edit:
this test actually randomly fails/passes. I guess it is caused by executing expect(find_button('Submit')[:disabled]).not_to be_falsey after AJAX call
Automatically wait for AJAX with Capybara
# spec/support/wait_for_ajax.rb
module WaitForAjax
def wait_for_ajax
Timeout.timeout(Capybara.default_wait_time) do
loop until finished_all_ajax_requests?
end
end
def finished_all_ajax_requests?
page.evaluate_script('jQuery.active').zero?
end
end
RSpec.configure do |config|
config.include WaitForAjax, type: :feature
end
Usage:
scenario 'user can not resubmit form before API response' do
expect(find_button('Submit')[:disabled]).to be_falsey
click_button 'Submit'
expect(find_button('Submit')[:disabled]).not_to be_falsey
wait_for_ajax
expect(find_button('Submit')[:disabled]).to be_falsey
end
The helper uses the jQuery.active variable, which tracks the number of
active AJAX requests. When it’s 0, there are no active AJAX requests,
meaning all of the requests have completed.
The test is not testing what you want because you're querying for the disabled attribute outside the finder which means Capybaras retrying behavior can't work. Instead you want to use the disabled option to the finder
expect(find_button('Submit', disabled: false)).to be
click...
expect(find_button('Submit', disabled: true)).to be
...
This way Capybara can retry the find until it passes (or timeouts). Note: this test is dependent on the Ajax request taking a bit of time, since if it doesn't it is possible for the button to be re-enabled before the find of the disabled button happens

Elasticsearch out of sync when overwhelmed on HTTP at test suite

I have a Rails app with an Rspec test suite which has some feature/controller tests depending on ElasticSearch.
When we test the "search" feature around the system (and other features depending on ES) we use a real ES, it works perfectly at development environment when we're running single spec files.
When the suite runs at our CI server it gets weird, because sometimes ES won't keep in sync fast enough for the tests to run successfully.
I have searched for some way to run ES in "syncronous mode", or to wait until ES is ready but haven't found anything so far. I've seen some workarounds using Ruby sleep but it feels unacceptable to me.
How can I guarantee ES synchronicity to run my tests?
How do you deal with ES on your test suite?
Here's one of my tests:
context "given params page or per_page is set", :elasticsearch do
let(:params) { {query: "Resultados", page: 1, per_page: 2} }
before(:each) do
3.times do |n|
Factory(:company, account: user.account, name: "Resultados Digitais #{n}")
end
sync_companies_index # this is a helper method available to all specs
end
it "paginates the results properly" do
get :index, params
expect(assigns[:companies].length).to eq 2
end
end
Here's my RSpec configure block and ES helper methods:
RSpec.configure do |config|
config.around :each do |example|
if example.metadata[:elasticsearch]
Lead.tire.index.delete # delete the index for a clean environment
Company.tire.index.delete # delete the index for a clean environment
example.run
else
FakeWeb.register_uri :any, %r(#{Tire::Configuration.url}), body: '{}'
example.run
FakeWeb.clean_registry
end
end
end
def sync_companies_index
sync_index_of Company
end
def sync_leads_index
sync_index_of Lead
end
def sync_index_of(klass)
mapping = MultiJson.encode(klass.tire.mapping_to_hash, :pretty => Tire::Configuration.pretty)
klass.tire.index.create(:mappings => klass.tire.mapping_to_hash, :settings => klass.tire.settings)
"#{klass}::#{klass}Index".constantize.rebuild_index
klass.index.refresh
end
Thanks for any help!
Your test is confused - it's testing assignment, pagination, and (implicitly) parameter passing. Break it out:
Parameters
let(:tire) { double('tire', :search => :sentinel) }
it 'passes the correct parameters to Companies.tire.search' do
expected_params = ... # Some transformation, if any, of params
Companies.stub(:tire).with(tire)
get :index, params
expect(tire).to have_received(:search).with(expected_params)
end
Assignment
We are only concerned that the code is taking one value and assigning it to something else, the value is irrelevant.
it 'assigns the search results to companies' do
Companies.stub(:tire).with(tire)
get :index, params
expect(assigns[:companies]).to eq :sentinel
end
Pagination
This is the tricky bit. You don't own the ES API, so you shouldn't stub it, but you also can't use a live instance of ES because you can't trust it to be reliable in all testing scenarios, it's just an HTTP API after all (this is the fundamental issue you're having). Gary Bernhardt tackled this issue in one of his excellent screencasts - you simply have to fake out the HTTP calls. Using VCR:
VCR.use_cassette :tire_companies_search do
get :index, params
search_result_length = assigns[:companies].length
expect(search_result_length).to eq 2
end
Run this once successfully then forever more use the cassette (which is simply a YAML file of the response). Your tests are no longer dependent on APIs you don't control. If ES or your pagination gem update their code, simply re-record the cassette when you know the API is up and working. There really isn't any other option without making your tests extremely brittle or stubbing things you shouldn't stub.
Note that although we have stubbed tire above - and we don't own it - it's ok in these cases because the return values are entirely irrelevant to the test.

How do I re-use Capybara sessions between tests?

I want to keep on using the same session and by that I mean Rails' session between various Test::Unit integration tests that use Capybara. The Capybara::Session object is the same in all the tests as it is re-used, but when I access another page in another test, I'm immediately logged out.
Digging in I found that capybara_session.driver.browser.manage.all_cookies is cleared between one test and the next.
Any ideas how? or why? or how to avoid it?
Trying to work-around that, I saved the cookie in a class variable and re-added later by running:
capybara_session.driver.browser.manage.add_cookie(##cookie)
and it seems to work, the cookie is there, but when there's a request, the cookie gets replaced for another one, so it had no effect.
Is there any other way of achieving this?
Add the following after your capybara code that interacts with the page:
Capybara.current_session.instance_variable_set(:#touched, false)
or
page.instance_variable_set(:#touched, false)
If that doesn't work, these might help:
https://github.com/railsware/rack_session_access
http://collectiveidea.com/blog/archives/2012/01/05/capybara-cucumber-and-how-the-cookie-crumbles/
If what you are doing is trying to string together individual examples into a story (cucumber style, but without cucumber), you can use a gem called rspec-steps to accomplish this. For example, normally this won't work:
describe "logging in" do
it "when I visit the sign-in page" do
visit "/login"
end
it "and I fill in my registration info and click submit" do
fill_in :username, :with => 'Foo'
fill_in :password, :with => 'foobar'
click_on "Submit"
end
it "should show a successful login" do
page.should have_content("Successfully logged in")
end
end
Because rspec rolls back all of its instance variables, sessions, cookies, etc.
If you install rspec-steps (note: currently not compatible with rspec newer than 2.9), you can replace 'describe' with 'steps' and Rspec and capybara will preserve state between the examples, allowing you to build a longer story, e.g.:
steps "logging in" do
it "when I visit the sign-in page" #... etc.
it "and I fill in" # ... etc.
it "should show a successful" # ... etc.
end
You can prevent the call to #browser.manage.delete_all_cookies that happens between tests by monkey patching the Capybara::Selenium::Driver#reset! method. It's not a clean way of doing it, but it should work...
Add the following code to your project so that it is executed after you require 'capybara':
class Capybara::Selenium::Driver < Capybara::Driver::Base
def reset!
# Use instance variable directly so we avoid starting the browser just to reset the session
if #browser
begin
##browser.manage.delete_all_cookies <= cookie deletion is commented out!
rescue Selenium::WebDriver::Error::UnhandledError => e
# delete_all_cookies fails when we've previously gone
# to about:blank, so we rescue this error and do nothing
# instead.
end
#browser.navigate.to('about:blank')
end
end
end
For interest's sake, the offending line can be seen in Capybara's codebase here: https://github.com/jnicklas/capybara/blob/master/lib/capybara/selenium/driver.rb#L71
It may be worth posting the reason why you need this kind of behaviour. Usually, having the need to monkey patch Capybara, is an indication that you are attempting to use it for something it was not intended for. It is often possible to restructure the tests, so that you don't need the cookies persisted across integration tests.

How do you POST to a URL in Capybara?

Just switched from Cucumber+Webrat to Cucumber+Capybara and I am wondering how you can POST content to a URL in Capybara.
In Cucumber+Webrat I was able to have a step:
When /^I send "([^\"]*)" to "([^\"]*)"$/ do |file, project|
proj = Project.find(:first, :conditions => "name='#{project}'")
f = File.new(File.join(::Rails.root.to_s, file))
visit "project/" + proj.id.to_s + "/upload",
:post, {:upload_path => File.join(::Rails.root.to_s, file)}
end
However, the Capybara documentation mentions:
The visit method only takes a single
parameter, the request method is
always GET.always GET.
How do I modify my step so that Cucumber+Capybara does a POST to the URL?
More recently I found this great blog post. Which is great for the cases like Tony and where you really want to post something in your cuke:
For my case this became:
def send_log(file, project)
proj = Project.find(:first, :conditions => "name='#{project}'")
f = File.new(File.join(::Rails.root.to_s, file))
page.driver.post("projects/" + proj.id.to_s + "/log?upload_path=" + f.to_path)
page.driver.status_code.should eql 200
end
You could do this:
rack_test_session_wrapper = Capybara.current_session.driver
rack_test_session_wrapper.submit :post, your_path, nil
You can replace :post which whatever method you care about e.g. :put or :delete.
Replace your_path with the Rails path you want e.g. rack_test_session_wrapper.submit :delete, document_path(Document.last), nil would delete the last Document in my app.
Updated answer 2022-10-05
If your driver doesn't have post (Poltergeist doesn't, for example), you can do this:
response = nil
open_session do |session|
session.post("/mypath", params: { foo: "bar" })
response = session.response
end
We can now e.g. assert on response.body.
You can also use integration_session.post(…) directly, but I think that can cause some confusion by not separating the POST session from the test's ordinary session.
As has been stated elsewhere, in a Capybara test you typically want to do POSTs by submitting a form just like the user would. I used the above to test what happens to the user if a POST happens in another session (via WebSockets), so a form wouldn't cut it.
Docs:
https://api.rubyonrails.org/classes/ActionDispatch/Integration/Runner.html#method-i-open_session
Old answer from 2014-06-22
If your driver doesn't have post (Poltergeist doesn't, for example), you can do this:
session = ActionDispatch::Integration::Session.new(Rails.application)
response = session.post("/mypath", my_params: "go_here")
But note that this request happens in a new session, so you will have to go through the response object to assert on it.
Docs:
http://api.rubyonrails.org/classes/ActionDispatch/Integration/Session.html
http://api.rubyonrails.org/classes/ActionDispatch/Integration/RequestHelpers.html
Capybara's visit only does GET requests. This is by design.
For a user to perform a POST, he must click a button or submit a form. There is no other way of doing this with a browser.
The correct way to test this behaviour would be:
visit "project/:id/edit" # This will only GET
attach_file "photo", File.open('cute_photo.jpg')
click_button 'Upload' # This will POST
If you want to test an API, I recommend using spec/request instead of cucumber, but that's just me.
I know the answer has already been accepted, but I'd like to provide an updated answer. Here is a technique from Anthony Eden and Corey Haines which passes Rack::Test to Cucumber's World object:
Testing REST APIs with Cucumber and Rack::Test
With this technique, I was able to directly send post requests within step definitions. While writing the step definitions, it was extremely helpful to learn the Rack::Test API from it's own specs.
# feature
Scenario: create resource from one time request
Given I am an admin
When I make an authenticated request for a new resource
Then I am redirected
And I see the message "Resource successfully created"
# step definitions using Rack::Test
When /^I make an authenticated request for a new resource$/ do
post resources_path, :auth_token => #admin.authentication_token
follow_redirect!
end
Then /^I am redirected$/ do
last_response.should_not be_redirect
last_request.env["HTTP_REFERER"].should include(resources_path)
end
Then /^I see the message "([^"]*)"$/ do |msg|
last_response.body.should include(msg)
end
Although, not an exact answer to the question, the best solution for me has been to use Capybara for specs that simulate user interaction (using visit), and Rack Test for test API like requests. They can be used together within the same test suite.
Adding the following to the spec helper gives access to get, post and other Rack test methods:
RSpec.configure do |config|
config.include Rack::Test::Methods
You may need to put the Rack Test specs in a spec/requests folder.
With an application using RSpec 3+, you would not want to make an HTTP POST request with Capybara. Capybara is for emulating user behavior, and accepting the JS behavior and page content that results. An end user doesnt form HTTP POST requests for resources in your application, a user clicks buttons, clicks ajax links, drags n drops elements, submits web forms, etc.
Check out this blog post on Capybara and other HTTP methods. The author makes the following claim:
Did you see any mention of methods like get, post or response? No? That’s because those don’t exist in Capybara. Let’s be very clear about this...Capybara is not a library suited to testing APIs. There you have it. Do not test APIs with Capybara. It wasn’t designed for it.
So, developing an API or not, if you have to make an explicit HTTP POST request, and it does not involve an HTML element and some sort of event (click, drag, select, focusout, whatever), then it shouldn't be tested with Capybara. If you can test the same feature by clicking some button, then do use Capybara.
What you likely want is RSpec Request specs. Here you can make post calls, and any other HTTP method as well, and assert expectations on the response. You can also mock n stub objects and methods to assert expectations in regards to side effects and other behaviors that happen in between your request and the response.
# spec located in spec/requests/project_file_upload_spec.rb
require "rails_helper"
RSpec.describe "Project File Upload", type: :request do
let(:project) { create(:project) }
let(:file) { File.new(File.join(::Rails.root.to_s, 'path/to/file.ext')) } # can probably extract this to a helper...
it "accepts a file uploaded to a Project resource" do
post "project/#{project.id}/upload", upload_path: file
expect(response).to be_success
expect(project.file?).to eq(true)
# expect(project.file).not_to eq(nil)
expect(response).to render_template(:show)
end
end
As others have said, there’s no direct way of doing a POST with Capybara because it’s all about browser interaction. For API testing, I’d very highly recommend the rspec_api_documentation gem.

Resources