capybara 3.0.0 → 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 (312) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/History.md +891 -12
  4. data/License.txt +1 -1
  5. data/README.md +257 -84
  6. data/lib/capybara/config.rb +29 -10
  7. data/lib/capybara/cucumber.rb +1 -1
  8. data/lib/capybara/driver/base.rb +22 -4
  9. data/lib/capybara/driver/node.rb +38 -9
  10. data/lib/capybara/dsl.rb +9 -7
  11. data/lib/capybara/helpers.rb +57 -8
  12. data/lib/capybara/minitest/spec.rb +185 -84
  13. data/lib/capybara/minitest.rb +264 -145
  14. data/lib/capybara/node/actions.rb +248 -124
  15. data/lib/capybara/node/base.rb +35 -20
  16. data/lib/capybara/node/document.rb +14 -2
  17. data/lib/capybara/node/document_matchers.rb +13 -15
  18. data/lib/capybara/node/element.rb +350 -113
  19. data/lib/capybara/node/finders.rb +104 -82
  20. data/lib/capybara/node/matchers.rb +363 -157
  21. data/lib/capybara/node/simple.rb +54 -15
  22. data/lib/capybara/node/whitespace_normalizer.rb +81 -0
  23. data/lib/capybara/queries/active_element_query.rb +18 -0
  24. data/lib/capybara/queries/ancestor_query.rb +9 -10
  25. data/lib/capybara/queries/base_query.rb +25 -18
  26. data/lib/capybara/queries/current_path_query.rb +16 -6
  27. data/lib/capybara/queries/match_query.rb +11 -0
  28. data/lib/capybara/queries/selector_query.rb +617 -104
  29. data/lib/capybara/queries/sibling_query.rb +9 -7
  30. data/lib/capybara/queries/style_query.rb +45 -0
  31. data/lib/capybara/queries/text_query.rb +40 -22
  32. data/lib/capybara/queries/title_query.rb +2 -2
  33. data/lib/capybara/rack_test/browser.rb +106 -31
  34. data/lib/capybara/rack_test/driver.rb +16 -7
  35. data/lib/capybara/rack_test/errors.rb +6 -0
  36. data/lib/capybara/rack_test/form.rb +74 -49
  37. data/lib/capybara/rack_test/node.rb +120 -47
  38. data/lib/capybara/rails.rb +1 -1
  39. data/lib/capybara/registration_container.rb +41 -0
  40. data/lib/capybara/registrations/drivers.rb +42 -0
  41. data/lib/capybara/registrations/patches/puma_ssl.rb +29 -0
  42. data/lib/capybara/registrations/servers.rb +66 -0
  43. data/lib/capybara/result.rb +87 -53
  44. data/lib/capybara/rspec/features.rb +8 -10
  45. data/lib/capybara/rspec/matcher_proxies.rb +39 -18
  46. data/lib/capybara/rspec/matchers/base.rb +113 -0
  47. data/lib/capybara/rspec/matchers/become_closed.rb +33 -0
  48. data/lib/capybara/rspec/matchers/compound.rb +88 -0
  49. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  50. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  51. data/lib/capybara/rspec/matchers/have_current_path.rb +29 -0
  52. data/lib/capybara/rspec/matchers/have_selector.rb +69 -0
  53. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  54. data/lib/capybara/rspec/matchers/have_text.rb +33 -0
  55. data/lib/capybara/rspec/matchers/have_title.rb +29 -0
  56. data/lib/capybara/rspec/matchers/match_selector.rb +27 -0
  57. data/lib/capybara/rspec/matchers/match_style.rb +43 -0
  58. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  59. data/lib/capybara/rspec/matchers.rb +142 -315
  60. data/lib/capybara/rspec.rb +3 -2
  61. data/lib/capybara/selector/builders/css_builder.rb +84 -0
  62. data/lib/capybara/selector/builders/xpath_builder.rb +71 -0
  63. data/lib/capybara/selector/css.rb +85 -8
  64. data/lib/capybara/selector/definition/button.rb +68 -0
  65. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  66. data/lib/capybara/selector/definition/css.rb +10 -0
  67. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  68. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  69. data/lib/capybara/selector/definition/element.rb +28 -0
  70. data/lib/capybara/selector/definition/field.rb +40 -0
  71. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  72. data/lib/capybara/selector/definition/file_field.rb +13 -0
  73. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  74. data/lib/capybara/selector/definition/frame.rb +17 -0
  75. data/lib/capybara/selector/definition/id.rb +6 -0
  76. data/lib/capybara/selector/definition/label.rb +62 -0
  77. data/lib/capybara/selector/definition/link.rb +55 -0
  78. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  79. data/lib/capybara/selector/definition/option.rb +27 -0
  80. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  81. data/lib/capybara/selector/definition/select.rb +81 -0
  82. data/lib/capybara/selector/definition/table.rb +109 -0
  83. data/lib/capybara/selector/definition/table_row.rb +21 -0
  84. data/lib/capybara/selector/definition/xpath.rb +5 -0
  85. data/lib/capybara/selector/definition.rb +280 -0
  86. data/lib/capybara/selector/filter.rb +1 -0
  87. data/lib/capybara/selector/filter_set.rb +72 -28
  88. data/lib/capybara/selector/filters/base.rb +45 -2
  89. data/lib/capybara/selector/filters/expression_filter.rb +5 -6
  90. data/lib/capybara/selector/filters/locator_filter.rb +29 -0
  91. data/lib/capybara/selector/filters/node_filter.rb +18 -4
  92. data/lib/capybara/selector/regexp_disassembler.rb +211 -0
  93. data/lib/capybara/selector/selector.rb +89 -200
  94. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  95. data/lib/capybara/selector.rb +474 -534
  96. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  97. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  98. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  99. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  100. data/lib/capybara/selenium/driver.rb +270 -245
  101. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +117 -0
  102. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +128 -0
  103. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +84 -0
  104. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +26 -0
  105. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +24 -0
  106. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  107. data/lib/capybara/selenium/extensions/find.rb +110 -0
  108. data/lib/capybara/selenium/extensions/html5_drag.rb +229 -0
  109. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  110. data/lib/capybara/selenium/extensions/scroll.rb +76 -0
  111. data/lib/capybara/selenium/node.rb +460 -170
  112. data/lib/capybara/selenium/nodes/chrome_node.rb +125 -0
  113. data/lib/capybara/selenium/nodes/edge_node.rb +110 -0
  114. data/lib/capybara/selenium/nodes/firefox_node.rb +136 -0
  115. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  116. data/lib/capybara/selenium/nodes/safari_node.rb +118 -0
  117. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  118. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  119. data/lib/capybara/selenium/patches/logs.rb +45 -0
  120. data/lib/capybara/selenium/patches/pause_duration_fix.rb +9 -0
  121. data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
  122. data/lib/capybara/server/animation_disabler.rb +80 -0
  123. data/lib/capybara/server/checker.rb +44 -0
  124. data/lib/capybara/server/middleware.rb +71 -0
  125. data/lib/capybara/server.rb +58 -67
  126. data/lib/capybara/session/config.rb +40 -6
  127. data/lib/capybara/session/matchers.rb +26 -19
  128. data/lib/capybara/session.rb +252 -194
  129. data/lib/capybara/spec/public/jquery.js +5 -5
  130. data/lib/capybara/spec/public/offset.js +6 -0
  131. data/lib/capybara/spec/public/test.js +126 -8
  132. data/lib/capybara/spec/session/accept_alert_spec.rb +11 -11
  133. data/lib/capybara/spec/session/accept_confirm_spec.rb +3 -3
  134. data/lib/capybara/spec/session/accept_prompt_spec.rb +9 -10
  135. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  136. data/lib/capybara/spec/session/all_spec.rb +135 -44
  137. data/lib/capybara/spec/session/ancestor_spec.rb +24 -19
  138. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +67 -38
  139. data/lib/capybara/spec/session/{assert_current_path.rb → assert_current_path_spec.rb} +20 -18
  140. data/lib/capybara/spec/session/assert_selector_spec.rb +143 -0
  141. data/lib/capybara/spec/session/assert_style_spec.rb +26 -0
  142. data/lib/capybara/spec/session/{assert_text.rb → assert_text_spec.rb} +76 -52
  143. data/lib/capybara/spec/session/{assert_title.rb → assert_title_spec.rb} +12 -12
  144. data/lib/capybara/spec/session/attach_file_spec.rb +126 -72
  145. data/lib/capybara/spec/session/body_spec.rb +11 -13
  146. data/lib/capybara/spec/session/check_spec.rb +112 -51
  147. data/lib/capybara/spec/session/choose_spec.rb +62 -30
  148. data/lib/capybara/spec/session/click_button_spec.rb +227 -161
  149. data/lib/capybara/spec/session/click_link_or_button_spec.rb +49 -30
  150. data/lib/capybara/spec/session/click_link_spec.rb +89 -55
  151. data/lib/capybara/spec/session/current_scope_spec.rb +8 -8
  152. data/lib/capybara/spec/session/current_url_spec.rb +44 -37
  153. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +3 -3
  154. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +2 -2
  155. data/lib/capybara/spec/session/element/{assert_match_selector.rb → assert_match_selector_spec.rb} +11 -9
  156. data/lib/capybara/spec/session/element/match_css_spec.rb +18 -10
  157. data/lib/capybara/spec/session/element/match_xpath_spec.rb +8 -6
  158. data/lib/capybara/spec/session/element/matches_selector_spec.rb +70 -56
  159. data/lib/capybara/spec/session/evaluate_async_script_spec.rb +7 -7
  160. data/lib/capybara/spec/session/evaluate_script_spec.rb +28 -8
  161. data/lib/capybara/spec/session/execute_script_spec.rb +8 -7
  162. data/lib/capybara/spec/session/fill_in_spec.rb +110 -44
  163. data/lib/capybara/spec/session/find_button_spec.rb +23 -23
  164. data/lib/capybara/spec/session/find_by_id_spec.rb +8 -8
  165. data/lib/capybara/spec/session/find_field_spec.rb +33 -31
  166. data/lib/capybara/spec/session/find_link_spec.rb +42 -14
  167. data/lib/capybara/spec/session/find_spec.rb +251 -142
  168. data/lib/capybara/spec/session/first_spec.rb +45 -44
  169. data/lib/capybara/spec/session/frame/frame_title_spec.rb +6 -6
  170. data/lib/capybara/spec/session/frame/frame_url_spec.rb +6 -6
  171. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +32 -20
  172. data/lib/capybara/spec/session/frame/within_frame_spec.rb +46 -19
  173. data/lib/capybara/spec/session/go_back_spec.rb +1 -1
  174. data/lib/capybara/spec/session/go_forward_spec.rb +1 -1
  175. data/lib/capybara/spec/session/has_all_selectors_spec.rb +23 -23
  176. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  177. data/lib/capybara/spec/session/has_any_selectors_spec.rb +29 -0
  178. data/lib/capybara/spec/session/has_button_spec.rb +98 -12
  179. data/lib/capybara/spec/session/has_css_spec.rb +271 -137
  180. data/lib/capybara/spec/session/has_current_path_spec.rb +50 -35
  181. data/lib/capybara/spec/session/has_element_spec.rb +47 -0
  182. data/lib/capybara/spec/session/has_field_spec.rb +137 -58
  183. data/lib/capybara/spec/session/has_link_spec.rb +46 -6
  184. data/lib/capybara/spec/session/has_none_selectors_spec.rb +33 -31
  185. data/lib/capybara/spec/session/has_select_spec.rb +84 -50
  186. data/lib/capybara/spec/session/has_selector_spec.rb +117 -69
  187. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  188. data/lib/capybara/spec/session/has_table_spec.rb +181 -4
  189. data/lib/capybara/spec/session/has_text_spec.rb +101 -53
  190. data/lib/capybara/spec/session/has_title_spec.rb +19 -14
  191. data/lib/capybara/spec/session/has_xpath_spec.rb +56 -38
  192. data/lib/capybara/spec/session/{headers.rb → headers_spec.rb} +1 -1
  193. data/lib/capybara/spec/session/html_spec.rb +13 -6
  194. data/lib/capybara/spec/session/matches_style_spec.rb +37 -0
  195. data/lib/capybara/spec/session/node_spec.rb +958 -122
  196. data/lib/capybara/spec/session/node_wrapper_spec.rb +15 -12
  197. data/lib/capybara/spec/session/refresh_spec.rb +9 -7
  198. data/lib/capybara/spec/session/reset_session_spec.rb +65 -37
  199. data/lib/capybara/spec/session/{response_code.rb → response_code_spec.rb} +1 -1
  200. data/lib/capybara/spec/session/save_and_open_page_spec.rb +2 -2
  201. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +5 -4
  202. data/lib/capybara/spec/session/save_page_spec.rb +41 -38
  203. data/lib/capybara/spec/session/save_screenshot_spec.rb +13 -11
  204. data/lib/capybara/spec/session/screenshot_spec.rb +2 -2
  205. data/lib/capybara/spec/session/scroll_spec.rb +119 -0
  206. data/lib/capybara/spec/session/select_spec.rb +102 -76
  207. data/lib/capybara/spec/session/selectors_spec.rb +51 -18
  208. data/lib/capybara/spec/session/sibling_spec.rb +9 -9
  209. data/lib/capybara/spec/session/text_spec.rb +26 -24
  210. data/lib/capybara/spec/session/title_spec.rb +8 -6
  211. data/lib/capybara/spec/session/uncheck_spec.rb +41 -22
  212. data/lib/capybara/spec/session/unselect_spec.rb +37 -37
  213. data/lib/capybara/spec/session/visit_spec.rb +79 -53
  214. data/lib/capybara/spec/session/window/become_closed_spec.rb +22 -19
  215. data/lib/capybara/spec/session/window/current_window_spec.rb +4 -3
  216. data/lib/capybara/spec/session/window/open_new_window_spec.rb +4 -3
  217. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +25 -21
  218. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +10 -5
  219. data/lib/capybara/spec/session/window/window_spec.rb +88 -54
  220. data/lib/capybara/spec/session/window/windows_spec.rb +11 -8
  221. data/lib/capybara/spec/session/window/within_window_spec.rb +17 -16
  222. data/lib/capybara/spec/session/within_spec.rb +82 -44
  223. data/lib/capybara/spec/spec_helper.rb +46 -52
  224. data/lib/capybara/spec/test_app.rb +148 -41
  225. data/lib/capybara/spec/views/animated.erb +49 -0
  226. data/lib/capybara/spec/views/form.erb +156 -42
  227. data/lib/capybara/spec/views/frame_child.erb +4 -3
  228. data/lib/capybara/spec/views/frame_one.erb +2 -1
  229. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  230. data/lib/capybara/spec/views/frame_two.erb +1 -1
  231. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  232. data/lib/capybara/spec/views/layout.erb +10 -0
  233. data/lib/capybara/spec/views/obscured.erb +47 -0
  234. data/lib/capybara/spec/views/offset.erb +33 -0
  235. data/lib/capybara/spec/views/path.erb +2 -2
  236. data/lib/capybara/spec/views/popup_one.erb +1 -1
  237. data/lib/capybara/spec/views/popup_two.erb +1 -1
  238. data/lib/capybara/spec/views/react.erb +45 -0
  239. data/lib/capybara/spec/views/scroll.erb +21 -0
  240. data/lib/capybara/spec/views/spatial.erb +31 -0
  241. data/lib/capybara/spec/views/tables.erb +68 -1
  242. data/lib/capybara/spec/views/with_animation.erb +81 -0
  243. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  244. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  245. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  246. data/lib/capybara/spec/views/with_hover.erb +3 -2
  247. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  248. data/lib/capybara/spec/views/with_html.erb +46 -11
  249. data/lib/capybara/spec/views/with_html5_svg.erb +20 -0
  250. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  251. data/lib/capybara/spec/views/with_js.erb +30 -5
  252. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  253. data/lib/capybara/spec/views/with_namespace.erb +21 -0
  254. data/lib/capybara/spec/views/with_scope.erb +2 -2
  255. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  256. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  257. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  258. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  259. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  260. data/lib/capybara/spec/views/with_windows.erb +1 -1
  261. data/lib/capybara/spec/views/within_frames.erb +1 -1
  262. data/lib/capybara/version.rb +1 -1
  263. data/lib/capybara/window.rb +32 -26
  264. data/lib/capybara.rb +128 -104
  265. data/spec/basic_node_spec.rb +59 -34
  266. data/spec/capybara_spec.rb +65 -51
  267. data/spec/counter_spec.rb +35 -0
  268. data/spec/css_builder_spec.rb +101 -0
  269. data/spec/css_splitter_spec.rb +38 -0
  270. data/spec/dsl_spec.rb +84 -55
  271. data/spec/filter_set_spec.rb +24 -7
  272. data/spec/fixtures/certificate.pem +25 -0
  273. data/spec/fixtures/key.pem +27 -0
  274. data/spec/fixtures/selenium_driver_rspec_failure.rb +5 -5
  275. data/spec/fixtures/selenium_driver_rspec_success.rb +5 -5
  276. data/spec/minitest_spec.rb +49 -5
  277. data/spec/minitest_spec_spec.rb +92 -62
  278. data/spec/per_session_config_spec.rb +6 -6
  279. data/spec/rack_test_spec.rb +183 -115
  280. data/spec/regexp_dissassembler_spec.rb +250 -0
  281. data/spec/result_spec.rb +99 -39
  282. data/spec/rspec/features_spec.rb +28 -25
  283. data/spec/rspec/scenarios_spec.rb +10 -6
  284. data/spec/rspec/shared_spec_matchers.rb +418 -364
  285. data/spec/rspec/views_spec.rb +4 -3
  286. data/spec/rspec_matchers_spec.rb +35 -10
  287. data/spec/rspec_spec.rb +109 -85
  288. data/spec/sauce_spec_chrome.rb +43 -0
  289. data/spec/selector_spec.rb +392 -62
  290. data/spec/selenium_spec_chrome.rb +183 -41
  291. data/spec/selenium_spec_chrome_remote.rb +96 -0
  292. data/spec/selenium_spec_edge.rb +41 -8
  293. data/spec/selenium_spec_firefox.rb +228 -0
  294. data/spec/selenium_spec_firefox_remote.rb +94 -0
  295. data/spec/selenium_spec_ie.rb +129 -11
  296. data/spec/selenium_spec_safari.rb +162 -0
  297. data/spec/server_spec.rb +192 -81
  298. data/spec/session_spec.rb +52 -16
  299. data/spec/shared_selenium_node.rb +79 -0
  300. data/spec/shared_selenium_session.rb +460 -123
  301. data/spec/spec_helper.rb +124 -2
  302. data/spec/whitespace_normalizer_spec.rb +54 -0
  303. data/spec/xpath_builder_spec.rb +93 -0
  304. metadata +344 -45
  305. data/.yard/templates_custom/default/class/html/selectors.erb +0 -38
  306. data/.yard/templates_custom/default/class/html/setup.rb +0 -17
  307. data/.yard/yard_extensions.rb +0 -78
  308. data/lib/capybara/rspec/compound.rb +0 -90
  309. data/lib/capybara/spec/session/assert_selector.rb +0 -149
  310. data/lib/capybara/spec/session/source_spec.rb +0 -0
  311. data/lib/capybara/spec/views/with_title.erb +0 -5
  312. data/spec/selenium_spec_marionette.rb +0 -143
