capybara 3.13.2 → 3.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/History.md +587 -16
  4. data/README.md +240 -90
  5. data/lib/capybara/config.rb +24 -11
  6. data/lib/capybara/cucumber.rb +1 -1
  7. data/lib/capybara/driver/base.rb +8 -0
  8. data/lib/capybara/driver/node.rb +20 -4
  9. data/lib/capybara/dsl.rb +5 -3
  10. data/lib/capybara/helpers.rb +25 -4
  11. data/lib/capybara/minitest/spec.rb +174 -90
  12. data/lib/capybara/minitest.rb +256 -142
  13. data/lib/capybara/node/actions.rb +123 -77
  14. data/lib/capybara/node/base.rb +20 -12
  15. data/lib/capybara/node/document.rb +2 -2
  16. data/lib/capybara/node/document_matchers.rb +3 -3
  17. data/lib/capybara/node/element.rb +223 -117
  18. data/lib/capybara/node/finders.rb +81 -71
  19. data/lib/capybara/node/matchers.rb +271 -134
  20. data/lib/capybara/node/simple.rb +18 -5
  21. data/lib/capybara/node/whitespace_normalizer.rb +81 -0
  22. data/lib/capybara/queries/active_element_query.rb +18 -0
  23. data/lib/capybara/queries/ancestor_query.rb +8 -9
  24. data/lib/capybara/queries/base_query.rb +3 -2
  25. data/lib/capybara/queries/current_path_query.rb +15 -5
  26. data/lib/capybara/queries/selector_query.rb +364 -54
  27. data/lib/capybara/queries/sibling_query.rb +8 -6
  28. data/lib/capybara/queries/style_query.rb +2 -2
  29. data/lib/capybara/queries/text_query.rb +13 -1
  30. data/lib/capybara/queries/title_query.rb +1 -1
  31. data/lib/capybara/rack_test/browser.rb +76 -11
  32. data/lib/capybara/rack_test/driver.rb +10 -5
  33. data/lib/capybara/rack_test/errors.rb +6 -0
  34. data/lib/capybara/rack_test/form.rb +31 -9
  35. data/lib/capybara/rack_test/node.rb +74 -23
  36. data/lib/capybara/registration_container.rb +41 -0
  37. data/lib/capybara/registrations/drivers.rb +42 -0
  38. data/lib/capybara/registrations/patches/puma_ssl.rb +29 -0
  39. data/lib/capybara/registrations/servers.rb +66 -0
  40. data/lib/capybara/result.rb +44 -20
  41. data/lib/capybara/rspec/matcher_proxies.rb +13 -11
  42. data/lib/capybara/rspec/matchers/base.rb +31 -16
  43. data/lib/capybara/rspec/matchers/compound.rb +1 -1
  44. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  45. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  46. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  47. data/lib/capybara/rspec/matchers/have_selector.rb +21 -21
  48. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  49. data/lib/capybara/rspec/matchers/have_text.rb +4 -4
  50. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  51. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  52. data/lib/capybara/rspec/matchers/match_style.rb +7 -2
  53. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  54. data/lib/capybara/rspec/matchers.rb +111 -68
  55. data/lib/capybara/rspec.rb +2 -0
  56. data/lib/capybara/selector/builders/css_builder.rb +11 -7
  57. data/lib/capybara/selector/builders/xpath_builder.rb +5 -3
  58. data/lib/capybara/selector/css.rb +11 -9
  59. data/lib/capybara/selector/definition/button.rb +68 -0
  60. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  61. data/lib/capybara/selector/definition/css.rb +10 -0
  62. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  63. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  64. data/lib/capybara/selector/definition/element.rb +28 -0
  65. data/lib/capybara/selector/definition/field.rb +40 -0
  66. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  67. data/lib/capybara/selector/definition/file_field.rb +13 -0
  68. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  69. data/lib/capybara/selector/definition/frame.rb +17 -0
  70. data/lib/capybara/selector/definition/id.rb +6 -0
  71. data/lib/capybara/selector/definition/label.rb +62 -0
  72. data/lib/capybara/selector/definition/link.rb +55 -0
  73. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  74. data/lib/capybara/selector/definition/option.rb +27 -0
  75. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  76. data/lib/capybara/selector/definition/select.rb +81 -0
  77. data/lib/capybara/selector/definition/table.rb +109 -0
  78. data/lib/capybara/selector/definition/table_row.rb +21 -0
  79. data/lib/capybara/selector/definition/xpath.rb +5 -0
  80. data/lib/capybara/selector/definition.rb +280 -0
  81. data/lib/capybara/selector/filter_set.rb +19 -18
  82. data/lib/capybara/selector/filters/base.rb +11 -2
  83. data/lib/capybara/selector/filters/locator_filter.rb +13 -3
  84. data/lib/capybara/selector/regexp_disassembler.rb +11 -7
  85. data/lib/capybara/selector/selector.rb +50 -440
  86. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  87. data/lib/capybara/selector.rb +473 -482
  88. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  89. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  90. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  91. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  92. data/lib/capybara/selenium/driver.rb +174 -62
  93. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +74 -18
  94. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +128 -0
  95. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +37 -3
  96. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +14 -1
  97. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +24 -0
  98. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  99. data/lib/capybara/selenium/extensions/find.rb +68 -45
  100. data/lib/capybara/selenium/extensions/html5_drag.rb +192 -22
  101. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  102. data/lib/capybara/selenium/extensions/scroll.rb +8 -10
  103. data/lib/capybara/selenium/node.rb +268 -72
  104. data/lib/capybara/selenium/nodes/chrome_node.rb +105 -9
  105. data/lib/capybara/selenium/nodes/edge_node.rb +110 -0
  106. data/lib/capybara/selenium/nodes/firefox_node.rb +51 -61
  107. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  108. data/lib/capybara/selenium/nodes/safari_node.rb +118 -0
  109. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  110. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  111. data/lib/capybara/selenium/patches/logs.rb +45 -0
  112. data/lib/capybara/selenium/patches/pause_duration_fix.rb +1 -1
  113. data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
  114. data/lib/capybara/server/animation_disabler.rb +43 -21
  115. data/lib/capybara/server/checker.rb +6 -2
  116. data/lib/capybara/server/middleware.rb +25 -13
  117. data/lib/capybara/server.rb +20 -4
  118. data/lib/capybara/session/config.rb +15 -11
  119. data/lib/capybara/session/matchers.rb +11 -11
  120. data/lib/capybara/session.rb +162 -131
  121. data/lib/capybara/spec/public/offset.js +6 -0
  122. data/lib/capybara/spec/public/test.js +105 -6
  123. data/lib/capybara/spec/session/accept_alert_spec.rb +1 -1
  124. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  125. data/lib/capybara/spec/session/all_spec.rb +89 -15
  126. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  127. data/lib/capybara/spec/session/assert_current_path_spec.rb +5 -2
  128. data/lib/capybara/spec/session/assert_text_spec.rb +26 -22
  129. data/lib/capybara/spec/session/attach_file_spec.rb +64 -31
  130. data/lib/capybara/spec/session/check_spec.rb +26 -4
  131. data/lib/capybara/spec/session/choose_spec.rb +14 -2
  132. data/lib/capybara/spec/session/click_button_spec.rb +109 -61
  133. data/lib/capybara/spec/session/click_link_or_button_spec.rb +9 -0
  134. data/lib/capybara/spec/session/click_link_spec.rb +23 -1
  135. data/lib/capybara/spec/session/current_scope_spec.rb +1 -1
  136. data/lib/capybara/spec/session/current_url_spec.rb +11 -1
  137. data/lib/capybara/spec/session/element/matches_selector_spec.rb +40 -39
  138. data/lib/capybara/spec/session/evaluate_script_spec.rb +12 -0
  139. data/lib/capybara/spec/session/fill_in_spec.rb +46 -5
  140. data/lib/capybara/spec/session/find_link_spec.rb +10 -0
  141. data/lib/capybara/spec/session/find_spec.rb +80 -7
  142. data/lib/capybara/spec/session/first_spec.rb +2 -2
  143. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +14 -1
  144. data/lib/capybara/spec/session/frame/within_frame_spec.rb +14 -1
  145. data/lib/capybara/spec/session/has_all_selectors_spec.rb +5 -5
  146. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  147. data/lib/capybara/spec/session/has_any_selectors_spec.rb +6 -2
  148. data/lib/capybara/spec/session/has_button_spec.rb +81 -0
  149. data/lib/capybara/spec/session/has_css_spec.rb +45 -8
  150. data/lib/capybara/spec/session/has_current_path_spec.rb +22 -7
  151. data/lib/capybara/spec/session/has_element_spec.rb +47 -0
  152. data/lib/capybara/spec/session/has_field_spec.rb +59 -1
  153. data/lib/capybara/spec/session/has_link_spec.rb +40 -0
  154. data/lib/capybara/spec/session/has_none_selectors_spec.rb +7 -7
  155. data/lib/capybara/spec/session/has_select_spec.rb +42 -8
  156. data/lib/capybara/spec/session/has_selector_spec.rb +19 -4
  157. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  158. data/lib/capybara/spec/session/has_table_spec.rb +177 -0
  159. data/lib/capybara/spec/session/has_text_spec.rb +31 -3
  160. data/lib/capybara/spec/session/html_spec.rb +1 -1
  161. data/lib/capybara/spec/session/matches_style_spec.rb +6 -4
  162. data/lib/capybara/spec/session/node_spec.rb +697 -23
  163. data/lib/capybara/spec/session/node_wrapper_spec.rb +1 -1
  164. data/lib/capybara/spec/session/refresh_spec.rb +2 -1
  165. data/lib/capybara/spec/session/reset_session_spec.rb +21 -7
  166. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  167. data/lib/capybara/spec/session/save_page_spec.rb +4 -4
  168. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  169. data/lib/capybara/spec/session/scroll_spec.rb +9 -7
  170. data/lib/capybara/spec/session/select_spec.rb +5 -10
  171. data/lib/capybara/spec/session/selectors_spec.rb +24 -3
  172. data/lib/capybara/spec/session/uncheck_spec.rb +3 -3
  173. data/lib/capybara/spec/session/unselect_spec.rb +1 -1
  174. data/lib/capybara/spec/session/visit_spec.rb +20 -0
  175. data/lib/capybara/spec/session/window/become_closed_spec.rb +20 -17
  176. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +1 -1
  177. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -1
  178. data/lib/capybara/spec/session/window/window_spec.rb +54 -57
  179. data/lib/capybara/spec/session/window/windows_spec.rb +2 -2
  180. data/lib/capybara/spec/session/within_spec.rb +36 -0
  181. data/lib/capybara/spec/spec_helper.rb +30 -19
  182. data/lib/capybara/spec/test_app.rb +122 -34
  183. data/lib/capybara/spec/views/animated.erb +49 -0
  184. data/lib/capybara/spec/views/form.erb +86 -8
  185. data/lib/capybara/spec/views/frame_child.erb +3 -2
  186. data/lib/capybara/spec/views/frame_one.erb +2 -1
  187. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  188. data/lib/capybara/spec/views/frame_two.erb +1 -1
  189. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  190. data/lib/capybara/spec/views/layout.erb +10 -0
  191. data/lib/capybara/spec/views/obscured.erb +10 -10
  192. data/lib/capybara/spec/views/offset.erb +33 -0
  193. data/lib/capybara/spec/views/path.erb +2 -2
  194. data/lib/capybara/spec/views/popup_one.erb +1 -1
  195. data/lib/capybara/spec/views/popup_two.erb +1 -1
  196. data/lib/capybara/spec/views/react.erb +45 -0
  197. data/lib/capybara/spec/views/scroll.erb +2 -1
  198. data/lib/capybara/spec/views/spatial.erb +31 -0
  199. data/lib/capybara/spec/views/tables.erb +67 -0
  200. data/lib/capybara/spec/views/with_animation.erb +39 -4
  201. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  202. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  203. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  204. data/lib/capybara/spec/views/with_hover.erb +3 -2
  205. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  206. data/lib/capybara/spec/views/with_html.erb +34 -6
  207. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  208. data/lib/capybara/spec/views/with_js.erb +7 -4
  209. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  210. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  211. data/lib/capybara/spec/views/with_scope.erb +2 -2
  212. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  213. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  214. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  215. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  216. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  217. data/lib/capybara/spec/views/with_windows.erb +1 -1
  218. data/lib/capybara/spec/views/within_frames.erb +1 -1
  219. data/lib/capybara/version.rb +1 -1
  220. data/lib/capybara/window.rb +14 -18
  221. data/lib/capybara.rb +91 -126
  222. data/spec/basic_node_spec.rb +30 -16
  223. data/spec/capybara_spec.rb +40 -28
  224. data/spec/counter_spec.rb +35 -0
  225. data/spec/css_builder_spec.rb +3 -1
  226. data/spec/css_splitter_spec.rb +1 -1
  227. data/spec/dsl_spec.rb +33 -22
  228. data/spec/filter_set_spec.rb +5 -5
  229. data/spec/fixtures/selenium_driver_rspec_failure.rb +3 -3
  230. data/spec/fixtures/selenium_driver_rspec_success.rb +3 -3
  231. data/spec/minitest_spec.rb +24 -2
  232. data/spec/minitest_spec_spec.rb +60 -45
  233. data/spec/per_session_config_spec.rb +1 -1
  234. data/spec/rack_test_spec.rb +131 -98
  235. data/spec/regexp_dissassembler_spec.rb +53 -39
  236. data/spec/result_spec.rb +68 -66
  237. data/spec/rspec/features_spec.rb +9 -4
  238. data/spec/rspec/scenarios_spec.rb +6 -2
  239. data/spec/rspec/shared_spec_matchers.rb +137 -98
  240. data/spec/rspec_matchers_spec.rb +25 -0
  241. data/spec/rspec_spec.rb +23 -21
  242. data/spec/sauce_spec_chrome.rb +43 -0
  243. data/spec/selector_spec.rb +77 -21
  244. data/spec/selenium_spec_chrome.rb +141 -39
  245. data/spec/selenium_spec_chrome_remote.rb +32 -17
  246. data/spec/selenium_spec_edge.rb +36 -8
  247. data/spec/selenium_spec_firefox.rb +110 -68
  248. data/spec/selenium_spec_firefox_remote.rb +22 -15
  249. data/spec/selenium_spec_ie.rb +29 -22
  250. data/spec/selenium_spec_safari.rb +162 -0
  251. data/spec/server_spec.rb +153 -81
  252. data/spec/session_spec.rb +11 -4
  253. data/spec/shared_selenium_node.rb +79 -0
  254. data/spec/shared_selenium_session.rb +179 -74
  255. data/spec/spec_helper.rb +80 -5
  256. data/spec/whitespace_normalizer_spec.rb +54 -0
  257. data/spec/xpath_builder_spec.rb +3 -1
  258. metadata +218 -30
  259. data/lib/capybara/spec/session/source_spec.rb +0 -0
  260. data/lib/capybara/spec/views/with_title.erb +0 -5
