2
votes

Since upgrading to Capybara 2.4, I've been running into this issue. Previously, this block worked fine:

page.document.synchronize do
  page.should have_no_css('#ajax_indicator', :visible => true)
end

It's meant to force waiting until the ajax indicator disappears before proceeding with the next step.

Since the above returns a RSpec::Expectations::ExpectationNotMetError, the synchronize doesn't rerun the block and instead just throws the error. Not sure why this was working in the version I was using before (I believe 2.1).

The synchronize block only reruns blocks that return something like:

Capybara::ElementNotFound
Capybara::ExpectationNotMet

And whatever a certain driver adds to that list.

See Justin's response for a more comprehensive explanation and examples not using synchronize, or look at my response for the direct solution.


Follow-up: I've since revisited this problem so here's a few tips:

1) document.synchronize more often than not does nothing useful, as mentioned by others most of the finders have built-in wait. You can manually force it to not wait using wait: 0 when that makes sense.

Note that due to this, the following are not equivalent:

!assert_css('#ajax_indicator')
assert_no_css('#ajax_indicator')

The former will wait until the element exists, while the latter will wait until the element doesn't exist, even if they are otherwise logically equivalent.

2) Problems that lead to us initially inserting synchronize were due to a chicken and egg problem of sorts.

#ajax_indicator in this case will appear when there is active loading, then disappear after. We cannot distinguish from the indicator having not appeared yet, and having appeared then disappeared.

Although I have yet to 100% resolve this issue, what has improved our test reliability is looking for indicators to ensure page loading has reached a certain point, e.g.

do_thing_that_triggers_ajax
find('#thing-that-should-exist')
assert_no_selector('#ajax_indicator', visible: true)

This differents from asserting #ajax_indicator exists and then assert it doesn't exist, because in that case if the ajax happens too fast capybara may not catch it in action.

Depending on your scripts, you could possibly find more reliable indicators.

3
Actually users of Capybara aren't supposed to use synchronize directly.Andrei Botalov
I don't see why not, it's publicly documented and everything. It's entirely possible that I'm using it incorrectly though (as described at elabs.se/blog/53-why-wait_until-was-removed-from-capybara).Karl
As you see it's intended for cases where you have to use native. Amount of such cases is very rare. And yes, you use it incorrectly.Andrei Botalov

3 Answers

3
votes

The have_no_css matcher already waits for the element to disappear. The problem seems to be using it within a synchronize block. The synchronize method only re-runs for certain exceptions, which does not include RSpec::Expectations::ExpectationNotMetError.

Removing the synchronize seems to do what you want - ie forces a wait until the element disappears. In other words, just do:

page.should have_no_css('#ajax_indicator', :visible => true)

Working Example

Here is a page, say "wait.htm", that I think reproduces your problem. It has a link that when clicked, waits 6 seconds and then hides the indicator element.

<html>
  <head>
    <title>wait test</title>
    <script type="text/javascript" charset="utf-8">
      function setTimeoutDisplay(id, display, timeout) {
          setTimeout(function() {
              document.getElementById(id).style.display = display;
          }, timeout);
      }
    </script>
  </head>

  <body>
    <div id="ajax_indicator" style="display:block;">indicator</div>
    <a id="hide_foo" href="#" onclick="setTimeoutDisplay('ajax_indicator', 'none', 6000);">hide indicator</a>
  </body>
</html>

The following spec shows that by using the page.should have_no_css without manually calling synchronize, Capybara is already forcing a wait. When waiting only 2 seconds, the spec fails since the element does not disappear. When waiting 10 seconds, the spec passes since the element has time to disappear.

require 'capybara/rspec'

Capybara.run_server = false
Capybara.current_driver = :selenium
Capybara.app_host = 'file:///C:/test/wait.htm'

RSpec.configure do |config|
  config.expect_with :rspec do |c|
    c.syntax = [:should, :expect]
  end
end

RSpec.describe "#have_no_css", :js => true, :type => :feature do
  it 'raise exception when element does not disappear in time' do
    Capybara.default_wait_time = 2

    visit('')
    click_link('hide indicator')
    page.should have_no_css('#ajax_indicator', :visible => true)
  end

  it 'passes when element disappears in time' do
    Capybara.default_wait_time = 10

    visit('')
    click_link('hide indicator')
    page.should have_no_css('#ajax_indicator', :visible => true)
  end
end
1
votes

Since version of Capybara 2.0 you can customize inline wait time parameter to pass it into the #have_no_css method:

page.should have_no_css('#ajax_indicator', visible: true, wait: 3)
-1
votes

The solution I've settled for is the following:

page.document.synchronize do
  page.assert_no_selector('#ajax_indicator', :visible => true)
end

The assert_no_selector method properly throws a Capybara::ExpectationNotMet error and appears to work in the same way as has_no_css, so I'm satisfied with this solution.

I still have no idea why the RSpec error is being thrown for some methods but not others.


Edit: While this works it isn't actually a good idea to do so, see other responses.