@@ -2,21 +2,23 @@
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
- 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
10
+ scope = node.respond_to?(:session) ? node.session.current_scope : node.find(:xpath, '/*')
11
+ match_results = super(scope, exact)
12
+ siblings = node.find_xpath((XPath.preceding_sibling + XPath.following_sibling).to_s)
13
+ .map(&method(:to_element))
14
+ .select { |el| match_results.include?(el) }
15
+ Capybara::Result.new(ordered_results(siblings), self)
14
16
  end
15
17
  end
16
18
 
17
- def description
19
+ def description(applied = false) # rubocop:disable Style/OptionalBooleanParameter
18
20
  desc = super
19
- sibling_query = @sibling_node && @sibling_node.instance_variable_get(:@query)
21
+ sibling_query = @sibling_node&.instance_variable_get(:@query)
20
22
  desc += " that is a sibling of #{sibling_query.description}" if sibling_query
21
23
  desc
22
24
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ # @api private
5
+ module Queries
6
+ class StyleQuery < BaseQuery
7
+ def initialize(expected_styles, session_options:, **options)
8
+ @expected_styles = stringify_keys(expected_styles)
9
+ @options = options
10
+ @actual_styles = {}
11
+ super(@options)
12
+ self.session_options = session_options
13
+
14
+ assert_valid_keys
15
+ end
16
+
17
+ def resolves_for?(node)
18
+ @node = node
19
+ @actual_styles = node.style(*@expected_styles.keys)
20
+ @expected_styles.all? do |style, value|
21
+ if value.is_a? Regexp
22
+ value.match? @actual_styles[style]
23
+ else
24
+ @actual_styles[style] == value
25
+ end
26
+ end
27
+ end
28
+
29
+ def failure_message
30
+ +"Expected node to have styles #{@expected_styles.inspect}. " \
31
+ "Actual styles were #{@actual_styles.inspect}"
32
+ end
33
+
34
+ private
35
+
36
+ def stringify_keys(hsh)
37
+ hsh.transform_keys(&:to_s)
38
+ end
39
+
40
+ def valid_keys
41
+ %i[wait]
42
+ end
43
+ end
44
+ end
45
+ end
@@ -5,17 +5,20 @@ module Capybara
5
5
  module Queries
