capybara 3.23.0 → 3.35.3

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 (183) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +264 -11
  3. data/README.md +10 -6
  4. data/lib/capybara.rb +20 -8
  5. data/lib/capybara/config.rb +10 -8
  6. data/lib/capybara/cucumber.rb +1 -1
  7. data/lib/capybara/driver/base.rb +4 -0
  8. data/lib/capybara/driver/node.rb +4 -0
  9. data/lib/capybara/dsl.rb +10 -2
  10. data/lib/capybara/helpers.rb +28 -2
  11. data/lib/capybara/minitest.rb +232 -144
  12. data/lib/capybara/minitest/spec.rb +156 -97
  13. data/lib/capybara/node/actions.rb +36 -36
  14. data/lib/capybara/node/base.rb +6 -6
  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 +77 -33
  18. data/lib/capybara/node/finders.rb +24 -17
  19. data/lib/capybara/node/matchers.rb +79 -64
  20. data/lib/capybara/node/simple.rb +11 -4
  21. data/lib/capybara/queries/ancestor_query.rb +6 -10
  22. data/lib/capybara/queries/base_query.rb +2 -1
  23. data/lib/capybara/queries/current_path_query.rb +14 -4
  24. data/lib/capybara/queries/selector_query.rb +259 -23
  25. data/lib/capybara/queries/sibling_query.rb +5 -11
  26. data/lib/capybara/queries/style_query.rb +1 -1
  27. data/lib/capybara/queries/text_query.rb +13 -1
  28. data/lib/capybara/rack_test/browser.rb +13 -4
  29. data/lib/capybara/rack_test/driver.rb +2 -1
  30. data/lib/capybara/rack_test/form.rb +2 -2
  31. data/lib/capybara/rack_test/node.rb +42 -6
  32. data/lib/capybara/registration_container.rb +44 -0
  33. data/lib/capybara/registrations/drivers.rb +18 -12
  34. data/lib/capybara/registrations/patches/puma_ssl.rb +29 -0
  35. data/lib/capybara/registrations/servers.rb +9 -2
  36. data/lib/capybara/result.rb +39 -19
  37. data/lib/capybara/rspec.rb +2 -0
  38. data/lib/capybara/rspec/matcher_proxies.rb +5 -5
  39. data/lib/capybara/rspec/matchers.rb +97 -74
  40. data/lib/capybara/rspec/matchers/base.rb +19 -6
  41. data/lib/capybara/rspec/matchers/count_sugar.rb +2 -1
  42. data/lib/capybara/rspec/matchers/have_ancestor.rb +5 -7
  43. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  44. data/lib/capybara/rspec/matchers/have_selector.rb +15 -10
  45. data/lib/capybara/rspec/matchers/have_sibling.rb +4 -7
  46. data/lib/capybara/rspec/matchers/have_text.rb +4 -7
  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 +7 -2
  50. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  51. data/lib/capybara/selector.rb +46 -19
  52. data/lib/capybara/selector/builders/css_builder.rb +10 -6
  53. data/lib/capybara/selector/builders/xpath_builder.rb +4 -2
  54. data/lib/capybara/selector/css.rb +1 -1
  55. data/lib/capybara/selector/definition.rb +13 -11
  56. data/lib/capybara/selector/definition/button.rb +32 -15
  57. data/lib/capybara/selector/definition/checkbox.rb +2 -2
  58. data/lib/capybara/selector/definition/css.rb +3 -1
  59. data/lib/capybara/selector/definition/datalist_input.rb +2 -2
  60. data/lib/capybara/selector/definition/datalist_option.rb +1 -1
  61. data/lib/capybara/selector/definition/element.rb +3 -2
  62. data/lib/capybara/selector/definition/field.rb +1 -1
  63. data/lib/capybara/selector/definition/file_field.rb +1 -1
  64. data/lib/capybara/selector/definition/fillable_field.rb +2 -2
  65. data/lib/capybara/selector/definition/label.rb +5 -3
  66. data/lib/capybara/selector/definition/link.rb +8 -0
  67. data/lib/capybara/selector/definition/option.rb +1 -1
  68. data/lib/capybara/selector/definition/radio_button.rb +2 -2
  69. data/lib/capybara/selector/definition/select.rb +33 -14
  70. data/lib/capybara/selector/definition/table.rb +6 -3
  71. data/lib/capybara/selector/definition/table_row.rb +2 -2
  72. data/lib/capybara/selector/filter_set.rb +13 -11
  73. data/lib/capybara/selector/filters/base.rb +6 -1
  74. data/lib/capybara/selector/filters/locator_filter.rb +1 -1
  75. data/lib/capybara/selector/regexp_disassembler.rb +7 -0
  76. data/lib/capybara/selector/selector.rb +13 -3
  77. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -1
  78. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -1
  79. data/lib/capybara/selenium/atoms/src/getAttribute.js +1 -1
  80. data/lib/capybara/selenium/atoms/src/isDisplayed.js +10 -10
  81. data/lib/capybara/selenium/driver.rb +86 -24
  82. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +24 -21
  83. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +21 -19
  84. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +17 -1
  85. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +0 -4
  86. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  87. data/lib/capybara/selenium/extensions/find.rb +37 -26
  88. data/lib/capybara/selenium/extensions/html5_drag.rb +55 -11
  89. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  90. data/lib/capybara/selenium/extensions/scroll.rb +8 -10
  91. data/lib/capybara/selenium/logger_suppressor.rb +8 -2
  92. data/lib/capybara/selenium/node.rb +160 -40
  93. data/lib/capybara/selenium/nodes/chrome_node.rb +72 -12
  94. data/lib/capybara/selenium/nodes/edge_node.rb +32 -14
  95. data/lib/capybara/selenium/nodes/firefox_node.rb +28 -32
  96. data/lib/capybara/selenium/nodes/safari_node.rb +5 -29
  97. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  98. data/lib/capybara/selenium/patches/atoms.rb +4 -4
  99. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  100. data/lib/capybara/selenium/patches/logs.rb +32 -7
  101. data/lib/capybara/server.rb +19 -3
  102. data/lib/capybara/server/animation_disabler.rb +8 -3
  103. data/lib/capybara/server/checker.rb +1 -1
  104. data/lib/capybara/server/middleware.rb +22 -10
  105. data/lib/capybara/session.rb +66 -40
  106. data/lib/capybara/session/config.rb +11 -3
  107. data/lib/capybara/session/matchers.rb +11 -11
  108. data/lib/capybara/spec/public/offset.js +6 -0
  109. data/lib/capybara/spec/public/test.js +75 -7
  110. data/lib/capybara/spec/session/accept_alert_spec.rb +1 -1
  111. data/lib/capybara/spec/session/all_spec.rb +60 -5
  112. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  113. data/lib/capybara/spec/session/assert_text_spec.rb +9 -5
  114. data/lib/capybara/spec/session/check_spec.rb +6 -0
  115. data/lib/capybara/spec/session/click_button_spec.rb +16 -0
  116. data/lib/capybara/spec/session/click_link_or_button_spec.rb +9 -0
  117. data/lib/capybara/spec/session/current_url_spec.rb +11 -1
  118. data/lib/capybara/spec/session/fill_in_spec.rb +29 -0
  119. data/lib/capybara/spec/session/find_spec.rb +55 -0
  120. data/lib/capybara/spec/session/has_ancestor_spec.rb +2 -0
  121. data/lib/capybara/spec/session/has_button_spec.rb +51 -0
  122. data/lib/capybara/spec/session/has_css_spec.rb +26 -4
  123. data/lib/capybara/spec/session/has_current_path_spec.rb +15 -2
  124. data/lib/capybara/spec/session/has_field_spec.rb +34 -0
  125. data/lib/capybara/spec/session/has_select_spec.rb +32 -4
  126. data/lib/capybara/spec/session/has_selector_spec.rb +4 -4
  127. data/lib/capybara/spec/session/has_table_spec.rb +51 -5
  128. data/lib/capybara/spec/session/has_text_spec.rb +30 -0
  129. data/lib/capybara/spec/session/html_spec.rb +1 -1
  130. data/lib/capybara/spec/session/matches_style_spec.rb +2 -2
  131. data/lib/capybara/spec/session/node_spec.rb +394 -9
  132. data/lib/capybara/spec/session/refresh_spec.rb +2 -1
  133. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  134. data/lib/capybara/spec/session/save_page_spec.rb +4 -4
  135. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -15
  136. data/lib/capybara/spec/session/selectors_spec.rb +16 -3
  137. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +1 -1
  138. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -1
  139. data/lib/capybara/spec/session/window/window_spec.rb +8 -8
  140. data/lib/capybara/spec/session/window/windows_spec.rb +1 -1
  141. data/lib/capybara/spec/spec_helper.rb +14 -14
  142. data/lib/capybara/spec/test_app.rb +27 -21
  143. data/lib/capybara/spec/views/form.erb +47 -4
  144. data/lib/capybara/spec/views/offset.erb +32 -0
  145. data/lib/capybara/spec/views/spatial.erb +31 -0
  146. data/lib/capybara/spec/views/with_animation.erb +37 -1
  147. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  148. data/lib/capybara/spec/views/with_html.erb +24 -2
  149. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  150. data/lib/capybara/spec/views/with_js.erb +4 -1
  151. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  152. data/lib/capybara/spec/views/with_sortable_js.erb +1 -1
  153. data/lib/capybara/version.rb +1 -1
  154. data/lib/capybara/window.rb +3 -7
  155. data/spec/basic_node_spec.rb +15 -14
  156. data/spec/capybara_spec.rb +28 -28
  157. data/spec/dsl_spec.rb +16 -3
  158. data/spec/filter_set_spec.rb +5 -5
  159. data/spec/fixtures/selenium_driver_rspec_failure.rb +1 -1
  160. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
  161. data/spec/minitest_spec.rb +3 -2
  162. data/spec/minitest_spec_spec.rb +46 -46
  163. data/spec/rack_test_spec.rb +38 -15
  164. data/spec/regexp_dissassembler_spec.rb +52 -38
  165. data/spec/result_spec.rb +43 -32
  166. data/spec/rspec/features_spec.rb +4 -1
  167. data/spec/rspec/scenarios_spec.rb +4 -0
  168. data/spec/rspec/shared_spec_matchers.rb +68 -56
  169. data/spec/rspec_spec.rb +9 -5
  170. data/spec/selector_spec.rb +32 -17
  171. data/spec/selenium_spec_chrome.rb +78 -11
  172. data/spec/selenium_spec_chrome_remote.rb +23 -6
  173. data/spec/selenium_spec_edge.rb +15 -12
  174. data/spec/selenium_spec_firefox.rb +24 -19
  175. data/spec/selenium_spec_firefox_remote.rb +0 -8
  176. data/spec/selenium_spec_ie.rb +1 -6
  177. data/spec/server_spec.rb +106 -44
  178. data/spec/session_spec.rb +5 -5
  179. data/spec/shared_selenium_node.rb +56 -2
  180. data/spec/shared_selenium_session.rb +122 -15
  181. data/spec/spec_helper.rb +2 -2
  182. metadata +63 -17
  183. data/lib/capybara/spec/session/source_spec.rb +0 -0
