capybara 3.8.1 → 3.33.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (242) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/History.md +465 -0
  4. data/License.txt +1 -1
  5. data/README.md +58 -57
  6. data/lib/capybara/config.rb +10 -4
  7. data/lib/capybara/cucumber.rb +1 -1
  8. data/lib/capybara/driver/base.rb +2 -2
  9. data/lib/capybara/driver/node.rb +26 -5
  10. data/lib/capybara/dsl.rb +12 -4
  11. data/lib/capybara/helpers.rb +8 -4
  12. data/lib/capybara/minitest/spec.rb +162 -85
  13. data/lib/capybara/minitest.rb +248 -148
  14. data/lib/capybara/node/actions.rb +149 -96
  15. data/lib/capybara/node/base.rb +27 -10
  16. data/lib/capybara/node/document.rb +12 -0
  17. data/lib/capybara/node/document_matchers.rb +9 -5
  18. data/lib/capybara/node/element.rb +254 -109
  19. data/lib/capybara/node/finders.rb +83 -76
  20. data/lib/capybara/node/matchers.rb +279 -141
  21. data/lib/capybara/node/simple.rb +25 -6
  22. data/lib/capybara/queries/ancestor_query.rb +5 -7
  23. data/lib/capybara/queries/base_query.rb +11 -5
  24. data/lib/capybara/queries/current_path_query.rb +3 -3
  25. data/lib/capybara/queries/match_query.rb +1 -0
  26. data/lib/capybara/queries/selector_query.rb +467 -103
  27. data/lib/capybara/queries/sibling_query.rb +5 -4
  28. data/lib/capybara/queries/style_query.rb +6 -2
  29. data/lib/capybara/queries/text_query.rb +17 -3
  30. data/lib/capybara/queries/title_query.rb +2 -2
  31. data/lib/capybara/rack_test/browser.rb +22 -15
  32. data/lib/capybara/rack_test/driver.rb +10 -1
  33. data/lib/capybara/rack_test/errors.rb +6 -0
  34. data/lib/capybara/rack_test/form.rb +33 -28
  35. data/lib/capybara/rack_test/node.rb +74 -6
  36. data/lib/capybara/registration_container.rb +44 -0
  37. data/lib/capybara/registrations/drivers.rb +36 -0
  38. data/lib/capybara/registrations/patches/puma_ssl.rb +27 -0
  39. data/lib/capybara/registrations/servers.rb +44 -0
  40. data/lib/capybara/result.rb +55 -23
  41. data/lib/capybara/rspec/features.rb +4 -4
  42. data/lib/capybara/rspec/matcher_proxies.rb +36 -15
  43. data/lib/capybara/rspec/matchers/base.rb +111 -0
  44. data/lib/capybara/rspec/matchers/become_closed.rb +33 -0
  45. data/lib/capybara/rspec/matchers/compound.rb +88 -0
  46. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  47. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  48. data/lib/capybara/rspec/matchers/have_current_path.rb +29 -0
  49. data/lib/capybara/rspec/matchers/have_selector.rb +77 -0
  50. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  51. data/lib/capybara/rspec/matchers/have_text.rb +33 -0
  52. data/lib/capybara/rspec/matchers/have_title.rb +29 -0
  53. data/lib/capybara/rspec/matchers/match_selector.rb +27 -0
  54. data/lib/capybara/rspec/matchers/match_style.rb +38 -0
  55. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  56. data/lib/capybara/rspec/matchers.rb +117 -311
  57. data/lib/capybara/selector/builders/css_builder.rb +84 -0
  58. data/lib/capybara/selector/builders/xpath_builder.rb +69 -0
  59. data/lib/capybara/selector/css.rb +17 -15
  60. data/lib/capybara/selector/definition/button.rb +52 -0
  61. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  62. data/lib/capybara/selector/definition/css.rb +10 -0
  63. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  64. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  65. data/lib/capybara/selector/definition/element.rb +27 -0
  66. data/lib/capybara/selector/definition/field.rb +40 -0
  67. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  68. data/lib/capybara/selector/definition/file_field.rb +13 -0
  69. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  70. data/lib/capybara/selector/definition/frame.rb +17 -0
  71. data/lib/capybara/selector/definition/id.rb +6 -0
  72. data/lib/capybara/selector/definition/label.rb +62 -0
  73. data/lib/capybara/selector/definition/link.rb +54 -0
  74. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  75. data/lib/capybara/selector/definition/option.rb +27 -0
  76. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  77. data/lib/capybara/selector/definition/select.rb +81 -0
  78. data/lib/capybara/selector/definition/table.rb +109 -0
  79. data/lib/capybara/selector/definition/table_row.rb +21 -0
  80. data/lib/capybara/selector/definition/xpath.rb +5 -0
  81. data/lib/capybara/selector/definition.rb +277 -0
  82. data/lib/capybara/selector/filter.rb +1 -0
  83. data/lib/capybara/selector/filter_set.rb +26 -19
  84. data/lib/capybara/selector/filters/base.rb +24 -5
  85. data/lib/capybara/selector/filters/expression_filter.rb +3 -3
  86. data/lib/capybara/selector/filters/locator_filter.rb +29 -0
  87. data/lib/capybara/selector/filters/node_filter.rb +16 -2
  88. data/lib/capybara/selector/regexp_disassembler.rb +214 -0
  89. data/lib/capybara/selector/selector.rb +73 -367
  90. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  91. data/lib/capybara/selector.rb +221 -480
  92. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  93. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  94. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  95. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  96. data/lib/capybara/selenium/driver.rb +203 -86
  97. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +88 -14
  98. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +124 -0
  99. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +89 -0
  100. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +26 -0
  101. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +24 -0
  102. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  103. data/lib/capybara/selenium/extensions/find.rb +110 -0
  104. data/lib/capybara/selenium/extensions/html5_drag.rb +191 -22
  105. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  106. data/lib/capybara/selenium/extensions/scroll.rb +78 -0
  107. data/lib/capybara/selenium/logger_suppressor.rb +34 -0
  108. data/lib/capybara/selenium/node.rb +298 -93
  109. data/lib/capybara/selenium/nodes/chrome_node.rb +100 -8
  110. data/lib/capybara/selenium/nodes/edge_node.rb +104 -0
  111. data/lib/capybara/selenium/nodes/firefox_node.rb +131 -0
  112. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  113. data/lib/capybara/selenium/nodes/safari_node.rb +118 -0
  114. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  115. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  116. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  117. data/lib/capybara/selenium/patches/logs.rb +45 -0
  118. data/lib/capybara/selenium/patches/pause_duration_fix.rb +1 -3
  119. data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
  120. data/lib/capybara/server/animation_disabler.rb +4 -3
  121. data/lib/capybara/server/checker.rb +6 -2
  122. data/lib/capybara/server/middleware.rb +23 -13
  123. data/lib/capybara/server.rb +30 -7
  124. data/lib/capybara/session/config.rb +14 -10
  125. data/lib/capybara/session/matchers.rb +11 -7
  126. data/lib/capybara/session.rb +152 -111
  127. data/lib/capybara/spec/public/offset.js +6 -0
  128. data/lib/capybara/spec/public/test.js +101 -10
  129. data/lib/capybara/spec/session/all_spec.rb +96 -6
  130. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  131. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +29 -0
  132. data/lib/capybara/spec/session/assert_current_path_spec.rb +5 -2
  133. data/lib/capybara/spec/session/assert_selector_spec.rb +0 -10
  134. data/lib/capybara/spec/session/assert_style_spec.rb +4 -4
  135. data/lib/capybara/spec/session/assert_text_spec.rb +9 -5
  136. data/lib/capybara/spec/session/attach_file_spec.rb +63 -36
  137. data/lib/capybara/spec/session/check_spec.rb +10 -4
  138. data/lib/capybara/spec/session/choose_spec.rb +8 -2
  139. data/lib/capybara/spec/session/click_button_spec.rb +117 -61
  140. data/lib/capybara/spec/session/click_link_or_button_spec.rb +16 -0
  141. data/lib/capybara/spec/session/click_link_spec.rb +17 -6
  142. data/lib/capybara/spec/session/element/matches_selector_spec.rb +40 -39
  143. data/lib/capybara/spec/session/evaluate_script_spec.rb +13 -0
  144. data/lib/capybara/spec/session/execute_script_spec.rb +1 -0
  145. data/lib/capybara/spec/session/fill_in_spec.rb +47 -6
  146. data/lib/capybara/spec/session/find_field_spec.rb +1 -1
  147. data/lib/capybara/spec/session/find_spec.rb +74 -4
  148. data/lib/capybara/spec/session/first_spec.rb +1 -1
  149. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +13 -1
  150. data/lib/capybara/spec/session/frame/within_frame_spec.rb +12 -1
  151. data/lib/capybara/spec/session/has_all_selectors_spec.rb +1 -1
  152. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  153. data/lib/capybara/spec/session/has_any_selectors_spec.rb +25 -0
  154. data/lib/capybara/spec/session/has_button_spec.rb +16 -0
  155. data/lib/capybara/spec/session/has_css_spec.rb +122 -12
  156. data/lib/capybara/spec/session/has_current_path_spec.rb +6 -4
  157. data/lib/capybara/spec/session/has_field_spec.rb +55 -0
  158. data/lib/capybara/spec/session/has_select_spec.rb +34 -6
  159. data/lib/capybara/spec/session/has_selector_spec.rb +11 -4
  160. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  161. data/lib/capybara/spec/session/has_table_spec.rb +166 -0
  162. data/lib/capybara/spec/session/has_text_spec.rb +48 -1
  163. data/lib/capybara/spec/session/has_xpath_spec.rb +17 -0
  164. data/lib/capybara/spec/session/html_spec.rb +7 -0
  165. data/lib/capybara/spec/session/matches_style_spec.rb +35 -0
  166. data/lib/capybara/spec/session/node_spec.rb +643 -18
  167. data/lib/capybara/spec/session/node_wrapper_spec.rb +1 -1
  168. data/lib/capybara/spec/session/refresh_spec.rb +4 -0
  169. data/lib/capybara/spec/session/reset_session_spec.rb +23 -8
  170. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  171. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  172. data/lib/capybara/spec/session/scroll_spec.rb +117 -0
  173. data/lib/capybara/spec/session/select_spec.rb +10 -10
  174. data/lib/capybara/spec/session/selectors_spec.rb +36 -5
  175. data/lib/capybara/spec/session/uncheck_spec.rb +2 -2
  176. data/lib/capybara/spec/session/unselect_spec.rb +1 -1
  177. data/lib/capybara/spec/session/window/become_closed_spec.rb +20 -17
  178. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +4 -0
  179. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +4 -0
  180. data/lib/capybara/spec/session/window/window_spec.rb +59 -58
  181. data/lib/capybara/spec/session/window/windows_spec.rb +4 -0
  182. data/lib/capybara/spec/session/within_spec.rb +23 -0
  183. data/lib/capybara/spec/spec_helper.rb +16 -6
  184. data/lib/capybara/spec/test_app.rb +28 -23
  185. data/lib/capybara/spec/views/animated.erb +49 -0
  186. data/lib/capybara/spec/views/form.erb +48 -7
  187. data/lib/capybara/spec/views/frame_child.erb +3 -2
  188. data/lib/capybara/spec/views/frame_one.erb +1 -0
  189. data/lib/capybara/spec/views/obscured.erb +47 -0
  190. data/lib/capybara/spec/views/offset.erb +32 -0
  191. data/lib/capybara/spec/views/react.erb +45 -0
  192. data/lib/capybara/spec/views/scroll.erb +20 -0
  193. data/lib/capybara/spec/views/spatial.erb +31 -0
  194. data/lib/capybara/spec/views/tables.erb +67 -0
  195. data/lib/capybara/spec/views/with_animation.erb +29 -1
  196. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  197. data/lib/capybara/spec/views/with_hover.erb +1 -0
  198. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  199. data/lib/capybara/spec/views/with_html.erb +32 -6
  200. data/lib/capybara/spec/views/with_js.erb +3 -1
  201. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  202. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  203. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  204. data/lib/capybara/version.rb +1 -1
  205. data/lib/capybara/window.rb +11 -11
  206. data/lib/capybara.rb +118 -111
  207. data/spec/basic_node_spec.rb +14 -3
  208. data/spec/capybara_spec.rb +29 -29
  209. data/spec/css_builder_spec.rb +101 -0
  210. data/spec/dsl_spec.rb +46 -21
  211. data/spec/filter_set_spec.rb +5 -5
  212. data/spec/fixtures/selenium_driver_rspec_failure.rb +1 -1
  213. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
  214. data/spec/minitest_spec.rb +18 -4
  215. data/spec/minitest_spec_spec.rb +59 -44
  216. data/spec/rack_test_spec.rb +117 -89
  217. data/spec/regexp_dissassembler_spec.rb +250 -0
  218. data/spec/result_spec.rb +51 -49
  219. data/spec/rspec/features_spec.rb +3 -0
  220. data/spec/rspec/shared_spec_matchers.rb +112 -97
  221. data/spec/rspec_spec.rb +35 -17
  222. data/spec/sauce_spec_chrome.rb +43 -0
  223. data/spec/selector_spec.rb +244 -28
  224. data/spec/selenium_spec_chrome.rb +125 -54
  225. data/spec/selenium_spec_chrome_remote.rb +26 -12
  226. data/spec/selenium_spec_edge.rb +23 -8
  227. data/spec/selenium_spec_firefox.rb +208 -0
  228. data/spec/selenium_spec_firefox_remote.rb +15 -18
  229. data/spec/selenium_spec_ie.rb +82 -13
  230. data/spec/selenium_spec_safari.rb +148 -0
  231. data/spec/server_spec.rb +118 -77
  232. data/spec/session_spec.rb +19 -3
  233. data/spec/shared_selenium_node.rb +83 -0
  234. data/spec/shared_selenium_session.rb +110 -65
  235. data/spec/spec_helper.rb +57 -9
  236. data/spec/xpath_builder_spec.rb +93 -0
  237. metadata +257 -17
  238. data/lib/capybara/rspec/compound.rb +0 -94
  239. data/lib/capybara/selenium/driver_specializations/marionette_driver.rb +0 -49
  240. data/lib/capybara/selenium/nodes/marionette_node.rb +0 -121
  241. data/lib/capybara/spec/session/has_style_spec.rb +0 -25
  242. data/spec/selenium_spec_marionette.rb +0 -172
