capybara 3.3.0 → 3.40.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (308) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/History.md +803 -13
  4. data/License.txt +1 -1
  5. data/README.md +257 -84
  6. data/lib/capybara/config.rb +25 -9
  7. data/lib/capybara/cucumber.rb +1 -1
  8. data/lib/capybara/driver/base.rb +17 -3
  9. data/lib/capybara/driver/node.rb +31 -6
  10. data/lib/capybara/dsl.rb +9 -7
  11. data/lib/capybara/helpers.rb +31 -7
  12. data/lib/capybara/minitest/spec.rb +180 -88
  13. data/lib/capybara/minitest.rb +262 -149
  14. data/lib/capybara/node/actions.rb +202 -116
  15. data/lib/capybara/node/base.rb +34 -19
  16. data/lib/capybara/node/document.rb +14 -2
  17. data/lib/capybara/node/document_matchers.rb +10 -12
  18. data/lib/capybara/node/element.rb +269 -115
  19. data/lib/capybara/node/finders.rb +99 -77
  20. data/lib/capybara/node/matchers.rb +327 -151
  21. data/lib/capybara/node/simple.rb +48 -13
  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 +8 -9
  25. data/lib/capybara/queries/base_query.rb +23 -16
  26. data/lib/capybara/queries/current_path_query.rb +16 -6
  27. data/lib/capybara/queries/match_query.rb +1 -0
  28. data/lib/capybara/queries/selector_query.rb +587 -130
  29. data/lib/capybara/queries/sibling_query.rb +8 -6
  30. data/lib/capybara/queries/style_query.rb +6 -2
  31. data/lib/capybara/queries/text_query.rb +28 -14
  32. data/lib/capybara/queries/title_query.rb +2 -2
  33. data/lib/capybara/rack_test/browser.rb +92 -25
  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 +68 -41
  37. data/lib/capybara/rack_test/node.rb +106 -39
  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 +75 -52
  44. data/lib/capybara/rspec/features.rb +7 -7
  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 +141 -339
  60. data/lib/capybara/rspec.rb +2 -0
  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 +27 -25
  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 +73 -25
  88. data/lib/capybara/selector/filters/base.rb +24 -5
  89. data/lib/capybara/selector/filters/expression_filter.rb +3 -3
  90. data/lib/capybara/selector/filters/locator_filter.rb +29 -0
  91. data/lib/capybara/selector/filters/node_filter.rb +16 -2
  92. data/lib/capybara/selector/regexp_disassembler.rb +211 -0
  93. data/lib/capybara/selector/selector.rb +85 -348
  94. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  95. data/lib/capybara/selector.rb +474 -447
  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 +255 -143
  101. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +93 -11
  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 +436 -134
  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 +56 -19
  123. data/lib/capybara/server/checker.rb +9 -3
  124. data/lib/capybara/server/middleware.rb +28 -12
  125. data/lib/capybara/server.rb +33 -10
  126. data/lib/capybara/session/config.rb +34 -10
  127. data/lib/capybara/session/matchers.rb +23 -16
  128. data/lib/capybara/session.rb +230 -170
  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 +121 -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 +127 -40
  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_spec.rb +21 -18
  140. data/lib/capybara/spec/session/assert_selector_spec.rb +52 -58
  141. data/lib/capybara/spec/session/assert_style_spec.rb +7 -7
  142. data/lib/capybara/spec/session/assert_text_spec.rb +74 -50
  143. data/lib/capybara/spec/session/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 +6 -6
  146. data/lib/capybara/spec/session/check_spec.rb +102 -47
  147. data/lib/capybara/spec/session/choose_spec.rb +58 -32
  148. data/lib/capybara/spec/session/click_button_spec.rb +219 -163
  149. data/lib/capybara/spec/session/click_link_or_button_spec.rb +49 -23
  150. data/lib/capybara/spec/session/click_link_spec.rb +77 -54
  151. data/lib/capybara/spec/session/current_scope_spec.rb +8 -8
  152. data/lib/capybara/spec/session/current_url_spec.rb +38 -29
  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_spec.rb +8 -8
  156. data/lib/capybara/spec/session/element/match_css_spec.rb +16 -10
  157. data/lib/capybara/spec/session/element/match_xpath_spec.rb +6 -6
  158. data/lib/capybara/spec/session/element/matches_selector_spec.rb +68 -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 +101 -46
  163. data/lib/capybara/spec/session/find_button_spec.rb +23 -23
  164. data/lib/capybara/spec/session/find_by_id_spec.rb +7 -7
  165. data/lib/capybara/spec/session/find_field_spec.rb +32 -30
  166. data/lib/capybara/spec/session/find_link_spec.rb +31 -21
  167. data/lib/capybara/spec/session/find_spec.rb +244 -141
  168. data/lib/capybara/spec/session/first_spec.rb +43 -43
  169. data/lib/capybara/spec/session/frame/frame_title_spec.rb +5 -5
  170. data/lib/capybara/spec/session/frame/frame_url_spec.rb +5 -5
  171. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +30 -18
  172. data/lib/capybara/spec/session/frame/within_frame_spec.rb +45 -18
  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 +94 -13
  179. data/lib/capybara/spec/session/has_css_spec.rb +272 -132
  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 +44 -4
  184. data/lib/capybara/spec/session/has_none_selectors_spec.rb +31 -31
  185. data/lib/capybara/spec/session/has_select_spec.rb +84 -50
  186. data/lib/capybara/spec/session/has_selector_spec.rb +111 -71
  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_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 +894 -142
  196. data/lib/capybara/spec/session/node_wrapper_spec.rb +10 -7
  197. data/lib/capybara/spec/session/refresh_spec.rb +9 -7
  198. data/lib/capybara/spec/session/reset_session_spec.rb +63 -35
  199. data/lib/capybara/spec/session/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 +2 -2
  202. data/lib/capybara/spec/session/save_page_spec.rb +37 -37
  203. data/lib/capybara/spec/session/save_screenshot_spec.rb +10 -10
  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 +85 -85
  207. data/lib/capybara/spec/session/selectors_spec.rb +49 -18
  208. data/lib/capybara/spec/session/sibling_spec.rb +9 -9
  209. data/lib/capybara/spec/session/text_spec.rb +25 -24
  210. data/lib/capybara/spec/session/title_spec.rb +7 -6
  211. data/lib/capybara/spec/session/uncheck_spec.rb +33 -21
  212. data/lib/capybara/spec/session/unselect_spec.rb +37 -37
  213. data/lib/capybara/spec/session/visit_spec.rb +68 -49
  214. data/lib/capybara/spec/session/window/become_closed_spec.rb +20 -17
  215. data/lib/capybara/spec/session/window/current_window_spec.rb +1 -1
  216. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +20 -16
  217. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +6 -2
  218. data/lib/capybara/spec/session/window/window_spec.rb +62 -63
  219. data/lib/capybara/spec/session/window/windows_spec.rb +5 -1
  220. data/lib/capybara/spec/session/window/within_window_spec.rb +14 -14
  221. data/lib/capybara/spec/session/within_spec.rb +79 -42
  222. data/lib/capybara/spec/spec_helper.rb +41 -53
  223. data/lib/capybara/spec/test_app.rb +132 -43
  224. data/lib/capybara/spec/views/animated.erb +49 -0
  225. data/lib/capybara/spec/views/form.erb +139 -42
  226. data/lib/capybara/spec/views/frame_child.erb +4 -3
  227. data/lib/capybara/spec/views/frame_one.erb +2 -1
  228. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  229. data/lib/capybara/spec/views/frame_two.erb +1 -1
  230. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  231. data/lib/capybara/spec/views/layout.erb +10 -0
  232. data/lib/capybara/spec/views/obscured.erb +47 -0
  233. data/lib/capybara/spec/views/offset.erb +33 -0
  234. data/lib/capybara/spec/views/path.erb +2 -2
  235. data/lib/capybara/spec/views/popup_one.erb +1 -1
  236. data/lib/capybara/spec/views/popup_two.erb +1 -1
  237. data/lib/capybara/spec/views/react.erb +45 -0
  238. data/lib/capybara/spec/views/scroll.erb +21 -0
  239. data/lib/capybara/spec/views/spatial.erb +31 -0
  240. data/lib/capybara/spec/views/tables.erb +67 -0
  241. data/lib/capybara/spec/views/with_animation.erb +39 -4
  242. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  243. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  244. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  245. data/lib/capybara/spec/views/with_hover.erb +3 -2
  246. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  247. data/lib/capybara/spec/views/with_html.erb +37 -9
  248. data/lib/capybara/spec/views/with_html5_svg.erb +20 -0
  249. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  250. data/lib/capybara/spec/views/with_js.erb +26 -5
  251. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  252. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  253. data/lib/capybara/spec/views/with_scope.erb +2 -2
  254. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  255. data/lib/capybara/spec/views/with_shadow.erb +31 -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 +1 -1
  261. data/lib/capybara/version.rb +1 -1
  262. data/lib/capybara/window.rb +19 -25
  263. data/lib/capybara.rb +126 -111
  264. data/spec/basic_node_spec.rb +59 -34
  265. data/spec/capybara_spec.rb +56 -44
  266. data/spec/counter_spec.rb +35 -0
  267. data/spec/css_builder_spec.rb +101 -0
  268. data/spec/css_splitter_spec.rb +8 -8
  269. data/spec/dsl_spec.rb +79 -52
  270. data/spec/filter_set_spec.rb +9 -9
  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 +45 -7
  274. data/spec/minitest_spec_spec.rb +87 -64
  275. data/spec/per_session_config_spec.rb +6 -6
  276. data/spec/rack_test_spec.rb +172 -116
  277. data/spec/regexp_dissassembler_spec.rb +250 -0
  278. data/spec/result_spec.rb +80 -72
  279. data/spec/rspec/features_spec.rb +21 -16
  280. data/spec/rspec/scenarios_spec.rb +10 -6
  281. data/spec/rspec/shared_spec_matchers.rb +407 -365
  282. data/spec/rspec/views_spec.rb +3 -3
  283. data/spec/rspec_matchers_spec.rb +35 -10
  284. data/spec/rspec_spec.rb +63 -41
  285. data/spec/sauce_spec_chrome.rb +43 -0
  286. data/spec/selector_spec.rb +334 -89
  287. data/spec/selenium_spec_chrome.rb +176 -62
  288. data/spec/selenium_spec_chrome_remote.rb +54 -14
  289. data/spec/selenium_spec_edge.rb +41 -8
  290. data/spec/selenium_spec_firefox.rb +228 -0
  291. data/spec/selenium_spec_firefox_remote.rb +94 -0
  292. data/spec/selenium_spec_ie.rb +129 -11
  293. data/spec/selenium_spec_safari.rb +162 -0
  294. data/spec/server_spec.rb +171 -97
  295. data/spec/session_spec.rb +34 -18
  296. data/spec/shared_selenium_node.rb +79 -0
  297. data/spec/shared_selenium_session.rb +344 -80
  298. data/spec/spec_helper.rb +124 -2
  299. data/spec/whitespace_normalizer_spec.rb +54 -0
  300. data/spec/xpath_builder_spec.rb +93 -0
  301. metadata +326 -28
  302. data/lib/capybara/rspec/compound.rb +0 -94
  303. data/lib/capybara/selenium/driver_specializations/marionette_driver.rb +0 -31
  304. data/lib/capybara/selenium/nodes/marionette_node.rb +0 -31
  305. data/lib/capybara/spec/session/has_style_spec.rb +0 -25
  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 -167
