capybara 3.0.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 (312) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/History.md +891 -12
  4. data/License.txt +1 -1
  5. data/README.md +257 -84
  6. data/lib/capybara/config.rb +29 -10
  7. data/lib/capybara/cucumber.rb +1 -1
  8. data/lib/capybara/driver/base.rb +22 -4
  9. data/lib/capybara/driver/node.rb +38 -9
  10. data/lib/capybara/dsl.rb +9 -7
  11. data/lib/capybara/helpers.rb +57 -8
  12. data/lib/capybara/minitest/spec.rb +185 -84
  13. data/lib/capybara/minitest.rb +264 -145
  14. data/lib/capybara/node/actions.rb +248 -124
  15. data/lib/capybara/node/base.rb +35 -20
  16. data/lib/capybara/node/document.rb +14 -2
  17. data/lib/capybara/node/document_matchers.rb +13 -15
  18. data/lib/capybara/node/element.rb +350 -113
  19. data/lib/capybara/node/finders.rb +104 -82
  20. data/lib/capybara/node/matchers.rb +363 -157
  21. data/lib/capybara/node/simple.rb +54 -15
  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 +9 -10
  25. data/lib/capybara/queries/base_query.rb +25 -18
  26. data/lib/capybara/queries/current_path_query.rb +16 -6
  27. data/lib/capybara/queries/match_query.rb +11 -0
  28. data/lib/capybara/queries/selector_query.rb +617 -104
  29. data/lib/capybara/queries/sibling_query.rb +9 -7
  30. data/lib/capybara/queries/style_query.rb +45 -0
  31. data/lib/capybara/queries/text_query.rb +40 -22
  32. data/lib/capybara/queries/title_query.rb +2 -2
  33. data/lib/capybara/rack_test/browser.rb +106 -31
  34. data/lib/capybara/rack_test/driver.rb +16 -7
  35. data/lib/capybara/rack_test/errors.rb +6 -0
  36. data/lib/capybara/rack_test/form.rb +74 -49
  37. data/lib/capybara/rack_test/node.rb +120 -47
  38. data/lib/capybara/rails.rb +1 -1
  39. data/lib/capybara/registration_container.rb +41 -0
  40. data/lib/capybara/registrations/drivers.rb +42 -0
  41. data/lib/capybara/registrations/patches/puma_ssl.rb +29 -0
  42. data/lib/capybara/registrations/servers.rb +66 -0
  43. data/lib/capybara/result.rb +87 -53
  44. data/lib/capybara/rspec/features.rb +8 -10
  45. data/lib/capybara/rspec/matcher_proxies.rb +39 -18
  46. data/lib/capybara/rspec/matchers/base.rb +113 -0
  47. data/lib/capybara/rspec/matchers/become_closed.rb +33 -0
  48. data/lib/capybara/rspec/matchers/compound.rb +88 -0
  49. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  50. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  51. data/lib/capybara/rspec/matchers/have_current_path.rb +29 -0
  52. data/lib/capybara/rspec/matchers/have_selector.rb +69 -0
  53. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  54. data/lib/capybara/rspec/matchers/have_text.rb +33 -0
  55. data/lib/capybara/rspec/matchers/have_title.rb +29 -0
  56. data/lib/capybara/rspec/matchers/match_selector.rb +27 -0
  57. data/lib/capybara/rspec/matchers/match_style.rb +43 -0
  58. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  59. data/lib/capybara/rspec/matchers.rb +142 -315
  60. data/lib/capybara/rspec.rb +3 -2
  61. data/lib/capybara/selector/builders/css_builder.rb +84 -0
  62. data/lib/capybara/selector/builders/xpath_builder.rb +71 -0
  63. data/lib/capybara/selector/css.rb +85 -8
  64. data/lib/capybara/selector/definition/button.rb +68 -0
  65. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  66. data/lib/capybara/selector/definition/css.rb +10 -0
  67. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  68. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  69. data/lib/capybara/selector/definition/element.rb +28 -0
  70. data/lib/capybara/selector/definition/field.rb +40 -0
  71. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  72. data/lib/capybara/selector/definition/file_field.rb +13 -0
  73. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  74. data/lib/capybara/selector/definition/frame.rb +17 -0
  75. data/lib/capybara/selector/definition/id.rb +6 -0
  76. data/lib/capybara/selector/definition/label.rb +62 -0
  77. data/lib/capybara/selector/definition/link.rb +55 -0
  78. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  79. data/lib/capybara/selector/definition/option.rb +27 -0
  80. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  81. data/lib/capybara/selector/definition/select.rb +81 -0
  82. data/lib/capybara/selector/definition/table.rb +109 -0
  83. data/lib/capybara/selector/definition/table_row.rb +21 -0
  84. data/lib/capybara/selector/definition/xpath.rb +5 -0
  85. data/lib/capybara/selector/definition.rb +280 -0
  86. data/lib/capybara/selector/filter.rb +1 -0
  87. data/lib/capybara/selector/filter_set.rb +72 -28
  88. data/lib/capybara/selector/filters/base.rb +45 -2
  89. data/lib/capybara/selector/filters/expression_filter.rb +5 -6
  90. data/lib/capybara/selector/filters/locator_filter.rb +29 -0
  91. data/lib/capybara/selector/filters/node_filter.rb +18 -4
  92. data/lib/capybara/selector/regexp_disassembler.rb +211 -0
  93. data/lib/capybara/selector/selector.rb +89 -200
  94. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  95. data/lib/capybara/selector.rb +474 -534
  96. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  97. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  98. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  99. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  100. data/lib/capybara/selenium/driver.rb +270 -245
  101. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +117 -0
  102. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +128 -0
  103. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +84 -0
  104. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +26 -0
  105. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +24 -0
  106. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  107. data/lib/capybara/selenium/extensions/find.rb +110 -0
  108. data/lib/capybara/selenium/extensions/html5_drag.rb +229 -0
  109. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  110. data/lib/capybara/selenium/extensions/scroll.rb +76 -0
  111. data/lib/capybara/selenium/node.rb +460 -170
  112. data/lib/capybara/selenium/nodes/chrome_node.rb +125 -0
  113. data/lib/capybara/selenium/nodes/edge_node.rb +110 -0
  114. data/lib/capybara/selenium/nodes/firefox_node.rb +136 -0
  115. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  116. data/lib/capybara/selenium/nodes/safari_node.rb +118 -0
  117. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  118. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  119. data/lib/capybara/selenium/patches/logs.rb +45 -0
  120. data/lib/capybara/selenium/patches/pause_duration_fix.rb +9 -0
  121. data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
  122. data/lib/capybara/server/animation_disabler.rb +80 -0
  123. data/lib/capybara/server/checker.rb +44 -0
  124. data/lib/capybara/server/middleware.rb +71 -0
  125. data/lib/capybara/server.rb +58 -67
  126. data/lib/capybara/session/config.rb +40 -6
  127. data/lib/capybara/session/matchers.rb +26 -19
  128. data/lib/capybara/session.rb +252 -194
  129. data/lib/capybara/spec/public/jquery.js +5 -5
  130. data/lib/capybara/spec/public/offset.js +6 -0
  131. data/lib/capybara/spec/public/test.js +126 -8
  132. data/lib/capybara/spec/session/accept_alert_spec.rb +11 -11
  133. data/lib/capybara/spec/session/accept_confirm_spec.rb +3 -3
  134. data/lib/capybara/spec/session/accept_prompt_spec.rb +9 -10
  135. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  136. data/lib/capybara/spec/session/all_spec.rb +135 -44
  137. data/lib/capybara/spec/session/ancestor_spec.rb +24 -19
  138. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +67 -38
  139. data/lib/capybara/spec/session/{assert_current_path.rb → assert_current_path_spec.rb} +20 -18
  140. data/lib/capybara/spec/session/assert_selector_spec.rb +143 -0
  141. data/lib/capybara/spec/session/assert_style_spec.rb +26 -0
  142. data/lib/capybara/spec/session/{assert_text.rb → assert_text_spec.rb} +76 -52
  143. data/lib/capybara/spec/session/{assert_title.rb → assert_title_spec.rb} +12 -12
  144. data/lib/capybara/spec/session/attach_file_spec.rb +126 -72
  145. data/lib/capybara/spec/session/body_spec.rb +11 -13
  146. data/lib/capybara/spec/session/check_spec.rb +112 -51
  147. data/lib/capybara/spec/session/choose_spec.rb +62 -30
  148. data/lib/capybara/spec/session/click_button_spec.rb +227 -161
  149. data/lib/capybara/spec/session/click_link_or_button_spec.rb +49 -30
  150. data/lib/capybara/spec/session/click_link_spec.rb +89 -55
  151. data/lib/capybara/spec/session/current_scope_spec.rb +8 -8
  152. data/lib/capybara/spec/session/current_url_spec.rb +44 -37
  153. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +3 -3
  154. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +2 -2
  155. data/lib/capybara/spec/session/element/{assert_match_selector.rb → assert_match_selector_spec.rb} +11 -9
  156. data/lib/capybara/spec/session/element/match_css_spec.rb +18 -10
  157. data/lib/capybara/spec/session/element/match_xpath_spec.rb +8 -6
  158. data/lib/capybara/spec/session/element/matches_selector_spec.rb +70 -56
  159. data/lib/capybara/spec/session/evaluate_async_script_spec.rb +7 -7
  160. data/lib/capybara/spec/session/evaluate_script_spec.rb +28 -8
  161. data/lib/capybara/spec/session/execute_script_spec.rb +8 -7
  162. data/lib/capybara/spec/session/fill_in_spec.rb +110 -44
  163. data/lib/capybara/spec/session/find_button_spec.rb +23 -23
  164. data/lib/capybara/spec/session/find_by_id_spec.rb +8 -8
  165. data/lib/capybara/spec/session/find_field_spec.rb +33 -31
  166. data/lib/capybara/spec/session/find_link_spec.rb +42 -14
  167. data/lib/capybara/spec/session/find_spec.rb +251 -142
  168. data/lib/capybara/spec/session/first_spec.rb +45 -44
  169. data/lib/capybara/spec/session/frame/frame_title_spec.rb +6 -6
  170. data/lib/capybara/spec/session/frame/frame_url_spec.rb +6 -6
  171. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +32 -20
  172. data/lib/capybara/spec/session/frame/within_frame_spec.rb +46 -19
  173. data/lib/capybara/spec/session/go_back_spec.rb +1 -1
  174. data/lib/capybara/spec/session/go_forward_spec.rb +1 -1
  175. data/lib/capybara/spec/session/has_all_selectors_spec.rb +23 -23
  176. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  177. data/lib/capybara/spec/session/has_any_selectors_spec.rb +29 -0
  178. data/lib/capybara/spec/session/has_button_spec.rb +98 -12
  179. data/lib/capybara/spec/session/has_css_spec.rb +271 -137
  180. data/lib/capybara/spec/session/has_current_path_spec.rb +50 -35
  181. data/lib/capybara/spec/session/has_element_spec.rb +47 -0
  182. data/lib/capybara/spec/session/has_field_spec.rb +137 -58
  183. data/lib/capybara/spec/session/has_link_spec.rb +46 -6
  184. data/lib/capybara/spec/session/has_none_selectors_spec.rb +33 -31
  185. data/lib/capybara/spec/session/has_select_spec.rb +84 -50
  186. data/lib/capybara/spec/session/has_selector_spec.rb +117 -69
  187. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  188. data/lib/capybara/spec/session/has_table_spec.rb +181 -4
  189. data/lib/capybara/spec/session/has_text_spec.rb +101 -53
  190. data/lib/capybara/spec/session/has_title_spec.rb +19 -14
  191. data/lib/capybara/spec/session/has_xpath_spec.rb +56 -38
  192. data/lib/capybara/spec/session/{headers.rb → headers_spec.rb} +1 -1
  193. data/lib/capybara/spec/session/html_spec.rb +13 -6
  194. data/lib/capybara/spec/session/matches_style_spec.rb +37 -0
  195. data/lib/capybara/spec/session/node_spec.rb +958 -122
  196. data/lib/capybara/spec/session/node_wrapper_spec.rb +15 -12
  197. data/lib/capybara/spec/session/refresh_spec.rb +9 -7
  198. data/lib/capybara/spec/session/reset_session_spec.rb +65 -37
  199. data/lib/capybara/spec/session/{response_code.rb → response_code_spec.rb} +1 -1
  200. data/lib/capybara/spec/session/save_and_open_page_spec.rb +2 -2
  201. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +5 -4
  202. data/lib/capybara/spec/session/save_page_spec.rb +41 -38
  203. data/lib/capybara/spec/session/save_screenshot_spec.rb +13 -11
  204. data/lib/capybara/spec/session/screenshot_spec.rb +2 -2
  205. data/lib/capybara/spec/session/scroll_spec.rb +119 -0
  206. data/lib/capybara/spec/session/select_spec.rb +102 -76
  207. data/lib/capybara/spec/session/selectors_spec.rb +51 -18
  208. data/lib/capybara/spec/session/sibling_spec.rb +9 -9
  209. data/lib/capybara/spec/session/text_spec.rb +26 -24
  210. data/lib/capybara/spec/session/title_spec.rb +8 -6
  211. data/lib/capybara/spec/session/uncheck_spec.rb +41 -22
  212. data/lib/capybara/spec/session/unselect_spec.rb +37 -37
  213. data/lib/capybara/spec/session/visit_spec.rb +79 -53
  214. data/lib/capybara/spec/session/window/become_closed_spec.rb +22 -19
  215. data/lib/capybara/spec/session/window/current_window_spec.rb +4 -3
  216. data/lib/capybara/spec/session/window/open_new_window_spec.rb +4 -3
  217. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +25 -21
  218. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +10 -5
  219. data/lib/capybara/spec/session/window/window_spec.rb +88 -54
  220. data/lib/capybara/spec/session/window/windows_spec.rb +11 -8
  221. data/lib/capybara/spec/session/window/within_window_spec.rb +17 -16
  222. data/lib/capybara/spec/session/within_spec.rb +82 -44
  223. data/lib/capybara/spec/spec_helper.rb +46 -52
  224. data/lib/capybara/spec/test_app.rb +148 -41
  225. data/lib/capybara/spec/views/animated.erb +49 -0
  226. data/lib/capybara/spec/views/form.erb +156 -42
  227. data/lib/capybara/spec/views/frame_child.erb +4 -3
  228. data/lib/capybara/spec/views/frame_one.erb +2 -1
  229. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  230. data/lib/capybara/spec/views/frame_two.erb +1 -1
  231. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  232. data/lib/capybara/spec/views/layout.erb +10 -0
  233. data/lib/capybara/spec/views/obscured.erb +47 -0
  234. data/lib/capybara/spec/views/offset.erb +33 -0
  235. data/lib/capybara/spec/views/path.erb +2 -2
  236. data/lib/capybara/spec/views/popup_one.erb +1 -1
  237. data/lib/capybara/spec/views/popup_two.erb +1 -1
  238. data/lib/capybara/spec/views/react.erb +45 -0
  239. data/lib/capybara/spec/views/scroll.erb +21 -0
  240. data/lib/capybara/spec/views/spatial.erb +31 -0
  241. data/lib/capybara/spec/views/tables.erb +68 -1
  242. data/lib/capybara/spec/views/with_animation.erb +81 -0
  243. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  244. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  245. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  246. data/lib/capybara/spec/views/with_hover.erb +3 -2
  247. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  248. data/lib/capybara/spec/views/with_html.erb +46 -11
  249. data/lib/capybara/spec/views/with_html5_svg.erb +20 -0
  250. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  251. data/lib/capybara/spec/views/with_js.erb +30 -5
  252. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  253. data/lib/capybara/spec/views/with_namespace.erb +21 -0
  254. data/lib/capybara/spec/views/with_scope.erb +2 -2
  255. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  256. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  257. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  258. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  259. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  260. data/lib/capybara/spec/views/with_windows.erb +1 -1
  261. data/lib/capybara/spec/views/within_frames.erb +1 -1
  262. data/lib/capybara/version.rb +1 -1
  263. data/lib/capybara/window.rb +32 -26
  264. data/lib/capybara.rb +128 -104
  265. data/spec/basic_node_spec.rb +59 -34
  266. data/spec/capybara_spec.rb +65 -51
  267. data/spec/counter_spec.rb +35 -0
  268. data/spec/css_builder_spec.rb +101 -0
  269. data/spec/css_splitter_spec.rb +38 -0
  270. data/spec/dsl_spec.rb +84 -55
  271. data/spec/filter_set_spec.rb +24 -7
  272. data/spec/fixtures/certificate.pem +25 -0
  273. data/spec/fixtures/key.pem +27 -0
  274. data/spec/fixtures/selenium_driver_rspec_failure.rb +5 -5
  275. data/spec/fixtures/selenium_driver_rspec_success.rb +5 -5
  276. data/spec/minitest_spec.rb +49 -5
  277. data/spec/minitest_spec_spec.rb +92 -62
  278. data/spec/per_session_config_spec.rb +6 -6
  279. data/spec/rack_test_spec.rb +183 -115
  280. data/spec/regexp_dissassembler_spec.rb +250 -0
  281. data/spec/result_spec.rb +99 -39
  282. data/spec/rspec/features_spec.rb +28 -25
  283. data/spec/rspec/scenarios_spec.rb +10 -6
  284. data/spec/rspec/shared_spec_matchers.rb +418 -364
  285. data/spec/rspec/views_spec.rb +4 -3
  286. data/spec/rspec_matchers_spec.rb +35 -10
  287. data/spec/rspec_spec.rb +109 -85
  288. data/spec/sauce_spec_chrome.rb +43 -0
  289. data/spec/selector_spec.rb +392 -62
  290. data/spec/selenium_spec_chrome.rb +183 -41
  291. data/spec/selenium_spec_chrome_remote.rb +96 -0
  292. data/spec/selenium_spec_edge.rb +41 -8
  293. data/spec/selenium_spec_firefox.rb +228 -0
  294. data/spec/selenium_spec_firefox_remote.rb +94 -0
  295. data/spec/selenium_spec_ie.rb +129 -11
  296. data/spec/selenium_spec_safari.rb +162 -0
  297. data/spec/server_spec.rb +192 -81
  298. data/spec/session_spec.rb +52 -16
  299. data/spec/shared_selenium_node.rb +79 -0
  300. data/spec/shared_selenium_session.rb +460 -123
  301. data/spec/spec_helper.rb +124 -2
  302. data/spec/whitespace_normalizer_spec.rb +54 -0
  303. data/spec/xpath_builder_spec.rb +93 -0
  304. metadata +344 -45
  305. data/.yard/templates_custom/default/class/html/selectors.erb +0 -38
  306. data/.yard/templates_custom/default/class/html/setup.rb +0 -17
  307. data/.yard/yard_extensions.rb +0 -78
  308. data/lib/capybara/rspec/compound.rb +0 -90
  309. data/lib/capybara/spec/session/assert_selector.rb +0 -149
  310. data/lib/capybara/spec/session/source_spec.rb +0 -0
  311. data/lib/capybara/spec/views/with_title.erb +0 -5
  312. data/spec/selenium_spec_marionette.rb +0 -143
