capybara 2.7.0 → 3.35.3

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 (318) hide show
  1. checksums.yaml +5 -5
  2. data/.yardopts +1 -0
  3. data/History.md +1147 -11
  4. data/License.txt +1 -1
  5. data/README.md +252 -131
  6. data/lib/capybara/config.rb +92 -0
  7. data/lib/capybara/cucumber.rb +3 -3
  8. data/lib/capybara/driver/base.rb +52 -21
  9. data/lib/capybara/driver/node.rb +48 -14
  10. data/lib/capybara/dsl.rb +16 -9
  11. data/lib/capybara/helpers.rb +72 -81
  12. data/lib/capybara/minitest/spec.rb +267 -0
  13. data/lib/capybara/minitest.rb +385 -0
  14. data/lib/capybara/node/actions.rb +337 -89
  15. data/lib/capybara/node/base.rb +50 -32
  16. data/lib/capybara/node/document.rb +19 -3
  17. data/lib/capybara/node/document_matchers.rb +22 -24
  18. data/lib/capybara/node/element.rb +388 -125
  19. data/lib/capybara/node/finders.rb +231 -121
  20. data/lib/capybara/node/matchers.rb +503 -217
  21. data/lib/capybara/node/simple.rb +64 -27
  22. data/lib/capybara/queries/ancestor_query.rb +27 -0
  23. data/lib/capybara/queries/base_query.rb +87 -11
  24. data/lib/capybara/queries/current_path_query.rb +24 -24
  25. data/lib/capybara/queries/match_query.rb +15 -10
  26. data/lib/capybara/queries/selector_query.rb +675 -81
  27. data/lib/capybara/queries/sibling_query.rb +26 -0
  28. data/lib/capybara/queries/style_query.rb +45 -0
  29. data/lib/capybara/queries/text_query.rb +88 -20
  30. data/lib/capybara/queries/title_query.rb +9 -11
  31. data/lib/capybara/rack_test/browser.rb +63 -39
  32. data/lib/capybara/rack_test/css_handlers.rb +6 -4
  33. data/lib/capybara/rack_test/driver.rb +26 -16
  34. data/lib/capybara/rack_test/errors.rb +6 -0
  35. data/lib/capybara/rack_test/form.rb +73 -58
  36. data/lib/capybara/rack_test/node.rb +187 -67
  37. data/lib/capybara/rails.rb +4 -8
  38. data/lib/capybara/registration_container.rb +44 -0
  39. data/lib/capybara/registrations/drivers.rb +42 -0
  40. data/lib/capybara/registrations/patches/puma_ssl.rb +29 -0
  41. data/lib/capybara/registrations/servers.rb +45 -0
  42. data/lib/capybara/result.rb +142 -14
  43. data/lib/capybara/rspec/features.rb +17 -42
  44. data/lib/capybara/rspec/matcher_proxies.rb +82 -0
  45. data/lib/capybara/rspec/matchers/base.rb +111 -0
  46. data/lib/capybara/rspec/matchers/become_closed.rb +33 -0
  47. data/lib/capybara/rspec/matchers/compound.rb +88 -0
  48. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  49. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  50. data/lib/capybara/rspec/matchers/have_current_path.rb +29 -0
  51. data/lib/capybara/rspec/matchers/have_selector.rb +77 -0
  52. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  53. data/lib/capybara/rspec/matchers/have_text.rb +33 -0
  54. data/lib/capybara/rspec/matchers/have_title.rb +29 -0
  55. data/lib/capybara/rspec/matchers/match_selector.rb +27 -0
  56. data/lib/capybara/rspec/matchers/match_style.rb +43 -0
  57. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  58. data/lib/capybara/rspec/matchers.rb +143 -244
  59. data/lib/capybara/rspec.rb +10 -12
  60. data/lib/capybara/selector/builders/css_builder.rb +84 -0
  61. data/lib/capybara/selector/builders/xpath_builder.rb +71 -0
  62. data/lib/capybara/selector/css.rb +102 -0
  63. data/lib/capybara/selector/definition/button.rb +63 -0
  64. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  65. data/lib/capybara/selector/definition/css.rb +10 -0
  66. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  67. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  68. data/lib/capybara/selector/definition/element.rb +28 -0
  69. data/lib/capybara/selector/definition/field.rb +40 -0
  70. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  71. data/lib/capybara/selector/definition/file_field.rb +13 -0
  72. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  73. data/lib/capybara/selector/definition/frame.rb +17 -0
  74. data/lib/capybara/selector/definition/id.rb +6 -0
  75. data/lib/capybara/selector/definition/label.rb +62 -0
  76. data/lib/capybara/selector/definition/link.rb +54 -0
  77. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  78. data/lib/capybara/selector/definition/option.rb +27 -0
  79. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  80. data/lib/capybara/selector/definition/select.rb +81 -0
  81. data/lib/capybara/selector/definition/table.rb +109 -0
  82. data/lib/capybara/selector/definition/table_row.rb +21 -0
  83. data/lib/capybara/selector/definition/xpath.rb +5 -0
  84. data/lib/capybara/selector/definition.rb +278 -0
  85. data/lib/capybara/selector/filter.rb +3 -46
  86. data/lib/capybara/selector/filter_set.rb +124 -0
  87. data/lib/capybara/selector/filters/base.rb +77 -0
  88. data/lib/capybara/selector/filters/expression_filter.rb +22 -0
  89. data/lib/capybara/selector/filters/locator_filter.rb +29 -0
  90. data/lib/capybara/selector/filters/node_filter.rb +31 -0
  91. data/lib/capybara/selector/regexp_disassembler.rb +214 -0
  92. data/lib/capybara/selector/selector.rb +155 -0
  93. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  94. data/lib/capybara/selector.rb +232 -369
  95. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  96. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  97. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  98. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  99. data/lib/capybara/selenium/driver.rb +380 -142
  100. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +117 -0
  101. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +124 -0
  102. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +89 -0
  103. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +26 -0
  104. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +24 -0
  105. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  106. data/lib/capybara/selenium/extensions/find.rb +110 -0
  107. data/lib/capybara/selenium/extensions/html5_drag.rb +228 -0
  108. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  109. data/lib/capybara/selenium/extensions/scroll.rb +76 -0
  110. data/lib/capybara/selenium/logger_suppressor.rb +40 -0
  111. data/lib/capybara/selenium/node.rb +528 -97
  112. data/lib/capybara/selenium/nodes/chrome_node.rb +137 -0
  113. data/lib/capybara/selenium/nodes/edge_node.rb +104 -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/action_pauser.rb +26 -0
  118. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  119. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  120. data/lib/capybara/selenium/patches/logs.rb +45 -0
  121. data/lib/capybara/selenium/patches/pause_duration_fix.rb +9 -0
  122. data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
  123. data/lib/capybara/server/animation_disabler.rb +63 -0
  124. data/lib/capybara/server/checker.rb +44 -0
  125. data/lib/capybara/server/middleware.rb +71 -0
  126. data/lib/capybara/server.rb +74 -71
  127. data/lib/capybara/session/config.rb +126 -0
  128. data/lib/capybara/session/matchers.rb +44 -27
  129. data/lib/capybara/session.rb +500 -297
  130. data/lib/capybara/spec/fixtures/no_extension +1 -0
  131. data/lib/capybara/spec/public/jquery.js +5 -5
  132. data/lib/capybara/spec/public/offset.js +6 -0
  133. data/lib/capybara/spec/public/test.js +168 -14
  134. data/lib/capybara/spec/session/accept_alert_spec.rb +37 -14
  135. data/lib/capybara/spec/session/accept_confirm_spec.rb +7 -6
  136. data/lib/capybara/spec/session/accept_prompt_spec.rb +38 -10
  137. data/lib/capybara/spec/session/all_spec.rb +179 -59
  138. data/lib/capybara/spec/session/ancestor_spec.rb +88 -0
  139. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +140 -0
  140. data/lib/capybara/spec/session/assert_current_path_spec.rb +75 -0
  141. data/lib/capybara/spec/session/assert_selector_spec.rb +143 -0
  142. data/lib/capybara/spec/session/assert_style_spec.rb +26 -0
  143. data/lib/capybara/spec/session/assert_text_spec.rb +258 -0
  144. data/lib/capybara/spec/session/assert_title_spec.rb +93 -0
  145. data/lib/capybara/spec/session/attach_file_spec.rb +154 -48
  146. data/lib/capybara/spec/session/body_spec.rb +12 -13
  147. data/lib/capybara/spec/session/check_spec.rb +168 -41
  148. data/lib/capybara/spec/session/choose_spec.rb +75 -23
  149. data/lib/capybara/spec/session/click_button_spec.rb +243 -175
  150. data/lib/capybara/spec/session/click_link_or_button_spec.rb +57 -32
  151. data/lib/capybara/spec/session/click_link_spec.rb +100 -53
  152. data/lib/capybara/spec/session/current_scope_spec.rb +11 -10
  153. data/lib/capybara/spec/session/current_url_spec.rb +61 -35
  154. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +7 -7
  155. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +5 -4
  156. data/lib/capybara/spec/session/element/{assert_match_selector.rb → assert_match_selector_spec.rb} +13 -6
  157. data/lib/capybara/spec/session/element/match_css_spec.rb +21 -7
  158. data/lib/capybara/spec/session/element/match_xpath_spec.rb +9 -7
  159. data/lib/capybara/spec/session/element/matches_selector_spec.rb +91 -34
  160. data/lib/capybara/spec/session/evaluate_async_script_spec.rb +23 -0
  161. data/lib/capybara/spec/session/evaluate_script_spec.rb +45 -3
  162. data/lib/capybara/spec/session/execute_script_spec.rb +24 -4
  163. data/lib/capybara/spec/session/fill_in_spec.rb +166 -64
  164. data/lib/capybara/spec/session/find_button_spec.rb +37 -18
  165. data/lib/capybara/spec/session/find_by_id_spec.rb +10 -9
  166. data/lib/capybara/spec/session/find_field_spec.rb +57 -34
  167. data/lib/capybara/spec/session/find_link_spec.rb +47 -10
  168. data/lib/capybara/spec/session/find_spec.rb +290 -144
  169. data/lib/capybara/spec/session/first_spec.rb +91 -48
  170. data/lib/capybara/spec/session/frame/frame_title_spec.rb +23 -0
  171. data/lib/capybara/spec/session/frame/frame_url_spec.rb +23 -0
  172. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +116 -0
  173. data/lib/capybara/spec/session/frame/within_frame_spec.rb +112 -0
  174. data/lib/capybara/spec/session/go_back_spec.rb +3 -2
  175. data/lib/capybara/spec/session/go_forward_spec.rb +3 -2
  176. data/lib/capybara/spec/session/has_all_selectors_spec.rb +69 -0
  177. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  178. data/lib/capybara/spec/session/has_any_selectors_spec.rb +25 -0
  179. data/lib/capybara/spec/session/has_button_spec.rb +76 -19
  180. data/lib/capybara/spec/session/has_css_spec.rb +277 -131
  181. data/lib/capybara/spec/session/has_current_path_spec.rb +98 -26
  182. data/lib/capybara/spec/session/has_field_spec.rb +177 -107
  183. data/lib/capybara/spec/session/has_link_spec.rb +13 -12
  184. data/lib/capybara/spec/session/has_none_selectors_spec.rb +78 -0
  185. data/lib/capybara/spec/session/has_select_spec.rb +191 -95
  186. data/lib/capybara/spec/session/has_selector_spec.rb +128 -64
  187. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  188. data/lib/capybara/spec/session/has_table_spec.rb +172 -5
  189. data/lib/capybara/spec/session/has_text_spec.rb +126 -60
  190. data/lib/capybara/spec/session/has_title_spec.rb +35 -12
  191. data/lib/capybara/spec/session/has_xpath_spec.rb +74 -53
  192. data/lib/capybara/spec/session/{headers.rb → headers_spec.rb} +3 -2
  193. data/lib/capybara/spec/session/html_spec.rb +14 -6
  194. data/lib/capybara/spec/session/matches_style_spec.rb +35 -0
  195. data/lib/capybara/spec/session/node_spec.rb +1028 -131
  196. data/lib/capybara/spec/session/node_wrapper_spec.rb +39 -0
  197. data/lib/capybara/spec/session/refresh_spec.rb +34 -0
  198. data/lib/capybara/spec/session/reset_session_spec.rb +75 -34
  199. data/lib/capybara/spec/session/{response_code.rb → response_code_spec.rb} +2 -1
  200. data/lib/capybara/spec/session/save_and_open_page_spec.rb +3 -2
  201. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +11 -15
  202. data/lib/capybara/spec/session/save_page_spec.rb +42 -55
  203. data/lib/capybara/spec/session/save_screenshot_spec.rb +16 -14
  204. data/lib/capybara/spec/session/screenshot_spec.rb +2 -2
  205. data/lib/capybara/spec/session/scroll_spec.rb +117 -0
  206. data/lib/capybara/spec/session/select_spec.rb +112 -85
  207. data/lib/capybara/spec/session/selectors_spec.rb +71 -8
  208. data/lib/capybara/spec/session/sibling_spec.rb +52 -0
  209. data/lib/capybara/spec/session/text_spec.rb +38 -23
  210. data/lib/capybara/spec/session/title_spec.rb +17 -5
  211. data/lib/capybara/spec/session/uncheck_spec.rb +71 -12
  212. data/lib/capybara/spec/session/unselect_spec.rb +44 -43
  213. data/lib/capybara/spec/session/visit_spec.rb +99 -32
  214. data/lib/capybara/spec/session/window/become_closed_spec.rb +33 -29
  215. data/lib/capybara/spec/session/window/current_window_spec.rb +5 -3
  216. data/lib/capybara/spec/session/window/open_new_window_spec.rb +5 -3
  217. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +39 -30
  218. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +17 -10
  219. data/lib/capybara/spec/session/window/window_spec.rb +121 -73
  220. data/lib/capybara/spec/session/window/windows_spec.rb +12 -10
  221. data/lib/capybara/spec/session/window/within_window_spec.rb +52 -82
  222. data/lib/capybara/spec/session/within_spec.rb +76 -43
  223. data/lib/capybara/spec/spec_helper.rb +67 -33
  224. data/lib/capybara/spec/test_app.rb +85 -36
  225. data/lib/capybara/spec/views/animated.erb +49 -0
  226. data/lib/capybara/spec/views/buttons.erb +1 -1
  227. data/lib/capybara/spec/views/fieldsets.erb +1 -1
  228. data/lib/capybara/spec/views/form.erb +227 -20
  229. data/lib/capybara/spec/views/frame_child.erb +10 -2
  230. data/lib/capybara/spec/views/frame_one.erb +2 -1
  231. data/lib/capybara/spec/views/frame_parent.erb +2 -2
  232. data/lib/capybara/spec/views/frame_two.erb +1 -1
  233. data/lib/capybara/spec/views/header_links.erb +1 -1
  234. data/lib/capybara/spec/views/host_links.erb +1 -1
  235. data/lib/capybara/spec/views/initial_alert.erb +10 -0
  236. data/lib/capybara/spec/views/obscured.erb +47 -0
  237. data/lib/capybara/spec/views/offset.erb +32 -0
  238. data/lib/capybara/spec/views/path.erb +1 -1
  239. data/lib/capybara/spec/views/popup_one.erb +1 -1
  240. data/lib/capybara/spec/views/popup_two.erb +1 -1
  241. data/lib/capybara/spec/views/postback.erb +1 -1
  242. data/lib/capybara/spec/views/react.erb +45 -0
  243. data/lib/capybara/spec/views/scroll.erb +20 -0
  244. data/lib/capybara/spec/views/spatial.erb +31 -0
  245. data/lib/capybara/spec/views/tables.erb +69 -2
  246. data/lib/capybara/spec/views/with_animation.erb +82 -0
  247. data/lib/capybara/spec/views/with_base_tag.erb +1 -1
  248. data/lib/capybara/spec/views/with_count.erb +1 -1
  249. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  250. data/lib/capybara/spec/views/with_fixed_header_footer.erb +17 -0
  251. data/lib/capybara/spec/views/with_hover.erb +7 -1
  252. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  253. data/lib/capybara/spec/views/with_html.erb +100 -10
  254. data/lib/capybara/spec/views/with_html5_svg.erb +20 -0
  255. data/lib/capybara/spec/views/with_html_entities.erb +1 -1
  256. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  257. data/lib/capybara/spec/views/with_js.erb +49 -3
  258. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  259. data/lib/capybara/spec/views/with_namespace.erb +20 -0
  260. data/lib/capybara/spec/views/with_scope.erb +1 -1
  261. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  262. data/lib/capybara/spec/views/with_simple_html.erb +1 -1
  263. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  264. data/lib/capybara/spec/views/with_title.erb +1 -1
  265. data/lib/capybara/spec/views/with_unload_alert.erb +3 -1
  266. data/lib/capybara/spec/views/with_windows.erb +7 -1
  267. data/lib/capybara/spec/views/within_frames.erb +6 -3
  268. data/lib/capybara/version.rb +2 -1
  269. data/lib/capybara/window.rb +39 -21
  270. data/lib/capybara.rb +208 -186
  271. data/spec/basic_node_spec.rb +52 -39
  272. data/spec/capybara_spec.rb +72 -50
  273. data/spec/css_builder_spec.rb +101 -0
  274. data/spec/css_splitter_spec.rb +38 -0
  275. data/spec/dsl_spec.rb +81 -61
  276. data/spec/filter_set_spec.rb +46 -0
  277. data/spec/fixtures/capybara.csv +1 -0
  278. data/spec/fixtures/certificate.pem +25 -0
  279. data/spec/fixtures/key.pem +27 -0
  280. data/spec/fixtures/selenium_driver_rspec_failure.rb +7 -3
  281. data/spec/fixtures/selenium_driver_rspec_success.rb +7 -3
  282. data/spec/minitest_spec.rb +164 -0
  283. data/spec/minitest_spec_spec.rb +162 -0
  284. data/spec/per_session_config_spec.rb +68 -0
  285. data/spec/rack_test_spec.rb +189 -96
  286. data/spec/regexp_dissassembler_spec.rb +250 -0
  287. data/spec/result_spec.rb +143 -13
  288. data/spec/rspec/features_spec.rb +38 -32
  289. data/spec/rspec/scenarios_spec.rb +9 -7
  290. data/spec/rspec/shared_spec_matchers.rb +959 -0
  291. data/spec/rspec/views_spec.rb +9 -3
  292. data/spec/rspec_matchers_spec.rb +62 -0
  293. data/spec/rspec_spec.rb +127 -30
  294. data/spec/sauce_spec_chrome.rb +43 -0
  295. data/spec/selector_spec.rb +458 -37
  296. data/spec/selenium_spec_chrome.rb +196 -9
  297. data/spec/selenium_spec_chrome_remote.rb +100 -0
  298. data/spec/selenium_spec_edge.rb +47 -0
  299. data/spec/selenium_spec_firefox.rb +210 -0
  300. data/spec/selenium_spec_firefox_remote.rb +80 -0
  301. data/spec/selenium_spec_ie.rb +150 -0
  302. data/spec/selenium_spec_safari.rb +148 -0
  303. data/spec/server_spec.rb +200 -101
  304. data/spec/session_spec.rb +91 -0
  305. data/spec/shared_selenium_node.rb +83 -0
  306. data/spec/shared_selenium_session.rb +558 -0
  307. data/spec/spec_helper.rb +94 -2
  308. data/spec/xpath_builder_spec.rb +93 -0
  309. metadata +420 -60
  310. data/lib/capybara/query.rb +0 -7
  311. data/lib/capybara/spec/session/assert_current_path.rb +0 -60
  312. data/lib/capybara/spec/session/assert_selector.rb +0 -148
  313. data/lib/capybara/spec/session/assert_text.rb +0 -196
  314. data/lib/capybara/spec/session/assert_title.rb +0 -70
  315. data/lib/capybara/spec/session/source_spec.rb +0 -0
  316. data/lib/capybara/spec/session/within_frame_spec.rb +0 -53
  317. data/spec/rspec/matchers_spec.rb +0 -827
  318. data/spec/selenium_spec.rb +0 -151
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Capybara
3
4
  module Node
