I'm trying to get Capybara working with selenium-webdriver
for the first time, with a legacy app. We have Capybara tests working that use rack_driver
, but trying to implement Selenium on the same tests has not worked yet - the session is lost within a single test. I'm
use_transactional_tests = false
database-cleaner
as suggested for this situation.DatabaseCleaner.strategy = :truncation
Something is clearly wrong with either my test setup or helpers, or Database Cleaner config, but after reading through the Capybara docs and SO threads going back more than 10 years, i can't figure out what it is.
The test that's failing is simple, it involves logging in and creating a record with a form, which at first should fail because a field is invalid, and then validate when the fields are OK.
At some point during the test, the controller throws an error because User.current
is undefined, which it should not be after login. The new
and create
actions both require a logged in user, so i don't understand how this integration test is even able to execute any code where User.current
would be undefined, unless the session is being dropped.
Note that this same test passes, when the test is run under rack_driver
.
I'm using old-school Minitest and everything seems pretty standard. I have a separate test class set up for Capybara integration tests. Note, though, that it's not a Rails System Test
.
I have tried switching this to a system test with the same configuration, and it seems to make no difference. When I run the test inheriting from ActionDispatch::SystemTestCase
the controller throws the same error: User.current
unexpectedly becomes nil
at some point after login.
class ObservationFormIntegrationTest < CapybaraIntegrationTestCase
def test_create_minimal_observation
rolf = users("rolf")
login!(rolf)
assert(page.has_link?("Create Observation"))
click_on("Create Observation")
assert(page.has_selector?("body.observations__new"))
within("#observation_form") do
fill_in("naming_name", with: "Unrealistic name")
fill_in("observation_place_name", with: locations.first.name)
click_commit
end
assert(page.has_selector?("#name_messages",
text: "We do not recognize the name"))
assert_flash_warning
assert_flash_text(
:form_observations_there_is_a_problem_with_name.t.html_to_ascii
)
assert(page.has_selector?("#observation_form"))
within("#observation_form") do
fill_in("naming_name", with: "Coprinus comatus")
fill_in("observation_place_name", with: locations.first.name)
click_commit
end
assert(page.has_selector?("body.observations__show"))
assert_flash_success
assert_flash_text(:runtime_observation_success.t.html_to_ascii)
end
So the error is thrown by the controller, that User.current
is nil
. Here is the error:
Minitest::UnexpectedError: NoMethodError: undefined method `projects_member' for nil:NilClass
User.current.projects_member(include: :observations).each do |project|
^^^^^^^^^^^^^^^^
app/controllers/observations_controller/edit_and_update.rb:163:in `update_projects'
app/controllers/observations_controller/new_and_create.rb:200:in `save_everything_else'
app/controllers/observations_controller/new_and_create.rb:116:in `create'
app/controllers/application_controller.rb:240:in `catch_errors_and_log_request_stats'
The odd thing is that if i binding.break
at any point in the middle of this test, and manually execute the rest of the test assertions by command line, through to the last line, they all pass. The controller does not throw the error in that case.
This test is calling some of our test helpers. This may be where the session is lost, but until now it has handled passing the session
(as self
) OK.
# Login the given user in the current session.
def login(login = users(:zero_user).login, password = "testpassword",
remember_me = true, session: self)
login = login.login if login.is_a?(User) # get the right user field
session.visit("/account/login/new")
session.within("#account_login_form") do
session.fill_in("user_login", with: login)
session.fill_in("user_password", with: password)
session.check("user_remember_me") if remember_me == true
session.first(:button, type: "submit").click
end
end
# Login the given user, testing to make sure it was successful.
def login!(user, *args, **kwargs)
login(user, *args, **kwargs)
session = kwargs[:session] || self
assert_flash_success(session: session)
user = User.find_by(login: user) if user.is_a?(String)
assert_equal(user.id, User.current_id, "Wrong user ended up logged in!")
end
def assert_flash_text(text = "", session: self)
session.assert_selector("#flash_notices")
session.assert_selector("#flash_notices", text: text)
end
def assert_flash_warning(text = "", session: self)
session.assert_selector("#flash_notices.alert-warning")
assert_flash_text(text, session: session) if text
end
def assert_flash_success(text = "", session: self)
session.assert_selector("#flash_notices.alert-success")
assert_flash_text(text, session: session) if text
end
And here's my Capybara Integration test case, where everything is configured.
# Allow simuluation of user-browser interaction with capybara
require("capybara/rails")
require("capybara/minitest")
require("database_cleaner/active_record")
class CapybaraIntegrationTestCase < ActionDispatch::IntegrationTest
# Make the Capybara DSL available in these integration tests
include Capybara::DSL
# Make `assert_*` methods behave like Minitest assertions
include Capybara::Minitest::Assertions
# Include our helpers
include GeneralExtensions
include FlashExtensions
include CapybaraSessionExtensions
include CapybaraMacros
# Important to allow integration tests test the CSRF stuff to avoid unpleasant
# surprises in production mode.
def setup
ApplicationController.allow_forgery_protection = true
Capybara.default_max_wait_time = 2
# default in our test_helper = true. for capybara with selenium should be false
self.use_transactional_tests = false
# NOTE: Shouldn't be necessary, but in case:
# Capybara.reset_sessions!
# needed for selenium
Capybara.server = :webrick
Capybara.current_driver = :selenium_headless
# https://github.com/DatabaseCleaner/database_cleaner
# https://github.com/DatabaseCleaner/database_cleaner#minitest-example
# https://stackoverflow.com/questions/15675125/database-cleaner-not-working-in-minitest-rails
DatabaseCleaner.strategy = :truncation # :transaction :truncation
DatabaseCleaner.start
# Treat Rails html requests as coming from non-robots.
# If it's a bot, controllers often do not serve the expected content.
# The requester looks like a bot to the `browser` gem because the User Agent
# in the request is blank. I don't see an easy way to change that. -JDC
Browser::Bot.any_instance.stubs(:bot?).returns(false)
end
def teardown
Capybara.reset_sessions!
Capybara.use_default_driver
DatabaseCleaner.clean
ApplicationController.allow_forgery_protection = false
end
end
Figured it out. Since this was our first ever system test for this app in 15 years, it didn't occur to me that our JS autocompleters would be firing on the input fields.
They were holding up test execution because the test wasn't waiting for the fields to be filled, and creating timeouts that caused Capybara to drop the session.
The reason turning CSRF protection off for the tests allowed them to pass (comment above), is that it disabled the autocompleters — they were making authenticated calls to the app endpoints.
It turns out CSRF auth in these autocompleter GET
requests was unnecessary, but it's lucky I started randomly changing configs like ApplicationController.allow_forgery_protection
, because this was the change that tipped me off to check the autocompleters.