capybara 3.32.2

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