From 29898295dc7a65c6587026fac4b3991420f0378d Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Mon, 6 Dec 2010 01:22:59 -0600 Subject: [PATCH 01/17] Fixes issue with multipart forms being submitted as x-www-form-urlencoded when they contain an empty file field --- lib/capybara/driver/rack_test_driver.rb | 34 +++++++++++++++++++------ lib/capybara/spec/test_app.rb | 8 ++++++ lib/capybara/spec/views/form.erb | 11 ++++++++ spec/session/rack_test_session_spec.rb | 10 ++++++++ 4 files changed, 55 insertions(+), 8 deletions(-) diff --git a/lib/capybara/driver/rack_test_driver.rb b/lib/capybara/driver/rack_test_driver.rb index ceb1f06c..af34ea16 100644 --- a/lib/capybara/driver/rack_test_driver.rb +++ b/lib/capybara/driver/rack_test_driver.rb @@ -97,6 +97,21 @@ class Capybara::Driver::RackTest < Capybara::Driver::Base end class Form < Node + # This only needs to inherit from Rack::Test::UploadedFile because Rack::Test checks for + # the class specifically when determing whether to consturct the request as multipart. + # That check should be based solely on the form element's 'enctype' attribute value, + # which should probably be provided to Rack::Test in its non-GET request methods. + class NilUploadedFile < Rack::Test::UploadedFile + def initialize + @empty_file = Tempfile.new("nil_uploaded_file") + @empty_file.close + end + + def original_filename; ""; end + def content_type; "application/octet-stream"; end + def path; @empty_file.path; end + end + def params(button) params = {} @@ -122,14 +137,17 @@ class Capybara::Driver::RackTest < Capybara::Driver::Base end end native.xpath(".//input[not(@disabled) and @type='file']").map do |input| - unless input['value'].to_s.empty? - if multipart? - content_type = MIME::Types.type_for(input['value'].to_s).first.to_s - file = Rack::Test::UploadedFile.new(input['value'].to_s, content_type) - merge_param!(params, input['name'].to_s, file) - else - merge_param!(params, input['name'].to_s, File.basename(input['value'].to_s)) - end + if multipart? + file = \ + if (value = input['value']).to_s.empty? + NilUploadedFile.new + else + content_type = MIME::Types.type_for(value).first.to_s + Rack::Test::UploadedFile.new(value, content_type) + end + merge_param!(params, input['name'].to_s, file) + else + merge_param!(params, input['name'].to_s, File.basename(input['value'].to_s)) end end merge_param!(params, button[:name], button[:value] || "") if button[:name] diff --git a/lib/capybara/spec/test_app.rb b/lib/capybara/spec/test_app.rb index 2a659390..dc851dbb 100644 --- a/lib/capybara/spec/test_app.rb +++ b/lib/capybara/spec/test_app.rb @@ -77,6 +77,14 @@ class TestApp < Sinatra::Base '
' + params[:form].to_yaml + '
' end + post '/upload_empty' do + if params[:form][:file].nil? + 'Successfully ignored empty file field.' + else + 'Something went wrong.' + end + end + post '/upload' do begin buffer = [] diff --git a/lib/capybara/spec/views/form.erb b/lib/capybara/spec/views/form.erb index 5825ff90..a98b80c5 100644 --- a/lib/capybara/spec/views/form.erb +++ b/lib/capybara/spec/views/form.erb @@ -250,6 +250,17 @@

+

+

+ + +

+ +

+ +

+

+

