Capybara: Session lost when adding a setTimeout on form.submit() - ruby-on-rails

Rails v4.2.7
I have a rails form (not remote) with a submit button with this event attached for both preventing a double submission, and showing a better feedback for the user (basically what data-disable-with would do for a remote form):
$('#my-submit-button').on "click", ->
this.disabled = "disabled"
this.value = "Loading..."
$(this).removeClass("button-active-class")
$(this).addClass("button-busy-class")
myForm = this.form
setTimeout ->
myForm.submit()
, 500
The reason for the setTimeout is that if I instantly submit the form, for some reason in Safari the style changes are not applied.
This works great.
however, I have a Capybara test (which uses headless_chrome) like this:
sign_in user
visit my_page_path
# fill in form
find("#my-submit-button").click
expect(page).to have_content "some content"
For some reason I noticed the current_user was being set to nil after the form was submitted, so I added a log for cookies as a before_filter in my ApplicationController:
def my_before_filter
p cookies.first
end
And the output was effectively showing me two different cookies between the visit, and the request triggered by the form submission.
So I removed the setTimeout in order to submit the form instantly:
$('#my-submit-button').on "click", ->
# [...]
# myForm = this.form
# setTimeout ->
# myForm.submit()
# , 500
this.form.submit()
And now, even though the styling is not correctly applied after clicking on the button, the test passes and cookies are the same.
Any idea what's going on and how to fix it?

Note: This is a guess based on the the assumption your sign_in method actually visits the page, fills in the login info and submits the form. It your sign_in method is actually a shortcut via something like the devise warden test mode this won't be correct.
It sounds like your $('#my-submit-button') is also being applied to the submit button on the login form. By delaying that half a second (seems like a long time to delay) the driver no longer detects that the actions done in sign_in are triggering a page transition and therefore doesn't wait for it. This means the visit my_page_path gets executed before login has completed and the correct cookies have been returned. Your sign_in method should have a positive expectation at the end of it for something on the page that indicates logging in has completed along the lines of
expect(page).to have_text('You are logged in!')

Related

Using Rspec/Capybara, how erase 'browser' session cookie after visiting form (to simulate common cause of CSRF errors)

One of the form pages on our website is showing, occasionally, inconsistently, a fair number of ActionController::InvalidAuthenticityToken errors.
Viewing the logs we can see this occurs when:
a) user visits form (works)
b) clicks an option (works)
c) clicks another option DAYS later (causes CSRF error)
We believe the issue is that after (b) the user quits their browser, which erases session cookies, then when they re-open their browser if they have "reopen pages" enabled on their browser it re-displays the page, but there is no session and therefore the expected CSRF token is missing from their session.
We've added some code to handle that particular CSRF error with a custom error message.
We'd like to TEST the new code in rspec by simulating the same sequence of events.
That requires a way to erase Capybara's session for that user, simulating what happens if browser is closed then reopens:
visit form_url
click_button button_a
?? erase session cookie, with page object still 'open', so form object still exists??
click_button button_b
Using reset_session! wipes the page object too.
In an Rspec spec, how can we erase/invalidate the "browser" session cookie so when we click another button on the form, a Rails CSRF error will be triggered?
I think there is a simple way to do this that involves making a request to your Rails backend. The high level is that you expose a route and a controller action to test mode that you do not expose in any other environment.
Make the reqeust to this endpoint in the specific situation you want to clear the session cookie.
In the controller action, you clear the session cookie and and set this so that the request you have just made is not going to set a new session
# config/routes.rb
get 'clear-session', :to => 'application#__clear_session' if Rails.env.test?
and
# controllers/application.rb
if Rails.env.test?
def __clear_session
request.session_options[:skip] = true
cookies['__yourapp_session'] = { :value => '', :expires => Time.at(0) }
end
end

Rspec testing, Capybara unable to find link or button