@@ -1,26 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'matrix'
4
+
3
5
  module Capybara
4
6
  module Queries
5
7
  class SelectorQuery < Queries::BaseQuery
6
- attr_accessor :selector, :locator, :options, :expression, :find, :negative
8
+ attr_reader :expression, :selector, :locator, :options
7
9
 
8
- VALID_KEYS = COUNT_KEYS + %i[text id class visible exact exact_text match wait filter_set]
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]
9
13
  VALID_MATCH = %i[first smart prefer_exact one].freeze
10
14
 
11
- def initialize(*args, session_options:, **options, &filter_block)
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)
12
24
  @resolved_node = nil
25
+ @resolved_count = 0
13
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
+ )
34
+ end
35
+
14
36
  super(@options)
15
37
  self.session_options = session_options
16
38
 
17
- @selector = find_selector(args[0].is_a?(Symbol) ? args.shift : args[0])
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
+
18
49
  @locator = args.shift
19
50
  @filter_block = filter_block
20
51
 
21
52
  raise ArgumentError, "Unused parameters passed to #{self.class.name} : #{args}" unless args.empty?
22
53
 
23
- @expression = @selector.call(@locator, @options.merge(enable_aria_label: session_options.enable_aria_label))
54
+ @expression = selector.call(@locator, **@options)
24
55
 
