diff --git a/History.txt b/History.txt index 8c3dbb04..1c99d17a 100644 --- a/History.txt +++ b/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 diff --git a/README.rdoc b/README.rdoc index d55a21ed..a1d0cf55 100644 --- a/README.rdoc +++ b/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 :resynchronize to +false. See the section on configuring drivers. + == Celerity Celerity only runs on JRuby, so you'll need to install the celerity gem under diff --git a/lib/capybara/driver/selenium_driver.rb b/lib/capybara/driver/selenium_driver.rb index fbfd51c9..df2b2e85 100644 --- a/lib/capybara/driver/selenium_driver.rb +++ b/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 @@ class Capybara::Driver::Selenium < Capybara::Driver::Base 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 @@ class Capybara::Driver::Selenium < Capybara::Driver::Base 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 @@ class Capybara::Driver::Selenium < Capybara::Driver::Base 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 @@ class Capybara::Driver::Selenium < Capybara::Driver::Base 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 @@ class Capybara::Driver::Selenium < Capybara::Driver::Base def wait?; true; end + def resynchronize + if options[:resynchronize] + load_wait_for_ajax_support + yield + 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 @@ class Capybara::Driver::Selenium < Capybara::Driver::Base 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 diff --git a/lib/capybara/spec/driver.rb b/lib/capybara/spec/driver.rb index df0baa6f..b9ffcd64 100644 --- a/lib/capybara/spec/driver.rb +++ b/lib/capybara/spec/driver.rb @@ -127,6 +127,42 @@ shared_examples_for "driver with javascript support" do @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 diff --git a/lib/capybara/spec/public/test.js b/lib/capybara/spec/public/test.js index 1ddc6f13..85e9499f 100644 --- a/lib/capybara/spec/public/test.js +++ b/lib/capybara/spec/public/test.js @@ -25,9 +25,14 @@ $(function() { }, 500); }); $('#with_focus_event').focus(function() { - $('body').append('

Focus Event triggered

') + $('body').append('

Focus Event triggered

'); }); $('#checkbox_with_event').click(function() { - $('body').append('

Checkbox event triggered

') + $('body').append('

Checkbox event triggered

'); + }); + $('#fire_ajax_request').click(function() { + $.ajax({url: "/slow_response", context: document.body, success: function() { + $('body').append('

Ajax request done

'); + }}); }); }); diff --git a/lib/capybara/spec/test_app.rb b/lib/capybara/spec/test_app.rb index 38c980af..394e9ed0 100644 --- a/lib/capybara/spec/test_app.rb +++ b/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) diff --git a/lib/capybara/spec/views/with_js.erb b/lib/capybara/spec/views/with_js.erb index 1517d67c..5e7ad161 100644 --- a/lib/capybara/spec/views/with_js.erb +++ b/lib/capybara/spec/views/with_js.erb @@ -34,6 +34,10 @@

+ +

+ +

diff --git a/lib/capybara/util/timeout.rb b/lib/capybara/util/timeout.rb index cc939e6c..7b7b3b83 100644 --- a/lib/capybara/util/timeout.rb +++ b/lib/capybara/util/timeout.rb @@ -4,7 +4,7 @@ module Capybara ## # 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 @@ module Capybara delay = seconds - (Time.now - start_time) if delay <= 0 - raise TimeoutError + raise TimeoutError, error_message || "timed out" end driver && driver.wait_until(delay) diff --git a/spec/driver/selenium_driver_spec.rb b/spec/driver/selenium_driver_spec.rb index c94f7cb2..4318e131 100644 --- a/spec/driver/selenium_driver_spec.rb +++ b/spec/driver/selenium_driver_spec.rb @@ -7,6 +7,7 @@ describe Capybara::Driver::Selenium do 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"