capybara 3.18.0 → 3.19.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +16 -0
  3. data/README.md +14 -44
  4. data/lib/capybara/node/actions.rb +2 -2
  5. data/lib/capybara/node/element.rb +3 -5
  6. data/lib/capybara/queries/selector_query.rb +30 -11
  7. data/lib/capybara/rack_test/node.rb +1 -1
  8. data/lib/capybara/result.rb +2 -0
  9. data/lib/capybara/rspec/matcher_proxies.rb +2 -0
  10. data/lib/capybara/rspec/matchers/base.rb +2 -2
  11. data/lib/capybara/rspec/matchers/count_sugar.rb +36 -0
  12. data/lib/capybara/rspec/matchers/have_selector.rb +3 -0
  13. data/lib/capybara/rspec/matchers/have_text.rb +3 -0
  14. data/lib/capybara/selector.rb +196 -599
  15. data/lib/capybara/selector/css.rb +2 -0
  16. data/lib/capybara/selector/definition.rb +276 -0
  17. data/lib/capybara/selector/definition/button.rb +46 -0
  18. data/lib/capybara/selector/definition/checkbox.rb +23 -0
  19. data/lib/capybara/selector/definition/css.rb +5 -0
  20. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  21. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  22. data/lib/capybara/selector/definition/element.rb +27 -0
  23. data/lib/capybara/selector/definition/field.rb +40 -0
  24. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  25. data/lib/capybara/selector/definition/file_field.rb +13 -0
  26. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  27. data/lib/capybara/selector/definition/frame.rb +17 -0
  28. data/lib/capybara/selector/definition/id.rb +6 -0
  29. data/lib/capybara/selector/definition/label.rb +43 -0
  30. data/lib/capybara/selector/definition/link.rb +45 -0
  31. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  32. data/lib/capybara/selector/definition/option.rb +27 -0
  33. data/lib/capybara/selector/definition/radio_button.rb +24 -0
  34. data/lib/capybara/selector/definition/select.rb +62 -0
  35. data/lib/capybara/selector/definition/table.rb +106 -0
  36. data/lib/capybara/selector/definition/table_row.rb +21 -0
  37. data/lib/capybara/selector/definition/xpath.rb +5 -0
  38. data/lib/capybara/selector/filters/base.rb +4 -0
  39. data/lib/capybara/selector/filters/locator_filter.rb +12 -2
  40. data/lib/capybara/selector/selector.rb +40 -452
  41. data/lib/capybara/selenium/driver.rb +4 -10
  42. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +3 -9
  43. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +8 -0
  44. data/lib/capybara/selenium/extensions/find.rb +1 -1
  45. data/lib/capybara/selenium/logger_suppressor.rb +5 -0
  46. data/lib/capybara/selenium/node.rb +19 -13
  47. data/lib/capybara/selenium/nodes/chrome_node.rb +30 -0
  48. data/lib/capybara/selenium/nodes/firefox_node.rb +14 -12
  49. data/lib/capybara/selenium/nodes/ie_node.rb +11 -0
  50. data/lib/capybara/selenium/nodes/safari_node.rb +7 -12
  51. data/lib/capybara/server/checker.rb +7 -3
  52. data/lib/capybara/session.rb +2 -2
  53. data/lib/capybara/spec/session/all_spec.rb +1 -1
  54. data/lib/capybara/spec/session/find_spec.rb +1 -1
  55. data/lib/capybara/spec/session/first_spec.rb +1 -1
  56. data/lib/capybara/spec/session/has_css_spec.rb +7 -0
  57. data/lib/capybara/spec/session/has_text_spec.rb +6 -0
  58. data/lib/capybara/spec/session/save_screenshot_spec.rb +11 -0
  59. data/lib/capybara/spec/session/select_spec.rb +0 -5
  60. data/lib/capybara/spec/test_app.rb +8 -3
  61. data/lib/capybara/version.rb +1 -1
  62. data/lib/capybara/window.rb +1 -1
  63. data/spec/minitest_spec_spec.rb +1 -0
  64. data/spec/selector_spec.rb +12 -6
  65. data/spec/selenium_spec_firefox.rb +0 -3
  66. data/spec/selenium_spec_firefox_remote.rb +0 -3
  67. data/spec/selenium_spec_ie.rb +3 -1
  68. data/spec/server_spec.rb +1 -1
  69. data/spec/shared_selenium_session.rb +1 -1
  70. data/spec/spec_helper.rb +9 -2
  71. metadata +54 -2
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'capybara/selector/selector'
4
+
3
5
  module Capybara
