capybara 3.18.0 → 3.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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