diff --git a/lib/capybara.rb b/lib/capybara.rb index 645033c6..64495e50 100644 --- a/lib/capybara.rb +++ b/lib/capybara.rb @@ -287,14 +287,15 @@ module Capybara self.default_driver = nil self.current_driver = nil - autoload :DSL, 'capybara/dsl' - autoload :Server, 'capybara/server' - autoload :Session, 'capybara/session' - autoload :Selector, 'capybara/selector' - autoload :Query, 'capybara/query' - autoload :Result, 'capybara/result' - autoload :Helpers, 'capybara/helpers' - autoload :VERSION, 'capybara/version' + autoload :DSL, 'capybara/dsl' + autoload :Server, 'capybara/server' + autoload :Session, 'capybara/session' + autoload :Selector, 'capybara/selector' + autoload :Query, 'capybara/query' + autoload :Result, 'capybara/result' + autoload :Helpers, 'capybara/helpers' + autoload :CountHelpers, 'capybara/helpers' + autoload :VERSION, 'capybara/version' module Node autoload :Base, 'capybara/node/base' diff --git a/lib/capybara/helpers.rb b/lib/capybara/helpers.rb index bf9c8728..e7ab2657 100644 --- a/lib/capybara/helpers.rb +++ b/lib/capybara/helpers.rb @@ -25,7 +25,7 @@ module Capybara # @return [String] Escaped text # def to_regexp(text) - text.is_a?(Regexp) ? text : Regexp.escape(normalize_whitespace(text)) + text.is_a?(Regexp) ? text : Regexp.new(Regexp.escape(normalize_whitespace(text))) end ## @@ -47,4 +47,46 @@ module Capybara end end end + + module CountHelpers + class << self + def matches_count?(count, options={}) + case + when options[:between] + options[:between] === count + when options[:count] + options[:count].to_i == count + when options[:maximum] + options[:maximum].to_i >= count + when options[:minimum] + options[:minimum].to_i <= count + else + count > 0 + end + end + + def failure_message(description, options={}) + message_prototype = "expected to find #{description} COUNT" + message = if options[:count] + message_prototype.sub(/COUNT/, "#{options[:count]} #{declension('time', 'times', options[:count])}") + elsif options[:between] + message_prototype.sub(/COUNT/, "between #{options[:between].first} and #{options[:between].last} times") + elsif options[:maximum] + message_prototype.sub(/COUNT/, "at most #{options[:maximum]} #{declension('time', 'times', options[:maximum])}") + elsif options[:minimum] + message_prototype.sub(/COUNT/, "at least #{options[:minimum]} #{declension('time', 'times', options[:minimum])}") + else + "expected to find #{description}" + end + end + + def declension(singular, plural, count) + if count == 1 + singular + else + plural + end + end + end + end end diff --git a/lib/capybara/node/matchers.rb b/lib/capybara/node/matchers.rb index 7a401330..96f5e554 100644 --- a/lib/capybara/node/matchers.rb +++ b/lib/capybara/node/matchers.rb @@ -28,8 +28,12 @@ module Capybara # page.has_selector?(:xpath, XPath.descendant(:p)) # # @param (see Capybara::Node::Finders#all) - # @option options [Integer] :count (nil) Number of times the expression should occur - # @return [Boolean] If the expression exists + # @param options a customizable set of options + # @option options [Integer] :count (nil) Number of times the text should occur + # @option options [Integer] :minimum (nil) Minimum number of times the text should occur + # @option options [Integer] :maximum (nil) Maximum number of times the text should occur + # @option options [Range] :between (nil) Range of times that should contain number of times text occurs + # @return [Boolean] If the expression exists # def has_selector?(*args) assert_selector(*args) @@ -193,18 +197,25 @@ module Capybara # Checks if the page or current node has the given text content, # ignoring any HTML tags and normalizing whitespace. # - # This only matches displayable text and specifically excludes text - # contained within non-display nodes such as script or head tags. + # By default it will check if the text occurs at least once, + # but a different number can be specified. # - # @param [:all, :visible] type Whether to only check for visible or all text - # @param [String] content The text to check for - # @return [Boolean] Whether it exists + # page.has_text?('lorem ipsum', between: 2..4) # - def has_text?(type=nil, content) + # This will check if the text occurs from 2 to 4 times. + # + # @param type [:all, :visible] Whether to check for only visible or all text + # @param content [String, Regexp] The text/regexp to check for + # @param options a customizable set of options + # @option options [Integer] :count (nil) Number of times the text should occur + # @option options [Integer] :minimum (nil) Minimum number of times the text should occur + # @option options [Integer] :maximum (nil) Maximum number of times the text should occur + # @option options [Range] :between (nil) Range of times that should contain number of times text occurs + # @return [Boolean] Whether it exists + # + def has_text?(*args) synchronize do - unless Capybara::Helpers.normalize_whitespace(text(type)).match(Capybara::Helpers.to_regexp(content)) - raise ExpectationNotMet - end + raise ExpectationNotMet unless text_found?(*args) end return true rescue Capybara::ExpectationNotMet @@ -217,17 +228,12 @@ module Capybara # Checks if the page or current node does not have the given text # content, ignoring any HTML tags and normalizing whitespace. # - # This only matches displayable text and specifically excludes text - # contained within non-display nodes such as script or head tags. + # @param (see #has_text?) + # @return [Boolean] Whether it doesn't exist # - # @param [String] content The text to check for - # @return [Boolean] Whether it doesn't exist - # - def has_no_text?(type=nil, content) + def has_no_text?(*args) synchronize do - if Capybara::Helpers.normalize_whitespace(text(type)).match(Capybara::Helpers.to_regexp(content)) - raise ExpectationNotMet - end + raise ExpectationNotMet if text_found?(*args) end return true rescue Capybara::ExpectationNotMet @@ -449,8 +455,16 @@ module Capybara self.eql?(other) or (other.respond_to?(:base) and base == other.base) end - private + private + def text_found?(*args) + options = (args.last.is_a?(Hash))? args.pop : {} + content = (args.last.is_a?(Regexp))? args.pop : args.pop.to_s + type = args.first + count = Capybara::Helpers.normalize_whitespace(text(type)).scan(Capybara::Helpers.to_regexp(content)).count + + Capybara::CountHelpers.matches_count?(count, options) + end end end end diff --git a/lib/capybara/query.rb b/lib/capybara/query.rb index 87bf21d0..fa87c7eb 100644 --- a/lib/capybara/query.rb +++ b/lib/capybara/query.rb @@ -52,21 +52,6 @@ module Capybara end end - def matches_count?(count) - case - when options[:between] - options[:between] === count - when options[:count] - options[:count].to_i == count - when options[:maximum] - options[:maximum].to_i >= count - when options[:minimum] - options[:minimum].to_i <= count - else - count > 0 - end - end - def visible if options.has_key?(:visible) case @options[:visible] diff --git a/lib/capybara/result.rb b/lib/capybara/result.rb index f799042d..efd9c330 100644 --- a/lib/capybara/result.rb +++ b/lib/capybara/result.rb @@ -32,23 +32,13 @@ module Capybara def_delegators :@result, :each, :[], :at, :size, :count, :length, :first, :last, :empty? def matches_count? - @query.matches_count?(@result.size) + Capybara::CountHelpers.matches_count?(@result.size, @query.options) end def failure_message - message = if @query.options[:count] - "expected #{@query.description} to be found #{@query.options[:count]} #{declension("time", "times", @query.options[:count])}" - elsif @query.options[:between] - "expected #{@query.description} to be found between #{@query.options[:between].first} and #{@query.options[:between].last} times" - elsif @query.options[:maximum] - "expected #{@query.description} to be found at most #{@query.options[:maximum]} #{declension("time", "times", @query.options[:maximum])}" - elsif @query.options[:minimum] - "expected #{@query.description} to be found at least #{@query.options[:minimum]} #{declension("time", "times", @query.options[:minimum])}" - else - "expected to find #{@query.description}" - end + message = Capybara::CountHelpers.failure_message(@query.description, @query.options) if count > 0 - message << ", found #{count} #{declension("match", "matches")}: " << @result.map(&:text).map(&:inspect).join(", ") + message << ", found #{count} #{Capybara::CountHelpers.declension("match", "matches", count)}: " << @result.map(&:text).map(&:inspect).join(", ") else message << " but there were no matches" end @@ -60,17 +50,7 @@ module Capybara end def negative_failure_message - "expected not to find #{@query.description}, but there #{declension("was", "were")} #{count} #{declension("match", "matches")}" - end - - private - - def declension(singular, plural, count=count) - if count == 1 - singular - else - plural - end + failure_message.sub(/(to be found|to find)/, 'not \1') end end end diff --git a/lib/capybara/rspec/matchers.rb b/lib/capybara/rspec/matchers.rb index 1f17b1f5..826cf023 100644 --- a/lib/capybara/rspec/matchers.rb +++ b/lib/capybara/rspec/matchers.rb @@ -33,38 +33,41 @@ module Capybara end class HaveText < Matcher - attr_reader :text, :type + attr_reader :type, :content, :options - def initialize(type, text) - @type = type - @text = text + def initialize(*args) + @options = (args.last.is_a?(Hash))? args.pop : {} + @content = args.pop + @type = args.first end def matches?(actual) @actual = wrap(actual) - @actual.has_text?(type, text) + @actual.has_text?(type, content, options) end def does_not_match?(actual) @actual = wrap(actual) - @actual.has_no_text?(type, text) + @actual.has_no_text?(type, content, options) end def failure_message_for_should - "expected there to be text #{format(text)} in #{format(@actual.text(type))}" + message = Capybara::CountHelpers.failure_message(description, options) + message << " in #{format(@actual.text(type))}" + message end def failure_message_for_should_not - "expected there not to be text #{format(text)} in #{format(@actual.text(type))}" + failure_message_for_should.sub(/(to find)/, 'not \1') end def description - "have text #{format(text)}" + "text #{format(content)}" end - def format(text) - text = Capybara::Helpers.normalize_whitespace(text) unless text.is_a? Regexp - text.inspect + def format(content) + content = Capybara::Helpers.normalize_whitespace(content) unless content.is_a? Regexp + content.inspect end end @@ -106,17 +109,14 @@ module Capybara HaveSelector.new(:xpath, xpath, options) end - def have_css(css, options={}) + def have_css(css, options={}) HaveSelector.new(:css, css, options) end - def have_content(type=nil, text) - HaveText.new(type, text) - end - - def have_text(type=nil, text) - HaveText.new(type, text) + def have_text(*args) + HaveText.new(*args) end + alias_method :have_content, :have_text def have_title(title) HaveTitle.new(title) diff --git a/lib/capybara/spec/session/has_text_spec.rb b/lib/capybara/spec/session/has_text_spec.rb index 371af37f..7bd46d31 100644 --- a/lib/capybara/spec/session/has_text_spec.rb +++ b/lib/capybara/spec/session/has_text_spec.rb @@ -116,6 +116,80 @@ Capybara::SpecHelper.spec '#has_text?' do @session.click_link('Click me') @session.should have_text("Has been clicked") end + + context "with between" do + it "should be true if the text occurs within the range given" do + @session.visit('/with_count') + @session.should have_text('count', between: 1..3) + @session.should have_text(/count/, between: 2..2) + end + + it "should be false if the text occurs more or fewer times than range" do + @session.visit('/with_count') + @session.should_not have_text('count', between: 0..1) + @session.should_not have_text('count', between: 3..10) + @session.should_not have_text(/count/, between: 2...2) + end + end + + context "with count" do + it "should be true if the text occurs the given number of times" do + @session.visit('/with_count') + @session.should have_text('count', count: 2) + end + + it "should be false if the text occurs a different number of times than the given" do + @session.visit('/with_count') + @session.should_not have_text('count', count: 0) + @session.should_not have_text('count', count: 1) + @session.should_not have_text(/count/, count: 3) + end + + it "should coerce count to an integer" do + @session.visit('/with_count') + @session.should have_text('count', count: '2') + @session.should_not have_text('count', count: '3') + end + end + + context "with maximum" do + it "should be true when text occurs same or fewer times than given" do + @session.visit('/with_count') + @session.should have_text('count', maximum: 2) + @session.should have_text(/count/, maximum: 3) + end + + it "should be false when text occurs more times than given" do + @session.visit('/with_count') + @session.should_not have_text('count', maximum: 1) + @session.should_not have_text('count', maximum: 0) + end + + it "should coerce maximum to an integer" do + @session.visit('/with_count') + @session.should have_text('count', maximum: '2') + @session.should_not have_text('count', maximum: '1') + end + end + + context "with minimum" do + it "should be true when text occurs same or more times than given" do + @session.visit('/with_count') + @session.should have_text('count', minimum: 2) + @session.should have_text(/count/, minimum: 0) + end + + it "should be false when text occurs fewer times than given" do + @session.visit('/with_count') + @session.should_not have_text('count', minimum: 3) + end + + it "should coerce minimum to an integer" do + @session.visit('/with_count') + @session.should have_text('count', minimum: '2') + @session.should_not have_text('count', minimum: '3') + end + end end Capybara::SpecHelper.spec '#has_no_text?' do diff --git a/lib/capybara/spec/views/with_count.erb b/lib/capybara/spec/views/with_count.erb new file mode 100644 index 00000000..d4ddcad9 --- /dev/null +++ b/lib/capybara/spec/views/with_count.erb @@ -0,0 +1,7 @@ +

