capybara 3.23.0 → 3.35.3

Sign up to get free protection for your applications and to get access to all the features.
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