@@ -2,15 +2,16 @@
2
2
 
3
3
  module Capybara
4
4
  module Queries
5
- class SiblingQuery < MatchQuery
5
+ class SiblingQuery < SelectorQuery
6
6
  # @api private
7
7
  def resolve_for(node, exact = nil)
8
8
  @sibling_node = node
9
9
  node.synchronize do
10
10
  match_results = super(node.session.current_scope, exact)
11
- node.all(:xpath, XPath.preceding_sibling + XPath.following_sibling) do |el|
12
- match_results.include?(el)
13
- end
11
+ siblings = node.find_xpath((XPath.preceding_sibling + XPath.following_sibling).to_s)
12
+ .map(&method(:to_element))
13
+ .select { |el| match_results.include?(el) }
14
+ Capybara::Result.new(ordered_results(siblings), self)
14
15
  end
15
16
  end
16
17
 
@@ -5,7 +5,7 @@ module Capybara
5
5
  module Queries
6
6
  class StyleQuery < BaseQuery
7
7
  def initialize(expected_styles, session_options:, **options)
8
- @expected_styles = expected_styles.each_with_object({}) { |(style, value), str_keys| str_keys[style.to_s] = value }
8
+ @expected_styles = stringify_keys(expected_styles)
9
9
  @options = options
