capybara 3.16.1 → 3.33.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (197) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/History.md +321 -0
  4. data/README.md +51 -60
  5. data/lib/capybara.rb +71 -114
  6. data/lib/capybara/config.rb +8 -5
  7. data/lib/capybara/cucumber.rb +1 -1
  8. data/lib/capybara/driver/node.rb +15 -3
  9. data/lib/capybara/dsl.rb +10 -2
  10. data/lib/capybara/helpers.rb +5 -3
  11. data/lib/capybara/minitest.rb +242 -141
  12. data/lib/capybara/minitest/spec.rb +159 -90
  13. data/lib/capybara/node/actions.rb +85 -74
  14. data/lib/capybara/node/base.rb +4 -4
  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 +216 -117
  18. data/lib/capybara/node/finders.rb +65 -65
  19. data/lib/capybara/node/matchers.rb +228 -126
  20. data/lib/capybara/node/simple.rb +9 -4
  21. data/lib/capybara/queries/ancestor_query.rb +5 -7
  22. data/lib/capybara/queries/base_query.rb +2 -1
  23. data/lib/capybara/queries/current_path_query.rb +1 -1
  24. data/lib/capybara/queries/selector_query.rb +296 -30
  25. data/lib/capybara/queries/sibling_query.rb +5 -4
  26. data/lib/capybara/queries/style_query.rb +2 -2
  27. data/lib/capybara/queries/text_query.rb +13 -1
  28. data/lib/capybara/queries/title_query.rb +1 -1
  29. data/lib/capybara/rack_test/browser.rb +7 -2
  30. data/lib/capybara/rack_test/driver.rb +1 -1
  31. data/lib/capybara/rack_test/form.rb +1 -1
  32. data/lib/capybara/rack_test/node.rb +43 -7
  33. data/lib/capybara/registration_container.rb +44 -0
  34. data/lib/capybara/registrations/drivers.rb +36 -0
  35. data/lib/capybara/registrations/patches/puma_ssl.rb +27 -0
  36. data/lib/capybara/registrations/servers.rb +44 -0
  37. data/lib/capybara/result.rb +36 -8
  38. data/lib/capybara/rspec/matcher_proxies.rb +6 -4
  39. data/lib/capybara/rspec/matchers.rb +100 -63
  40. data/lib/capybara/rspec/matchers/base.rb +23 -10
  41. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  42. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  43. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  44. data/lib/capybara/rspec/matchers/have_selector.rb +16 -8
  45. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  46. data/lib/capybara/rspec/matchers/have_text.rb +4 -4
  47. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  48. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  49. data/lib/capybara/rspec/matchers/match_style.rb +2 -2
  50. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  51. data/lib/capybara/selector.rb +219 -588
  52. data/lib/capybara/selector/builders/css_builder.rb +10 -6
  53. data/lib/capybara/selector/builders/xpath_builder.rb +1 -1
  54. data/lib/capybara/selector/css.rb +4 -2
  55. data/lib/capybara/selector/definition.rb +277 -0
  56. data/lib/capybara/selector/definition/button.rb +52 -0
  57. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  58. data/lib/capybara/selector/definition/css.rb +10 -0
  59. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  60. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  61. data/lib/capybara/selector/definition/element.rb +27 -0
  62. data/lib/capybara/selector/definition/field.rb +40 -0
  63. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  64. data/lib/capybara/selector/definition/file_field.rb +13 -0
  65. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  66. data/lib/capybara/selector/definition/frame.rb +17 -0
  67. data/lib/capybara/selector/definition/id.rb +6 -0
  68. data/lib/capybara/selector/definition/label.rb +62 -0
  69. data/lib/capybara/selector/definition/link.rb +54 -0
  70. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  71. data/lib/capybara/selector/definition/option.rb +27 -0
  72. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  73. data/lib/capybara/selector/definition/select.rb +81 -0
  74. data/lib/capybara/selector/definition/table.rb +109 -0
  75. data/lib/capybara/selector/definition/table_row.rb +21 -0
  76. data/lib/capybara/selector/definition/xpath.rb +5 -0
  77. data/lib/capybara/selector/filter_set.rb +13 -9
  78. data/lib/capybara/selector/filters/base.rb +11 -2
  79. data/lib/capybara/selector/filters/locator_filter.rb +13 -3
  80. data/lib/capybara/selector/regexp_disassembler.rb +9 -2
  81. data/lib/capybara/selector/selector.rb +43 -448
  82. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  83. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  84. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  85. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  86. data/lib/capybara/selenium/driver.rb +125 -56
  87. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +73 -17
  88. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +124 -0
  89. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +41 -2
  90. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +14 -1
  91. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +14 -5
  92. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  93. data/lib/capybara/selenium/extensions/find.rb +67 -45
  94. data/lib/capybara/selenium/extensions/html5_drag.rb +152 -36
  95. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  96. data/lib/capybara/selenium/logger_suppressor.rb +34 -0
  97. data/lib/capybara/selenium/node.rb +227 -56
  98. data/lib/capybara/selenium/nodes/chrome_node.rb +93 -8
  99. data/lib/capybara/selenium/nodes/edge_node.rb +104 -0
  100. data/lib/capybara/selenium/nodes/firefox_node.rb +37 -59
  101. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  102. data/lib/capybara/selenium/nodes/safari_node.rb +27 -54
  103. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  104. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  105. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  106. data/lib/capybara/selenium/patches/logs.rb +45 -0
  107. data/lib/capybara/server.rb +19 -3
  108. data/lib/capybara/server/animation_disabler.rb +2 -2
  109. data/lib/capybara/server/checker.rb +6 -2
  110. data/lib/capybara/server/middleware.rb +23 -13
  111. data/lib/capybara/session.rb +124 -106
  112. data/lib/capybara/session/config.rb +12 -10
  113. data/lib/capybara/session/matchers.rb +6 -6
  114. data/lib/capybara/spec/public/offset.js +6 -0
  115. data/lib/capybara/spec/public/test.js +94 -5
  116. data/lib/capybara/spec/session/all_spec.rb +84 -6
  117. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  118. data/lib/capybara/spec/session/assert_current_path_spec.rb +5 -2
  119. data/lib/capybara/spec/session/assert_text_spec.rb +9 -5
  120. data/lib/capybara/spec/session/attach_file_spec.rb +14 -6
  121. data/lib/capybara/spec/session/check_spec.rb +10 -4
  122. data/lib/capybara/spec/session/choose_spec.rb +8 -2
  123. data/lib/capybara/spec/session/click_button_spec.rb +44 -1
  124. data/lib/capybara/spec/session/click_link_spec.rb +11 -0
  125. data/lib/capybara/spec/session/evaluate_script_spec.rb +12 -0
  126. data/lib/capybara/spec/session/fill_in_spec.rb +37 -2
  127. data/lib/capybara/spec/session/find_spec.rb +60 -6
  128. data/lib/capybara/spec/session/first_spec.rb +1 -1
  129. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +14 -1
  130. data/lib/capybara/spec/session/frame/within_frame_spec.rb +12 -1
  131. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  132. data/lib/capybara/spec/session/has_button_spec.rb +16 -0
  133. data/lib/capybara/spec/session/has_css_spec.rb +35 -6
  134. data/lib/capybara/spec/session/has_current_path_spec.rb +6 -4
  135. data/lib/capybara/spec/session/has_field_spec.rb +34 -0
  136. data/lib/capybara/spec/session/has_select_spec.rb +32 -4
  137. data/lib/capybara/spec/session/has_selector_spec.rb +4 -4
  138. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  139. data/lib/capybara/spec/session/has_table_spec.rb +51 -5
  140. data/lib/capybara/spec/session/has_text_spec.rb +47 -0
  141. data/lib/capybara/spec/session/matches_style_spec.rb +2 -2
  142. data/lib/capybara/spec/session/node_spec.rb +574 -16
  143. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  144. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  145. data/lib/capybara/spec/session/scroll_spec.rb +1 -1
  146. data/lib/capybara/spec/session/select_spec.rb +5 -10
  147. data/lib/capybara/spec/session/selectors_spec.rb +24 -3
  148. data/lib/capybara/spec/session/uncheck_spec.rb +2 -2
  149. data/lib/capybara/spec/session/unselect_spec.rb +1 -1
  150. data/lib/capybara/spec/session/window/window_spec.rb +10 -9
  151. data/lib/capybara/spec/spec_helper.rb +7 -2
  152. data/lib/capybara/spec/test_app.rb +26 -21
  153. data/lib/capybara/spec/views/animated.erb +49 -0
  154. data/lib/capybara/spec/views/form.erb +25 -4
  155. data/lib/capybara/spec/views/frame_child.erb +2 -1
  156. data/lib/capybara/spec/views/frame_one.erb +1 -0
  157. data/lib/capybara/spec/views/obscured.erb +9 -9
  158. data/lib/capybara/spec/views/offset.erb +32 -0
  159. data/lib/capybara/spec/views/react.erb +45 -0
  160. data/lib/capybara/spec/views/spatial.erb +31 -0
  161. data/lib/capybara/spec/views/with_animation.erb +29 -1
  162. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  163. data/lib/capybara/spec/views/with_html.erb +28 -2
  164. data/lib/capybara/spec/views/with_js.erb +2 -1
  165. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  166. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  167. data/lib/capybara/version.rb +1 -1
  168. data/lib/capybara/window.rb +10 -10
  169. data/spec/basic_node_spec.rb +6 -6
  170. data/spec/capybara_spec.rb +28 -28
  171. data/spec/dsl_spec.rb +16 -3
  172. data/spec/filter_set_spec.rb +5 -5
  173. data/spec/fixtures/selenium_driver_rspec_failure.rb +1 -1
  174. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
  175. data/spec/minitest_spec.rb +12 -2
  176. data/spec/minitest_spec_spec.rb +56 -45
  177. data/spec/rack_test_spec.rb +25 -12
  178. data/spec/regexp_dissassembler_spec.rb +53 -39
  179. data/spec/result_spec.rb +50 -54
  180. data/spec/rspec/features_spec.rb +1 -0
  181. data/spec/rspec/shared_spec_matchers.rb +78 -62
  182. data/spec/rspec_spec.rb +5 -5
  183. data/spec/sauce_spec_chrome.rb +1 -0
  184. data/spec/selector_spec.rb +26 -16
  185. data/spec/selenium_spec_chrome.rb +84 -5
  186. data/spec/selenium_spec_chrome_remote.rb +23 -8
  187. data/spec/selenium_spec_edge.rb +23 -8
  188. data/spec/selenium_spec_firefox.rb +16 -21
  189. data/spec/selenium_spec_firefox_remote.rb +4 -13
  190. data/spec/selenium_spec_ie.rb +23 -15
  191. data/spec/selenium_spec_safari.rb +17 -17
  192. data/spec/server_spec.rb +87 -42
  193. data/spec/session_spec.rb +11 -4
  194. data/spec/shared_selenium_node.rb +83 -0
  195. data/spec/shared_selenium_session.rb +62 -72
  196. data/spec/spec_helper.rb +43 -5
  197. metadata +114 -16