25
56
  warn_exact_usage
26
57
 
@@ -30,37 +61,74 @@ module Capybara
30
61
  def name; selector.name; end
31
62
  def label; selector.label || selector.name; end
32
63
 
33
- def description
34
- @description = "".dup
35
- @description << "visible " if visible == :visible
36
- @description << "non-visible " if visible == :hidden
37
- @description << "#{label} #{locator.inspect}"
38
- @description << " with#{" exact" if exact_text == true} text #{options[:text].inspect}" if options[:text]
39
- @description << " with exact text #{options[:exact_text]}" if options[:exact_text].is_a?(String)
40
- @description << " with id #{options[:id]}" if options[:id]
41
- @description << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
42
- @description << selector.description(options)
43
- @description << " that also matches the custom filter block" if @filter_block
44
- @description << " within #{@resolved_node.inspect}" if describe_within?
45
- @description
46
- end
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
71
+ end
72
+
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)
79
+ end
80
+
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 ''
94
+ end
47
95
 
48
- def matches_filters?(node)
49
- return false if options[:text] && !matches_text_filter(node, options[:text])
50
- return false if exact_text.is_a?(String) && !matches_exact_text_filter(node, exact_text)
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
99
+ end
100
+ end
101
+
102
+ desc << selector.description(node_filters: show_for[:node], **options)
103
+
104
+ desc << ' that also matches the custom filter block' if @filter_block && show_for[:node]
51
105
 