10
10
  @actual_styles = {}
11
11
  super(@options)
@@ -19,7 +19,7 @@ module Capybara
19
19
  @actual_styles = node.style(*@expected_styles.keys)
20
20
  @expected_styles.all? do |style, value|
21
21
  if value.is_a? Regexp
22
- @actual_styles[style] =~ value
22
+ value.match? @actual_styles[style]
23
23
  else
24
24
  @actual_styles[style] == value
25
25
  end
@@ -33,6 +33,10 @@ module Capybara
33
33
 
34
34
  private
35
35
 
36
+ def stringify_keys(hsh)
37
+ hsh.transform_keys(&:to_s)
38
+ end
39
+
36
40
  def valid_keys
37
41
  %i[wait]
38
42
  end
@@ -6,11 +6,19 @@ 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
- @expected_text = expected_text.is_a?(Regexp) ? expected_text : expected_text.to_s
9
+ raise ArgumentError, '${@type} is not a valid type for a text query' unless valid_types.include?(@type)
10
+
10
11
  @options = options
11
12
  super(@options)
12
13
  self.session_options = session_options
13
14
 
15
+ if expected_text.nil? && !exact?
16
+ warn 'Checking for expected text of nil is confusing and/or pointless since it will always match. '\
17
+ 'Please specify a string or regexp instead.'
18
+ end
19
+
20
+ @expected_text = expected_text.is_a?(Regexp) ? expected_text : expected_text.to_s
21
+
14
22
  @search_regexp = Capybara::Helpers.to_regexp(@expected_text, exact: exact?)
