capybara 3.8.1 → 3.33.0

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