Merge pull request #1651 from jnicklas/match_selector

Add match selector to match a given element against selectors
This commit is contained in:
Thomas Walpole 2016-03-25 09:42:11 -07:00
commit ac6cd3b272
15 changed files with 435 additions and 141 deletions

View File

@ -419,10 +419,12 @@ module Capybara
require 'capybara/version'
require 'capybara/queries/base_query'
require 'capybara/query'
require 'capybara/queries/selector_query'
require 'capybara/queries/text_query'
require 'capybara/queries/title_query'
require 'capybara/queries/current_path_query'
require 'capybara/queries/match_query'
require 'capybara/query'
require 'capybara/node/finders'
require 'capybara/node/matchers'

View File

@ -29,7 +29,7 @@ module Capybara
# @raise [Capybara::ElementNotFound] If the element can't be found before time expires
#
def find(*args)
query = Capybara::Query.new(*args)
query = Capybara::Queries::SelectorQuery.new(*args)
synchronize(query.wait) do
if query.match == :smart or query.match == :prefer_exact
result = query.resolve_for(self, true)
@ -178,7 +178,7 @@ module Capybara
# @return [Capybara::Result] A collection of found elements
#
def all(*args)
query = Capybara::Query.new(*args)
query = Capybara::Queries::SelectorQuery.new(*args)
synchronize(query.wait) do
result = query.resolve_for(self)
raise Capybara::ExpectationNotMet, result.failure_message unless result.matches_count?

View File

@ -56,6 +56,36 @@ module Capybara
return false
end
##
#
# Checks if the current node matches given selector
# Usage is identical to Capybara::Node::Matchers#has_selector?
#
# @param (see Capybara::Node::Finders#has_selector?)
# @return [Boolean]
#
def matches_selector?(*args)
assert_matches_selector(*args)
rescue Capybara::ExpectationNotMet
return false
end
##
#
# Checks if the current node does not match given selector
# Usage is identical to Capybara::Node::Matchers#has_selector?
#
# @param (see Capybara::Node::Finders#has_selector?)
# @return [Boolean]
#
def not_matches_selector?(*args)
assert_not_matches_selector(*args)
rescue Capybara::ExpectationNotMet
return false
end
##
#
# Asserts that a given selector is on the page or current node.
@ -90,7 +120,7 @@ module Capybara
# @raise [Capybara::ExpectationNotMet] If the selector does not exist
#
def assert_selector(*args)
query = Capybara::Query.new(*args)
query = Capybara::Queries::SelectorQuery.new(*args)
synchronize(query.wait) do
result = query.resolve_for(self)
matches_count = Capybara::Helpers.matches_count?(result.size, query.options)
@ -118,7 +148,7 @@ module Capybara
# @raise [Capybara::ExpectationNotMet] If the selector exists
#
def assert_no_selector(*args)
query = Capybara::Query.new(*args)
query = Capybara::Queries::SelectorQuery.new(*args)
synchronize(query.wait) do
result = query.resolve_for(self)
matches_count = Capybara::Helpers.matches_count?(result.size, query.options)
@ -130,6 +160,45 @@ module Capybara
end
alias_method :refute_selector, :assert_no_selector
##
#
# Asserts that the current_node matches a given selector
#
# node.assert_matches_selector('p#foo')
# node.assert_matches_selector(:xpath, '//p[@id="foo"]')
# node.assert_matches_selector(:foo)
#
# It also accepts all options that {Capybara::Node::Finders#all} accepts,
# such as :text and :visible.
#
# node.assert_matches_selector('li', :text => 'Horse', :visible => true)
#
# @param (see Capybara::Node::Finders#all)
# @raise [Capybara::ExpectationNotMet] If the selector does not match
#
def assert_matches_selector(*args)
query = Capybara::Queries::MatchQuery.new(*args)
synchronize(query.wait) do
result = query.resolve_for(self.parent)
unless result.include? self
raise Capybara::ExpectationNotMet, "Item does not match the provided selector"
end
end
return true
end
def assert_not_matches_selector(*args)
query = Capybara::Queries::MatchQuery.new(*args)
synchronize(query.wait) do
result = query.resolve_for(self.parent)
if result.include? self
raise Capybara::ExpectationNotMet, 'Item matched the provided selector'
end
end
return true
end
alias_method :refute_matches_selector, :assert_not_matches_selector
##
#
# Checks if a given XPath expression is on the page or current node.

