capybara 3.35.0 → 3.40.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (184) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +168 -5
  3. data/README.md +199 -39
  4. data/lib/capybara/config.rb +16 -4
  5. data/lib/capybara/driver/base.rb +4 -0
  6. data/lib/capybara/driver/node.rb +5 -1
  7. data/lib/capybara/dsl.rb +4 -10
  8. data/lib/capybara/helpers.rb +9 -14
  9. data/lib/capybara/minitest/spec.rb +18 -6
  10. data/lib/capybara/minitest.rb +14 -1
  11. data/lib/capybara/node/actions.rb +14 -9
  12. data/lib/capybara/node/base.rb +2 -1
  13. data/lib/capybara/node/document.rb +2 -2
  14. data/lib/capybara/node/element.rb +13 -2
  15. data/lib/capybara/node/finders.rb +11 -2
  16. data/lib/capybara/node/matchers.rb +25 -0
  17. data/lib/capybara/node/simple.rb +5 -1
  18. data/lib/capybara/node/whitespace_normalizer.rb +81 -0
  19. data/lib/capybara/queries/active_element_query.rb +18 -0
  20. data/lib/capybara/queries/ancestor_query.rb +2 -1
  21. data/lib/capybara/queries/base_query.rb +2 -2
  22. data/lib/capybara/queries/current_path_query.rb +1 -1
  23. data/lib/capybara/queries/selector_query.rb +40 -11
  24. data/lib/capybara/queries/sibling_query.rb +2 -1
  25. data/lib/capybara/queries/text_query.rb +1 -1
  26. data/lib/capybara/rack_test/browser.rb +64 -8
  27. data/lib/capybara/rack_test/driver.rb +4 -4
  28. data/lib/capybara/rack_test/form.rb +29 -7
  29. data/lib/capybara/rack_test/node.rb +32 -33
  30. data/lib/capybara/registration_container.rb +2 -5
  31. data/lib/capybara/registrations/drivers.rb +7 -7
  32. data/lib/capybara/registrations/servers.rb +37 -16
  33. data/lib/capybara/result.rb +2 -2
  34. data/lib/capybara/rspec/matcher_proxies.rb +6 -6
  35. data/lib/capybara/rspec/matchers/base.rb +8 -6
  36. data/lib/capybara/rspec/matchers/compound.rb +1 -1
  37. data/lib/capybara/rspec/matchers/have_selector.rb +9 -17
  38. data/lib/capybara/rspec/matchers.rb +21 -16
  39. data/lib/capybara/selector/builders/css_builder.rb +1 -1
  40. data/lib/capybara/selector/builders/xpath_builder.rb +1 -1
  41. data/lib/capybara/selector/css.rb +6 -6
  42. data/lib/capybara/selector/definition/button.rb +10 -5
  43. data/lib/capybara/selector/definition/checkbox.rb +1 -1
  44. data/lib/capybara/selector/definition/file_field.rb +1 -1
  45. data/lib/capybara/selector/definition/fillable_field.rb +1 -1
  46. data/lib/capybara/selector/definition/link.rb +2 -1
  47. data/lib/capybara/selector/definition/radio_button.rb +1 -1
  48. data/lib/capybara/selector/definition/table.rb +1 -1
  49. data/lib/capybara/selector/definition/table_row.rb +2 -2
  50. data/lib/capybara/selector/definition.rb +4 -2
  51. data/lib/capybara/selector/filter_set.rb +4 -7
  52. data/lib/capybara/selector/regexp_disassembler.rb +2 -5
  53. data/lib/capybara/selector/selector.rb +5 -1
  54. data/lib/capybara/selector.rb +252 -0
  55. data/lib/capybara/selenium/driver.rb +31 -54
  56. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +1 -1
  57. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +9 -5
  58. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +2 -7
  59. data/lib/capybara/selenium/extensions/html5_drag.rb +5 -4
  60. data/lib/capybara/selenium/node.rb +60 -38
  61. data/lib/capybara/selenium/nodes/chrome_node.rb +4 -16
  62. data/lib/capybara/selenium/nodes/edge_node.rb +19 -13
  63. data/lib/capybara/selenium/nodes/firefox_node.rb +3 -3
  64. data/lib/capybara/selenium/nodes/safari_node.rb +4 -4
  65. data/lib/capybara/selenium/patches/atoms.rb +1 -1
  66. data/lib/capybara/selenium/patches/pause_duration_fix.rb +1 -1
  67. data/lib/capybara/server/animation_disabler.rb +40 -23
  68. data/lib/capybara/server/middleware.rb +1 -1
  69. data/lib/capybara/server.rb +1 -1
  70. data/lib/capybara/session/config.rb +4 -2
  71. data/lib/capybara/session.rb +34 -34
  72. data/lib/capybara/spec/public/test.js +4 -0
  73. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  74. data/lib/capybara/spec/session/all_spec.rb +11 -15
  75. data/lib/capybara/spec/session/assert_text_spec.rb +17 -17
  76. data/lib/capybara/spec/session/attach_file_spec.rb +6 -0
  77. data/lib/capybara/spec/session/check_spec.rb +10 -0
  78. data/lib/capybara/spec/session/choose_spec.rb +6 -0
  79. data/lib/capybara/spec/session/click_link_or_button_spec.rb +9 -0
  80. data/lib/capybara/spec/session/click_link_spec.rb +12 -1
  81. data/lib/capybara/spec/session/current_scope_spec.rb +1 -1
  82. data/lib/capybara/spec/session/fill_in_spec.rb +6 -0
  83. data/lib/capybara/spec/session/find_link_spec.rb +10 -0
  84. data/lib/capybara/spec/session/find_spec.rb +15 -1
  85. data/lib/capybara/spec/session/first_spec.rb +1 -1
  86. data/lib/capybara/spec/session/frame/within_frame_spec.rb +2 -0
  87. data/lib/capybara/spec/session/has_all_selectors_spec.rb +5 -5
  88. data/lib/capybara/spec/session/has_ancestor_spec.rb +2 -2
  89. data/lib/capybara/spec/session/has_any_selectors_spec.rb +6 -2
  90. data/lib/capybara/spec/session/has_button_spec.rb +30 -0
  91. data/lib/capybara/spec/session/has_current_path_spec.rb +3 -3
  92. data/lib/capybara/spec/session/has_element_spec.rb +47 -0
  93. data/lib/capybara/spec/session/has_field_spec.rb +25 -1
  94. data/lib/capybara/spec/session/has_link_spec.rb +40 -0
  95. data/lib/capybara/spec/session/has_none_selectors_spec.rb +7 -7
  96. data/lib/capybara/spec/session/has_select_spec.rb +10 -4
  97. data/lib/capybara/spec/session/has_selector_spec.rb +15 -0
  98. data/lib/capybara/spec/session/has_table_spec.rb +13 -2
  99. data/lib/capybara/spec/session/has_text_spec.rb +6 -14
  100. data/lib/capybara/spec/session/matches_style_spec.rb +2 -0
  101. data/lib/capybara/spec/session/node_spec.rb +88 -1
  102. data/lib/capybara/spec/session/node_wrapper_spec.rb +1 -1
  103. data/lib/capybara/spec/session/reset_session_spec.rb +13 -0
  104. data/lib/capybara/spec/session/scroll_spec.rb +7 -5
  105. data/lib/capybara/spec/session/uncheck_spec.rb +1 -1
  106. data/lib/capybara/spec/session/visit_spec.rb +20 -0
  107. data/lib/capybara/spec/session/window/window_spec.rb +1 -1
  108. data/lib/capybara/spec/session/window/windows_spec.rb +1 -1
  109. data/lib/capybara/spec/session/within_spec.rb +13 -0
  110. data/lib/capybara/spec/spec_helper.rb +12 -5
  111. data/lib/capybara/spec/test_app.rb +91 -14
  112. data/lib/capybara/spec/views/animated.erb +1 -1
  113. data/lib/capybara/spec/views/form.erb +34 -4
  114. data/lib/capybara/spec/views/frame_child.erb +1 -1
  115. data/lib/capybara/spec/views/frame_one.erb +1 -1
  116. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  117. data/lib/capybara/spec/views/frame_two.erb +1 -1
  118. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  119. data/lib/capybara/spec/views/layout.erb +10 -0
  120. data/lib/capybara/spec/views/obscured.erb +1 -1
  121. data/lib/capybara/spec/views/offset.erb +2 -1
  122. data/lib/capybara/spec/views/path.erb +2 -2
  123. data/lib/capybara/spec/views/popup_one.erb +1 -1
  124. data/lib/capybara/spec/views/popup_two.erb +1 -1
  125. data/lib/capybara/spec/views/react.erb +2 -2
  126. data/lib/capybara/spec/views/scroll.erb +2 -1
  127. data/lib/capybara/spec/views/spatial.erb +1 -1
  128. data/lib/capybara/spec/views/with_animation.erb +2 -3
  129. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  130. data/lib/capybara/spec/views/with_dragula.erb +2 -2
  131. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  132. data/lib/capybara/spec/views/with_hover.erb +2 -2
  133. data/lib/capybara/spec/views/with_html.erb +5 -3
  134. data/lib/capybara/spec/views/with_jquery_animation.erb +1 -1
  135. data/lib/capybara/spec/views/with_js.erb +2 -3
  136. data/lib/capybara/spec/views/with_jstree.erb +1 -1
  137. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  138. data/lib/capybara/spec/views/with_scope.erb +2 -2
  139. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  140. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  141. data/lib/capybara/spec/views/with_sortable_js.erb +2 -2
  142. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  143. data/lib/capybara/spec/views/with_windows.erb +1 -1
  144. data/lib/capybara/spec/views/within_frames.erb +1 -1
  145. data/lib/capybara/version.rb +1 -1
  146. data/lib/capybara/window.rb +1 -1
  147. data/lib/capybara.rb +30 -30
  148. data/spec/basic_node_spec.rb +16 -3
  149. data/spec/capybara_spec.rb +12 -0
  150. data/spec/counter_spec.rb +35 -0
  151. data/spec/css_builder_spec.rb +1 -1
  152. data/spec/css_splitter_spec.rb +1 -1
  153. data/spec/dsl_spec.rb +5 -3
  154. data/spec/fixtures/selenium_driver_rspec_failure.rb +2 -2
  155. data/spec/fixtures/selenium_driver_rspec_success.rb +2 -2
  156. data/spec/minitest_spec.rb +12 -1
  157. data/spec/minitest_spec_spec.rb +4 -0
  158. data/spec/per_session_config_spec.rb +1 -1
  159. data/spec/rack_test_spec.rb +30 -12
  160. data/spec/result_spec.rb +41 -35
  161. data/spec/rspec/features_spec.rb +3 -3
  162. data/spec/rspec/scenarios_spec.rb +2 -2
  163. data/spec/rspec/shared_spec_matchers.rb +27 -3
  164. data/spec/rspec_matchers_spec.rb +25 -0
  165. data/spec/rspec_spec.rb +3 -3
  166. data/spec/sauce_spec_chrome.rb +5 -5
  167. data/spec/selector_spec.rb +4 -4
  168. data/spec/selenium_spec_chrome.rb +20 -18
  169. data/spec/selenium_spec_chrome_remote.rb +15 -19
  170. data/spec/selenium_spec_edge.rb +19 -6
  171. data/spec/selenium_spec_firefox.rb +26 -8
  172. data/spec/selenium_spec_firefox_remote.rb +18 -4
  173. data/spec/selenium_spec_ie.rb +7 -8
  174. data/spec/selenium_spec_safari.rb +34 -20
  175. data/spec/server_spec.rb +19 -7
  176. data/spec/shared_selenium_node.rb +0 -4
  177. data/spec/shared_selenium_session.rb +22 -14
  178. data/spec/spec_helper.rb +36 -3
  179. data/spec/whitespace_normalizer_spec.rb +54 -0
  180. data/spec/xpath_builder_spec.rb +1 -1
  181. metadata +49 -30
  182. data/lib/capybara/selenium/logger_suppressor.rb +0 -34
  183. data/lib/capybara/selenium/patches/action_pauser.rb +0 -26
  184. data/lib/capybara/spec/views/with_title.erb +0 -5