52
- case visible
53
- when :visible then return false unless node.visible?
54
- when :hidden then return false if node.visible?
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"
55
110
  end
111
+ desc
112
+ end
113
+
114
+ def applied_description
115
+ description(true)
116
+ end
56
117
 
57
- matches_node_filters?(node) && matches_filter_block?(node)
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)
58
126
  rescue *(node.respond_to?(:session) ? node.session.driver.invalid_element_errors : [])
59
- return false
127
+ false
60
128
  end
61
129
 
62
130
  def visible
63
- case (vis = options.fetch(:visible) { @selector.default_visibility(session_options.ignore_hidden_elements) })
131
+ case (vis = options.fetch(:visible) { default_visibility })
64
132
  when true then :visible
65
133
  when false then :all
66
134
  else vis
@@ -79,60 +147,156 @@ module Capybara
79
147
  exact = exact? if exact.nil?
80
148
  expr = apply_expression_filters(@expression)
81
149
  expr = exact ? expr.to_xpath(:exact) : expr.to_s if expr.respond_to?(:to_xpath)
82
- filtered_xpath(expr)
150
+ expr = filtered_expression(expr)
151
+ expr = "(#{expr})[#{xpath_text_conditions}]" if try_text_match_in_expression?
152
+ expr
83
153
  end