@@ -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. #{Capybara::Helpers.filter_backtrace(caller)}"
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
@@ -83,6 +91,10 @@ module Capybara
83
91
  COUNT_KEYS + %i[wait exact normalize_ws]
84
92
  end
85
93
 
94
+ def valid_types
95
+ %i[all visible]
96
+ end
97
+
86
98
  def check_visible_text?
87
99
  @type == :visible
88
100
  end
@@ -13,7 +13,7 @@ module Capybara
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
@@ -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
@@ -28,20 +31,29 @@ class Capybara::RackTest::Browser
28
31
  request(last_request.fullpath, last_request.env)
29
32
  end
30
33
 
31
- def submit(method, path, attributes)
34
+ def submit(method, path, attributes, content_type: nil)
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
+ env = { 'HTTP_REFERER' => referer_url }
39
+ env['CONTENT_TYPE'] = content_type if content_type
40
+ process_and_follow_redirects(
41
+ method,
42
+ uri.to_s,
43
+ attributes,
44
+ env
45
+ )
34
46
  end
35
47
 
36
48
  def follow(method, path, **attributes)
37
49
  return if fragment_or_script?(path)
38
50
 
39
- process_and_follow_redirects(method, path, attributes, 'HTTP_REFERER' => current_url)
51
+ process_and_follow_redirects(method, path, attributes, 'HTTP_REFERER' => referer_url)
40
52
  end