data/lib/capybara/dsl.rb CHANGED
@@ -47,17 +47,11 @@ module Capybara
47
47
  end
48
48
 
49
49
  Session::DSL_METHODS.each do |method|
50
- if RUBY_VERSION >= '2.7'
51
- class_eval <<~METHOD, __FILE__, __LINE__ + 1
52
- def #{method}(...)
53
- page.method("#{method}").call(...)
54
- end
55
- METHOD
56
- else
57
- define_method method do |*args, &block|
58
- page.send method, *args, &block
50
+ class_eval <<~METHOD, __FILE__, __LINE__ + 1
51
+ def #{method}(...)
52
+ page.method("#{method}").call(...)
59
53
  end
60
- end
54
+ METHOD
61
55
  end
62
56
  end
63
57
 
@@ -73,28 +73,23 @@ module Capybara
73
73
  def filter_backtrace(trace)
74
74
  return 'No backtrace' unless trace
75
75
 
76
- filter = %r{lib/capybara/|lib/rspec/|lib/minitest/}
76
+ filter = %r{lib/capybara/|lib/rspec/|lib/minitest/|delegate.rb}
77
77
  new_trace = trace.take_while { |line| line !~ filter }
78
- new_trace = trace.reject { |line| line =~ filter } if new_trace.empty?
78
+ new_trace = trace.grep_v(filter) if new_trace.empty?
79
79
  new_trace = trace.dup if new_trace.empty?