4
-
5
5
  ##
6
6
  #
7
7
  # A {Capybara::Node::Simple} is a simpler version of {Capybara::Node::Base} which
@@ -28,8 +28,9 @@ module Capybara
28
28
  #
29
29
  # @return [String] The text of the element
30
30
  #
31
- def text(type=nil)
32
- native.text
31
+ def text(_type = nil, normalize_ws: false)
32
+ txt = native.text
33
+ normalize_ws ? txt.gsub(/[[:space:]]+/, ' ').strip : txt
33
34
  end
34
35
 
35
36
  ##
@@ -45,7 +46,7 @@ module Capybara
45
46
  attr_name = name.to_s
46
47
  if attr_name == 'value'
47
48
  value
48
- elsif 'input' == tag_name and 'checkbox' == native[:type] and 'checked' == attr_name
49
+ elsif (tag_name == 'input') && (native[:type] == 'checkbox') && (attr_name == 'checked')
49
50
  native['checked'] == 'checked'
50
51
  else
51
52
  native[attr_name]
@@ -76,15 +77,15 @@ module Capybara
76
77
  #
77
78
  def value
78
79
  if tag_name == 'textarea'
79
- native.content
80
+ native['_capybara_raw_value']
80
81
  elsif tag_name == 'select'
81
- if native['multiple'] == 'multiple'
82
- native.xpath(".//option[@selected='selected']").map { |option| option[:value] || option.content }
82
+ selected_options = find_xpath('.//option[@selected]')
83
+ if multiple?
84
+ selected_options.map(&method(:option_value))
83
85
  else