@@ -102,12 +102,15 @@ module Capybara
102
102
  #
103
103
  def visible?(check_ancestors = true)
104
104
  return false if (tag_name == 'input') && (native[:type] == 'hidden')
105
+ return false if tag_name == 'template'
105
106
 
106
107
  if check_ancestors
107
108
  !find_xpath(VISIBILITY_XPATH)
108
109
  else
109
110
  # No need for an xpath if only checking the current element
110
- !(native.key?('hidden') || (native[:style] =~ /display:\s?none/) || %w[script head].include?(tag_name))
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
  ##
@@ -148,7 +152,7 @@ module Capybara
148
152
  yield # simple nodes don't need to wait
149
153
  end
150
154
 
151
- def allow_reload!
155
+ def allow_reload!(*)
152
156
  # no op
153
157
  end
154
158
 
@@ -195,7 +199,8 @@ module Capybara
195
199
  x.ancestor_or_self[
196
200
  x.attr(:style)[x.contains('display:none') | x.contains('display: none')] |
197
201
  x.attr(:hidden) |
198
- x.qname.one_of('script', 'head')
202
+ x.qname.one_of('script', 'head') |
203
+ (~x.self(:summary) & XPath.parent(:details)[!XPath.attr(:open)])
199
204
  ].boolean