80
80
 
81
- new_trace.first.split(/:in /, 2).first
81
+ new_trace.first.split(':in ', 2).first
82
82
  end
83
83
 
84
84
  def warn(message, uplevel: 1)
85
- return Kernel.warn(message, uplevel: uplevel) if RUBY_VERSION >= '2.6'
86
-
87
- # TODO: Remove when we drop support for Ruby 2.5
88
- # Workaround for emulating `warn '...', uplevel: n` in Ruby 2.5 or lower.
89
- if (match = /^(?<file>.+?):(?<line>\d+)(?::in `.*')?/.match(caller[uplevel]))
90
- location = [match[:file], match[:line]].join(':')
91
- Kernel.warn "#{location}: #{message}"
92
- else
93
- Kernel.warn message
94
- end
85
+ Kernel.warn(message, uplevel: uplevel)
95
86
  end
96
87
 
97
- if defined?(Process::CLOCK_MONOTONIC)
88
+ if defined?(Process::CLOCK_MONOTONIC_RAW)
89
+ def monotonic_time; Process.clock_gettime Process::CLOCK_MONOTONIC_RAW; end
90
+ elsif defined?(Process::CLOCK_MONOTONIC_PRECISE)
91
+ def monotonic_time; Process.clock_gettime Process::CLOCK_MONOTONIC_PRECISE; end
92
+ elsif defined?(Process::CLOCK_MONOTONIC)
98
93
  def monotonic_time; Process.clock_gettime Process::CLOCK_MONOTONIC; end
99
94
  else
100
95
  def monotonic_time; Time.now.to_f; end
@@ -95,6 +95,18 @@ module Capybara
95
95
  # @!method wont_have_field
96
96
  # See {Capybara::Node::Matchers#has_no_field?}
97
97
 
98
+ ##
99
+ # Expectation that there is element
100
+ #
101
+ # @!method must_have_element
102
+ # See {Capybara::Node::Matchers#has_element?}
103
+
104
+ ##
105
+ # Expectation that there is no element
106
+ #
107
+ # @!method wont_have_element
108
+ # See {Capybara::Node::Matchers#has_no_element?}
109
+
98
110
  ##
99
111
  # Expectation that there is link
100
112
  #
@@ -230,15 +242,15 @@ module Capybara
230
242
  %W[refute_matches_#{assertion} wont_match_#{assertion}]]
231
243
  end).each do |(meth, new_name)|
232
244
  class_eval <<-ASSERTION, __FILE__, __LINE__ + 1
233
- def #{new_name} *args, **kw_args, &block
234
- ::Minitest::Expectation.new(self, ::Minitest::Spec.current).#{new_name}(*args, **kw_args, &block)
245
+ def #{new_name}(...)
246
+ ::Minitest::Expectation.new(self, ::Minitest::Spec.current).#{new_name}(...)
235
247
  end
236
248
  ASSERTION
237
249
 
238
250
  ::Minitest::Expectation.class_eval <<-ASSERTION, __FILE__, __LINE__ + 1
239
- def #{new_name} *args, **kw_args, &block
251
+ def #{new_name}(...)
240
252
  raise "Calling ##{new_name} outside of test." unless ctx
241
- ctx.#{meth}(target, *args, **kw_args, &block)
253
+ ctx.#{meth}(target, ...)
242
254
  end
243
255
  ASSERTION
244
256
  end
@@ -246,9 +258,9 @@ module Capybara
246
258
 
247
259
  ##
248
260
  # @deprecated
249
- def must_have_style(*args, **kw_args, &block)
261
+ def must_have_style(...)
250
262
  warn 'must_have_style is deprecated, please use must_match_style'
251
- must_match_style(*args, **kw_args, &block)
263
+ must_match_style(...)
252
264
  end
253
265
  end
254
266
  end
@@ -190,6 +190,19 @@ module Capybara
190
190
  # @!method assert_no_css
191
191
  # See {Capybara::Node::Matchers#has_no_css?}
192
192
 
193
+ ##
194
+ # Assert that provided element exists
195
+ #
196
+ # @!method assert_element
197
+ # See {Capybara::Node::Matchers#has_element?}
198
+
199
+ ##
200
+ # Assert that provided element does not exist
201
+ #
202
+ # @!method assert_no_element
203
+ # @!method refute_element
204
+ # See {Capybara::Node::Matchers#has_no_element?}
205
+
193
206
  ##
194
207
  # Assert that provided link exists
195
208
  #
@@ -281,7 +294,7 @@ module Capybara
281
294
  # @!method assert_no_table
282
295
  # See {Capybara::Node::Matchers#has_no_table?}
283
296
 
284
- %w[xpath css link button field select table].each do |selector_type|
297
+ %w[xpath css element link button field select table].each do |selector_type|
285
298
  define_method "assert_#{selector_type}" do |*args, &optional_filter_block|
286
299
  subject, args = determine_subject(args)
287
300
  locator, options = extract_locator(args)
@@ -92,8 +92,9 @@ module Capybara
92
92
  end
93
93
 
94
94
  # @!macro label_click
95
- # @option options [Boolean] allow_label_click
95
+ # @option options [Boolean, Hash] allow_label_click
96
96
  # Attempt to click the label to toggle state if element is non-visible. Defaults to {Capybara.configure automatic_label_click}.
97
+ # If set to a Hash it is passed as options to the `click` on the label
97
98
 
98
99
  ##
99
100
  #
@@ -277,7 +278,7 @@ module Capybara
277
278
  # @return [Capybara::Node::Element] The file field element
278
279
  def attach_file(locator = nil, paths, make_visible: nil, **options) # rubocop:disable Style/OptionalArguments
279
280
  if locator && block_given?
280
- raise ArgumentError, '``#attach_file` does not support passing both a locator and a block'
281
+ raise ArgumentError, '`#attach_file` does not support passing both a locator and a block'
281
282
  end
282
283
 
283
284
  Array(paths).each do |path|
@@ -314,7 +315,7 @@ module Capybara
314
315
 
315
316
  begin
316
317
  find(:datalist_input, from, **options)
317
- rescue Capybara::ElementNotFound => dlinput_error # rubocop:disable Naming/RescuedExceptionsVariableName
318
+ rescue Capybara::ElementNotFound => dlinput_error
318
319
  raise Capybara::ElementNotFound, "#{select_error.message} and #{dlinput_error.message}"
319
320
  end
320
321
  end
@@ -371,14 +372,18 @@ module Capybara
371
372
 
372
373
  begin
373
374
  el ||= find(selector, locator, **options.merge(visible: :all))
374
- el.session.find(:label, for: el, visible: true, match: :first).click unless el.checked? == checked
375
+ unless el.checked? == checked
376
+ el.session
377
+ .find(:label, for: el, visible: true, match: :first)
378
+ .click(**(Hash.try_convert(allow_label_click) || {}))
379
+ end
375
380
  rescue StandardError # swallow extra errors - raise original
376
381
  raise e
377
382
  end
378
383
  end
379
384
  end
380
385
 
381
- UPDATE_STYLE_SCRIPT = <<~'JS'
386
+ UPDATE_STYLE_SCRIPT = <<~JS
382
387
  this.capybara_style_cache = this.style.cssText;
383
388
  var css = arguments[0];
384
389
  for (var prop in css){
@@ -388,27 +393,27 @@ module Capybara
388
393
  }
389
394
  JS
390
395
 
391
- RESET_STYLE_SCRIPT = <<~'JS'
396
+ RESET_STYLE_SCRIPT = <<~JS
392
397
  if (this.hasOwnProperty('capybara_style_cache')) {
393
398
  this.style.cssText = this.capybara_style_cache;
394
399
  delete this.capybara_style_cache;
395
400
  }
396
401
  JS
397
402
 
398
- DATALIST_OPTIONS_SCRIPT = <<~'JS'
403
+ DATALIST_OPTIONS_SCRIPT = <<~JS
399
404
  Array.prototype.slice.call((this.list||{}).options || []).
400
405
  filter(function(el){ return !el.disabled }).
401
406
  map(function(el){ return { "value": el.value, "label": el.label} })
402
407
  JS
403
408
 
404
- CAPTURE_FILE_ELEMENT_SCRIPT = <<~'JS'
409
+ CAPTURE_FILE_ELEMENT_SCRIPT = <<~JS
405
410
  document.addEventListener('click', function file_catcher(e){
406
411
  if (e.target.matches("input[type='file']")) {
407
412
  window._capybara_clicked_file_input = e.target;
408
413
  this.removeEventListener('click', file_catcher);
409
414
  e.preventDefault();
410
415
  }
411
- })
416
+ }, {capture: true})
412
417
  JS