@@ -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 = +""
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 #{exact_text}" if 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
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,68 +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
- unapplied_options = options.keys - valid_keys
279
+ def matches_node_filters?(node, errors)
280
+ applied_filters << :node
130
281
 
131
- node_filters.all? do |filter_name, filter|
132
- if filter.matcher?
133
- unapplied_options.select { |option_name| filter.handles_option?(option_name) }.all? do |option_name|
134
- unapplied_options.delete(option_name)
135
- filter.matches?(node, option_name, options[option_name])
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
136
299
  end
137
- elsif options.key?(filter_name)
138
- unapplied_options.delete(filter_name)
139
- filter.matches?(node, filter_name, options[filter_name])
140
- elsif filter.default?
141
- filter.matches?(node, filter_name, filter.default)
142
- else
143
- true
144
300
  end
145
301
  end
146
302
  end
@@ -155,115 +311,107 @@ module Capybara
155
311
  end
156
312
  end
157
313
 
314
+ def filter_set(name)
315
+ ::Capybara::Selector::FilterSet[name]
316
+ end
317
+
158
318
  def node_filters
159
319
  if options.key?(:filter_set)
160
- ::Capybara::Selector::FilterSet.all[options[:filter_set]].node_filters
320
+ filter_set(options[:filter_set])
161
321
  else
