capybara 3.3.0 → 3.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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