413
418
  end
414
419
  end
@@ -77,6 +77,7 @@ module Capybara
77
77
  return yield if session.synchronized
78
78
 
79
79
  seconds = session_options.default_max_wait_time if [nil, true].include? seconds
80
+ interval = session_options.default_retry_interval
80
81
  session.synchronized = true
81
82
  timer = Capybara::Helpers.timer(expire_in: seconds)
82
83
  begin
@@ -88,7 +89,7 @@ module Capybara
88
89
  if driver.wait?
89
90
  raise e if timer.expired?
90
91
 
91
- sleep(0.01)
92
+ sleep interval
92
93
  reload if session_options.automatic_reload
93
94
  else
94
95
  old_base = @base
@@ -40,8 +40,8 @@ module Capybara
40
40
  find(:xpath, '/html').evaluate_script(*args)
41
41
  end
42
42
 
43
- def scroll_to(*args, **options)
44
- find(:xpath, '//body').scroll_to(*args, **options)
43
+ def scroll_to(*args, quirks: false, **options)
44
+ find(:xpath, quirks ? '//body' : '/html').scroll_to(*args, **options)
45
45
  end
46
46
  end
47
47
  end
@@ -115,7 +115,7 @@ module Capybara
115
115
  #
116
116
  # @return [Capybara::Node::Element] The element