@@ -100,7 +100,7 @@ module Capybara
100
100
  # @param [Boolean] check_ancestors Whether to inherit visibility from ancestors
101
101
  # @return [Boolean] Whether the element is visible
102
102
  #
103
- def visible?(check_ancestors = true)
103
+ def visible?(check_ancestors = true) # rubocop:disable Style/OptionalBooleanParameter
104
104
  return false if (tag_name == 'input') && (native[:type] == 'hidden')
105
105
  return false if tag_name == 'template'
106
106
 
@@ -108,7 +108,9 @@ module Capybara
108
108
  !find_xpath(VISIBILITY_XPATH)
109
109
  else
110
110
  # No need for an xpath if only checking the current element
111
- !(native.key?('hidden') || (/display:\s?none/.match? native[:style]) || %w[script head].include?(tag_name))
111
+ !(native.key?('hidden') ||
112
+ /display:\s?none/.match?(native[:style] || '') ||
113
+ %w[script head].include?(tag_name))
112
114
  end
113
115
  end
114
116
 
@@ -146,11 +148,15 @@ module Capybara
146
148
  native.has_attribute?('multiple')
147
149
  end
148
150
 
151
+ def readonly?
152
+ native.has_attribute?('readonly')
153
+ end
154
+
149
155
  def synchronize(_seconds = nil)