41
53
 
42
54
  def process_and_follow_redirects(method, path, attributes = {}, env = {})
55
+ @current_fragment = build_uri(path).fragment
43
56
  process(method, path, attributes, env)
44
-
45
57
  return unless driver.follow_redirects?
46
58
 
47
59
  driver.redirect_limit.times do
@@ -53,32 +65,42 @@ class Capybara::RackTest::Browser
53
65
  end
54
66
  end
55
67
  end
56
- raise Capybara::InfiniteRedirectError, "redirected more than #{driver.redirect_limit} times, check for infinite redirects." if last_response.redirect?
68
+
69
+ if last_response.redirect? # rubocop:disable Style/GuardClause
70
+ raise Capybara::InfiniteRedirectError, "redirected more than #{driver.redirect_limit} times, check for infinite redirects."
71
+ end
57
72
  end
58
73
 
59
74
  def process(method, path, attributes = {}, env = {})
60
75
  method = method.downcase
61
76
  new_uri = build_uri(path)
62
77
  @current_scheme, @current_host, @current_port = new_uri.select(:scheme, :host, :port)
63
-
78
+ @current_fragment = new_uri.fragment || @current_fragment
64
79
  reset_cache!
80
+ @new_visit_request = false
65
81
  send(method, new_uri.to_s, attributes, env.merge(options[:headers] || {}))
