Browse files

Selenium driver automatically waits for AJAX requests to finish

  • Loading branch information...
1 parent 6f5c1c3 commit 021b87f6ba512f9903b209178c584def45d62e7f Jonas Nicklas and Nicklas Ramhöj committed with Jonas Nicklas and Nicklas Ramhöj Feb 22, 2011
View
1 History.txt
@@ -5,6 +5,7 @@ Release date:
### Added
* Added DSL for acceptance tests, inspired by Luismi Cavall??'s Steak [Jonas Nicklas]
+* Selenium driver automatically waits for AJAX requests to finish [mgiambalvo, Nicklas Ramh??j and Jonas Nicklas]
### Changed
View
5 README.rdoc
@@ -174,6 +174,11 @@ At the moment, Capybara supports Webdriver, also called Selenium 2.0, *not*
Selenium RC. Provided Firefox is installed, everything is set up for you, and
you should be able to start using Selenium right away.
+By default Capybara tried to synchronize AJAX requests, so it will wait for
+AJAX requests to finish after you've interacted with the page. You can switch
+off this behaviour by setting the driver option <tt>:resynchronize</tt> to
+<tt>false</tt>. See the section on configuring drivers.
+
== Celerity
Celerity only runs on JRuby, so you'll need to install the celerity gem under
View
92 lib/capybara/driver/selenium_driver.rb
@@ -1,6 +1,13 @@
require 'selenium-webdriver'
class Capybara::Driver::Selenium < Capybara::Driver::Base
+ DEFAULT_OPTIONS = {
+ :resynchronize => true,
+ :resynchronization_timeout => 10,
+ :browser => :firefox
+ }
+ SPECIAL_OPTIONS = [:browser, :resynchronize, :resynchronization_timeout]
+
class Node < Capybara::Driver::Node
def text
native.text
@@ -26,32 +33,34 @@ def value
def set(value)
if tag_name == 'input' and type == 'radio'
- native.click
+ click
elsif tag_name == 'input' and type == 'checkbox'
- native.click if value ^ native.attribute('checked').to_s.eql?("true")
+ click if value ^ native.attribute('checked').to_s.eql?("true")
elsif tag_name == 'textarea' or tag_name == 'input'
- native.clear
- native.send_keys(value.to_s)
+ resynchronize do
+ native.clear
+ native.send_keys(value.to_s)
+ end
end
end
def select_option
- native.select
+ resynchronize { native.select }
end
def unselect_option
if select_node['multiple'] != 'multiple' and select_node['multiple'] != 'true'
raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box."
end
- native.toggle if selected?
+ resynchronize { native.toggle } if selected?
end
def click
- native.click
+ resynchronize { native.click }
end
def drag_to(element)
- native.drag_and_drop_on(element.native)
+ resynchronize { native.drag_and_drop_on(element.native) }
end
def tag_name
@@ -76,6 +85,10 @@ def find(locator)
private
+ def resynchronize
+ driver.resynchronize { yield }
+ end
+
# a reference to the select node if this is an option node
def select_node
find('./ancestor::select').first
@@ -91,7 +104,7 @@ def type
def browser
unless @browser
- @browser = Selenium::WebDriver.for(options[:browser] || :firefox, options.reject{|key,val| key == :browser})
+ @browser = Selenium::WebDriver.for(options[:browser], options.reject { |key,val| SPECIAL_OPTIONS.include?(key) })
at_exit do
@browser.quit
end
@@ -101,7 +114,7 @@ def browser
def initialize(app, options={})
@app = app
- @options = options
+ @options = DEFAULT_OPTIONS.merge(options)
@rack_server = Capybara::Server.new(@app)
@rack_server.boot if Capybara.run_server
end
@@ -128,6 +141,18 @@ def find(selector)
def wait?; true; end
+ def resynchronize
+ if options[:resynchronize]
+ load_wait_for_ajax_support
+ yield
@ryana
ryana added a note Mar 27, 2011

Is this yield in the wrong spot? Wouldn't we want to yield after waiting for the ajax requests to synchronize?

@joliss
Collaborator
joliss added a note Mar 27, 2011

The idea is, let's say you write

click_on 'Ajax link'  # this calls resynchronize
node = page.find_css('...')

then you don't want to accidentally pick up a node that will be removed from the DOM a second later (when the Ajax finishes). So first you click (i.e. yield), then you wait.

@ryana
ryana added a note Mar 27, 2011

Ahh I see. So I'm running into a problem where a click pops up up a jQuery QTip floater. I then want to click a link inside that floater. So:

  click_link @user.email
  click_link 'Sign out'

fails because it takes a couple hundred milliseconds before 'Sign Out' is visible. The odd thing is that if I do this:

  click_link @user.email
  sleep(5)
  click_link 'Sign out'

it works, but it doesn't take 5 seconds longer. It takes dozens of seconds to complete. For now it's working, but it'd be nice to be able speed things up. I'm gonna look into modifying capybaraRequestsOutstanding to include some notion of jquery/prototype effects currently executing.

@jnicklas
Owner

I think it's a very bad idea to put framework specific code (jquery or prototype specific) into Capybara. This is going to break sooner rather than later.

@joliss
Collaborator
joliss added a note Mar 28, 2011

That's funny -- shouldn't the second click_link retry finding the 'Sign Out' link for two seconds (because it uses find, which in turn uses wait_conditionally_until)?

Perhaps try

Capybara.configure do |config|
  config.default_wait_time = 10
end