15
23
 
16
24
  assert_valid_keys
@@ -64,14 +72,16 @@ module Capybara
64
72
  insensitive_regexp = Capybara::Helpers.to_regexp(@expected_text, options: Regexp::IGNORECASE)
65
73
  insensitive_count = @actual_text.scan(insensitive_regexp).size
66
74
  return if insensitive_count == @count
67
- "it was found #{insensitive_count} #{Capybara::Helpers.declension('time', 'times', insensitive_count)} using a case insensitive search"
75
+
76
+ "it was found #{occurrences insensitive_count} using a case insensitive search"
68
77
  end
69
78
 
70
79
  def invisible_message
71
80
  invisible_text = text(query_type: :all)
72
81
  invisible_count = invisible_text.scan(@search_regexp).size
73
82
  return if invisible_count == @count
74
- "it was found #{invisible_count} #{Capybara::Helpers.declension('time', 'times', invisible_count)} including non-visible text"
83
+
84
+ "it was found #{occurrences invisible_count} including non-visible text"
75
85
  rescue StandardError
76
86
  # An error getting the non-visible text (if element goes out of scope) should not affect the response
77
87
  nil
@@ -81,6 +91,10 @@ module Capybara
81
91
  COUNT_KEYS + %i[wait exact normalize_ws]
82
92
  end
83
93
 
94
+ def valid_types
95
+ %i[all visible]
96
+ end
97
+
84
98
  def check_visible_text?
85
99
  @type == :visible
86
100
  end
@@ -8,12 +8,12 @@ module Capybara
8
8
  @expected_title = expected_title.is_a?(Regexp) ? expected_title : expected_title.to_s
9
9
  @options = options
10
10
  super(@options)
11
- @search_regexp = Capybara::Helpers.to_regexp(@expected_title, all_whitespace: true, exact: options.fetch(:exact, false))
11
+ @search_regexp = Helpers.to_regexp(@expected_title, all_whitespace: true, exact: options.fetch(:exact, false))
12
12
  assert_valid_keys
