capybara 3.29.0 → 3.36.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (200) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +203 -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 +1 -1
  8. data/lib/capybara/dsl.rb +10 -2
  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 +24 -21
  17. data/lib/capybara/node/finders.rb +25 -18
  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 +71 -23
  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 +13 -4
  29. data/lib/capybara/rack_test/driver.rb +2 -1
  30. data/lib/capybara/rack_test/form.rb +2 -2
  31. data/lib/capybara/rack_test/node.rb +43 -15
  32. data/lib/capybara/registration_container.rb +44 -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 +14 -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 +13 -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 +79 -16
  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 +110 -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 +17 -6
  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 +9 -3
  99. data/lib/capybara/session/matchers.rb +11 -11
  100. data/lib/capybara/session.rb +73 -37
  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 +62 -9
  105. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  106. data/lib/capybara/spec/session/assert_text_spec.rb +9 -5
  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 +31 -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 +15 -2
  118. data/lib/capybara/spec/session/has_field_spec.rb +40 -0
  119. data/lib/capybara/spec/session/has_link_spec.rb +24 -0
  120. data/lib/capybara/spec/session/has_select_spec.rb +32 -4
  121. data/lib/capybara/spec/session/has_selector_spec.rb +4 -4
  122. data/lib/capybara/spec/session/has_table_spec.rb +51 -5
  123. data/lib/capybara/spec/session/has_text_spec.rb +25 -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 +184 -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/window/switch_to_window_spec.rb +1 -1
  134. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -1
  135. data/lib/capybara/spec/session/window/window_spec.rb +8 -8
  136. data/lib/capybara/spec/session/window/windows_spec.rb +1 -1
  137. data/lib/capybara/spec/spec_helper.rb +17 -17
  138. data/lib/capybara/spec/test_app.rb +40 -29
  139. data/lib/capybara/spec/views/animated.erb +1 -1
  140. data/lib/capybara/spec/views/form.erb +52 -6
  141. data/lib/capybara/spec/views/frame_child.erb +1 -1
  142. data/lib/capybara/spec/views/frame_one.erb +1 -1
  143. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  144. data/lib/capybara/spec/views/frame_two.erb +1 -1
  145. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  146. data/lib/capybara/spec/views/layout.erb +10 -0
  147. data/lib/capybara/spec/views/obscured.erb +1 -1
  148. data/lib/capybara/spec/views/offset.erb +2 -1
  149. data/lib/capybara/spec/views/path.erb +2 -2
  150. data/lib/capybara/spec/views/popup_one.erb +1 -1
  151. data/lib/capybara/spec/views/popup_two.erb +1 -1
  152. data/lib/capybara/spec/views/react.erb +2 -2
  153. data/lib/capybara/spec/views/scroll.erb +2 -1
  154. data/lib/capybara/spec/views/spatial.erb +1 -1
  155. data/lib/capybara/spec/views/with_animation.erb +10 -3
  156. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  157. data/lib/capybara/spec/views/with_dragula.erb +5 -3
  158. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  159. data/lib/capybara/spec/views/with_hover.erb +2 -2
  160. data/lib/capybara/spec/views/with_html.erb +3 -3
  161. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  162. data/lib/capybara/spec/views/with_js.erb +5 -3
  163. data/lib/capybara/spec/views/with_jstree.erb +1 -1
  164. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  165. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  166. data/lib/capybara/spec/views/with_sortable_js.erb +3 -3
  167. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  168. data/lib/capybara/spec/views/with_windows.erb +1 -1
  169. data/lib/capybara/spec/views/within_frames.erb +1 -1
  170. data/lib/capybara/version.rb +1 -1
  171. data/lib/capybara/window.rb +3 -7
  172. data/lib/capybara.rb +35 -29
  173. data/spec/basic_node_spec.rb +25 -11
  174. data/spec/capybara_spec.rb +1 -1
  175. data/spec/dsl_spec.rb +16 -3
  176. data/spec/fixtures/selenium_driver_rspec_success.rb +2 -2
  177. data/spec/minitest_spec.rb +3 -2
  178. data/spec/minitest_spec_spec.rb +46 -46
  179. data/spec/rack_test_spec.rb +37 -11
  180. data/spec/regexp_dissassembler_spec.rb +40 -36
  181. data/spec/result_spec.rb +42 -32
  182. data/spec/rspec/features_spec.rb +5 -2
  183. data/spec/rspec/scenarios_spec.rb +4 -0
  184. data/spec/rspec/shared_spec_matchers.rb +68 -56
  185. data/spec/rspec_spec.rb +8 -4
  186. data/spec/selector_spec.rb +17 -2
  187. data/spec/selenium_spec_chrome.rb +48 -25
  188. data/spec/selenium_spec_chrome_remote.rb +13 -6
  189. data/spec/selenium_spec_firefox.rb +25 -17
  190. data/spec/selenium_spec_firefox_remote.rb +2 -2
  191. data/spec/selenium_spec_ie.rb +3 -6
  192. data/spec/selenium_spec_safari.rb +27 -19
  193. data/spec/server_spec.rb +84 -31
  194. data/spec/session_spec.rb +1 -1
  195. data/spec/shared_selenium_node.rb +21 -7
  196. data/spec/shared_selenium_session.rb +114 -19
  197. data/spec/spec_helper.rb +1 -1
  198. metadata +78 -21
  199. data/lib/capybara/spec/session/source_spec.rb +0 -0
  200. data/lib/capybara/spec/views/with_title.erb +0 -5