84
- option = native.xpath(".//option[@selected='selected']").first || native.xpath(".//option").first
85
- option[:value] || option.content if option
86
+ option_value(selected_options.first || find_xpath('.//option').first)
86
87
  end
87
- elsif tag_name == 'input' && %w(radio checkbox).include?(native[:type])
88
+ elsif tag_name == 'input' && %w[radio checkbox].include?(native[:type])
88
89
  native[:value] || 'on'
89
90
  else
90
91
  native[:value]
@@ -99,15 +100,17 @@ module Capybara
99
100
  # @param [Boolean] check_ancestors Whether to inherit visibility from ancestors
100
101
  # @return [Boolean] Whether the element is visible
101
102
  #
102
- def visible?(check_ancestors = true)
103
- return false if (tag_name == 'input') && (native[:type]=="hidden")
103
+ def visible?(check_ancestors = true) # rubocop:disable Style/OptionalBooleanParameter
104
+ return false if (tag_name == 'input') && (native[:type] == 'hidden')
105
+ return false if tag_name == 'template'
104
106
 
105
107
  if check_ancestors
106
- #check size because oldest supported nokogiri doesnt support xpath boolean() function
107
- native.xpath("./ancestor-or-self::*[contains(@style, 'display:none') or contains(@style, 'display: none') or @hidden or name()='script' or name()='head']").size() == 0
108
+ !find_xpath(VISIBILITY_XPATH)
108
109
  else
109
- #no need for an xpath if only checking the current element
110
- !(native.has_attribute?('hidden') || (native[:style] =~ /display:\s?none/) || %w(script head).include?(tag_name))
110
+ # No need for an xpath if only checking the current element
111
+ !(native.key?('hidden') ||
112
+ /display:\s?none/.match?(native[:style] || '') ||
113
+ %w[script head].include?(tag_name))
111
114
  end
112
115
  end
113
116
 
@@ -127,7 +130,8 @@ module Capybara
127
130
  #