200
205
  end.to_s.freeze
201
206
  end
@@ -6,9 +6,13 @@ module Capybara
6
6
  # @api private
7
7
  def resolve_for(node, exact = nil)
8
8
  @child_node = node
9
+
9
10
  node.synchronize do
10
11
  match_results = super(node.session.current_scope, exact)
11
- node.all(:xpath, XPath.ancestor) { |el| match_results.include?(el) }
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)
12
16
  end
13
17
  end
14
18
 
@@ -18,12 +22,6 @@ module Capybara
18
22
  desc += " that is an ancestor of #{child_query.description}" if child_query
19
23
  desc
20
24
  end
21
-
22
- private
23
-
24
- def valid_keys
25
- super - COUNT_KEYS
26
- end
27
25
  end
28
26
  end
29
27
  end
@@ -79,7 +79,8 @@ module Capybara
79
79
  if count
80
80
  message << " #{occurrences count}"
81
81
  elsif between
82
- message << " between #{between.first} and #{between.end ? between.last : 'infinite'} times"
82
+ message << " between #{between.begin ? between.first : 1} and" \
83
+ " #{between.end ? between.last : 'infinite'} times"
83
84
  elsif maximum
84
85
  message << " at most #{occurrences maximum}"
85
86
  elsif minimum
@@ -22,7 +22,7 @@ module Capybara
22
22
  @actual_path = options[:url] ? uri&.to_s : uri&.request_uri
