capybara 3.29.0 → 3.37.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (204) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +229 -15
  3. data/README.md +13 -4
  4. data/lib/capybara/config.rb +24 -10
  5. data/lib/capybara/cucumber.rb +1 -1
  6. data/lib/capybara/driver/base.rb +8 -0
  7. data/lib/capybara/driver/node.rb +5 -1
  8. data/lib/capybara/dsl.rb +5 -3
  9. data/lib/capybara/helpers.rb +19 -2
  10. data/lib/capybara/minitest/spec.rb +156 -97
  11. data/lib/capybara/minitest.rb +232 -144
  12. data/lib/capybara/node/actions.rb +41 -37
  13. data/lib/capybara/node/base.rb +6 -6
  14. data/lib/capybara/node/document.rb +2 -2
  15. data/lib/capybara/node/document_matchers.rb +3 -3
  16. data/lib/capybara/node/element.rb +35 -21
  17. data/lib/capybara/node/finders.rb +33 -19
  18. data/lib/capybara/node/matchers.rb +72 -57
  19. data/lib/capybara/node/simple.rb +13 -3
  20. data/lib/capybara/queries/active_element_query.rb +18 -0
  21. data/lib/capybara/queries/ancestor_query.rb +4 -3
  22. data/lib/capybara/queries/base_query.rb +2 -1
  23. data/lib/capybara/queries/current_path_query.rb +14 -4
  24. data/lib/capybara/queries/selector_query.rb +91 -30
  25. data/lib/capybara/queries/sibling_query.rb +4 -3
  26. data/lib/capybara/queries/style_query.rb +1 -1
  27. data/lib/capybara/queries/text_query.rb +7 -1
  28. data/lib/capybara/rack_test/browser.rb +68 -10
  29. data/lib/capybara/rack_test/driver.rb +6 -5
  30. data/lib/capybara/rack_test/form.rb +2 -2
  31. data/lib/capybara/rack_test/node.rb +44 -16
  32. data/lib/capybara/registration_container.rb +41 -0
  33. data/lib/capybara/registrations/drivers.rb +18 -12
  34. data/lib/capybara/registrations/patches/puma_ssl.rb +3 -1
  35. data/lib/capybara/registrations/servers.rb +3 -2
  36. data/lib/capybara/result.rb +35 -15
  37. data/lib/capybara/rspec/matcher_proxies.rb +8 -8
  38. data/lib/capybara/rspec/matchers/base.rb +12 -6
  39. data/lib/capybara/rspec/matchers/count_sugar.rb +2 -1
  40. data/lib/capybara/rspec/matchers/have_ancestor.rb +4 -3
  41. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  42. data/lib/capybara/rspec/matchers/have_selector.rb +16 -8
  43. data/lib/capybara/rspec/matchers/have_sibling.rb +3 -3
  44. data/lib/capybara/rspec/matchers/have_text.rb +3 -3
  45. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  46. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  47. data/lib/capybara/rspec/matchers/match_style.rb +7 -2
  48. data/lib/capybara/rspec/matchers/spatial_sugar.rb +2 -1
  49. data/lib/capybara/rspec/matchers.rb +33 -32
  50. data/lib/capybara/rspec.rb +2 -0
  51. data/lib/capybara/selector/builders/css_builder.rb +2 -2
  52. data/lib/capybara/selector/builders/xpath_builder.rb +4 -2
  53. data/lib/capybara/selector/css.rb +2 -2
  54. data/lib/capybara/selector/definition/button.rb +35 -13
  55. data/lib/capybara/selector/definition/checkbox.rb +3 -3
  56. data/lib/capybara/selector/definition/css.rb +3 -1
  57. data/lib/capybara/selector/definition/datalist_input.rb +2 -2
  58. data/lib/capybara/selector/definition/datalist_option.rb +1 -1
  59. data/lib/capybara/selector/definition/element.rb +3 -2
  60. data/lib/capybara/selector/definition/field.rb +1 -1
  61. data/lib/capybara/selector/definition/file_field.rb +2 -2
  62. data/lib/capybara/selector/definition/fillable_field.rb +3 -3
  63. data/lib/capybara/selector/definition/label.rb +5 -3
  64. data/lib/capybara/selector/definition/link.rb +8 -0
  65. data/lib/capybara/selector/definition/radio_button.rb +3 -3
  66. data/lib/capybara/selector/definition/select.rb +33 -14
  67. data/lib/capybara/selector/definition/table.rb +6 -3
  68. data/lib/capybara/selector/definition/table_row.rb +2 -2
  69. data/lib/capybara/selector/definition.rb +15 -11
  70. data/lib/capybara/selector/filter_set.rb +17 -17
  71. data/lib/capybara/selector/filters/base.rb +6 -1
  72. data/lib/capybara/selector/filters/locator_filter.rb +1 -1
  73. data/lib/capybara/selector/selector.rb +17 -3
  74. data/lib/capybara/selector.rb +37 -19
  75. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -1
  76. data/lib/capybara/selenium/atoms/src/getAttribute.js +1 -1
  77. data/lib/capybara/selenium/atoms/src/isDisplayed.js +1 -1
  78. data/lib/capybara/selenium/driver.rb +84 -17
  79. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +11 -13
  80. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +10 -12
  81. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +4 -4
  82. data/lib/capybara/selenium/extensions/find.rb +4 -4
  83. data/lib/capybara/selenium/extensions/html5_drag.rb +30 -13
  84. data/lib/capybara/selenium/extensions/scroll.rb +8 -10
  85. data/lib/capybara/selenium/logger_suppressor.rb +8 -2
  86. data/lib/capybara/selenium/node.rb +122 -26
  87. data/lib/capybara/selenium/nodes/chrome_node.rb +34 -19
  88. data/lib/capybara/selenium/nodes/edge_node.rb +5 -3
  89. data/lib/capybara/selenium/nodes/firefox_node.rb +11 -6
  90. data/lib/capybara/selenium/nodes/safari_node.rb +3 -3
  91. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  92. data/lib/capybara/selenium/patches/atoms.rb +4 -4
  93. data/lib/capybara/selenium/patches/logs.rb +7 -9
  94. data/lib/capybara/server/animation_disabler.rb +38 -15
  95. data/lib/capybara/server/checker.rb +1 -1
  96. data/lib/capybara/server/middleware.rb +22 -10
  97. data/lib/capybara/server.rb +15 -3
  98. data/lib/capybara/session/config.rb +10 -4
  99. data/lib/capybara/session/matchers.rb +11 -11
  100. data/lib/capybara/session.rb +62 -39
  101. data/lib/capybara/spec/public/test.js +75 -7
  102. data/lib/capybara/spec/session/accept_alert_spec.rb +1 -1
  103. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  104. data/lib/capybara/spec/session/all_spec.rb +63 -12
  105. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  106. data/lib/capybara/spec/session/assert_text_spec.rb +26 -22
  107. data/lib/capybara/spec/session/check_spec.rb +15 -0
  108. data/lib/capybara/spec/session/choose_spec.rb +6 -0
  109. data/lib/capybara/spec/session/click_button_spec.rb +16 -0
  110. data/lib/capybara/spec/session/click_link_or_button_spec.rb +9 -0
  111. data/lib/capybara/spec/session/current_url_spec.rb +11 -1
  112. data/lib/capybara/spec/session/fill_in_spec.rb +29 -0
  113. data/lib/capybara/spec/session/find_spec.rb +37 -8
  114. data/lib/capybara/spec/session/has_any_selectors_spec.rb +4 -0
  115. data/lib/capybara/spec/session/has_button_spec.rb +75 -0
  116. data/lib/capybara/spec/session/has_css_spec.rb +14 -10
  117. data/lib/capybara/spec/session/has_current_path_spec.rb +17 -4
  118. data/lib/capybara/spec/session/has_field_spec.rb +41 -1
  119. data/lib/capybara/spec/session/has_link_spec.rb +30 -0
  120. data/lib/capybara/spec/session/has_select_spec.rb +36 -8
  121. data/lib/capybara/spec/session/has_selector_spec.rb +19 -4
  122. data/lib/capybara/spec/session/has_table_spec.rb +51 -5
  123. data/lib/capybara/spec/session/has_text_spec.rb +21 -1
  124. data/lib/capybara/spec/session/html_spec.rb +1 -1
  125. data/lib/capybara/spec/session/matches_style_spec.rb +2 -2
  126. data/lib/capybara/spec/session/node_spec.rb +226 -33
  127. data/lib/capybara/spec/session/refresh_spec.rb +2 -1
  128. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  129. data/lib/capybara/spec/session/save_page_spec.rb +4 -4
  130. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  131. data/lib/capybara/spec/session/scroll_spec.rb +4 -4
  132. data/lib/capybara/spec/session/selectors_spec.rb +15 -2
  133. data/lib/capybara/spec/session/visit_spec.rb +20 -0
  134. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +1 -1
  135. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -1
  136. data/lib/capybara/spec/session/window/window_spec.rb +9 -9
  137. data/lib/capybara/spec/session/window/windows_spec.rb +1 -1
  138. data/lib/capybara/spec/spec_helper.rb +17 -17
  139. data/lib/capybara/spec/test_app.rb +89 -29
  140. data/lib/capybara/spec/views/animated.erb +1 -1
  141. data/lib/capybara/spec/views/form.erb +52 -6
  142. data/lib/capybara/spec/views/frame_child.erb +1 -1
  143. data/lib/capybara/spec/views/frame_one.erb +1 -1
  144. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  145. data/lib/capybara/spec/views/frame_two.erb +1 -1
  146. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  147. data/lib/capybara/spec/views/layout.erb +10 -0
  148. data/lib/capybara/spec/views/obscured.erb +1 -1
  149. data/lib/capybara/spec/views/offset.erb +2 -1
  150. data/lib/capybara/spec/views/path.erb +2 -2
  151. data/lib/capybara/spec/views/popup_one.erb +1 -1
  152. data/lib/capybara/spec/views/popup_two.erb +1 -1
  153. data/lib/capybara/spec/views/react.erb +2 -2
  154. data/lib/capybara/spec/views/scroll.erb +2 -1
  155. data/lib/capybara/spec/views/spatial.erb +1 -1
  156. data/lib/capybara/spec/views/with_animation.erb +10 -3
  157. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  158. data/lib/capybara/spec/views/with_dragula.erb +5 -3
  159. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  160. data/lib/capybara/spec/views/with_hover.erb +2 -2
  161. data/lib/capybara/spec/views/with_html.erb +3 -3
  162. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  163. data/lib/capybara/spec/views/with_js.erb +5 -3
  164. data/lib/capybara/spec/views/with_jstree.erb +1 -1
  165. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  166. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  167. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  168. data/lib/capybara/spec/views/with_sortable_js.erb +3 -3
  169. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  170. data/lib/capybara/spec/views/with_windows.erb +1 -1
  171. data/lib/capybara/spec/views/within_frames.erb +1 -1
  172. data/lib/capybara/version.rb +1 -1
  173. data/lib/capybara/window.rb +4 -8
  174. data/lib/capybara.rb +36 -29
  175. data/spec/basic_node_spec.rb +25 -11
  176. data/spec/capybara_spec.rb +1 -1
  177. data/spec/dsl_spec.rb +18 -5
  178. data/spec/fixtures/selenium_driver_rspec_failure.rb +2 -2
  179. data/spec/fixtures/selenium_driver_rspec_success.rb +3 -3
  180. data/spec/minitest_spec.rb +3 -2
  181. data/spec/minitest_spec_spec.rb +46 -46
  182. data/spec/rack_test_spec.rb +43 -11
  183. data/spec/regexp_dissassembler_spec.rb +40 -36
  184. data/spec/result_spec.rb +53 -45
  185. data/spec/rspec/features_spec.rb +7 -4
  186. data/spec/rspec/scenarios_spec.rb +5 -1
  187. data/spec/rspec/shared_spec_matchers.rb +68 -56
  188. data/spec/rspec_spec.rb +8 -4
  189. data/spec/sauce_spec_chrome.rb +3 -3
  190. data/spec/selector_spec.rb +19 -4
  191. data/spec/selenium_spec_chrome.rb +49 -26
  192. data/spec/selenium_spec_chrome_remote.rb +13 -6
  193. data/spec/selenium_spec_firefox.rb +29 -17
  194. data/spec/selenium_spec_firefox_remote.rb +2 -2
  195. data/spec/selenium_spec_ie.rb +3 -6
  196. data/spec/selenium_spec_safari.rb +31 -19
  197. data/spec/server_spec.rb +88 -35
  198. data/spec/session_spec.rb +1 -1
  199. data/spec/shared_selenium_node.rb +21 -7
  200. data/spec/shared_selenium_session.rb +123 -21
  201. data/spec/spec_helper.rb +2 -2
  202. metadata +80 -21
  203. data/lib/capybara/spec/session/source_spec.rb +0 -0
  204. data/lib/capybara/spec/views/with_title.erb +0 -5
