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