162
- @selector.node_filters
163
- end
322
+ @selector
323
+ end.node_filters
164
324
  end
165
325
 
166
326
  def expression_filters
167
327
  filters = @selector.expression_filters
168
- 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)
169
329
  filters
170
330
  end
171
331
 
332
+ def ordered_results(results)
333
+ case @order
334
+ when :reverse
335
+ results.reverse
336
+ else
337
+ results
338
+ end
339
+ end
340
+
172
341
  def custom_keys
173
342
  @custom_keys ||= node_filters.keys + expression_filters.keys
174
343
  end
175
344
 
176
345
  def assert_valid_keys
177
346
  unless VALID_MATCH.include?(match)
178
- 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(', ')}"
179
348
  end
180
- unhandled_options = @options.keys - valid_keys
181
- unhandled_options -= @options.keys.select do |option_name|
182
- expression_filters.any? { |_nmae, ef| ef.handles_option? option_name } ||
349
+
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 } ||
183
353
  node_filters.any? { |_name, nf| nf.handles_option? option_name }
184
354
  end
185
355
 
186
356
  return if unhandled_options.empty?
187
- invalid_names = unhandled_options.map(&:inspect).join(", ")
188
- valid_names = valid_keys.map(&:inspect).join(", ")
189
- raise ArgumentError, "invalid keys #{invalid_names}, should be one of #{valid_names}"
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}"
190
361
  end
