capybara 2.15.1 → 3.35.3

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