@@ -6,29 +6,43 @@ module Capybara
6
6
  module Queries
7
7
  class SelectorQuery < Queries::BaseQuery
8
8
  attr_reader :expression, :selector, :locator, :options
9
+
9
10
  SPATIAL_KEYS = %i[above below left_of right_of near].freeze
10
11
  VALID_KEYS = SPATIAL_KEYS + COUNT_KEYS +
11
- %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]
12
13
  VALID_MATCH = %i[first smart prefer_exact one].freeze
13
14
 
14
15
  def initialize(*args,
15
16
  session_options:,
16
17
  enable_aria_label: session_options.enable_aria_label,
18
+ enable_aria_role: session_options.enable_aria_role,
17
19
  test_id: session_options.test_id,
18
20
  selector_format: nil,
21
+ order: nil,
19
22
  **options,
20
23
  &filter_block)
21
24
  @resolved_node = nil
22
25
  @resolved_count = 0
23
26
  @options = options.dup
27
+ @order = order
24
28
  @filter_cache = Hash.new { |hsh, key| hsh[key] = {} }
25
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
+
26
36
  super(@options)
27
37
  self.session_options = session_options
28
38
 
29
39
  @selector = Selector.new(
30
40
  find_selector(args[0].is_a?(Symbol) ? args.shift : args[0]),
31
- config: { enable_aria_label: enable_aria_label, test_id: test_id },
41
+ config: {
42
+ enable_aria_label: enable_aria_label,
43
+ enable_aria_role: enable_aria_role,
44
+ test_id: test_id
45
+ },
32
46
  format: selector_format
33
47
  )
