Warning: Creating default object from empty value in /homepages/39/d161420129/htdocs/p373.net/wp/wp-content/themes/p373b/admin/functions.php on line 183
Capybara, ajax, requirejs and how to pull your hair out in 8 easy hours…

Capybara, ajax, requirejs and how to pull your hair out in 8 easy hours…

Editors Note:  I wrote this post after a full day of hair pulling and shotgun debugging, so it may be a bit incoherent…but I wanted to get the situation documented as soon as possible.  Read at your own risk

Capybara is awesome for rails acceptance testing. However, I do have a love/hate relationship with it. For every hour its saved me in avoiding regressions, I’ve definitely had a few hours of hair pulling trying to get it to work properly, particularly with regards to javascript and ajax usage.

Currently, I’ve spent many hours trying to figure out why my tests were intermittently passing when testing a registration form that was submitted via ajax.

The first issue I had to come to terms with is that capybara will not know when asynchronously loaded javascript files have loaded(think requirejs).  This is a problem if you attach behavior to elements via one of these files.  Capybara will try to interact with your elements but they will be missing some behavior because those files may not have loaded yet.

The second issue is that Capybara doesn’t play nice when you do a full redirect via javascript.  It doesn’t seem to execute the redirect until after the spec example group is completed.  The solution I came up with is to “stall”(or in computer speak, block) Capybara by using one of its own matchers that knows how to wait if an element its trying to match on is missing.  The two methods I figured out that do this are “visit” and any of the matchers of the “have_selector” or “have_content” variety.

For brevity, my specs looked something like this:

context "when loading page" do
  before(:each) do
    @initial_user_count = User.count
    visit new_user_path
    page.fill_in :login, :with => "John"
    page.fill_in :email, :with => "john@example.com"
    page.fill_in :password, :with => "password"
    page.fill_in :password_confirmation, :with => "password"
    click_on "Register"
  end

  it "should create user" do
    User.count.should == @initial_user_count + 1
  end

  it "should show homepage" do
    page.current_path.should == root_path
  end
end

So the problem here is that when a user clicks on “Register”, an ajax request is sent off which submits the form, and if everything is ok, it will redirect the user via javascript.  The behavior I’m seeing is that capybara doesn’t seem to execute the javascript redirect, unless I use a matcher of the form have_selector, like have_selector(“#user_nav”).  So one solution was to put this in the before(:each) block:

click_on "Register"
page.should have_selector "#user_nav"

This seemed to do a decent job of fixing the problem, but there was another issue that took me a half-day to figure out.  In the end, I realized that I was getting intermittent passing results while I was perfecting the spec’s because of a weird situation with DatabaseCleaner.

The relevant portion of my spec_helper.rb looked like the following:

    config.before(:suite) do
      DatabaseCleaner.strategy = :truncation

    end

    config.before(:each) do
      DatabaseCleaner.start
    end

    config.after(:each) do
      DatabaseCleaner.clean
    end

So, what was happening was that my spec fired off an async request to add a user instance, but before that actually hit the server, the spec example was completing and DatabaseCleaner did its just of truncating the tables.  But then, lo and behold, the request made its way to the server, and added the user instance, leaving me with a dirty database!  This of course led to the next spec failing because a user with those credentials already existed, but of course the reason why wasn’t clear…because during this run, DatabaseCleaner did its job at the appropriate time and thus the test would pass on the next round….madness!

So the full solution involves forcing DatabaseCleaner to fully cleanup after the suite has run:

    config.before(:suite) do
      DatabaseCleaner.strategy = :truncation
      DatabaseCleaner.clean
    end

    config.before(:each) do
      DatabaseCleaner.start
    end

    config.after(:each) do
      DatabaseCleaner.clean
    end

    config.after(:suite) do
      DatabaseCleaner.clean
    end

And the full spec solution:

  before(:each) do
    @initial_user_count = User.count
    visit new_user_path
    page.fill_in :login, :with => "John"
    page.fill_in :email, :with => "john@example.com"
    page.fill_in :password, :with => "password"
    page.fill_in :password_confirmation, :with => "password"
    click_on "Register"
    page.should have_selector "#user_nav"
  end

  it "should create user" do
    User.count.should == @initial_user_count + 1
  end

  it "should show homepage" do
    page.current_path.should == root_path
  end

UPDATE(8/7/2012): Thanks to @badeball on github(https://github.com/jnicklas/capybara/issues/771#issuecomment-7552948), I was able to fully iron out the issue when assets weren’t loading fast enough and capybara would try to click a link before the remote form processing event handlers had been bound.

So I created a helper: spec/support/asychronous_helper.rb to handle the various ui cases I needed to wait for:

#/spec/support/asynchronous_helper.rb
module AsynchronousHelper

  def wait_until_remote_events_are_bound
    page.wait_until do
      page.evaluate_script("typeof($assetsloaded) === 'object'")
    end
  end

  def wait_until_overlay_has_popped_up
    page.wait_until do
      page.evaluate_script("$('div.overlay').is(':visible')")
    end    
  end

  def wait_until_overlay_disappears
    page.wait_until do
      !page.evaluate_script("$('div.overlay').is(':visible')")
    end
  end
end

Bam!

    This entry was posted in Coding, Ruby/Rails, Technology. Bookmark the permalink. Both comments and trackbacks are currently closed.
    • vladimeeer

      Excellent post.