66
82
  end
67
83
 
68
84
  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?('/')
85
+ uri = URI.parse(path)
86
+ base_uri = base_relative_uri_for(uri)
87
+
88
+ uri.path = base_uri.path + uri.path unless uri.absolute? || uri.path.start_with?('/')
73
89
 
90
+ if base_uri.absolute?
91
+ base_uri.merge(uri)
92
+ else
74
93
  uri.scheme ||= @current_scheme
75
94
  uri.host ||= @current_host
76
95
  uri.port ||= @current_port unless uri.default_port == @current_port
96
+ uri
77
97
  end
78
98
  end
79
99
 
80
100
  def current_url
81
- last_request.url
101
+ uri = build_uri(last_request.url)
102
+ uri.fragment = @current_fragment if @current_fragment
103
+ uri.to_s
82
104
  rescue Rack::Test::Error
83
105
  ''
84
106
  end
@@ -114,8 +136,39 @@ class Capybara::RackTest::Browser
114
136
  dom.title
115
137
  end
116
138
 
139
+ def last_request
140
+ raise Rack::Test::Error if @new_visit_request
141
+
142
+ super
143
+ end
144
+
145
+ def last_response
146
+ raise Rack::Test::Error if @new_visit_request
147
+
148
+ super
149
+ end
150
+
117
151
  protected