117
117
  def set(value, **options)
118
- if ENV['CAPYBARA_THOROUGH'] && readonly?
118
+ if ENV.fetch('CAPYBARA_THOROUGH', nil) && readonly?
119
119
  raise Capybara::ReadOnlyElementError, "Attempt to set readonly element with value: #{value}"
120
120
  end
121
121
 
@@ -472,6 +472,17 @@ module Capybara
472
472
  self
473
473
  end
474
474
 
475
+ ##
476
+ #
477
+ # Return the shadow_root for the current element
478
+ #
479
+ # @return [Capybara::Node::Element] The shadow root
480
+
481
+ def shadow_root
482
+ root = synchronize { base.shadow_root }
483
+ root && Capybara::Node::Element.new(session, root, nil, nil)
484
+ end
485
+
475
486
  ##
476
487
  #
477
488
  # Execute the given JS in the context of the element not returning a result. This is useful for scripts that return
@@ -550,7 +561,7 @@ module Capybara
550
561
  return self unless @allow_reload
551
562
 
552
563
  begin
553
- reloaded = @query.resolve_for(query_scope.reload)[@query_idx.to_i]
564
+ reloaded = @query.resolve_for(query_scope ? query_scope.reload : session)[@query_idx.to_i]
554
565
  @base = reloaded.base if reloaded
555
566
  rescue StandardError => e
556
567
  raise e unless catch_error?(e)
@@ -18,11 +18,11 @@ module Capybara
18
18
  #
19
19
  # @!macro system_filters
20
20
  # @option options [String, Regexp] text Only find elements which contain this text or match this regexp
21
- # @option options [String, Boolean] exact_text
21
+ # @option options [String, Regexp, String] exact_text
22
22
  # When String the elements contained text must match exactly, when Boolean controls whether the `text` option must match exactly.
23
23
  # Defaults to {Capybara.configure exact_text}.
24
24
  # @option options [Boolean] normalize_ws
25
- # Whether the `text`/`exact_text` options are compared against elment text with whitespace normalized or as returned by the driver.
25
+ # Whether the `text`/`exact_text` options are compared against element text with whitespace normalized or as returned by the driver.
26
26
  # Defaults to {Capybara.configure default_normalize_ws}.