150
156
  yield # simple nodes don't need to wait
151
157
  end
152
158
 
153
- def allow_reload!
159
+ def allow_reload!(*)
154
160
  # no op
155
161
  end
156
162
 
@@ -197,7 +203,8 @@ module Capybara
197
203
  x.ancestor_or_self[
198
204
  x.attr(:style)[x.contains('display:none') | x.contains('display: none')] |
199
205
  x.attr(:hidden) |
200
- x.qname.one_of('script', 'head')
206
+ x.qname.one_of('script', 'head') |
207
+ (~x.self(:summary) & XPath.parent(:details)[!XPath.attr(:open)])
201
208
  ].boolean
202
209
  end.to_s.freeze
203
210
  end
@@ -3,24 +3,20 @@
3
3
  module Capybara
4
4
  module Queries
5
5
  class AncestorQuery < Capybara::Queries::SelectorQuery
6
- def initialize(*args)
7
- super
8
- @count_options = {}
9
- COUNT_KEYS.each do |key|
10
- @count_options[key] = @options.delete(key) if @options.key?(key)
11
- end
12
- end
13
-
14
6
  # @api private
15
7
  def resolve_for(node, exact = nil)
16
8
  @child_node = node
9
+
17
10
  node.synchronize do
18
11
  match_results = super(node.session.current_scope, exact)