118
152
 
153
+ def base_href
154
+ find(:css, 'head > base').first&.[](:href).to_s
155
+ end
156
+
157
+ def base_relative_uri_for(uri)
158
+ base_uri = URI.parse(base_href)
159
+ current_uri = URI.parse(safe_last_request&.url.to_s).tap do |c|
160
+ c.path.sub!(%r{/[^/]*$}, '/') unless uri.path.empty?
161
+ c.path = '/' if c.path.empty?
162
+ end
163
+
164
+ if [current_uri, base_uri].any?(&:absolute?)
165
+ current_uri.merge(base_uri)
166
+ else
167
+ base_uri.path = current_uri.path if base_uri.path.empty?
168
+ base_uri
169
+ end
170
+ end
171
+
119
172
  def build_rack_mock_session
120
173
  reset_host! unless current_host
121
174
  Rack::MockSession.new(app, current_host)
@@ -127,9 +180,21 @@ protected
127
180
  '/'
128
181
  end
129
182
 
183
+ def safe_last_request
184
+ last_request
185
+ rescue Rack::Test::Error
186
+ nil
187
+ end
188
+
130
189
  private
131
190
 
132
191
  def fragment_or_script?(path)
133
192
  path.gsub(/^#{Regexp.escape(request_path)}/, '').start_with?('#') || path.downcase.start_with?('javascript:')
134
193
  end
194
+
195
+ def referer_url
196
+ build_uri(last_request.url).to_s
197
+ rescue Rack::Test::Error
198
+ ''
199
+ end
135
200
  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,9 +98,13 @@ 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
106
+
107
+ def invalid_element_errors
108
+ [Capybara::RackTest::Errors::StaleElementReferenceError]
109
+ end
105
110
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara::RackTest::Errors
4
+ class StaleElementReferenceError < StandardError
5
+ end
6
+ end
@@ -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
@@ -16,6 +16,8 @@ class Capybara::RackTest::Form < Capybara::RackTest::Node
16
16
  def path; @empty_file.path; end
17
17
  def size; 0; end
18
18
  def read; ''; end
19
+ def append_to(_); end
20
+ def set_encoding(_); end # rubocop:disable Naming/AccessorMethodName
19
21
  end
20
22
 
21
23
  def params(button)
@@ -28,19 +30,31 @@ class Capybara::RackTest::Form < Capybara::RackTest::Node
28
30
 
29
31
  form_elements = native.xpath(form_elements_xpath).reject { |el| submitter?(el) && (el != button.native) }
30
32
 
31
- form_elements.each_with_object(make_params) do |field, params|
33
+ form_params = form_elements.each_with_object({}.compare_by_identity) do |field, params|
32
34
  case field.name
33
35
  when 'input', 'button' then add_input_param(field, params)
34
36
  when 'select' then add_select_param(field, params)
35
37
  when 'textarea' then add_textarea_param(field, params)
36
38
  end
39
+ end
40
+
41
+ form_params.each_with_object(make_params) do |(name, value), params|
42
+ merge_param!(params, name, value)
37
43
  end.to_params_hash
44
+
45
+ # form_elements.each_with_object(make_params) do |field, params|
46
+ # case field.name
47
+ # when 'input', 'button' then add_input_param(field, params)
48
+ # when 'select' then add_select_param(field, params)
49
+ # when 'textarea' then add_textarea_param(field, params)
50
+ # end
51
+ # end.to_params_hash
38
52
  end
39
53
 
40
54
  def submit(button)
41
55
  action = button&.[]('formaction') || native['action']
42
56
  method = button&.[]('formmethod') || request_method
43
- driver.submit(method, action.to_s, params(button))
57
+ driver.submit(method, action.to_s, params(button), content_type: native['enctype'])
44
58
  end
45
59
 
46
60
  def multipart?
@@ -56,7 +70,7 @@ private
56
70
  end
57
71
 
58
72
  def request_method
59
- self[:method] =~ /post/i ? :post : :get
73
+ /post/i.match?(self[:method] || '') ? :post : :get
60
74
  end
61
75
 
62
76
  def merge_param!(params, key, value)
@@ -86,6 +100,8 @@ private
86
100
 
87
101
  Capybara::RackTest::Node.new(driver, field).value.to_s
88
102
  when 'file'
103
+ return if value.empty? && params.keys.include?(name) && Rack::Test::VERSION.to_f >= 2.0 # rubocop:disable Performance/InefficientHashSearch
104
+
89
105
  if multipart?
90
106
  file_to_upload(value)
91
107
  else
@@ -94,7 +110,8 @@ private
94
110
  else
95
111
  value
96
112
  end
97
- merge_param!(params, name, value)
113
+ # merge_param!(params, name, value)
114
+ params[name] = value
98
115
  end
99
116
 
100
117
  def file_to_upload(filename)
@@ -107,18 +124,23 @@ private
107
124
  end
108
125
 
109
126
  def add_select_param(field, params)
127
+ name = field['name']
110
128
  if field.has_attribute?('multiple')
111
- field.xpath('.//option[@selected]').each do |option|
112
- merge_param!(params, field['name'], (option['value'] || option.text).to_s)
129
+ value = field.xpath('.//option[@selected]').map do |option|
130
+ # merge_param!(params, field['name'], (option['value'] || option.text).to_s)
131
+ (option['value'] || option.text).to_s
113
132
  end
133
+ params[name] = value unless value.empty?
114
134
  else
115
135
  option = field.xpath('.//option[@selected]').first || field.xpath('.//option').first
116
- merge_param!(params, field['name'], (option['value'] || option.text).to_s) if option
136
+ # merge_param!(params, field['name'], (option['value'] || option.text).to_s) if option
137
+ params[name] = (option['value'] || option.text).to_s if option
117
138
  end
118
139
  end
119
140
 
120
141
  def add_textarea_param(field, params)
121
- merge_param!(params, field['name'], field['_capybara_raw_value'].to_s.gsub(/\n/, "\r\n"))
142
+ # merge_param!(params, field['name'], field['_capybara_raw_value'].to_s.gsub(/\r?\n/, "\r\n"))
143
+ params[field['name']] = field['_capybara_raw_value'].to_s.gsub(/\r?\n/, "\r\n")
122
144
  end
123
145
 
124
146
  def submitter?(el)
@@ -1,23 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'capybara/rack_test/errors'
4
+ require 'capybara/node/whitespace_normalizer'
5
+
3
6
  class Capybara::RackTest::Node < Capybara::Driver::Node
7
+ include Capybara::Node::WhitespaceNormalizer
8
+
4
9
  BLOCK_ELEMENTS = %w[p h1 h2 h3 h4 h5 h6 ol ul pre address blockquote dl div fieldset form hr noscript table].freeze
5
10
 
6
11
  def all_text
7
- native.text
8
- .gsub(/[\u200b\u200e\u200f]/, '')
9
- .gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
10
- .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
11
- .gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
12
- .tr("\u00a0", ' ')
12
+ normalize_spacing(native.text)
13
13
  end
14
14
 
15
15
  def visible_text
16
- displayed_text.gsub(/\ +/, ' ')
17
- .gsub(/[\ \n]*\n[\ \n]*/, "\n")
18
- .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
19
- .gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
20
- .tr("\u00a0", ' ')
16
+ normalize_visible_spacing(displayed_text)
21
17
  end
22
18
 
23
19
  def [](name)
@@ -43,6 +39,7 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
43
39
 
44
40
  if radio? then set_radio(value)
45
41
  elsif checkbox? then set_checkbox(value)
42
+ elsif range? then set_range(value)
46
43
  elsif input_field? then set_input(value)
47
44
  elsif textarea? then native['_capybara_raw_value'] = value.to_s
48
45
  end
@@ -61,8 +58,9 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
61
58
  native.remove_attribute('selected')
62
59
  end
63
60
 
64
- def click(keys = [], **offset)
65
- raise ArgumentError, 'The RackTest driver does not support click options' unless keys.empty? && offset.empty?
61
+ def click(keys = [], **options)
62
+ options.delete(:offset)
63
+ raise ArgumentError, 'The RackTest driver does not support click options' unless keys.empty? && options.empty?
66
64
 
67
65
  if link?
68
66
  follow_link
@@ -73,6 +71,8 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
73
71
  set(!checked?)
74
72
  elsif tag_name == 'label'
75
73
  click_label
74
+ elsif (details = native.xpath('.//ancestor-or-self::details').last)
75
+ toggle_details(details)
76
76
  end
77
77
  end
78
78
 
@@ -102,20 +102,35 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
102
102
  end
103
103
  end
104
104
 
105
+ def readonly?
106
+ # readonly attribute not valid on these input types
107
+ return false if input_field? && %w[hidden range color checkbox radio file submit image reset button].include?(type)
108
+
109
+ super
110
+ end
111
+
105
112
  def path
106
113
  native.path
107
114
  end
108
115
 
109
- def find_xpath(locator)
116
+ def find_xpath(locator, **_hints)
110
117
  native.xpath(locator).map { |el| self.class.new(driver, el) }
111
118
  end
112
119
 
113
- def find_css(locator)
120
+ def find_css(locator, **_hints)
114
121
  native.css(locator, Capybara::RackTest::CSSHandlers.new).map { |el| self.class.new(driver, el) }
115
122
  end
116
123
 
117
- def ==(other)
118
- native == other.native
124
+ public_instance_methods(false).each do |meth_name|
125
+ alias_method "unchecked_#{meth_name}", meth_name
126
+ private "unchecked_#{meth_name}" # rubocop:disable Style/AccessModifierDeclarations
127
+
128
+ class_eval <<~METHOD, __FILE__, __LINE__ + 1
129
+ def #{meth_name}(...)
130
+ stale_check
131
+ method(:"unchecked_#{meth_name}").call(...)
132
+ end
133
+ METHOD
119
134
  end
120
135
 
121
136
  protected
@@ -125,22 +140,28 @@ protected
125
140
  if !string_node.visible?(check_ancestor)
126
141
  ''
127
142
  elsif native.text?
128
- native.text
129
- .gsub(/[\u200b\u200e\u200f]/, '')
130
- .gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
143
+ native
144
+ .text
145
+ .delete(REMOVED_CHARACTERS)
146
+ .tr(SQUEEZED_SPACES, ' ')
147
+ .squeeze(' ')
131
148
  elsif native.element?
132
149
  text = native.children.map do |child|
133
150
  Capybara::RackTest::Node.new(driver, child).displayed_text(check_ancestor: false)
134
151
  end.join || ''
135
152
  text = "\n#{text}\n" if BLOCK_ELEMENTS.include?(tag_name)
136
153
  text
137
- else
154
+ else # rubocop:disable Lint/DuplicateBranch
138
155
  ''
139
156
  end
140
157
  end
141
158
 
142
159
  private
143
160
 
161
+ def stale_check
162
+ raise Capybara::RackTest::Errors::StaleElementReferenceError unless native.document == driver.dom
163
+ end
164
+
144
165
  def deselect_options
145
166
  select_node.find_xpath('.//option[@selected]').each { |node| node.native.remove_attribute('selected') }
146
167
  end
@@ -180,6 +201,14 @@ private
180
201
  end
181
202
  end
182
203
 
204
+ def set_range(value) # rubocop:disable Naming/AccessorMethodName
205
+ min, max, step = (native['min'] || 0).to_f, (native['max'] || 100).to_f, (native['step'] || 1).to_f
206
+ value = value.to_f
207
+ value = value.clamp(min, max)
208
+ value = (((value - min) / step).round * step) + min
209
+ native['value'] = value.clamp(min, max)
210
+ end
211
+
183
212
  def set_input(value) # rubocop:disable Naming/AccessorMethodName
184
213
  if text_or_password? && attribute_is_not_blank?(:maxlength)
185
214
  # Browser behavior for maxlength="0" is inconsistent, so we stick with
@@ -195,7 +224,14 @@ private
195
224
  end
196
225
  native.remove
197
226
  else
198
- native['value'] = value.to_s
227
+ value.to_s.tap do |set_value|
228
+ if set_value.end_with?("\n") && form&.css('input, textarea')&.count == 1
229
+ native['value'] = set_value.to_s.chop
230
+ Capybara::RackTest::Form.new(driver, form).submit(self)
231
+ else
232
+ native['value'] = set_value
233
+ end
234
+ end
199
235
  end
200
236
  end
201
237
 
@@ -204,7 +240,7 @@ private
204
240
  end
205
241
 
206
242
  def follow_link
207
- method = self['data-method'] if driver.options[:respect_data_method]
243
+ method = self['data-method'] || self['data-turbo-method'] if driver.options[:respect_data_method]
208
244
  method ||= :get
209
245
  driver.follow(method, self[:href].to_s)
210
246
  end
@@ -219,6 +255,17 @@ private
219
255
  labelled_control.set(!labelled_control.checked?) if checkbox_or_radio?(labelled_control)
220
256
  end
221
257
 
258
+ def toggle_details(details = nil)
259
+ details ||= native.xpath('.//ancestor-or-self::details').last
260
+ return unless details
261
+
262
+ if details.has_attribute?('open')
263
+ details.remove_attribute('open')
264
+ else
265
+ details.set_attribute('open', 'open')
266
+ end
267
+ end
268
+
222
269
  def link?
223
270
  tag_name == 'a' && !self[:href].nil?
224
271
  end
@@ -257,6 +304,10 @@ protected
257
304
  tag_name == 'textarea'
258
305
  end
259
306
 
307
+ def range?
308
+ input_field? && type == 'range'
309
+ end
310
+
260
311
  OPTION_OWNER_XPATH = XPath.parent(:optgroup, :select, :datalist).to_s.freeze
261
312
  DISABLED_BY_FIELDSET_XPATH = XPath.generate do |x|
262
313
  x.parent(:fieldset)[
@@ -0,0 +1,41 @@
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, ...)
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
+ return @registered.public_send(method_name, ...)
23
+ end
24
+ super
25
+ end
26
+
27
+ def respond_to_missing?(method_name, include_all)
28
+ @registered.respond_to?(method_name) || super
29
+ end
30
+
31
+ private
32
+
33
+ def initialize
34
+ @registered = {}
35
+ end
36
+
37
+ def register(name, block)
38
+ @registered[name] = block
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,42 @@
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
+ 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 })
18
+ end
19
+
20
+ Capybara.register_driver :selenium_chrome do |app|
21
+ version = Capybara::Selenium::Driver.load_selenium
22
+ options_key = Capybara::Selenium::Driver::CAPS_VERSION.satisfied_by?(version) ? :capabilities : :options
23
+ browser_options = Selenium::WebDriver::Chrome::Options.new.tap do |opts|
24
+ # Workaround https://bugs.chromium.org/p/chromedriver/issues/detail?id=2650&q=load&sort=-id&colspec=ID%20Status%20Pri%20Owner%20Summary
25
+ opts.add_argument('--disable-site-isolation-trials')
26
+ end
27
+
28
+ Capybara::Selenium::Driver.new(app, **{ :browser => :chrome, options_key => browser_options })
29
+ end
30
+
31
+ Capybara.register_driver :selenium_chrome_headless do |app|
32
+ version = Capybara::Selenium::Driver.load_selenium
33
+ options_key = Capybara::Selenium::Driver::CAPS_VERSION.satisfied_by?(version) ? :capabilities : :options
34
+ browser_options = Selenium::WebDriver::Chrome::Options.new.tap do |opts|
35
+ opts.add_argument('--headless=new')
36
+ opts.add_argument('--disable-gpu') if Gem.win_platform?
37
+ # Workaround https://bugs.chromium.org/p/chromedriver/issues/detail?id=2650&q=load&sort=-id&colspec=ID%20Status%20Pri%20Owner%20Summary
38
+ opts.add_argument('--disable-site-isolation-trials')
39
+ end
40
+
41
+ Capybara::Selenium::Driver.new(app, **{ :browser => :chrome, options_key => browser_options })
42
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+ module MiniSSL
5
+ class Socket
6
+ def read_nonblock(size, *_)
7
+ wait_states = %i[wait_readable wait_writable]
8
+
9
+ loop do
10
+ output = engine_read_all
11
+ return output if output
12
+
13
+ data = @socket.read_nonblock(size, exception: false)
14
+ raise IO::EAGAINWaitReadable if wait_states.include? data
15
+ return nil if data.nil?
16
+
17
+ @engine.inject(data)
18
+ output = engine_read_all
19
+
20
+ return output if output
21
+
22
+ while (neg_data = @engine.extract)
23
+ @socket.write neg_data
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end