4
6
  class Selector
5
7
  class CSS
@@ -0,0 +1,276 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/selector/filter_set'
4
+ require 'capybara/selector/css'
5
+ require 'capybara/selector/regexp_disassembler'
6
+ require 'capybara/selector/builders/xpath_builder'
7
+ require 'capybara/selector/builders/css_builder'
8
+
9
+ module Capybara
10
+ class Selector
11
+ class Definition
12
+ attr_reader :name, :expressions
13
+ extend Forwardable
14
+
15
+ def initialize(name, locator_type: nil, raw_locator: false, supports_exact: nil, &block)
16
+ @name = name
17
+ @filter_set = Capybara::Selector::FilterSet.add(name) {}
18
+ @match = nil
19
+ @label = nil
20
+ @failure_message = nil
21
+ @expressions = {}
22
+ @expression_filters = {}
23
+ @locator_filter = nil
24
+ @default_visibility = nil
25
+ @locator_type = locator_type
26
+ @raw_locator = raw_locator
27
+ @supports_exact = supports_exact
28
+ instance_eval(&block)
29
+ end
30
+
31
+ def custom_filters
32
+ warn "Deprecated: Selector#custom_filters is not valid when same named expression and node filter exist - don't use"
33
+ node_filters.merge(expression_filters).freeze
34
+ end
35
+
36
+ def node_filters
37
+ @filter_set.node_filters
38
+ end
39
+
40
+ def expression_filters
41
+ @filter_set.expression_filters
42
+ end
43
+
44
+ ##
45
+ #
46
+ # Define a selector by an xpath expression
47
+ #
48
+ # @overload xpath(*expression_filters, &block)
49
+ # @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
50
+ # @yield [locator, options] The block to use to generate the XPath expression
51
+ # @yieldparam [String] locator The locator string passed to the query
52
+ # @yieldparam [Hash] options The options hash passed to the query
53
+ # @yieldreturn [#to_xpath, #to_s] An object that can produce an xpath expression
54
+ #
55
+ # @overload xpath()
56
+ # @return [#call] The block that will be called to generate the XPath expression
57
+ #
58
+ def xpath(*allowed_filters, &block)
59
+ expression(:xpath, allowed_filters, &block)
60
+ end
61
+
62
+ ##
63
+ #
64
+ # Define a selector by a CSS selector
65
+ #
66
+ # @overload css(*expression_filters, &block)
67
+ # @param [Array<Symbol>] expression_filters ([]) Names of filters that can be implemented via this CSS selector
68
+ # @yield [locator, options] The block to use to generate the CSS selector
69
+ # @yieldparam [String] locator The locator string passed to the query
70
+ # @yieldparam [Hash] options The options hash passed to the query
71
+ # @yieldreturn [#to_s] An object that can produce a CSS selector
72
+ #
73
+ # @overload css()
74
+ # @return [#call] The block that will be called to generate the CSS selector
75
+ #
76
+ def css(*allowed_filters, &block)
77
+ expression(:css, allowed_filters, &block)
78
+ end
79
+
80
+ ##
81
+ #
82
+ # Automatic selector detection
83
+ #
84
+ # @yield [locator] This block takes the passed in locator string and returns whether or not it matches the selector
85
+ # @yieldparam [String], locator The locator string used to determin if it matches the selector
86
+ # @yieldreturn [Boolean] Whether this selector matches the locator string
87
+ # @return [#call] The block that will be used to detect selector match
88
+ #
89
+ def match(&block)
90
+ @match = block if block
91
+ @match
92
+ end
93
+
94
+ ##
95
+ #
96
+ # Set/get a descriptive label for the selector
97
+ #
98
+ # @overload label(label)
99
+ # @param [String] label A descriptive label for this selector - used in error messages
100
+ # @overload label()
101
+ # @return [String] The currently set label
102
+ #
103
+ def label(label = nil)
104
+ @label = label if label
105
+ @label
106
+ end
107
+
108
+ ##
109
+ #
110
+ # Description of the selector
111
+ #
112
+ # @!method description(options)
113
+ # @param [Hash] options The options of the query used to generate the description
114
+ # @return [String] Description of the selector when used with the options passed
115
+ def_delegator :@filter_set, :description
116
+
117
+ ##
118
+ #
119
+ # Should this selector be used for the passed in locator
120
+ #
121
+ # This is used by the automatic selector selection mechanism when no selector type is passed to a selector query
122
+ #
123
+ # @param [String] locator The locator passed to the query
124
+ # @return [Boolean] Whether or not to use this selector
125
+ #
126
+ def match?(locator)
127
+ @match&.call(locator)
128
+ end
129
+
130
+ ##
131
+ #
132
+ # Define a node filter for use with this selector
133
+ #
134
+ # @!method node_filter(name, *types, options={}, &block)
135
+ # @param [Symbol, Regexp] name The filter name
136
+ # @param [Array<Symbol>] types The types of the filter - currently valid types are [:boolean]
137
+ # @param [Hash] options ({}) Options of the filter
138
+ # @option options [Array<>] :valid_values Valid values for this filter
139
+ # @option options :default The default value of the filter (if any)
140
+ # @option options :skip_if Value of the filter that will cause it to be skipped
141
+ # @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.
142
+ #
143
+ # If a Symbol is passed for the name the block should accept | node, option_value |, while if a Regexp
144
+ # is passed for the name the block should accept | node, option_name, option_value |. In either case
145
+ # the block should return `true` if the node passes the filer or `false` if it doesn't
146
+
147
+ ##
148
+ #
149
+ # Define an expression filter for use with this selector
150
+ #
151
+ # @!method expression_filter(name, *types, matcher: nil, **options, &block)
152
+ # @param [Symbol, Regexp] name The filter name
153
+ # @param [Regexp] matcher (nil) A Regexp used to check whether a specific option is handled by this filter
154
+ # @param [Array<Symbol>] types The types of the filter - currently valid types are [:boolean]
155
+ # @param [Hash] options ({}) Options of the filter
156
+ # @option options [Array<>] :valid_values Valid values for this filter
157
+ # @option options :default The default value of the filter (if any)
158
+ # @option options :skip_if Value of the filter that will cause it to be skipped
159
+ # @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.
160
+ #
161
+ # If a Symbol is passed for the name the block should accept | current_expression, option_value |, while if a Regexp
162
+ # is passed for the name the block should accept | current_expression, option_name, option_value |. In either case
163
+ # the block should return the modified expression
164
+
165
+ def_delegators :@filter_set, :node_filter, :expression_filter, :filter
166
+
167
+ def locator_filter(*types, **options, &block)
168
+ types.each { |type| options[type] = true }
169
+ @locator_filter = Capybara::Selector::Filters::LocatorFilter.new(block, options) if block
170
+ @locator_filter
171
+ end
172
+
173
+ def filter_set(name, filters_to_use = nil)
174
+ @filter_set.import(name, filters_to_use)
175
+ end
176
+
177
+ def_delegator :@filter_set, :describe
178
+
179
+ def describe_expression_filters(&block)
180
+ if block_given?
181
+ describe(:expression_filters, &block)
182
+ else
183
+ describe(:expression_filters) do |**options|
184
+ describe_all_expression_filters(options)
185
+ end
186
+ end
187
+ end
188
+
189
+ def describe_all_expression_filters(**opts)
190
+ expression_filters.map do |ef_name, ef|
191
+ if ef.matcher?
192
+ handled_custom_keys(ef, opts.keys).map { |key| " with #{ef_name}[#{key} => #{opts[key]}]" }.join
193
+ elsif opts.key?(ef_name)
194
+ " with #{ef_name} #{opts[ef_name]}"
195
+ end
196
+ end.join
197
+ end
198
+
199
+ def describe_node_filters(&block)
200
+ describe(:node_filters, &block)
201
+ end
202
+
203
+ ##
204
+ #
205
+ # Set the default visibility mode that shouble be used if no visibile option is passed when using the selector.
206
+ # If not specified will default to the behavior indicated by Capybara.ignore_hidden_elements
207
+ #
208
+ # @param [Symbol] default_visibility Only find elements with the specified visibility:
209
+ # * :all - finds visible and invisible elements.
210
+ # * :hidden - only finds invisible elements.
211
+ # * :visible - only finds visible elements.
212
+ def visible(default_visibility = nil, &block)
213
+ @default_visibility = block || default_visibility
214
+ end
215
+
216
+ def default_visibility(fallback = Capybara.ignore_hidden_elements, options = {})
217
+ vis = if @default_visibility&.respond_to?(:call)
218
+ @default_visibility.call(options)
219
+ else
220
+ @default_visibility
221
+ end
222
+ vis.nil? ? fallback : vis
223
+ end
224
+
225
+ # @api private
226
+ def raw_locator?
227
+ !!@raw_locator
228
+ end
229
+
230
+ # @api private
231
+ def supports_exact?
232
+ @supports_exact
233
+ end
234
+
235
+ def default_format
236
+ return nil if @expressions.keys.empty?
237
+
238
+ if @expressions.size == 1
239
+ @expressions.keys.first
240
+ else
241
+ :xpath
242
+ end
243
+ end
244
+
245
+ # @api private
246
+ def locator_types
247
+ return nil unless @locator_type
248
+
249
+ Array(@locator_type)
250
+ end
251
+
252
+ private
253
+
254
+ def handled_custom_keys(filter, keys)
255
+ keys.select do |key|
256
+ filter.handles_option?(key) && !::Capybara::Queries::SelectorQuery::VALID_KEYS.include?(key)
257
+ end
258
+ end
259
+
260
+ def parameter_names(block)
261
+ block.parameters.select { |(type, _name)| %i[key keyreq].include? type }.map { |(_type, name)| name }
262
+ end
263
+
264
+ def expression(type, allowed_filters, &block)
265
+ if block
266
+ @expressions[type] = block
267
+ allowed_filters = parameter_names(block) if allowed_filters.empty?
268
+ allowed_filters.flatten.each do |ef|
269
+ expression_filters[ef] = Capybara::Selector::Filters::IdentityExpressionFilter.new(ef)
270
+ end
271
+ end
272
+ @expressions[type]
273
+ end
274
+ end
275
+ end
276
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:button, locator_type: [String, Symbol]) do
4
+ xpath(:value, :title, :type, :name) do |locator, **options|
5
+ input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
6
+ btn_xpath = XPath.descendant(:button)
7
+ image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type) == 'image']
8
+
9
+ unless locator.nil?
10
+ locator = locator.to_s
11
+ locator_matchers = XPath.attr(:id).equals(locator) |
12
+ XPath.attr(:name).equals(locator) |
13
+ XPath.attr(:value).is(locator) |
14
+ XPath.attr(:title).is(locator)
15
+ locator_matchers |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
16
+ locator_matchers |= XPath.attr(test_id) == locator if test_id
17
+
18
+ input_btn_xpath = input_btn_xpath[locator_matchers]
19
+
20
+ btn_xpath = btn_xpath[locator_matchers |
21
+ XPath.string.n.is(locator) |
22
+ XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]
23
+
24
+ alt_matches = XPath.attr(:alt).is(locator)
25
+ alt_matches |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
26
+ image_btn_xpath = image_btn_xpath[alt_matches]
27
+ end
28
+
29
+ %i[value title type name].inject(input_btn_xpath.union(btn_xpath).union(image_btn_xpath)) do |memo, ef|
30
+ memo[find_by_attr(ef, options[ef])]
31
+ end
32
+ end
33
+
34
+ node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
35
+ expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
36
+
37
+ describe_expression_filters do |disabled: nil, **options|
38
+ desc = +''
39
+ desc << ' that is not disabled' if disabled == false
40
+ desc << describe_all_expression_filters(options)
41
+ end
42
+
43
+ describe_node_filters do |disabled: nil, **|
44
+ ' that is disabled' if disabled == true
45
+ end
46
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:checkbox, locator_type: [String, Symbol]) do
4
+ xpath do |locator, allow_self: nil, **options|
5
+ xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
6
+ XPath.attr(:type) == 'checkbox'
7
+ ]
8
+ locate_field(xpath, locator, options)
9
+ end
10
+
11
+ filter_set(:_field, %i[checked unchecked disabled name])
12
+
13
+ node_filter(:option) do |node, value|
14
+ val = node.value
15
+ (val == value.to_s).tap do |res|
16
+ add_error("Expected option value to be #{value.inspect} but it was #{val.inspect}") unless res
17
+ end
18
+ end
19
+
20
+ describe_node_filters do |option: nil, **|
21
+ " with value #{option.inspect}" if option
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:css, locator_type: [String, Symbol], raw_locator: true) do
4
+ css { |css| css }
5
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:datalist_input, locator_type: [String, Symbol]) do
4
+ label 'input box with datalist completion'
5
+
6
+ xpath do |locator, **options|
7
+ xpath = XPath.descendant(:input)[XPath.attr(:list)]
8
+ locate_field(xpath, locator, options)
9
+ end
10
+
11
+ filter_set(:_field, %i[disabled name placeholder])
12
+
13
+ node_filter(:options) do |node, options|
14
+ actual = node.find("//datalist[@id=#{node[:list]}]", visible: :all).all(:datalist_option, wait: false).map(&:value)
15
+ (options.sort == actual.sort).tap do |res|
16
+ add_error("Expected #{options.inspect} options found #{actual.inspect}") unless res
17
+ end
18
+ end
19
+
20
+ expression_filter(:with_options) do |expr, options|
21
+ options.inject(expr) do |xpath, option|
22
+ xpath[XPath.attr(:list) == XPath.anywhere(:datalist)[expression_for(:datalist_option, option)].attr(:id)]
23
+ end
24
+ end
25
+
26
+ describe_expression_filters do |with_options: nil, **|
27
+ desc = +''
28
+ desc << " with at least options #{with_options.inspect}" if with_options
29
+ desc
30
+ end
31
+
32
+ describe_node_filters do |options: nil, **|
33
+ " with options #{options.inspect}" if options
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:datalist_option, locator_type: [String, Symbol]) do
4
+ label 'datalist option'
5
+ visible(:all)
6
+
7
+ xpath do |locator|
8
+ xpath = XPath.descendant(:option)
9
+ xpath = xpath[XPath.string.n.is(locator.to_s) | (XPath.attr(:value) == locator.to_s)] unless locator.nil?
10
+ xpath
11
+ end
12
+
13
+ node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
14
+ expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
15
+
16
+ describe_expression_filters do |disabled: nil, **options|
17
+ desc = +''
18
+ desc << ' that is not disabled' if disabled == false
19
+ desc << describe_all_expression_filters(options)
20
+ end
21
+
22
+ describe_node_filters do |**options|
23
+ ' that is disabled' if options[:disabled]
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:element, locator_type: [String, Symbol]) do
4
+ xpath do |locator, **|
5
+ XPath.descendant.where(locator ? XPath.local_name == locator.to_s : nil)
6
+ end
7
+
8
+ expression_filter(:attributes, matcher: /.+/) do |xpath, name, val|
9
+ builder(xpath).add_attribute_conditions(name => val)
10
+ end
11
+
12
+ node_filter(:attributes, matcher: /.+/) do |node, name, val|
13
+ next true unless val.is_a?(Regexp)
14
+
15
+ (val.match? node[name]).tap do |res|
16
+ add_error("Expected #{name} to match #{val.inspect} but it was #{node[name]}") unless res
17
+ end
18
+ end
19
+
20
+ describe_expression_filters do |**options|
21
+ booleans, values = options.partition { |_k, v| [true, false].include? v }.map(&:to_h)
22
+ desc = describe_all_expression_filters(values)
23
+ desc + booleans.map do |k, v|
24
+ v ? " with #{k} attribute" : "without #{k} attribute"
25
+ end.join
26
+ end
27
+ end