View File

@ -0,0 +1,21 @@
module Capybara
module Queries
class MatchQuery < Capybara::Queries::SelectorQuery
VALID_KEYS = [:text, :visible, :exact, :wait]
def visible
if options.has_key?(:visible)
super
else
:all
end
end
private
def valid_keys
VALID_KEYS + @selector.custom_filters.keys
end
end
end
end

View File

@ -0,0 +1,136 @@
# frozen_string_literal: true
module Capybara
module Queries
class SelectorQuery < Queries::BaseQuery
attr_accessor :selector, :locator, :options, :expression, :find, :negative
VALID_KEYS = [:text, :visible, :between, :count, :maximum, :minimum, :exact, :match, :wait]
VALID_MATCH = [:first, :smart, :prefer_exact, :one]
def initialize(*args)
@options = if args.last.is_a?(Hash) then args.pop.dup else {} end
if args[0].is_a?(Symbol)
@selector = Selector.all[args[0]]
@locator = args[1]
else
@selector = Selector.all.values.find { |s| s.match?(args[0]) }
@locator = args[0]
end
@selector ||= Selector.all[Capybara.default_selector]
# for compatibility with Capybara 2.0
if Capybara.exact_options and @selector == Selector.all[:option]
@options[:exact] = true
end
@expression = @selector.call(@locator)
assert_valid_keys
end
def name; selector.name; end
def label; selector.label or selector.name; end
def description
@description = String.new("#{label} #{locator.inspect}")
@description << " with text #{options[:text].inspect}" if options[:text]
@description << selector.description(options)
@description
end
def matches_filters?(node)
if options[:text]
regexp = options[:text].is_a?(Regexp) ? options[:text] : Regexp.escape(options[:text].to_s)
return false if not node.text(visible).match(regexp)
end
case visible
when :visible then return false unless node.visible?
when :hidden then return false if node.visible?
end
selector.custom_filters.each do |name, filter|
if options.has_key?(name)
return false unless filter.matches?(node, options[name])
elsif filter.default?
return false unless filter.matches?(node, filter.default)
end
end
end
def visible
if options.has_key?(:visible)
case @options[:visible]
when true then :visible
when false then :all
else @options[:visible]
end
else
if Capybara.ignore_hidden_elements
:visible
else
:all
end
end
end
def exact?
if options.has_key?(:exact)
@options[:exact]
else
Capybara.exact
end
end
def match
if options.has_key?(:match)
@options[:match]
else
Capybara.match
end
end
def xpath(exact=nil)
exact = self.exact? if exact == nil
if @expression.respond_to?(:to_xpath) and exact
@expression.to_xpath(:exact)
else
@expression.to_s
end
end
def css
@expression
end
# @api private
def resolve_for(node, exact = nil)
node.synchronize do
children = if selector.format == :css
node.find_css(self.css)
else
node.find_xpath(self.xpath(exact))
end.map do |child|
if node.is_a?(Capybara::Node::Base)
Capybara::Node::Element.new(node.session, child, node, self)
else
Capybara::Node::Simple.new(child)
end
end
Capybara::Result.new(children, self)
end
end
private
def valid_keys
COUNT_KEYS + [:text, :visible, :exact, :match, :wait] + @selector.custom_filters.keys
end
def assert_valid_keys
super
unless VALID_MATCH.include?(match)
raise ArgumentError, "invalid option #{match.inspect} for :match, should be one of #{VALID_MATCH.map(&:inspect).join(", ")}"
end
end
end
end
end

View File