13
13
  end
14
14
 
15
15
  def resolves_for?(node)
16
- (@actual_title = node.title).match(@search_regexp)
16
+ (@actual_title = node.title).match?(@search_regexp)
17
17
  end
18
18
 
19
19
  def failure_message
@@ -30,11 +30,14 @@ class Capybara::RackTest::Browser
30
30
 
31
31
  def submit(method, path, attributes)
32
32
  path = request_path if path.nil? || path.empty?
33
- process_and_follow_redirects(method, path, attributes, 'HTTP_REFERER' => current_url)
33
+ uri = build_uri(path)
34
+ uri.query = '' if method&.to_s&.downcase == 'get'
35
+ process_and_follow_redirects(method, uri.to_s, attributes, 'HTTP_REFERER' => current_url)
34
36
  end
35
37
 
36
38
  def follow(method, path, **attributes)
37
39
  return if fragment_or_script?(path)
40
+
38
41
  process_and_follow_redirects(method, path, attributes, 'HTTP_REFERER' => current_url)
39
42
  end
40
43
 
@@ -52,29 +55,33 @@ class Capybara::RackTest::Browser
52
55
  end
53
56
  end
54
57
  end
55
- raise Capybara::InfiniteRedirectError, "redirected more than #{driver.redirect_limit} times, check for infinite redirects." if last_response.redirect?
56
- end
57
58
 
58
- def process(method, path, attributes = {}, env = {})
59
- new_uri = URI.parse(path)
60
- method.downcase! unless method.is_a? Symbol
61
- if path.empty?
62
- new_uri.path = request_path
63
- else
64
- new_uri.path = request_path if path.start_with?('?')
65
- new_uri.path = '/' if new_uri.path.empty?
66
- new_uri.path = request_path.sub(%r{/[^/]*$}, '/') + new_uri.path unless new_uri.path.start_with?('/')
59
+ if last_response.redirect? # rubocop:disable Style/GuardClause
60
+ raise Capybara::InfiniteRedirectError, "redirected more than #{driver.redirect_limit} times, check for infinite redirects."
67
61
  end
68
- new_uri.scheme ||= @current_scheme
69
- new_uri.host ||= @current_host
70
- new_uri.port ||= @current_port unless new_uri.default_port == @current_port
62
+ end
71
63
 
64
+ def process(method, path, attributes = {}, env = {})
65
+ method = method.downcase
66
+ new_uri = build_uri(path)
72
67
  @current_scheme, @current_host, @current_port = new_uri.select(:scheme, :host, :port)
73
68
 
74
69
  reset_cache!
75
70
  send(method, new_uri.to_s, attributes, env.merge(options[:headers] || {}))
76
71
  end
77
72
 
73
+ def build_uri(path)
74
+ URI.parse(path).tap do |uri|
75
+ uri.path = request_path if path.empty? || path.start_with?('?')
76
+ uri.path = '/' if uri.path.empty?
77
+ uri.path = request_path.sub(%r{/[^/]*$}, '/') + uri.path unless uri.path.start_with?('/')
78
+
79
+ uri.scheme ||= @current_scheme
80
+ uri.host ||= @current_host
81
+ uri.port ||= @current_port unless uri.default_port == @current_port
82
+ end
83
+ end
84
+
78
85
  def current_url
79
86
  last_request.url
80
87
  rescue Rack::Test::Error
@@ -16,6 +16,7 @@ class Capybara::RackTest::Driver < Capybara::Driver::Base
16
16
 
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
  @app = app
20
21
  @options = DEFAULT_OPTIONS.merge(options)
21
22
  end
@@ -41,7 +42,7 @@ class Capybara::RackTest::Driver < Capybara::Driver::Base
41
42
  end
42
43
 
43
44
  def visit(path, **attributes)
44
- browser.visit(path, attributes)
45
+ browser.visit(path, **attributes)
45
46
  end
46
47
 
47
48
  def refresh
@@ -74,6 +75,10 @@ class Capybara::RackTest::Driver < Capybara::Driver::Base
74
75
 
75
76
  def find_css(selector)
76
77
  browser.find(:css, selector)
78
+ rescue Nokogiri::CSS::SyntaxError
79
+ raise unless selector.include?(' i]')
80
+
81
+ raise ArgumentError, "This driver doesn't support case insensitive attribute matching when using CSS base selectors"
77
82
  end
78
83
 
79
84
  def html
@@ -97,4 +102,8 @@ class Capybara::RackTest::Driver < Capybara::Driver::Base
97
102
  def put(*args, &block); browser.put(*args, &block); end
98
103
  def delete(*args, &block); browser.delete(*args, &block); end
99
104
  def header(key, value); browser.header(key, value); end