23
23
 
24
24
  if @expected_path.is_a? Regexp
25
- @actual_path.to_s.match(@expected_path)
25
+ @actual_path.to_s.match?(@expected_path)
26
26
  else
27
27
  ::Addressable::URI.parse(@expected_path) == ::Addressable::URI.parse(@actual_path)
28
28
  end
@@ -1,32 +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 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]
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
+
19
30
  super(@options)
20
31
  self.session_options = session_options
21
32
 
22
- @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
+
23
43
  @locator = args.shift
24
44
  @filter_block = filter_block
25
45
 
26
46
  raise ArgumentError, "Unused parameters passed to #{self.class.name} : #{args}" unless args.empty?
27
47
 
28
- selector_config = { enable_aria_label: enable_aria_label, test_id: test_id }
29
- @expression = selector.call(@locator, @options.merge(selector_config: selector_config))
48
+ @expression = selector.call(@locator, **@options)
30
49
 
31
50
  warn_exact_usage
32
51
 
@@ -44,13 +63,17 @@ module Capybara
44
63
  desc << 'visible ' if visible == :visible
45
64
  desc << 'non-visible ' if visible == :hidden
46
65
  end
66
+
47
67
  desc << "#{label} #{locator.inspect}"
68
+
48
69
  if show_for[:any]
49
70
  desc << " with#{' exact' if exact_text == true} text #{options[:text].inspect}" if options[:text]
50
71
  desc << " with exact text #{exact_text}" if exact_text.is_a?(String)
51
72
  end
73
+
52
74
  desc << " with id #{options[:id]}" if options[:id]
53
75
  desc << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
76
+
54
77
  desc << case options[:style]
55
78
  when String
56
79
  " with style attribute #{options[:style].inspect}"
@@ -60,8 +83,17 @@ module Capybara
60
83
  " with styles #{options[:style].inspect}"
61
84
  else ''
62
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
+
63
93
  desc << selector.description(node_filters: show_for[:node], **options)
94
+
64
95
  desc << ' that also matches the custom filter block' if @filter_block && show_for[:node]
96
+
65
97
  desc << " within #{@resolved_node.inspect}" if describe_within?
66
98
  if locator.is_a?(String) && locator.start_with?('#', './/', '//')
67
99
  unless selector.raw_locator?
@@ -81,6 +113,7 @@ module Capybara
81
113
 
82
114
  matches_locator_filter?(node) &&
83
115
  matches_system_filters?(node) &&
116
+ matches_spatial_filters?(node) &&
84
117
  matches_node_filters?(node, node_filter_errors) &&
85
118
  matches_filter_block?(node)
86
119
  rescue *(node.respond_to?(:session) ? node.session.driver.invalid_element_errors : [])
@@ -119,17 +152,21 @@ module Capybara
119
152
  # @api private
120
153
  def resolve_for(node, exact = nil)
121
154
  applied_filters.clear
155
+ @filter_cache.clear
122
156
  @resolved_node = node
123
157
  @resolved_count += 1
158
+
124
159
  node.synchronize do
125
160
  children = find_nodes_by_selector_format(node, exact).map(&method(:to_element))
126
- Capybara::Result.new(children, self)
161
+ Capybara::Result.new(ordered_results(children), self)
127
162
  end
