capybara 2.15.1 → 3.36.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (311) hide show
  1. checksums.yaml +5 -5
  2. data/.yardopts +1 -1
  3. data/History.md +902 -10
  4. data/License.txt +1 -1
  5. data/README.md +103 -75
  6. data/lib/capybara/config.rb +29 -56
  7. data/lib/capybara/cucumber.rb +2 -3
  8. data/lib/capybara/driver/base.rb +39 -18
  9. data/lib/capybara/driver/node.rb +36 -10
  10. data/lib/capybara/dsl.rb +15 -6
  11. data/lib/capybara/helpers.rb +65 -30
  12. data/lib/capybara/minitest/spec.rb +173 -81
  13. data/lib/capybara/minitest.rb +220 -111
  14. data/lib/capybara/node/actions.rb +274 -171
  15. data/lib/capybara/node/base.rb +41 -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 +353 -137
  19. data/lib/capybara/node/finders.rb +144 -138
  20. data/lib/capybara/node/matchers.rb +369 -209
  21. data/lib/capybara/node/simple.rb +59 -26
  22. data/lib/capybara/queries/active_element_query.rb +18 -0
  23. data/lib/capybara/queries/ancestor_query.rb +13 -10
  24. data/lib/capybara/queries/base_query.rb +39 -28
  25. data/lib/capybara/queries/current_path_query.rb +22 -25
  26. data/lib/capybara/queries/match_query.rb +14 -7
  27. data/lib/capybara/queries/selector_query.rb +646 -145
  28. data/lib/capybara/queries/sibling_query.rb +12 -10
  29. data/lib/capybara/queries/style_query.rb +45 -0
  30. data/lib/capybara/queries/text_query.rb +56 -38
  31. data/lib/capybara/queries/title_query.rb +8 -11
  32. data/lib/capybara/rack_test/browser.rb +57 -41
  33. data/lib/capybara/rack_test/css_handlers.rb +6 -4
  34. data/lib/capybara/rack_test/driver.rb +18 -13
  35. data/lib/capybara/rack_test/errors.rb +6 -0
  36. data/lib/capybara/rack_test/form.rb +73 -58
  37. data/lib/capybara/rack_test/node.rb +188 -81
  38. data/lib/capybara/rails.rb +3 -7
  39. data/lib/capybara/registration_container.rb +44 -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 +45 -0
  43. data/lib/capybara/result.rb +96 -62
  44. data/lib/capybara/rspec/features.rb +17 -50
  45. data/lib/capybara/rspec/matcher_proxies.rb +52 -15
  46. data/lib/capybara/rspec/matchers/base.rb +111 -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 +77 -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 +144 -264
  60. data/lib/capybara/rspec.rb +7 -11
  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 +89 -17
  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 +54 -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 +279 -0
  86. data/lib/capybara/selector/filter.rb +2 -17
  87. data/lib/capybara/selector/filter_set.rb +81 -33
  88. data/lib/capybara/selector/filters/base.rb +50 -6
  89. data/lib/capybara/selector/filters/expression_filter.rb +8 -26
  90. data/lib/capybara/selector/filters/locator_filter.rb +29 -0
  91. data/lib/capybara/selector/filters/node_filter.rb +16 -12
  92. data/lib/capybara/selector/regexp_disassembler.rb +214 -0
  93. data/lib/capybara/selector/selector.rb +89 -210
  94. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  95. data/lib/capybara/selector.rb +227 -526
  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 +343 -276
  101. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +117 -0
  102. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +124 -0
  103. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +89 -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 +228 -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/logger_suppressor.rb +40 -0
  112. data/lib/capybara/selenium/node.rb +508 -124
  113. data/lib/capybara/selenium/nodes/chrome_node.rb +137 -0
  114. data/lib/capybara/selenium/nodes/edge_node.rb +104 -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/action_pauser.rb +26 -0
  119. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  120. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  121. data/lib/capybara/selenium/patches/logs.rb +45 -0
  122. data/lib/capybara/selenium/patches/pause_duration_fix.rb +9 -0
  123. data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
  124. data/lib/capybara/server/animation_disabler.rb +69 -0
  125. data/lib/capybara/server/checker.rb +44 -0
  126. data/lib/capybara/server/middleware.rb +71 -0
  127. data/lib/capybara/server.rb +59 -67
  128. data/lib/capybara/session/config.rb +79 -59
  129. data/lib/capybara/session/matchers.rb +41 -25
  130. data/lib/capybara/session.rb +371 -357
  131. data/lib/capybara/spec/public/jquery.js +5 -5
  132. data/lib/capybara/spec/public/offset.js +6 -0
  133. data/lib/capybara/spec/public/test.js +159 -13
  134. data/lib/capybara/spec/session/accept_alert_spec.rb +12 -11
  135. data/lib/capybara/spec/session/accept_confirm_spec.rb +6 -5
  136. data/lib/capybara/spec/session/accept_prompt_spec.rb +34 -6
  137. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  138. data/lib/capybara/spec/session/all_spec.rb +161 -55
  139. data/lib/capybara/spec/session/ancestor_spec.rb +27 -24
  140. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +68 -38
  141. data/lib/capybara/spec/session/assert_current_path_spec.rb +75 -0
  142. data/lib/capybara/spec/session/assert_selector_spec.rb +143 -0
  143. data/lib/capybara/spec/session/assert_style_spec.rb +26 -0
  144. data/lib/capybara/spec/session/{assert_text.rb → assert_text_spec.rb} +91 -59
  145. data/lib/capybara/spec/session/{assert_title.rb → assert_title_spec.rb} +22 -12
  146. data/lib/capybara/spec/session/attach_file_spec.rb +138 -69
  147. data/lib/capybara/spec/session/body_spec.rb +12 -13
  148. data/lib/capybara/spec/session/check_spec.rb +116 -55
  149. data/lib/capybara/spec/session/choose_spec.rb +64 -31
  150. data/lib/capybara/spec/session/click_button_spec.rb +231 -173
  151. data/lib/capybara/spec/session/click_link_or_button_spec.rb +55 -35
  152. data/lib/capybara/spec/session/click_link_spec.rb +82 -58
  153. data/lib/capybara/spec/session/current_scope_spec.rb +11 -10
  154. data/lib/capybara/spec/session/current_url_spec.rb +57 -39
  155. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +4 -4
  156. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +3 -2
  157. data/lib/capybara/spec/session/element/{assert_match_selector.rb → assert_match_selector_spec.rb} +11 -9
  158. data/lib/capybara/spec/session/element/match_css_spec.rb +18 -10
  159. data/lib/capybara/spec/session/element/match_xpath_spec.rb +9 -7
  160. data/lib/capybara/spec/session/element/matches_selector_spec.rb +71 -57
  161. data/lib/capybara/spec/session/evaluate_async_script_spec.rb +23 -0
  162. data/lib/capybara/spec/session/evaluate_script_spec.rb +30 -9
  163. data/lib/capybara/spec/session/execute_script_spec.rb +10 -8
  164. data/lib/capybara/spec/session/fill_in_spec.rb +128 -43
  165. data/lib/capybara/spec/session/find_button_spec.rb +25 -24
  166. data/lib/capybara/spec/session/find_by_id_spec.rb +10 -9
  167. data/lib/capybara/spec/session/find_field_spec.rb +37 -41
  168. data/lib/capybara/spec/session/find_link_spec.rb +36 -17
  169. data/lib/capybara/spec/session/find_spec.rb +245 -144
  170. data/lib/capybara/spec/session/first_spec.rb +79 -51
  171. data/lib/capybara/spec/session/frame/frame_title_spec.rb +23 -0
  172. data/lib/capybara/spec/session/frame/frame_url_spec.rb +23 -0
  173. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +33 -20
  174. data/lib/capybara/spec/session/frame/within_frame_spec.rb +50 -32
  175. data/lib/capybara/spec/session/go_back_spec.rb +2 -1
  176. data/lib/capybara/spec/session/go_forward_spec.rb +2 -1
  177. data/lib/capybara/spec/session/has_all_selectors_spec.rb +69 -0
  178. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  179. data/lib/capybara/spec/session/has_any_selectors_spec.rb +29 -0
  180. data/lib/capybara/spec/session/has_button_spec.rb +94 -13
  181. data/lib/capybara/spec/session/has_css_spec.rb +272 -137
  182. data/lib/capybara/spec/session/has_current_path_spec.rb +87 -45
  183. data/lib/capybara/spec/session/has_field_spec.rb +139 -59
  184. data/lib/capybara/spec/session/has_link_spec.rb +34 -9
  185. data/lib/capybara/spec/session/has_none_selectors_spec.rb +78 -0
  186. data/lib/capybara/spec/session/has_select_spec.rb +103 -74
  187. data/lib/capybara/spec/session/has_selector_spec.rb +105 -71
  188. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  189. data/lib/capybara/spec/session/has_table_spec.rb +172 -5
  190. data/lib/capybara/spec/session/has_text_spec.rb +113 -61
  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 +35 -0
  196. data/lib/capybara/spec/session/node_spec.rb +950 -152
  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 +69 -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 +117 -0
  207. data/lib/capybara/spec/session/select_spec.rb +107 -80
  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 +42 -22
  213. data/lib/capybara/spec/session/unselect_spec.rb +39 -38
  214. data/lib/capybara/spec/session/visit_spec.rb +99 -32
  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 +70 -44
  224. data/lib/capybara/spec/spec_helper.rb +49 -43
  225. data/lib/capybara/spec/test_app.rb +89 -42
  226. data/lib/capybara/spec/views/animated.erb +49 -0
  227. data/lib/capybara/spec/views/form.erb +141 -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 +11 -0
  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 +18 -0
  247. data/lib/capybara/spec/views/with_hover.erb +8 -2
  248. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  249. data/lib/capybara/spec/views/with_html.erb +70 -11
  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 +39 -3
  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_other.erb +6 -0
  256. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  257. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  258. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  259. data/lib/capybara/spec/views/with_windows.erb +1 -1
  260. data/lib/capybara/spec/views/within_frames.erb +5 -2
  261. data/lib/capybara/version.rb +2 -1
  262. data/lib/capybara/window.rb +35 -33
  263. data/lib/capybara.rb +126 -103
  264. data/spec/basic_node_spec.rb +60 -34
  265. data/spec/capybara_spec.rb +53 -104
  266. data/spec/css_builder_spec.rb +101 -0
  267. data/spec/css_splitter_spec.rb +38 -0
  268. data/spec/dsl_spec.rb +81 -62
  269. data/spec/filter_set_spec.rb +27 -9
  270. data/spec/fixtures/certificate.pem +25 -0
  271. data/spec/fixtures/key.pem +27 -0
  272. data/spec/fixtures/selenium_driver_rspec_failure.rb +5 -4
  273. data/spec/fixtures/selenium_driver_rspec_success.rb +5 -4
  274. data/spec/minitest_spec.rb +49 -7
  275. data/spec/minitest_spec_spec.rb +94 -59
  276. data/spec/per_session_config_spec.rb +14 -13
  277. data/spec/rack_test_spec.rb +180 -125
  278. data/spec/regexp_dissassembler_spec.rb +250 -0
  279. data/spec/result_spec.rb +99 -45
  280. data/spec/rspec/features_spec.rb +37 -31
  281. data/spec/rspec/scenarios_spec.rb +9 -7
  282. data/spec/rspec/shared_spec_matchers.rb +448 -421
  283. data/spec/rspec/views_spec.rb +5 -3
  284. data/spec/rspec_matchers_spec.rb +27 -11
  285. data/spec/rspec_spec.rb +109 -89
  286. data/spec/sauce_spec_chrome.rb +43 -0
  287. data/spec/selector_spec.rb +396 -67
  288. data/spec/selenium_spec_chrome.rb +183 -35
  289. data/spec/selenium_spec_chrome_remote.rb +101 -0
  290. data/spec/selenium_spec_edge.rb +47 -0
  291. data/spec/selenium_spec_firefox.rb +184 -41
  292. data/spec/selenium_spec_firefox_remote.rb +80 -0
  293. data/spec/selenium_spec_ie.rb +147 -0
  294. data/spec/selenium_spec_safari.rb +156 -0
  295. data/spec/server_spec.rb +198 -99
  296. data/spec/session_spec.rb +53 -16
  297. data/spec/shared_selenium_node.rb +79 -0
  298. data/spec/shared_selenium_session.rb +489 -97
  299. data/spec/spec_helper.rb +93 -7
  300. data/spec/xpath_builder_spec.rb +93 -0
  301. metadata +365 -70
  302. data/.yard/templates_custom/default/class/html/selectors.erb +0 -38
  303. data/.yard/templates_custom/default/class/html/setup.rb +0 -17
  304. data/.yard/yard_extensions.rb +0 -78
  305. data/lib/capybara/query.rb +0 -7
  306. data/lib/capybara/rspec/compound.rb +0 -95
  307. data/lib/capybara/spec/session/assert_current_path.rb +0 -72
  308. data/lib/capybara/spec/session/assert_selector.rb +0 -148
  309. data/lib/capybara/spec/session/source_spec.rb +0 -0
  310. data/lib/capybara/spec/views/with_title.erb +0 -5
  311. data/spec/selenium_spec_marionette.rb +0 -117