6
6
  class TextQuery < BaseQuery
7
7
  def initialize(type = nil, expected_text, session_options:, **options) # rubocop:disable Style/OptionalArguments
8
- @type = if type.nil?
9
- Capybara.ignore_hidden_elements || Capybara.visible_text_only ? :visible : :all
10
- else
11
- type
12
- end
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)
13
10
 
14
- @expected_text = expected_text.is_a?(Regexp) ? expected_text : expected_text.to_s
15
11
  @options = options
16
12
  super(@options)
17
13
  self.session_options = session_options
18
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
+
19
22
  @search_regexp = Capybara::Helpers.to_regexp(@expected_text, exact: exact?)
20
23
 
21
24
  assert_valid_keys
@@ -23,7 +26,7 @@ module Capybara
23
26
 
24
27
  def resolve_for(node)
25
28
  @node = node
26
- @actual_text = text(node, @type)
29
+ @actual_text = text
27
30
  @count = @actual_text.scan(@search_regexp).size
28
31
  end
29
32
 
@@ -39,7 +42,7 @@ module Capybara
39
42
  if @expected_text.is_a?(Regexp)
40
43
  "text matching #{@expected_text.inspect}"
41
44
  else
42
- "#{"exact " if exact?}text #{@expected_text.inspect}"
45
+ "#{'exact ' if exact?}text #{@expected_text.inspect}"
43
46
  end