19
- node.all(:xpath, XPath.ancestor, **@count_options) { |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)
20
16
  end
21
17
  end
22
18
 
23
- def description(applied = false)
19
+ def description(applied = false) # rubocop:disable Style/OptionalBooleanParameter
24
20
  child_query = @child_node&.instance_variable_get(:@query)
25
21
  desc = super
26
22
  desc += " that is an ancestor of #{child_query.description}" if child_query
@@ -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
@@ -6,26 +6,30 @@ module Capybara
6
6
  # @api private
7
7
  module Queries
8
8
  class CurrentPathQuery < BaseQuery
9
- def initialize(expected_path, **options)
9
+ def initialize(expected_path, **options, &optional_filter_block)
10
10
  super(options)
11
11
  @expected_path = expected_path
12
12
  @options = {
13
13
  url: !@expected_path.is_a?(Regexp) && !::Addressable::URI.parse(@expected_path || '').hostname.nil?,
14
14
  ignore_query: false
15
15
  }.merge(options)
16
+ @filter_block = optional_filter_block
16
17
  assert_valid_keys
17
18
  end
18
19
 
19
20
  def resolves_for?(session)
20
21
  uri = ::Addressable::URI.parse(session.current_url)
21
- uri&.query = nil if options[:ignore_query]
22
- @actual_path = options[:url] ? uri&.to_s : uri&.request_uri
22
+ @actual_path = (options[:ignore_query] ? uri&.omit(:query) : uri).yield_self do |u|
23
+ options[:url] ? u&.to_s : u&.request_uri
24
+ end
23
25
 
24
- if @expected_path.is_a? Regexp
26
+ res = if @expected_path.is_a? Regexp
25
27
  @actual_path.to_s.match?(@expected_path)
26
28
  else
27
29
  ::Addressable::URI.parse(@expected_path) == ::Addressable::URI.parse(@actual_path)
28
30
  end
31
+
32
+ res && matches_filter_block?(uri)
29
33
  end