34
48
 
@@ -37,7 +51,7 @@ module Capybara
37
51
 
38
52
  raise ArgumentError, "Unused parameters passed to #{self.class.name} : #{args}" unless args.empty?
39
53
 
40
- @expression = selector.call(@locator, @options)
54
+ @expression = selector.call(@locator, **@options)
41
55
 
42
56
  warn_exact_usage
43
57
 
@@ -47,7 +61,7 @@ module Capybara
47
61
  def name; selector.name; end
48
62
  def label; selector.label || selector.name; end
49
63
 
50
- def description(only_applied = false)
64
+ def description(only_applied = false) # rubocop:disable Style/OptionalBooleanParameter
51
65
  desc = +''
52
66
  show_for = show_for_stage(only_applied)
53
67
 
@@ -65,6 +79,8 @@ module Capybara
65
79
 
66
80
  desc << " with id #{options[:id]}" if options[:id]
67
81
  desc << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
82
+ desc << ' that is focused' if options[:focused]
83
+ desc << ' that is not focused' if options[:focused] == false
68
84
 
69
85
  desc << case options[:style]
70
86
  when String
@@ -77,7 +93,9 @@ module Capybara
77
93
  end
78
94
 
79
95
  %i[above below left_of right_of near].each do |spatial_filter|