128
131
  # @return [Boolean] Whether the element is disabled
129
132
  def disabled?
130
- native.has_attribute?('disabled')
133
+ native.has_attribute?('disabled') &&
134
+ %w[button input select textarea optgroup option menuitem fieldset].include?(tag_name)
131
135
  end
132
136
 
133
137
  ##
@@ -140,21 +144,27 @@ module Capybara
140
144
  native.has_attribute?('selected')
141
145
  end
142
146
 
143
- def synchronize(seconds=nil)
147
+ def multiple?
148
+ native.has_attribute?('multiple')
149
+ end
150
+
151
+ def readonly?
152
+ native.has_attribute?('readonly')
153
+ end
154
+
155
+ def synchronize(_seconds = nil)
144
156
  yield # simple nodes don't need to wait
145
157
  end
146
158
 
147
- def allow_reload!
159
+ def allow_reload!(*)
148
160
  # no op
149
161
  end
150
162
 
163
+ ##
164
+ #
165
+ # @return [String] The title of the document
151
166
  def title
152
- if native.respond_to? :title
153
- native.title
154
- else
155
- #old versions of nokogiri don't have #title - remove in 3.0
156
- native.xpath('/html/head/title | /html/title').first.text
157
- end
167
+ native.title
158
168
  end
159
169
 
160
170
  def inspect
@@ -162,14 +172,41 @@ module Capybara
162
172
  end
163
173
 
164
174
  # @api private
165
- def find_css(css)
175
+ def find_css(css, **_options)
166
176
  native.css(css)
167
177
  end
168
178
 
169
179
  # @api private
170
- def find_xpath(xpath)
180
+ def find_xpath(xpath, **_options)
171
181
  native.xpath(xpath)
172
182
  end
183
+
184
+ # @api private
185
+ def session_options
186
+ Capybara.session_options
187
+ end
188
+
189
+ # @api private
190
+ def initial_cache
191
+ {}
192
+ end
193
+
194
+ private
195
+
196
+ def option_value(option)
197
+ return nil if option.nil?
198
+
199
+ option[:value] || option.content
200
+ end
201
+
202
+ VISIBILITY_XPATH = XPath.generate do |x|
203
+ x.ancestor_or_self[
204
+ x.attr(:style)[x.contains('display:none') | x.contains('display: none')] |
205
+ x.attr(:hidden) |
206
+ x.qname.one_of('script', 'head') |
207
+ (~x.self(:summary) & XPath.parent(:details)[!XPath.attr(:open)])
208
+ ].boolean
209
+ end.to_s.freeze
173
210
  end
174
211
  end
175
212
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Queries
5
+ class AncestorQuery < Capybara::Queries::SelectorQuery
6
+ # @api private
7
+ def resolve_for(node, exact = nil)
8
+ @child_node = node
9
+
10
+ node.synchronize do
11
+ match_results = super(node.session.current_scope, exact)
12
+ ancestors = node.find_xpath(XPath.ancestor.to_s)
13
+ .map(&method(:to_element))
14
+ .select { |el| match_results.include?(el) }
15
+ Capybara::Result.new(ordered_results(ancestors), self)
16
+ end
17
+ end
18
+
19
+ def description(applied = false) # rubocop:disable Style/OptionalBooleanParameter
20
+ child_query = @child_node&.instance_variable_get(:@query)
21
+ desc = super
22
+ desc += " that is an ancestor of #{child_query.description}" if child_query
23
+ desc
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,29 +1,105 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Capybara
3
4
  # @api private
4
5
  module Queries
5
6
  class BaseQuery
6
- COUNT_KEYS = [:count, :minimum, :maximum, :between]
7
+ COUNT_KEYS = %i[count minimum maximum between].freeze
7
8
 
8
9
  attr_reader :options
10
+ attr_writer :session_options
11
+
12
+ def initialize(options)
13
+ @session_options = options.delete(:session_options)
14
+ end
15
+
16
+ def session_options
17
+ @session_options || Capybara.session_options
18
+ end
9
19
 
10
20
  def wait
11
- if @options.has_key?(:wait)
12
- @options[:wait] || 0
13
- else
14
- Capybara.default_max_wait_time
21
+ self.class.wait(options, session_options.default_max_wait_time)
22
+ end
23
+
24
+ def self.wait(options, default = Capybara.default_max_wait_time)
25
+ # if no value or nil for the :wait option is passed it should default to the default
26
+ wait = options.fetch(:wait, nil)
27
+ wait = default if wait.nil?
28
+ wait || 0
29
+ end
30
+
31
+ ##
32
+ #
33
+ # Checks if a count of 0 is valid for the query
34
+ # Returns false if query does not have any count options specified.
35
+ #
36
+ def expects_none?
37
+ count_specified? ? matches_count?(0) : false
38
+ end
39
+
40
+ ##
41
+ #
42
+ # Checks if the given count matches the query count options.
43
+ # Defaults to true if no count options are specified. If multiple
44
+ # count options exist, it tests that all conditions are met;
45
+ # however, if :count is specified, all other options are ignored.
46
+ #
47
+ # @param [Integer] count The actual number. Should be coercible via Integer()
48
+ #
49
+ def matches_count?(count)
50
+ return (Integer(options[:count]) == count) if options[:count]
51
+ return false if options[:maximum] && (Integer(options[:maximum]) < count)
52
+ return false if options[:minimum] && (Integer(options[:minimum]) > count)
53
+ return false if options[:between] && !options[:between].include?(count)
54
+
55
+ true
56
+ end
57
+
58
+ ##
59
+ #
60
+ # Generates a failure message from the query description and count options.
61
+ #
62
+ def failure_message
63
+ +"expected to find #{description}" << count_message
64
+ end
65
+
66
+ def negative_failure_message
67
+ +"expected not to find #{description}" << count_message
68
+ end
69
+
70
+ private
71
+
72
+ def count_specified?
73
+ COUNT_KEYS.any? { |key| options.key? key }
74
+ end
75
+
76
+ def count_message
77
+ message = +''
78
+ count, between, maximum, minimum = options.values_at(:count, :between, :maximum, :minimum)
79
+ if count
80
+ message << " #{occurrences count}"
81
+ elsif between
82
+ message << " between #{between.begin ? between.first : 1} and" \
83
+ " #{between.end ? between.last : 'infinite'} times"
84
+ elsif maximum
85
+ message << " at most #{occurrences maximum}"
86
+ elsif minimum
87
+ message << " at least #{occurrences minimum}"
15
88
  end
89
+ message
16
90
  end
17
91
 
18
- private
92
+ def occurrences(count)
93
+ "#{count} #{Capybara::Helpers.declension('time', 'times', count)}"
94
+ end
19
95
 
20
96
  def assert_valid_keys
21
97
  invalid_keys = @options.keys - valid_keys
22
- unless invalid_keys.empty?
23
- invalid_names = invalid_keys.map(&:inspect).join(", ")
24
- valid_names = valid_keys.map(&:inspect).join(", ")
25
- raise ArgumentError, "invalid keys #{invalid_names}, should be one of #{valid_names}"
26
- end
98
+ return if invalid_keys.empty?
99
+
100
+ invalid_names = invalid_keys.map(&:inspect).join(', ')
101
+ valid_names = valid_keys.map(&:inspect).join(', ')
102
+ raise ArgumentError, "Invalid option(s) #{invalid_names}, should be one of #{valid_names}"
27
103
  end
28
104
  end
29
105
  end
@@ -1,34 +1,35 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'addressable/uri'
3
4
 
4
5
  module Capybara
5
6
  # @api private
6
7
  module Queries
7
8
  class CurrentPathQuery < BaseQuery
8
- def initialize(expected_path, options = {})
9
+ def initialize(expected_path, **options, &optional_filter_block)
10
+ super(options)
9
11
  @expected_path = expected_path
