File: selector.rb

package info (click to toggle)
ruby-capybara 3.40.0%2Bds-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,368 kB
  • sloc: ruby: 23,988; javascript: 752; makefile: 11
file content (159 lines) | stat: -rw-r--r-- 4,489 bytes parent folder | download
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