84
154
 
85
155
  def css
86
- filtered_css(apply_expression_filters(@expression))
156
+ filtered_expression(apply_expression_filters(@expression))
87
157
  end
88
158
 
89
159
  # @api private
90
160
  def resolve_for(node, exact = nil)
161
+ applied_filters.clear
162
+ @filter_cache.clear
91
163
  @resolved_node = node
164
+ @resolved_count += 1
165
+
92
166
  node.synchronize do
93
- children = if selector.format == :css
94
- node.find_css(css)
95
- else
96
- node.find_xpath(xpath(exact))
97
- end.map do |child|
98
- if node.is_a?(Capybara::Node::Base)
99
- Capybara::Node::Element.new(node.session, child, node, self)
100
- else
101
- Capybara::Node::Simple.new(child)
102
- end
103
- end
104
- 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)
105
169
  end
106
170
  end
107
171
 
108
172
  # @api private
109
173
  def supports_exact?
110
- @expression.respond_to? :to_xpath
174
+ return @expression.respond_to? :to_xpath if @selector.supports_exact?.nil?
175
+
176
+ @selector.supports_exact?
177
+ end
178
+
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
111
185
  end
112
186
 
113
187
  private
114
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
+
115
235
  def find_selector(locator)
116
- selector = if locator.is_a?(Symbol)
117
- Selector.all.fetch(locator) { |sel_type| raise ArgumentError, "Unknown selector type (:#{sel_type})" }
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)
118
270
  else