80
- desc << " #{spatial_filter} #{options[spatial_filter] rescue '<ERROR>'}" if options[spatial_filter] && show_for[:spatial] # rubocop:disable Style/RescueModifier
96
+ if options[spatial_filter] && show_for[:spatial]
97
+ desc << " #{spatial_filter} #{options[spatial_filter] rescue '<ERROR>'}" # rubocop:disable Style/RescueModifier
98
+ end
81
99
  end
82
100
 
83
101
  desc << selector.description(node_filters: show_for[:node], **options)
@@ -85,11 +103,9 @@ module Capybara
85
103
  desc << ' that also matches the custom filter block' if @filter_block && show_for[:node]
86
104
 
87
105
  desc << " within #{@resolved_node.inspect}" if describe_within?
88
- if locator.is_a?(String) && locator.start_with?('#', './/', '//')
89
- unless selector.raw_locator?
90
- desc << "\nNote: It appears you may be passing a CSS selector or XPath expression rather than a locator. " \
91
- "Please see the documentation for acceptable locator values.\n\n"
92
- end
106
+ if locator.is_a?(String) && locator.start_with?('#', './/', '//') && !selector.raw_locator?
107
+ desc << "\nNote: It appears you may be passing a CSS selector or XPath expression rather than a locator. " \
108
+ "Please see the documentation for acceptable locator values.\n\n"
93
109
  end
94
110
  desc
95
111
  end
@@ -148,7 +164,7 @@ module Capybara
148
164
 
149
165
  node.synchronize do
150
166
  children = find_nodes_by_selector_format(node, exact).map(&method(:to_element))
151
- Capybara::Result.new(children, self)
167
+ Capybara::Result.new(ordered_results(children), self)
152
168
  end
153
169
  end
154
170
 
@@ -229,17 +245,18 @@ module Capybara
229
245
  hints[:styles] = options[:style] if use_default_style_filter?
230
246
  hints[:position] = true if use_spatial_filter?
231
247
 
232
- if selector_format == :css
233
- if node.method(:find_css).arity != 1
234
- node.find_css(css, **hints)
235
- else
248
+ case selector_format
249
+ when :css
250
+ if node.method(:find_css).arity == 1
236
251
  node.find_css(css)
237
- end
238
- elsif selector_format == :xpath
239
- if node.method(:find_xpath).arity != 1
240
- node.find_xpath(xpath(exact), **hints)
241
252
  else
253
+ node.find_css(css, **hints)
254
+ end
255
+ when :xpath
256
+ if node.method(:find_xpath).arity == 1
242
257
  node.find_xpath(xpath(exact))
258
+ else
259
+ node.find_xpath(xpath(exact), **hints)
243
260
  end
244
261
  else
245
262
  raise ArgumentError, "Unknown format: #{selector_format}"
@@ -311,6 +328,15 @@ module Capybara
311
328
  filters
312
329
  end
313
330
 
331
+ def ordered_results(results)
332
+ case @order
333
+ when :reverse
334
+ results.reverse
335
+ else
336
+ results
337
+ end
338
+ end
339
+
314
340
  def custom_keys
315
341
  @custom_keys ||= node_filters.keys + expression_filters.keys
316
342
  end
@@ -338,7 +364,7 @@ module Capybara
338
364
  conditions[:id] = options[:id] if use_default_id_filter?
339
365
  conditions[:class] = options[:class] if use_default_class_filter?
340
366
  conditions[:style] = options[:style] if use_default_style_filter? && !options[:style].is_a?(Hash)
341
- builder(expr).add_attribute_conditions(conditions)
367
+ builder(expr).add_attribute_conditions(**conditions)
342
368
  end
343
369
 
344
370
  def use_default_id_filter?
@@ -353,6 +379,10 @@ module Capybara
353
379
  options.key?(:style) && !custom_keys.include?(:style)
354
380
  end