10
12
  @options = {
11
- url: false,
12
- only_path: false }.merge(options)
13
+ url: !@expected_path.is_a?(Regexp) && !::Addressable::URI.parse(@expected_path || '').hostname.nil?,
14
+ ignore_query: false
15
+ }.merge(options)
16
+ @filter_block = optional_filter_block
13
17
  assert_valid_keys
14
18
  end
15
19
 
16
20
  def resolves_for?(session)
17
- @actual_path = if options[:url]
18
- session.current_url
19
- else
20
- if options[:only_path]
21
- ::Addressable::URI.parse(session.current_url).path
22
- else
23
- ::Addressable::URI.parse(session.current_url).request_uri
24
- end
21
+ uri = ::Addressable::URI.parse(session.current_url)
22
+ @actual_path = (options[:ignore_query] ? uri&.omit(:query) : uri).yield_self do |u|
23
+ options[:url] ? u&.to_s : u&.request_uri
25
24
  end
26
25
 
27
- if @expected_path.is_a? Regexp
28
- @actual_path.match(@expected_path)
26
+ res = if @expected_path.is_a? Regexp
27
+ @actual_path.to_s.match?(@expected_path)
29
28
  else
30
- ::Addressable::URI.parse(@expected_path) == Addressable::URI.parse(@actual_path)
29
+ ::Addressable::URI.parse(@expected_path) == ::Addressable::URI.parse(@actual_path)
31
30
  end
31
+
32
+ res && matches_filter_block?(uri)
32
33
  end
33
34
 
34
35
  def failure_message
@@ -39,22 +40,21 @@ module Capybara
39
40
  failure_message_helper(' not')
40
41
  end
41
42
 
42
- private
43
+ private
44
+
45
+ def matches_filter_block?(url)
46
+ return true unless @filter_block
47
+
48
+ @filter_block.call(url)
49
+ end
43
50
 
44
51
  def failure_message_helper(negated = '')
45
- verb = (@expected_path.is_a?(Regexp))? 'match' : 'equal'
52
+ verb = @expected_path.is_a?(Regexp) ? 'match' : 'equal'
46
53
  "expected #{@actual_path.inspect}#{negated} to #{verb} #{@expected_path.inspect}"
47
54
  end
48
55
 
49
56
  def valid_keys
50
- [:wait, :url, :only_path]
51
- end
52
-
53
- def assert_valid_keys
54
- super
55
- if options[:url] && options[:only_path]
56
- raise ArgumentError, "the :url and :only_path options cannot both be true"
57
- end
57
+ %i[wait url ignore_query]
58
58
  end
59
59
  end
60
60
  end
@@ -1,21 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Capybara
2
4
  module Queries
3
5
  class MatchQuery < Capybara::Queries::SelectorQuery
4
- VALID_KEYS = [:text, :visible, :exact, :wait]
5
-
6
6
  def visible
7
- if options.has_key?(:visible)
8
- super
9
- else
10
- :all
11
- end
7
+ options.key?(:visible) ? super : :all
12
8
  end
13
9
 
14
- private
10
+ private
11
+
12
+ def assert_valid_keys
13
+ invalid_options = @options.keys & COUNT_KEYS
14
+ unless invalid_options.empty?
15
+ raise ArgumentError, "Match queries don't support quantity options. Invalid keys - #{invalid_options.join(', ')}"
16
+ end
17
+
18
+ super
19
+ end
15
20
 
16
21
  def valid_keys
17
- VALID_KEYS + @selector.custom_filters.keys
22
+ super - COUNT_KEYS
18
23
  end
19
24
  end
20
25
  end
21
- end
26
+ end
@@ -1,138 +1,732 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require 'matrix'
4
+
2
5
  module Capybara
3
6
  module Queries
4
7
  class SelectorQuery < Queries::BaseQuery
5
- attr_accessor :selector, :locator, :options, :expression, :find, :negative
8
+ attr_reader :expression, :selector, :locator, :options
6
9
 
7
- VALID_KEYS = [:text, :visible, :between, :count, :maximum, :minimum, :exact, :match, :wait]
8
- VALID_MATCH = [:first, :smart, :prefer_exact, :one]
10
+ SPATIAL_KEYS = %i[above below left_of right_of near].freeze
11
+ VALID_KEYS = SPATIAL_KEYS + COUNT_KEYS +
12
+ %i[text id class style visible obscured exact exact_text normalize_ws match wait filter_set]
13
+ VALID_MATCH = %i[first smart prefer_exact one].freeze
9
14
 
10
- def initialize(*args)
11
- @options = if args.last.is_a?(Hash) then args.pop.dup else {} end
15
+ def initialize(*args,
16
+ session_options:,
17
+ enable_aria_label: session_options.enable_aria_label,
18
+ enable_aria_role: session_options.enable_aria_role,
19
+ test_id: session_options.test_id,
20
+ selector_format: nil,
21
+ order: nil,
22
+ **options,
23
+ &filter_block)
24
+ @resolved_node = nil
25
+ @resolved_count = 0
26
+ @options = options.dup
27
+ @order = order
28
+ @filter_cache = Hash.new { |hsh, key| hsh[key] = {} }
12
29
 
13
- if args[0].is_a?(Symbol)
14
- @selector = Selector.all[args.shift]
15
- @locator = args.shift
16
- else
17
- @selector = Selector.all.values.find { |s| s.match?(args[0]) }
18
- @locator = args.shift
19
- end
20
- @selector ||= Selector.all[Capybara.default_selector]
30
+ super(@options)
31
+ self.session_options = session_options
21
32
 
22
- warn "Unused parameters passed to #{self.class.name} : #{args.to_s}" unless args.empty?
33
+ @selector = Selector.new(
34
+ find_selector(args[0].is_a?(Symbol) ? args.shift : args[0]),
35
+ config: {
36
+ enable_aria_label: enable_aria_label,
37
+ enable_aria_role: enable_aria_role,
38
+ test_id: test_id
39
+ },
40
+ format: selector_format
41
+ )
23
42
 
24
- # for compatibility with Capybara 2.0
25
- if Capybara.exact_options and @selector == Selector.all[:option]
26
- @options[:exact] = true
27
- end
43
+ @locator = args.shift
44
+ @filter_block = filter_block
45
+
46
+ raise ArgumentError, "Unused parameters passed to #{self.class.name} : #{args}" unless args.empty?
47
+
48
+ @expression = selector.call(@locator, **@options)
49
+
50
+ warn_exact_usage
28
51
 
29
- @expression = @selector.call(@locator)
30
52
  assert_valid_keys
31
53
  end
32
54
 
33
55
  def name; selector.name; end
34
- def label; selector.label or selector.name; end
56
+ def label; selector.label || selector.name; end
35
57
 
36
- def description
37
- @description = String.new("#{label} #{locator.inspect}")
38
- @description << " with text #{options[:text].inspect}" if options[:text]
39
- @description << selector.description(options)
40
- @description
41
- end
58
+ def description(only_applied = false) # rubocop:disable Style/OptionalBooleanParameter
59
+ desc = +''
60
+ show_for = show_for_stage(only_applied)
61
+
62
+ if show_for[:any]
63
+ desc << 'visible ' if visible == :visible
64
+ desc << 'non-visible ' if visible == :hidden
65
+ end
66
+
67
+ desc << "#{label} #{locator.inspect}"
42
68
 
43
- def matches_filters?(node)
44
- if options[:text]
45
- regexp = options[:text].is_a?(Regexp) ? options[:text] : Regexp.escape(options[:text].to_s)
46
- return false if not node.text(visible).match(regexp)
69
+ if show_for[:any]
70
+ desc << " with#{' exact' if exact_text == true} text #{options[:text].inspect}" if options[:text]
71
+ desc << " with exact text #{exact_text}" if exact_text.is_a?(String)
47
72
  end
48
- case visible
49
- when :visible then return false unless node.visible?
50
- when :hidden then return false if node.visible?
73
+
74
+ desc << " with id #{options[:id]}" if options[:id]
75
+ desc << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
76
+
77
+ desc << case options[:style]
78
+ when String
79
+ " with style attribute #{options[:style].inspect}"
80
+ when Regexp
81
+ " with style attribute matching #{options[:style].inspect}"
82
+ when Hash
83
+ " with styles #{options[:style].inspect}"
84
+ else ''
51
85
  end