This page is used for testing number options of has_text?

+ +

count1

+
+

2 count

+

Count

+
\ No newline at end of file diff --git a/spec/rspec/matchers_spec.rb b/spec/rspec/matchers_spec.rb index 7719293a..10271dd3 100644 --- a/spec/rspec/matchers_spec.rb +++ b/spec/rspec/matchers_spec.rb @@ -30,7 +30,7 @@ describe Capybara::RSpecMatchers do it "fails if matched node count does not equal expected count" do expect do "

Text

".should have_css('h1', :count => 2) - end.to raise_error(/expected css "h1" to be found 2 times/) + end.to raise_error("expected to find css \"h1\" 2 times, found 1 match: \"Text\"") end end @@ -222,7 +222,7 @@ describe Capybara::RSpecMatchers do describe "have_content matcher" do it "gives proper description" do - have_content('Text').description.should == "have text \"Text\"" + have_content('Text').description.should == "text \"Text\"" end context "on a string" do @@ -238,7 +238,7 @@ describe Capybara::RSpecMatchers do it "fails if has_content? returns false" do expect do "

Text

".should have_content('No such Text') - end.to raise_error(/expected there to be text "No such Text" in "Text"/) + end.to raise_error(/expected to find text "No such Text" in "Text"/) end end @@ -254,7 +254,7 @@ describe Capybara::RSpecMatchers do it "fails if has_no_content? returns false" do expect do "