30
34
 
31
35
  def failure_message
@@ -38,6 +42,12 @@ module Capybara
38
42
 
39
43
  private
40
44
 
45
+ def matches_filter_block?(url)
46
+ return true unless @filter_block
47
+
48
+ @filter_block.call(url)
49
+ end
50
+
41
51
  def failure_message_helper(negated = '')
42
52
  verb = @expected_path.is_a?(Regexp) ? 'match' : 'equal'
43
53
  "expected #{@actual_path.inspect}#{negated} to #{verb} #{@expected_path.inspect}"
@@ -1,29 +1,42 @@
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 +
9
+
10
+ SPATIAL_KEYS = %i[above below left_of right_of near].freeze
11
+ VALID_KEYS = SPATIAL_KEYS + COUNT_KEYS +
8
12
  %i[text id class style visible obscured exact exact_text normalize_ws match wait filter_set]
9
13
  VALID_MATCH = %i[first smart prefer_exact one].freeze
10
14
 
11
15
  def initialize(*args,
12
16
  session_options:,
13
17
  enable_aria_label: session_options.enable_aria_label,
18
+ enable_aria_role: session_options.enable_aria_role,
14
19
  test_id: session_options.test_id,
15
20
  selector_format: nil,
21
+ order: nil,
16
22
  **options,
17
23
  &filter_block)
18
24
  @resolved_node = nil
19
25
  @resolved_count = 0
20
26
  @options = options.dup
27
+ @order = order
28
+ @filter_cache = Hash.new { |hsh, key| hsh[key] = {} }
29
+
21
30
  super(@options)
22
31
  self.session_options = session_options
23
32
 
24
33
  @selector = Selector.new(
25
34
  find_selector(args[0].is_a?(Symbol) ? args.shift : args[0]),
26
- config: { enable_aria_label: enable_aria_label, test_id: test_id },
35
+ config: {
36
+ enable_aria_label: enable_aria_label,
37
+ enable_aria_role: enable_aria_role,
38
+ test_id: test_id
39
+ },
27
40
  format: selector_format
28
41
  )
29
42
 
@@ -32,7 +45,7 @@ module Capybara
32
45
 
33
46
  raise ArgumentError, "Unused parameters passed to #{self.class.name} : #{args}" unless args.empty?
34
47
 
35
- @expression = selector.call(@locator, @options)
48
+ @expression = selector.call(@locator, **@options)
36
49
 
37
50
  warn_exact_usage
38
51
 
@@ -42,7 +55,7 @@ module Capybara
42
55
  def name; selector.name; end
43
56
  def label; selector.label || selector.name; end
44
57
 
45
- def description(only_applied = false)
58
+ def description(only_applied = false) # rubocop:disable Style/OptionalBooleanParameter
46
59
  desc = +''
47
60
  show_for = show_for_stage(only_applied)
48
61
 
@@ -50,13 +63,17 @@ module Capybara
50
63
  desc << 'visible ' if visible == :visible
51
64
  desc << 'non-visible ' if visible == :hidden
52
65
  end
66
+
53
67
  desc << "#{label} #{locator.inspect}"
68
+
54
69
  if show_for[:any]
55
70
  desc << " with#{' exact' if exact_text == true} text #{options[:text].inspect}" if options[:text]
56
71
  desc << " with exact text #{exact_text}" if exact_text.is_a?(String)
57
72
  end
73
+
58
74
  desc << " with id #{options[:id]}" if options[:id]
59
75
  desc << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
76
+
60
77
  desc << case options[:style]
61
78
  when String
62
79
  " with style attribute #{options[:style].inspect}"
@@ -66,14 +83,21 @@ module Capybara
66
83
  " with styles #{options[:style].inspect}"
67
84
  else ''
68
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
+
69
93
  desc << selector.description(node_filters: show_for[:node], **options)
94
+
70
95
  desc << ' that also matches the custom filter block' if @filter_block && show_for[:node]
96
+
71
97
  desc << " within #{@resolved_node.inspect}" if describe_within?