52
- selector.custom_filters.each do |name, filter|
53
- if options.has_key?(name)
54
- return false unless filter.matches?(node, options[name])
55
- elsif filter.default?
56
- return false unless filter.matches?(node, filter.default)
86
+
87
+ %i[above below left_of right_of near].each do |spatial_filter|
88
+ if options[spatial_filter] && show_for[:spatial]
89
+ desc << " #{spatial_filter} #{options[spatial_filter] rescue '<ERROR>'}" # rubocop:disable Style/RescueModifier
57
90
  end
58
91
  end
92
+
93
+ desc << selector.description(node_filters: show_for[:node], **options)
94
+
95
+ desc << ' that also matches the custom filter block' if @filter_block && show_for[:node]
96
+
97
+ desc << " within #{@resolved_node.inspect}" if describe_within?
98
+ if locator.is_a?(String) && locator.start_with?('#', './/', '//') && !selector.raw_locator?
99
+ desc << "\nNote: It appears you may be passing a CSS selector or XPath expression rather than a locator. " \
100
+ "Please see the documentation for acceptable locator values.\n\n"
101
+ end
102
+ desc
103
+ end
104
+
105
+ def applied_description
106
+ description(true)
107
+ end
108
+
109
+ def matches_filters?(node, node_filter_errors = [])
110
+ return true if (@resolved_node&.== node) && options[:allow_self]
111
+
112
+ matches_locator_filter?(node) &&
113
+ matches_system_filters?(node) &&
114
+ matches_spatial_filters?(node) &&
115
+ matches_node_filters?(node, node_filter_errors) &&
116
+ matches_filter_block?(node)
117
+ rescue *(node.respond_to?(:session) ? node.session.driver.invalid_element_errors : [])
118
+ false
59
119
  end
60
120
 
61
121
  def visible
62
- if options.has_key?(:visible)
63
- case @options[:visible]
64
- when true then :visible
65
- when false then :all
66
- else @options[:visible]
67
- end
68
- else
69
- if Capybara.ignore_hidden_elements
70
- :visible
71
- else
72
- :all
73
- end
122
+ case (vis = options.fetch(:visible) { default_visibility })
123
+ when true then :visible
124
+ when false then :all
125
+ else vis
74
126
  end
75
127
  end
76
128
 
77
129
  def exact?
78
- if options.has_key?(:exact)
79
- @options[:exact]
80
- else
81
- Capybara.exact
82
- end
130
+ supports_exact? ? options.fetch(:exact, session_options.exact) : false
83
131
  end
84
132
 
85
133
  def match
86
- if options.has_key?(:match)
87
- @options[:match]
88
- else
89
- Capybara.match
90
- end
134
+ options.fetch(:match, session_options.match)
91
135
  end
92
136
 
93
- def xpath(exact=nil)
94
- exact = self.exact? if exact == nil
95
- if @expression.respond_to?(:to_xpath) and exact
96
- @expression.to_xpath(:exact)
97
- else
98
- @expression.to_s
99
- end
137
+ def xpath(exact = nil)
138
+ exact = exact? if exact.nil?
139
+ expr = apply_expression_filters(@expression)
140
+ expr = exact ? expr.to_xpath(:exact) : expr.to_s if expr.respond_to?(:to_xpath)
141
+ expr = filtered_expression(expr)
142
+ expr = "(#{expr})[#{xpath_text_conditions}]" if try_text_match_in_expression?
143
+ expr
100
144
  end
101
145
 
102
146
  def css
103
- @expression
147
+ filtered_expression(apply_expression_filters(@expression))
104
148
  end
105
149
 
106
150
  # @api private
107
151
  def resolve_for(node, exact = nil)
152
+ applied_filters.clear
153
+ @filter_cache.clear
154
+ @resolved_node = node
155
+ @resolved_count += 1
156
+
108
157
  node.synchronize do
109
- children = if selector.format == :css
110
- node.find_css(self.css)
158
+ children = find_nodes_by_selector_format(node, exact).map(&method(:to_element))
159
+ Capybara::Result.new(ordered_results(children), self)
160
+ end
161
+ end
162
+
163
+ # @api private
164
+ def supports_exact?
165
+ return @expression.respond_to? :to_xpath if @selector.supports_exact?.nil?
166
+
167
+ @selector.supports_exact?
168
+ end
169
+
170
+ def failure_message
171
+ +"expected to find #{applied_description}" << count_message
172
+ end
173
+
174
+ def negative_failure_message
175
+ +"expected not to find #{applied_description}" << count_message
176
+ end
177
+
178
+ private
179
+
180
+ def selector_format
181
+ @selector.format
182
+ end
183
+
184
+ def matching_text
185
+ options[:text] || options[:exact_text]
186
+ end
187
+
188
+ def text_fragments
189
+ (text = matching_text).is_a?(String) ? text.split : []
190
+ end
191
+
192
+ def xpath_text_conditions
193
+ case (text = matching_text)
194
+ when String
195
+ text.split.map { |txt| XPath.contains(txt) }.reduce(&:&)
196
+ when Regexp
197
+ condition = XPath.current
198
+ condition = condition.uppercase if text.casefold?
199
+ Selector::RegexpDisassembler.new(text).alternated_substrings.map do |strs|
200
+ strs.flat_map(&:split).map { |str| condition.contains(str) }.reduce(:&)
201
+ end.reduce(:|)
202
+ end
203
+ end
204
+
205
+ def try_text_match_in_expression?
206
+ first_try? &&
207
+ matching_text &&
208
+ @resolved_node.is_a?(Capybara::Node::Base) &&
209
+ @resolved_node.session&.driver&.wait?
210
+ end
211
+
212
+ def first_try?
213
+ @resolved_count == 1
214
+ end
215
+
216
+ def show_for_stage(only_applied)
217
+ lambda do |stage = :any|
218
+ !only_applied || (stage == :any ? applied_filters.any? : applied_filters.include?(stage))
219
+ end
220
+ end
221
+
222
+ def applied_filters
223
+ @applied_filters ||= []
224
+ end
225
+
226
+ def find_selector(locator)
227
+ case locator
228
+ when Symbol then Selector[locator]
229
+ else Selector.for(locator)
230
+ end || Selector[session_options.default_selector]
231
+ end
232
+
233
+ def find_nodes_by_selector_format(node, exact)
234
+ hints = {}
235
+ hints[:uses_visibility] = true unless visible == :all
236
+ hints[:texts] = text_fragments unless selector_format == :xpath
237
+ hints[:styles] = options[:style] if use_default_style_filter?
238
+ hints[:position] = true if use_spatial_filter?
239
+
240
+ case selector_format
241
+ when :css
242
+ if node.method(:find_css).arity == 1
243
+ node.find_css(css)
244
+ else
245
+ node.find_css(css, **hints)
246
+ end
247
+ when :xpath
248
+ if node.method(:find_xpath).arity == 1
249
+ node.find_xpath(xpath(exact))
111
250
  else
112
- node.find_xpath(self.xpath(exact))
113
- end.map do |child|
114
- if node.is_a?(Capybara::Node::Base)
115
- Capybara::Node::Element.new(node.session, child, node, self)
251
+ node.find_xpath(xpath(exact), **hints)
252
+ end
253
+ else
254
+ raise ArgumentError, "Unknown format: #{selector_format}"
255
+ end
256
+ end
257
+
258
+ def to_element(node)
259
+ if @resolved_node.is_a?(Capybara::Node::Base)
260
+ Capybara::Node::Element.new(@resolved_node.session, node, @resolved_node, self)
261
+ else
262
+ Capybara::Node::Simple.new(node)
263
+ end
264
+ end
265
+
266
+ def valid_keys
267
+ VALID_KEYS + custom_keys
268
+ end
269
+
270
+ def matches_node_filters?(node, errors)
271
+ applied_filters << :node
272
+
273
+ unapplied_options = options.keys - valid_keys
274
+ @selector.with_filter_errors(errors) do
275
+ node_filters.all? do |filter_name, filter|
276
+ next true unless apply_filter?(filter)
277
+
278
+ if filter.matcher?
279
+ unapplied_options.select { |option_name| filter.handles_option?(option_name) }.all? do |option_name|
280
+ unapplied_options.delete(option_name)
281
+ filter.matches?(node, option_name, options[option_name], @selector)
282
+ end
283
+ elsif options.key?(filter_name)
284
+ unapplied_options.delete(filter_name)
285
+ filter.matches?(node, filter_name, options[filter_name], @selector)
286
+ elsif filter.default?
287
+ filter.matches?(node, filter_name, filter.default, @selector)
116
288
  else