355
381
 
382
+ def use_default_focused_filter?
383
+ options.key?(:focused) && !custom_keys.include?(:focused)
384
+ end
385
+
356
386
  def use_spatial_filter?
357
387
  options.values_at(*SPATIAL_KEYS).compact.any?
358
388
  end
@@ -417,6 +447,7 @@ module Capybara
417
447
  matches_id_filter?(node) &&
418
448
  matches_class_filter?(node) &&
419
449
  matches_style_filter?(node) &&
450
+ matches_focused_filter?(node) &&
420
451
  matches_text_filter?(node) &&
421
452
  matches_exact_text_filter?(node)
422
453
  end
@@ -464,9 +495,31 @@ module Capybara
464
495
  end
465
496
 
466
497
  def matches_class_filter?(node)
467
- return true unless use_default_class_filter? && options[:class].is_a?(Regexp)
498
+ return true unless use_default_class_filter? && need_to_process_classes?
468
499
 
469
- options[:class].match? node[:class]
500
+ if options[:class].is_a? Regexp
501
+ options[:class].match? node[:class]
502
+ else
503
+ classes = (node[:class] || '').split
504
+ options[:class].select { |c| c.is_a? Regexp }.all? do |r|
505
+ classes.any? { |cls| r.match? cls }
506
+ end
507
+ end
508
+ end
509
+
510
+ def matches_focused_filter?(node)
511
+ return true unless use_default_focused_filter?
512
+
513
+ (node == node.session.active_element) == options[:focused]
514
+ end
515
+
516
+ def need_to_process_classes?
517
+ case options[:class]
518
+ when Regexp then true
519
+ when Array then options[:class].any?(Regexp)
520
+ else
521
+ false
522
+ end
470
523
  end
471
524
 
472
525
  def matches_style_filter?(node)
@@ -494,16 +547,19 @@ module Capybara
494
547
  def matches_text_filter?(node)
495
548
  value = options[:text]
496
549
  return true unless value
497
- return matches_text_exactly?(node, value) if exact_text == true
550
+ return matches_text_exactly?(node, value) if exact_text == true && !value.is_a?(Regexp)
498
551
 
499
552
  regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s)
500
553
  matches_text_regexp?(node, regexp)
501
554
  end
502
555
 
503
556
  def matches_exact_text_filter?(node)
504
- return true unless exact_text.is_a?(String)
505
-
506
- matches_text_exactly?(node, exact_text)
557
+ case exact_text
558
+ when String, Regexp
559
+ matches_text_exactly?(node, exact_text)
560
+ else
561
+ true
562
+ end
507
563
  end
508
564
 
509
565
  def matches_visibility_filters?(node)
@@ -531,17 +587,21 @@ module Capybara
531
587
 
532
588
  def matches_text_exactly?(node, value)
533
589
  regexp = value.is_a?(Regexp) ? value : /\A#{Regexp.escape(value.to_s)}\z/
534
- matches_text_regexp?(node, regexp)
590
+ matches_text_regexp(node, regexp).then { |m| m&.pre_match == '' && m&.post_match == '' }
535
591
  end
536
592
 
537
593
  def normalize_ws
538
594
  options.fetch(:normalize_ws, session_options.default_normalize_ws)
539
595
  end
540
596
 
541
- def matches_text_regexp?(node, regexp)
597
+ def matches_text_regexp(node, regexp)
542
598
  text_visible = visible
543
599
  text_visible = :all if text_visible == :hidden
544
- node.text(text_visible, normalize_ws: normalize_ws).match?(regexp)
600
+ node.text(text_visible, normalize_ws: normalize_ws).match(regexp)
601
+ end
602
+
603
+ def matches_text_regexp?(node, regexp)
604
+ !matches_text_regexp(node, regexp).nil?
545
605
  end
546
606
 
547
607
  def default_visibility
@@ -562,6 +622,7 @@ module Capybara
562
622
 
563
623
  class Rectangle
564
624
  attr_reader :top, :bottom, :left, :right
625
+
565
626
  def initialize(position)
566
627
  # rubocop:disable Style/RescueModifier
567
628
  @top = position['top'] rescue position['y']
@@ -632,7 +693,7 @@ module Capybara
632
693
 
633
694
  d = u.dot w
634
695
  e = v.dot w
635
- cap_d = (a * c) - (b * b)
696
+ cap_d = (a * c) - (b**2)
636
697
  sD = tD = cap_d
637
698
 
638
699
  # compute the line parameters of the two closest points
@@ -7,15 +7,16 @@ 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) }
14
- Capybara::Result.new(siblings, self)
15
+ Capybara::Result.new(ordered_results(siblings), self)
15
16
  end
16
17
  end
17
18
 
18
- def description(applied = false)
19
+ def description(applied = false) # rubocop:disable Style/OptionalBooleanParameter
19
20
  desc = super
20
21
  sibling_query = @sibling_node&.instance_variable_get(:@query)
21
22
  desc += " that is a sibling of #{sibling_query.description}" if sibling_query