72
- if locator.is_a?(String) && locator.start_with?('#', './/', '//')
73
- unless selector.raw_locator?
74
- desc << "\nNote: It appears you may be passing a CSS selector or XPath expression rather than a locator. " \
75
- "Please see the documentation for acceptable locator values.\n\n"
76
- end
98
+ if locator.is_a?(String) && locator.start_with?('#', './/', '//') && !selector.raw_locator?
99
+ desc << "\nNote: It appears you may be passing a CSS selector or XPath expression rather than a locator. " \
100
+ "Please see the documentation for acceptable locator values.\n\n"
77
101
  end
78
102
  desc
79
103
  end
@@ -87,6 +111,7 @@ module Capybara
87
111
 
88
112
  matches_locator_filter?(node) &&
89
113
  matches_system_filters?(node) &&
114
+ matches_spatial_filters?(node) &&
90
115
  matches_node_filters?(node, node_filter_errors) &&
91
116
  matches_filter_block?(node)
92
117
  rescue *(node.respond_to?(:session) ? node.session.driver.invalid_element_errors : [])
@@ -125,11 +150,13 @@ module Capybara
125
150
  # @api private
126
151
  def resolve_for(node, exact = nil)
127
152
  applied_filters.clear
153
+ @filter_cache.clear
128
154
  @resolved_node = node
129
155
  @resolved_count += 1
156
+
130
157
  node.synchronize do
131
158
  children = find_nodes_by_selector_format(node, exact).map(&method(:to_element))
132
- Capybara::Result.new(children, self)
159
+ Capybara::Result.new(ordered_results(children), self)
133
160
  end
134
161
  end
135
162
 
@@ -208,18 +235,20 @@ module Capybara
208
235
  hints[:uses_visibility] = true unless visible == :all
209
236
  hints[:texts] = text_fragments unless selector_format == :xpath
210
237
  hints[:styles] = options[:style] if use_default_style_filter?
238
+ hints[:position] = true if use_spatial_filter?
211
239
 
212
- if selector_format == :css
213
- if node.method(:find_css).arity != 1
214
- node.find_css(css, **hints)
215
- else
240
+ case selector_format
241
+ when :css
242
+ if node.method(:find_css).arity == 1
216
243
  node.find_css(css)
217
- end
218
- elsif selector_format == :xpath
219
- if node.method(:find_xpath).arity != 1
220
- node.find_xpath(xpath(exact), **hints)
221
244
  else
245
+ node.find_css(css, **hints)
246
+ end
247
+ when :xpath
248
+ if node.method(:find_xpath).arity == 1
222
249
  node.find_xpath(xpath(exact))
250
+ else
251
+ node.find_xpath(xpath(exact), **hints)
223
252
  end
224
253
  else
225
254
  raise ArgumentError, "Unknown format: #{selector_format}"
@@ -291,6 +320,15 @@ module Capybara
291
320
  filters
292
321
  end
293
322
 
323
+ def ordered_results(results)
324
+ case @order
325
+ when :reverse
326
+ results.reverse
327
+ else
328
+ results
329
+ end
330
+ end
331
+
294
332
  def custom_keys
295
333
  @custom_keys ||= node_filters.keys + expression_filters.keys
296
334
  end
@@ -318,7 +356,7 @@ module Capybara
318
356
  conditions[:id] = options[:id] if use_default_id_filter?
319
357
  conditions[:class] = options[:class] if use_default_class_filter?
320
358
  conditions[:style] = options[:style] if use_default_style_filter? && !options[:style].is_a?(Hash)
321
- builder(expr).add_attribute_conditions(conditions)
359
+ builder(expr).add_attribute_conditions(**conditions)
322
360
  end
323
361
 
324
362
  def use_default_id_filter?
@@ -333,6 +371,10 @@ module Capybara
333
371
  options.key?(:style) && !custom_keys.include?(:style)
334
372
  end
335
373
 