44
47
  end
45
48
 
@@ -50,15 +53,15 @@ module Capybara
50
53
  end
51
54
 
52
55
  def build_message(report_on_invisible)
53
- message = "".dup
56
+ message = +''
54
57
  unless (COUNT_KEYS & @options.keys).empty?
55
58
  message << " but found #{@count} #{Capybara::Helpers.declension('time', 'times', @count)}"
56
59
  end
57
60
  message << " in #{@actual_text.inspect}"
58
61
 
59
62
  details_message = []
60
- details_message << case_insensitive_message if @node and !@expected_text.is_a? Regexp
61
- details_message << invisible_message if @node and check_visible_text? and report_on_invisible
63
+ details_message << case_insensitive_message if @node && check_case_insensitive?
64
+ details_message << invisible_message if @node && check_visible_text? && report_on_invisible
62
65
  details_message.compact!
63
66
 
64
67
  message << ". (However, #{details_message.join(' and ')}.)" unless details_message.empty?
@@ -68,30 +71,45 @@ module Capybara
68
71
  def case_insensitive_message
69
72
  insensitive_regexp = Capybara::Helpers.to_regexp(@expected_text, options: Regexp::IGNORECASE)
70
73
  insensitive_count = @actual_text.scan(insensitive_regexp).size
71
- if insensitive_count != @count
72
- "it was found #{insensitive_count} #{Capybara::Helpers.declension("time", "times", insensitive_count)} using a case insensitive search"
73
- end
74
+ return if insensitive_count == @count
75
+
76
+ "it was found #{occurrences insensitive_count} using a case insensitive search"
74
77
  end