105
+
106
+ def invalid_element_errors
107
+ [Capybara::RackTest::Errors::StaleElementReferenceError]
108
+ end
100
109
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara::RackTest::Errors
4
+ class StaleElementReferenceError < StandardError
5
+ end
6
+ end
@@ -19,25 +19,22 @@ class Capybara::RackTest::Form < Capybara::RackTest::Node
19
19
  end
20
20
 
21
21
  def params(button)
22
- params = make_params
23
-
24
- form_element_types = %i[input select textarea]
22
+ form_element_types = %i[input select textarea button]
25
23
  form_elements_xpath = XPath.generate do |xp|
26
24
  xpath = xp.descendant(*form_element_types).where(!xp.attr(:form))
27
25
  xpath += xp.anywhere(*form_element_types).where(xp.attr(:form) == native[:id]) if native[:id]
28
26
  xpath.where(!xp.attr(:disabled))
29
27
  end.to_s
30
28
 
31
- native.xpath(form_elements_xpath).map do |field|
29
+ form_elements = native.xpath(form_elements_xpath).reject { |el| submitter?(el) && (el != button.native) }
30
+
31
+ form_elements.each_with_object(make_params) do |field, params|
32
32
  case field.name
33
- when 'input' then add_input_param(field, params)
33
+ when 'input', 'button' then add_input_param(field, params)
34
34
  when 'select' then add_select_param(field, params)
35
35
  when 'textarea' then add_textarea_param(field, params)
36
36
  end
37
- end
38
- merge_param!(params, button[:name], button[:value] || '') if button[:name]
39
-
40
- params.to_params_hash
37
+ end.to_params_hash
41
38
  end
42
39
 
43
40
  def submit(button)
@@ -59,7 +56,7 @@ private
59
56
  end
60
57
 
61
58
  def request_method
62
- self[:method] =~ /post/i ? :post : :get
59
+ /post/i.match?(self[:method] || '') ? :post : :get
63
60
  end
64
61
 
65
62
  def merge_param!(params, key, value)
@@ -80,28 +77,32 @@ private
80
77
  end
81
78
 
82
79
  def add_input_param(field, params)
83
- if %w[radio checkbox].include? field['type']
84
- if field['checked']
85
- node = Capybara::RackTest::Node.new(driver, field)
86
- merge_param!(params, field['name'], node.value.to_s)
87
- end
88
- elsif %w[submit image].include? field['type']
89
- # TODO: identify the click button here (in document order, rather
90
- # than leaving until the end of the params)
91
- elsif field['type'] == 'file'
80
+ name, value = field['name'].to_s, field['value'].to_s
81
+ return if name.empty?
82
+
83
+ value = case field['type']
84
+ when 'radio', 'checkbox'
85
+ return unless field['checked']
86
+
87
+ Capybara::RackTest::Node.new(driver, field).value.to_s
88
+ when 'file'
92
89
  if multipart?
93
- file = if (value = field['value']).to_s.empty?
94
- NilUploadedFile.new
95
- else
96
- mime_info = MiniMime.lookup_by_filename(value)
97
- Rack::Test::UploadedFile.new(value, mime_info&.content_type&.to_s)
98
- end
99
- merge_param!(params, field['name'], file)
90
+ file_to_upload(value)
100
91
  else
101
- merge_param!(params, field['name'], File.basename(field['value'].to_s))
92
+ File.basename(value)
102
93
  end
103
94
  else
104
- merge_param!(params, field['name'], field['value'].to_s)
95
+ value
96
+ end
97
+ merge_param!(params, name, value)
98
+ end
99
+
100
+ def file_to_upload(filename)
101
+ if filename.empty?
102
+ NilUploadedFile.new
103
+ else
104
+ mime_info = MiniMime.lookup_by_filename(filename)
105
+ Rack::Test::UploadedFile.new(filename, mime_info&.content_type&.to_s)
105
106
  end
106
107
  end
107
108
 
@@ -119,4 +120,8 @@ private
119
120
  def add_textarea_param(field, params)
120
121
  merge_param!(params, field['name'], field['_capybara_raw_value'].to_s.gsub(/\n/, "\r\n"))
121
122
  end
123
+
124
+ def submitter?(el)
125
+ (%w[submit image].include? el['type']) || (el.name == 'button')
126
+ end
122
127
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'capybara/rack_test/errors'
4
+
3
5
  class Capybara::RackTest::Node < Capybara::Driver::Node
4
6
  BLOCK_ELEMENTS = %w[p h1 h2 h3 h4 h5 h6 ol ul pre address blockquote dl div fieldset form hr noscript table].freeze
5
7
 
@@ -43,6 +45,7 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
43
45
 
44
46
  if radio? then set_radio(value)
45
47
  elsif checkbox? then set_checkbox(value)
48
+ elsif range? then set_range(value)
46
49
  elsif input_field? then set_input(value)
47
50
  elsif textarea? then native['_capybara_raw_value'] = value.to_s
48
51
  end
@@ -50,17 +53,20 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
50
53
 
51
54
  def select_option