117
- Capybara::Node::Simple.new(child)
289
+ true
118
290
  end
119
291
  end
120
- Capybara::Result.new(children, self)
121
292
  end
122
293
  end
123
294
 
124
- private
295
+ def matches_filter_block?(node)
296
+ return true unless @filter_block
125
297
 
126
- def valid_keys
127
- COUNT_KEYS + [:text, :visible, :exact, :match, :wait] + @selector.custom_filters.keys
298
+ if node.respond_to?(:session)
299
+ node.session.using_wait_time(0) { @filter_block.call(node) }
300
+ else
301
+ @filter_block.call(node)
302
+ end
303
+ end
304
+
305
+ def filter_set(name)
306
+ ::Capybara::Selector::FilterSet[name]
307
+ end
308
+
309
+ def node_filters
310
+ if options.key?(:filter_set)
311
+ filter_set(options[:filter_set])
312
+ else
313
+ @selector
314
+ end.node_filters
315
+ end
316
+
317
+ def expression_filters
318
+ filters = @selector.expression_filters
319
+ filters.merge filter_set(options[:filter_set]).expression_filters if options.key?(:filter_set)
320
+ filters
321
+ end
322
+
323
+ def ordered_results(results)
324
+ case @order
325
+ when :reverse
326
+ results.reverse
327
+ else
328
+ results
329
+ end
330
+ end
331
+
332
+ def custom_keys
333
+ @custom_keys ||= node_filters.keys + expression_filters.keys
128
334
  end
129
335
 
130
336
  def assert_valid_keys
131
- super
132
337
  unless VALID_MATCH.include?(match)
133
- raise ArgumentError, "invalid option #{match.inspect} for :match, should be one of #{VALID_MATCH.map(&:inspect).join(", ")}"
338
+ raise ArgumentError, "Invalid option #{match.inspect} for :match, should be one of #{VALID_MATCH.map(&:inspect).join(', ')}"
339
+ end
340
+
341
+ unhandled_options = @options.keys.reject do |option_name|
342
+ valid_keys.include?(option_name) ||
343
+ expression_filters.any? { |_name, ef| ef.handles_option? option_name } ||
344
+ node_filters.any? { |_name, nf| nf.handles_option? option_name }
345
+ end
346
+
347
+ return if unhandled_options.empty?
348
+
349
+ invalid_names = unhandled_options.map(&:inspect).join(', ')
350
+ valid_names = (valid_keys - [:allow_self]).map(&:inspect).join(', ')
351
+ raise ArgumentError, "Invalid option(s) #{invalid_names}, should be one of #{valid_names}"
352
+ end
353
+
354
+ def filtered_expression(expr)
355
+ conditions = {}
356
+ conditions[:id] = options[:id] if use_default_id_filter?
357
+ conditions[:class] = options[:class] if use_default_class_filter?
358
+ conditions[:style] = options[:style] if use_default_style_filter? && !options[:style].is_a?(Hash)
359
+ builder(expr).add_attribute_conditions(**conditions)
360
+ end
361
+
362
+ def use_default_id_filter?
363
+ options.key?(:id) && !custom_keys.include?(:id)
364
+ end
365
+
366
+ def use_default_class_filter?
367
+ options.key?(:class) && !custom_keys.include?(:class)
368
+ end
369
+
370
+ def use_default_style_filter?
371
+ options.key?(:style) && !custom_keys.include?(:style)
372
+ end
373
+
374
+ def use_spatial_filter?
375
+ options.values_at(*SPATIAL_KEYS).compact.any?
376
+ end
377
+
378
+ def apply_expression_filters(expression)
379
+ unapplied_options = options.keys - valid_keys
380
+ expression_filters.inject(expression) do |expr, (name, ef)|
381
+ next expr unless apply_filter?(ef)
382
+
383
+ if ef.matcher?
384
+ unapplied_options.select(&ef.method(:handles_option?)).inject(expr) do |memo, option_name|
385
+ unapplied_options.delete(option_name)
386
+ ef.apply_filter(memo, option_name, options[option_name], @selector)
387
+ end
388
+ elsif options.key?(name)
389
+ unapplied_options.delete(name)
390
+ ef.apply_filter(expr, name, options[name], @selector)
391
+ elsif ef.default?
392
+ ef.apply_filter(expr, name, ef.default, @selector)
393
+ else
394
+ expr
395
+ end
134
396
  end
135
397
  end