374
+ def use_spatial_filter?
375
+ options.values_at(*SPATIAL_KEYS).compact.any?
376
+ end
377
+
336
378
  def apply_expression_filters(expression)
337
379
  unapplied_options = options.keys - valid_keys
338
380
  expression_filters.inject(expression) do |expr, (name, ef)|
@@ -397,6 +439,42 @@ module Capybara
397
439
  matches_exact_text_filter?(node)
398
440
  end
399
441
 
442
+ def matches_spatial_filters?(node)
443
+ applied_filters << :spatial
444
+ return true unless use_spatial_filter?
445
+
446
+ node_rect = Rectangle.new(node.initial_cache[:position] || node.rect)
447
+
448
+ if options[:above]
449
+ el_rect = rect_cache(options[:above])
450
+ return false unless node_rect.above? el_rect
451
+ end
452
+
453
+ if options[:below]
454
+ el_rect = rect_cache(options[:below])
455
+ return false unless node_rect.below? el_rect
456
+ end
457
+
458
+ if options[:left_of]
459
+ el_rect = rect_cache(options[:left_of])
460
+ return false unless node_rect.left_of? el_rect
461
+ end
462
+
463
+ if options[:right_of]
464
+ el_rect = rect_cache(options[:right_of])
465
+ return false unless node_rect.right_of? el_rect
466
+ end
467
+
468
+ if options[:near]
469
+ return false if node == options[:near]
470
+
471
+ el_rect = rect_cache(options[:near])
472
+ return false unless node_rect.near? el_rect
473
+ end
474
+
475
+ true
476
+ end
477
+
400
478
  def matches_id_filter?(node)
401
479
  return true unless use_default_id_filter? && options[:id].is_a?(Regexp)
402
480
 
@@ -404,9 +482,25 @@ module Capybara
404
482
  end
405
483
 
406
484
  def matches_class_filter?(node)
407
- return true unless use_default_class_filter? && options[:class].is_a?(Regexp)
485
+ return true unless use_default_class_filter? && need_to_process_classes?
486
+
487
+ if options[:class].is_a? Regexp
488
+ options[:class].match? node[:class]
489
+ else
490
+ classes = (node[:class] || '').split
491
+ options[:class].select { |c| c.is_a? Regexp }.all? do |r|
492
+ classes.any? { |cls| r.match? cls }
493
+ end
494
+ end
495
+ end
408
496
 
409
- options[:class].match? node[:class]
497
+ def need_to_process_classes?
498
+ case options[:class]
499
+ when Regexp then true
500
+ when Array then options[:class].any?(Regexp)
501
+ else
502
+ false
503
+ end
410
504
  end
411
505
 
412
506
  def matches_style_filter?(node)
@@ -451,9 +545,9 @@ module Capybara
451
545
  return (visible != :hidden) && (node.initial_cache[:visible] != false) && !node.obscured? if obscured == false
452
546
 
453
547
  vis = case visible
454
- when :visible then
548
+ when :visible
455
549
  node.initial_cache[:visible] || (node.initial_cache[:visible].nil? && node.visible?)
456
- when :hidden then
550
+ when :hidden
457
551
  (node.initial_cache[:visible] == false) || (node.initial_cache[:visbile].nil? && !node.visible?)
458
552
  else
459
553
  true
@@ -491,6 +585,148 @@ module Capybara
491
585
  def builder(expr)
492
586
  selector.builder(expr)
493
587
  end
