capybara 3.32.2

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