@@ -34,7 +34,7 @@ module Capybara
34
34
  private
35
35
 
36
36
  def stringify_keys(hsh)
37
- hsh.each_with_object({}) { |(k, v), str_keys| str_keys[k.to_s] = v }
37
+ hsh.transform_keys(&:to_s)
38
38
  end
39
39
 
40
40
  def valid_keys
@@ -6,13 +6,15 @@ module Capybara
6
6
  class TextQuery < BaseQuery
7
7
  def initialize(type = nil, expected_text, session_options:, **options) # rubocop:disable Style/OptionalArguments
8
8
  @type = type.nil? ? default_type : type
9
+ raise ArgumentError, "#{@type} is not a valid type for a text query" unless valid_types.include?(@type)
10
+
9
11
  @options = options
10
12
  super(@options)
11
13
  self.session_options = session_options
12
14
 
13
15
  if expected_text.nil? && !exact?
14
16
  warn 'Checking for expected text of nil is confusing and/or pointless since it will always match. '\
15
- 'Please specify a string or regexp instead.'
17
+ "Please specify a string or regexp instead. #{Capybara::Helpers.filter_backtrace(caller)}"
16
18
  end
17
19
 
18
20
  @expected_text = expected_text.is_a?(Regexp) ? expected_text : expected_text.to_s
@@ -89,6 +91,10 @@ module Capybara
89
91
  COUNT_KEYS + %i[wait exact normalize_ws]
90
92
  end
91
93
 
94
+ def valid_types
95
+ %i[all visible]
96
+ end
97
+
92
98
  def check_visible_text?
93
99
  @type == :visible
94
100
  end
@@ -8,6 +8,7 @@ class Capybara::RackTest::Browser
8
8
 
9
9
  def initialize(driver)
10
10
  @driver = driver
11
+ @current_fragment = nil
11
12
  end
12
13
 
13
14
  def app
@@ -19,6 +20,8 @@ class Capybara::RackTest::Browser
19
20
  end
20
21
 
21
22
  def visit(path, **attributes)
23
+ @new_visit_request = true
24
+ reset_cache!
22
25
  reset_host!
23
26
  process_and_follow_redirects(:get, path, attributes)
24
27
  end
@@ -30,18 +33,20 @@ class Capybara::RackTest::Browser
30
33
 
31
34
  def submit(method, path, attributes)
32
35
  path = request_path if path.nil? || path.empty?
33
- process_and_follow_redirects(method, path, attributes, 'HTTP_REFERER' => current_url)
36
+ uri = build_uri(path)
37
+ uri.query = '' if method.to_s.casecmp('get').zero?
38
+ process_and_follow_redirects(method, uri.to_s, attributes, 'HTTP_REFERER' => referer_url)
34
39
  end
35
40
 
36
41
  def follow(method, path, **attributes)
37
42
  return if fragment_or_script?(path)
38
43
 
39
- process_and_follow_redirects(method, path, attributes, 'HTTP_REFERER' => current_url)
44
+ process_and_follow_redirects(method, path, attributes, 'HTTP_REFERER' => referer_url)
40
45
  end
41
46
 
42
47
  def process_and_follow_redirects(method, path, attributes = {}, env = {})
48
+ @current_fragment = build_uri(path).fragment
43
49
  process(method, path, attributes, env)
44
-
45
50
  return unless driver.follow_redirects?
46
51
 
47
52
  driver.redirect_limit.times do
@@ -53,32 +58,42 @@ class Capybara::RackTest::Browser
53
58
  end
54
59
  end
55
60
  end
56
- raise Capybara::InfiniteRedirectError, "redirected more than #{driver.redirect_limit} times, check for infinite redirects." if last_response.redirect?
61
+
62
+ if last_response.redirect? # rubocop:disable Style/GuardClause
63
+ raise Capybara::InfiniteRedirectError, "redirected more than #{driver.redirect_limit} times, check for infinite redirects."
64
+ end
57
65
  end
58
66
 
59
67
  def process(method, path, attributes = {}, env = {})
60
68
  method = method.downcase
61
69
  new_uri = build_uri(path)
62
70
  @current_scheme, @current_host, @current_port = new_uri.select(:scheme, :host, :port)
63
-
71
+ @current_fragment = new_uri.fragment || @current_fragment
64
72
  reset_cache!
73
+ @new_visit_request = false
65
74
  send(method, new_uri.to_s, attributes, env.merge(options[:headers] || {}))
66
75
  end
67
76
 
68
77
  def build_uri(path)
69
- URI.parse(path).tap do |uri|
70
- uri.path = request_path if path.empty? || path.start_with?('?')
71
- uri.path = '/' if uri.path.empty?
72
- uri.path = request_path.sub(%r{/[^/]*$}, '/') + uri.path unless uri.path.start_with?('/')
78
+ uri = URI.parse(path)
79
+ base_uri = base_relative_uri_for(uri)
80
+
81
+ uri.path = base_uri.path + uri.path unless uri.absolute? || uri.path.start_with?('/')
73
82
 