52
55
  return if disabled?
56
+
53
57
  deselect_options unless select_node.multiple?
54
58
  native['selected'] = 'selected'
55
59
  end
56
60
 
57
61
  def unselect_option
58
62
  raise Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.' unless select_node.multiple?
63
+
59
64
  native.remove_attribute('selected')
60
65
  end
61
66
 
62
- def click(keys = [], **offset)
63
- raise ArgumentError, 'The RackTest driver does not support click options' unless keys.empty? && offset.empty?
67
+ def click(keys = [], **options)
68
+ options.delete(:offset)
69
+ raise ArgumentError, 'The RackTest driver does not support click options' unless keys.empty? && options.empty?
64
70
 
65
71
  if link?
66
72
  follow_link
@@ -71,6 +77,8 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
71
77
  set(!checked?)
72
78
  elsif tag_name == 'label'
73
79
  click_label
80
+ elsif (details = native.xpath('.//ancestor-or-self::details').last)
81
+ toggle_details(details)
74
82
  end
75
83
  end
76
84
 
@@ -94,9 +102,9 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
94
102
  return true if string_node.disabled?
95
103
 
96
104
  if %w[option optgroup].include? tag_name
97
- find_xpath('parent::*[self::optgroup or self::select or self::datalist]')[0].disabled?
105
+ find_xpath(OPTION_OWNER_XPATH)[0].disabled?
98
106
  else
99
- !find_xpath('parent::fieldset[@disabled] | ancestor::*[not(self::legend) or preceding-sibling::legend][parent::fieldset[@disabled]]').empty?
107
+ !find_xpath(DISABLED_BY_FIELDSET_XPATH).empty?
100
108
  end
101
109
  end
102
110
 
@@ -104,14 +112,33 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
104
112
  native.path
105
113
  end
106
114
 
107
- def find_xpath(locator)
115
+ def find_xpath(locator, **_hints)
108
116
  native.xpath(locator).map { |el| self.class.new(driver, el) }
109
117
  end
110
118
 
111
- def find_css(locator)
119
+ def find_css(locator, **_hints)
112
120
  native.css(locator, Capybara::RackTest::CSSHandlers.new).map { |el| self.class.new(driver, el) }
113
121
  end
114
122
 
123
+ public_instance_methods(false).each do |meth_name|
124
+ alias_method "unchecked_#{meth_name}", meth_name
125
+ private "unchecked_#{meth_name}" # rubocop:disable Style/AccessModifierDeclarations
126
+
127
+ if RUBY_VERSION >= '2.7'
128
+ class_eval <<~METHOD, __FILE__, __LINE__ + 1
129
+ def #{meth_name}(...)
130
+ stale_check
131
+ method(:"unchecked_#{meth_name}").call(...)
132
+ end
133
+ METHOD
134
+ else
135
+ define_method meth_name do |*args|
136
+ stale_check
137
+ send("unchecked_#{meth_name}", *args)
138
+ end
139
+ end
140
+ end
141
+
115
142
  def ==(other)
116
143
  native == other.native
117
144
  end
@@ -139,6 +166,10 @@ protected
139
166
 
140
167
  private
141
168
 
169
+ def stale_check
170
+ raise Capybara::RackTest::Errors::StaleElementReferenceError unless native.document == driver.dom
171
+ end
172
+
142
173
  def deselect_options
143
174
  select_node.find_xpath('.//option[@selected]').each { |node| node.native.remove_attribute('selected') }
144
175
  end
@@ -178,6 +209,14 @@ private
178
209
  end
179
210
  end
180
211
 
212
+ def set_range(value) # rubocop:disable Naming/AccessorMethodName
213
+ min, max, step = (native['min'] || 0).to_f, (native['max'] || 100).to_f, (native['step'] || 1).to_f
214
+ value = value.to_f
215
+ value = value.clamp(min, max)
216
+ value = ((value - min) / step).round * step + min
217
+ native['value'] = value.clamp(min, max)
218
+ end
219
+
181
220
  def set_input(value) # rubocop:disable Naming/AccessorMethodName
182
221
  if text_or_password? && attribute_is_not_blank?(:maxlength)
183
222
  # Browser behavior for maxlength="0" is inconsistent, so we stick with
@@ -217,6 +256,17 @@ private
217
256
  labelled_control.set(!labelled_control.checked?) if checkbox_or_radio?(labelled_control)
218
257
  end
219
258
 
259
+ def toggle_details(details = nil)
260
+ details ||= native.xpath('.//ancestor-or-self::details').last
261
+ return unless details
262
+
263
+ if details.has_attribute?('open')
264
+ details.remove_attribute('open')
265
+ else
266
+ details.set_attribute('open', 'open')
267
+ end
268
+ end
269
+
220
270
  def link?
221
271
  tag_name == 'a' && !self[:href].nil?
222
272
  end
@@ -254,4 +304,22 @@ protected
254
304
  def textarea?
