File: definition.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 (280 lines) | stat: -rw-r--r-- 11,021 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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# 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 determine 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
          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_options(ef, opts).map { |option, value| " with #{ef_name}[#{option} => #{value}]" }.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 should be used if no visible 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_options(filter, options)
        options.select do |option, _|
          filter.handles_option?(option) && !::Capybara::Queries::SelectorQuery::VALID_KEYS.include?(option)
        end
      end

      def parameter_names(block)
        key_types = %i[key keyreq]
        # user filter_map when we drop dupport for 2.6
        # block.parameters.select { |(type, _name)| key_types.include? type }.map { |(_, name)| name }
        block.parameters.filter_map { |(type, name)| name if key_types.include? type }
      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