588
+
589
+ def position_cache(key)
590
+ @filter_cache[key][:position] ||= key.rect
591
+ end
592
+
593
+ def rect_cache(key)
594
+ @filter_cache[key][:rect] ||= Rectangle.new(position_cache(key))
595
+ end
596
+
597
+ class Rectangle
598
+ attr_reader :top, :bottom, :left, :right
599
+
600
+ def initialize(position)
601
+ # rubocop:disable Style/RescueModifier
602
+ @top = position['top'] rescue position['y']
603
+ @bottom = position['bottom'] rescue (@top + position['height'])
604
+ @left = position['left'] rescue position['x']
605
+ @right = position['right'] rescue (@left + position['width'])
606
+ # rubocop:enable Style/RescueModifier
607
+ end
608
+
609
+ def distance(other)
610
+ distance = Float::INFINITY
611
+
612
+ line_segments.each do |ls1|
613
+ other.line_segments.each do |ls2|
614
+ distance = [
615
+ distance,
616
+ distance_segment_segment(*ls1, *ls2)
617
+ ].min
618
+ end
619
+ end
620
+
621
+ distance
622
+ end
623
+
624
+ def above?(other)
625
+ bottom <= other.top
626
+ end
627
+
628
+ def below?(other)
629
+ top >= other.bottom
630
+ end
631
+
632
+ def left_of?(other)
633
+ right <= other.left
634
+ end
635
+
636
+ def right_of?(other)
637
+ left >= other.right
638
+ end
639
+
640
+ def near?(other)
641
+ distance(other) <= 50
642
+ end
643
+
644
+ protected
645
+
646
+ def line_segments
647
+ [
648
+ [Vector[top, left], Vector[top, right]],
649
+ [Vector[top, right], Vector[bottom, left]],
650
+ [Vector[bottom, left], Vector[bottom, right]],
651
+ [Vector[bottom, right], Vector[top, left]]
652
+ ]
653
+ end
654
+
655
+ private
656
+
657
+ def distance_segment_segment(l1p1, l1p2, l2p1, l2p2)
658
+ # See http://geomalgorithms.com/a07-_distance.html
659
+ # rubocop:disable Naming/VariableName
660
+ u = l1p2 - l1p1
661
+ v = l2p2 - l2p1
662
+ w = l1p1 - l2p1
663
+
664
+ a = u.dot u
665
+ b = u.dot v
666
+ c = v.dot v
667
+
668
+ d = u.dot w
669
+ e = v.dot w
670
+ cap_d = (a * c) - (b**2)
671
+ sD = tD = cap_d
672
+
673
+ # compute the line parameters of the two closest points
674
+ if cap_d < Float::EPSILON # the lines are almost parallel
675
+ sN = 0.0 # force using point P0 on segment S1
676
+ sD = 1.0 # to prevent possible division by 0.0 later
677
+ tN = e
678
+ tD = c
679
+ else # get the closest points on the infinite lines
680
+ sN = (b * e) - (c * d)
681
+ tN = (a * e) - (b * d)
682
+ if sN.negative? # sc < 0 => the s=0 edge is visible
683
+ sN = 0
684
+ tN = e
685
+ tD = c
686
+ elsif sN > sD # sc > 1 => the s=1 edge is visible
687
+ sN = sD
688
+ tN = e + b
689
+ tD = c
690
+ end
691
+ end
692
+
693
+ if tN.negative? # tc < 0 => the t=0 edge is visible
694
+ tN = 0
695
+ # recompute sc for this edge
696
+ if (-d).negative?
697
+ sN = 0.0
698
+ elsif -d > a
699
+ sN = sD
700
+ else
701
+ sN = -d
702
+ sD = a
703
+ end
704
+ elsif tN > tD # tc > 1 => the t=1 edge is visible
705
+ tN = tD
706
+ # recompute sc for this edge
707
+ if (-d + b).negative?
708
+ sN = 0.0
709
+ elsif (-d + b) > a
710
+ sN = sD
711
+ else
712
+ sN = (-d + b)
713
+ sD = a
714
+ end
715
+ end
716
+
717
+ # finally do the division to get sc and tc
718
+ sc = sN.abs < Float::EPSILON ? 0.0 : sN / sD
719
+ tc = tN.abs < Float::EPSILON ? 0.0 : tN / tD
720
+
721
+ # difference of the two closest points
722
+ dP = w + (u * sc) - (v * tc)
723
+
724
+ Math.sqrt(dP.dot(dP))
725
+ # rubocop:enable Naming/VariableName
726
+ end
727
+ end
728
+
729
+ private_constant :Rectangle
494
730
  end
495
731
  end
496
732
  end