27
27
  # @option options [Boolean, Symbol] visible
28
28
  # Only find elements with the specified visibility. Defaults to behavior indicated by {Capybara.configure ignore_hidden_elements}.
@@ -50,6 +50,13 @@ module Capybara
50
50
  #
51
51
  def find(*args, **options, &optional_filter_block)
52
52
  options[:session_options] = session_options
53
+ count_options = options.slice(*Capybara::Queries::BaseQuery::COUNT_KEYS)
54
+ unless count_options.empty?
55
+ Capybara::Helpers.warn(
56
+ "'find' does not support count options (#{count_options}) ignoring. " \
57
+ "Called from: #{Capybara::Helpers.filter_backtrace(caller)}"
58
+ )
59
+ end
53
60
  synced_resolve Capybara::Queries::SelectorQuery.new(*args, **options, &optional_filter_block)
54
61
  end
55
62
 
@@ -142,6 +149,8 @@ module Capybara
142
149
  # @option options [String, Regexp] id Match links with the id provided
143
150
  # @option options [String] title Match links with the title provided
144
151
  # @option options [String] alt Match links with a contained img element whose alt matches
152
+ # @option options [String, Boolean] download Match links with the download provided
153
+ # @option options [String] target Match links with the target provided
145
154
  # @option options [String, Array<String>, Regexp] class Match links that match the class(es) provided
146
155
  # @return [Capybara::Node::Element] The found element
147
156
  #
@@ -322,6 +322,31 @@ module Capybara
322
322
  has_no_selector?(:css, path, **options, &optional_filter_block)
323
323
  end
324
324
 
325
+ ##
326
+ #
327
+ # Checks if the page or current node has a element with the given
328
+ # local name.
329
+ #
330
+ # @param [String] locator The local name of a element to check for
331
+ # @option options [String, Regexp] The attributes values of matching elements
332
+ # @return [Boolean] Whether it exists
333
+ #
334
+ def has_element?(locator = nil, **options, &optional_filter_block)
335
+ has_selector?(:element, locator, **options, &optional_filter_block)
336
+ end
337
+
338
+ ##
339
+ #
340
+ # Checks if the page or current node has no element with the given
341
+ # local name.
342
+ #
343
+ # @param (see #has_element?)
344
+ # @return [Boolean] Whether it doesn't exist
345
+ #
346
+ def has_no_element?(locator = nil, **options, &optional_filter_block)
347
+ has_no_selector?(:element, locator, **options, &optional_filter_block)
348
+ end
349
+
325
350
  ##
326
351
  #
327
352
  # Checks if the page or current node has a link with the given
@@ -110,7 +110,7 @@ module Capybara
110
110
  # No need for an xpath if only checking the current element
111
111
  !(native.key?('hidden') ||
112
112
  /display:\s?none/.match?(native[:style] || '') ||
113
- %w[script head].include?(tag_name))
113
+ %w[script head style].include?(tag_name))
114
114
  end
115
115
  end
116
116
 
@@ -191,6 +191,10 @@ module Capybara
191
191
  {}
192
192
  end
193
193
 
194
+ def ==(other)
195
+ eql?(other) || (other.respond_to?(:native) && native == other.native)
196
+ end
197
+
194
198
  private
195
199
 
196
200
  def option_value(option)
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Node
5
+ ##
6
+ #
7
+ # {Capybara::Node::WhitespaceNormalizer} provides methods that
8
+ # help to normalize the spacing of text content inside of
9
+ # {Capybara::Node::Element}s by removing various unicode
10
+ # spacing and directional markings.
11
+ #
12
+ module WhitespaceNormalizer
13
+ # Unicode for NBSP, or &nbsp;
14
+ NON_BREAKING_SPACE = "\u00a0"
15
+ LINE_SEPERATOR = "\u2028"
16
+ PARAGRAPH_SEPERATOR = "\u2029"
17
+
18
+ # All spaces except for NBSP
19
+ BREAKING_SPACES = "[[:space:]&&[^#{NON_BREAKING_SPACE}]]".freeze
20
+
21
+ # Whitespace we want to substitute with plain spaces
22
+ SQUEEZED_SPACES = " \n\f\t\v#{LINE_SEPERATOR}#{PARAGRAPH_SEPERATOR}".freeze
23
+
24
+ # Any whitespace at the front of text
25
+ LEADING_SPACES = /\A#{BREAKING_SPACES}+/
26
+
27
+ # Any whitespace at the end of text
28
+ TRAILING_SPACES = /#{BREAKING_SPACES}+\z/
29
+
30
+ # "Invisible" space character
31
+ ZERO_WIDTH_SPACE = "\u200b"
32
+
33
+ # Signifies text is read left to right
34
+ LEFT_TO_RIGHT_MARK = "\u200e"
35
+
36
+ # Signifies text is read right to left
37
+ RIGHT_TO_LEFT_MARK = "\u200f"
38
+
39
+ # Characters we want to truncate from text
40
+ REMOVED_CHARACTERS = [ZERO_WIDTH_SPACE, LEFT_TO_RIGHT_MARK, RIGHT_TO_LEFT_MARK].join
41
+
42
+ # Matches multiple empty lines
43
+ EMPTY_LINES = /[\ \n]*\n[\ \n]*/
44
+
45
+ ##
46
+ #
47
+ # Normalizes the spacing of a node's text to be similar to
48
+ # what matchers might expect.
49
+ #
50
+ # @param text [String]
51
+ # @return [String]
52
+ #
53
+ def normalize_spacing(text)
54
+ text
55
+ .delete(REMOVED_CHARACTERS)
56
+ .tr(SQUEEZED_SPACES, ' ')
57
+ .squeeze(' ')
58
+ .sub(LEADING_SPACES, '')
59
+ .sub(TRAILING_SPACES, '')
60
+ .tr(NON_BREAKING_SPACE, ' ')
61
+ end
62
+
63
+ ##
64
+ #
65
+ # Variant on {Capybara::Node::Normalizer#normalize_spacing} that
66
+ # targets the whitespace of visible elements only.
67
+ #
68
+ # @param text [String]
69
+ # @return [String]
70
+ #
71
+ def normalize_visible_spacing(text)
72
+ text
73
+ .squeeze(' ')
74
+ .gsub(EMPTY_LINES, "\n")
75
+ .sub(LEADING_SPACES, '')
76
+ .sub(TRAILING_SPACES, '')
77
+ .tr(NON_BREAKING_SPACE, ' ')
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ # @api private
5
+ module Queries
6
+ class ActiveElementQuery < BaseQuery
7
+ def initialize(**options)
8
+ @options = options
9
+ super(@options)
10
+ end
11
+
12
+ def resolve_for(session)
13
+ node = session.driver.active_element
14
+ [Capybara::Node::Element.new(session, node, nil, self)]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -8,7 +8,8 @@ module Capybara
8
8
  @child_node = node