@ -1,136 +1,7 @@
# frozen_string_literal: true
require 'capybara/queries/selector_query'
module Capybara
# @deprecated This class and its methods are not supposed to be used by users of Capybara's public API.
# It may be removed in future versions of Capybara.
class Query < Queries::BaseQuery
attr_accessor :selector, :locator, :options, :expression, :find, :negative
VALID_KEYS = [:text, :visible, :between, :count, :maximum, :minimum, :exact, :match, :wait]
VALID_MATCH = [:first, :smart, :prefer_exact, :one]
def initialize(*args)
@options = if args.last.is_a?(Hash) then args.pop.dup else {} end
if args[0].is_a?(Symbol)
@selector = Selector.all[args[0]]
@locator = args[1]
else
@selector = Selector.all.values.find { |s| s.match?(args[0]) }
@locator = args[0]
end
@selector ||= Selector.all[Capybara.default_selector]
# for compatibility with Capybara 2.0
if Capybara.exact_options and @selector == Selector.all[:option]
@options[:exact] = true
end
@expression = @selector.call(@locator)
assert_valid_keys
end
def name; selector.name; end
def label; selector.label or selector.name; end
def description
@description = String.new("#{label} #{locator.inspect}")
@description << " with text #{options[:text].inspect}" if options[:text]
@description << selector.description(options)
@description
end
def matches_filters?(node)
if options[:text]
regexp = options[:text].is_a?(Regexp) ? options[:text] : Regexp.escape(options[:text].to_s)
return false if not node.text(visible).match(regexp)
end
case visible
when :visible then return false unless node.visible?
when :hidden then return false if node.visible?
end
selector.custom_filters.each do |name, filter|
if options.has_key?(name)
return false unless filter.matches?(node, options[name])
elsif filter.default?
return false unless filter.matches?(node, filter.default)
end
end
end
def visible
if options.has_key?(:visible)
case @options[:visible]
when true then :visible
when false then :all
else @options[:visible]
end
else
if Capybara.ignore_hidden_elements
:visible
else
:all
end
end
end
def exact?
if options.has_key?(:exact)
@options[:exact]
else
Capybara.exact
end
end
def match
if options.has_key?(:match)
@options[:match]
else
Capybara.match
end
end
def xpath(exact=nil)
exact = self.exact? if exact == nil
if @expression.respond_to?(:to_xpath) and exact
@expression.to_xpath(:exact)
else
@expression.to_s
end
end
def css
@expression
end
# @api private
def resolve_for(node, exact = nil)
node.synchronize do
children = if selector.format == :css
node.find_css(self.css)
else
node.find_xpath(self.xpath(exact))
end.map do |child|
if node.is_a?(Capybara::Node::Base)
Capybara::Node::Element.new(node.session, child, node, self)
else
Capybara::Node::Simple.new(child)
end
end
Capybara::Result.new(children, self)
end
end
private
def valid_keys
COUNT_KEYS + [:text, :visible, :exact, :match, :wait] + @selector.custom_filters.keys
end
def assert_valid_keys
super
unless VALID_MATCH.include?(match)
raise ArgumentError, "invalid option #{match.inspect} for :match, should be one of #{VALID_MATCH.map(&:inspect).join(", ")}"
end
end
end
Query = Queries::SelectorQuery
end

View File

