277 lines
11 KiB
Ruby
277 lines
11 KiB
Ruby
# 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
|
|
|
|
##
|
|
#
|
|
# 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
|