75
78
 
76
79
  def invisible_message
77
- invisible_text = text(@node, :all)
80
+ invisible_text = text(query_type: :all)
78
81
  invisible_count = invisible_text.scan(@search_regexp).size
79
- if invisible_count != @count
80
- "it was found #{invisible_count} #{Capybara::Helpers.declension("time", "times", invisible_count)} including non-visible text"
81
- end
82
- rescue # An error getting the non-visible text (if element goes out of scope) should not affect the response
82
+ return if invisible_count == @count
83
+
84
+ "it was found #{occurrences invisible_count} including non-visible text"
85
+ rescue StandardError
86
+ # An error getting the non-visible text (if element goes out of scope) should not affect the response
87
+ nil
83
88
  end
84
89
 
85
90
  def valid_keys
86
- COUNT_KEYS + %i[wait exact]
91
+ COUNT_KEYS + %i[wait exact normalize_ws]
92
+ end
93
+
94
+ def valid_types
95
+ %i[all visible]
87
96
  end
88
97
 
89
98
  def check_visible_text?
90
99
  @type == :visible
91
100
  end
92
101
 
93
- def text(node, query_type)
94
- node.text(query_type)
102
+ def check_case_insensitive?
103
+ !@expected_text.is_a?(Regexp)
104
+ end
105
+
106
+ def text(node: @node, query_type: @type)
107
+ normalize_ws = options.fetch(:normalize_ws, session_options.default_normalize_ws)
108
+ node.text(query_type, normalize_ws: normalize_ws)
109
+ end
110
+
111
+ def default_type
112
+ Capybara.ignore_hidden_elements || Capybara.visible_text_only ? :visible : :all
95
113
  end
