capybara 2.18.0 → 3.40.0

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