Text

".should_not have_content('Text') - end.to raise_error(/expected there not to be text "Text" in "Text"/) + end.to raise_error(/expected not to find text "Text" in "Text"/) end end end @@ -276,7 +276,7 @@ describe Capybara::RSpecMatchers do it "fails if has_content? returns false" do expect do page.should have_content('No such Text') - end.to raise_error(/expected there to be text "No such Text" in "(.*)This is a test(.*)"/) + end.to raise_error(/expected to find text "No such Text" in "(.*)This is a test(.*)"/) end context "with default selector CSS" do @@ -284,7 +284,7 @@ describe Capybara::RSpecMatchers do it "fails if has_content? returns false" do expect do page.should have_content('No such Text') - end.to raise_error(/expected there to be text "No such Text" in "(.*)This is a test(.*)"/) + end.to raise_error(/expected to find text "No such Text" in "(.*)This is a test(.*)"/) end after { Capybara.default_selector = :xpath } end @@ -298,7 +298,7 @@ describe Capybara::RSpecMatchers do it "fails if has_no_content? returns false" do expect do page.should_not have_content('This is a test') - end.to raise_error(/expected there not to be text "This is a test"/) + end.to raise_error(/expected not to find text "This is a test"/) end end end @@ -306,7 +306,7 @@ describe Capybara::RSpecMatchers do describe "have_text matcher" do it "gives proper description" do - have_text('Text').description.should == "have text \"Text\"" + have_text('Text').description.should == "text \"Text\"" end context "on a string" do @@ -322,13 +322,13 @@ describe Capybara::RSpecMatchers do it "fails if has_text? returns false" do expect do "

