From 941e50132a9699eebc2747ac484e92eeceb63f56 Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Wed, 13 Jul 2011 15:39:17 +0200 Subject: [PATCH] Elements are automatically reloaded --- lib/capybara.rb | 3 +- lib/capybara/driver/base.rb | 4 ++ lib/capybara/node/base.rb | 6 ++- lib/capybara/node/element.rb | 55 ++++++++++++++++++------- lib/capybara/node/finders.rb | 27 ++++++------ lib/capybara/node/simple.rb | 6 +-- lib/capybara/selenium/driver.rb | 6 ++- lib/capybara/session.rb | 2 +- lib/capybara/spec/public/test.js | 5 +++ lib/capybara/spec/session/find_spec.rb | 6 +++ lib/capybara/spec/session/javascript.rb | 42 +++++++++++++++++++ lib/capybara/spec/views/with_js.erb | 5 +++ 12 files changed, 129 insertions(+), 38 deletions(-) diff --git a/lib/capybara.rb b/lib/capybara.rb index 12e59da4..65307f26 100644 --- a/lib/capybara.rb +++ b/lib/capybara.rb @@ -16,7 +16,7 @@ module Capybara attr_accessor :asset_root, :app_host, :run_server, :default_host attr_accessor :server_port, :server_boot_timeout attr_accessor :default_selector, :default_wait_time, :ignore_hidden_elements, :prefer_visible_elements - attr_accessor :save_and_open_page_path + attr_accessor :save_and_open_page_path, :automatic_reload ## # @@ -236,6 +236,7 @@ Capybara.configure do |config| config.ignore_hidden_elements = false config.prefer_visible_elements = true config.default_host = "http://www.example.com" + config.automatic_reload = true end Capybara.register_driver :rack_test do |app| diff --git a/lib/capybara/driver/base.rb b/lib/capybara/driver/base.rb index 2b0d4f97..87ea8abc 100644 --- a/lib/capybara/driver/base.rb +++ b/lib/capybara/driver/base.rb @@ -43,6 +43,10 @@ class Capybara::Driver::Base raise Capybara::NotSupportedByDriverError end + def invalid_element_errors + [] + end + def wait? false end diff --git a/lib/capybara/node/base.rb b/lib/capybara/node/base.rb index 798dbec5..598ef243 100644 --- a/lib/capybara/node/base.rb +++ b/lib/capybara/node/base.rb @@ -22,7 +22,7 @@ module Capybara # session.has_css?('#foobar') # from Capybara::Node::Matchers # class Base - attr_reader :session, :base + attr_reader :session, :base, :parent include Capybara::Node::Finders include Capybara::Node::Actions @@ -33,6 +33,10 @@ module Capybara @base = base end + def reload + self + end + protected def wait? diff --git a/lib/capybara/node/element.rb b/lib/capybara/node/element.rb index 67c10939..0fe756d5 100644 --- a/lib/capybara/node/element.rb +++ b/lib/capybara/node/element.rb @@ -22,12 +22,18 @@ module Capybara # class Element < Base + def initialize(session, base, parent=nil, selector) + super(session, base) + @parent = parent + @selector = selector + end + ## # # @return [Object] The native element from the driver, this allows access to driver specific methods # def native - base.native + carefully { base.native } end ## @@ -35,7 +41,7 @@ module Capybara # @return [String] The text of the element # def text - base.text + carefully { base.text } end ## @@ -48,7 +54,7 @@ module Capybara # @return [String] The value of the attribute # def [](attribute) - base[attribute] + carefully { base[attribute] } end ## @@ -56,7 +62,7 @@ module Capybara # @return [String] The value of the form element # def value - base.value + carefully { base.value } end ## @@ -66,7 +72,7 @@ module Capybara # @param [String] value The new value # def set(value) - base.set(value) + carefully { base.set(value) } end ## @@ -74,7 +80,7 @@ module Capybara # Select this node if is an option element inside a select tag # def select_option - base.select_option + carefully { base.select_option } end ## @@ -82,7 +88,7 @@ module Capybara # Unselect this node if is an option element inside a multiple select tag # def unselect_option - base.unselect_option + carefully { base.unselect_option } end ## @@ -90,7 +96,7 @@ module Capybara # Click the Element # def click - base.click + carefully { base.click } end ## @@ -98,7 +104,7 @@ module Capybara # @return [String] The tag name of the element # def tag_name - base.tag_name + carefully { base.tag_name } end ## @@ -109,7 +115,7 @@ module Capybara # @return [Boolean] Whether the element is visible # def visible? - base.visible? + carefully { base.visible? } end ## @@ -119,7 +125,7 @@ module Capybara # @return [Boolean] Whether the element is checked # def checked? - base.checked? + carefully { base.checked? } end ## @@ -129,7 +135,7 @@ module Capybara # @return [Boolean] Whether the element is selected # def selected? - base.selected? + carefully { base.selected? } end ## @@ -139,7 +145,7 @@ module Capybara # @return [String] An XPath expression # def path - base.path + carefully { base.path } end ## @@ -150,7 +156,7 @@ module Capybara # @param [String] event The name of the event to trigger # def trigger(event) - base.trigger(event) + carefully { base.trigger(event) } end ## @@ -164,7 +170,13 @@ module Capybara # @param [Capybara::Element] node The element to drag to # def drag_to(node) - base.drag_to(node.base) + carefully { base.drag_to(node.base) } + end + + def reload + reloaded = parent.reload.first(@selector.name, @selector.locator, @selector.options) + @base = reloaded.base if reloaded + self end def inspect @@ -173,6 +185,19 @@ module Capybara %(#) end + def carefully(seconds=Capybara.default_wait_time) + start_time = Time.now + + begin + yield + rescue => e + raise e unless driver.respond_to?(:invalid_element_errors) and driver.invalid_element_errors.include?(e.class) + raise e if (Time.now - start_time) >= seconds + sleep(0.05) + reload + retry + end + end end end end diff --git a/lib/capybara/node/finders.rb b/lib/capybara/node/finders.rb index bc76ab07..43a7645f 100644 --- a/lib/capybara/node/finders.rb +++ b/lib/capybara/node/finders.rb @@ -120,10 +120,10 @@ module Capybara def all(*args) options = extract_normalized_options(args) - Capybara::Selector.normalize(*args).xpaths. - map { |path| find_in_base(path) }.flatten. - select { |node| matches_options(node, options) }. - map { |node| convert_element(node) } + selector = Capybara::Selector.normalize(*args) + selector.xpaths. + map { |path| find_in_base(selector, path) }.flatten. + select { |node| matches_options(node, options) } end ## @@ -143,10 +143,11 @@ module Capybara options = extract_normalized_options(args) found_elements = [] - Capybara::Selector.normalize(*args).xpaths.each do |path| - find_in_base(path).each do |node| + selector = Capybara::Selector.normalize(*args) + selector.xpaths.each do |path| + find_in_base(selector, path).each do |node| if matches_options(node, options) - found_elements << convert_element(node) + found_elements << node return found_elements.last if not Capybara.prefer_visible_elements or node.visible? end end @@ -156,12 +157,10 @@ module Capybara protected - def find_in_base(xpath) - base.find(xpath) - end - - def convert_element(element) - Capybara::Node::Element.new(session, element) + def find_in_base(selector, xpath) + base.find(xpath).map do |node| + Capybara::Node::Element.new(session, node, self, selector) + end end def wait_conditionally_until @@ -197,7 +196,7 @@ module Capybara end def has_selected_options?(node, expected) - actual = node.find('.//option').select { |option| option.selected? }.map { |option| option.text } + actual = node.all(:xpath, './/option').select { |option| option.selected? }.map { |option| option.text } (expected - actual).empty? end end diff --git a/lib/capybara/node/simple.rb b/lib/capybara/node/simple.rb index 66ec2a6c..bf5e2d4f 100644 --- a/lib/capybara/node/simple.rb +++ b/lib/capybara/node/simple.rb @@ -120,14 +120,10 @@ module Capybara protected - def find_in_base(xpath) + def find_in_base(selector, xpath) native.xpath(xpath).map { |node| self.class.new(node) } end - def convert_element(element) - element - end - def wait? false end diff --git a/lib/capybara/selenium/driver.rb b/lib/capybara/selenium/driver.rb index a373601f..1319f0d9 100644 --- a/lib/capybara/selenium/driver.rb +++ b/lib/capybara/selenium/driver.rb @@ -2,7 +2,7 @@ require 'selenium-webdriver' class Capybara::Selenium::Driver < Capybara::Driver::Base DEFAULT_OPTIONS = { - :resynchronize => false, + :resynchronize => true, :resynchronization_timeout => 10, :browser => :firefox } @@ -118,6 +118,10 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base # Browser must have already gone end + def invalid_element_errors + [Selenium::WebDriver::Error::ObsoleteElementError] + end + private def load_wait_for_ajax_support diff --git a/lib/capybara/session.rb b/lib/capybara/session.rb index e683c301..af6ef5f0 100644 --- a/lib/capybara/session.rb +++ b/lib/capybara/session.rb @@ -284,7 +284,7 @@ module Capybara end def document - Capybara::Node::Document.new(self, driver) + @document ||= Capybara::Node::Document.new(self, driver) end NODE_METHODS.each do |method| diff --git a/lib/capybara/spec/public/test.js b/lib/capybara/spec/public/test.js index 85e9499f..c6066a61 100644 --- a/lib/capybara/spec/public/test.js +++ b/lib/capybara/spec/public/test.js @@ -35,4 +35,9 @@ $(function() { $('body').append('