@@ -1,38 +1,51 @@
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
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)
24
+ @resolved_node = nil
25
+ @resolved_count = 0
26
+ @options = options.dup
27
+ @order = order
28
+ @filter_cache = Hash.new { |hsh, key| hsh[key] = {} }
6
29
 
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)
11
- @options = if args.last.is_a?(Hash) then args.pop.dup else {} end
12
30
  super(@options)
13
-
31
+ self.session_options = session_options
32
+
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
+
43
+ @locator = args.shift
14
44
  @filter_block = filter_block
15
45
 
16
- if args[0].is_a?(Symbol)
17
- @selector = Selector.all.fetch(args.shift) do |selector_type|
18
- raise ArgumentError, "Unknown selector type (:#{selector_type})"
19
- nil
20
- end
21
- @locator = args.shift
22
- else
23
- @selector = Selector.all.values.find { |s| s.match?(args[0]) }
24
- @locator = args.shift
25
- end
26
- @selector ||= Selector.all[session_options.default_selector]
46
+ raise ArgumentError, "Unused parameters passed to #{self.class.name} : #{args}" unless args.empty?
27
47
 
28
- warn "Unused parameters passed to #{self.class.name} : #{args.to_s}" unless args.empty?
29
-
30
- # for compatibility with Capybara 2.0
31
- if session_options.exact_options and @selector == Selector.all[:option]
32
- @options[:exact] = true
33
- end
34
-
35
- @expression = @selector.call(@locator, @options.merge(enable_aria_label: session_options.enable_aria_label))
48
+ @expression = selector.call(@locator, **@options)
36
49
 
