capybara 2.18.0 → 3.39.2

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