diff --git a/spec/session/rack_test_session_spec.rb b/spec/session/rack_test_session_spec.rb index 7330ff3b..5a54a368 100644 --- a/spec/session/rack_test_session_spec.rb +++ b/spec/session/rack_test_session_spec.rb @@ -26,6 +26,16 @@ describe Capybara::Session do end end + describe "#attach_file" do + context "with multipart form" do + it "should submit an empty form-data section if no file is submitted" do + @session.visit("/form") + @session.click_button("Upload Empty") + @session.body.should include('Successfully ignored empty file field.') + end + end + end + it_should_behave_like "session" it_should_behave_like "session without javascript support" it_should_behave_like "session with headers support" From 2b78681d07355a72801e03e25fe93e2dfcc3288c Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Wed, 22 Dec 2010 16:10:42 +0100 Subject: [PATCH 02/17] :visible => false now works as expected --- History.txt | 1 + lib/capybara/node/finders.rb | 8 +++++++- lib/capybara/spec/session/all_spec.rb | 5 +++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/History.txt b/History.txt index be8f8cea..eb06aebd 100644 --- a/History.txt +++ b/History.txt @@ -5,6 +5,7 @@ Release date: ### Added * New click_on alias for click_link_or_button, shorter yet unambiguous. +* Finders now accept :visible => false which will find all elements regardless of Capybara.ignore_hidden_elements # Version 0.4.0 diff --git a/lib/capybara/node/finders.rb b/lib/capybara/node/finders.rb index de9f7e1f..8ab5427e 100644 --- a/lib/capybara/node/finders.rb +++ b/lib/capybara/node/finders.rb @@ -124,7 +124,13 @@ module Capybara results = results.select { |node| node.text.match(text) } end - if options[:visible] or Capybara.ignore_hidden_elements + ignore_hidden = if options.has_key?(:visible) + options[:visible] + else + Capybara.ignore_hidden_elements + end + + if ignore_hidden results = results.select { |node| node.visible? } end diff --git a/lib/capybara/spec/session/all_spec.rb b/lib/capybara/spec/session/all_spec.rb index 0ac08e4d..dc375e54 100644 --- a/lib/capybara/spec/session/all_spec.rb +++ b/lib/capybara/spec/session/all_spec.rb @@ -56,6 +56,11 @@ shared_examples_for "all" do Capybara.ignore_hidden_elements = true @session.all("//a[@title='awesome title']").should have(1).elements end + + it "should only find invisible nodes" do + Capybara.ignore_hidden_elements = true + @session.all("//a[@title='awesome title']", :visible => false).should have(2).elements + end end context "within a scope" do From 86491cb060e6442badec0db17b02ea09f20b9ecb Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Wed, 22 Dec 2010 16:15:06 +0100 Subject: [PATCH 03/17] Within now accepts same args as other finders. closes #227 --- lib/capybara/session.rb | 8 ++++---- lib/capybara/spec/session/within_spec.rb | 13 ++++++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/capybara/session.rb b/lib/capybara/session.rb index e0db6e97..72c299a7 100644 --- a/lib/capybara/session.rb +++ b/lib/capybara/session.rb @@ -151,11 +151,11 @@ module Capybara # fill_in('Street', :with => '12 Main Street') # end # - # @param [:css, :xpath, String] kind The type of selector or the selector if the second argument is blank - # @param [String] selector The selector within which to execute the given block + # @param (see Capybara::Node::Finders#all) + # @raise [Capybara::ElementNotFound] If the scope can't be found before time expires # - def within(kind, selector=nil) - new_scope = find(kind, selector, :message => "scope '#{selector || kind}' not found on page") + def within(*args) + new_scope = find(*args) begin scopes.push(new_scope) yield diff --git a/lib/capybara/spec/session/within_spec.rb b/lib/capybara/spec/session/within_spec.rb index d05762fb..997034a6 100644 --- a/lib/capybara/spec/session/within_spec.rb +++ b/lib/capybara/spec/session/within_spec.rb @@ -1,4 +1,4 @@ -shared_examples_for "within" do +shared_examples_for "within" do describe '#within' do before do @session.visit('/with_scope') @@ -11,6 +11,13 @@ shared_examples_for "within" do end @session.body.should include('Bar') end + + it "should accept additional options" do + @session.within(:css, "ul li", :text => 'With Simple HTML') do + @session.click_link('Go') + end + @session.body.should include('Bar') + end end context "with XPath selector" do @@ -32,14 +39,14 @@ shared_examples_for "within" do end context "with the default selector set to CSS" do - before { Capybara.default_selector = :css } + before { Capybara.default_selector = :css } it "should use CSS" do @session.within("ul li[contains('With Simple HTML')]") do @session.click_link('Go') end @session.body.should include('Bar') end - after { Capybara.default_selector = :xpath } + after { Capybara.default_selector = :xpath } end context "with click_link" do From e514f2e99cceb3db5be2a6fef8fdfd9a2cb74e2d Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Wed, 22 Dec 2010 16:51:10 +0100 Subject: [PATCH 04/17] Don't delete browser from options, closes #200 --- lib/capybara/driver/selenium_driver.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/capybara/driver/selenium_driver.rb b/lib/capybara/driver/selenium_driver.rb index d5950f35..a638cbb6 100644 --- a/lib/capybara/driver/selenium_driver.rb +++ b/lib/capybara/driver/selenium_driver.rb @@ -83,7 +83,7 @@ class Capybara::Driver::Selenium < Capybara::Driver::Base def browser unless @browser - @browser = Selenium::WebDriver.for(options.delete(:browser) || :firefox, options) + @browser = Selenium::WebDriver.for(options[:browser] || :firefox, options) at_exit do @browser.quit end From a2b1b3315e984890893e18c846afdd0dafdd271e Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Wed, 22 Dec 2010 12:34:51 -0800 Subject: [PATCH 05/17] Fix spec failures --- lib/capybara/node/finders.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/capybara/node/finders.rb b/lib/capybara/node/finders.rb index 8ab5427e..0a6abcb4 100644 --- a/lib/capybara/node/finders.rb +++ b/lib/capybara/node/finders.rb @@ -24,7 +24,10 @@ module Capybara # @raise [Capybara::ElementNotFound] If the element can't be found before time expires # def find(*args) - node = wait_conditionally_until { all(*args).first } + begin + node = wait_conditionally_until { all(*args).first } + rescue TimeoutError + end unless node options = if args.last.is_a?(Hash) then args.last else {} end raise Capybara::ElementNotFound, options[:message] || "Unable to find '#{args[1] || args[0]}'" From af9dd3670419b3e39743e1b15bb271b6aaf4dc15 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Wed, 22 Dec 2010 16:42:38 -0800 Subject: [PATCH 06/17] Don't roundtrip through Selenium twice to determine visibility --- lib/capybara/driver/selenium_driver.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/capybara/driver/selenium_driver.rb b/lib/capybara/driver/selenium_driver.rb index a638cbb6..5566ff33 100644 --- a/lib/capybara/driver/selenium_driver.rb +++ b/lib/capybara/driver/selenium_driver.rb @@ -59,7 +59,8 @@ class Capybara::Driver::Selenium < Capybara::Driver::Base end def visible? - native.displayed? and native.displayed? != "false" + displayed = native.displayed? + displayed and displayed != "false" end def find(locator) From e5d71977b2343a7a92e644110bf9e285cd7263bd Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Thu, 23 Dec 2010 15:36:22 -0800 Subject: [PATCH 07/17] Fix spec failures --- lib/capybara/driver/celerity_driver.rb | 6 ++++-- spec/rspec_spec.rb | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/capybara/driver/celerity_driver.rb b/lib/capybara/driver/celerity_driver.rb index 90de1315..e9914b77 100644 --- a/lib/capybara/driver/celerity_driver.rb +++ b/lib/capybara/driver/celerity_driver.rb @@ -11,7 +11,7 @@ class Capybara::Driver::Celerity < Capybara::Driver::Base def value if tag_name == "select" and native.multiple? - find(".//option[@selected]").map { |n| n.value || n.text } + find(".//option[@selected]").map { |n| if n.has_value? then n.value else n.text end } else native.value end @@ -77,7 +77,9 @@ class Capybara::Driver::Celerity < Capybara::Driver::Base find('./ancestor::select').first end - + def has_value? + native.object.hasAttribute('value') + end end attr_reader :app, :rack_server, :options diff --git a/spec/rspec_spec.rb b/spec/rspec_spec.rb index 89cf8170..9cb61369 100644 --- a/spec/rspec_spec.rb +++ b/spec/rspec_spec.rb @@ -32,7 +32,7 @@ describe 'capybara/rspec' do end it "switches to the javascript driver when giving it as metadata", :js => true do - Capybara.current_driver.should == :selenium + Capybara.current_driver.should == Capybara.javascript_driver end it "switches to the given driver when giving it as metadata", :driver => :culerity do From 9d6aebdea890216fe37236b78844838d7eaeccd1 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Thu, 23 Dec 2010 10:38:44 -0800 Subject: [PATCH 08/17] Ensure that matchers work correctly after actions, closes #171 With browser-based drivers, the has_value?, has_select?, and has_checked_field? matchers (and their negative counterparts) did not always work correctly. It is not possible to determine the dynamic state of inputs purely with XPath, because XPath has access only to the value, selected, and checked _attributes_, and only the corresponding _properties_ change in response to user input. This commit replaces the pure XPath implementation with one that retrieves all matching inputs and then filters the results using driver-specific means that do reflect user input. --- lib/capybara/driver/celerity_driver.rb | 12 ++++ lib/capybara/driver/node.rb | 8 +++ lib/capybara/driver/rack_test_driver.rb | 8 +++ lib/capybara/driver/selenium_driver.rb | 9 ++- lib/capybara/node/element.rb | 20 +++++++ lib/capybara/node/finders.rb | 22 +++++++ lib/capybara/node/matchers.rb | 23 ++++++-- lib/capybara/spec/driver.rb | 14 +++++ lib/capybara/spec/session/has_field_spec.rb | 60 ++++++++++++++++++++ lib/capybara/spec/session/has_select_spec.rb | 40 +++++++++++++ 10 files changed, 209 insertions(+), 7 deletions(-) diff --git a/lib/capybara/driver/celerity_driver.rb b/lib/capybara/driver/celerity_driver.rb index e9914b77..3d57e11f 100644 --- a/lib/capybara/driver/celerity_driver.rb +++ b/lib/capybara/driver/celerity_driver.rb @@ -56,6 +56,18 @@ class Capybara::Driver::Celerity < Capybara::Driver::Base native.visible? end + def checked? + native.checked? + rescue # https://github.com/langalex/culerity/issues/issue/33 + false + end + + def selected? + native.selected? + rescue # https://github.com/langalex/culerity/issues/issue/33 + false + end + def path native.xpath end diff --git a/lib/capybara/driver/node.rb b/lib/capybara/driver/node.rb index d31d0ddd..330b0e6e 100644 --- a/lib/capybara/driver/node.rb +++ b/lib/capybara/driver/node.rb @@ -48,6 +48,14 @@ module Capybara raise NotImplementedError end + def checked? + raise NotImplementedError + end + + def selected? + raise NotImplementedError + end + def path raise NotSupportedByDriverError end diff --git a/lib/capybara/driver/rack_test_driver.rb b/lib/capybara/driver/rack_test_driver.rb index ed37dea4..9f301f27 100644 --- a/lib/capybara/driver/rack_test_driver.rb +++ b/lib/capybara/driver/rack_test_driver.rb @@ -68,6 +68,14 @@ class Capybara::Driver::RackTest < Capybara::Driver::Base string_node.visible? end + def checked? + self[:checked] + end + + def selected? + self[:selected] + end + def path native.path end diff --git a/lib/capybara/driver/selenium_driver.rb b/lib/capybara/driver/selenium_driver.rb index 5566ff33..f462645f 100644 --- a/lib/capybara/driver/selenium_driver.rb +++ b/lib/capybara/driver/selenium_driver.rb @@ -43,7 +43,7 @@ class Capybara::Driver::Selenium < Capybara::Driver::Base if select_node['multiple'] != 'multiple' and select_node['multiple'] != 'true' raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box." end - native.clear + native.toggle if selected? end def click @@ -63,6 +63,13 @@ class Capybara::Driver::Selenium < Capybara::Driver::Base displayed and displayed != "false" end + def selected? + selected = native.selected? + selected and selected != "false" + end + + alias :checked? :selected? + def find(locator) native.find_elements(:xpath, locator).map { |n| self.class.new(driver, n) } end diff --git a/lib/capybara/node/element.rb b/lib/capybara/node/element.rb index 60f3f099..67c10939 100644 --- a/lib/capybara/node/element.rb +++ b/lib/capybara/node/element.rb @@ -112,6 +112,26 @@ module Capybara base.visible? end + ## + # + # Whether or not the element is checked. + # + # @return [Boolean] Whether the element is checked + # + def checked? + base.checked? + end + + ## + # + # Whether or not the element is selected. + # + # @return [Boolean] Whether the element is selected + # + def selected? + base.selected? + end + ## # # An XPath expression describing where on the page the element can be found diff --git a/lib/capybara/node/finders.rb b/lib/capybara/node/finders.rb index 0a6abcb4..2dd177ad 100644 --- a/lib/capybara/node/finders.rb +++ b/lib/capybara/node/finders.rb @@ -127,6 +127,23 @@ module Capybara results = results.select { |node| node.text.match(text) } end + if value = options[:with] + results = results.select { |node| node.value == value } + end + + if options[:checked] + results = results.select { |node| node.checked? } + end + + if options[:unchecked] + results = results.reject { |node| node.checked? } + end + + if selected = options[:selected] + selected = [selected].flatten + results = results.select { |node| has_selected_options?(node, selected) } + end + ignore_hidden = if options.has_key?(:visible) options[:visible] else @@ -146,6 +163,11 @@ module Capybara base.find(xpath) end + def has_selected_options?(node, expected) + actual = node.find('.//option').select { |option| option.selected? }.map { |option| option.text } + (expected - actual).empty? + end + def convert_elements(elements) elements.map { |element| Capybara::Node::Element.new(session, element) } end diff --git a/lib/capybara/node/matchers.rb b/lib/capybara/node/matchers.rb index b3eab6f9..0d546209 100644 --- a/lib/capybara/node/matchers.rb +++ b/lib/capybara/node/matchers.rb @@ -260,7 +260,8 @@ module Capybara # @return [Boolean] Whether it exists # def has_field?(locator, options={}) - has_xpath?(XPath::HTML.field(locator, options)) + options, with = split_options(options, :with) + has_xpath?(XPath::HTML.field(locator, options), with) end ## @@ -273,7 +274,8 @@ module Capybara # @return [Boolean] Whether it doesn't exist # def has_no_field?(locator, options={}) - has_no_xpath?(XPath::HTML.field(locator, options)) + options, with = split_options(options, :with) + has_no_xpath?(XPath::HTML.field(locator, options), with) end ## @@ -286,7 +288,7 @@ module Capybara # @return [Boolean] Whether it exists # def has_checked_field?(locator) - has_xpath?(XPath::HTML.field(locator, :checked => true)) + has_xpath?(XPath::HTML.field(locator), :checked => true) end ## @@ -299,7 +301,7 @@ module Capybara # @return [Boolean] Whether it exists # def has_unchecked_field?(locator) - has_xpath?(XPath::HTML.field(locator, :unchecked => true)) + has_xpath?(XPath::HTML.field(locator), :unchecked => true) end ## @@ -326,7 +328,8 @@ module Capybara # @return [Boolean] Whether it exists # def has_select?(locator, options={}) - has_xpath?(XPath::HTML.select(locator, options)) + options, selected = split_options(options, :selected) + has_xpath?(XPath::HTML.select(locator, options), selected) end ## @@ -338,7 +341,8 @@ module Capybara # @return [Boolean] Whether it doesn't exist # def has_no_select?(locator, options={}) - has_no_xpath?(XPath::HTML.select(locator, options)) + options, selected = split_options(options, :selected) + has_no_xpath?(XPath::HTML.select(locator, options), selected) end ## @@ -373,6 +377,13 @@ module Capybara def has_no_table?(locator, options={}) has_no_xpath?(XPath::HTML.table(locator, options)) end + + protected + + def split_options(options, key) + options = options.dup + [options, if options.has_key?(key) then {key => options.delete(key)} else {} end] + end end end end diff --git a/lib/capybara/spec/driver.rb b/lib/capybara/spec/driver.rb index 516d94f0..9bd3582a 100644 --- a/lib/capybara/spec/driver.rb +++ b/lib/capybara/spec/driver.rb @@ -71,6 +71,20 @@ shared_examples_for 'driver' do @driver.find('//div[@id="hidden"]')[0].should_not be_visible @driver.find('//div[@id="hidden_via_ancestor"]')[0].should_not be_visible end + + it "should extract node checked state" do + @driver.visit('/form') + @driver.find('//input[@id="gender_female"]')[0].should be_checked + @driver.find('//input[@id="gender_male"]')[0].should_not be_checked + @driver.find('//h1')[0].should_not be_checked + end + + it "should extract node selected state" do + @driver.visit('/form') + @driver.find('//option[@value="en"]')[0].should be_selected + @driver.find('//option[@value="sv"]')[0].should_not be_selected + @driver.find('//h1')[0].should_not be_selected + end end end end diff --git a/lib/capybara/spec/session/has_field_spec.rb b/lib/capybara/spec/session/has_field_spec.rb index 261888cb..b2128de9 100644 --- a/lib/capybara/spec/session/has_field_spec.rb +++ b/lib/capybara/spec/session/has_field_spec.rb @@ -25,6 +25,16 @@ shared_examples_for "has_field" do @session.should_not have_field('Wrong Name', :with => 'John') @session.should_not have_field('Description', :with => 'Monkey') end + + it "should be true after the field has been filled in with the given value" do + @session.fill_in('First Name', :with => 'Jonas') + @session.should have_field('First Name', :with => 'Jonas') + end + + it "should be false after the field has been filled in with a different value" do + @session.fill_in('First Name', :with => 'Jonas') + @session.should_not have_field('First Name', :with => 'John') + end end end @@ -54,6 +64,16 @@ shared_examples_for "has_field" do @session.should have_no_field('Wrong Name', :with => 'John') @session.should have_no_field('Description', :with => 'Monkey') end + + it "should be false after the field has been filled in with the given value" do + @session.fill_in('First Name', :with => 'Jonas') + @session.should_not have_no_field('First Name', :with => 'Jonas') + end + + it "should be true after the field has been filled in with a different value" do + @session.fill_in('First Name', :with => 'Jonas') + @session.should have_no_field('First Name', :with => 'John') + end end end @@ -73,6 +93,26 @@ shared_examples_for "has_field" do it "should be false if no field is on the page" do @session.should_not have_checked_field('Does Not Exist') end + + it "should be true after an unchecked checkbox is checked" do + @session.check('form_pets_cat') + @session.should have_checked_field('form_pets_cat') + end + + it "should be false after a checked checkbox is unchecked" do + @session.uncheck('form_pets_dog') + @session.should_not have_checked_field('form_pets_dog') + end + + it "should be true after an unchecked radio button is chosen" do + @session.choose('gender_male') + @session.should have_checked_field('gender_male') + end + + it "should be false after another radio button in the group is chosen" do + @session.choose('gender_male') + @session.should_not have_checked_field('gender_female') + end end describe '#has_unchecked_field?' do @@ -91,6 +131,26 @@ shared_examples_for "has_field" do it "should be false if no field is on the page" do @session.should_not have_unchecked_field('Does Not Exist') end + + it "should be false after an unchecked checkbox is checked" do + @session.check('form_pets_cat') + @session.should_not have_unchecked_field('form_pets_cat') + end + + it "should be true after a checked checkbox is unchecked" do + @session.uncheck('form_pets_dog') + @session.should have_unchecked_field('form_pets_dog') + end + + it "should be false after an unchecked radio button is chosen" do + @session.choose('gender_male') + @session.should_not have_unchecked_field('gender_male') + end + + it "should be true after another radio button in the group is chosen" do + @session.choose('gender_male') + @session.should have_unchecked_field('gender_female') + end end end diff --git a/lib/capybara/spec/session/has_select_spec.rb b/lib/capybara/spec/session/has_select_spec.rb index a891126b..4d53597e 100644 --- a/lib/capybara/spec/session/has_select_spec.rb +++ b/lib/capybara/spec/session/has_select_spec.rb @@ -26,6 +26,26 @@ shared_examples_for "has_select" do @session.should_not have_select('Underwear', :selected => ['Briefs', 'Nonexistant']) @session.should_not have_select('Underwear', :selected => ['Briefs', 'Boxers']) end + + it "should be true after the given value is selected" do + @session.select('Swedish', :from => 'Locale') + @session.should have_select('Locale', :selected => 'Swedish') + end + + it "should be false after a different value is selected" do + @session.select('Swedish', :from => 'Locale') + @session.should_not have_select('Locale', :selected => 'English') + end + + it "should be true after the given values are selected" do + @session.select('Boxers', :from => 'Underwear') + @session.should have_select('Underwear', :selected => ['Briefs', 'Boxers', 'Commando']) + end + + it "should be false after one of the values is unselected" do + @session.unselect('Briefs', :from => 'Underwear') + @session.should_not have_select('Underwear', :selected => ['Briefs', 'Commando']) + end end context 'with options' do @@ -69,6 +89,26 @@ shared_examples_for "has_select" do @session.should have_no_select('Underwear', :selected => ['Briefs', 'Nonexistant']) @session.should have_no_select('Underwear', :selected => ['Briefs', 'Boxers']) end + + it "should be false after the given value is selected" do + @session.select('Swedish', :from => 'Locale') + @session.should_not have_no_select('Locale', :selected => 'Swedish') + end + + it "should be true after a different value is selected" do + @session.select('Swedish', :from => 'Locale') + @session.should have_no_select('Locale', :selected => 'English') + end + + it "should be false after the given values are selected" do + @session.select('Boxers', :from => 'Underwear') + @session.should_not have_no_select('Underwear', :selected => ['Briefs', 'Boxers', 'Commando']) + end + + it "should be true after one of the values is unselected" do + @session.unselect('Briefs', :from => 'Underwear') + @session.should have_no_select('Underwear', :selected => ['Briefs', 'Commando']) + end end context 'with options' do From 8a2ce34691f6e3773a81ef01d73a1112cef99ad1 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Fri, 31 Dec 2010 10:15:45 -0800 Subject: [PATCH 09/17] Avoid failed DNS lookups slowing down specs --- lib/capybara/spec/views/with_html.erb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/capybara/spec/views/with_html.erb b/lib/capybara/spec/views/with_html.erb index 7549fbc0..4fc1cd43 100644 --- a/lib/capybara/spec/views/with_html.erb +++ b/lib/capybara/spec/views/with_html.erb @@ -14,7 +14,7 @@ et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. - awesome image + awesome image

