1
0
Fork 0
mirror of https://github.com/teamcapybara/capybara.git synced 2022-11-09 12:08:07 -05:00

Split selector definition and configured selector (#2178)

* Split selector definition and configured selector
This commit is contained in:
Thomas Walpole 2019-04-22 17:43:02 -07:00 committed by GitHub
parent eca6ad475d
commit d6f570ea19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 394 additions and 310 deletions

View file

@ -30,6 +30,9 @@ cache:
matrix:
include:
- gemfile: gemfiles/Gemfile.gumbo
rvm: 2.5
script: bundle exec rake rack_smoke
- gemfile: Gemfile
rvm: 2.5
env: CAPYBARA_REMOTE=true
@ -88,9 +91,6 @@ matrix:
env: W3C=true HEADLESS=true
addons:
chrome: beta
- gemfile: gemfiles/Gemfile.gumbo
rvm: 2.5
script: bundle exec rake rack_smoke
allow_failures:
- gemfile: gemfiles/Gemfile.beta-versions
- gemfile: gemfiles/Gemfile.edge-firefox

View file

@ -19,14 +19,17 @@ module Capybara
super(@options)
self.session_options = session_options
@selector = find_selector(args[0].is_a?(Symbol) ? args.shift : args[0])
@selector = Selector.new(
find_selector(args[0].is_a?(Symbol) ? args.shift : args[0]),
config: { enable_aria_label: enable_aria_label, test_id: test_id }
)
@locator = args.shift
@filter_block = filter_block
raise ArgumentError, "Unused parameters passed to #{self.class.name} : #{args}" unless args.empty?
selector_config = { enable_aria_label: enable_aria_label, test_id: test_id }
@expression = selector.call(@locator, @options.merge(selector_config: selector_config))
@expression = selector.call(@locator, @options)
warn_exact_usage
@ -129,7 +132,9 @@ module Capybara
# @api private
def supports_exact?
@expression.respond_to? :to_xpath
return @expression.respond_to? :to_xpath if @selector.supports_exact?.nil?
@selector.supports_exact?
end
def failure_message
@ -142,6 +147,10 @@ module Capybara
private
def selector_format
@selector.format
end
def text_fragments
text = (options[:text] || options[:exact_text])
text.is_a?(String) ? text.split : []
@ -192,23 +201,23 @@ module Capybara
def find_nodes_by_selector_format(node, exact)
hints = {}
hints[:uses_visibility] = true unless visible == :all
hints[:texts] = text_fragments unless selector.format == :xpath
hints[:texts] = text_fragments unless selector_format == :xpath
hints[:styles] = options[:style] if use_default_style_filter?
if selector.format == :css
if selector_format == :css
if node.method(:find_css).arity != 1
node.find_css(css, **hints)
else
node.find_css(css)
end
elsif selector.format == :xpath
elsif selector_format == :xpath
if node.method(:find_xpath).arity != 1
node.find_xpath(xpath(exact), **hints)
else
node.find_xpath(xpath(exact))
end
else
raise ArgumentError, "Unknown format: #{selector.format}"
raise ArgumentError, "Unknown format: #{selector_format}"
end
end
@ -230,6 +239,8 @@ module Capybara
unapplied_options = options.keys - valid_keys
@selector.with_filter_errors(errors) do
node_filters.all? do |filter_name, filter|
next true unless apply_filter?(filter)
if filter.matcher?
unapplied_options.select { |option_name| filter.handles_option?(option_name) }.all? do |option_name|
unapplied_options.delete(option_name)
@ -320,6 +331,8 @@ module Capybara
def apply_expression_filters(expression)
unapplied_options = options.keys - valid_keys
expression_filters.inject(expression) do |expr, (name, ef)|
next expr unless apply_filter?(ef)
if ef.matcher?
unapplied_options.select(&ef.method(:handles_option?)).inject(expr) do |memo, option_name|
unapplied_options.delete(option_name)
@ -358,10 +371,14 @@ module Capybara
node.is_a?(::Capybara::Node::Simple) && node.path == '/'
end
def matches_locator_filter?(node)
return true unless @selector.locator_filter
def apply_filter?(_filter)
true
end
@selector.locator_filter.matches?(node, @locator, @selector)
def matches_locator_filter?(node)
return true unless @selector.locator_filter && apply_filter?(@selector.locator_filter)
@selector.locator_filter.matches?(node, @locator, @selector, exact: exact?)
end
def matches_system_filters?(node)

View file

@ -50,6 +50,7 @@ end
Capybara.add_selector(:field, locator_type: [String, Symbol]) do
visible { |options| :hidden if options[:type].to_s == 'hidden' }
xpath do |locator, **options|
invalid_types = %w[submit image]
invalid_types << 'hidden' unless options[:type].to_s == 'hidden'
@ -69,6 +70,7 @@ Capybara.add_selector(:field, locator_type: [String, Symbol]) do
filter_set(:_field) # checked/unchecked/disabled/multiple/name/placeholder
node_filter(:readonly, :boolean) { |node, value| !(value ^ node.readonly?) }
node_filter(:with) do |node, with|
val = node.value
(with.is_a?(Regexp) ? with.match?(val) : val == with.to_s).tap do |res|
@ -185,8 +187,8 @@ end
Capybara.add_selector(:link_or_button, locator_type: [String, Symbol]) do
label 'link or button'
xpath do |locator, **options|
self.class.all.values_at(:link, :button).map do |selector|
instance_exec(locator, options, &selector.xpath)
%i[link button].map do |selector|
expression_for(selector, locator, **options)
end.reduce(:union)
end
@ -297,7 +299,7 @@ Capybara.add_selector(:select, locator_type: [String, Symbol]) do
expression_filter(:with_options) do |expr, options|
options.inject(expr) do |xpath, option|
xpath[self.class.all[:option].call(option)]
xpath[expression_for(:option, option)]
end
end
@ -350,7 +352,7 @@ Capybara.add_selector(:datalist_input, locator_type: [String, Symbol]) do
expression_filter(:with_options) do |expr, options|
options.inject(expr) do |xpath, option|
xpath[XPath.attr(:list) == XPath.anywhere(:datalist)[self.class.all[:datalist_option].call(option)].attr(:id)]
xpath[XPath.attr(:list) == XPath.anywhere(:datalist)[expression_for(:datalist_option, option)].attr(:id)]
end
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
require 'capybara/selector/selector'
module Capybara
class Selector
class CSS

View file

@ -0,0 +1,279 @@
# frozen_string_literal: true
require 'capybara/selector/filter_set'
require 'capybara/selector/css'
require 'capybara/selector/regexp_disassembler'
require 'capybara/selector/builders/xpath_builder'
require 'capybara/selector/builders/css_builder'
module Capybara
class Selector
class Definition
attr_reader :name, :expressions
extend Forwardable
def initialize(name, locator_type: nil, raw_locator: false, supports_exact: nil, &block)
@name = name
@filter_set = Capybara::Selector::FilterSet.add(name) {}
@match = nil
@label = nil
@failure_message = nil
@expressions = {}
@expression_filters = {}
@locator_filter = nil
@default_visibility = nil
@locator_type = locator_type
@raw_locator = raw_locator
@supports_exact = supports_exact
instance_eval(&block)
end
def custom_filters
warn "Deprecated: Selector#custom_filters is not valid when same named expression and node filter exist - don't use"
node_filters.merge(expression_filters).freeze
end
def node_filters
@filter_set.node_filters
end
def expression_filters
@filter_set.expression_filters
end
##
#
# Define a selector by an xpath expression
#
# @overload xpath(*expression_filters, &block)
# @param [Array<Symbol>] expression_filters ([]) Names of filters that are implemented via this expression, if not specified the names of any keyword parameters in the block will be used
# @yield [locator, options] The block to use to generate the XPath expression
# @yieldparam [String] locator The locator string passed to the query
# @yieldparam [Hash] options The options hash passed to the query
# @yieldreturn [#to_xpath, #to_s] An object that can produce an xpath expression
#
# @overload xpath()
# @return [#call] The block that will be called to generate the XPath expression
#
def xpath(*allowed_filters, &block)
expression(:xpath, allowed_filters, &block)
end
##
#
# Define a selector by a CSS selector
#
# @overload css(*expression_filters, &block)
# @param [Array<Symbol>] expression_filters ([]) Names of filters that can be implemented via this CSS selector
# @yield [locator, options] The block to use to generate the CSS selector
# @yieldparam [String] locator The locator string passed to the query
# @yieldparam [Hash] options The options hash passed to the query
# @yieldreturn [#to_s] An object that can produce a CSS selector
#
# @overload css()
# @return [#call] The block that will be called to generate the CSS selector
#
def css(*allowed_filters, &block)
expression(:css, allowed_filters, &block)
end
##
#
# Automatic selector detection
#
# @yield [locator] This block takes the passed in locator string and returns whether or not it matches the selector
# @yieldparam [String], locator The locator string used to determin if it matches the selector
# @yieldreturn [Boolean] Whether this selector matches the locator string
# @return [#call] The block that will be used to detect selector match
#
def match(&block)
@match = block if block
@match
end
##
#
# Set/get a descriptive label for the selector
#
# @overload label(label)
# @param [String] label A descriptive label for this selector - used in error messages
# @overload label()
# @return [String] The currently set label
#
def label(label = nil)
@label = label if label
@label
end
##
#
# Description of the selector
#
# @!method description(options)
# @param [Hash] options The options of the query used to generate the description
# @return [String] Description of the selector when used with the options passed
def_delegator :@filter_set, :description
##
#
# Should this selector be used for the passed in locator
#
# This is used by the automatic selector selection mechanism when no selector type is passed to a selector query
#
# @param [String] locator The locator passed to the query
# @return [Boolean] Whether or not to use this selector
#
def match?(locator)
@match&.call(locator)
end
##
#
# Define a node filter for use with this selector
#
# @!method node_filter(name, *types, options={}, &block)
# @param [Symbol, Regexp] name The filter name
# @param [Array<Symbol>] types The types of the filter - currently valid types are [:boolean]
# @param [Hash] options ({}) Options of the filter
# @option options [Array<>] :valid_values Valid values for this filter
# @option options :default The default value of the filter (if any)
# @option options :skip_if Value of the filter that will cause it to be skipped
# @option options [Regexp] :matcher (nil) A Regexp used to check whether a specific option is handled by this filter. If not provided the filter will be used for options matching the filter name.
#
# If a Symbol is passed for the name the block should accept | node, option_value |, while if a Regexp
# is passed for the name the block should accept | node, option_name, option_value |. In either case
# the block should return `true` if the node passes the filer or `false` if it doesn't
# @!method filter
# See {Selector#node_filter}
##
#
# Define an expression filter for use with this selector
#
# @!method expression_filter(name, *types, matcher: nil, **options, &block)
# @param [Symbol, Regexp] name The filter name
# @param [Regexp] matcher (nil) A Regexp used to check whether a specific option is handled by this filter
# @param [Array<Symbol>] types The types of the filter - currently valid types are [:boolean]
# @param [Hash] options ({}) Options of the filter
# @option options [Array<>] :valid_values Valid values for this filter
# @option options :default The default value of the filter (if any)
# @option options :skip_if Value of the filter that will cause it to be skipped
# @option options [Regexp] :matcher (nil) A Regexp used to check whether a specific option is handled by this filter. If not provided the filter will be used for options matching the filter name.
#
# If a Symbol is passed for the name the block should accept | current_expression, option_value |, while if a Regexp
# is passed for the name the block should accept | current_expression, option_name, option_value |. In either case
# the block should return the modified expression
def_delegators :@filter_set, :node_filter, :expression_filter, :filter
def locator_filter(*types, **options, &block)
types.each { |type| options[type] = true }
@locator_filter = Capybara::Selector::Filters::LocatorFilter.new(block, options) if block
@locator_filter
end
def filter_set(name, filters_to_use = nil)
@filter_set.import(name, filters_to_use)
end
def_delegator :@filter_set, :describe
def describe_expression_filters(&block)
if block_given?
describe(:expression_filters, &block)
else
describe(:expression_filters) do |**options|
describe_all_expression_filters(options)
end
end
end
def describe_all_expression_filters(**opts)
expression_filters.map do |ef_name, ef|
if ef.matcher?
handled_custom_keys(ef, opts.keys).map { |key| " with #{ef_name}[#{key} => #{opts[key]}]" }.join
elsif opts.key?(ef_name)
" with #{ef_name} #{opts[ef_name]}"
end
end.join
end
def describe_node_filters(&block)
describe(:node_filters, &block)
end
##
#
# Set the default visibility mode that shouble be used if no visibile option is passed when using the selector.
# If not specified will default to the behavior indicated by Capybara.ignore_hidden_elements
#
# @param [Symbol] default_visibility Only find elements with the specified visibility:
# * :all - finds visible and invisible elements.
# * :hidden - only finds invisible elements.
# * :visible - only finds visible elements.
def visible(default_visibility = nil, &block)
@default_visibility = block || default_visibility
end
def default_visibility(fallback = Capybara.ignore_hidden_elements, options = {})
vis = if @default_visibility&.respond_to?(:call)
@default_visibility.call(options)
else
@default_visibility
end
vis.nil? ? fallback : vis
end
# @api private
def raw_locator?
!!@raw_locator
end
# @api private
def supports_exact?
@supports_exact
end
def default_format
return nil if @expressions.keys.empty?
if @expressions.size == 1
@expressions.keys.first
else
:xpath
end
end
# @api private
def locator_types
return nil unless @locator_type
Array(@locator_type)
end
private
def handled_custom_keys(filter, keys)
keys.select do |key|
filter.handles_option?(key) && !::Capybara::Queries::SelectorQuery::VALID_KEYS.include?(key)
end
end
def parameter_names(block)
block.parameters.select { |(type, _name)| %i[key keyreq].include? type }.map { |(_type, name)| name }
end
def expression(type, allowed_filters, &block)
if block
@expressions[type] = block
allowed_filters = parameter_names(block) if allowed_filters.empty?
allowed_filters.flatten.each do |ef|
expression_filters[ef] = Capybara::Selector::Filters::IdentityExpressionFilter.new(ef)
end
end
@expressions[type]
end
end
end
end

View file

@ -10,8 +10,18 @@ module Capybara
super(nil, nil, block, options)
end
def matches?(node, value, context = nil)
super(node, nil, value, context)
def matches?(node, value, context = nil, exact:)
apply(node, value, true, context, exact: exact)
rescue Capybara::ElementNotFound
false
end
private
def apply(subject, value, skip_value, ctx, **options)
return skip_value if skip?(value)
filter_context(ctx).instance_exec(subject, value, **options, &@block)
end
end
end

View file

@ -2,12 +2,6 @@
# rubocop:disable Style/AsciiComments
require 'capybara/selector/filter_set'
require 'capybara/selector/css'
require 'capybara/selector/regexp_disassembler'
require 'capybara/selector/builders/xpath_builder'
require 'capybara/selector/builders/css_builder'
module Capybara
#
# ## Built-in Selectors
@ -179,13 +173,11 @@ module Capybara
# * Locator: Type of element ('div', 'a', etc) - if not specified defaults to '*'
# * Filters: Matches on any element attribute
#
class Selector
attr_reader :name, :format
extend Forwardable
class Selector < SimpleDelegator
class << self
def all
@selectors ||= {} # rubocop:disable Naming/MemoizedInstanceVariableName
@definitions ||= {} # rubocop:disable Naming/MemoizedInstanceVariableName
end
def [](name)
@ -193,7 +185,7 @@ module Capybara
end
def add(name, **options, &block)
all[name.to_sym] = Capybara::Selector.new(name.to_sym, **options, &block)
all[name.to_sym] = Definition.new(name.to_sym, **options, &block)
end
def update(name, &block)
@ -209,116 +201,34 @@ module Capybara
end
end
def initialize(name, locator_type: nil, raw_locator: false, &block)
@name = name
@filter_set = FilterSet.add(name) {}
@match = nil
@label = nil
@failure_message = nil
@format = nil
@expression = nil
@expression_filters = {}
@locator_filter = nil
@default_visibility = nil
@locator_type = locator_type
@raw_locator = raw_locator
@config = {
enable_aria_label: false,
test_id: nil
}
instance_eval(&block)
attr_reader :errors
def initialize(definition, config:)
definition = self.class[definition] unless definition.is_a? Definition
super(definition)
@definition = definition
@config = config
@errors = []
end
def custom_filters
warn "Deprecated: Selector#custom_filters is not valid when same named expression and node filter exist - don't use"
node_filters.merge(expression_filters).freeze
def format
@definition.default_format
end
alias_method :current_format, :format
def enable_aria_label
@config[:enable_aria_label]
end
def node_filters
@filter_set.node_filters
def test_id
@config[:test_id]
end
def expression_filters
@filter_set.expression_filters
end
##
#
# Define a selector by an xpath expression
#
# @overload xpath(*expression_filters, &block)
# @param [Array<Symbol>] expression_filters ([]) Names of filters that are implemented via this expression, if not specified the names of any keyword parameters in the block will be used
# @yield [locator, options] The block to use to generate the XPath expression
# @yieldparam [String] locator The locator string passed to the query
# @yieldparam [Hash] options The options hash passed to the query
# @yieldreturn [#to_xpath, #to_s] An object that can produce an xpath expression
#
# @overload xpath()
# @return [#call] The block that will be called to generate the XPath expression
#
def xpath(*allowed_filters, &block)
expression(:xpath, allowed_filters, &block)
end
##
#
# Define a selector by a CSS selector
#
# @overload css(*expression_filters, &block)
# @param [Array<Symbol>] expression_filters ([]) Names of filters that can be implemented via this CSS selector
# @yield [locator, options] The block to use to generate the CSS selector
# @yieldparam [String] locator The locator string passed to the query
# @yieldparam [Hash] options The options hash passed to the query
# @yieldreturn [#to_s] An object that can produce a CSS selector
#
# @overload css()
# @return [#call] The block that will be called to generate the CSS selector
#
def css(*allowed_filters, &block)
expression(:css, allowed_filters, &block)
end
##
#
# Automatic selector detection
#
# @yield [locator] This block takes the passed in locator string and returns whether or not it matches the selector
# @yieldparam [String], locator The locator string used to determin if it matches the selector
# @yieldreturn [Boolean] Whether this selector matches the locator string
# @return [#call] The block that will be used to detect selector match
#
def match(&block)
@match = block if block
@match
end
##
#
# Set/get a descriptive label for the selector
#
# @overload label(label)
# @param [String] label A descriptive label for this selector - used in error messages
# @overload label()
# @return [String] The currently set label
#
def label(label = nil)
@label = label if label
@label
end
##
#
# Description of the selector
#
# @!method description(options)
# @param [Hash] options The options of the query used to generate the description
# @return [String] Description of the selector when used with the options passed
def_delegator :@filter_set, :description
def call(locator, selector_config: {}, **options)
@config.merge! selector_config
def call(locator, **options)
if format
@expression.call(locator, options)
raise ArgumentError, "Selector #{@name} does not support #{format}" unless expressions.key?(format)
instance_exec(locator, options, &expressions[format])
else
warn 'Selector has no format'
end
@ -326,111 +236,27 @@ module Capybara
warn "Locator #{locator.inspect} must #{locator_description}. This will raise an error in a future version of Capybara." unless locator_valid?(locator)
end
##
#
# Should this selector be used for the passed in locator
#
# This is used by the automatic selector selection mechanism when no selector type is passed to a selector query
#
# @param [String] locator The locator passed to the query
# @return [Boolean] Whether or not to use this selector
#
def match?(locator)
@match&.call(locator)
end
##
#
# Define a node filter for use with this selector
#
# @!method node_filter(name, *types, options={}, &block)
# @param [Symbol, Regexp] name The filter name
# @param [Array<Symbol>] types The types of the filter - currently valid types are [:boolean]
# @param [Hash] options ({}) Options of the filter
# @option options [Array<>] :valid_values Valid values for this filter
# @option options :default The default value of the filter (if any)
# @option options :skip_if Value of the filter that will cause it to be skipped
# @option options [Regexp] :matcher (nil) A Regexp used to check whether a specific option is handled by this filter. If not provided the filter will be used for options matching the filter name.
#
# If a Symbol is passed for the name the block should accept | node, option_value |, while if a Regexp
# is passed for the name the block should accept | node, option_name, option_value |. In either case
# the block should return `true` if the node passes the filer or `false` if it doesn't
# @!method filter
# See {Selector#node_filter}
##
#
# Define an expression filter for use with this selector
#
# @!method expression_filter(name, *types, matcher: nil, **options, &block)
# @param [Symbol, Regexp] name The filter name
# @param [Regexp] matcher (nil) A Regexp used to check whether a specific option is handled by this filter
# @param [Array<Symbol>] types The types of the filter - currently valid types are [:boolean]
# @param [Hash] options ({}) Options of the filter
# @option options [Array<>] :valid_values Valid values for this filter
# @option options :default The default value of the filter (if any)
# @option options :skip_if Value of the filter that will cause it to be skipped
# @option options [Regexp] :matcher (nil) A Regexp used to check whether a specific option is handled by this filter. If not provided the filter will be used for options matching the filter name.
#
# If a Symbol is passed for the name the block should accept | current_expression, option_value |, while if a Regexp
# is passed for the name the block should accept | current_expression, option_name, option_value |. In either case
# the block should return the modified expression
def_delegators :@filter_set, :node_filter, :expression_filter, :filter
def locator_filter(*types, **options, &block)
types.each { |type| options[type] = true }
@locator_filter = Filters::LocatorFilter.new(block, options) if block
@locator_filter
end
def filter_set(name, filters_to_use = nil)
@filter_set.import(name, filters_to_use)
end
def_delegator :@filter_set, :describe
def describe_expression_filters(&block)
if block_given?
describe(:expression_filters, &block)
else
describe(:expression_filters) do |**options|
describe_all_expression_filters(options)
end
end
end
def describe_node_filters(&block)
describe(:node_filters, &block)
end
##
#
# Set the default visibility mode that shouble be used if no visibile option is passed when using the selector.
# If not specified will default to the behavior indicated by Capybara.ignore_hidden_elements
#
# @param [Symbol] default_visibility Only find elements with the specified visibility:
# * :all - finds visible and invisible elements.
# * :hidden - only finds invisible elements.
# * :visible - only finds visible elements.
def visible(default_visibility = nil, &block)
@default_visibility = block || default_visibility
end
def default_visibility(fallback = Capybara.ignore_hidden_elements, options = {})
vis = if @default_visibility&.respond_to?(:call)
@default_visibility.call(options)
else
@default_visibility
end
vis.nil? ? fallback : vis
end
def add_error(error_msg)
errors << error_msg
end
def expression_for(name, locator, config: @config, **options)
Selector.new(name, config: config).call(locator, **options)
end
# def expression_for(name, locator, config: @config, format: current_format, **options)
# Selector.new(name, config: config, format: format).call(locator, **options)
# end
# @api private
def with_filter_errors(errors)
old_errors = @errors
@errors = errors
yield
ensure
@errors = old_errors
end
# @api private
def builder(expr = nil)
case format
@ -439,39 +265,12 @@ module Capybara
when :xpath
Capybara::Selector::XPathBuilder
else
raise NotImplementedError, "No builder exists for selector of type #{format}"
raise NotImplementedError, "No builder exists for selector of type #{default_format}"
end.new(expr)
end
# @api private
def with_filter_errors(errors)
Thread.current["capybara_#{object_id}_errors"] = errors
yield
ensure
Thread.current["capybara_#{object_id}_errors"] = nil
end
# @api private
def raw_locator?
!!@raw_locator
end
private
def locator_types
return nil unless @locator_type
Array(@locator_type)
end
def locator_valid?(locator)
return true unless locator && locator_types
locator_types&.any? do |type_or_method|
type_or_method.is_a?(Symbol) ? locator.respond_to?(type_or_method) : type_or_method === locator # rubocop:disable Style/CaseEquality
end
end
def locator_description
locator_types.group_by { |lt| lt.is_a? Symbol }.map do |symbol, types_or_methods|
if symbol
@ -482,16 +281,12 @@ module Capybara
end.join(' or ')
end
def errors
Thread.current["capybara_#{object_id}_errors"] || []
end
def locator_valid?(locator)
return true unless locator && locator_types
def enable_aria_label
@config[:enable_aria_label]
end
def test_id
@config[:test_id]
locator_types&.any? do |type_or_method|
type_or_method.is_a?(Symbol) ? locator.respond_to?(type_or_method) : type_or_method === locator # rubocop:disable Style/CaseEquality
end
end
def locate_field(xpath, locator, **_options)
@ -510,22 +305,6 @@ module Capybara
locate_xpath + XPath.descendant(:label)[XPath.string.n.is(locator)].descendant(xpath)
end
def describe_all_expression_filters(**opts)
expression_filters.map do |ef_name, ef|
if ef.matcher?
handled_custom_keys(ef, opts.keys).map { |key| " with #{ef_name}[#{key} => #{opts[key]}]" }.join
elsif opts.key?(ef_name)
" with #{ef_name} #{opts[ef_name]}"
end
end.join
end
def handled_custom_keys(filter, keys)
keys.select do |key|
filter.handles_option?(key) && !::Capybara::Queries::SelectorQuery::VALID_KEYS.include?(key)
end
end
def find_by_attr(attribute, value)
finder_name = "find_by_#{attribute}_attr"
if respond_to?(finder_name, true)
@ -538,20 +317,9 @@ module Capybara
def find_by_class_attr(classes)
Array(classes).map { |klass| XPath.attr(:class).contains_word(klass) }.reduce(:&)
end
def parameter_names(block)
block.parameters.select { |(type, _name)| %i[key keyreq].include? type }.map { |(_type, name)| name }
end
def expression(type, allowed_filters, &block)
if block
@format, @expression = type, block
allowed_filters = parameter_names(block) if allowed_filters.empty?
allowed_filters.flatten.each { |ef| expression_filters[ef] = Filters::IdentityExpressionFilter.new(ef) }
end
format == type ? @expression : nil
end
end
end
# rubocop:enable Style/AsciiComments
require 'capybara/selector/definition'

View file

@ -30,7 +30,7 @@ Capybara::SpecHelper.spec '#all' do
it 'should accept an XPath instance', :exact_false do
@session.visit('/form')
@xpath = Capybara::Selector[:fillable_field].call('Name')
@xpath = Capybara::Selector.new(:fillable_field, config: {}).call('Name')
expect(@xpath).to be_a(::XPath::Union)
@result = @session.all(@xpath).map(&:value)
expect(@result).to include('Smith', 'John', 'John Smith')

View file

@ -235,7 +235,7 @@ Capybara::SpecHelper.spec '#find' do
it 'should accept an XPath instance' do
@session.visit('/form')
@xpath = Capybara::Selector[:fillable_field].call('First Name')
@xpath = Capybara::Selector.new(:fillable_field, config: {}).call('First Name')
expect(@xpath).to be_a(::XPath::Union)
expect(@session.find(@xpath).value).to eq('John')
end

View file

@ -24,7 +24,7 @@ Capybara::SpecHelper.spec '#first' do
it 'should accept an XPath instance' do
@session.visit('/form')
@xpath = Capybara::Selector[:fillable_field].call('First Name')
@xpath = Capybara::Selector.new(:fillable_field, config: {}).call('First Name')
expect(@xpath).to be_a(::XPath::Union)
expect(@session.first(@xpath).value).to eq('John')
end

View file

@ -164,25 +164,28 @@ RSpec.describe Capybara do
describe 'xpath' do
it 'uses filter names passed in' do
selector = Capybara::Selector.new :test do
Capybara.add_selector :test do
xpath(:something, :other) { |_locator| XPath.descendant }
end
selector = Capybara::Selector.new :test, config: nil
expect(selector.expression_filters.keys).to include(:something, :other)
end
it 'gets filter names from block if none passed to xpath method' do
selector = Capybara::Selector.new :test do
Capybara.add_selector :test do
xpath { |_locator, valid3:, valid4: nil| "#{valid3} #{valid4}" }
end
selector = Capybara::Selector.new :test, config: nil
expect(selector.expression_filters.keys).to include(:valid3, :valid4)
end
it 'ignores block parameters if names passed in' do
selector = Capybara::Selector.new :test do
Capybara.add_selector :test do
xpath(:valid1) { |_locator, valid3:, valid4: nil| "#{valid3} #{valid4}" }
end
selector = Capybara::Selector.new :test, config: nil
expect(selector.expression_filters.keys).to include(:valid1)
expect(selector.expression_filters.keys).not_to include(:valid3, :valid4)
@ -202,25 +205,28 @@ RSpec.describe Capybara do
end
it 'uses filter names passed in' do
selector = Capybara::Selector.new :text do
Capybara.add_selector :test do
css(:name, :other_name) { |_locator| '' }
end
selector = Capybara::Selector.new :test, config: nil
expect(selector.expression_filters.keys).to include(:name, :other_name)
end
it 'gets filter names from block if none passed to css method' do
selector = Capybara::Selector.new :test do
Capybara.add_selector :test do
css { |_locator, valid3:, valid4: nil| "#{valid3} #{valid4}" }
end
selector = Capybara::Selector.new :test, config: nil
expect(selector.expression_filters.keys).to include(:valid3, :valid4)
end
it 'ignores block parameters if names passed in' do
selector = Capybara::Selector.new :test do
Capybara.add_selector :test do
css(:valid1) { |_locator, valid3:, valid4: nil| "#{valid3} #{valid4}" }
end
selector = Capybara::Selector.new :test, config: nil
expect(selector.expression_filters.keys).to include(:valid1)
expect(selector.expression_filters.keys).not_to include(:valid3, :valid4)