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
@@ -4,22 +4,22 @@
|
|
4
4
|
|
5
5
|
require 'capybara/selenium/extensions/find'
|
6
6
|
require 'capybara/selenium/extensions/scroll'
|
7
|
+
require 'capybara/node/whitespace_normalizer'
|
7
8
|
|
8
9
|
class Capybara::Selenium::Node < Capybara::Driver::Node
|
10
|
+
include Capybara::Node::WhitespaceNormalizer
|
9
11
|
include Capybara::Selenium::Find
|
10
12
|
include Capybara::Selenium::Scroll
|
11
13
|
|
12
14
|
def visible_text
|
15
|
+
raise NotImplementedError, 'Getting visible text is not currently supported directly on shadow roots' if shadow_root?
|
16
|
+
|
13
17
|
native.text
|
14
18
|
end
|
15
19
|
|
16
20
|
def all_text
|
17
|
-
text = driver.evaluate_script('arguments[0].textContent', self)
|
18
|
-
text
|
19
|
-
.gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
|
20
|
-
.gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
|
21
|
-
.gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
|
22
|
-
.tr("\u00a0", ' ')
|
21
|
+
text = driver.evaluate_script('arguments[0].textContent', self) || ''
|
22
|
+
normalize_spacing(text)
|
23
23
|
end
|
24
24
|
|
25
25
|
def [](name)
|
@@ -37,9 +37,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def style(styles)
|
40
|
-
styles.
|
41
|
-
result[style] = native.css_value(style)
|
42
|
-
end
|
40
|
+
styles.to_h { |style| [style, native.css_value(style)] }
|
43
41
|
end
|
44
42
|
|
45
43
|
##
|
@@ -53,12 +51,22 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
|
|
53
51
|
# :none => append the new value to the existing value <br/>
|
54
52
|
# :backspace => send backspace keystrokes to clear the field <br/>
|
55
53
|
# Array => an array of keys to send before the value being set, e.g. [[:command, 'a'], :backspace]
|
54
|
+
# @option options [Boolean] :rapid (nil) Whether setting text inputs should use a faster "rapid" mode<br/>
|
55
|
+
# nil => Text inputs with length greater than 30 characters will be set using a faster driver script mode<br/>
|
56
|
+
# true => Rapid mode will be used regardless of input length<br/>
|
57
|
+
# false => Sends keys via conventional mode. This may be required to avoid losing key-presses if you have certain
|
58
|
+
# Javascript interactions on form inputs<br/>
|
56
59
|
def set(value, **options)
|
57
|
-
|
60
|
+
if value.is_a?(Array) && !multiple?
|
61
|
+
raise ArgumentError, "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}"
|
62
|
+
end
|
63
|
+
|
64
|
+
tag_name, type = attrs(:tagName, :type).map { |val| val&.downcase }
|
65
|
+
@tag_name ||= tag_name
|
58
66
|
|
59
67
|
case tag_name
|
60
68
|
when 'input'
|
61
|
-
case
|
69
|
+
case type
|
62
70
|
when 'radio'
|
63
71
|
click
|
64
72
|
when 'checkbox'
|
@@ -71,13 +79,17 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
|
|
71
79
|
set_time(value)
|
72
80
|
when 'datetime-local'
|
73
81
|
set_datetime_local(value)
|
82
|
+
when 'color'
|
83
|
+
set_color(value)
|
84
|
+
when 'range'
|
85
|
+
set_range(value)
|
74
86
|
else
|
75
|
-
set_text(value, options)
|
87
|
+
set_text(value, **options)
|
76
88
|
end
|
77
89
|
when 'textarea'
|
78
|
-
set_text(value, options)
|
90
|
+
set_text(value, **options)
|
79
91
|
else
|
80
|
-
set_content_editable(value)
|
92
|
+
set_content_editable(value)
|
81
93
|
end
|
82
94
|
end
|
83
95
|
|
@@ -95,26 +107,45 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
|
|
95
107
|
click_options = ClickOptions.new(keys, options)
|
96
108
|
return native.click if click_options.empty?
|
97
109
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
110
|
+
perform_with_options(click_options) do |action|
|
111
|
+
target = click_options.coords? ? nil : native
|
112
|
+
if click_options.delay.zero?
|
113
|
+
action.click(target)
|
114
|
+
else
|
115
|
+
action.click_and_hold(target)
|
116
|
+
action_pause(action, click_options.delay)
|
117
|
+
action.release
|
118
|
+
end
|
119
|
+
end
|
120
|
+
rescue StandardError => e
|
121
|
+
if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
|
122
|
+
e.message.include?('Other element would receive the click')
|
102
123
|
scroll_to_center
|
103
124
|
end
|
104
125
|
|
105
|
-
raise
|
126
|
+
raise e
|
106
127
|
end
|
107
128
|
|
108
129
|
def right_click(keys = [], **options)
|
109
130
|
click_options = ClickOptions.new(keys, options)
|
110
|
-
|
111
|
-
click_options.coords? ?
|
131
|
+
perform_with_options(click_options) do |action|
|
132
|
+
target = click_options.coords? ? nil : native
|
133
|
+
if click_options.delay.zero?
|
134
|
+
action.context_click(target)
|
135
|
+
else
|
136
|
+
action.move_to(target) if target
|
137
|
+
action.pointer_down(:right).then do |act|
|
138
|
+
action_pause(act, click_options.delay)
|
139
|
+
end.pointer_up(:right)
|
140
|
+
end
|
112
141
|
end
|
113
142
|
end
|
114
143
|
|
115
144
|
def double_click(keys = [], **options)
|
116
145
|
click_options = ClickOptions.new(keys, options)
|
117
|
-
|
146
|
+
raise ArgumentError, "double_click doesn't support a delay option" unless click_options.delay.zero?
|
147
|
+
|
148
|
+
perform_with_options(click_options) do |action|
|
118
149
|
click_options.coords? ? action.double_click : action.double_click(native)
|
119
150
|
end
|
120
151
|
end
|
@@ -127,15 +158,30 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
|
|
127
158
|
scroll_if_needed { browser_action.move_to(native).perform }
|
128
159
|
end
|
129
160
|
|
130
|
-
def drag_to(element)
|
161
|
+
def drag_to(element, drop_modifiers: [], **)
|
162
|
+
drop_modifiers = Array(drop_modifiers)
|
131
163
|
# Due to W3C spec compliance - The Actions API no longer scrolls to elements when necessary
|
132
164
|
# which means Seleniums `drag_and_drop` is now broken - do it manually
|
133
165
|
scroll_if_needed { browser_action.click_and_hold(native).perform }
|
134
|
-
element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
|
166
|
+
# element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
|
167
|
+
element.scroll_if_needed do
|
168
|
+
keys_down = modifiers_down(browser_action, drop_modifiers)
|
169
|
+
keys_up = modifiers_up(keys_down.move_to(element.native).release, drop_modifiers)
|
170
|
+
keys_up.perform
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def drop(*_)
|
175
|
+
raise NotImplementedError, 'Out of browser drop emulation is not implemented for the current browser'
|
135
176
|
end
|
136
177
|
|
137
178
|
def tag_name
|
138
|
-
|
179
|
+
@tag_name ||=
|
180
|
+
if native.respond_to? :tag_name
|
181
|
+
native.tag_name.downcase
|
182
|
+
else
|
183
|
+
shadow_root? ? 'ShadowRoot' : 'Unknown'
|
184
|
+
end
|
139
185
|
end
|
140
186
|
|
141
187
|
def visible?; boolean_attr(native.displayed?); end
|
@@ -148,21 +194,33 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
|
|
148
194
|
return true unless native.enabled?
|
149
195
|
|
150
196
|
# WebDriver only defines `disabled?` for form controls but fieldset makes sense too
|
151
|
-
|
197
|
+
find_xpath('self::fieldset/ancestor-or-self::fieldset[@disabled]').any?
|
152
198
|
end
|
153
199
|
|
154
200
|
def content_editable?
|
155
|
-
native.attribute('isContentEditable')
|
156
|
-
end
|
157
|
-
|
158
|
-
def ==(other)
|
159
|
-
native == other.native
|
201
|
+
native.attribute('isContentEditable') == 'true'
|
160
202
|
end
|
161
203
|
|
162
204
|
def path
|
163
205
|
driver.evaluate_script GET_XPATH_SCRIPT, self
|
164
206
|
end
|
165
207
|
|
208
|
+
def obscured?(x: nil, y: nil)
|
209
|
+
res = driver.evaluate_script(OBSCURED_OR_OFFSET_SCRIPT, self, x, y)
|
210
|
+
return true if res == true
|
211
|
+
|
212
|
+
driver.frame_obscured_at?(x: res['x'], y: res['y'])
|
213
|
+
end
|
214
|
+
|
215
|
+
def rect
|
216
|
+
native.rect
|
217
|
+
end
|
218
|
+
|
219
|
+
def shadow_root
|
220
|
+
root = native.shadow_root
|
221
|
+
root && build_node(native.shadow_root)
|
222
|
+
end
|
223
|
+
|
166
224
|
protected
|
167
225
|
|
168
226
|
def scroll_if_needed
|
@@ -172,6 +230,21 @@ protected
|
|
172
230
|
yield
|
173
231
|
end
|
174
232
|
|
233
|
+
def scroll_to_center
|
234
|
+
script = <<-JS
|
235
|
+
try {
|
236
|
+
arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
|
237
|
+
} catch(e) {
|
238
|
+
arguments[0].scrollIntoView(true);
|
239
|
+
}
|
240
|
+
JS
|
241
|
+
begin
|
242
|
+
driver.execute_script(script, self)
|
243
|
+
rescue StandardError
|
244
|
+
# Swallow error if scrollIntoView with options isn't supported
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
175
248
|
private
|
176
249
|
|
177
250
|
def sibling_index(parent, node, selector)
|
@@ -197,29 +270,38 @@ private
|
|
197
270
|
find_xpath(XPath.ancestor(:select)[1]).first
|
198
271
|
end
|
199
272
|
|
200
|
-
def set_text(value, clear: nil, **_unused)
|
273
|
+
def set_text(value, clear: nil, rapid: nil, **_unused)
|
201
274
|
value = value.to_s
|
202
275
|
if value.empty? && clear.nil?
|
203
276
|
native.clear
|
204
277
|
elsif clear == :backspace
|
205
278
|
# Clear field by sending the correct number of backspace keys.
|
206
279
|
backspaces = [:backspace] * self.value.to_s.length
|
207
|
-
send_keys(
|
280
|
+
send_keys(:end, *backspaces, value)
|
208
281
|
elsif clear.is_a? Array
|
209
282
|
send_keys(*clear, value)
|
210
283
|
else
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
284
|
+
driver.execute_script 'arguments[0].select()', self unless clear == :none
|
285
|
+
if rapid == true || ((value.length > auto_rapid_set_length) && rapid != false)
|
286
|
+
send_keys(value[0..3])
|
287
|
+
driver.execute_script RAPID_APPEND_TEXT, self, value[4...-3]
|
288
|
+
send_keys(value[-3..])
|
289
|
+
else
|
290
|
+
send_keys(value)
|
291
|
+
end
|
216
292
|
end
|
217
293
|
end
|
218
294
|
|
219
|
-
def
|
295
|
+
def auto_rapid_set_length
|
296
|
+
30
|
297
|
+
end
|
298
|
+
|
299
|
+
def perform_with_options(click_options, &block)
|
300
|
+
raise ArgumentError, 'A block must be provided' unless block
|
301
|
+
|
220
302
|
scroll_if_needed do
|
221
303
|
action_with_modifiers(click_options) do |action|
|
222
|
-
if
|
304
|
+
if block
|
223
305
|
yield action
|
224
306
|
else
|
225
307
|
click_options.coords? ? action.click : action.click(native)
|
@@ -228,21 +310,6 @@ private
|
|
228
310
|
end
|
229
311
|
end
|
230
312
|
|
231
|
-
def scroll_to_center
|
232
|
-
script = <<-'JS'
|
233
|
-
try {
|
234
|
-
arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
|
235
|
-
} catch(e) {
|
236
|
-
arguments[0].scrollIntoView(true);
|
237
|
-
}
|
238
|
-
JS
|
239
|
-
begin
|
240
|
-
driver.execute_script(script, self)
|
241
|
-
rescue StandardError # rubocop:disable Lint/HandleExceptions
|
242
|
-
# Swallow error if scrollIntoView with options isn't supported
|
243
|
-
end
|
244
|
-
end
|
245
|
-
|
246
313
|
def set_date(value) # rubocop:disable Naming/AccessorMethodName
|
247
314
|
value = SettableValue.new(value)
|
248
315
|
return set_text(value) unless value.dateable?
|
@@ -267,8 +334,17 @@ private
|
|
267
334
|
update_value_js(value.to_datetime_str)
|
268
335
|
end
|
269
336
|
|
337
|
+
def set_color(value) # rubocop:disable Naming/AccessorMethodName
|
338
|
+
update_value_js(value)
|
339
|
+
end
|
340
|
+
|
341
|
+
def set_range(value) # rubocop:disable Naming/AccessorMethodName
|
342
|
+
update_value_js(value)
|
343
|
+
end
|
344
|
+
|
270
345
|
def update_value_js(value)
|
271
346
|
driver.execute_script(<<-JS, self, value)
|
347
|
+
if (arguments[0].readOnly) { return };
|
272
348
|
if (document.activeElement !== arguments[0]){
|
273
349
|
arguments[0].focus();
|
274
350
|
}
|
@@ -281,32 +357,75 @@ private
|
|
281
357
|
end
|
282
358
|
|
283
359
|
def set_file(value) # rubocop:disable Naming/AccessorMethodName
|
284
|
-
|
285
|
-
|
360
|
+
with_file_detector do
|
361
|
+
path_names = value.to_s.empty? ? [] : value
|
362
|
+
file_names = Array(path_names).map do |pn|
|
363
|
+
Pathname.new(pn).absolute? ? pn : File.expand_path(pn)
|
364
|
+
end.join("\n")
|
365
|
+
native.send_keys(file_names)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
def with_file_detector
|
370
|
+
if driver.options[:browser] == :remote &&
|
371
|
+
bridge.respond_to?(:file_detector) &&
|
372
|
+
bridge.file_detector.nil?
|
373
|
+
begin
|
374
|
+
bridge.file_detector = lambda do |(fn, *)|
|
375
|
+
str = fn.to_s
|
376
|
+
str if File.exist?(str)
|
377
|
+
end
|
378
|
+
yield
|
379
|
+
ensure
|
380
|
+
bridge.file_detector = nil
|
381
|
+
end
|
382
|
+
else
|
383
|
+
yield
|
384
|
+
end
|
286
385
|
end
|
287
386
|
|
288
387
|
def set_content_editable(value) # rubocop:disable Naming/AccessorMethodName
|
289
388
|
# Ensure we are focused on the element
|
290
389
|
click
|
291
390
|
|
292
|
-
driver.execute_script <<-JS, self
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
391
|
+
editable = driver.execute_script <<-JS, self
|
392
|
+
if (arguments[0].isContentEditable) {
|
393
|
+
var range = document.createRange();
|
394
|
+
var sel = window.getSelection();
|
395
|
+
arguments[0].focus();
|
396
|
+
range.selectNodeContents(arguments[0]);
|
397
|
+
sel.removeAllRanges();
|
398
|
+
sel.addRange(range);
|
399
|
+
return true;
|
400
|
+
}
|
401
|
+
return false;
|
299
402
|
JS
|
300
403
|
|
301
404
|
# The action api has a speed problem but both chrome and firefox 58 raise errors
|
302
405
|
# if we use the faster direct send_keys. For now just send_keys to the element
|
303
406
|
# we've already focused.
|
304
407
|
# native.send_keys(value.to_s)
|
305
|
-
browser_action.send_keys(value.to_s).perform
|
408
|
+
browser_action.send_keys(value.to_s).perform if editable
|
306
409
|
end
|
307
410
|
|
308
411
|
def action_with_modifiers(click_options)
|
309
|
-
actions = browser_action.
|
412
|
+
actions = browser_action.tap do |acts|
|
413
|
+
if click_options.coords?
|
414
|
+
if click_options.center_offset?
|
415
|
+
acts.move_to(native, *click_options.coords)
|
416
|
+
else
|
417
|
+
right_by, down_by = *click_options.coords
|
418
|
+
size = native.size
|
419
|
+
left_offset = (size[:width] / 2).to_i
|
420
|
+
top_offset = (size[:height] / 2).to_i
|
421
|
+
left = -left_offset + right_by
|
422
|
+
top = -top_offset + down_by
|
423
|
+
acts.move_to(native, left, top)
|
424
|
+
end
|
425
|
+
else
|
426
|
+
acts.move_to(native)
|
427
|
+
end
|
428
|
+
end
|
310
429
|
modifiers_down(actions, click_options.keys)
|
311
430
|
yield actions
|
312
431
|
modifiers_up(actions, click_options.keys)
|
@@ -318,32 +437,49 @@ private
|
|
318
437
|
|
319
438
|
def modifiers_down(actions, keys)
|
320
439
|
each_key(keys) { |key| actions.key_down(key) }
|
440
|
+
actions
|
321
441
|
end
|
322
442
|
|
323
443
|
def modifiers_up(actions, keys)
|
324
444
|
each_key(keys) { |key| actions.key_up(key) }
|
445
|
+
actions
|
325
446
|
end
|
326
447
|
|
327
448
|
def browser
|
328
449
|
driver.browser
|
329
450
|
end
|
330
451
|
|
452
|
+
def bridge
|
453
|
+
browser.send(:bridge)
|
454
|
+
end
|
455
|
+
|
331
456
|
def browser_action
|
332
457
|
browser.action
|
333
458
|
end
|
334
459
|
|
335
|
-
def
|
336
|
-
|
337
|
-
|
460
|
+
def capabilities
|
461
|
+
browser.capabilities
|
462
|
+
end
|
463
|
+
|
464
|
+
def action_pause(action, duration)
|
465
|
+
action.pause(device: action.pointer_inputs.first, duration: duration)
|
466
|
+
end
|
467
|
+
|
468
|
+
def normalize_keys(keys)
|
469
|
+
keys.map do |key|
|
470
|
+
case key
|
338
471
|
when :ctrl then :control
|
339
472
|
when :command, :cmd then :meta
|
340
473
|
else
|
341
474
|
key
|
342
475
|
end
|
343
|
-
yield key
|
344
476
|
end
|
345
477
|
end
|
346
478
|
|
479
|
+
def each_key(keys, &block)
|
480
|
+
normalize_keys(keys).each(&block)
|
481
|
+
end
|
482
|
+
|
347
483
|
def find_context
|
348
484
|
native
|
349
485
|
end
|
@@ -352,11 +488,36 @@ private
|
|
352
488
|
self.class.new(driver, native_node, initial_cache)
|
353
489
|
end
|
354
490
|
|
491
|
+
def attrs(*attr_names)
|
492
|
+
return attr_names.map { |name| self[name.to_s] } if ENV['CAPYBARA_THOROUGH']
|
493
|
+
|
494
|
+
driver.evaluate_script <<~JS, self, attr_names.map(&:to_s)
|
495
|
+
(function(el, names){
|
496
|
+
return names.map(function(name){
|
497
|
+
return el[name]
|
498
|
+
});
|
499
|
+
})(arguments[0], arguments[1]);
|
500
|
+
JS
|
501
|
+
end
|
502
|
+
|
503
|
+
def native_id
|
504
|
+
# Selenium 3 -> 4 changed the return of ref
|
505
|
+
type_or_id, id = native.ref
|
506
|
+
id || type_or_id
|
507
|
+
end
|
508
|
+
|
509
|
+
def shadow_root?
|
510
|
+
defined?(::Selenium::WebDriver::ShadowRoot) && native.is_a?(::Selenium::WebDriver::ShadowRoot)
|
511
|
+
end
|
512
|
+
|
355
513
|
GET_XPATH_SCRIPT = <<~'JS'
|
356
514
|
(function(el, xml){
|
357
515
|
var xpath = '';
|
358
516
|
var pos, tempitem2;
|
359
517
|
|
518
|
+
if (el.getRootNode && el.getRootNode() instanceof ShadowRoot) {
|
519
|
+
return "(: Shadow DOM element - no XPath :)";
|
520
|
+
};
|
360
521
|
while(el !== xml.documentElement) {
|
361
522
|
pos = 0;
|
362
523
|
tempitem2 = el;
|
@@ -381,6 +542,33 @@ private
|
|
381
542
|
})(arguments[0], document)
|
382
543
|
JS
|
383
544
|
|
545
|
+
OBSCURED_OR_OFFSET_SCRIPT = <<~JS
|
546
|
+
(function(el, x, y) {
|
547
|
+
var box = el.getBoundingClientRect();
|
548
|
+
if (x == null) x = box.width/2;
|
549
|
+
if (y == null) y = box.height/2 ;
|
550
|
+
|
551
|
+
var px = box.left + x,
|
552
|
+
py = box.top + y,
|
553
|
+
e = document.elementFromPoint(px, py);
|
554
|
+
|
555
|
+
if (!el.contains(e))
|
556
|
+
return true;
|
557
|
+
|
558
|
+
return { x: px, y: py };
|
559
|
+
})(arguments[0], arguments[1], arguments[2])
|
560
|
+
JS
|
561
|
+
|
562
|
+
RAPID_APPEND_TEXT = <<~JS
|
563
|
+
(function(el, value) {
|
564
|
+
value = el.value + value;
|
565
|
+
if (el.maxLength && el.maxLength != -1){
|
566
|
+
value = value.slice(0, el.maxLength);
|
567
|
+
}
|
568
|
+
el.value = value;
|
569
|
+
})(arguments[0], arguments[1])
|
570
|
+
JS
|
571
|
+
|
384
572
|
# SettableValue encapsulates time/date field formatting
|
385
573
|
class SettableValue
|
386
574
|
attr_reader :value
|
@@ -398,7 +586,7 @@ private
|
|
398
586
|
end
|
399
587
|
|
400
588
|
def to_date_str
|
401
|
-
value.to_date.
|
589
|
+
value.to_date.iso8601
|
402
590
|
end
|
403
591
|
|
404
592
|
def timeable?
|
@@ -432,8 +620,16 @@ private
|
|
432
620
|
[options[:x], options[:y]]
|
433
621
|
end
|
434
622
|
|
623
|
+
def center_offset?
|
624
|
+
options[:offset] == :center
|
625
|
+
end
|
626
|
+
|
435
627
|
def empty?
|
436
|
-
keys.empty? && !coords?
|
628
|
+
keys.empty? && !coords? && delay.zero?
|
629
|
+
end
|
630
|
+
|
631
|
+
def delay
|
632
|
+
options[:delay] || 0
|
437
633
|
end
|
438
634
|
end
|
439
635
|
private_constant :ClickOptions
|
@@ -1,29 +1,125 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'capybara/selenium/extensions/html5_drag'
|
4
|
+
require 'capybara/selenium/extensions/file_input_click_emulation'
|
4
5
|
|
5
6
|
class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
|
6
7
|
include Html5Drag
|
8
|
+
include FileInputClickEmulation
|
9
|
+
|
10
|
+
def set_text(value, clear: nil, **_unused)
|
11
|
+
super.tap do
|
12
|
+
# React doesn't see the chromedriver element clear
|
13
|
+
send_keys(:space, :backspace) if value.to_s.empty? && clear.nil?
|
14
|
+
end
|
15
|
+
end
|
7
16
|
|
8
17
|
def set_file(value) # rubocop:disable Naming/AccessorMethodName
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
18
|
+
# In Chrome 75+ files are appended (due to WebDriver spec - why?) so we have to clear here if its multiple and already set
|
19
|
+
if browser_version >= 75.0
|
20
|
+
driver.execute_script(<<~JS, self)
|
21
|
+
if (arguments[0].multiple && arguments[0].files.length){
|
22
|
+
arguments[0].value = null;
|
23
|
+
}
|
24
|
+
JS
|
25
|
+
end
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
def drop(*args)
|
30
|
+
html5_drop(*args)
|
31
|
+
end
|
32
|
+
|
33
|
+
def click(*, **)
|
34
|
+
super
|
35
|
+
rescue ::Selenium::WebDriver::Error::ElementClickInterceptedError
|
36
|
+
raise
|
37
|
+
rescue ::Selenium::WebDriver::Error::WebDriverError => e
|
38
|
+
# chromedriver 74 (at least on mac) raises the wrong error for this
|
39
|
+
if e.message.include?('element click intercepted')
|
40
|
+
raise ::Selenium::WebDriver::Error::ElementClickInterceptedError, e.message
|
13
41
|
end
|
14
42
|
|
15
43
|
raise
|
16
44
|
end
|
17
45
|
|
18
|
-
def
|
19
|
-
|
46
|
+
def disabled?
|
47
|
+
driver.evaluate_script("arguments[0].matches(':disabled, select:disabled *')", self)
|
48
|
+
end
|
20
49
|
|
21
|
-
|
50
|
+
def select_option
|
51
|
+
# To optimize to only one check and then click
|
52
|
+
selected_or_disabled = driver.evaluate_script(<<~JS, self)
|
53
|
+
arguments[0].matches(':disabled, select:disabled *, :checked')
|
54
|
+
JS
|
55
|
+
click unless selected_or_disabled
|
56
|
+
end
|
57
|
+
|
58
|
+
def visible?
|
59
|
+
return super unless native_displayed?
|
60
|
+
|
61
|
+
begin
|
62
|
+
bridge.send(:execute, :is_element_displayed, id: native_id)
|
63
|
+
rescue Selenium::WebDriver::Error::UnknownCommandError
|
64
|
+
# If the is_element_displayed command is unknown, no point in trying again
|
65
|
+
driver.options[:native_displayed] = false
|
66
|
+
super
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def send_keys(*args)
|
71
|
+
args.chunk { |inp| inp.is_a?(String) && inp.match?(/\p{Emoji Presentation}/) }
|
72
|
+
.each do |contains_emoji, inputs|
|
73
|
+
if contains_emoji
|
74
|
+
inputs.join.grapheme_clusters.chunk { |gc| gc.match?(/\p{Emoji Presentation}/) }
|
75
|
+
.each do |emoji, clusters|
|
76
|
+
if emoji
|
77
|
+
driver.send(:execute_cdp, 'Input.insertText', text: clusters.join)
|
78
|
+
else
|
79
|
+
super(clusters.join)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
else
|
83
|
+
super(*inputs)
|
84
|
+
end
|
85
|
+
end
|
22
86
|
end
|
23
87
|
|
24
88
|
private
|
25
89
|
|
26
|
-
def
|
27
|
-
|
90
|
+
def perform_legacy_drag(element, drop_modifiers)
|
91
|
+
return super if chromedriver_fixed_actions_key_state? || element.obscured?
|
92
|
+
|
93
|
+
raise ArgumentError, 'Modifier keys are not supported while dragging in this version of Chrome.' unless drop_modifiers.empty?
|
94
|
+
|
95
|
+
# W3C Chrome/chromedriver < 77 doesn't maintain mouse button state across actions API performs
|
96
|
+
# https://bugs.chromium.org/p/chromedriver/issues/detail?id=2981
|
97
|
+
browser_action.release.perform
|
98
|
+
browser_action.click_and_hold(native).move_to(element.native).release.perform
|
99
|
+
end
|
100
|
+
|
101
|
+
def browser_version(to_float: true)
|
102
|
+
caps = capabilities
|
103
|
+
ver = caps[:browser_version] || caps[:version]
|
104
|
+
ver = ver.to_f if to_float
|
105
|
+
ver
|
106
|
+
end
|
107
|
+
|
108
|
+
def chromedriver_fixed_actions_key_state?
|
109
|
+
Gem::Requirement.new('>= 76.0.3809.68').satisfied_by?(chromedriver_version)
|
110
|
+
end
|
111
|
+
|
112
|
+
def chromedriver_supports_displayed_endpoint?
|
113
|
+
Gem::Requirement.new('>= 76.0.3809.25').satisfied_by?(chromedriver_version)
|
114
|
+
end
|
115
|
+
|
116
|
+
def chromedriver_version
|
117
|
+
Gem::Version.new(capabilities['chrome']['chromedriverVersion'].split(' ')[0]) # rubocop:disable Style/RedundantArgument
|
118
|
+
end
|
119
|
+
|
120
|
+
def native_displayed?
|
121
|
+
(driver.options[:native_displayed] != false) &&
|
122
|
+
chromedriver_supports_displayed_endpoint? &&
|
123
|
+
(!ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS'])
|
28
124
|
end
|
29
125
|
end
|