1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
|
# frozen_string_literal: true
module Capybara
class Selector < SimpleDelegator
class << self
def all
@definitions ||= {} # rubocop:disable Naming/MemoizedInstanceVariableName
end
def [](name)
all.fetch(name.to_sym) { |sel_type| raise ArgumentError, "Unknown selector type (:#{sel_type})" }
end
def add(name, **options, &block)
all[name.to_sym] = Definition.new(name.to_sym, **options, &block)
end
def update(name, &block)
self[name].instance_eval(&block)
end
def remove(name)
all.delete(name.to_sym)
end
def for(locator)
all.values.find { |sel| sel.match?(locator) }
end
end
attr_reader :errors
def initialize(definition, config:, format:)
definition = self.class[definition] unless definition.is_a? Definition
super(definition)
@definition = definition
@config = config
@format = format
@errors = []
end
def format
@format || @definition.default_format
end
alias_method :current_format, :format
def enable_aria_label
@config[:enable_aria_label]
end
def enable_aria_role
@config[:enable_aria_role]
end
def test_id
@config[:test_id]
end
def call(locator, **options)
if format
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
ensure
unless locator_valid?(locator)
Capybara::Helpers.warn(
"Locator #{locator.class}:#{locator.inspect} for selector #{name.inspect} must #{locator_description}. " \
'This will raise an error in a future version of Capybara. ' \
"Called from: #{Capybara::Helpers.filter_backtrace(caller)}"
)
end
end
def add_error(error_msg)
errors << error_msg
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
when :css
Capybara::Selector::CSSBuilder
when :xpath
Capybara::Selector::XPathBuilder
else
raise NotImplementedError, "No builder exists for selector of type #{default_format}"
end.new(expr)
end
private
def locator_description
locator_types.group_by { |lt| lt.is_a? Symbol }.map do |symbol, types_or_methods|
if symbol
"respond to #{types_or_methods.join(' or ')}"
else
"be an instance of #{types_or_methods.join(' or ')}"
end
end.join(' or ')
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 locate_field(xpath, locator, **_options)
return xpath if locator.nil?
locate_xpath = xpath # Need to save original xpath for the label wrap
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
attr_matchers |= XPath.attr(test_id) == locator if test_id
locate_xpath = locate_xpath[attr_matchers]
locate_xpath + locate_label(locator).descendant(xpath)
end
def locate_label(locator)
XPath.descendant(:label)[XPath.string.n.is(locator)]
end
def find_by_attr(attribute, value)
finder_name = "find_by_#{attribute}_attr"
if respond_to?(finder_name, true)
send(finder_name, value)
else
value ? XPath.attr(attribute) == value : nil
end
end
def find_by_class_attr(classes)
Array(classes).map { |klass| XPath.attr(:class).contains_word(klass) }.reduce(:&)
end
end
end
|