83
+ if base_uri.absolute?
84
+ base_uri.merge(uri)
85
+ else
74
86
  uri.scheme ||= @current_scheme
75
87
  uri.host ||= @current_host
76
88
  uri.port ||= @current_port unless uri.default_port == @current_port
89
+ uri
77
90
  end
78
91
  end
79
92
 
80
93
  def current_url
81
- last_request.url
94
+ uri = build_uri(last_request.url)
95
+ uri.fragment = @current_fragment if @current_fragment
96
+ uri.to_s
82
97
  rescue Rack::Test::Error
83
98
  ''
84
99
  end
@@ -114,8 +129,39 @@ class Capybara::RackTest::Browser
114
129
  dom.title
115
130
  end
116
131
 
132
+ def last_request
133
+ raise Rack::Test::Error if @new_visit_request
134
+
135
+ super
136
+ end
137
+
138
+ def last_response
139
+ raise Rack::Test::Error if @new_visit_request
140
+
141
+ super
142
+ end
143
+
117
144
  protected
118
145
 
146
+ def base_href
147
+ find(:css, 'head > base').first&.[](:href).to_s
148
+ end
149
+
150
+ def base_relative_uri_for(uri)
151
+ base_uri = URI.parse(base_href)
152
+ current_uri = URI.parse(safe_last_request&.url.to_s).tap do |c|
153
+ c.path.sub!(%r{/[^/]*$}, '/') unless uri.path.empty?
154
+ c.path = '/' if c.path.empty?
155
+ end
156
+
157
+ if [current_uri, base_uri].any?(&:absolute?)
158
+ current_uri.merge(base_uri)
159
+ else
160
+ base_uri.path = current_uri.path if base_uri.path.empty?
161
+ base_uri
162
+ end
163
+ end
164
+
119
165
  def build_rack_mock_session
120
166
  reset_host! unless current_host
121
167
  Rack::MockSession.new(app, current_host)
@@ -127,9 +173,21 @@ protected
127
173
  '/'
128
174
  end
129
175
 
176
+ def safe_last_request
177
+ last_request
178
+ rescue Rack::Test::Error
179
+ nil
180
+ end
181
+
130
182
  private
131
183
 
132
184
  def fragment_or_script?(path)