128
163
  end
129
164
 
130
165
  # @api private
131
166
  def supports_exact?
132
- @expression.respond_to? :to_xpath
167
+ return @expression.respond_to? :to_xpath if @selector.supports_exact?.nil?
168
+
169
+ @selector.supports_exact?
133
170
  end
134
171
 
135
172
  def failure_message
@@ -142,20 +179,36 @@ module Capybara
142
179
 
143
180
  private
144
181
 
182
+ def selector_format
183
+ @selector.format
184
+ end
185
+
186
+ def matching_text
187
+ options[:text] || options[:exact_text]
188
+ end
189
+
145
190
  def text_fragments
146
- text = (options[:text] || options[:exact_text])
147
- text.is_a?(String) ? text.split : []
191
+ (text = matching_text).is_a?(String) ? text.split : []
148
192
  end
149
193
 
150
194
  def xpath_text_conditions
151
- (options[:text] || options[:exact_text]).split.map { |txt| XPath.contains(txt) }.reduce(&:&)
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
152
205
  end
153
206
 
154
207
  def try_text_match_in_expression?
155
208
  first_try? &&
156
- (options[:text] || options[:exact_text]).is_a?(String) &&
157
- @resolved_node&.respond_to?(:session) &&
158
- @resolved_node.session.driver.wait?
209
+ matching_text &&
210
+ @resolved_node.is_a?(Capybara::Node::Base) &&
211
+ @resolved_node.session&.driver&.wait?
159
212
  end
160
213
 
161
214
  def first_try?
@@ -182,23 +235,24 @@ module Capybara
182
235
  def find_nodes_by_selector_format(node, exact)
183
236
  hints = {}
184
237
  hints[:uses_visibility] = true unless visible == :all
185
- hints[:texts] = text_fragments unless selector.format == :xpath
238
+ hints[:texts] = text_fragments unless selector_format == :xpath
186
239
  hints[:styles] = options[:style] if use_default_style_filter?
240
+ hints[:position] = true if use_spatial_filter?
187
241
 
188
- if selector.format == :css
242
+ if selector_format == :css
189
243
  if node.method(:find_css).arity != 1
190
244
  node.find_css(css, **hints)
191
245
  else
192
246
  node.find_css(css)
193
247
  end
194
- elsif selector.format == :xpath
248
+ elsif selector_format == :xpath
195
249
  if node.method(:find_xpath).arity != 1
196
250
  node.find_xpath(xpath(exact), **hints)
197
251
  else
198
252
  node.find_xpath(xpath(exact))
199
253
  end
200
254
  else
201
- raise ArgumentError, "Unknown format: #{selector.format}"
255
+ raise ArgumentError, "Unknown format: #{selector_format}"
202
256
  end
203
257
  end
204
258
 
@@ -220,6 +274,8 @@ module Capybara
220
274
  unapplied_options = options.keys - valid_keys
221
275
  @selector.with_filter_errors(errors) do
222
276
  node_filters.all? do |filter_name, filter|
277
+ next true unless apply_filter?(filter)
278
+
223
279
  if filter.matcher?
224
280
  unapplied_options.select { |option_name| filter.handles_option?(option_name) }.all? do |option_name|
225
281
  unapplied_options.delete(option_name)
@@ -265,6 +321,15 @@ module Capybara
265
321
  filters
266
322
  end
267
323
 
324
+ def ordered_results(results)
325
+ case @order
326
+ when :reverse
327
+ results.reverse
328
+ else
329
+ results
330
+ end
331
+ end
332
+
268
333
  def custom_keys
269
334
  @custom_keys ||= node_filters.keys + expression_filters.keys
270
335
  end
@@ -292,7 +357,7 @@ module Capybara
292
357
  conditions[:id] = options[:id] if use_default_id_filter?
293
358
  conditions[:class] = options[:class] if use_default_class_filter?
294
359
  conditions[:style] = options[:style] if use_default_style_filter? && !options[:style].is_a?(Hash)
295
- builder(expr).add_attribute_conditions(conditions)
360
+ builder(expr).add_attribute_conditions(**conditions)
296
361
  end
297
362
 
298
363
  def use_default_id_filter?
@@ -307,9 +372,15 @@ module Capybara
307
372
  options.key?(:style) && !custom_keys.include?(:style)
308
373
  end
309
374
 