@@ -6,21 +6,25 @@ 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
 
26
30
  super(@options)
@@ -28,7 +32,11 @@ module Capybara
28
32
 
29
33
  @selector = Selector.new(
30
34
  find_selector(args[0].is_a?(Symbol) ? args.shift : args[0]),
31
- config: { enable_aria_label: enable_aria_label, test_id: test_id },
35
+ config: {
36
+ enable_aria_label: enable_aria_label,
37
+ enable_aria_role: enable_aria_role,
38
+ test_id: test_id
39
+ },
32
40
  format: selector_format
33
41
  )
34
42
 
@@ -37,7 +45,7 @@ module Capybara
37
45
 
38
46
  raise ArgumentError, "Unused parameters passed to #{self.class.name} : #{args}" unless args.empty?
39
47
 
40
- @expression = selector.call(@locator, @options)
48
+ @expression = selector.call(@locator, **@options)
41
49
 
42
50
  warn_exact_usage
43
51
 
@@ -47,7 +55,7 @@ module Capybara
47
55
  def name; selector.name; end
48
56
  def label; selector.label || selector.name; end
49
57
 
50
- def description(only_applied = false)
58
+ def description(only_applied = false) # rubocop:disable Style/OptionalBooleanParameter
51
59
  desc = +''
52
60
  show_for = show_for_stage(only_applied)
53
61
 
@@ -65,6 +73,8 @@ module Capybara
65
73
 
66
74
  desc << " with id #{options[:id]}" if options[:id]
67
75
  desc << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
76
+ desc << ' that is focused' if options[:focused]
77
+ desc << ' that is not focused' if options[:focused] == false
68
78
 
69
79
  desc << case options[:style]
70
80
  when String
@@ -77,7 +87,9 @@ module Capybara
77
87
  end
78
88
 
79
89
  %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
90
+ if options[spatial_filter] && show_for[:spatial]
91
+ desc << " #{spatial_filter} #{options[spatial_filter] rescue '<ERROR>'}" # rubocop:disable Style/RescueModifier
92
+ end
81
93
  end
82
94
 
83
95
  desc << selector.description(node_filters: show_for[:node], **options)
@@ -85,11 +97,9 @@ module Capybara
85
97
  desc << ' that also matches the custom filter block' if @filter_block && show_for[:node]
86
98
 
87
99
  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
100
+ if locator.is_a?(String) && locator.start_with?('#', './/', '//') && !selector.raw_locator?
101
+ desc << "\nNote: It appears you may be passing a CSS selector or XPath expression rather than a locator. " \
102
+ "Please see the documentation for acceptable locator values.\n\n"
93
103
  end
94
104
  desc
95
105
  end
@@ -148,7 +158,7 @@ module Capybara
148
158
 
149
159
  node.synchronize do
150
160
  children = find_nodes_by_selector_format(node, exact).map(&method(:to_element))
151
- Capybara::Result.new(children, self)
161
+ Capybara::Result.new(ordered_results(children), self)
152
162
  end
153
163
  end
154
164
 
@@ -229,17 +239,18 @@ module Capybara
229
239
  hints[:styles] = options[:style] if use_default_style_filter?
230
240
  hints[:position] = true if use_spatial_filter?
231
241
 