191
362
 
192
- def filtered_xpath(expr)
193
- if options.key?(:id) && !custom_keys.include?(:id)
194
- expr = if options[:id].is_a? XPath::Expression
195
- "(#{expr})[#{XPath.attr(:id)[options[:id]]}]"
196
- else
197
- "(#{expr})[#{XPath.attr(:id) == options[:id]}]"
198
- end
199
- end
200
- if options.key?(:class) && !custom_keys.include?(:class)
201
- class_xpath = if options[:class].is_a?(XPath::Expression)
202
- XPath.attr(:class)[options[:class]]
203
- else
204
- Array(options[:class]).map do |klass|
205
- if klass.start_with?('!')
206
- !XPath.attr(:class).contains_word(klass.slice(1))
207
- else
208
- XPath.attr(:class).contains_word(klass)
209
- end
210
- end.reduce(:&)
211
- end
212
- expr = "(#{expr})[#{class_xpath}]"
213
- end
214
- 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)
215
369
  end
216
370
 
217
- def filtered_css(expr)
218
- process_id = options.key?(:id) && !custom_keys.include?(:id)
219
- process_class = options.key?(:class) && !custom_keys.include?(:class)
371
+ def use_default_id_filter?
372
+ options.key?(:id) && !custom_keys.include?(:id)
373
+ end
220
374
 