375
+ def use_spatial_filter?
376
+ options.values_at(*SPATIAL_KEYS).compact.any?
377
+ end
378
+
310
379
  def apply_expression_filters(expression)
311
380
  unapplied_options = options.keys - valid_keys
312
381
  expression_filters.inject(expression) do |expr, (name, ef)|
382
+ next expr unless apply_filter?(ef)
383
+
313
384
  if ef.matcher?
314
385
  unapplied_options.select(&ef.method(:handles_option?)).inject(expr) do |memo, option_name|
315
386
  unapplied_options.delete(option_name)
@@ -348,16 +419,20 @@ module Capybara
348
419
  node.is_a?(::Capybara::Node::Simple) && node.path == '/'
349
420
  end
350
421
 
422
+ def apply_filter?(filter)
423
+ filter.format.nil? || (filter.format == selector_format)
424
+ end
425
+
351
426
  def matches_locator_filter?(node)
352
- return true unless @selector.locator_filter
427
+ return true unless @selector.locator_filter && apply_filter?(@selector.locator_filter)
353
428
 
354
- @selector.locator_filter.matches?(node, @locator, @selector)
429
+ @selector.locator_filter.matches?(node, @locator, @selector, exact: exact?)
355
430
  end
356
431
 
357
432
  def matches_system_filters?(node)
358
433
  applied_filters << :system
359
434
 
360
- matches_visible_filter?(node) &&
435
+ matches_visibility_filters?(node) &&
361
436
  matches_id_filter?(node) &&
362
437
  matches_class_filter?(node) &&
363
438
  matches_style_filter?(node) &&
@@ -365,16 +440,52 @@ module Capybara
365
440
  matches_exact_text_filter?(node)
366
441
  end
367
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
+
368
479
  def matches_id_filter?(node)
369
480
  return true unless use_default_id_filter? && options[:id].is_a?(Regexp)
370
481
 
371
- node[:id] =~ options[:id]
482
+ options[:id].match? node[:id]
372
483
  end
373
484
 
374
485
  def matches_class_filter?(node)
375
486
  return true unless use_default_class_filter? && options[:class].is_a?(Regexp)
376
487
 
377
- node[:class] =~ options[:class]
488
+ options[:class].match? node[:class]
378
489
  end
379
490
 
380
491
  def matches_style_filter?(node)
@@ -382,7 +493,7 @@ module Capybara
382
493
  when String, nil
383
494
  true
384
495
  when Regexp
385
- node[:style] =~ options[:style]
496
+ options[:style].match? node[:style]
386
497
  when Hash
387
498
  matches_style?(node, options[:style])
388
499
  end
@@ -392,7 +503,7 @@ module Capybara
392
503
  @actual_styles = node.initial_cache[:style] || node.style(*styles.keys)
393
504
  styles.all? do |style, value|
394
505
  if value.is_a? Regexp
395
- @actual_styles[style.to_s] =~ value
506
+ value.match? @actual_styles[style.to_s]
396
507
  else
397
508
  @actual_styles[style.to_s] == value
398
509
  end
@@ -414,14 +525,27 @@ module Capybara
414
525
  matches_text_exactly?(node, exact_text)
415
526
  end
416
527
 
417
- def matches_visible_filter?(node)
418
- case visible
419
- when :visible then
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
420
534
  node.initial_cache[:visible] || (node.initial_cache[:visible].nil? && node.visible?)
421
- when :hidden then
535
+ when :hidden
422
536
  (node.initial_cache[:visible] == false) || (node.initial_cache[:visbile].nil? && !node.visible?)
423
- else true
537
+ else
538
+ true
424
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
425
549
  end
426
550
 
427
551
  def matches_text_exactly?(node, value)
@@ -436,7 +560,7 @@ module Capybara
436
560
  def matches_text_regexp?(node, regexp)
437
561
  text_visible = visible
438
562
  text_visible = :all if text_visible == :hidden
439
- !!node.text(text_visible, normalize_ws: normalize_ws).match(regexp)
563
+ node.text(text_visible, normalize_ws: normalize_ws).match?(regexp)
440
564
  end
441
565
 
442
566
  def default_visibility
@@ -446,6 +570,148 @@ module Capybara
446
570
  def builder(expr)
447
571
  selector.builder(expr)
448
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
449
715
  end
450
716
  end
451
717
  end