Proper way to wait for a second page to load with Capybara when the first has the same field as the second
Asked Answered
S

8

18

I'm having a problem with a spec that visits two forms with the same field ("Email") on both forms. If I don't manually sleep, Capybara seems to be finding the "Email" field from the first visit in the second portion of the test.

# visit the first form and fill out a subscription
visit new_front_form_subscription_path(@web_form_1.id)
fill_in "Email", with: "[email protected]"
fill_in "Field 1", with: "my first data"
click_button "Subscribe"

# visit the second form and fill out a subscription
visit new_front_form_subscription_path(@web_form_2.id)
sleep 1
fill_in "Email", with: "[email protected]"
fill_in "Field 2", with: "my second data"
click_button "Subscribe"

With the sleep in there, the spec passes with flying colors. Without the sleep, the second form submission gets a validation error -- blaming a blank "Email" value.

Is there a proper way to handle this? I dislike introducing manual sleeps into the specs.

Seeger answered 23/1, 2014 at 16:27 Comment(0)
O
15

Here are a couple of ways using which you can solve it:

  1. Use another locator that is present only at the second page to select "Email" field:

    find('#second_page #email').set('[email protected]')
    
  2. Write a statement that will wait for a second page to be loaded instead of sleep:

    visit new_front_form_subscription_path(@web_form_2.id)
    expect(page).to have_css('locator_present_only_at_second_page')
    fill_in "Email", with: "[email protected]"
    
Officialdom answered 24/1, 2014 at 21:20 Comment(4)
This is a little bit kludgey but probably the best way to do it. Thank you for the response!Seeger
Maybe I'm mistaken, but wouldn't it be possible that the expect line fails if the second page hasn't loaded yet?Benner
@Benner Almost all capybara methods wait automatically (including rspec matchers) - github.com/jnicklas/…Officialdom
Cool! Spurred me on to reading: elabs.se/blog/53-why-wait_until-was-removed-from-capybaraBenner
J
8

The above answers are correct, but difficult to implement if you have a large test suite, because you have to find a unique element on each page. If you are a graduate from the Michael Hartl school of rails there is an easy way to solve this. Hartl teaches setting a different @title instance variable for every controller action. This is then used in application.html.erb to set an html title element for every page.

I certainly do, and it allowed a very easy way to fix up all my flaky tests that failed because the page had not loaded properly. You cannot find the title tag because it is not visible. So what I did was put at the bottom of the application.html.erb, the following code;

<%= content_tag :div, "[#{title}]", class: 'rspec_eyes_only' if Rails.env.test? %>

This inserts, if you are in test mode, a div with title text, surrounded by square brackets. Then I wrote a helper that goes in rails_helper.rb

def wait_for_page_load(title)
  find('div.rspec_eyes_only', text: title)
end

I already has another helper to test for the correct title, so I modified it to add in wait_for_page_load i.e;

def title_is_correct(title)
  full_title = "#{BaseTitle} | #{title}"
  wait_for_page_load "[#{full_title}]"
  expect(page.title).to eq full_title
end

These simple changes cleared up almost all of my flaky tests.

Jubbah answered 7/1, 2017 at 7:7 Comment(0)
B
1

If you resorted to Andrey's 2) above, you might want to check out the holdon gem, which will allow you to write a block that will wait for an element to load, then move to the fill_in method once that element loads.

Benner answered 27/1, 2014 at 21:59 Comment(1)
Equivalent for e = HoldOn.until(timeout: 60, interval: 5) { s.first(:css, "div#bastard") } is e = find('div#bastard', wait: 60, match: :first). Holdon gem is completely unnecessary. But I prefer not to pass :match param: find('div#bastard', wait: 60)Officialdom
C
1

Generally, it can be done this way. For example to click on buttons which are found after a modal pop-up (or another page) is loaded, add the following block to your macro file.

def wait_until_modal
   page.has_css?('.modal')
end

Here class .modal is found on the on the pop-up page.

Crissy answered 10/5, 2016 at 20:34 Comment(0)
V
0

There is the "timestamping" approach. Change your layout file (e.g.application.html.haml) to serve the timestamp:

%body{"data-rendered-at" => Time.now.to_f.to_s}

Then create a helper method to wait the page unloading:

# in capybara_helper.rb
def with_waiting_unload(&block)
  current_rendered_at = find('body')['data-rendered-at']
  yield
  expect(page).not_to have_css("body[data-rendered-at='#{current_rendered_at}']")
end

Finally your code will be:

# (snip)...
with_waiting_unload do
    visit new_front_form_subscription_path(@web_form_2.id)
end
sleep 1
fill_in "Email", with: "[email protected]"
# ...(snip)
Virgo answered 23/1, 2014 at 16:27 Comment(0)
K
0

Try adding , wait: 10 to the first line following the new page load:

visit new_front_form_subscription_path(@web_form_2.id)
fill_in "Email", with: "[email protected]", wait: 10 # <--

I just had a similar situation:

visit promo_path
fill_in 'promo_code', with: 'HALFPRICEPROMO'
click_button 'Activate' # onClick does things and eventually loads the next page
assert page.has_content? 'Confirm your email address', wait: 10 # <--

Any and all assertions fail without the wait above. I put 10 to play it extra safe; 3 seems to be the minimum for my situation.

I found the answer here: Make capybara wait until Ajax finishes

Kennet answered 22/11, 2020 at 8:16 Comment(0)
E
0

With SitePrism you can use wait_until_true to test the current_url.

new_path = new_front_form_subscription_path(@web_form_2.id)
visit new_path
wait_until_true { current_url.include? new_path }
fill_in "Email", with: "[email protected]"
fill_in "Field 2", with: "my second data"
click_button "Subscribe"
Eggers answered 13/1, 2021 at 20:48 Comment(0)
F
-1

I use this for page transitions:

visit page1

do_something

visit page2

Timeout.timeout(Capybara.default_max_wait_time) do
  sleep(0.1) until current_url.include?(page2)
end

do_something_else
Fiedling answered 3/8, 2021 at 16:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.