Text

".should have_text('No such Text') - end.to raise_error(/expected there to be text "No such Text" in "Text"/) + end.to raise_error(/expected to find text "No such Text" in "Text"/) end it "casts has_text? argument to string" do expect do "

Text

".should have_text(:cast_me) - end.to raise_error(/expected there to be text "cast_me" in "Text"/) + end.to raise_error(/expected to find text "cast_me" in "Text"/) end end @@ -344,7 +344,7 @@ describe Capybara::RSpecMatchers do it "fails if has_no_text? returns false" do expect do "

Text

".should_not have_text('Text') - end.to raise_error(/expected there not to be text "Text" in "Text"/) + end.to raise_error(/expected not to find text "Text" in "Text"/) end end end @@ -375,7 +375,7 @@ describe Capybara::RSpecMatchers do it "fails if has_text? returns false" do expect do page.should have_text('No such Text') - end.to raise_error(/expected there to be text "No such Text" in "(.*)This is a test(.*)"/) + end.to raise_error(/expected to find text "No such Text" in "(.*)This is a test(.*)"/) end context "with default selector CSS" do @@ -383,7 +383,7 @@ describe Capybara::RSpecMatchers do it "fails if has_text? returns false" do expect do page.should have_text('No such Text') - end.to raise_error(/expected there to be text "No such Text" in "(.*)This is a test(.*)"/) + end.to raise_error(/expected to find text "No such Text" in "(.*)This is a test(.*)"/) end after { Capybara.default_selector = :xpath } end @@ -397,7 +397,7 @@ describe Capybara::RSpecMatchers do it "fails if has_no_text? returns false" do expect do page.should_not have_text('This is a test') - end.to raise_error(/expected there not to be text "This is a test"/) + end.to raise_error(/expected not to find text "This is a test"/) end end end