37
50
  warn_exact_usage
38
51
 
@@ -40,205 +53,693 @@ module Capybara
40
53
  end
41
54
 
42
55
  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
57
- end
58
-
59
- def matches_filters?(node)
60
- if options[:text]
61
- regexp = if options[:text].is_a?(Regexp)
62
- options[:text]
63
- else
64
- if exact_text == true
65
- /\A#{Regexp.escape(options[:text].to_s)}\z/
66
- else
67
- Regexp.escape(options[:text].to_s)
68
- end
69
- end
70
- text_visible = visible
71
- text_visible = :all if text_visible == :hidden
72
- return false if not node.text(text_visible).match(regexp)
56
+ def label; selector.label || selector.name; end
57
+
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
73
65
  end
74
66
 
75
- if exact_text.is_a?(String)
76
- regexp = /\A#{Regexp.escape(options[:exact_text])}\z/
77
- text_visible = visible
78
- text_visible = :all if text_visible == :hidden
79
- return false if not node.text(text_visible).match(regexp)
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)
80
72
  end
81
73
 
82
- case visible
83
- when :visible then return false unless node.visible?
84
- when :hidden then return false if node.visible?
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 ''
85
87
  end
86
88
 
87
- res = node_filters.all? do |name, filter|
88
- if options.has_key?(name)
89
- filter.matches?(node, options[name])
90
- elsif filter.default?
91
- filter.matches?(node, filter.default)
92
- else
93
- true
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
94
92
  end
