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