96
114
  end
97
115
  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
@@ -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,60 +31,83 @@ 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
- return if path.gsub(/^#{Regexp.escape(request_path)}/, '').start_with?('#') || path.downcase.start_with?('javascript:')
38
- process_and_follow_redirects(method, path, attributes, 'HTTP_REFERER' => current_url)
49
+ return if fragment_or_script?(path)
50
+
51
+ process_and_follow_redirects(method, path, attributes, 'HTTP_REFERER' => referer_url)
39
52
  end
40
53
 
41
54
  def process_and_follow_redirects(method, path, attributes = {}, env = {})
55
+ @current_fragment = build_uri(path).fragment
42
56
  process(method, path, attributes, env)
43
-
44
57
  return unless driver.follow_redirects?
45
58
 
46
59
  driver.redirect_limit.times do
47
- process(:get, last_response["Location"], {}, env) if last_response.redirect?
60
+ if last_response.redirect?
61
+ if [307, 308].include? last_response.status
62
+ process(last_request.request_method, last_response['Location'], last_request.params, env)
63
+ else
64
+ process(:get, last_response['Location'], {}, env)
65
+ end
66
+ end
48
67
  end
49
- raise Capybara::InfiniteRedirectError, "redirected more than #{driver.redirect_limit} times, check for infinite redirects." if last_response.redirect?
50
- end
51
68
 
52
- def process(method, path, attributes = {}, env = {})
53
- new_uri = URI.parse(path)
54
- method.downcase! unless method.is_a? Symbol
55
- if path.empty?
56
- new_uri.path = request_path
57
- else
58
- new_uri.path = request_path if path.start_with?("?")
59
- new_uri.path = "/" if new_uri.path.empty?
60
- new_uri.path = request_path.sub(%r{/[^/]*$}, '/') + new_uri.path unless new_uri.path.start_with?('/')
69
+ if last_response.redirect? # rubocop:disable Style/GuardClause
70
+ raise Capybara::InfiniteRedirectError, "redirected more than #{driver.redirect_limit} times, check for infinite redirects."
61
71
  end
62
- new_uri.scheme ||= @current_scheme
63
- new_uri.host ||= @current_host
64
- new_uri.port ||= @current_port unless new_uri.default_port == @current_port
65
-
66
- @current_scheme = new_uri.scheme
67
- @current_host = new_uri.host
68
- @current_port = new_uri.port
72
+ end
69
73
 
74
+ def process(method, path, attributes = {}, env = {})
75
+ method = method.downcase
76
+ new_uri = build_uri(path)
77
+ @current_scheme, @current_host, @current_port = new_uri.select(:scheme, :host, :port)
78
+ @current_fragment = new_uri.fragment || @current_fragment
70
79
  reset_cache!
80
+ @new_visit_request = false
71
81
  send(method, new_uri.to_s, attributes, env.merge(options[:headers] || {}))
72
82
  end
73
83
 
84
+ def build_uri(path)
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?('/')
89
+
90
+ if base_uri.absolute?
91
+ base_uri.merge(uri)
92
+ else
93
+ uri.scheme ||= @current_scheme
94
+ uri.host ||= @current_host
95
+ uri.port ||= @current_port unless uri.default_port == @current_port
96
+ uri
97
+ end
98
+ end
99
+
74
100
  def current_url
75
- last_request.url
101
+ uri = build_uri(last_request.url)
102
+ uri.fragment = @current_fragment if @current_fragment
103
+ uri.to_s
76
104
  rescue Rack::Test::Error
77
- ""
105
+ ''
78
106
  end
79
107
 
80
108
  def reset_host!
81
109
  uri = URI.parse(driver.session_options.app_host || driver.session_options.default_host)
82
- @current_scheme = uri.scheme
83
- @current_host = uri.host
84
- @current_port = uri.port
110
+ @current_scheme, @current_host, @current_port = uri.select(:scheme, :host, :port)
85
111
  end
86
112
 
87
113
  def reset_cache!
@@ -103,15 +129,46 @@ class Capybara::RackTest::Browser
103
129
  def html
104
130
  last_response.body
105
131
  rescue Rack::Test::Error
106
- ""
132
+ ''
107
133
  end
108
134
 
109
135
  def title
110
136
  dom.title
111
137
  end
112
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
+
113
151
  protected
114
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
+
115
172
  def build_rack_mock_session
116
173
  reset_host! unless current_host
117
174
  Rack::MockSession.new(app, current_host)
@@ -120,6 +177,24 @@ protected
120
177
  def request_path
121
178
  last_request.path
122
179
  rescue Rack::Test::Error
123
- "/"
180
+ '/'
181
+ end
182
+
183
+ def safe_last_request
184
+ last_request
185
+ rescue Rack::Test::Error
186
+ nil
187
+ end
188
+
189
+ private
190
+
191
+ def fragment_or_script?(path)
192
+ path.gsub(/^#{Regexp.escape(request_path)}/, '').start_with?('#') || path.downcase.start_with?('javascript:')
193
+ end
194
+
195
+ def referer_url
196
+ build_uri(last_request.url).to_s
197
+ rescue Rack::Test::Error
198
+ ''
124
199
  end
125
200
  end
@@ -15,8 +15,9 @@ class Capybara::RackTest::Driver < Capybara::Driver::Base
15
15
  attr_reader :app, :options
16
16
 
17
17
  def initialize(app, **options)
18
- raise ArgumentError, "rack-test requires a rack application, but none was given" unless app
19
- @session = nil
18
+ raise ArgumentError, 'rack-test requires a rack application, but none was given' unless app
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
@@ -75,6 +76,10 @@ class Capybara::RackTest::Driver < Capybara::Driver::Base
75
76
 
76
77
  def find_css(selector)
77
78
  browser.find(:css, selector)
79
+ rescue Nokogiri::CSS::SyntaxError
80
+ raise unless selector.include?(' i]')
81
+
82
+ raise ArgumentError, "This driver doesn't support case insensitive attribute matching when using CSS base selectors"
78
83
  end
79
84
 
80
85
  def html
@@ -93,9 +98,13 @@ class Capybara::RackTest::Driver < Capybara::Driver::Base
93
98
  @browser = nil
94
99
  end
95
100
 
96
- def get(*args, &block); browser.get(*args, &block); end
97
- def post(*args, &block); browser.post(*args, &block); end
98
- def put(*args, &block); browser.put(*args, &block); end
99
- 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
100
105
  def header(key, value); browser.header(key, value); end
106
+
107
+ def invalid_element_errors
108
+ [Capybara::RackTest::Errors::StaleElementReferenceError]
109
+ end
101
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,51 +6,59 @@ 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
10
- @empty_file = Tempfile.new("nil_uploaded_file")
9
+ def initialize # rubocop:disable Lint/MissingSuper
10
+ @empty_file = Tempfile.new('nil_uploaded_file')
11
11
  @empty_file.close
12
12
  end
13
13
 
14
- def original_filename; ""; end
15
- def content_type; "application/octet-stream"; end
14
+ def original_filename; ''; end
15
+ def content_type; 'application/octet-stream'; end
16
16
  def path; @empty_file.path; end
17
17
  def size; 0; end
18
- def read; ""; end
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)
22
- params = make_params
23
-
24
- form_element_types = %i[input select textarea]
25
- form_elements_xpath = XPath.generate do |x|
26
- xpath = x.descendant(*form_element_types).where(!x.attr(:form))
27
- xpath += x.anywhere(*form_element_types).where(x.attr(:form) == native[:id]) if native[:id]
28
- xpath.where(!x.attr(:disabled))
24
+ form_element_types = %i[input select textarea button]
25
+ form_elements_xpath = XPath.generate do |xp|
26
+ xpath = xp.descendant(*form_element_types).where(!xp.attr(:form))
27
+ xpath += xp.anywhere(*form_element_types).where(xp.attr(:form) == native[:id]) if native[:id]
28
+ xpath.where(!xp.attr(:disabled))
29
29
  end.to_s
30
30
 
31
- native.xpath(form_elements_xpath).map do |field|
31
+ form_elements = native.xpath(form_elements_xpath).reject { |el| submitter?(el) && (el != button.native) }
32
+
33
+ form_params = form_elements.each_with_object({}.compare_by_identity) do |field, params|
32
34
  case field.name
33
- when 'input'
34
- add_input_param(field, params)
35
- when 'select'
36
- add_select_param(field, params)
37
- when 'textarea'
38
- add_textarea_param(field, params)
35
+ when 'input', 'button' then add_input_param(field, params)
36
+ when 'select' then add_select_param(field, params)
37
+ when 'textarea' then add_textarea_param(field, params)
39
38
  end
40
39
  end
41
- merge_param!(params, button[:name], button[:value] || "") if button[:name]
42
40
 
43
- params.to_params_hash
41
+ form_params.each_with_object(make_params) do |(name, value), params|
42
+ merge_param!(params, name, value)
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
44
52
  end
45
53
 
46
54
  def submit(button)
47
- action = (button && button['formaction']) || native['action']
48
- method = (button && button['formmethod']) || request_method
49
- driver.submit(method, action.to_s, params(button))
55
+ action = button&.[]('formaction') || native['action']
56
+ method = button&.[]('formmethod') || request_method
57
+ driver.submit(method, action.to_s, params(button), content_type: native['enctype'])
50
58
  end
51
59
 
52
60
  def multipart?
53
- self[:enctype] == "multipart/form-data"
61
+ self[:enctype] == 'multipart/form-data'
54
62
  end
55
63
 
56
64
  private
@@ -62,10 +70,11 @@ private
62
70
  end
63
71
 
64
72
  def request_method
65
- self[:method] =~ /post/i ? :post : :get
73
+ /post/i.match?(self[:method] || '') ? :post : :get
66
74
  end
67
75
 
68
76
  def merge_param!(params, key, value)
77
+ key = key.to_s
69
78
  if Rack::Utils.respond_to?(:default_query_parser)
70
79
  Rack::Utils.default_query_parser.normalize_params(params, key, value, Rack::Utils.param_depth_limit)
71
80
  else
@@ -82,43 +91,59 @@ private
82
91
  end
83
92
 
84
93
  def add_input_param(field, params)
85
- if %w[radio checkbox].include? field['type']
86
- if field['checked']
87
- node = Capybara::RackTest::Node.new(driver, field)
88
- merge_param!(params, field['name'].to_s, node.value.to_s)
89
- end
90
- elsif %w[submit image].include? field['type']
91
- # TODO: identify the click button here (in document order, rather
92
- # than leaving until the end of the params)
93
- elsif field['type'] == 'file'
94
+ name, value = field['name'].to_s, field['value'].to_s
95
+ return if name.empty?
96
+
97
+ value = case field['type']
98
+ when 'radio', 'checkbox'
99
+ return unless field['checked']
100
+
101
+ Capybara::RackTest::Node.new(driver, field).value.to_s
102
+ when 'file'
103
+ return if value.empty? && params.keys.include?(name) && Rack::Test::VERSION.to_f >= 2.0 # rubocop:disable Performance/InefficientHashSearch
104
+
94
105
  if multipart?
95
- file = if (value = field['value']).to_s.empty?
96
- NilUploadedFile.new
97
- else
98
- mime_info = MiniMime.lookup_by_filename(value)
99
- Rack::Test::UploadedFile.new(value, (mime_info && mime_info.content_type).to_s)
100
- end
101
- merge_param!(params, field['name'].to_s, file)
106
+ file_to_upload(value)
102
107
  else
103
- merge_param!(params, field['name'].to_s, File.basename(field['value'].to_s))
108
+ File.basename(value)
104
109
  end
105
110
  else
106
- merge_param!(params, field['name'].to_s, field['value'].to_s)
111
+ value
112
+ end
113
+ # merge_param!(params, name, value)
114
+ params[name] = value
115
+ end
116
+
117
+ def file_to_upload(filename)
118
+ if filename.empty?
119
+ NilUploadedFile.new
120
+ else
121
+ mime_info = MiniMime.lookup_by_filename(filename)
122
+ Rack::Test::UploadedFile.new(filename, mime_info&.content_type&.to_s)
107
123
  end
108
124
  end
109
125
 
110
126
  def add_select_param(field, params)
111
- if field['multiple'] == 'multiple'
112
- field.xpath(".//option[@selected]").each do |option|
113
- merge_param!(params, field['name'].to_s, (option['value'] || option.text).to_s)
127
+ name = field['name']
128
+ if field.has_attribute?('multiple')
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
114
132
  end
133
+ params[name] = value unless value.empty?
115
134
  else
116
135
  option = field.xpath('.//option[@selected]').first || field.xpath('.//option').first
117
- merge_param!(params, field['name'].to_s, (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
118
138
  end
119
139
  end
120
140
 
121
141
  def add_textarea_param(field, params)
122
- merge_param!(params, field['name'].to_s, 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")
144
+ end
145
+
146
+ def submitter?(el)
147
+ (%w[submit image].include? el['type']) || (el.name == 'button')
123
148
  end
124
149
  end