@ -2,7 +2,7 @@
module Capybara
module RSpecMatchers
class Matcher
include ::RSpec::Matchers::Composable if defined?(::RSpec::Expectations::Version) && RSpec::Expectations::Version::STRING.to_f >= 3.0
include ::RSpec::Matchers::Composable if defined?(::RSpec::Expectations::Version) && (Gem::Version.new(RSpec::Expectations::Version::STRING) >= Gem::Version.new('3.0'))
def wrap(actual)
if actual.respond_to?("has_selector?")
@ -39,7 +39,7 @@ module Capybara
end
def query
@query ||= Capybara::Query.new(*@args)
@query ||= Capybara::Queries::SelectorQuery.new(*@args)
end
# RSpec 2 compatibility:
@ -161,7 +161,7 @@ module Capybara
class BecomeClosed
def initialize(options)
@wait_time = Capybara::Query.new(options).wait
@wait_time = Capybara::Queries::SelectorQuery.new(options).wait
end
def matches?(window)
@ -187,18 +187,68 @@ module Capybara
alias_method :failure_message_for_should_not, :failure_message_when_negated
end
class MatchSelector < Matcher
attr_reader :failure_message, :failure_message_when_negated
def initialize(*args)
@args = args
end
def matches?(actual)
actual.assert_matches_selector(*@args)
rescue Capybara::ExpectationNotMet => e
@failure_message = e.message
return false
end
def does_not_match?(actual)
actual.assert_not_matches_selector(*@args)
rescue Capybara::ExpectationNotMet => e
@failure_message_when_negated = e.message
return false
end
def description
"match #{query.description}"
end
def query
@query ||= Capybara::Queries::MatchQuery.new(*@args)
end
# RSpec 2 compatibility:
alias_method :failure_message_for_should, :failure_message
alias_method :failure_message_for_should_not, :failure_message_when_negated
end
def have_selector(*args)
HaveSelector.new(*args)
end
def match_selector(*args)
MatchSelector.new(*args)
end
# defined_negated_matcher was added in RSpec 3.1 - it's syntactic sugar only since a user can do
# expect(page).not_to match_selector, so not sure we really need to support not_match_selector for prior to RSpec 3.1
::RSpec::Matchers.define_negated_matcher :not_match_selector, :match_selector if defined?(::RSpec::Expectations::Version) && (Gem::Version.new(RSpec::Expectations::Version::STRING) >= Gem::Version.new('3.1'))
def have_xpath(xpath, options={})
HaveSelector.new(:xpath, xpath, options)
end
def match_xpath(xpath, options={})
MatchSelector.new(:xpath, xpath, options)
end
def have_css(css, options={})
HaveSelector.new(:css, css, options)
end
def match_css(css, options={})
MatchSelector.new(:css, css, options)
end
def have_text(*args)
HaveText.new(*args)
end

View File

@ -404,7 +404,7 @@ module Capybara
driver.switch_to_window(window.handle)
window
else
wait_time = Capybara::Query.new(options).wait
wait_time = Capybara::Queries::SelectorQuery.new(options).wait
document.synchronize(wait_time, errors: [Capybara::WindowError]) do
original_window_handle = driver.current_window_handle
begin
@ -501,7 +501,7 @@ module Capybara
old_handles = driver.window_handles
block.call
wait_time = Capybara::Query.new(options).wait
wait_time = Capybara::Queries::SelectorQuery.new(options).wait
document.synchronize(wait_time, errors: [Capybara::WindowError]) do
opened_handles = (driver.window_handles - old_handles)
if opened_handles.size != 1

View File

@ -117,4 +117,10 @@ $(function() {
$('#with-key-events').keydown(function(e){
$('#key-events-output').append('keydown:'+e.which+' ')
});
$('#disable-on-click').click(function(e){
var input = this
setTimeout(function() {
input.disabled = true;
}, 500)
})
});

View File

@ -0,0 +1,31 @@
Capybara::SpecHelper.spec '#assert_matches_selector' do
before do
@session.visit('/with_html')
@element = @session.find(:css, 'span', text: '42')
end
it "should be true if the given selector matches the element" do
expect(@element.assert_matches_selector(:css, '.number')).to be true
end
it "should be false if the given selector does not match the element" do
expect { @element.assert_matches_selector(:css, '.not_number') }.to raise_error(Capybara::ElementNotFound)
end
it "should not be callable on the session" do
expect { @session.assert_matches_selector(:css, '.number') }.to raise_error(NoMethodError)
end
it "should wait for match to occur", requires: [:js] do
@session.visit('/with_js')
input = @session.find(:css, '#disable-on-click')
expect(input.assert_matches_selector(:css, 'input:enabled')).to be true
input.click
expect(input.assert_matches_selector(:css, 'input:disabled')).to be true
end
it "should not accept count options" do
expect { @element.assert_matches_selector(:css, '.number', count: 1) }.to raise_error(ArgumentError)
end
end

View File