95
93
  end
96
94
 
97
- res &&= if node.respond_to?(:session)
98
- node.session.using_wait_time(0){ @filter_block.call(node) }
99
- else
100
- @filter_block.call(node)
101
- end unless @filter_block.nil?
95
+ desc << selector.description(node_filters: show_for[:node], **options)
96
+
97
+ desc << ' that also matches the custom filter block' if @filter_block && show_for[:node]
98
+
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"
103
+ end
104
+ desc
105
+ end
106
+
107
+ def applied_description
108
+ description(true)
109
+ end
102
110
 
103
- res
111
+ def matches_filters?(node, node_filter_errors = [])
112
+ return true if (@resolved_node&.== node) && options[:allow_self]
104
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)
105
119
  rescue *(node.respond_to?(:session) ? node.session.driver.invalid_element_errors : [])
106
- return false
120
+ false
107
121
  end
108
122
 
109
123
  def visible
110
- case (vis = options.fetch(:visible){ @selector.default_visibility(session_options.ignore_hidden_elements) })
111
- when true then :visible
112
- when false then :all
113
- else vis
124
+ case (vis = options.fetch(:visible) { default_visibility })
125
+ when true then :visible
126
+ when false then :all
127
+ else vis
114
128
  end
115
129
  end
116
130
 