232
- if selector_format == :css
233
- if node.method(:find_css).arity != 1
234
- node.find_css(css, **hints)
235
- else
242
+ case selector_format
243
+ when :css
244
+ if node.method(:find_css).arity == 1
236
245
  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
246
  else
247
+ node.find_css(css, **hints)
248
+ end
249
+ when :xpath
250
+ if node.method(:find_xpath).arity == 1
242
251
  node.find_xpath(xpath(exact))
252
+ else
253
+ node.find_xpath(xpath(exact), **hints)
243
254
  end
244
255
  else
245
256
  raise ArgumentError, "Unknown format: #{selector_format}"
@@ -311,6 +322,15 @@ module Capybara
311
322
  filters
312
323
  end
313
324
 
325
+ def ordered_results(results)
326
+ case @order
327
+ when :reverse
328
+ results.reverse
329
+ else
330
+ results
331
+ end
332
+ end
333
+
314
334
  def custom_keys
315
335
  @custom_keys ||= node_filters.keys + expression_filters.keys
316
336
  end
@@ -338,7 +358,7 @@ module Capybara
338
358
  conditions[:id] = options[:id] if use_default_id_filter?
339
359
  conditions[:class] = options[:class] if use_default_class_filter?
340
360
  conditions[:style] = options[:style] if use_default_style_filter? && !options[:style].is_a?(Hash)
341
- builder(expr).add_attribute_conditions(conditions)
361
+ builder(expr).add_attribute_conditions(**conditions)
342
362
  end
343
363
 
344
364
  def use_default_id_filter?
@@ -353,6 +373,10 @@ module Capybara
353
373
  options.key?(:style) && !custom_keys.include?(:style)
354
374
  end
355
375
 
376
+ def use_default_focused_filter?
377
+ options.key?(:focused) && !custom_keys.include?(:focused)
378
+ end
379
+
356
380
  def use_spatial_filter?
357
381
  options.values_at(*SPATIAL_KEYS).compact.any?
358
382
  end
@@ -417,6 +441,7 @@ module Capybara
417
441
  matches_id_filter?(node) &&
418
442
  matches_class_filter?(node) &&
419
443
  matches_style_filter?(node) &&
444
+ matches_focused_filter?(node) &&
420
445
  matches_text_filter?(node) &&
421
446
  matches_exact_text_filter?(node)
422
447
  end
@@ -464,9 +489,31 @@ module Capybara
464
489
  end
465
490
 
466
491
  def matches_class_filter?(node)
467
- return true unless use_default_class_filter? && options[:class].is_a?(Regexp)
492
+ return true unless use_default_class_filter? && need_to_process_classes?
493
+
494
+ if options[:class].is_a? Regexp
495
+ options[:class].match? node[:class]
496
+ else
497
+ classes = (node[:class] || '').split
498
+ options[:class].select { |c| c.is_a? Regexp }.all? do |r|
499
+ classes.any? { |cls| r.match? cls }
500
+ end
501
+ end
502
+ end
503
+
504
+ def matches_focused_filter?(node)
505
+ return true unless use_default_focused_filter?
468
506
 
469
- options[:class].match? node[:class]
507
+ (node == node.session.active_element) == options[:focused]
508
+ end
509
+
510
+ def need_to_process_classes?
511
+ case options[:class]
512
+ when Regexp then true
513
+ when Array then options[:class].any?(Regexp)
514
+ else
515
+ false
516
+ end
470
517
  end
471
518
 
472
519
  def matches_style_filter?(node)
@@ -562,6 +609,7 @@ module Capybara
562
609
 
563
610
  class Rectangle
564
611
  attr_reader :top, :bottom, :left, :right
612
+
565
613
  def initialize(position)
566
614
  # rubocop:disable Style/RescueModifier
567
615
  @top = position['top'] rescue position['y']
@@ -632,7 +680,7 @@ module Capybara
632
680
 
633
681
  d = u.dot w
634
682
  e = v.dot w
635
- cap_d = (a * c) - (b * b)
683
+ cap_d = (a * c) - (b**2)
636
684
  sD = tD = cap_d
637
685
 
638
686
  # 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
@@ -30,7 +31,9 @@ class Capybara::RackTest::Browser
30
31
 
31
32
  def submit(method, path, attributes)
32
33
  path = request_path if path.nil? || path.empty?
