Search code examples
ruby-on-railstestingdevisecapybara

Why in Capybara test user logs out after any redirect?


I am trying to test feature of editing the question by the user with js: true, and it fails because user is not signed in during the test (even though there should be user session).

I use Devise for authentication.

feature 'Authenticated User can edit own questions' do
  given(:user) { create(:user) }
  given(:another_user) { create(:user) }
  given!(:question) { create(:question, user:) }
  given!(:another_question) { create(:question, user: another_user) }

  describe 'Authenticated user', js: true do
    background { sign_in user }  ### Here I am signing in

    scenario 'can edit the question' do
      visit question_path(question) 
      
      ### Here save_and_open_page shows that no active session

      within "#question_#{question.id}" do ### Finds turbo frame tag #question_{id}
        click_on 'Edit'
        fill_in 'Body', with: 'Question is edited.'
        click_on 'Save'

        expect(page).to_not have_content question.body
        expect(page).to have_content 'Question is edited.'
        expect(page).to_not have_selector 'textarea'
      end

      expect(page).to have_content 'Question is edited.'
    end
  end
end

This is my feature helper for signing in:

def sign_in(user)
    visit new_user_session_path
    within 'form#new_user' do
      fill_in 'Email', with: user.email
      fill_in 'Password', with: user.password
      click_on 'Log in'
    end
end

If I am testing without js: true the test works and user session is not terminated after each redirect during the test.

I use Capybara.javascript_driver = :selenium_chrome_headless as a js driver.


Solution

  • The accepted answer here may solve the issue but it's a bad solution. The "solution" is dependent on the speed the test hardware runs at, and therefore may be flaky when you move to a cloud test infrastructure, or when your local machine is heavily loaded. The root cause of the issue is that the tests run asynchronously and require checking for visible items in the page to synchronize. In the test you call 'sign_in' which fills out the login page and clicks the "Log In" button, but there's nothing waiting for the login to actually occur. Therefore the test continues, and calls visit ... which changes the page, cancelling the browsers submission, before the login has completed and before the session cookie has been returned. Since the browser doesn't yet have the session cookie the user isn't authenticated on the new page. The sleep(1) "fixes" that by trying to give the login time to finish. In some cases that 1 second will be waiting too long (wasted time) and in other it may not be enough (flaky tests). The better solution is to check for a change on the page which indicates the login has completed

    def sign_in(user)
        visit new_user_session_path
        within 'form#new_user' do
          fill_in 'Email', with: user.email
          fill_in 'Password', with: user.password
          click_on 'Log in'
        end
        expect(page).to have_text('Login Succeeded') # text shown on page that indicates a login has finished - or .have_css('#logged_in_menu'), etc
    end
    

    Summary: Don't add sleep to tests, check for changes in the page which indicate it's safe to move on