255
305
  tag_name == 'textarea'
256
306
  end
307
+
308
+ def range?
309
+ input_field? && type == 'range'
310
+ end
311
+
312
+ OPTION_OWNER_XPATH = XPath.parent(:optgroup, :select, :datalist).to_s.freeze
313
+ DISABLED_BY_FIELDSET_XPATH = XPath.generate do |x|
314
+ x.parent(:fieldset)[
315
+ XPath.attr(:disabled)
316
+ ] + x.ancestor[
317
+ ~x.self(:legend) |
318
+ x.preceding_sibling(:legend)
319
+ ][
320
+ x.parent(:fieldset)[
321
+ x.attr(:disabled)
322
+ ]
323
+ ]
324
+ end.to_s.freeze
257
325
  end
@@ -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
+ 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
+ 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_private = false)
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
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.register_driver :rack_test do |app|
4
+ Capybara::RackTest::Driver.new(app)
5
+ end
6
+
7
+ Capybara.register_driver :selenium do |app|
8
+ Capybara::Selenium::Driver.new(app)
9
+ end
10
+
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)
16
+ end
17
+
18
+ Capybara.register_driver :selenium_chrome do |app|
19
+ Capybara::Selenium::Driver.load_selenium
20
+ browser_options = ::Selenium::WebDriver::Chrome::Options.new.tap do |opts|
21
+ # 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'
23
+ end
24
+ Capybara::Selenium::Driver.new(app, browser: :chrome, options: browser_options)
25
+ end
26
+
27
+ Capybara.register_driver :selenium_chrome_headless do |app|
28
+ Capybara::Selenium::Driver.load_selenium
29
+ browser_options = ::Selenium::WebDriver::Chrome::Options.new.tap do |opts|
30
+ opts.args << '--headless'
31
+ opts.args << '--disable-gpu' if Gem.win_platform?
32
+ # 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'
34
+ end
35
+ Capybara::Selenium::Driver.new(app, browser: :chrome, options: browser_options)
36
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+ module MiniSSL
5
+ class Socket
6
+ def read_nonblock(size, *_)
7
+ loop do
8
+ output = engine_read_all
9
+ return output if output
10
+
11
+ data = @socket.read_nonblock(size, exception: false)
12
+ raise IO::EAGAINWaitReadable if %i[wait_readable wait_writable].include? data
13
+ return nil if data.nil?
14
+
15
+ @engine.inject(data)
16
+ output = engine_read_all
17
+
18
+ return output if output
19
+
20
+ while (neg_data = @engine.extract)
21
+ @socket.write neg_data
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.register_server :default do |app, port, _host|
4
+ Capybara.run_default_server(app, port)
5
+ end
6
+
7
+ Capybara.register_server :webrick do |app, port, host, **options|
8
+ require 'rack/handler/webrick'
9
+ options = { Host: host, Port: port, AccessLog: [], Logger: WEBrick::Log.new(nil, 0) }.merge(options)
10
+ Rack::Handler::WEBrick.run(app, **options)
11
+ end
12
+
13
+ Capybara.register_server :puma do |app, port, host, **options|
14
+ begin
15
+ require 'rack/handler/puma'
16
+ rescue LoadError
17
+ raise LoadError, 'Capybara is unable to load `puma` for its server, please add `puma` to your project or specify a different server via something like `Capybara.server = :webrick`.'
18
+ else
19
+ unless Rack::Handler::Puma.respond_to?(:config)
20
+ raise LoadError, 'Capybara requires `puma` version 3.8.0 or higher, please upgrade `puma` or register and specify your own server block'
21
+ end
22
+ end
23
+
24
+ # If we just run the Puma Rack handler it installs signal handlers which prevent us from being able to interrupt tests.
25
+ # Therefore construct and run the Server instance ourselves.
26
+ # Rack::Handler::Puma.run(app, { Host: host, Port: port, Threads: "0:4", workers: 0, daemon: false }.merge(options))
27
+ default_options = { Host: host, Port: port, Threads: '0:4', workers: 0, daemon: false }
28
+ options = default_options.merge(options)
29
+
30
+ conf = Rack::Handler::Puma.config(app, options)
31
+ events = conf.options[:Silent] ? ::Puma::Events.strings : ::Puma::Events.stdio
32
+
33
+ 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
+
36
+ events.log 'Capybara starting Puma...'
37
+ events.log "* Version #{Puma::Const::PUMA_VERSION} , codename: #{Puma::Const::CODE_NAME}"
38
+ events.log "* Min threads: #{conf.options[:min_threads]}, max threads: #{conf.options[:max_threads]}"
39
+
40
+ Puma::Server.new(conf.app, events, conf.options).tap do |s|
41
+ s.binder.parse conf.options[:binds], s.events
42
+ s.min_threads, s.max_threads = conf.options[:min_threads], conf.options[:max_threads]
43
+ end.run.join
44
+ end