33
- process_and_follow_redirects(method, path, attributes, 'HTTP_REFERER' => current_url)
34
+ uri = build_uri(path)
35
+ uri.query = '' if method.to_s.casecmp('get').zero?
36
+ process_and_follow_redirects(method, uri.to_s, attributes, 'HTTP_REFERER' => current_url)
34
37
  end
35
38
 
36
39
  def follow(method, path, **attributes)
@@ -40,6 +43,7 @@ class Capybara::RackTest::Browser
40
43
  end
41
44
 
42
45
  def process_and_follow_redirects(method, path, attributes = {}, env = {})
46
+ @current_fragment = build_uri(path).fragment
43
47
  process(method, path, attributes, env)
44
48
 
45
49
  return unless driver.follow_redirects?
@@ -53,14 +57,17 @@ class Capybara::RackTest::Browser
53
57
  end
54
58
  end
55
59
  end
56
- raise Capybara::InfiniteRedirectError, "redirected more than #{driver.redirect_limit} times, check for infinite redirects." if last_response.redirect?
60
+
61
+ if last_response.redirect? # rubocop:disable Style/GuardClause
62
+ raise Capybara::InfiniteRedirectError, "redirected more than #{driver.redirect_limit} times, check for infinite redirects."
63
+ end
57
64
  end
58
65
 
59
66
  def process(method, path, attributes = {}, env = {})
60
67
  method = method.downcase
61
68
  new_uri = build_uri(path)
62
69
  @current_scheme, @current_host, @current_port = new_uri.select(:scheme, :host, :port)
63
-
70
+ @current_fragment = new_uri.fragment || @current_fragment
64
71
  reset_cache!
65
72
  send(method, new_uri.to_s, attributes, env.merge(options[:headers] || {}))
66
73
  end
@@ -78,7 +85,9 @@ class Capybara::RackTest::Browser
78
85
  end
79
86
 
80
87
  def current_url
81
- last_request.url
88
+ uri = build_uri(last_request.url)
89
+ uri.fragment = @current_fragment if @current_fragment
90
+ uri.to_s
82
91
  rescue Rack::Test::Error
83
92
  ''
84
93
  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
