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:
parent
eca6ad475d
commit
d6f570ea19
11 changed files with 394 additions and 310 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'capybara/selector/selector'
|
||||
|
||||
module Capybara
|
||||
class Selector
|
||||
class CSS
|
||||
|
|
279
lib/capybara/selector/definition.rb
Normal file
279
lib/capybara/selector/definition.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue