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.
- checksums.yaml +4 -4
- data/History.md +264 -11
- data/README.md +10 -6
- data/lib/capybara.rb +20 -8
- data/lib/capybara/config.rb +10 -8
- data/lib/capybara/cucumber.rb +1 -1
- data/lib/capybara/driver/base.rb +4 -0
- data/lib/capybara/driver/node.rb +4 -0
- data/lib/capybara/dsl.rb +10 -2
- data/lib/capybara/helpers.rb +28 -2
- data/lib/capybara/minitest.rb +232 -144
- data/lib/capybara/minitest/spec.rb +156 -97
- data/lib/capybara/node/actions.rb +36 -36
- data/lib/capybara/node/base.rb +6 -6
- data/lib/capybara/node/document.rb +2 -2
- data/lib/capybara/node/document_matchers.rb +3 -3
- data/lib/capybara/node/element.rb +77 -33
- data/lib/capybara/node/finders.rb +24 -17
- data/lib/capybara/node/matchers.rb +79 -64
- data/lib/capybara/node/simple.rb +11 -4
- data/lib/capybara/queries/ancestor_query.rb +6 -10
- data/lib/capybara/queries/base_query.rb +2 -1
- data/lib/capybara/queries/current_path_query.rb +14 -4
- data/lib/capybara/queries/selector_query.rb +259 -23
- data/lib/capybara/queries/sibling_query.rb +5 -11
- data/lib/capybara/queries/style_query.rb +1 -1
- data/lib/capybara/queries/text_query.rb +13 -1
- data/lib/capybara/rack_test/browser.rb +13 -4
- data/lib/capybara/rack_test/driver.rb +2 -1
- data/lib/capybara/rack_test/form.rb +2 -2
- data/lib/capybara/rack_test/node.rb +42 -6
- data/lib/capybara/registration_container.rb +44 -0
- data/lib/capybara/registrations/drivers.rb +18 -12
- data/lib/capybara/registrations/patches/puma_ssl.rb +29 -0
- data/lib/capybara/registrations/servers.rb +9 -2
- data/lib/capybara/result.rb +39 -19
- data/lib/capybara/rspec.rb +2 -0
- data/lib/capybara/rspec/matcher_proxies.rb +5 -5
- data/lib/capybara/rspec/matchers.rb +97 -74
- data/lib/capybara/rspec/matchers/base.rb +19 -6
- data/lib/capybara/rspec/matchers/count_sugar.rb +2 -1
- data/lib/capybara/rspec/matchers/have_ancestor.rb +5 -7
- data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
- data/lib/capybara/rspec/matchers/have_selector.rb +15 -10
- data/lib/capybara/rspec/matchers/have_sibling.rb +4 -7
- data/lib/capybara/rspec/matchers/have_text.rb +4 -7
- data/lib/capybara/rspec/matchers/have_title.rb +2 -2
- data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
- data/lib/capybara/rspec/matchers/match_style.rb +7 -2
- data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
- data/lib/capybara/selector.rb +46 -19
- data/lib/capybara/selector/builders/css_builder.rb +10 -6
- data/lib/capybara/selector/builders/xpath_builder.rb +4 -2
- data/lib/capybara/selector/css.rb +1 -1
- data/lib/capybara/selector/definition.rb +13 -11
- data/lib/capybara/selector/definition/button.rb +32 -15
- data/lib/capybara/selector/definition/checkbox.rb +2 -2
- data/lib/capybara/selector/definition/css.rb +3 -1
- data/lib/capybara/selector/definition/datalist_input.rb +2 -2
- data/lib/capybara/selector/definition/datalist_option.rb +1 -1
- data/lib/capybara/selector/definition/element.rb +3 -2
- data/lib/capybara/selector/definition/field.rb +1 -1
- data/lib/capybara/selector/definition/file_field.rb +1 -1
- data/lib/capybara/selector/definition/fillable_field.rb +2 -2
- data/lib/capybara/selector/definition/label.rb +5 -3
- data/lib/capybara/selector/definition/link.rb +8 -0
- data/lib/capybara/selector/definition/option.rb +1 -1
- data/lib/capybara/selector/definition/radio_button.rb +2 -2
- data/lib/capybara/selector/definition/select.rb +33 -14
- data/lib/capybara/selector/definition/table.rb +6 -3
- data/lib/capybara/selector/definition/table_row.rb +2 -2
- data/lib/capybara/selector/filter_set.rb +13 -11
- data/lib/capybara/selector/filters/base.rb +6 -1
- data/lib/capybara/selector/filters/locator_filter.rb +1 -1
- data/lib/capybara/selector/regexp_disassembler.rb +7 -0
- data/lib/capybara/selector/selector.rb +13 -3
- data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -1
- data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -1
- data/lib/capybara/selenium/atoms/src/getAttribute.js +1 -1
- data/lib/capybara/selenium/atoms/src/isDisplayed.js +10 -10
- data/lib/capybara/selenium/driver.rb +86 -24
- data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +24 -21
- data/lib/capybara/selenium/driver_specializations/edge_driver.rb +21 -19
- data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +17 -1
- data/lib/capybara/selenium/driver_specializations/safari_driver.rb +0 -4
- data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
- data/lib/capybara/selenium/extensions/find.rb +37 -26
- data/lib/capybara/selenium/extensions/html5_drag.rb +55 -11
- data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
- data/lib/capybara/selenium/extensions/scroll.rb +8 -10
- data/lib/capybara/selenium/logger_suppressor.rb +8 -2
- data/lib/capybara/selenium/node.rb +160 -40
- data/lib/capybara/selenium/nodes/chrome_node.rb +72 -12
- data/lib/capybara/selenium/nodes/edge_node.rb +32 -14
- data/lib/capybara/selenium/nodes/firefox_node.rb +28 -32
- data/lib/capybara/selenium/nodes/safari_node.rb +5 -29
- data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
- data/lib/capybara/selenium/patches/atoms.rb +4 -4
- data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
- data/lib/capybara/selenium/patches/logs.rb +32 -7
- data/lib/capybara/server.rb +19 -3
- data/lib/capybara/server/animation_disabler.rb +8 -3
- data/lib/capybara/server/checker.rb +1 -1
- data/lib/capybara/server/middleware.rb +22 -10
- data/lib/capybara/session.rb +66 -40
- data/lib/capybara/session/config.rb +11 -3
- data/lib/capybara/session/matchers.rb +11 -11
- data/lib/capybara/spec/public/offset.js +6 -0
- data/lib/capybara/spec/public/test.js +75 -7
- data/lib/capybara/spec/session/accept_alert_spec.rb +1 -1
- data/lib/capybara/spec/session/all_spec.rb +60 -5
- data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
- data/lib/capybara/spec/session/assert_text_spec.rb +9 -5
- data/lib/capybara/spec/session/check_spec.rb +6 -0
- data/lib/capybara/spec/session/click_button_spec.rb +16 -0
- data/lib/capybara/spec/session/click_link_or_button_spec.rb +9 -0
- data/lib/capybara/spec/session/current_url_spec.rb +11 -1
- data/lib/capybara/spec/session/fill_in_spec.rb +29 -0
- data/lib/capybara/spec/session/find_spec.rb +55 -0
- data/lib/capybara/spec/session/has_ancestor_spec.rb +2 -0
- data/lib/capybara/spec/session/has_button_spec.rb +51 -0
- data/lib/capybara/spec/session/has_css_spec.rb +26 -4
- data/lib/capybara/spec/session/has_current_path_spec.rb +15 -2
- data/lib/capybara/spec/session/has_field_spec.rb +34 -0
- data/lib/capybara/spec/session/has_select_spec.rb +32 -4
- data/lib/capybara/spec/session/has_selector_spec.rb +4 -4
- data/lib/capybara/spec/session/has_table_spec.rb +51 -5
- data/lib/capybara/spec/session/has_text_spec.rb +30 -0
- data/lib/capybara/spec/session/html_spec.rb +1 -1
- data/lib/capybara/spec/session/matches_style_spec.rb +2 -2
- data/lib/capybara/spec/session/node_spec.rb +394 -9
- data/lib/capybara/spec/session/refresh_spec.rb +2 -1
- data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
- data/lib/capybara/spec/session/save_page_spec.rb +4 -4
- data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -15
- data/lib/capybara/spec/session/selectors_spec.rb +16 -3
- data/lib/capybara/spec/session/window/switch_to_window_spec.rb +1 -1
- data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -1
- data/lib/capybara/spec/session/window/window_spec.rb +8 -8
- data/lib/capybara/spec/session/window/windows_spec.rb +1 -1
- data/lib/capybara/spec/spec_helper.rb +14 -14
- data/lib/capybara/spec/test_app.rb +27 -21
- data/lib/capybara/spec/views/form.erb +47 -4
- data/lib/capybara/spec/views/offset.erb +32 -0
- data/lib/capybara/spec/views/spatial.erb +31 -0
- data/lib/capybara/spec/views/with_animation.erb +37 -1
- data/lib/capybara/spec/views/with_dragula.erb +24 -0
- data/lib/capybara/spec/views/with_html.erb +24 -2
- data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
- data/lib/capybara/spec/views/with_js.erb +4 -1
- data/lib/capybara/spec/views/with_jstree.erb +26 -0
- data/lib/capybara/spec/views/with_sortable_js.erb +1 -1
- data/lib/capybara/version.rb +1 -1
- data/lib/capybara/window.rb +3 -7
- data/spec/basic_node_spec.rb +15 -14
- data/spec/capybara_spec.rb +28 -28
- data/spec/dsl_spec.rb +16 -3
- data/spec/filter_set_spec.rb +5 -5
- data/spec/fixtures/selenium_driver_rspec_failure.rb +1 -1
- data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
- data/spec/minitest_spec.rb +3 -2
- data/spec/minitest_spec_spec.rb +46 -46
- data/spec/rack_test_spec.rb +38 -15
- data/spec/regexp_dissassembler_spec.rb +52 -38
- data/spec/result_spec.rb +43 -32
- data/spec/rspec/features_spec.rb +4 -1
- data/spec/rspec/scenarios_spec.rb +4 -0
- data/spec/rspec/shared_spec_matchers.rb +68 -56
- data/spec/rspec_spec.rb +9 -5
- data/spec/selector_spec.rb +32 -17
- data/spec/selenium_spec_chrome.rb +78 -11
- data/spec/selenium_spec_chrome_remote.rb +23 -6
- data/spec/selenium_spec_edge.rb +15 -12
- data/spec/selenium_spec_firefox.rb +24 -19
- data/spec/selenium_spec_firefox_remote.rb +0 -8
- data/spec/selenium_spec_ie.rb +1 -6
- data/spec/server_spec.rb +106 -44
- data/spec/session_spec.rb +5 -5
- data/spec/shared_selenium_node.rb +56 -2
- data/spec/shared_selenium_session.rb +122 -15
- data/spec/spec_helper.rb +2 -2
- metadata +63 -17
- data/lib/capybara/spec/session/source_spec.rb +0 -0
data/lib/capybara/node/simple.rb
CHANGED
@@ -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') ||
|
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.
|
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.
|
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
|
-
|
22
|
-
|
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
|
-
|
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: {
|
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,
|
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
|
-
|
74
|
-
|
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
|
-
|
213
|
-
|
214
|
-
|
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? &&
|
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
|
-
|
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
|
548
|
+
when :visible
|
455
549
|
node.initial_cache[:visible] || (node.initial_cache[:visible].nil? && node.visible?)
|
456
|
-
when :hidden
|
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
|