@@ -41,8 +41,8 @@ Anchor on same page - very fine image - fine image + very fine image + fine image Naked Query String <% if params[:query_string] %> From c7de62d88a1569ac63961e8a254f5374803033c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 1 Jan 2011 03:43:30 -0800 Subject: [PATCH 10/17] Some browsers automatically adds the charset to Content-Type. Be less restrictive in tests. --- lib/capybara/spec/driver.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/capybara/spec/driver.rb b/lib/capybara/spec/driver.rb index 9bd3582a..724e9c74 100644 --- a/lib/capybara/spec/driver.rb +++ b/lib/capybara/spec/driver.rb @@ -118,7 +118,7 @@ end shared_examples_for "driver with header support" do it "should make headers available through response_headers" do @driver.visit('/with_simple_html') - @driver.response_headers['Content-Type'].should == 'text/html' + @driver.response_headers['Content-Type'].should =~ /text\/html/ end end From 74aa0ca844a2ad160d67e0338df9c0b08496d82b Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Wed, 22 Dec 2010 19:24:20 -0800 Subject: [PATCH 11/17] Add #first, and use it in preference to all(*args).first. This provides a ~10% speed improvement on selenium specs. --- lib/capybara/node/finders.rb | 106 +++++++++++++++++++++-------------- lib/capybara/node/simple.rb | 4 +- 2 files changed, 65 insertions(+), 45 deletions(-) diff --git a/lib/capybara/node/finders.rb b/lib/capybara/node/finders.rb index 2dd177ad..9f8b1e64 100644 --- a/lib/capybara/node/finders.rb +++ b/lib/capybara/node/finders.rb @@ -25,7 +25,7 @@ module Capybara # def find(*args) begin - node = wait_conditionally_until { all(*args).first } + node = wait_conditionally_until { first(*args) } rescue TimeoutError end unless node @@ -115,46 +115,39 @@ module Capybara # @return [Capybara::Element] The found elements # def all(*args) - options = if args.last.is_a?(Hash) then args.pop else {} end + options = extract_normalized_options(args) - results = Capybara::Selector.normalize(*args).map do |path| - find_in_base(path) - end.flatten + Capybara::Selector.normalize(*args). + map { |path| find_in_base(path) }.flatten. + select { |node| matches_options(node, options) }. + map { |node| convert_element(node) } + end - if text = options[:text] - text = Regexp.escape(text) unless text.kind_of?(Regexp) + ## + # + # Find the first element on the page matching the given selector + # and options, or nil if no element matches. + # + # When only the first matching element is needed, this method can + # be faster than all(*args).first. + # + # @param [:css, :xpath, String] kind_or_locator Either the kind of selector or the selector itself + # @param [String] locator The selector + # @param [Hash{Symbol => Object}] options Additional options; see {all} + # @return Capybara::Element The found element + # + def first(*args) + options = extract_normalized_options(args) - results = results.select { |node| node.text.match(text) } + Capybara::Selector.normalize(*args).each do |path| + find_in_base(path).each do |node| + if matches_options(node, options) + return convert_element(node) + end + end end - if value = options[:with] - results = results.select { |node| node.value == value } - end - - if options[:checked] - results = results.select { |node| node.checked? } - end - - if options[:unchecked] - results = results.reject { |node| node.checked? } - end - - if selected = options[:selected] - selected = [selected].flatten - results = results.select { |node| has_selected_options?(node, selected) } - end - - ignore_hidden = if options.has_key?(:visible) - options[:visible] - else - Capybara.ignore_hidden_elements - end - - if ignore_hidden - results = results.select { |node| node.visible? } - end - - convert_elements(results) + nil end protected @@ -163,19 +156,46 @@ module Capybara base.find(xpath) end - def has_selected_options?(node, expected) - actual = node.find('.//option').select { |option| option.selected? }.map { |option| option.text } - (expected - actual).empty? - end - - def convert_elements(elements) - elements.map { |element| Capybara::Node::Element.new(session, element) } + def convert_element(element) + Capybara::Node::Element.new(session, element) end def wait_conditionally_until if wait? then session.wait_until { yield } else yield end end + def extract_normalized_options(args) + options = if args.last.is_a?(Hash) then args.pop.dup else {} end + + if text = options[:text] + options[:text] = Regexp.escape(text) unless text.kind_of?(Regexp) + end + + if !options.has_key?(:visible) + options[:visible] = Capybara.ignore_hidden_elements + end + + if selected = options[:selected] + options[:selected] = [selected].flatten + end + + options + end + + def matches_options(node, options) + return false if options[:text] and not node.text.match(options[:text]) + return false if options[:visible] and not node.visible? + return false if options[:with] and not node.value == options[:with] + return false if options[:checked] and not node.checked? + return false if options[:unchecked] and node.checked? + return false if options[:selected] and not has_selected_options?(node, options[:selected]) + true + end + + def has_selected_options?(node, expected) + actual = node.find('.//option').select { |option| option.selected? }.map { |option| option.text } + (expected - actual).empty? + end end end end diff --git a/lib/capybara/node/simple.rb b/lib/capybara/node/simple.rb index 387ce82d..f9d7bab7 100644 --- a/lib/capybara/node/simple.rb +++ b/lib/capybara/node/simple.rb @@ -104,8 +104,8 @@ module Capybara native.xpath(xpath).map { |node| self.class.new(node) } end - def convert_elements(elements) - elements + def convert_element(element) + element end def wait? From f06e1cb718e5a4727eaa23946ca7d03bf2ea3795 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Sat, 1 Jan 2011 11:58:39 -0800 Subject: [PATCH 12/17] Specs for #first --- lib/capybara/session.rb | 2 +- lib/capybara/spec/session.rb | 1 + lib/capybara/spec/session/first_spec.rb | 72 +++++++++++++++++++++++++ lib/capybara/spec/views/with_html.erb | 8 +++ 4 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 lib/capybara/spec/session/first_spec.rb diff --git a/lib/capybara/session.rb b/lib/capybara/session.rb index 72c299a7..ea00db68 100644 --- a/lib/capybara/session.rb +++ b/lib/capybara/session.rb @@ -27,7 +27,7 @@ module Capybara # class Session DSL_METHODS = [ - :all, :attach_file, :body, :check, :choose, :click_link_or_button, :click_button, :click_link, :current_url, :drag, :evaluate_script, + :all, :first, :attach_file, :body, :check, :choose, :click_link_or_button, :click_button, :click_link, :current_url, :drag, :evaluate_script, :field_labeled, :fill_in, :find, :find_button, :find_by_id, :find_field, :find_link, :has_content?, :has_css?, :has_no_content?, :has_no_css?, :has_no_xpath?, :has_xpath?, :locate, :save_and_open_page, :select, :source, :uncheck, :visit, :wait_until, :within, :within_fieldset, :within_table, :within_frame, :within_window, :has_link?, :has_no_link?, :has_button?, diff --git a/lib/capybara/spec/session.rb b/lib/capybara/spec/session.rb index 42042277..a137ebef 100644 --- a/lib/capybara/spec/session.rb +++ b/lib/capybara/spec/session.rb @@ -42,6 +42,7 @@ shared_examples_for "session" do end it_should_behave_like "all" + it_should_behave_like "first" it_should_behave_like "attach_file" it_should_behave_like "check" it_should_behave_like "choose" diff --git a/lib/capybara/spec/session/first_spec.rb b/lib/capybara/spec/session/first_spec.rb new file mode 100644 index 00000000..bcb83174 --- /dev/null +++ b/lib/capybara/spec/session/first_spec.rb @@ -0,0 +1,72 @@ +shared_examples_for "first" do + describe '#first' do + before do + @session.visit('/with_html') + end + + it "should find the first element using the given locator" do + @session.first('//h1').text.should == 'This is a test' + @session.first("//input[@id='test_field']")[:value].should == 'monkey' + end + + it "should return nil when nothing was found" do + @session.first('//div[@id="nosuchthing"]').should be_nil + end + + it "should accept an XPath instance" do + @session.visit('/form') + @xpath = XPath::HTML.fillable_field('Name') + @session.first(@xpath).value.should == 'John Smith' + end + + context "with css selectors" do + it "should find the first element using the given selector" do + @session.first(:css, 'h1').text.should == 'This is a test' + @session.first(:css, "input[id='test_field']")[:value].should == 'monkey' + end + end + + context "with xpath selectors" do + it "should find the first element using the given locator" do + @session.first(:xpath, '//h1').text.should == 'This is a test' + @session.first(:xpath, "//input[@id='test_field']")[:value].should == 'monkey' + end + end + + context "with css as default selector" do + before { Capybara.default_selector = :css } + it "should find the first element using the given locator" do + @session.first('h1').text.should == 'This is a test' + @session.first("input[id='test_field']")[:value].should == 'monkey' + end + after { Capybara.default_selector = :xpath } + end + + context "with visible filter" do + after { Capybara.ignore_hidden_elements = false } + it "should only find visible nodes" do + @session.first(:css, "a.visibility").should_not be_visible + @session.first(:css, "a.visibility", :visible => true).should be_visible + Capybara.ignore_hidden_elements = true + @session.first(:css, "a.visibility").should be_visible + end + + it "should only find invisible nodes" do + Capybara.ignore_hidden_elements = true + @session.first(:css, "a.visibility", :visible => false).should_not be_visible + end + end + + context "within a scope" do + before do + @session.visit('/with_scope') + end + + it "should find the first element using the given locator" do + @session.within(:xpath, "//div[@id='for_bar']") do + @session.first('.//form').should_not be_nil + end + end + end + end +end diff --git a/lib/capybara/spec/views/with_html.erb b/lib/capybara/spec/views/with_html.erb index 4fc1cd43..1e55c087 100644 --- a/lib/capybara/spec/views/with_html.erb +++ b/lib/capybara/spec/views/with_html.erb @@ -55,6 +55,14 @@ hidden link +

+ hidden link +
+ +
+ visible link +
+