117
131
  def exact?
118
- return false if !supports_exact?
119
- options.fetch(:exact, session_options.exact)
132
+ supports_exact? ? options.fetch(:exact, session_options.exact) : false
120
133
  end
121
134
 
122
135
  def match
123
136
  options.fetch(:match, session_options.match)
124
137
  end
125
138
 
126
- def xpath(exact=nil)
127
- exact = self.exact? if exact.nil?
139
+ def xpath(exact = nil)
140
+ exact = exact? if exact.nil?
128
141
  expr = apply_expression_filters(@expression)
129
- expr = if expr.respond_to?(:to_xpath) and exact
130
- expr.to_xpath(:exact)
131
- else
132
- expr.to_s
133
- end
134
- filtered_xpath(expr)
142
+ expr = exact ? expr.to_xpath(:exact) : expr.to_s if expr.respond_to?(:to_xpath)
143
+ expr = filtered_expression(expr)
144
+ expr = "(#{expr})[#{xpath_text_conditions}]" if try_text_match_in_expression?
145
+ expr
135
146
  end
136
147
 
137
148
  def css
138
- filtered_css(apply_expression_filters(@expression))
149
+ filtered_expression(apply_expression_filters(@expression))
139
150
  end
140
151
 
141
152
  # @api private
142
153
  def resolve_for(node, exact = nil)
154
+ applied_filters.clear
155
+ @filter_cache.clear
156
+ @resolved_node = node
157
+ @resolved_count += 1
158
+
143
159
  node.synchronize do
144
- children = if selector.format == :css
145
- node.find_css(self.css)
146
- else
147
- node.find_xpath(self.xpath(exact))
148
- end.map do |child|
149
- if node.is_a?(Capybara::Node::Base)
150
- Capybara::Node::Element.new(node.session, child, node, self)
151
- else
152
- Capybara::Node::Simple.new(child)
153
- end
154
- end
155
- 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)
156
162
  end
157
163
  end
158
164
 
159
165
  # @api private
160
166
  def supports_exact?
161
- @expression.respond_to? :to_xpath
167
+ return @expression.respond_to? :to_xpath if @selector.supports_exact?.nil?
168
+
169
+ @selector.supports_exact?
162
170
  end
163
171
 
164
- private
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
178
+ end
179
+
180
+ private
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
+
228
+ def find_selector(locator)
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)
263
+ else
264
+ Capybara::Node::Simple.new(node)
265
+ end
266
+ end
165
267
 
166
268
  def valid_keys
167
269
  VALID_KEYS + custom_keys
168
270
  end
169
271
 
170
- def node_filters
171
- if options.has_key?(:filter_set)
172
- ::Capybara::Selector::FilterSet.all[options[:filter_set]].node_filters
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
293
+ end
294
+ end
295
+ end
296
+
297
+ def matches_filter_block?(node)
298
+ return true unless @filter_block
299
+
300
+ if node.respond_to?(:session)
301
+ node.session.using_wait_time(0) { @filter_block.call(node) }
173
302
  else
