capybara 2.7.0 → 3.35.3

Sign up to get free protection for your applications and to get access to all the features.
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