From e898cc2b8e6ce16f896793159057ef6bf714b166 Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Fri, 15 Feb 2013 20:32:05 +0100 Subject: [PATCH] Add `match` option --- lib/capybara.rb | 4 +- lib/capybara/node/finders.rb | 36 ++++++-- lib/capybara/query.rb | 30 +++++-- lib/capybara/result.rb | 13 --- lib/capybara/spec/session/find_by_id_spec.rb | 1 - lib/capybara/spec/session/find_spec.rb | 88 ++++++++++++++++++++ lib/capybara/spec/spec_helper.rb | 1 + lib/capybara/spec/views/with_html.erb | 8 ++ 8 files changed, 151 insertions(+), 30 deletions(-) diff --git a/lib/capybara.rb b/lib/capybara.rb index fd547436..6c0cf4de 100644 --- a/lib/capybara.rb +++ b/lib/capybara.rb @@ -16,7 +16,7 @@ module Capybara class << self attr_accessor :asset_root, :app_host, :run_server, :default_host, :always_include_port - attr_accessor :server_host, :server_port, :exact + attr_accessor :server_host, :server_port, :exact, :match attr_accessor :default_selector, :default_wait_time, :ignore_hidden_elements attr_accessor :save_and_open_page_path, :automatic_reload attr_writer :default_driver, :current_driver, :javascript_driver, :session_name @@ -344,6 +344,8 @@ Capybara.configure do |config| config.ignore_hidden_elements = true config.default_host = "http://www.example.com" config.automatic_reload = true + config.match = :smart + config.exact = false end Capybara.register_driver :rack_test do |app| diff --git a/lib/capybara/node/finders.rb b/lib/capybara/node/finders.rb index 206744ca..4566d5e8 100644 --- a/lib/capybara/node/finders.rb +++ b/lib/capybara/node/finders.rb @@ -23,7 +23,22 @@ module Capybara # @raise [Capybara::ElementNotFound] If the element can't be found before time expires # def find(*args) - synchronize { all(*args).find! }.tap(&:allow_reload!) + synchronize do + query = Capybara::Query.new(*args) + if query.match == :smart + result = resolve_query(query, true) + result = resolve_query(query, false) if result.size == 0 and not query.exact + else + result = resolve_query(query) + end + if query.match == :one or query.match == :smart and result.size > 1 + raise Capybara::Ambiguous.new("Ambiguous match, found #{result.size} elements matching #{query.description}") + end + if result.size == 0 + raise Capybara::ElementNotFound.new("Unable to find #{query.description}") + end + result.first + end.tap(&:allow_reload!) end ## @@ -108,13 +123,7 @@ module Capybara # @return [Array[Capybara::Element]] The found elements # def all(*args) - query = Capybara::Query.new(*args) - elements = synchronize do - base.find(query.xpath).map do |node| - Capybara::Node::Element.new(session, node, self, query) - end - end - Capybara::Result.new(elements, query) + resolve_query(Capybara::Query.new(*args)) end ## @@ -131,6 +140,17 @@ module Capybara def first(*args) all(*args).first end + + private + + def resolve_query(query, exact=nil) + elements = synchronize do + base.find(query.xpath(exact)).map do |node| + Capybara::Node::Element.new(session, node, self, query) + end + end + Capybara::Result.new(elements, query) + end end end end diff --git a/lib/capybara/query.rb b/lib/capybara/query.rb index b528cca7..0d219d02 100644 --- a/lib/capybara/query.rb +++ b/lib/capybara/query.rb @@ -2,7 +2,7 @@ module Capybara class Query attr_accessor :selector, :locator, :options, :xpath, :find, :negative - VALID_KEYS = [:text, :visible, :between, :count, :maximum, :minimum, :exact] + VALID_KEYS = [:text, :visible, :between, :count, :maximum, :minimum, :exact, :match] def initialize(*args) @options = if args.last.is_a?(Hash) then args.pop.dup else {} end @@ -15,6 +15,10 @@ module Capybara @options[:exact] = Capybara.exact end + unless options.has_key?(:match) + @options[:match] = Capybara.match + end + if args[0].is_a?(Symbol) @selector = Selector.all[args[0]] @locator = args[1] @@ -25,12 +29,6 @@ module Capybara @selector ||= Selector.all[Capybara.default_selector] @xpath = @selector.call(@locator) - if @xpath.respond_to?(:to_xpath) and @options[:exact] - @xpath = @xpath.to_xpath(:exact) - else - @xpath = @xpath.to_s - end - assert_valid_keys! end @@ -72,6 +70,24 @@ module Capybara end end + def exact + @options[:exact] + end + + def match + @options[:match] + end + + def xpath(exact=nil) + exact = @options[:exact] if exact == nil + + if @xpath.respond_to?(:to_xpath) and exact + @xpath.to_xpath(:exact) + else + @xpath.to_s + end + end + private def assert_valid_keys! diff --git a/lib/capybara/result.rb b/lib/capybara/result.rb index 711f66b8..de985dd4 100644 --- a/lib/capybara/result.rb +++ b/lib/capybara/result.rb @@ -18,19 +18,6 @@ module Capybara @query.matches_count?(@result.size) end - def find! - raise find_error if @result.size != 1 - @result.first - end - - def find_error - if @result.size == 0 - Capybara::ElementNotFound.new("Unable to find #{@query.description}") - elsif @result.size > 1 - Capybara::Ambiguous.new("Ambiguous match, found #{size} elements matching #{@query.description}") - end - end - def failure_message message = if @query.options[:count] "expected #{@query.description} to be found #{@query.options[:count]} #{declension("time", "times", @query.options[:count])}" diff --git a/lib/capybara/spec/session/find_by_id_spec.rb b/lib/capybara/spec/session/find_by_id_spec.rb index 21407bb0..7204defb 100644 --- a/lib/capybara/spec/session/find_by_id_spec.rb +++ b/lib/capybara/spec/session/find_by_id_spec.rb @@ -5,7 +5,6 @@ Capybara::SpecHelper.spec '#find_by_id' do it "should find any element by id" do @session.find_by_id('red').tag_name.should == 'a' - @session.find_by_id('hidden_via_ancestor').tag_name.should == 'div' end it "casts to string" do diff --git a/lib/capybara/spec/session/find_spec.rb b/lib/capybara/spec/session/find_spec.rb index 1f463681..99c63d20 100644 --- a/lib/capybara/spec/session/find_spec.rb +++ b/lib/capybara/spec/session/find_spec.rb @@ -136,6 +136,94 @@ Capybara::SpecHelper.spec '#find' do end end + context "with :match option" do + context "when set to `one`" do + it "raises an error when multiple matches exist" do + expect do + @session.find(:css, ".multiple", :match => :one) + end.to raise_error(Capybara::Ambiguous) + end + it "raises an error even if there the match is exact and the others are inexact" do + expect do + @session.find(:xpath, XPath.descendant[XPath.attr(:class).is("almost_singular")], :exact => false, :match => :one) + end.to raise_error(Capybara::Ambiguous) + end + it "returns the element if there is only one" do + @session.find(:css, ".singular", :match => :one).text.should == "singular" + end + it "raises an error if there is no match" do + expect do + @session.find(:css, ".does-not-exist", :match => :any) + end.to raise_error(Capybara::ElementNotFound) + end + end + + context "when set to `any`" do + it "returns the first matched element" do + @session.find(:css, ".multiple", :match => :any).text.should == "multiple one" + end + it "raises an error if there is no match" do + expect do + @session.find(:css, ".does-not-exist", :match => :any) + end.to raise_error(Capybara::ElementNotFound) + end + end + + context "when set to `smart`" do + context "and `exact` set to `false`" do + it "raises an error when there are multiple exact matches" do + expect do + @session.find(:xpath, XPath.descendant[XPath.attr(:class).is("multiple")], :match => :smart, :exact => false) + end.to raise_error(Capybara::Ambiguous) + end + it "finds a single exact match when there also are inexact matches" do + result = @session.find(:xpath, XPath.descendant[XPath.attr(:class).is("almost_singular")], :match => :smart, :exact => false) + result.text.should == "almost singular" + end + it "raises an error when there are multiple inexact matches" do + expect do + @session.find(:xpath, XPath.descendant[XPath.attr(:class).is("almost_singul")], :match => :smart, :exact => false) + end.to raise_error(Capybara::Ambiguous) + end + it "finds a single inexact match" do + result = @session.find(:xpath, XPath.descendant[XPath.attr(:class).is("almost_singular but")], :match => :smart, :exact => false) + result.text.should == "almost singular but not quite" + end + end + + context "with `exact` set to `true`" do + it "raises an error when there are multiple exact matches" do + expect do + @session.find(:xpath, XPath.descendant[XPath.attr(:class).is("multiple")], :match => :smart, :exact => true) + end.to raise_error(Capybara::Ambiguous) + end + it "finds a single exact match when there also are inexact matches" do + result = @session.find(:xpath, XPath.descendant[XPath.attr(:class).is("almost_singular")], :match => :smart, :exact => true) + result.text.should == "almost singular" + end + it "raises an error when there are multiple inexact matches" do + expect do + @session.find(:xpath, XPath.descendant[XPath.attr(:class).is("almost_singul")], :match => :smart, :exact => true) + end.to raise_error(Capybara::ElementNotFound) + end + it "raises an error when there is a single inexact matches" do + expect do + result = @session.find(:xpath, XPath.descendant[XPath.attr(:class).is("almost_singular but")], :match => :smart, :exact => true) + end.to raise_error(Capybara::ElementNotFound) + end + end + end + + it "defaults to `Capybara.match`" do + Capybara.match = :one + expect do + @session.find(:css, ".multiple") + end.to raise_error(Capybara::Ambiguous) + Capybara.match = :any + @session.find(:css, ".multiple").text.should == "multiple one" + end + end + context "within a scope" do before do @session.visit('/with_scope') diff --git a/lib/capybara/spec/spec_helper.rb b/lib/capybara/spec/spec_helper.rb index 30e6a9c7..0b170017 100644 --- a/lib/capybara/spec/spec_helper.rb +++ b/lib/capybara/spec/spec_helper.rb @@ -20,6 +20,7 @@ module Capybara Capybara.default_selector = :xpath Capybara.default_wait_time = 1 Capybara.exact = false + Capybara.match = :smart end def filter(requires, metadata) diff --git a/lib/capybara/spec/views/with_html.erb b/lib/capybara/spec/views/with_html.erb index 7116dba6..babb19a9 100644 --- a/lib/capybara/spec/views/with_html.erb +++ b/lib/capybara/spec/views/with_html.erb @@ -81,3 +81,11 @@ banana
  • Monkey John
  • Monkey Paul
  • + +
    +
    singular
    +
    multiple one
    +
    multiple two
    +
    almost singular but not quite
    +
    almost singular
    +