2016-08-18 13:27:35 -07:00
# frozen_string_literal: true
2018-01-08 12:23:54 -08:00
2018-05-29 16:46:04 -07:00
# rubocop:disable Style/AsciiComments
2016-08-18 13:27:35 -07:00
require 'capybara/selector/filter_set'
2016-09-22 16:55:54 -07:00
require 'capybara/selector/css'
2016-08-18 13:27:35 -07:00
module Capybara
2018-05-29 16:02:53 -07:00
#
# ## Built-in Selectors
#
# * **:xpath** - Select elements by XPath expression
# * Locator: An XPath expression
#
# * **:css** - Select elements by CSS selector
# * Locator: A CSS selector
#
# * **:id** - Select element by id
# * Locator: The id of the element to match
#
# * **:field** - Select field elements (input [not of type submit, image, or hidden], textarea, select)
2018-07-05 15:29:24 -07:00
# * Locator: Matches against the id, Capybara.test_id attribute, name, or placeholder
2018-05-29 16:02:53 -07:00
# * Filters:
# * :id (String) — Matches the id attribute
# * :name (String) — Matches the name attribute
# * :placeholder (String) — Matches the placeholder attribute
# * :type (String) — Matches the type attribute of the field or element type for 'textarea' and 'select'
# * :readonly (Boolean)
# * :with (String) — Matches the current value of the field
# * :class (String, Array<String>) — Matches the class(es) provided
# * :checked (Boolean) — Match checked fields?
# * :unchecked (Boolean) — Match unchecked fields?
# * :disabled (Boolean) — Match disabled field?
# * :multiple (Boolean) — Match fields that accept multiple values
#
# * **:fieldset** - Select fieldset elements
# * Locator: Matches id or contents of wrapped legend
# * Filters:
# * :id (String) — Matches id attribute
# * :legend (String) — Matches contents of wrapped legend
# * :class (String, Array<String>) — Matches the class(es) provided
#
# * **:link** - Find links ( <a> elements with an href attribute )
# * Locator: Matches the id or title attributes, or the string content of the link, or the alt attribute of a contained img element
# * Filters:
# * :id (String) — Matches the id attribute
# * :title (String) — Matches the title attribute
# * :alt (String) — Matches the alt attribute of a contained img element
# * :class (String) — Matches the class(es) provided
# * :href (String, Regexp, nil) — Matches the normalized href of the link, if nil will find <a> elements with no href attribute
#
# * **:button** - Find buttons ( input [of type submit, reset, image, button] or button elements )
2018-07-05 15:29:24 -07:00
# * Locator: Matches the id, Capybara.test_id attribute, value, or title attributes, string content of a button, or the alt attribute of an image type button or of a descendant image of a button
2018-05-29 16:02:53 -07:00
# * Filters:
# * :id (String) — Matches the id attribute
# * :title (String) — Matches the title attribute
# * :class (String) — Matches the class(es) provided
# * :value (String) — Matches the value of an input button
# * :type
#
# * **:link_or_button** - Find links or buttons
# * Locator: See :link and :button selectors
#
# * **:fillable_field** - Find text fillable fields ( textarea, input [not of type submit, image, radio, checkbox, hidden, file] )
2018-07-05 15:29:24 -07:00
# * Locator: Matches against the id, Capybara.test_id attribute, name, or placeholder
2018-05-29 16:02:53 -07:00
# * Filters:
# * :id (String) — Matches the id attribute
# * :name (String) — Matches the name attribute
# * :placeholder (String) — Matches the placeholder attribute
# * :with (String) — Matches the current value of the field
# * :type (String) — Matches the type attribute of the field or element type for 'textarea'
# * :class (String, Array<String>) — Matches the class(es) provided
# * :disabled (Boolean) — Match disabled field?
# * :multiple (Boolean) — Match fields that accept multiple values
#
# * **:radio_button** - Find radio buttons
2018-07-05 15:29:24 -07:00
# * Locator: Match id, Capybara.test_id attribute, name, or associated label text
2018-05-29 16:02:53 -07:00
# * Filters:
# * :id (String) — Matches the id attribute
# * :name (String) — Matches the name attribute
# * :class (String, Array<String>) — Matches the class(es) provided
# * :checked (Boolean) — Match checked fields?
# * :unchecked (Boolean) — Match unchecked fields?
# * :disabled (Boolean) — Match disabled field?
# * :option (String) — Match the value
#
# * **:checkbox** - Find checkboxes
2018-07-05 15:29:24 -07:00
# * Locator: Match id, Capybara.test_id attribute, name, or associated label text
2018-05-29 16:02:53 -07:00
# * Filters:
# * *:id (String) — Matches the id attribute
# * *:name (String) — Matches the name attribute
# * *:class (String, Array<String>) — Matches the class(es) provided
# * *:checked (Boolean) — Match checked fields?
# * *:unchecked (Boolean) — Match unchecked fields?
# * *:disabled (Boolean) — Match disabled field?
# * *:option (String) — Match the value
#
# * **:select** - Find select elements
2018-07-05 15:29:24 -07:00
# * Locator: Match id, Capybara.test_id attribute, name, placeholder, or associated label text
2018-05-29 16:02:53 -07:00
# * Filters:
# * :id (String) — Matches the id attribute
# * :name (String) — Matches the name attribute
# * :placeholder (String) — Matches the placeholder attribute
# * :class (String, Array<String>) — Matches the class(es) provided
# * :disabled (Boolean) — Match disabled field?
# * :multiple (Boolean) — Match fields that accept multiple values
# * :options (Array<String>) — Exact match options
# * :with_options (Array<String>) — Partial match options
# * :selected (String, Array<String>) — Match the selection(s)
# * :with_selected (String, Array<String>) — Partial match the selection(s)
#
# * **:option** - Find option elements
# * Locator: Match text of option
# * Filters:
# * :disabled (Boolean) — Match disabled option
# * :selected (Boolean) — Match selected option
#
# * **:datalist_input**
# * Locator:
# * Filters:
# * :disabled
# * :name
# * :placeholder
#
# * **:datalist_option**
# * Locator:
#
# * **:file_field** - Find file input elements
2018-07-05 15:29:24 -07:00
# * Locator: Match id, Capybara.test_id attribute, name, or associated label text
2018-05-29 16:02:53 -07:00
# * Filters:
# * :id (String) — Matches the id attribute
# * :name (String) — Matches the name attribute
# * :class (String, Array<String>) — Matches the class(es) provided
# * :disabled (Boolean) — Match disabled field?
# * :multiple (Boolean) — Match field that accepts multiple values
#
# * **:label** - Find label elements
# * Locator: Match id or text contents
# * Filters:
# * :for (Element, String) — The element or id of the element associated with the label
#
# * **:table** - Find table elements
# * Locator: id or caption text of table
# * Filters:
# * :id (String) — Match id attribute of table
# * :caption (String) — Match text of associated caption
# * :class (String, Array<String>) — Matches the class(es) provided
#
# * **:frame** - Find frame/iframe elements
# * Locator: Match id or name
# * Filters:
# * :id (String) — Match id attribute
# * :name (String) — Match name attribute
# * :class (String, Array<String>) — Matches the class(es) provided
#
# * **:element**
# * Locator: Type of element ('div', 'a', etc) - if not specified defaults to '*'
# * Filters: Matches on any element attribute
#
2016-08-18 13:27:35 -07:00
class Selector
2016-10-05 15:16:00 -07:00
attr_reader :name , :format
2018-05-24 15:27:34 -07:00
extend Forwardable
2016-08-18 13:27:35 -07:00
class << self
def all
2018-03-16 09:46:35 -07:00
@selectors || = { } # rubocop:disable Naming/MemoizedInstanceVariableName
2016-08-18 13:27:35 -07:00
end
def add ( name , & block )
all [ name . to_sym ] = Capybara :: Selector . new ( name . to_sym , & block )
end
def update ( name , & block )
all [ name . to_sym ] . instance_eval ( & block )
end
def remove ( name )
all . delete ( name . to_sym )
end
end
def initialize ( name , & block )
@name = name
2018-01-09 14:05:50 -08:00
@filter_set = FilterSet . add ( name ) { }
2016-08-18 13:27:35 -07:00
@match = nil
@label = nil
@failure_message = nil
@description = nil
@format = nil
@expression = nil
2016-10-05 15:16:00 -07:00
@expression_filters = { }
2016-10-02 18:28:52 -07:00
@default_visibility = nil
2018-07-19 10:06:04 -07:00
@config = {
enable_aria_label : false ,
test_id : nil
}
2016-08-18 13:27:35 -07:00
instance_eval ( & block )
end
def custom_filters
2018-05-31 17:00:20 -07:00
warn " Deprecated: Selector # custom_filters is not valid when same named expression and node filter exist - don't use "
2018-05-24 15:27:34 -07:00
node_filters . merge ( expression_filters ) . freeze
2016-08-18 13:27:35 -07:00
end
2016-10-05 15:16:00 -07:00
def node_filters
@filter_set . node_filters
end
def expression_filters
@filter_set . expression_filters
end
2016-09-07 00:34:15 -07:00
##
#
# Define a selector by an xpath expression
#
# @overload xpath(*expression_filters, &block)
# @param [Array<Symbol>] expression_filters ([]) Names of filters that can be implemented via this expression
# @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
#
2018-05-24 15:27:34 -07:00
def xpath ( * allowed_filters , & block )
2016-10-05 15:16:00 -07:00
if block
@format , @expression = :xpath , block
2018-06-07 12:45:50 -07:00
allowed_filters . flatten . each { | ef | expression_filters [ ef ] = Filters :: IdentityExpressionFilter . new ( ef ) }
2016-10-05 15:16:00 -07:00
end
2016-08-18 13:27:35 -07:00
format == :xpath ? @expression : nil
end
2016-09-07 00:34:15 -07:00
##
#
# 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
#
2018-05-24 15:27:34 -07:00
def css ( * allowed_filters , & block )
2016-10-05 15:16:00 -07:00
if block
@format , @expression = :css , block
2018-05-24 15:27:34 -07:00
allowed_filters . flatten . each { | ef | expression_filters [ ef ] = nil }
2016-10-05 15:16:00 -07:00
end
2016-08-18 13:27:35 -07:00
format == :css ? @expression : nil
end
2016-09-07 00:34:15 -07:00
##
#
# 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
#
2016-08-18 13:27:35 -07:00
def match ( & block )
@match = block if block
@match
end
2016-09-07 00:34:15 -07:00
##
#
# 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
#
2018-01-09 14:05:50 -08:00
def label ( label = nil )
2016-08-18 13:27:35 -07:00
@label = label if label
@label
end
2016-09-07 00:34:15 -07:00
##
#
# Description of the selector
#
2018-05-29 16:02:53 -07:00
# @!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
2018-05-24 15:27:34 -07:00
def_delegator :@filter_set , :description
2016-08-18 13:27:35 -07:00
2018-07-19 10:06:04 -07:00
def call ( locator , selector_config : { } , ** options )
@config . merge! selector_config
2016-08-18 13:27:35 -07:00
if format
@expression . call ( locator , options )
else
2018-07-10 14:18:39 -07:00
warn 'Selector has no format'
2016-08-18 13:27:35 -07:00
end
end
2016-09-07 00:34:15 -07:00
##
#
# 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
#
2016-08-18 13:27:35 -07:00
def match? ( locator )
2018-05-10 13:20:23 -07:00
@match & . call ( locator )
2016-08-18 13:27:35 -07:00
end
2016-09-07 00:34:15 -07:00
##
#
2018-05-24 15:27:34 -07:00
# Define a node filter for use with this selector
2016-09-08 11:01:22 -07:00
#
2018-05-29 16:02:53 -07:00
# @!method node_filter(name, *types, options={}, &block)
2018-05-24 15:27:34 -07:00
# @param [Symbol, Regexp] name The filter name
2016-09-08 11:01:22 -07:00
# @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
2018-05-26 10:26:44 -07:00
# @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.
2016-09-07 00:34:15 -07:00
#
2018-05-24 15:27:34 -07:00
# 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
2016-08-18 13:27:35 -07:00
2018-05-29 16:02:53 -07:00
# @!method filter
# See {Selector#node_filter}
2018-05-24 15:27:34 -07:00
##
#
# Define an expression filter for use with this selector
#
2018-08-16 13:49:12 -07:00
# @!method expression_filter(name, *types, matcher: nil, **options, &block)
2018-05-24 15:27:34 -07:00
# @param [Symbol, Regexp] name The filter name
2018-05-26 10:26:44 -07:00
# @param [Regexp] matcher (nil) A Regexp used to check whether a specific option is handled by this filter
2018-05-24 15:27:34 -07:00
# @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
2018-05-26 10:26:44 -07:00
# @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.
2018-05-24 15:27:34 -07:00
#
# 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
2018-05-29 14:02:03 -07:00
def_delegators :@filter_set , :node_filter , :expression_filter , :filter
2016-10-05 15:16:00 -07:00
2016-08-18 13:27:35 -07:00
def filter_set ( name , filters_to_use = nil )
2018-07-17 12:26:59 -07:00
@filter_set . import ( name , filters_to_use )
2016-08-18 13:27:35 -07:00
end
2018-05-24 15:27:34 -07:00
def_delegator :@filter_set , :describe
2016-08-18 13:27:35 -07:00
2018-07-12 12:32:41 -07:00
def describe_expression_filters ( & block )
if block_given?
2018-07-17 12:26:59 -07:00
describe ( :expression_filters , & block )
2018-07-12 12:32:41 -07:00
else
2018-07-17 12:26:59 -07:00
describe ( :expression_filters ) do | ** options |
2018-07-12 12:32:41 -07:00
describe_all_expression_filters ( options )
end
end
end
def describe_node_filters ( & block )
2018-07-17 12:26:59 -07:00
describe ( :node_filters , & block )
2018-07-12 12:32:41 -07:00
end
2016-10-02 18:28:52 -07:00
##
#
# 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 )
@default_visibility = default_visibility
end
2016-12-15 09:04:01 -08:00
def default_visibility ( fallback = Capybara . ignore_hidden_elements )
2018-05-31 17:00:20 -07:00
return @default_visibility unless @default_visibility . nil?
fallback
2016-10-02 18:28:52 -07:00
end
2018-01-09 14:05:50 -08:00
private
2016-08-18 13:27:35 -07:00
2018-07-19 10:06:04 -07:00
def enable_aria_label
@config [ :enable_aria_label ]
end
def test_id
@config [ :test_id ]
end
def locate_field ( xpath , locator , ** _options )
2018-05-31 17:00:20 -07:00
return xpath if locator . nil?
2018-01-09 14:05:50 -08:00
locate_xpath = xpath # Need to save original xpath for the label wrap
2018-05-31 17:00:20 -07:00
locator = locator . to_s
attr_matchers = [ XPath . attr ( :id ) == locator ,
XPath . attr ( :name ) == locator ,
XPath . attr ( :placeholder ) == locator ,
XPath . attr ( :id ) == XPath . anywhere ( :label ) [ XPath . string . n . is ( locator ) ] . attr ( :for ) ] . reduce ( :| )
attr_matchers |= XPath . attr ( :'aria-label' ) . is ( locator ) if enable_aria_label
2018-07-19 09:50:21 -07:00
attr_matchers |= XPath . attr ( test_id ) == locator if test_id
2018-05-31 17:00:20 -07:00
locate_xpath = locate_xpath [ attr_matchers ]
locate_xpath + XPath . descendant ( :label ) [ XPath . string . n . is ( locator ) ] . descendant ( xpath )
2016-09-07 00:34:15 -07:00
end
2017-05-01 18:39:08 -07:00
def describe_all_expression_filters ( ** opts )
2018-05-26 10:26:44 -07:00
expression_filters . map do | ef_name , ef |
if ef . matcher?
2018-05-24 15:27:34 -07:00
opts . keys . map do | k |
2018-05-26 10:26:44 -07:00
" with #{ ef_name } [ #{ k } => #{ opts [ k ] } ] " if ef . handles_option? ( k ) && ! :: Capybara :: Queries :: SelectorQuery :: VALID_KEYS . include? ( k )
2018-05-24 15:27:34 -07:00
end . join
elsif opts . key? ( ef_name )
" with #{ ef_name } #{ opts [ ef_name ] } "
end
end . join
2016-11-21 16:28:45 -08:00
end
2016-09-07 00:34:15 -07:00
def find_by_attr ( attribute , value )
2016-11-21 16:28:45 -08:00
finder_name = " find_by_ #{ attribute } _attr "
2016-09-07 00:34:15 -07:00
if respond_to? ( finder_name , true )
send ( finder_name , value )
else
2018-01-11 16:45:50 -08:00
value ? XPath . attr ( attribute ) == value : nil
2016-09-07 00:34:15 -07:00
end
end
def find_by_class_attr ( classes )
2018-01-13 13:06:03 -08:00
Array ( classes ) . map { | klass | XPath . attr ( :class ) . contains_word ( klass ) } . reduce ( :& )
2016-08-18 13:27:35 -07:00
end
end
end
2018-05-29 16:46:04 -07:00
# rubocop:enable Style/AsciiComments