221
- if process_id && options[:id].is_a?(XPath::Expression)
222
- raise ArgumentError, "XPath expressions are not supported for the :id filter with CSS based selectors"
223
- end
224
- if process_class && options[:class].is_a?(XPath::Expression)
225
- raise ArgumentError, "XPath expressions are not supported for the :class filter with CSS based selectors"
226
- end
375
+ def use_default_class_filter?
376
+ options.key?(:class) && !custom_keys.include?(:class)
377
+ end
227
378
 
228
- if process_id || process_class
229
- expr = ::Capybara::Selector::CSS.split(expr).map do |sel|
230
- sel += "##{::Capybara::Selector::CSS.escape(options[:id])}" if process_id
231
- sel += css_from_classes(Array(options[:class])) if process_class
232
- sel
233
- end.join(", ")
234
- end
379
+ def use_default_style_filter?
380
+ options.key?(:style) && !custom_keys.include?(:style)
381
+ end
235
382
 
236
- expr
383
+ def use_default_focused_filter?
384
+ options.key?(:focused) && !custom_keys.include?(:focused)
237
385
  end
238
386
 
239
- def css_from_classes(classes)
240
- classes = classes.group_by { |c| c.start_with? '!' }
241
- (classes[false].to_a.map { |c| ".#{Capybara::Selector::CSS.escape(c)}" } +
242
- classes[true].to_a.map { |c| ":not(.#{Capybara::Selector::CSS.escape(c.slice(1))})" }).join
387
+ def use_spatial_filter?
388
+ options.values_at(*SPATIAL_KEYS).compact.any?
243
389
  end
244
390
 
245
- def apply_expression_filters(expr)
391
+ def apply_expression_filters(expression)
246
392
  unapplied_options = options.keys - valid_keys
247
- expression_filters.inject(expr) do |memo, (name, ef)|
393
+ expression_filters.inject(expression) do |expr, (name, ef)|
394
+ next expr unless apply_filter?(ef)
395
+
248
396
  if ef.matcher?
249
- unapplied_options.select { |option_name| ef.handles_option?(option_name) }.each do |option_name|
397
+ unapplied_options.select(&ef.method(:handles_option?)).inject(expr) do |memo, option_name|
250
398
  unapplied_options.delete(option_name)
251
- memo = ef.apply_filter(memo, option_name, options[option_name])
399
+ ef.apply_filter(memo, option_name, options[option_name], @selector)
252
400
  end
253
- memo
254
401
  elsif options.key?(name)
255
402
  unapplied_options.delete(name)
256
- ef.apply_filter(memo, name, options[name])
403
+ ef.apply_filter(expr, name, options[name], @selector)
257
404
  elsif ef.default?
258
- ef.apply_filter(memo, name, ef.default)
405
+ ef.apply_filter(expr, name, ef.default, @selector)
259
406
  else
260
- memo
407
+ expr
261
408
  end
262
409
  end
263
410
  end
264
411
 
265
412
  def warn_exact_usage
266
413
  return unless options.key?(:exact) && !supports_exact?
414
+
267
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."
268
416
  end
269
417
 
@@ -283,22 +431,331 @@ module Capybara
283
431
  node.is_a?(::Capybara::Node::Simple) && node.path == '/'
284
432
  end
285
433
 
