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
@@ -3,6 +3,25 @@
|
|
3
3
|
require 'capybara/selenium/nodes/firefox_node'
|
4
4
|
|
5
5
|
module Capybara::Selenium::Driver::FirefoxDriver
|
6
|
+
def self.extended(driver)
|
7
|
+
driver.extend Capybara::Selenium::Driver::W3CFirefoxDriver
|
8
|
+
bridge = driver.send(:bridge)
|
9
|
+
bridge.extend Capybara::Selenium::IsDisplayed unless bridge.send(:commands, :is_element_displayed)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Capybara::Selenium::Driver::W3CFirefoxDriver
|
14
|
+
class << self
|
15
|
+
def extended(driver)
|
16
|
+
require 'capybara/selenium/patches/pause_duration_fix' if pause_broken?(driver.browser)
|
17
|
+
driver.options[:native_displayed] = false if driver.options[:native_displayed].nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
def pause_broken?(sel_driver)
|
21
|
+
sel_driver.capabilities['moz:geckodriverVersion']&.start_with?('0.22.')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
6
25
|
def resize_window_to(handle, width, height)
|
7
26
|
within_given_window(handle) do
|
8
27
|
# Don't set the size if already set - See https://github.com/mozilla/geckodriver/issues/643
|
@@ -18,8 +37,17 @@ module Capybara::Selenium::Driver::FirefoxDriver
|
|
18
37
|
# Use instance variable directly so we avoid starting the browser just to reset the session
|
19
38
|
return unless @browser
|
20
39
|
|
40
|
+
if browser_version >= 68
|
41
|
+
begin
|
42
|
+
# Firefox 68 hangs if we try to switch windows while a modal is visible
|
43
|
+
browser.switch_to.alert&.dismiss
|
44
|
+
rescue Selenium::WebDriver::Error::NoSuchAlertError
|
45
|
+
# Swallow
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
21
49
|
switch_to_window(window_handles.first)
|
22
|
-
window_handles.slice(1
|
50
|
+
window_handles.slice(1..).each { |win| close_window(win) }
|
23
51
|
super
|
24
52
|
end
|
25
53
|
|
@@ -28,7 +56,7 @@ module Capybara::Selenium::Driver::FirefoxDriver
|
|
28
56
|
accept_modal :confirm, wait: 0.1 do
|
29
57
|
super
|
30
58
|
end
|
31
|
-
rescue Capybara::ModalNotFound
|
59
|
+
rescue Capybara::ModalNotFound
|
32
60
|
# No modal was opened - page has refreshed - ignore
|
33
61
|
end
|
34
62
|
|
@@ -39,7 +67,7 @@ module Capybara::Selenium::Driver::FirefoxDriver
|
|
39
67
|
# so we have to move to the default_content and iterate back through the frames
|
40
68
|
handles = @frame_handles[current_window_handle]
|
41
69
|
browser.switch_to.default_content
|
42
|
-
handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh) }
|
70
|
+
handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh.native) }
|
43
71
|
end
|
44
72
|
|
45
73
|
private
|
@@ -47,4 +75,10 @@ private
|
|
47
75
|
def build_node(native_node, initial_cache = {})
|
48
76
|
::Capybara::Selenium::FirefoxNode.new(self, native_node, initial_cache)
|
49
77
|
end
|
78
|
+
|
79
|
+
def browser_version
|
80
|
+
browser.capabilities[:browser_version].to_f
|
81
|
+
end
|
50
82
|
end
|
83
|
+
|
84
|
+
Capybara::Selenium::Driver.register_specialization :firefox, Capybara::Selenium::Driver::FirefoxDriver
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'capybara/selenium/nodes/ie_node'
|
4
|
+
|
3
5
|
module Capybara::Selenium::Driver::InternetExplorerDriver
|
4
6
|
def switch_to_frame(frame)
|
5
7
|
return super unless frame == :parent
|
@@ -8,6 +10,17 @@ module Capybara::Selenium::Driver::InternetExplorerDriver
|
|
8
10
|
# so we have to move to the default_content and iterate back through the frames
|
9
11
|
handles = @frame_handles[current_window_handle]
|
10
12
|
browser.switch_to.default_content
|
11
|
-
handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh) }
|
13
|
+
handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh.native) }
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def build_node(native_node, initial_cache = {})
|
19
|
+
::Capybara::Selenium::IENode.new(self, native_node, initial_cache)
|
12
20
|
end
|
13
21
|
end
|
22
|
+
|
23
|
+
module Capybara::Selenium
|
24
|
+
Driver.register_specialization :ie, Driver::InternetExplorerDriver
|
25
|
+
Driver.register_specialization :internet_explorer, Driver::InternetExplorerDriver
|
26
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'capybara/selenium/nodes/safari_node'
|
4
|
+
|
5
|
+
module Capybara::Selenium::Driver::SafariDriver
|
6
|
+
def switch_to_frame(frame)
|
7
|
+
return super unless frame == :parent
|
8
|
+
|
9
|
+
# safaridriver/safari has an issue where switch_to_frame(:parent)
|
10
|
+
# behaves like switch_to_frame(:top)
|
11
|
+
handles = @frame_handles[current_window_handle]
|
12
|
+
browser.switch_to.default_content
|
13
|
+
handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh.native) }
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def build_node(native_node, initial_cache = {})
|
19
|
+
::Capybara::Selenium::SafariNode.new(self, native_node, initial_cache)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
Capybara::Selenium::Driver.register_specialization(/^(safari|Safari_Technology_Preview)$/,
|
24
|
+
Capybara::Selenium::Driver::SafariDriver)
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Capybara::Selenium::Node
|
4
|
+
module FileInputClickEmulation
|
5
|
+
def click(keys = [], **options)
|
6
|
+
super
|
7
|
+
rescue Selenium::WebDriver::Error::InvalidArgumentError
|
8
|
+
return emulate_click if attaching_file? && visible_file_field?
|
9
|
+
|
10
|
+
raise
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def visible_file_field?
|
16
|
+
(attrs(:tagName, :type).map { |val| val&.downcase } == %w[input file]) && visible?
|
17
|
+
end
|
18
|
+
|
19
|
+
def attaching_file?
|
20
|
+
caller_locations.any? { |cl| cl.base_label == 'attach_file' }
|
21
|
+
end
|
22
|
+
|
23
|
+
def emulate_click
|
24
|
+
driver.execute_script(<<~JS, self)
|
25
|
+
arguments[0].dispatchEvent(
|
26
|
+
new MouseEvent('click', {
|
27
|
+
view: window,
|
28
|
+
bubbles: true,
|
29
|
+
cancelable: true
|
30
|
+
}));
|
31
|
+
JS
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -3,66 +3,47 @@
|
|
3
3
|
module Capybara
|
4
4
|
module Selenium
|
5
5
|
module Find
|
6
|
-
def find_xpath(selector, uses_visibility: false, styles: nil, **_options)
|
7
|
-
find_by(:xpath, selector, uses_visibility: uses_visibility, texts: [], styles: styles)
|
6
|
+
def find_xpath(selector, uses_visibility: false, styles: nil, position: false, **_options)
|
7
|
+
find_by(:xpath, selector, uses_visibility: uses_visibility, texts: [], styles: styles, position: position)
|
8
8
|
end
|
9
9
|
|
10
|
-
def find_css(selector, uses_visibility: false, texts: [], styles: nil, **_options)
|
11
|
-
find_by(:css, selector, uses_visibility: uses_visibility, texts: texts, styles: styles)
|
10
|
+
def find_css(selector, uses_visibility: false, texts: [], styles: nil, position: false, **_options)
|
11
|
+
find_by(:css, selector, uses_visibility: uses_visibility, texts: texts, styles: styles, position: position)
|
12
12
|
end
|
13
13
|
|
14
14
|
private
|
15
15
|
|
16
|
-
def find_by(format, selector, uses_visibility:, texts:, styles:)
|
16
|
+
def find_by(format, selector, uses_visibility:, texts:, styles:, position:)
|
17
17
|
els = find_context.find_elements(format, selector)
|
18
18
|
hints = []
|
19
19
|
|
20
20
|
if (els.size > 2) && !ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS']
|
21
21
|
els = filter_by_text(els, texts) unless texts.empty?
|
22
|
-
|
23
|
-
hints_js = +''
|
24
|
-
functions = []
|
25
|
-
if uses_visibility && !is_displayed_atom.empty?
|
26
|
-
hints_js << <<~VISIBILITY_JS
|
27
|
-
var vis_func = #{is_displayed_atom};
|
28
|
-
VISIBILITY_JS
|
29
|
-
functions << 'vis_func'
|
30
|
-
end
|
31
|
-
|
32
|
-
if styles.is_a? Hash
|
33
|
-
hints_js << <<~STYLE_JS
|
34
|
-
var style_func = function(el){
|
35
|
-
var el_styles = window.getComputedStyle(el);
|
36
|
-
return #{styles.keys.map(&:to_s)}.reduce(function(res, style){
|
37
|
-
res[style] = el_styles[style];
|
38
|
-
return res;
|
39
|
-
}, {});
|
40
|
-
};
|
41
|
-
STYLE_JS
|
42
|
-
functions << 'style_func'
|
43
|
-
end
|
44
|
-
|
45
|
-
unless functions.empty?
|
46
|
-
hints_js << <<~EACH_JS
|
47
|
-
return arguments[0].map(function(el){
|
48
|
-
return [#{functions.join(',')}].map(function(fn){ return fn.call(null, el) }); });
|
49
|
-
EACH_JS
|
50
|
-
|
51
|
-
hints = es_context.execute_script hints_js, els
|
52
|
-
hints.map! do |results|
|
53
|
-
result = {}
|
54
|
-
result[:style] = results.pop if styles.is_a? Hash
|
55
|
-
result[:visible] = results.pop if uses_visibility
|
56
|
-
result
|
57
|
-
end
|
58
|
-
end
|
22
|
+
hints = gather_hints(els, uses_visibility: uses_visibility, styles: styles, position: position)
|
59
23
|
end
|
60
24
|
els.map.with_index { |el, idx| build_node(el, hints[idx] || {}) }
|
61
25
|
end
|
62
26
|
|
27
|
+
def gather_hints(elements, uses_visibility:, styles:, position:)
|
28
|
+
hints_js, functions = build_hints_js(uses_visibility, styles, position)
|
29
|
+
return [] unless functions.any?
|
30
|
+
|
31
|
+
(es_context.execute_script(hints_js, elements) || []).map! do |results|
|
32
|
+
hint = {}
|
33
|
+
hint[:style] = results.pop if functions.include?(:style_func)
|
34
|
+
hint[:position] = results.pop if functions.include?(:position_func)
|
35
|
+
hint[:visible] = results.pop if functions.include?(:vis_func)
|
36
|
+
hint
|
37
|
+
end
|
38
|
+
rescue ::Selenium::WebDriver::Error::StaleElementReferenceError,
|
39
|
+
::Capybara::NotSupportedByDriverError
|
40
|
+
# warn 'Unexpected Stale Element Error - skipping optimization'
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
|
63
44
|
def filter_by_text(elements, texts)
|
64
45
|
es_context.execute_script <<~JS, elements, texts
|
65
|
-
var texts = arguments[1]
|
46
|
+
var texts = arguments[1];
|
66
47
|
return arguments[0].filter(function(el){
|
67
48
|
var content = el.textContent.toLowerCase();
|
68
49
|
return texts.every(function(txt){ return content.indexOf(txt.toLowerCase()) != -1 });
|
@@ -70,16 +51,58 @@ module Capybara
|
|
70
51
|
JS
|
71
52
|
end
|
72
53
|
|
54
|
+
def build_hints_js(uses_visibility, styles, position)
|
55
|
+
functions = []
|
56
|
+
hints_js = +''
|
57
|
+
|
58
|
+
if uses_visibility && !is_displayed_atom.empty?
|
59
|
+
hints_js << <<~VISIBILITY_JS
|
60
|
+
var vis_func = #{is_displayed_atom};
|
61
|
+
VISIBILITY_JS
|
62
|
+
functions << :vis_func
|
63
|
+
end
|
64
|
+
|
65
|
+
if position
|
66
|
+
hints_js << <<~POSITION_JS
|
67
|
+
var position_func = function(el){
|
68
|
+
return el.getBoundingClientRect();
|
69
|
+
};
|
70
|
+
POSITION_JS
|
71
|
+
functions << :position_func
|
72
|
+
end
|
73
|
+
|
74
|
+
if styles.is_a? Hash
|
75
|
+
hints_js << <<~STYLE_JS
|
76
|
+
var style_func = function(el){
|
77
|
+
var el_styles = window.getComputedStyle(el);
|
78
|
+
return #{styles.keys.map(&:to_s)}.reduce(function(res, style){
|
79
|
+
res[style] = el_styles[style];
|
80
|
+
return res;
|
81
|
+
}, {});
|
82
|
+
};
|
83
|
+
STYLE_JS
|
84
|
+
functions << :style_func
|
85
|
+
end
|
86
|
+
|
87
|
+
hints_js << <<~EACH_JS
|
88
|
+
return arguments[0].map(function(el){
|
89
|
+
return [#{functions.join(',')}].map(function(fn){ return fn.call(null, el) });
|
90
|
+
});
|
91
|
+
EACH_JS
|
92
|
+
|
93
|
+
[hints_js, functions]
|
94
|
+
end
|
95
|
+
|
73
96
|
def es_context
|
74
97
|
respond_to?(:execute_script) ? self : driver
|
75
98
|
end
|
76
99
|
|
77
100
|
def is_displayed_atom # rubocop:disable Naming/PredicateName
|
78
|
-
@@is_displayed_atom ||= begin
|
101
|
+
@@is_displayed_atom ||= begin # rubocop:disable Style/ClassVars
|
79
102
|
browser.send(:bridge).send(:read_atom, 'isDisplayed')
|
80
103
|
rescue StandardError
|
81
104
|
# If the atom doesn't exist or other error
|
82
|
-
|
105
|
+
''
|
83
106
|
end
|
84
107
|
end
|
85
108
|
end
|
@@ -2,37 +2,215 @@
|
|
2
2
|
|
3
3
|
class Capybara::Selenium::Node
|
4
4
|
module Html5Drag
|
5
|
-
|
5
|
+
# Implement methods to emulate HTML5 drag and drop
|
6
|
+
|
7
|
+
def drag_to(element, html5: nil, delay: 0.05, drop_modifiers: [])
|
8
|
+
drop_modifiers = Array(drop_modifiers)
|
6
9
|
|
7
|
-
def html5_drag_to(element)
|
8
10
|
driver.execute_script MOUSEDOWN_TRACKER
|
9
11
|
scroll_if_needed { browser_action.click_and_hold(native).perform }
|
10
|
-
|
11
|
-
|
12
|
+
html5 = !driver.evaluate_script(LEGACY_DRAG_CHECK, self) if html5.nil?
|
13
|
+
if html5
|
14
|
+
perform_html5_drag(element, delay, drop_modifiers)
|
12
15
|
else
|
13
|
-
|
14
|
-
browser_action.release.perform
|
16
|
+
perform_legacy_drag(element, drop_modifiers)
|
15
17
|
end
|
16
18
|
end
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
|
20
|
+
private
|
21
|
+
|
22
|
+
def perform_legacy_drag(element, drop_modifiers)
|
23
|
+
element.scroll_if_needed do
|
24
|
+
# browser_action.move_to(element.native).release.perform
|
25
|
+
keys_down = modifiers_down(browser_action, drop_modifiers)
|
26
|
+
keys_up = modifiers_up(keys_down.move_to(element.native).release, drop_modifiers)
|
27
|
+
keys_up.perform
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def perform_html5_drag(element, delay, drop_modifiers)
|
32
|
+
driver.evaluate_async_script HTML5_DRAG_DROP_SCRIPT, self, element, delay * 1000, normalize_keys(drop_modifiers)
|
33
|
+
browser_action.release.perform
|
34
|
+
end
|
35
|
+
|
36
|
+
def html5_drop(*args)
|
37
|
+
if args[0].is_a? String
|
38
|
+
input = driver.evaluate_script ATTACH_FILE
|
39
|
+
input.set_file(args)
|
40
|
+
driver.execute_script DROP_FILE, self, input
|
41
|
+
else
|
42
|
+
items = args.flat_map do |arg|
|
43
|
+
arg.map { |(type, data)| { type: type, data: data } }
|
44
|
+
end
|
45
|
+
driver.execute_script DROP_STRING, items, self
|
46
|
+
end
|
21
47
|
end
|
22
48
|
|
49
|
+
DROP_STRING = <<~JS
|
50
|
+
var strings = arguments[0],
|
51
|
+
el = arguments[1],
|
52
|
+
dt = new DataTransfer(),
|
53
|
+
opts = { cancelable: true, bubbles: true, dataTransfer: dt };
|
54
|
+
for (var i=0; i < strings.length; i++){
|
55
|
+
if (dt.items) {
|
56
|
+
dt.items.add(strings[i]['data'], strings[i]['type']);
|
57
|
+
} else {
|
58
|
+
dt.setData(strings[i]['type'], strings[i]['data']);
|
59
|
+
}
|
60
|
+
}
|
61
|
+
var dropEvent = new DragEvent('drop', opts);
|
62
|
+
el.dispatchEvent(dropEvent);
|
63
|
+
JS
|
64
|
+
|
65
|
+
DROP_FILE = <<~JS
|
66
|
+
var el = arguments[0],
|
67
|
+
input = arguments[1],
|
68
|
+
files = input.files,
|
69
|
+
dt = new DataTransfer(),
|
70
|
+
opts = { cancelable: true, bubbles: true, dataTransfer: dt };
|
71
|
+
input.parentElement.removeChild(input);
|
72
|
+
if (dt.items){
|
73
|
+
for (var i=0; i<files.length; i++){
|
74
|
+
dt.items.add(files[i]);
|
75
|
+
}
|
76
|
+
} else {
|
77
|
+
Object.defineProperty(dt, "files", {
|
78
|
+
value: files,
|
79
|
+
writable: false
|
80
|
+
});
|
81
|
+
}
|
82
|
+
var dropEvent = new DragEvent('drop', opts);
|
83
|
+
el.dispatchEvent(dropEvent);
|
84
|
+
JS
|
85
|
+
|
86
|
+
ATTACH_FILE = <<~JS
|
87
|
+
(function(){
|
88
|
+
var input = document.createElement('INPUT');
|
89
|
+
input.type = "file";
|
90
|
+
input.id = "_capybara_drop_file";
|
91
|
+
input.multiple = true;
|
92
|
+
document.body.appendChild(input);
|
93
|
+
return input;
|
94
|
+
})()
|
95
|
+
JS
|
96
|
+
|
23
97
|
MOUSEDOWN_TRACKER = <<~JS
|
98
|
+
window.capybara_mousedown_prevented = null;
|
24
99
|
document.addEventListener('mousedown', ev => {
|
25
100
|
window.capybara_mousedown_prevented = ev.defaultPrevented;
|
26
101
|
}, { once: true, passive: true })
|
27
102
|
JS
|
28
103
|
|
104
|
+
LEGACY_DRAG_CHECK = <<~JS
|
105
|
+
(function(el){
|
106
|
+
if ([true, null].indexOf(window.capybara_mousedown_prevented) >= 0){
|
107
|
+
return true;
|
108
|
+
}
|
109
|
+
|
110
|
+
do {
|
111
|
+
if (el.draggable) return false;
|
112
|
+
} while (el = el.parentElement );
|
113
|
+
return true;
|
114
|
+
})(arguments[0])
|
115
|
+
JS
|
116
|
+
|
29
117
|
HTML5_DRAG_DROP_SCRIPT = <<~JS
|
30
|
-
|
31
|
-
|
118
|
+
function rectCenter(rect){
|
119
|
+
return new DOMPoint(
|
120
|
+
(rect.left + rect.right)/2,
|
121
|
+
(rect.top + rect.bottom)/2
|
122
|
+
);
|
123
|
+
}
|
124
|
+
|
125
|
+
function pointOnRect(pt, rect) {
|
126
|
+
var rectPt = rectCenter(rect);
|
127
|
+
var slope = (rectPt.y - pt.y) / (rectPt.x - pt.x);
|
128
|
+
|
129
|
+
if (pt.x <= rectPt.x) { // left side
|
130
|
+
var minXy = slope * (rect.left - pt.x) + pt.y;
|
131
|
+
if (rect.top <= minXy && minXy <= rect.bottom)
|
132
|
+
return new DOMPoint(rect.left, minXy);
|
133
|
+
}
|
134
|
+
|
135
|
+
if (pt.x >= rectPt.x) { // right side
|
136
|
+
var maxXy = slope * (rect.right - pt.x) + pt.y;
|
137
|
+
if (rect.top <= maxXy && maxXy <= rect.bottom)
|
138
|
+
return new DOMPoint(rect.right, maxXy);
|
139
|
+
}
|
140
|
+
|
141
|
+
if (pt.y <= rectPt.y) { // top side
|
142
|
+
var minYx = (rectPt.top - pt.y) / slope + pt.x;
|
143
|
+
if (rect.left <= minYx && minYx <= rect.right)
|
144
|
+
return new DOMPoint(minYx, rect.top);
|
145
|
+
}
|
146
|
+
|
147
|
+
if (pt.y >= rectPt.y) { // bottom side
|
148
|
+
var maxYx = (rect.bottom - pt.y) / slope + pt.x;
|
149
|
+
if (rect.left <= maxYx && maxYx <= rect.right)
|
150
|
+
return new DOMPoint(maxYx, rect.bottom);
|
151
|
+
}
|
152
|
+
|
153
|
+
return new DOMPoint(pt.x,pt.y);
|
154
|
+
}
|
155
|
+
|
156
|
+
function dragEnterTarget() {
|
157
|
+
target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
|
158
|
+
var targetRect = target.getBoundingClientRect();
|
159
|
+
var sourceCenter = rectCenter(source.getBoundingClientRect());
|
160
|
+
|
161
|
+
for (var i = 0; i < drop_modifier_keys.length; i++) {
|
162
|
+
key = drop_modifier_keys[i];
|
163
|
+
if (key == "control"){
|
164
|
+
key = "ctrl"
|
165
|
+
}
|
166
|
+
opts[key + 'Key'] = true;
|
167
|
+
}
|
168
|
+
|
169
|
+
var dragEnterEvent = new DragEvent('dragenter', opts);
|
170
|
+
target.dispatchEvent(dragEnterEvent);
|
171
|
+
|
172
|
+
// fire 2 dragover events to simulate dragging with a direction
|
173
|
+
var entryPoint = pointOnRect(sourceCenter, targetRect)
|
174
|
+
var dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
|
175
|
+
var dragOverEvent = new DragEvent('dragover', dragOverOpts);
|
176
|
+
target.dispatchEvent(dragOverEvent);
|
177
|
+
window.setTimeout(dragOnTarget, step_delay);
|
178
|
+
}
|
179
|
+
|
180
|
+
function dragOnTarget() {
|
181
|
+
var targetCenter = rectCenter(target.getBoundingClientRect());
|
182
|
+
var dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
|
183
|
+
var dragOverEvent = new DragEvent('dragover', dragOverOpts);
|
184
|
+
target.dispatchEvent(dragOverEvent);
|
185
|
+
window.setTimeout(dragLeave, step_delay, dragOverEvent.defaultPrevented, dragOverOpts);
|
186
|
+
}
|
187
|
+
|
188
|
+
function dragLeave(drop, dragOverOpts) {
|
189
|
+
var dragLeaveOptions = Object.assign({}, opts, dragOverOpts);
|
190
|
+
var dragLeaveEvent = new DragEvent('dragleave', dragLeaveOptions);
|
191
|
+
target.dispatchEvent(dragLeaveEvent);
|
192
|
+
if (drop) {
|
193
|
+
var dropEvent = new DragEvent('drop', dragLeaveOptions);
|
194
|
+
target.dispatchEvent(dropEvent);
|
195
|
+
}
|
196
|
+
var dragEndEvent = new DragEvent('dragend', dragLeaveOptions);
|
197
|
+
source.dispatchEvent(dragEndEvent);
|
198
|
+
callback.call(true);
|
199
|
+
}
|
200
|
+
|
201
|
+
var source = arguments[0],
|
202
|
+
target = arguments[1],
|
203
|
+
step_delay = arguments[2],
|
204
|
+
drop_modifier_keys = arguments[3],
|
205
|
+
callback = arguments[4];
|
32
206
|
|
33
207
|
var dt = new DataTransfer();
|
34
208
|
var opts = { cancelable: true, bubbles: true, dataTransfer: dt };
|
35
209
|
|
210
|
+
while (source && !source.draggable) {
|
211
|
+
source = source.parentElement;
|
212
|
+
}
|
213
|
+
|
36
214
|
if (source.tagName == 'A'){
|
37
215
|
dt.setData('text/uri-list', source.href);
|
38
216
|
dt.setData('text', source.href);
|
@@ -41,19 +219,11 @@ class Capybara::Selenium::Node
|
|
41
219
|
dt.setData('text/uri-list', source.src);
|
42
220
|
dt.setData('text', source.src);
|
43
221
|
}
|
222
|
+
|
44
223
|
var dragEvent = new DragEvent('dragstart', opts);
|
45
224
|
source.dispatchEvent(dragEvent);
|
46
|
-
|
47
|
-
|
48
|
-
target.dispatchEvent(dragOverEvent);
|
49
|
-
var dragLeaveEvent = new DragEvent('dragleave', opts);
|
50
|
-
target.dispatchEvent(dragLeaveEvent);
|
51
|
-
if (dragOverEvent.defaultPrevented) {
|
52
|
-
var dropEvent = new DragEvent('drop', opts);
|
53
|
-
target.dispatchEvent(dropEvent);
|
54
|
-
}
|
55
|
-
var dragEndEvent = new DragEvent('dragend', opts);
|
56
|
-
source.dispatchEvent(dragEndEvent);
|
225
|
+
|
226
|
+
window.setTimeout(dragEnterTarget, step_delay);
|
57
227
|
JS
|
58
228
|
end
|
59
229
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Capybara::Selenium::Node
|
4
|
+
#
|
5
|
+
# @api private
|
6
|
+
#
|
7
|
+
class ModifierKeysStack
|
8
|
+
def initialize
|
9
|
+
@stack = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def include?(key)
|
13
|
+
@stack.flatten.include?(key)
|
14
|
+
end
|
15
|
+
|
16
|
+
def press(key)
|
17
|
+
@stack.last.push(key)
|
18
|
+
end
|
19
|
+
|
20
|
+
def push
|
21
|
+
@stack.push []
|
22
|
+
end
|
23
|
+
|
24
|
+
def pop
|
25
|
+
@stack.pop
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -45,20 +45,18 @@ module Capybara
|
|
45
45
|
JS
|
46
46
|
end
|
47
47
|
|
48
|
+
SCROLL_POSITIONS = {
|
49
|
+
top: '0',
|
50
|
+
bottom: 'arguments[0].scrollHeight',
|
51
|
+
center: '(arguments[0].scrollHeight - arguments[0].clientHeight)/2'
|
52
|
+
}.freeze
|
53
|
+
|
48
54
|
def scroll_to_location(location)
|
49
|
-
scroll_y = case location
|
50
|
-
when :top
|
51
|
-
'0'
|
52
|
-
when :bottom
|
53
|
-
'arguments[0].scrollHeight'
|
54
|
-
when :center
|
55
|
-
'(arguments[0].scrollHeight - arguments[0].clientHeight)/2'
|
56
|
-
end
|
57
55
|
driver.execute_script <<~JS, self
|
58
56
|
if (arguments[0].scrollTo){
|
59
|
-
arguments[0].scrollTo(0, #{
|
57
|
+
arguments[0].scrollTo(0, #{SCROLL_POSITIONS[location]});
|
60
58
|
} else {
|
61
|
-
arguments[0].scrollTop = #{
|
59
|
+
arguments[0].scrollTop = #{SCROLL_POSITIONS[location]};
|
62
60
|
}
|
63
61
|
JS
|
64
62
|
end
|