@ -0,0 +1,17 @@
Capybara::SpecHelper.spec '#match_css?' do
before do
@session.visit('/with_html')
@element = @session.find(:css, 'span', text: '42')
end
it "should be true if the given selector matches the element" do
expect(@element).to match_css("span")
expect(@element).to match_css("span.number")
end
it "should be false if the given selector does not match" do
expect(@element).not_to match_css("div")
expect(@element).not_to match_css("p a#doesnotexist")
expect(@element).not_to match_css("p.nosuchclass")
end
end

View File

@ -0,0 +1,23 @@
Capybara::SpecHelper.spec '#match_xpath?' do
before do
@session.visit('/with_html')
@element = @session.find(:css, 'span.number')
end
it "should be true if the given selector is on the page" do
expect(@element).to match_xpath("//span")
expect(@element).to match_xpath("//span[@class='number']")
end
it "should be false if the given selector is not on the page" do
expect(@element).not_to match_xpath("//abbr")
expect(@element).not_to match_xpath("//div")
expect(@element).not_to match_xpath("//span[@class='not_a_number']")
end
it "should use xpath even if default selector is CSS" do
Capybara.default_selector = :css
expect(@element).not_to have_xpath("//span[@class='not_a_number']")
expect(@element).not_to have_xpath("//div[@class='number']")
end
end

View File

@ -0,0 +1,63 @@
Capybara::SpecHelper.spec '#match_xpath?' do
before do
@session.visit('/with_html')
@element = @session.find('//span', text: '42')
end
it "should be true if the element matches the given selector" do
expect(@element).to match_selector(:xpath, "//span")
expect(@element).to match_selector(:css, 'span.number')
expect(@element.matches_selector?(:css, 'span.number')).to be true
end
it "should be false if the element does not match the given selector" do
expect(@element).not_to match_selector(:xpath, "//div")
expect(@element).not_to match_selector(:css, "span.not_a_number")
expect(@element.matches_selector?(:css, "span.not_a_number")).to be false
end
it "should use default selector" do
Capybara.default_selector = :css
expect(@element).not_to match_selector("span.not_a_number")
expect(@element).to match_selector("span.number")
end
context "with text" do
it "should discard all matches where the given string is not contained" do
expect(@element).to match_selector("//span", :text => "42")
expect(@element).not_to match_selector("//span", :text => "Doesnotexist")
end
end
end
Capybara::SpecHelper.spec '#not_matches_selector?' do
before do
@session.visit('/with_html')
@element = @session.find(:css, "span", text: 42)
end
it "should be false if the given selector matches the element" do
expect(@element).not_to not_match_selector(:xpath, "//span")
expect(@element).not_to not_match_selector(:css, "span.number")
expect(@element.not_matches_selector?(:css, "span.number")).to be false
end
it "should be true if the given selector does not match the element" do
expect(@element).to not_match_selector(:xpath, "//abbr")
expect(@element).to not_match_selector(:css, "p a#doesnotexist")
expect(@element.not_matches_selector?(:css, "p a#doesnotexist")).to be true
end
it "should use default selector" do
Capybara.default_selector = :css
expect(@element).to not_match_selector("p a#doesnotexist")
expect(@element).not_to not_match_selector("span.number")
end
context "with text" do
it "should discard all matches where the given string is contained" do
expect(@element).not_to not_match_selector(:css, "span.number", :text => "42")
expect(@element).to not_match_selector(:css, "span.number", :text => "Doesnotexist")
end
end
end if Gem::Version.new(RSpec::Expectations::Version::STRING) >= Gem::Version.new('3.1')

View File

@ -11,6 +11,7 @@
<h2 class="head">Header Class Test Five</h2>
<span class="number">42</span>
<span>Other span</span>
<p class="para" id="first">
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod

View File

@ -94,6 +94,10 @@
<a href="#" id="open-prompt">Open prompt</a>
</p>
<p>
<input id="disable-on-click"/>
</p>
<p>
<a href="#" id="delayed-page-change">Change page</a>
<a href="/with_html?options[]=things">Non-escaped query options</a>