133
185
  path.gsub(/^#{Regexp.escape(request_path)}/, '').start_with?('#') || path.downcase.start_with?('javascript:')
134
186
  end
187
+
188
+ def referer_url
189
+ build_uri(last_request.url).to_s
190
+ rescue Rack::Test::Error
191
+ ''
192
+ end
135
193
  end
@@ -17,6 +17,7 @@ class Capybara::RackTest::Driver < Capybara::Driver::Base
17
17
  def initialize(app, **options)
18
18
  raise ArgumentError, 'rack-test requires a rack application, but none was given' unless app
19
19
 
20
+ super()
20
21
  @app = app
21
22
  @options = DEFAULT_OPTIONS.merge(options)
22
23
  end
@@ -42,7 +43,7 @@ class Capybara::RackTest::Driver < Capybara::Driver::Base
42
43
  end
43
44
 
44
45
  def visit(path, **attributes)
45
- browser.visit(path, attributes)
46
+ browser.visit(path, **attributes)
46
47
  end
47
48
 
48
49
  def refresh
@@ -97,10 +98,10 @@ class Capybara::RackTest::Driver < Capybara::Driver::Base
97
98
  @browser = nil
98
99
  end
99
100
 
100
- def get(*args, &block); browser.get(*args, &block); end
101
- def post(*args, &block); browser.post(*args, &block); end
102
- def put(*args, &block); browser.put(*args, &block); end
103
- def delete(*args, &block); browser.delete(*args, &block); end
101
+ def get(...); browser.get(...); end
102
+ def post(...); browser.post(...); end
103
+ def put(...); browser.put(...); end
104
+ def delete(...); browser.delete(...); end
104
105
  def header(key, value); browser.header(key, value); end
105
106
 
106
107
  def invalid_element_errors
@@ -6,7 +6,7 @@ class Capybara::RackTest::Form < Capybara::RackTest::Node
6
6
  # That check should be based solely on the form element's 'enctype' attribute value,
7
7
  # which should probably be provided to Rack::Test in its non-GET request methods.
8
8
  class NilUploadedFile < Rack::Test::UploadedFile
9
- def initialize
9
+ def initialize # rubocop:disable Lint/MissingSuper
10
10
  @empty_file = Tempfile.new('nil_uploaded_file')
11
11
  @empty_file.close
12
12
  end
@@ -56,7 +56,7 @@ private
56
56
  end
57
57
 
58
58
  def request_method
59
- /post/i.match?(self[:method]) ? :post : :get
59
+ /post/i.match?(self[:method] || '') ? :post : :get
60
60
  end
61
61
 
62
62
  def merge_param!(params, key, value)
@@ -15,7 +15,7 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
15
15
  end
16
16
 
17
17
  def visible_text
18
- displayed_text.gsub(/\ +/, ' ')
18
+ displayed_text.squeeze(' ')
19
19
  .gsub(/[\ \n]*\n[\ \n]*/, "\n")
20
20
  .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
21
21
  .gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
@@ -45,6 +45,7 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
45
45
 
46
46
  if radio? then set_radio(value)
47
47
  elsif checkbox? then set_checkbox(value)
48
+ elsif range? then set_range(value)
48
49
  elsif input_field? then set_input(value)
49
50
  elsif textarea? then native['_capybara_raw_value'] = value.to_s
50
51
  end
@@ -76,8 +77,8 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
76
77
  set(!checked?)
77
78
  elsif tag_name == 'label'
78
79
  click_label
79
- elsif tag_name == 'details'
80
- toggle_details
80
+ elsif (details = native.xpath('.//ancestor-or-self::details').last)
81
+ toggle_details(details)
81
82
  end
82
83
  end
83
84
 
@@ -107,6 +108,13 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
107
108
  end
108
109
  end
109
110
 
111
+ def readonly?
112
+ # readonly attribute not valid on these input types
113
+ return false if input_field? && %w[hidden range color checkbox radio file submit image reset button].include?(type)
114
+
115
+ super
116
+ end
117
+
110
118
  def path
111
119
  native.path
112
120
  end
@@ -123,16 +131,21 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
123
131
  alias_method "unchecked_#{meth_name}", meth_name
124
132
  private "unchecked_#{meth_name}" # rubocop:disable Style/AccessModifierDeclarations
125
133
 
126
- define_method meth_name do |*args|
127
- stale_check
128
- send("unchecked_#{meth_name}", *args)
134
+ if RUBY_VERSION >= '2.7'
135
+ class_eval <<~METHOD, __FILE__, __LINE__ + 1
136
+ def #{meth_name}(...)
137
+ stale_check
138
+ method(:"unchecked_#{meth_name}").call(...)
139
+ end
140
+ METHOD
141
+ else
142
+ define_method meth_name do |*args|
143
+ stale_check
144
+ send("unchecked_#{meth_name}", *args)
145
+ end
129
146
  end
130
147
  end
131
148
 
132
- def ==(other)
133
- native == other.native
134
- end
135
-
136
149
  protected
137
150
 
138
151
  # @api private
@@ -149,7 +162,7 @@ protected
149
162
  end.join || ''
150
163
  text = "\n#{text}\n" if BLOCK_ELEMENTS.include?(tag_name)
151
164
  text
152
- else
165
+ else # rubocop:disable Lint/DuplicateBranch
153
166
  ''
154
167
  end
155
168
  end
@@ -199,6 +212,14 @@ private
199
212
  end
200
213
  end
201
214
 
215
+ def set_range(value) # rubocop:disable Naming/AccessorMethodName
216
+ min, max, step = (native['min'] || 0).to_f, (native['max'] || 100).to_f, (native['step'] || 1).to_f
217
+ value = value.to_f
218
+ value = value.clamp(min, max)
219
+ value = (((value - min) / step).round * step) + min
220
+ native['value'] = value.clamp(min, max)
221
+ end
222
+
202
223
  def set_input(value) # rubocop:disable Naming/AccessorMethodName
203
224
  if text_or_password? && attribute_is_not_blank?(:maxlength)
204
225
  # Browser behavior for maxlength="0" is inconsistent, so we stick with
@@ -223,7 +244,7 @@ private
223
244
  end
224
245
 
225
246
  def follow_link
226
- method = self['data-method'] if driver.options[:respect_data_method]
247
+ method = self['data-method'] || self['data-turbo-method'] if driver.options[:respect_data_method]
227
248
  method ||= :get
228
249
  driver.follow(method, self[:href].to_s)
229
250
  end
@@ -238,11 +259,14 @@ private
238
259
  labelled_control.set(!labelled_control.checked?) if checkbox_or_radio?(labelled_control)
239
260
  end
240
261
 
241
- def toggle_details
242
- if native.has_attribute?('open')
243
- native.remove_attribute('open')
262
+ def toggle_details(details = nil)
263
+ details ||= native.xpath('.//ancestor-or-self::details').last
264
+ return unless details
265
+
266
+ if details.has_attribute?('open')
267
+ details.remove_attribute('open')
244
268
  else
245
- native.set_attribute('open', 'open')
269
+ details.set_attribute('open', 'open')
246
270
  end
247
271
  end
248
272
 
@@ -284,6 +308,10 @@ protected
284
308
  tag_name == 'textarea'
285
309
  end
286
310
 
311
+ def range?
312
+ input_field? && type == 'range'
313
+ end
314
+
287
315
  OPTION_OWNER_XPATH = XPath.parent(:optgroup, :select, :datalist).to_s.freeze
288
316
  DISABLED_BY_FIELDSET_XPATH = XPath.generate do |x|
289
317
  x.parent(:fieldset)[