119
- Selector.all.values.find { |s| s.match?(locator) }
271
+ Capybara::Node::Simple.new(node)
120
272
  end
121
- selector || Selector.all[session_options.default_selector]
122
273
  end
123
274
 
124
275
  def valid_keys
125
- VALID_KEYS + custom_keys
276
+ (VALID_KEYS + custom_keys).uniq
126
277
  end
127
278
 
128
- def matches_node_filters?(node)
129
- node_filters.all? do |name, filter|
130
- if options.key?(name)
131
- filter.matches?(node, options[name])
132
- elsif filter.default?
133
- filter.matches?(node, filter.default)
134
- else
135
- true
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
136
300
  end
137
301
  end
138
302
  end
@@ -147,71 +311,107 @@ module Capybara
147
311
  end
148
312
  end
149
313
 
314
+ def filter_set(name)
315
+ ::Capybara::Selector::FilterSet[name]
316
+ end
317
+
150
318
  def node_filters
151
319
  if options.key?(:filter_set)
152
- ::Capybara::Selector::FilterSet.all[options[:filter_set]].node_filters
320
+ filter_set(options[:filter_set])
153
321
  else
154
- @selector.node_filters
155
- end
322
+ @selector
323
+ end.node_filters
156
324
  end
157
325
 
158
326
  def expression_filters