9
9
 
10
10
  node.synchronize do
11
- match_results = super(node.session.current_scope, exact)
11
+ scope = node.respond_to?(:session) ? node.session.current_scope : node.find(:xpath, '/*')
12
+ match_results = super(scope, exact)
12
13
  ancestors = node.find_xpath(XPath.ancestor.to_s)
13
14
  .map(&method(:to_element))
14
15
  .select { |el| match_results.include?(el) }
@@ -79,8 +79,8 @@ module Capybara
79
79
  if count
80
80
  message << " #{occurrences count}"
81
81
  elsif between
82
- message << " between #{between.begin ? between.first : 1} and" \
83
- " #{between.end ? between.last : 'infinite'} times"
82
+ message << " between #{between.begin ? between.first : 1} and " \
83
+ "#{between.end ? between.last : 'infinite'} times"
84
84
  elsif maximum
85
85
  message << " at most #{occurrences maximum}"
86
86
  elsif minimum
@@ -19,7 +19,7 @@ module Capybara
19
19
 
20
20
  def resolves_for?(session)
21
21
  uri = ::Addressable::URI.parse(session.current_url)
22
- @actual_path = (options[:ignore_query] ? uri&.omit(:query) : uri).yield_self do |u|
22
+ @actual_path = (options[:ignore_query] ? uri&.omit(:query) : uri).then do |u|
23
23
  options[:url] ? u&.to_s : u&.request_uri
24
24
  end
25
25
 
@@ -9,7 +9,7 @@ module Capybara
9
9
 
10
10
  SPATIAL_KEYS = %i[above below left_of right_of near].freeze
11
11
  VALID_KEYS = SPATIAL_KEYS + COUNT_KEYS +
12
- %i[text id class style visible obscured exact exact_text normalize_ws match wait filter_set]
12
+ %i[text id class style visible obscured exact exact_text normalize_ws match wait filter_set focused]
13
13
  VALID_MATCH = %i[first smart prefer_exact one].freeze
14
14
 