@@ -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
@@ -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)[
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ # @api private
5
+ class RegistrationContainer
6
+ def names
7
+ @registered.keys
8
+ end
9
+
10
+ def [](name)
11
+ @registered[name]
12
+ end
13
+
14
+ def []=(name, value)
15
+ Capybara::Helpers.warn 'DEPRECATED: Directly setting drivers/servers is deprecated, please use Capybara.register_driver/register_server instead'
16
+ @registered[name] = value
17
+ end
18
+
19
+ def method_missing(method_name, *args, **options, &block)
20
+ if @registered.respond_to?(method_name)
21
+ Capybara::Helpers.warn "DEPRECATED: Calling '#{method_name}' on the drivers/servers container is deprecated without replacement"
22
+ # RUBY 2.6 will send an empty hash rather than nothing with **options so fix that
23
+ return @registered.public_send(method_name, *args, &block) if options.empty?
24
+
25
+ return @registered.public_send(method_name, *args, **options, &block)
26
+ end
27
+ super
28
+ end
29
+
30
+ def respond_to_missing?(method_name, include_all)
31
+ @registered.respond_to?(method_name) || super
32
+ end
33
+
34
+ private
35
+
36
+ def initialize
37
+ @registered = {}
38
+ end
39
+
40
+ def register(name, block)
41
+ @registered[name] = block
42
+ end
43
+ end
44
+ end
@@ -9,28 +9,34 @@ Capybara.register_driver :selenium do |app|
9
9
  end
10
10
 
11
11
  Capybara.register_driver :selenium_headless do |app|
12
- Capybara::Selenium::Driver.load_selenium
13
- browser_options = ::Selenium::WebDriver::Firefox::Options.new
14
- browser_options.args << '-headless'
15
- Capybara::Selenium::Driver.new(app, browser: :firefox, options: browser_options)
12
+ version = Capybara::Selenium::Driver.load_selenium
13
+ options_key = Capybara::Selenium::Driver::CAPS_VERSION.satisfied_by?(version) ? :capabilities : :options
14
+ browser_options = ::Selenium::WebDriver::Firefox::Options.new.tap do |opts|
15
+ opts.add_argument '-headless'
16
+ end
17
+ Capybara::Selenium::Driver.new(app, **{ :browser => :firefox, options_key => browser_options })
16
18
  end
17
19
 
18
20
  Capybara.register_driver :selenium_chrome do |app|
19
- Capybara::Selenium::Driver.load_selenium
21
+ version = Capybara::Selenium::Driver.load_selenium
22
+ options_key = Capybara::Selenium::Driver::CAPS_VERSION.satisfied_by?(version) ? :capabilities : :options
20
23
  browser_options = ::Selenium::WebDriver::Chrome::Options.new.tap do |opts|
21
24
  # Workaround https://bugs.chromium.org/p/chromedriver/issues/detail?id=2650&q=load&sort=-id&colspec=ID%20Status%20Pri%20Owner%20Summary
22
- opts.args << '--disable-site-isolation-trials'
25
+ opts.add_argument('--disable-site-isolation-trials')
23
26
  end
24
- Capybara::Selenium::Driver.new(app, browser: :chrome, options: browser_options)
27
+
28
+ Capybara::Selenium::Driver.new(app, **{ :browser => :chrome, options_key => browser_options })
25
29
  end
26
30
 
27
31
  Capybara.register_driver :selenium_chrome_headless do |app|
28
- Capybara::Selenium::Driver.load_selenium
32
+ version = Capybara::Selenium::Driver.load_selenium
33
+ options_key = Capybara::Selenium::Driver::CAPS_VERSION.satisfied_by?(version) ? :capabilities : :options
29
34
  browser_options = ::Selenium::WebDriver::Chrome::Options.new.tap do |opts|
30
- opts.args << '--headless'
31
- opts.args << '--disable-gpu' if Gem.win_platform?
35
+ opts.add_argument('--headless')
36
+ opts.add_argument('--disable-gpu') if Gem.win_platform?
32
37
  # Workaround https://bugs.chromium.org/p/chromedriver/issues/detail?id=2650&q=load&sort=-id&colspec=ID%20Status%20Pri%20Owner%20Summary
33
- opts.args << '--disable-site-isolation-trials'
38
+ opts.add_argument('--disable-site-isolation-trials')
34
39
  end
35
- Capybara::Selenium::Driver.new(app, browser: :chrome, options: browser_options)
40
+
41
+ Capybara::Selenium::Driver.new(app, **{ :browser => :chrome, options_key => browser_options })
36
42
  end
@@ -4,12 +4,14 @@ module Puma
4
4
  module MiniSSL
5
5
  class Socket
6
6
  def read_nonblock(size, *_)
7
+ wait_states = %i[wait_readable wait_writable]
8
+
7
9
  loop do
8
10
  output = engine_read_all
9
11
  return output if output
10
12
 
11
13
  data = @socket.read_nonblock(size, exception: false)
12
- raise IO::EAGAINWaitReadable if %i[wait_readable wait_writable].include? data
14
+ raise IO::EAGAINWaitReadable if wait_states.include? data
13
15
  return nil if data.nil?
14
16
 
15
17
  @engine.inject(data)
@@ -7,7 +7,7 @@ end
7
7
  Capybara.register_server :webrick do |app, port, host, **options|
8
8
  require 'rack/handler/webrick'
9
9
  options = { Host: host, Port: port, AccessLog: [], Logger: WEBrick::Log.new(nil, 0) }.merge(options)
10
- Rack::Handler::WEBrick.run(app, options)
10
+ Rack::Handler::WEBrick.run(app, **options)
11
11
  end
12
12
 
13
13
  Capybara.register_server :puma do |app, port, host, **options|
@@ -28,10 +28,11 @@ Capybara.register_server :puma do |app, port, host, **options|
28
28
  options = default_options.merge(options)
29
29
 
30
30
  conf = Rack::Handler::Puma.config(app, options)
31
+ conf.clamp
31
32
  events = conf.options[:Silent] ? ::Puma::Events.strings : ::Puma::Events.stdio
32
33
 
33
34
  puma_ver = Gem::Version.new(Puma::Const::PUMA_VERSION)
34
- require_relative 'patches/puma_ssl' if (Gem::Version.new('4.0.0')...Gem::Version.new('4.1.0')).cover? puma_ver
35
+ require_relative 'patches/puma_ssl' if Gem::Requirement.new('>=4.0.0', '< 4.1.0').satisfied_by?(puma_ver)
35
36
 
36
37
  events.log 'Capybara starting Puma...'
37
38
  events.log "* Version #{Puma::Const::PUMA_VERSION} , codename: #{Puma::Const::CODE_NAME}"