for a longer timeout than the default 2 seconds?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ Capybara.timeout(options[:resynchronization_timeout], self, "failed to resynchronize, ajax request timed out") do
+ evaluate_script("!window.capybaraRequestsOutstanding")
+ end
+ else
+ yield
+ end
+ end
+
def execute_script(script)
browser.execute_script script
end
@@ -148,12 +173,57 @@ def within_frame(frame_id)
browser.switch_to.window old_window
end
- def within_window(handle, &blk)
+ def find_window( selector )
+ original_handle = browser.window_handle
+ browser.window_handles.each do |handle|
+ browser.switch_to.window handle
+ if( selector == browser.execute_script("return window.name") ||
+ browser.title.include?(selector) ||
+ browser.current_url.include?(selector) ||
+ (selector == handle) )
+ browser.switch_to.window original_handle
+ return handle
+ end
+ end
+ raise Capybara::ElementNotFound, "Could not find a window identified by #{selector}"
+ end
+
+ def within_window(selector, &blk)
+ handle = find_window( selector )
browser.switch_to.window(handle, &blk)
end
private
+ def load_wait_for_ajax_support
+ browser.execute_script <<-JS
+ window.capybaraRequestsOutstanding = 0;
+ (function() { // Overriding XMLHttpRequest
+ var oldXHR = window.XMLHttpRequest;
+
+ function newXHR() {
+ var realXHR = new oldXHR();
+
+ window.capybaraRequestsOutstanding++;
+ realXHR.addEventListener("readystatechange", function() {
+ if( realXHR.readyState == 4 ) {
+ setTimeout( function() {
+ window.capybaraRequestsOutstanding--;
+ if(window.capybaraRequestsOutstanding < 0) {
+ window.capybaraRequestsOutstanding = 0;
+ }
+ }, 500 );
+ }
+ }, false);
+
+ return realXHR;
+ }
+
+ window.XMLHttpRequest = newXHR;
+ })();
+ JS
+ end
+
def url(path)
rack_server.url(path)
end
View
36 lib/capybara/spec/driver.rb
@@ -127,6 +127,42 @@
@driver.evaluate_script('1+1').should == 2
end
end
+
+end
+
+shared_examples_for "driver with resynchronization support" do
+ before { @driver.visit('/with_js') }
+ describe "#find" do
+ context "with synchronization turned on" do
+ it "should wait for all ajax requests to finish" do
+ @driver.find('//input[@id="fire_ajax_request"]').first.click
+ @driver.find('//p[@id="ajax_request_done"]').should_not be_empty
+ end
+ end
+
+ context "with resynchronization turned off" do
+ before { @driver.options[:resynchronize] = false }
+
+ it "should not wait for ajax requests to finish" do
+ @driver.find('//input[@id="fire_ajax_request"]').first.click
+ @driver.find('//p[@id="ajax_request_done"]').should be_empty
+ end
+
+ after { @driver.options[:resynchronize] = true }
+ end
+
+ context "with short synchronization timeout" do
+ before { @driver.options[:resynchronization_timeout] = 0.1 }
+
+ it "should raise an error" do
+ expect do
+ @driver.find('//input[@id="fire_ajax_request"]').first.click
+ end.to raise_error(Capybara::TimeoutError, "failed to resynchronize, ajax request timed out")
+ end
+
+ after { @driver.options[:resynchronization_timeout] = 10 }
+ end
+ end
end
shared_examples_for "driver with header support" do
View
9 lib/capybara/spec/public/test.js
@@ -25,9 +25,14 @@ $(function() {
}, 500);
});
$('#with_focus_event').focus(function() {
- $('body').append('<p id="focus_event_triggered">Focus Event triggered</p>')
+ $('body').append('<p id="focus_event_triggered">Focus Event triggered</p>');
});
$('#checkbox_with_event').click(function() {
- $('body').append('<p id="checkbox_event_triggered">Checkbox event triggered</p>')
+ $('body').append('<p id="checkbox_event_triggered">Checkbox event triggered</p>');
+ });
+ $('#fire_ajax_request').click(function() {
+ $.ajax({url: "/slow_response", context: document.body, success: function() {
+ $('body').append('<p id="ajax_request_done">Ajax request done</p>');
+ }});
});
});
View
5 lib/capybara/spec/test_app.rb
@@ -59,6 +59,11 @@ class TestApp < Sinatra::Base
redirect back
end
+ get '/slow_response' do
+ sleep 2
+ 'Finally!'
+ end
+
get '/set_cookie' do
cookie_value = 'test_cookie'
response.set_cookie('capybara', cookie_value)
View
4 lib/capybara/spec/views/with_js.erb
@@ -34,6 +34,10 @@
<p>
<input type="checkbox" id="checkbox_with_event"/>
</p>
+
+ <p>
+ <input type="submit" id="fire_ajax_request" value="Fire Ajax Request"/>
+ </p>
</body>
</html>
View
4 lib/capybara/util/timeout.rb
@@ -4,7 +4,7 @@ class << self
##
# Provides timeout similar to standard library Timeout, but avoids threads
#
- def timeout(seconds = 1, driver = nil, &block)
+ def timeout(seconds = 1, driver = nil, error_message = nil, &block)
start_time = Time.now
result = nil
@@ -14,7 +14,7 @@ def timeout(seconds = 1, driver = nil, &block)
delay = seconds - (Time.now - start_time)
if delay <= 0
- raise TimeoutError
+ raise TimeoutError, error_message || "timed out"
end
driver && driver.wait_until(delay)
View
1 spec/driver/selenium_driver_spec.rb
@@ -7,6 +7,7 @@
it_should_behave_like "driver"
it_should_behave_like "driver with javascript support"
+ it_should_behave_like "driver with resynchronization support"
it_should_behave_like "driver with frame support"
it_should_behave_like "driver with support for window switching"
it_should_behave_like "driver without status code support"

1 comment on commit 021b87f

@mattwynne

You fucking beauty!

Please sign in to comment.