159
327
  filters = @selector.expression_filters
160
- filters.merge ::Capybara::Selector::FilterSet.all[options[:filter_set]].expression_filters if options.key?(:filter_set)
328
+ filters.merge filter_set(options[:filter_set]).expression_filters if options.key?(:filter_set)
161
329
  filters
162
330
  end
163
331
 
332
+ def ordered_results(results)
333
+ case @order
334
+ when :reverse
335
+ results.reverse
336
+ else
337
+ results
338
+ end
339
+ end
340
+
164
341
  def custom_keys
165
342
  @custom_keys ||= node_filters.keys + expression_filters.keys
166
343
  end
167
344
 
168
345
  def assert_valid_keys
169
- super
170
346
  unless VALID_MATCH.include?(match)
171
- 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(', ')}"
172
348
  end
173
- end
174
349
 
175
- def filtered_xpath(expr)
176
- if options.key?(:id) || options.key?(:class)
177
- expr = "(#{expr})"
178
- expr = "#{expr}[#{XPath.attr(:id) == options[:id]}]" if options.key?(:id) && !custom_keys.include?(:id)
179
- if options.key?(:class) && !custom_keys.include?(:class)
180
- class_xpath = Array(options[:class]).map do |klass|
181
- XPath.attr(:class).contains_word(klass)
182
- end.reduce(:&)
183
- expr = "#{expr}[#{class_xpath}]"
184
- 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 }
185
354
  end
186
- 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}"
187
361
  end
188
362
 
189
- def filtered_css(expr)
190
- if options.key?(:id) || options.key?(:class)
191
- css_selectors = expr.split(',').map(&:rstrip)
192
- expr = css_selectors.map do |sel|
193
- sel += "##{Capybara::Selector::CSS.escape(options[:id])}" if options.key?(:id) && !custom_keys.include?(:id)
194
- sel += Array(options[:class]).map { |k| ".#{Capybara::Selector::CSS.escape(k)}" }.join if options.key?(:class) && !custom_keys.include?(:class)
195
- sel
196
- end.join(", ")
197
- end
198
- 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)
373
+ end
374
+
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?
199
389
  end
200
390
 
201
- def apply_expression_filters(expr)
202
- expression_filters.inject(expr) do |memo, (name, ef)|
203
- if options.key?(name)
204
- ef.apply_filter(memo, options[name])
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)
205
404
  elsif ef.default?
206
- ef.apply_filter(memo, ef.default)
405
+ ef.apply_filter(expr, name, ef.default, @selector)
207
406
  else
208
- memo
407
+ expr
209
408
  end
210
409
  end
211
410
  end
212
411
 
213
412
  def warn_exact_usage
214
413
  return unless options.key?(:exact) && !supports_exact?
414
+
215
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."
216
416
  end
217
417
 
@@ -220,29 +420,342 @@ module Capybara
220
420
  end
221
421
 
222
422
  def describe_within?
223
- @resolved_node && !(@resolved_node.is_a?(::Capybara::Node::Document) ||
224
- (@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)
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?)
225
442
  end
226
443
 
227
- def matches_text_filter(node, text_option)
228
- regexp = if text_option.is_a?(Regexp)
229
- text_option
230
- elsif exact_text == true
231
- /\A#{Regexp.escape(text_option.to_s)}\z/
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]
232
503
  else
233
- Regexp.escape(text_option.to_s)
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
234
508
  end
235
- text_visible = visible
236
- text_visible = :all if text_visible == :hidden
237
- node.text(text_visible).match(regexp)
238
509
  end
239
510
 
240
- def matches_exact_text_filter(node, exact_text_option)
241
- regexp = /\A#{Regexp.escape(exact_text_option)}\z/
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)
242
601
  text_visible = visible
243
602
  text_visible = :all if text_visible == :hidden
244
- node.text(text_visible).match(regexp)
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))
245
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
246
759
  end
247
760
  end
248
761
  end