286
- def matches_text_filter(node, value)
287
- return matches_exact_text_filter(node, value) if exact_text == true
434
+ def apply_filter?(filter)
435
+ filter.format.nil? || (filter.format == selector_format)
436
+ end
437
+
438
+ def matches_locator_filter?(node)
439
+ return true unless @selector.locator_filter && apply_filter?(@selector.locator_filter)
440
+
441
+ @selector.locator_filter.matches?(node, @locator, @selector, exact: exact?)
442
+ end
443
+
444
+ def matches_system_filters?(node)
445
+ applied_filters << :system
446
+
447
+ matches_visibility_filters?(node) &&
448
+ matches_id_filter?(node) &&
449
+ matches_class_filter?(node) &&
450
+ matches_style_filter?(node) &&
451
+ matches_focused_filter?(node) &&
452
+ matches_text_filter?(node) &&
453
+ matches_exact_text_filter?(node)
454
+ end
455
+
456
+ def matches_spatial_filters?(node)
457
+ applied_filters << :spatial
458
+ return true unless use_spatial_filter?
459
+
460
+ node_rect = Rectangle.new(node.initial_cache[:position] || node.rect)
461
+
462
+ if options[:above]
463
+ el_rect = rect_cache(options[:above])
464
+ return false unless node_rect.above? el_rect
465
+ end
466
+
467
+ if options[:below]
468
+ el_rect = rect_cache(options[:below])
469
+ return false unless node_rect.below? el_rect
470
+ end
471
+
472
+ if options[:left_of]
473
+ el_rect = rect_cache(options[:left_of])
474
+ return false unless node_rect.left_of? el_rect
475
+ end
476
+
477
+ if options[:right_of]
478
+ el_rect = rect_cache(options[:right_of])
479
+ return false unless node_rect.right_of? el_rect
480
+ end
481
+
482
+ if options[:near]
483
+ return false if node == options[:near]
484
+
485
+ el_rect = rect_cache(options[:near])
486
+ return false unless node_rect.near? el_rect
487
+ end
488
+
489
+ true
490
+ end
491
+
492
+ def matches_id_filter?(node)
493
+ return true unless use_default_id_filter? && options[:id].is_a?(Regexp)
494
+
495
+ options[:id].match? node[:id]
496
+ end
497
+
498
+ def matches_class_filter?(node)
499
+ return true unless use_default_class_filter? && need_to_process_classes?
500
+
501
+ if options[:class].is_a? Regexp
502
+ options[:class].match? node[:class]
503
+ else
504
+ classes = (node[:class] || '').split
505
+ options[:class].select { |c| c.is_a? Regexp }.all? do |r|
506
+ classes.any? { |cls| r.match? cls }
507
+ end
508
+ end
509
+ end
510
+
511
+ def matches_focused_filter?(node)
512
+ return true unless use_default_focused_filter?
513
+
514
+ (node == node.session.active_element) == options[:focused]
515
+ end
516
+
517
+ def need_to_process_classes?
518
+ case options[:class]
519
+ when Regexp then true
520
+ when Array then options[:class].any?(Regexp)
521
+ else
522
+ false
523
+ end
524
+ end
525
+
526
+ def matches_style_filter?(node)
527
+ case options[:style]
528
+ when String, nil
529
+ true
530
+ when Regexp
531
+ options[:style].match? node[:style]
532
+ when Hash
533
+ matches_style?(node, options[:style])
534
+ end
535
+ end
536
+
537
+ def matches_style?(node, styles)
538
+ @actual_styles = node.initial_cache[:style] || node.style(*styles.keys)
539
+ styles.all? do |style, value|
540
+ if value.is_a? Regexp
541
+ value.match? @actual_styles[style.to_s]
542
+ else
543
+ @actual_styles[style.to_s] == value
544
+ end
545
+ end
546
+ end
547
+
548
+ def matches_text_filter?(node)
549
+ value = options[:text]
550
+ return true unless value
551
+ return matches_text_exactly?(node, value) if exact_text == true && !value.is_a?(Regexp)
552
+
288
553
  regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s)
289
- matches_text_regexp(node, regexp)
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
290
564
  end
291
565
 
292
- def matches_exact_text_filter(node, value)
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)
293
592
  regexp = value.is_a?(Regexp) ? value : /\A#{Regexp.escape(value.to_s)}\z/
294
- matches_text_regexp(node, regexp)
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)
295
598
  end
296
599
 
297
600
  def matches_text_regexp(node, regexp)
298
601
  text_visible = visible
299
602
  text_visible = :all if text_visible == :hidden
300
- 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))
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
301
756
  end
757
+
758
+ private_constant :Rectangle
302
759
  end
303
760
  end
304
761
  end