This the part of my requests test that fails:
scenario 'Admin destroys a job posting + gets notified' do
parent = create(:parent)
create(:assignment, user_id: #user.id, role_id: 1)
demand = create(:demand, shift_id: 4)
sign_in(#user)
visit demands_path
click_on 'Destroy'
expect(page).to have_content('successfully')
end
This is the error:
Failure/Error: click_on 'Destroy'
Capybara::ElementNotFound:
Unable to find link or button "Destroy"
And here is the corresponding index view, including a "Destroy" link in the app:
Any idea why this test fails??
Odds are the data you assume is on the page actually isn't. This could be for a number of reasons.
Your page requires JS and you're not using a JS capable driver - see https://github.com/teamcapybara/capybara#drivers
Your sign_in method is defined to fill in user/pass and then click a button, but doesn't have an expectation for content that confirms the user has completed login at the end. This can lead to the following visit occurring before login has completed and therefore not actually logging in. Verify that by inspecting the result of page.html or calling page.save_and_open_screenshot before the click.
Your 'Destroy' "button" is neither an actual <a> element or <button> element. Fix that by either using semantic markup or swapping to find(...).click
You are using a JS capable driver but your records aren't actually visible to the app - this would affect all your tests though so I assume it's probably not this. If this was the case the login would fail and you'd probably need to install database_cleaner and configure for use with RSpec & Capybara - https://github.com/DatabaseCleaner/database_cleaner#rspec-with-capybara-example

Rails 5 - how to implement ajax flash message for devise

I am making a rails5 blog app with devise. In the comments controller, I check if the user exists using "before_action :authenticate_user!".
So, if the user is logged in then create comment method gets executed and the server response gets send back to the client via AJAX. However, if the user is not logged in then "before_action :authenticate_user!" check creates a 401 authentication error in the response ( can be seen in the browser log).
I want to just display the flash message just above the comment box or somewhere on the page so that user knows that he/she can not enter a comment without logged in.
There are many similar questions I have found but they seem very confusing and none of the solutions worked for me. Most of them talk about the changing the devise confg (e.g. config.http_authenticatable_on_xhr = false) or override the authenticate_user method etc.
Can someone just explain is it possible to show a flash message warning to the user to login to add comment (ajax call). I don't want automatic redirect for AJAX calls.
Create a flash method then catch the unauthorized error in jQuery and show your flash message :
#flash = (content, type) ->
content = "<div class='container'><div class=\"alert alert-#{type}\">#{content}</div></div>" if type?
$('#flash').clearQueue().hide().html(content).fadeIn('slow').delay(3000).fadeOut('slow')
$(document).ajaxError (e, xhr, settings) ->
flash("Unauthorized", "danger") if xhr.status == 401

Rails/Capybara- Test that form does not submit

In my Rails app, I used the following jQuery to disable the ability to submit a form by pressing 'enter' in an input field:
$('input').on('keydown', function(e) {
if(e.which == '13') {
return false;
}
})
I wanted to test this with Capybara, but can't figure out how. For example:
# the form being tested creates a new Shift object:
test "enter does not submit form" do
shift_count = Shift.count
# fill in form, etc...
page.driver.browser.execute_script "var e = jQuery.Event('keydown'); e.which = 13; $('#shift_employee').trigger( e );"
assert Shift.count == shift_count
end
This test passes even when the jQuery function is commented out, presumably because it calculates Shift.count before ActiveRecord has had time to save the new the record to the database.
I also tried having the jQuery function do a custom $.get() request, to which the controller responds with simple text, thinking that this would build in enough time for all server-side work to be completed. But this still doesn't work, and I think it's because, no matter how I set up the jQuery function, the submission of the form and saving of the new record will always occur after any AJAX stuff I build in.
Any ideas on how I can reliably test that the form was not submitted?
So the form submission is done via AJAX? This is a common problem that testers run in to with Capybara. What you need to do is wait for the AJAX to complete.
There are two ways to do this. One is very specific and is the preferred method, the other is generic and should only be used if the first method can't be done.
The first way is to wait for content to change on the DOM. In your case nothing might change, but if, for example, you had a warning come up saying "You cannot press Enter, you must click the Save button!" then you could wait for that to come up by doing something like:
page.driver.browser ... # your code
page.should have_css(".info", :text => "You cannot ...etc...")
Of course you don't have to use the :text option though.
The other way is to make a helper method, like wait_for_ajax, that does a generic wait until AJAX is complete.
def wait_for_ajax
start = Time.now
while true
break if (page.evaluate_script('$.active') == 0)
if Time.now > start + Capybara.default_wait_time.seconds
fail "AJAX did not register as complete after #{Capybara.default_wait_time} seconds!"
end
sleep 0.1
end
end
This is based off the old way that people used to check for AJAX, but this new script is preferred for Capybara 2.0.
Then your step would do:
page.driver.etc
wait_for_ajax
That wait_for_ajax method can go in to any file Cucumber will load.
I got it to work by having Capybara revisit the page, and then checking Shift.count. The test now fails when the jQuery function is commented out, and passes when the function is uncommented. I'd still like to know if there's a better way, though.

Sorcery redirects wrong

User wants to sign up at domain.com/users/new, however the user accidentally presses "enter" and all fields are filled in blank.
Now comes the problem. I have two options. If I remove...
...else
redirect_to new_user_path
end
the user will automatically be redirected to domain.com/users (after filling the fields in blank) and where the sign up field errors will nicely be displayed above the form.
However if I add that code above instead of removing it, the user will be redirected to the same link - that means /users/new and unfortunately the user will not have his form errors displayed nicely, they will actually not be displayed at all.
How come that the errors show up at domain.com/users and not at domain.com/users/new?

Resources