capybara 3.13.2 → 3.40.0

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