15
15
  def initialize(*args,
@@ -27,6 +27,12 @@ module Capybara
27
27
  @order = order
28
28
  @filter_cache = Hash.new { |hsh, key| hsh[key] = {} }
29
29
 
30
+ if @options[:text].is_a?(Regexp) && [true, false].include?(@options[:exact_text])
31
+ Capybara::Helpers.warn(
32
+ "Boolean 'exact_text' option is not supported when 'text' option is a Regexp - ignoring"
33
+ )
34
+ end
35
+
30
36
  super(@options)
31
37
  self.session_options = session_options
32
38
 
@@ -64,7 +70,8 @@ module Capybara
64
70
  desc << 'non-visible ' if visible == :hidden
65
71
  end
66
72
 
67
- desc << "#{label} #{locator.inspect}"
73
+ desc << label.to_s
74
+ desc << " #{locator.inspect}" unless locator.nil?
68
75
 
69
76
  if show_for[:any]
70
77
  desc << " with#{' exact' if exact_text == true} text #{options[:text].inspect}" if options[:text]
@@ -73,6 +80,8 @@ module Capybara
73
80
 
74
81
  desc << " with id #{options[:id]}" if options[:id]
75
82
  desc << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
83
+ desc << ' that is focused' if options[:focused]
84
+ desc << ' that is not focused' if options[:focused] == false
76
85
 
77
86
  desc << case options[:style]
78
87
  when String
@@ -264,7 +273,7 @@ module Capybara
264
273
  end
265
274
 
266
275
  def valid_keys
267
- VALID_KEYS + custom_keys
276
+ (VALID_KEYS + custom_keys).uniq
268
277
  end
269
278
 
270
279
  def matches_node_filters?(node, errors)
@@ -371,6 +380,10 @@ module Capybara
371
380
  options.key?(:style) && !custom_keys.include?(:style)
372
381
  end
373
382
 
383
+ def use_default_focused_filter?
384
+ options.key?(:focused) && !custom_keys.include?(:focused)
385
+ end
386
+
374
387
  def use_spatial_filter?
375
388
  options.values_at(*SPATIAL_KEYS).compact.any?
376
389
  end
@@ -435,6 +448,7 @@ module Capybara
435
448
  matches_id_filter?(node) &&
436
449
  matches_class_filter?(node) &&
437
450
  matches_style_filter?(node) &&
451
+ matches_focused_filter?(node) &&
438
452
  matches_text_filter?(node) &&
439
453
  matches_exact_text_filter?(node)
440
454
  end
@@ -494,6 +508,12 @@ module Capybara
494
508
  end
495
509
  end
496
510
 
511
+ def matches_focused_filter?(node)
512
+ return true unless use_default_focused_filter?
513
+
514
+ (node == node.session.active_element) == options[:focused]
515
+ end
516
+
497
517
  def need_to_process_classes?
498
518
  case options[:class]
499
519
  when Regexp then true
@@ -528,16 +548,19 @@ module Capybara
528
548
  def matches_text_filter?(node)
529
549
  value = options[:text]
530
550
  return true unless value
531
- return matches_text_exactly?(node, value) if exact_text == true
551
+ return matches_text_exactly?(node, value) if exact_text == true && !value.is_a?(Regexp)
532
552
 
533
553
  regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s)
534
554
  matches_text_regexp?(node, regexp)
535
555
  end
536
556
 
537
557
  def matches_exact_text_filter?(node)
538
- return true unless exact_text.is_a?(String)
539
-
540
- matches_text_exactly?(node, exact_text)
558
+ case exact_text
559
+ when String, Regexp
560
+ matches_text_exactly?(node, exact_text)
561
+ else
562
+ true
563
+ end
541
564
  end
542
565
 
543
566
  def matches_visibility_filters?(node)
@@ -548,7 +571,9 @@ module Capybara
548
571
  when :visible
549
572
  node.initial_cache[:visible] || (node.initial_cache[:visible].nil? && node.visible?)
550
573
  when :hidden
551
- (node.initial_cache[:visible] == false) || (node.initial_cache[:visbile].nil? && !node.visible?)
574
+ # TODO: check why the 'visbile' cache spelling mistake wasn't caught in a test
575
+ # (node.initial_cache[:visible] == false) || (node.initial_cache[:visbile].nil? && !node.visible?)
576
+ (node.initial_cache[:visible] == false) || (node.initial_cache[:visible].nil? && !node.visible?)
552
577
  else
553
578
  true
554
579
  end
@@ -565,17 +590,21 @@ module Capybara
565
590
 
566
591
  def matches_text_exactly?(node, value)
567
592
  regexp = value.is_a?(Regexp) ? value : /\A#{Regexp.escape(value.to_s)}\z/
568
- matches_text_regexp?(node, regexp)
593
+ matches_text_regexp(node, regexp).then { |m| m&.pre_match == '' && m&.post_match == '' }
569
594
  end
570
595
 
571
596
  def normalize_ws
572
597
  options.fetch(:normalize_ws, session_options.default_normalize_ws)
573
598
  end
574
599
 
575
- def matches_text_regexp?(node, regexp)
600
+ def matches_text_regexp(node, regexp)
576
601
  text_visible = visible
577
602
  text_visible = :all if text_visible == :hidden
578
- node.text(text_visible, normalize_ws: normalize_ws).match?(regexp)
603
+ node.text(text_visible, normalize_ws: normalize_ws).match(regexp)
604
+ end
605
+
606
+ def matches_text_regexp?(node, regexp)
607
+ !matches_text_regexp(node, regexp).nil?
579
608
  end
580
609
 
581
610
  def default_visibility
@@ -7,7 +7,8 @@ module Capybara
7
7
  def resolve_for(node, exact = nil)
8
8
  @sibling_node = node
9
9
  node.synchronize do
10
- match_results = super(node.session.current_scope, exact)
10
+ scope = node.respond_to?(:session) ? node.session.current_scope : node.find(:xpath, '/*')
11
+ match_results = super(scope, exact)
11
12
  siblings = node.find_xpath((XPath.preceding_sibling + XPath.following_sibling).to_s)
12
13
  .map(&method(:to_element))
13
14
  .select { |el| match_results.include?(el) }
@@ -13,7 +13,7 @@ module Capybara
13
13
  self.session_options = session_options
14
14
 
15
15
  if expected_text.nil? && !exact?
16
- warn 'Checking for expected text of nil is confusing and/or pointless since it will always match. '\
16
+ warn 'Checking for expected text of nil is confusing and/or pointless since it will always match. ' \
17
17
  "Please specify a string or regexp instead. #{Capybara::Helpers.filter_backtrace(caller)}"
18
18
  end
19
19