398
+
399
+ def warn_exact_usage
400
+ return unless options.key?(:exact) && !supports_exact?
401
+
402
+ warn "The :exact option only has an effect on queries using the XPath#is method. Using it with the query \"#{expression}\" has no effect."
403
+ end
404
+
405
+ def exact_text
406
+ options.fetch(:exact_text, session_options.exact_text)
407
+ end
408
+
409
+ def describe_within?
410
+ @resolved_node && !document?(@resolved_node) && !simple_root?(@resolved_node)
411
+ end
412
+
413
+ def document?(node)
414
+ node.is_a?(::Capybara::Node::Document)
415
+ end
416
+
417
+ def simple_root?(node)
418
+ node.is_a?(::Capybara::Node::Simple) && node.path == '/'
419
+ end
420
+
421
+ def apply_filter?(filter)
422
+ filter.format.nil? || (filter.format == selector_format)
423
+ end
424
+
425
+ def matches_locator_filter?(node)
426
+ return true unless @selector.locator_filter && apply_filter?(@selector.locator_filter)
427
+
428
+ @selector.locator_filter.matches?(node, @locator, @selector, exact: exact?)
429
+ end
430
+
431
+ def matches_system_filters?(node)
432
+ applied_filters << :system
433
+
434
+ matches_visibility_filters?(node) &&
435
+ matches_id_filter?(node) &&
436
+ matches_class_filter?(node) &&
437
+ matches_style_filter?(node) &&
438
+ matches_text_filter?(node) &&
439
+ matches_exact_text_filter?(node)
440
+ end
441
+
442
+ def matches_spatial_filters?(node)
443
+ applied_filters << :spatial
444
+ return true unless use_spatial_filter?
445
+
446
+ node_rect = Rectangle.new(node.initial_cache[:position] || node.rect)
447
+
448
+ if options[:above]
449
+ el_rect = rect_cache(options[:above])
450
+ return false unless node_rect.above? el_rect
451
+ end
452
+
453
+ if options[:below]
454
+ el_rect = rect_cache(options[:below])
455
+ return false unless node_rect.below? el_rect
456
+ end
457
+
458
+ if options[:left_of]
459
+ el_rect = rect_cache(options[:left_of])
460
+ return false unless node_rect.left_of? el_rect
461
+ end
462
+
463
+ if options[:right_of]
464
+ el_rect = rect_cache(options[:right_of])
465
+ return false unless node_rect.right_of? el_rect
466
+ end
467
+
468
+ if options[:near]
469
+ return false if node == options[:near]
470
+
471
+ el_rect = rect_cache(options[:near])
472
+ return false unless node_rect.near? el_rect
473
+ end
474
+
475
+ true
476
+ end
477
+
478
+ def matches_id_filter?(node)
479
+ return true unless use_default_id_filter? && options[:id].is_a?(Regexp)
480
+
481
+ options[:id].match? node[:id]
482
+ end
483
+
484
+ def matches_class_filter?(node)
485
+ return true unless use_default_class_filter? && need_to_process_classes?
486
+
487
+ if options[:class].is_a? Regexp
488
+ options[:class].match? node[:class]
489
+ else
490
+ classes = (node[:class] || '').split
491
+ options[:class].select { |c| c.is_a? Regexp }.all? do |r|
492
+ classes.any? { |cls| r.match? cls }
493
+ end
494
+ end
495
+ end
496
+
497
+ def need_to_process_classes?
498
+ case options[:class]
499
+ when Regexp then true
500
+ when Array then options[:class].any?(Regexp)
501
+ else
502
+ false
503
+ end
504
+ end
505
+
506
+ def matches_style_filter?(node)
507
+ case options[:style]
508
+ when String, nil
509
+ true
510
+ when Regexp
511
+ options[:style].match? node[:style]
512
+ when Hash
513
+ matches_style?(node, options[:style])
514
+ end
515
+ end
516
+
517
+ def matches_style?(node, styles)
518
+ @actual_styles = node.initial_cache[:style] || node.style(*styles.keys)
519
+ styles.all? do |style, value|
520
+ if value.is_a? Regexp
521
+ value.match? @actual_styles[style.to_s]
522
+ else
523
+ @actual_styles[style.to_s] == value
524
+ end
525
+ end
526
+ end
527
+
528
+ def matches_text_filter?(node)
529
+ value = options[:text]
530
+ return true unless value
531
+ return matches_text_exactly?(node, value) if exact_text == true
532
+
533
+ regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s)
534
+ matches_text_regexp?(node, regexp)
535
+ end
536
+
537
+ def matches_exact_text_filter?(node)
538
+ return true unless exact_text.is_a?(String)
539
+
540
+ matches_text_exactly?(node, exact_text)
541
+ end
542
+
543
+ def matches_visibility_filters?(node)
544
+ obscured = options[:obscured]
545
+ return (visible != :hidden) && (node.initial_cache[:visible] != false) && !node.obscured? if obscured == false
546
+
547
+ vis = case visible
548
+ when :visible
549
+ node.initial_cache[:visible] || (node.initial_cache[:visible].nil? && node.visible?)
550
+ when :hidden
551
+ (node.initial_cache[:visible] == false) || (node.initial_cache[:visbile].nil? && !node.visible?)
552
+ else
553
+ true
554
+ end
555
+
556
+ vis && case obscured
557
+ when true
558
+ node.obscured?
559
+ when false
560
+ !node.obscured?
561
+ else
562
+ true
563
+ end
564
+ end
565
+
566
+ def matches_text_exactly?(node, value)
567
+ regexp = value.is_a?(Regexp) ? value : /\A#{Regexp.escape(value.to_s)}\z/
568
+ matches_text_regexp?(node, regexp)
569
+ end
570
+
571
+ def normalize_ws
572
+ options.fetch(:normalize_ws, session_options.default_normalize_ws)
573
+ end
574
+
575
+ def matches_text_regexp?(node, regexp)
576
+ text_visible = visible
577
+ text_visible = :all if text_visible == :hidden
578
+ node.text(text_visible, normalize_ws: normalize_ws).match?(regexp)
579
+ end
580
+
581
+ def default_visibility
582
+ @selector.default_visibility(session_options.ignore_hidden_elements, options)
583
+ end
584
+
585
+ def builder(expr)
586
+ selector.builder(expr)
587
+ end
588
+
589
+ def position_cache(key)
590
+ @filter_cache[key][:position] ||= key.rect
591
+ end
592
+
593
+ def rect_cache(key)
594
+ @filter_cache[key][:rect] ||= Rectangle.new(position_cache(key))
595
+ end
596
+
597
+ class Rectangle
598
+ attr_reader :top, :bottom, :left, :right
599
+
600
+ def initialize(position)
601
+ # rubocop:disable Style/RescueModifier
602
+ @top = position['top'] rescue position['y']
603
+ @bottom = position['bottom'] rescue (@top + position['height'])
604
+ @left = position['left'] rescue position['x']
605
+ @right = position['right'] rescue (@left + position['width'])
606
+ # rubocop:enable Style/RescueModifier
607
+ end
608
+
609
+ def distance(other)
610
+ distance = Float::INFINITY
611
+
612
+ line_segments.each do |ls1|
613
+ other.line_segments.each do |ls2|
614
+ distance = [
615
+ distance,
616
+ distance_segment_segment(*ls1, *ls2)
617
+ ].min
618
+ end
619
+ end
620
+
621
+ distance
622
+ end
623
+
624
+ def above?(other)
625
+ bottom <= other.top
626
+ end
627
+
628
+ def below?(other)
629
+ top >= other.bottom
630
+ end
631
+
632
+ def left_of?(other)
633
+ right <= other.left
634
+ end
635
+
636
+ def right_of?(other)
637
+ left >= other.right
638
+ end
639
+
640
+ def near?(other)
641
+ distance(other) <= 50
642
+ end
643
+
644
+ protected
645
+
646
+ def line_segments
647
+ [
648
+ [Vector[top, left], Vector[top, right]],
649
+ [Vector[top, right], Vector[bottom, left]],
650
+ [Vector[bottom, left], Vector[bottom, right]],
651
+ [Vector[bottom, right], Vector[top, left]]
652
+ ]
653
+ end
654
+
655
+ private
656
+
657
+ def distance_segment_segment(l1p1, l1p2, l2p1, l2p2)
658
+ # See http://geomalgorithms.com/a07-_distance.html
659
+ # rubocop:disable Naming/VariableName
660
+ u = l1p2 - l1p1
661
+ v = l2p2 - l2p1
662
+ w = l1p1 - l2p1
663
+
664
+ a = u.dot u
665
+ b = u.dot v
666
+ c = v.dot v
667
+
668
+ d = u.dot w
669
+ e = v.dot w
670
+ cap_d = (a * c) - (b**2)
671
+ sD = tD = cap_d
672
+
673
+ # compute the line parameters of the two closest points
674
+ if cap_d < Float::EPSILON # the lines are almost parallel
675
+ sN = 0.0 # force using point P0 on segment S1
676
+ sD = 1.0 # to prevent possible division by 0.0 later
677
+ tN = e
678
+ tD = c
679
+ else # get the closest points on the infinite lines
680
+ sN = (b * e) - (c * d)
681
+ tN = (a * e) - (b * d)
682
+ if sN.negative? # sc < 0 => the s=0 edge is visible
683
+ sN = 0
684
+ tN = e
685
+ tD = c
686
+ elsif sN > sD # sc > 1 => the s=1 edge is visible
687
+ sN = sD
688
+ tN = e + b
689
+ tD = c
690
+ end
691
+ end
692
+
693
+ if tN.negative? # tc < 0 => the t=0 edge is visible
694
+ tN = 0
695
+ # recompute sc for this edge
696
+ if (-d).negative?
697
+ sN = 0.0
698
+ elsif -d > a
699
+ sN = sD
700
+ else
701
+ sN = -d
702
+ sD = a
703
+ end
704
+ elsif tN > tD # tc > 1 => the t=1 edge is visible
705
+ tN = tD
706
+ # recompute sc for this edge
707
+ if (-d + b).negative?
708
+ sN = 0.0
709
+ elsif (-d + b) > a
710
+ sN = sD
711
+ else
712
+ sN = (-d + b)
713
+ sD = a
714
+ end
715
+ end
716
+
717
+ # finally do the division to get sc and tc
718
+ sc = sN.abs < Float::EPSILON ? 0.0 : sN / sD
719
+ tc = tN.abs < Float::EPSILON ? 0.0 : tN / tD
720
+
721
+ # difference of the two closest points
722
+ dP = w + (u * sc) - (v * tc)
723
+
724
+ Math.sqrt(dP.dot(dP))
725
+ # rubocop:enable Naming/VariableName
726
+ end
727
+ end
728
+
729
+ private_constant :Rectangle
136
730
  end
137
731
  end
138
732
  end