Capybara expect BEFORE ajax request completed - ruby-on-rails

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

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

Rspec Capybara test with ajax call not updating record

I have a form where when a user submits an ajax form with a single name field, a record is created that belongs to a Template object. This all works fine manually, but for some reason when I test this via rspec its telling me that the association was not created. Why isn't the template object updated here?
describe "edit page" do
before(:each) do
visit edit_admin_template_path(template)
end
context "form sections", js: true do
it "allows admin to create one" do
find("#btn-add-section").click
within('#new_form_section') do
fill_in 'Name', with: "Personal Info"
click_button "Create Section"
end
expect(template.form_sections.length).to eq(1)
end
end
end
This is the failure that I am getting
Failure/Error: expect(template.form_sections.length).to eq(1)
expected: 1
got: 0
(compared using ==)
UPDATE: just noticed this in the rspec output
An error occurred in an after hook
ActionView::Template::Error: undefined method `form_sections' for nil:NilClass
so its telling me that the template object does not exist, then its able to compare it afterwards?
UPDATE 2: So it appears that the issue is waiting for the ajax call to complete in capybara, before expecting. I added this to the spec and it now works, obviously I need to refacotr this to something better like a helper
it "allows admin to create one" do
find("#btn-add-section").click
within('#new_form_section') do
fill_in 'Name', with: "Personal Info"
click_button "Create Section"
end
sleep(10) #this waits for the request to complete
expect(template.form_sections.length).to eq(1)
end
The key is telling RSpec to wait for the ajax call to complete before doing any expectations. This is a great post on the solution that I used:
Thoughtbot: Automatically Wait for AJAX with Capybara
Just in case others stumble upon this, rather than using sleep you could test based on the UI itself. Capybara will wait for that element to reload, if you use have_css or find matchers, like this:
expect(page).to have_css("#form-page-two", visible: :visible) #this will force test to wait for ajax call
Capybara.automatic_reload must not be set to false. (defaults to true)
This strategy will reduce the run time of your test suite; if the test only took 2 or 3 seconds to load it will only wait that long (not 10).
However, it can be frustrating to write because of intermittent failures. To avoid this you may need to bump up the wait time setting.
Capybara.default_max_wait_time = 10
https://github.com/teamcapybara/capybara#asynchronous-javascript-ajax-and-friends

Waiting for redirect in Capybara 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!

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