Ajax request done

'); }}); }); + $('#reload-link').click(function() { + setTimeout(function() { + $('#reload-me').replaceWith('
RELOADED
'); + }, 250) + }); }); diff --git a/lib/capybara/spec/session/find_spec.rb b/lib/capybara/spec/session/find_spec.rb index 792faac0..2e4bfc52 100644 --- a/lib/capybara/spec/session/find_spec.rb +++ b/lib/capybara/spec/session/find_spec.rb @@ -32,6 +32,12 @@ shared_examples_for "find" do it "should scope CSS selectors" do @session.find(:css, '#second').should have_no_css('h1') end + + it "should have a reference to its parent if there is one" do + @node = @session.find(:css, '#first') + @node.parent.should == @node.session.document + @node.find('a').parent.should == @node + end end context "with css selectors" do diff --git a/lib/capybara/spec/session/javascript.rb b/lib/capybara/spec/session/javascript.rb index 55011ae6..ea7bb4aa 100644 --- a/lib/capybara/spec/session/javascript.rb +++ b/lib/capybara/spec/session/javascript.rb @@ -18,6 +18,48 @@ shared_examples_for "session with javascript support" do end end + describe 'Node#reload', :focus => true do + context "without automatic reload" do + before { Capybara.automatic_reload = false } + it "should reload the current context of the node" do + @session.visit('/with_js') + node = @session.find(:css, '#reload-me') + @session.click_link('Reload!') + sleep(0.3) + node.reload.text.should == 'RELOADED' + node.text.should == 'RELOADED' + end + + it "should reload a parent node" do + @session.visit('/with_js') + node = @session.find(:css, '#reload-me').find(:css, 'em') + @session.click_link('Reload!') + sleep(0.3) + node.reload.text.should == 'RELOADED' + node.text.should == 'RELOADED' + end + after { Capybara.automatic_reload = true } + end + + context "with automatic reload" do + it "should reload the current context of the node automatically" do + @session.visit('/with_js') + node = @session.find(:css, '#reload-me') + @session.click_link('Reload!') + sleep(0.3) + node.text.should == 'RELOADED' + end + + it "should reload a parent node automatically" do + @session.visit('/with_js') + node = @session.find(:css, '#reload-me').find(:css, 'em') + @session.click_link('Reload!') + sleep(0.3) + node.text.should == 'RELOADED' + end + end + end + describe '#find' do it "should allow triggering of custom JS events" do pending "cannot figure out how to do this with selenium" if @session.mode == :selenium diff --git a/lib/capybara/spec/views/with_js.erb b/lib/capybara/spec/views/with_js.erb index 30d6638e..beabc3d0 100644 --- a/lib/capybara/spec/views/with_js.erb +++ b/lib/capybara/spec/views/with_js.erb @@ -38,6 +38,11 @@

+ +

+ Reload! +

waiting to be reloaded
+