174
- @selector.node_filters
303
+ @filter_block.call(node)
175
304
  end
176
305
  end
177
306
 
307
+ def filter_set(name)
308
+ ::Capybara::Selector::FilterSet[name]
309
+ end
310
+
311
+ def node_filters
312
+ if options.key?(:filter_set)
313
+ filter_set(options[:filter_set])
314
+ else
315
+ @selector
316
+ end.node_filters
317
+ end
318
+
178
319
  def expression_filters
179
320
  filters = @selector.expression_filters
180
- filters.merge ::Capybara::Selector::FilterSet.all[options[:filter_set]].expression_filters if options.has_key?(:filter_set)
321
+ filters.merge filter_set(options[:filter_set]).expression_filters if options.key?(:filter_set)
181
322
  filters
182
323
  end
183
324
 
325
+ def ordered_results(results)
326
+ case @order
327
+ when :reverse
328
+ results.reverse
329
+ else
330
+ results
331
+ end
332
+ end
333
+
184
334
  def custom_keys
185
335
  @custom_keys ||= node_filters.keys + expression_filters.keys
186
336
  end
187
337
 
188
338
  def assert_valid_keys
189
- super
190
339
  unless VALID_MATCH.include?(match)
191
- 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(', ')}"
192
341
  end
193
- end
194
342
 
195
- def filtered_xpath(expr)
196
- if options.has_key?(:id) || options.has_key?(:class)
197
- expr = "(#{expr})"
198
- expr = "#{expr}[#{XPath.attr(:id) == options[:id]}]" if options.has_key?(:id) && !custom_keys.include?(:id)
199
- if options.has_key?(:class) && !custom_keys.include?(:class)
200
- class_xpath = Array(options[:class]).map do |klass|
201
- "contains(concat(' ',normalize-space(@class),' '),' #{klass} ')"
202
- end.join(" and ")
203
- expr = "#{expr}[#{class_xpath}]"
204
- 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 }
205
347
  end
206
- 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}"
207
354
  end
208
355
 
209
- def filtered_css(expr)
210
- if options.has_key?(:id) || options.has_key?(:class)
211
- css_selectors = expr.split(',').map(&:rstrip)
212
- expr = css_selectors.map do |sel|
213
- sel += "##{Capybara::Selector::CSS.escape(options[:id])}" if options.has_key?(:id) && !custom_keys.include?(:id)
214
- sel += Array(options[:class]).map { |k| ".#{Capybara::Selector::CSS.escape(k)}"}.join if options.has_key?(:class) && !custom_keys.include?(:class)
215
- sel
216
- end.join(", ")
217
- end
218
- 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?
219
382
  end
220
383
 
221
- def apply_expression_filters(expr)
222
- expression_filters.inject(expr) do |memo, (name, ef)|
223
- if options.has_key?(name)
224
- 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)
225
397
  elsif ef.default?
226
- ef.apply_filter(memo, ef.default)
398
+ ef.apply_filter(expr, name, ef.default, @selector)
227
399
  else
228
- memo
400
+ expr
229
401
  end
230
402
  end
231
403
  end
232
404
 
233
405
  def warn_exact_usage
234
- if options.has_key?(:exact) && !supports_exact?
235
- warn "The :exact option only has an effect on queries using the XPath#is method. Using it with the query \"#{expression.to_s}\" has no effect."
236
- end
406
+ return unless options.key?(:exact) && !supports_exact?
407
+
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."
237
409
  end
238
410
 
239
411
  def exact_text
240
412
  options.fetch(:exact_text, session_options.exact_text)
241
413
  end
414
+
415
+ def describe_within?
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?)
435
+ end
436
+
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]
496
+ else
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
501
+ end
502
+ end
503
+
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)
589
+ text_visible = visible
590
+ text_visible = :all if text_visible == :hidden
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
740
+ end
741
+
742
+ private_constant :Rectangle
242
743
  end
243
744
  end
244
745
  end